Merge pull request #16700 from strapi/webhooks/list-view

This commit is contained in:
Jamie Howard 2023-05-19 16:16:38 +01:00 committed by GitHub
commit ee413187df
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
12 changed files with 311 additions and 1980 deletions

View File

@ -1 +1 @@
export const MARKETPLACE_API_URL = 'https://market-api.strapi.io'; export const MARKETPLACE_API_URL = 'https://market-api.strapi.io';

View File

@ -5,7 +5,18 @@ import { useFormikContext } from 'formik';
import { useIntl } from 'react-intl'; import { useIntl } from 'react-intl';
import styled from 'styled-components'; import styled from 'styled-components';
import EventRow from './EventRow'; import EventRow from './EventRow';
import formatValue from './utils/formatValue';
export const formatValue = (value) =>
value.reduce((acc, curr) => {
const key = curr.split('.')[0];
if (!acc[key]) {
acc[key] = [];
}
acc[key].push(curr);
return acc;
}, {});
const StyledTable = styled.table` const StyledTable = styled.table`
td { td {

View File

@ -1,4 +1,4 @@
import formatValue from '../utils/formatValue'; import { formatValue } from '..';
describe('utils | formatValue', () => { describe('utils | formatValue', () => {
it('should format array to object', () => { it('should format array to object', () => {

View File

@ -1,13 +0,0 @@
const formatValue = (value) =>
value.reduce((acc, curr) => {
const key = curr.split('.')[0];
if (!acc[key]) {
acc[key] = [];
}
acc[key].push(curr);
return acc;
}, {});
export default formatValue;

View File

@ -1,13 +1,9 @@
/* eslint-disable no-nested-ternary */ /* eslint-disable no-nested-ternary */
/** import React, { useState, useEffect } from 'react';
*
* ListView
*
*/
import React, { useEffect, useReducer, useRef, useState } from 'react';
import { useHistory, useLocation } from 'react-router-dom'; import { useHistory, useLocation } from 'react-router-dom';
import { useIntl } from 'react-intl'; import { useIntl } from 'react-intl';
import { useQuery, useMutation } from 'react-query';
import { import {
useFetchClient, useFetchClient,
useRBAC, useRBAC,
@ -19,6 +15,7 @@ import {
onRowClick, onRowClick,
stopPropagation, stopPropagation,
LinkButton, LinkButton,
useAPIErrorHandler,
} from '@strapi/helper-plugin'; } from '@strapi/helper-plugin';
import { import {
HeaderLayout, HeaderLayout,
@ -45,212 +42,124 @@ import {
VisuallyHidden, VisuallyHidden,
} from '@strapi/design-system'; } from '@strapi/design-system';
import { Plus, Pencil, Trash, EmptyDocuments } from '@strapi/icons'; import { Plus, Pencil, Trash, EmptyDocuments } from '@strapi/icons';
import reducer, { initialState } from './reducer';
import adminPermissions from '../../../../../permissions'; import adminPermissions from '../../../../../permissions';
const ListView = () => { const ListView = () => {
const {
isLoading,
allowedActions: { canCreate, canRead, canUpdate, canDelete },
} = useRBAC(adminPermissions.settings.webhooks);
const toggleNotification = useNotification();
const isMounted = useRef(true);
const { formatMessage } = useIntl();
const [showModal, setShowModal] = useState(false); const [showModal, setShowModal] = useState(false);
const [{ webhooks, webhooksToDelete, webhookToDelete, loadingWebhooks }, dispatch] = useReducer( const [webhooksToDelete, setWebhooksToDelete] = useState([]);
reducer,
initialState
);
const { notifyStatus } = useNotifyAT();
const { get, del, post, put } = useFetchClient();
const { formatMessage } = useIntl();
const { formatAPIError } = useAPIErrorHandler();
const toggleNotification = useNotification();
useFocusWhenNavigate(); useFocusWhenNavigate();
const { push } = useHistory(); const { push } = useHistory();
const { pathname } = useLocation(); const { pathname } = useLocation();
const rowsCount = webhooks.length;
const webhooksToDeleteLength = webhooksToDelete.length; const {
const getWebhookIndex = (id) => webhooks.findIndex((webhook) => webhook.id === id); isLoading: isRBACLoading,
allowedActions: { canCreate, canUpdate, canDelete },
} = useRBAC(adminPermissions.settings.webhooks);
const { get, post, put } = useFetchClient();
const { notifyStatus } = useNotifyAT();
const QUERY_KEY = 'webhooks';
const {
isLoading: isWebhooksLoading,
data: webhooks,
error: webhooksError,
refetch: refetchWebhooks,
} = useQuery(QUERY_KEY, async () => {
const {
data: { data },
} = await get('/admin/webhooks');
return data;
});
useEffect(() => { useEffect(() => {
isMounted.current = true; if (webhooksError) {
toggleNotification({
return () => { type: 'warning',
isMounted.current = false; message: formatAPIError(webhooksError),
};
}, []);
/**
* TODO: refactor this, but actually refactor
* the whole component. Needs some love.
*/
useEffect(() => {
const fetchWebHooks = async () => {
try {
const {
data: { data },
} = await get('/admin/webhooks');
if (isMounted.current) {
dispatch({
type: 'GET_DATA_SUCCEEDED',
data,
});
notifyStatus('webhooks have been loaded');
}
} catch (err) {
console.log(err);
if (isMounted.current) {
if (err.code !== 20) {
toggleNotification({
type: 'warning',
message: { id: 'notification.error' },
});
}
dispatch({
type: 'TOGGLE_LOADING',
});
}
}
};
if (canRead) {
fetchWebHooks();
}
}, [canRead, get, notifyStatus, toggleNotification]);
const handleToggleModal = () => {
setShowModal((prev) => !prev);
};
const handleConfirmDelete = () => {
if (webhookToDelete) {
handleConfirmDeleteOne();
} else {
handleConfirmDeleteAll();
}
};
const handleConfirmDeleteOne = async () => {
try {
await del(`/admin/webhooks/${webhookToDelete}`);
dispatch({
type: 'WEBHOOK_DELETED',
index: getWebhookIndex(webhookToDelete),
}); });
} catch (err) {
/** return;
* TODO: especially this. }
*/ if (webhooks) {
if (err.code !== 20) { notifyStatus(
formatMessage({
id: 'Settings.webhooks.list.loading.success',
defaultMessage: 'Webhooks have been loaded',
})
);
}
}, [webhooks, webhooksError, toggleNotification, formatMessage, notifyStatus, formatAPIError]);
const deleteMutation = useMutation(
async () => {
await post('/admin/webhooks/batch-delete', {
ids: webhooksToDelete,
});
},
{
onError(error) {
toggleNotification({ toggleNotification({
type: 'warning', type: 'warning',
message: { id: 'notification.error' }, message: formatAPIError(error),
}); });
} setShowModal(false);
},
onSuccess() {
setWebhooksToDelete([]);
setShowModal(false);
refetchWebhooks();
},
} }
setShowModal(false); );
};
const handleConfirmDeleteAll = async () => { const enabledMutation = useMutation(
const body = { async ({ isEnabled, id }) => {
ids: webhooksToDelete, const { id: _, ...webhook } = webhooks.find((webhook) => webhook.id === id) ?? {};
}; const body = {
...webhook,
try { isEnabled,
await post('/admin/webhooks/batch-delete', body); };
if (isMounted.current) {
dispatch({
type: 'WEBHOOKS_DELETED',
});
}
} catch (err) {
if (isMounted.current) {
if (err.code !== 20) {
toggleNotification({
type: 'warning',
message: { id: 'notification.error' },
});
}
}
}
setShowModal(false);
};
const handleDeleteClick = (id) => {
setShowModal(true);
if (id !== 'all') {
dispatch({
type: 'SET_WEBHOOK_TO_DELETE',
id,
});
}
};
const handleEnabledChange = async (value, id) => {
const webhookIndex = getWebhookIndex(id);
const initialWebhookProps = webhooks[webhookIndex];
const keys = [webhookIndex, 'isEnabled'];
const body = {
...initialWebhookProps,
isEnabled: value,
};
delete body.id;
try {
dispatch({
type: 'SET_WEBHOOK_ENABLED',
keys,
value,
});
await put(`/admin/webhooks/${id}`, body); await put(`/admin/webhooks/${id}`, body);
} catch (err) { },
if (isMounted.current) { {
dispatch({ onError(error) {
type: 'SET_WEBHOOK_ENABLED', toggleNotification({
keys, type: 'warning',
value: !value, message: formatAPIError(error),
}); });
},
if (err.code !== 20) { onSuccess() {
toggleNotification({ refetchWebhooks();
type: 'warning', },
message: { id: 'notification.error' },
});
}
}
} }
}; );
const handleSelectAllCheckbox = () => { const confirmDelete = () => deleteMutation.mutate();
dispatch({
type: 'SET_ALL_WEBHOOKS_TO_DELETE',
});
};
const handleSelectOneCheckbox = (value, id) => { const selectAllCheckbox = (selected) =>
dispatch({ selected ? setWebhooksToDelete(webhooks.map((webhook) => webhook.id)) : setWebhooksToDelete([]);
type: 'SET_WEBHOOKS_TO_DELETE',
value,
id,
});
};
const handleGoTo = (to) => { const selectOneCheckbox = (selected, id) =>
push(`${pathname}/${to}`); selected
}; ? setWebhooksToDelete((prev) => [...prev, id])
: setWebhooksToDelete((prev) => prev.filter((webhookId) => webhookId !== id));
const goTo = (to) => push(`${pathname}/${to}`);
const isLoading = isRBACLoading || isWebhooksLoading;
const numberOfWebhooks = webhooks?.length ?? 0;
const webhooksToDeleteLength = webhooksToDelete.length;
return ( return (
<Layout> <Layout>
<SettingsPageTitle name="Webhooks" /> <SettingsPageTitle name="Webhooks" />
<Main aria-busy={isLoading || loadingWebhooks}> <Main aria-busy={isLoading}>
<HeaderLayout <HeaderLayout
title={formatMessage({ id: 'Settings.webhooks.title', defaultMessage: 'Webhooks' })} title={formatMessage({ id: 'Settings.webhooks.title', defaultMessage: 'Webhooks' })}
subtitle={formatMessage({ subtitle={formatMessage({
@ -259,7 +168,7 @@ const ListView = () => {
})} })}
primaryAction={ primaryAction={
canCreate && canCreate &&
!loadingWebhooks && ( !isLoading && (
<LinkButton startIcon={<Plus />} variant="default" to={`${pathname}/create`} size="S"> <LinkButton startIcon={<Plus />} variant="default" to={`${pathname}/create`} size="S">
{formatMessage({ {formatMessage({
id: 'Settings.webhooks.list.button.add', id: 'Settings.webhooks.list.button.add',
@ -278,13 +187,13 @@ const ListView = () => {
{ {
id: 'Settings.webhooks.to.delete', id: 'Settings.webhooks.to.delete',
defaultMessage: defaultMessage:
'{webhooksToDeleteLength, plural, one {# asset} other {# assets}} selected', '{webhooksToDeleteLength, plural, one {# webhook} other {# webhooks}} selected',
}, },
{ webhooksToDeleteLength } { webhooksToDeleteLength }
)} )}
</Typography> </Typography>
<Button <Button
onClick={() => handleDeleteClick('all')} onClick={() => setShowModal(true)}
startIcon={<Trash />} startIcon={<Trash />}
size="L" size="L"
variant="danger-light" variant="danger-light"
@ -299,16 +208,16 @@ const ListView = () => {
/> />
)} )}
<ContentLayout> <ContentLayout>
{isLoading || loadingWebhooks ? ( {isLoading ? (
<Box background="neutral0" padding={6} shadow="filterShadow" hasRadius> <Box background="neutral0" padding={6} shadow="filterShadow" hasRadius>
<LoadingIndicatorPage /> <LoadingIndicatorPage />
</Box> </Box>
) : rowsCount > 0 ? ( ) : numberOfWebhooks > 0 ? (
<Table <Table
colCount={5} colCount={5}
rowCount={rowsCount + 1} rowCount={numberOfWebhooks + 1}
footer={ footer={
<TFooter onClick={() => (canCreate ? handleGoTo('create') : {})} icon={<Plus />}> <TFooter onClick={() => (canCreate ? goTo('create') : {})} icon={<Plus />}>
{formatMessage({ {formatMessage({
id: 'Settings.webhooks.list.button.add', id: 'Settings.webhooks.list.button.add',
defaultMessage: 'Create new webhook', defaultMessage: 'Create new webhook',
@ -325,10 +234,10 @@ const ListView = () => {
defaultMessage: 'Select all entries', defaultMessage: 'Select all entries',
})} })}
indeterminate={ indeterminate={
webhooksToDeleteLength > 0 && webhooksToDeleteLength < rowsCount webhooksToDeleteLength > 0 && webhooksToDeleteLength < numberOfWebhooks
} }
value={webhooksToDeleteLength === rowsCount} value={webhooksToDeleteLength === numberOfWebhooks}
onValueChange={handleSelectAllCheckbox} onValueChange={selectAllCheckbox}
/> />
</Th> </Th>
<Th width="20%"> <Th width="20%">
@ -370,7 +279,7 @@ const ListView = () => {
<Tr <Tr
key={webhook.id} key={webhook.id}
{...onRowClick({ {...onRowClick({
fn: () => handleGoTo(webhook.id), fn: () => goTo(webhook.id),
condition: canUpdate, condition: canUpdate,
})} })}
> >
@ -381,8 +290,7 @@ const ListView = () => {
defaultMessage: 'Select', defaultMessage: 'Select',
})} ${webhook.name}`} })} ${webhook.name}`}
value={webhooksToDelete?.includes(webhook.id)} value={webhooksToDelete?.includes(webhook.id)}
onValueChange={(value) => handleSelectOneCheckbox(value, webhook.id)} onValueChange={(selected) => selectOneCheckbox(selected, webhook.id)}
id="select"
name="select" name="select"
/> />
</Td> </Td>
@ -395,7 +303,7 @@ const ListView = () => {
<Typography textColor="neutral800">{webhook.url}</Typography> <Typography textColor="neutral800">{webhook.url}</Typography>
</Td> </Td>
<Td> <Td>
<Flex {...stopPropagation}> <Flex>
<Switch <Switch
onLabel={formatMessage({ onLabel={formatMessage({
id: 'global.enabled', id: 'global.enabled',
@ -410,18 +318,21 @@ const ListView = () => {
defaultMessage: 'Status', defaultMessage: 'Status',
})}`} })}`}
selected={webhook.isEnabled} selected={webhook.isEnabled}
onChange={() => handleEnabledChange(!webhook.isEnabled, webhook.id)} onChange={(e) => {
e.stopPropagation();
enabledMutation.mutate({
isEnabled: !webhook.isEnabled,
id: webhook.id,
});
}}
visibleLabels visibleLabels
/> />
</Flex> </Flex>
</Td> </Td>
<Td> <Td>
<Flex gap={1} {...stopPropagation}> <Flex gap={1}>
{canUpdate && ( {canUpdate && (
<IconButton <IconButton
onClick={() => {
handleGoTo(webhook.id);
}}
label={formatMessage({ label={formatMessage({
id: 'Settings.webhooks.events.update', id: 'Settings.webhooks.events.update',
defaultMessage: 'Update', defaultMessage: 'Update',
@ -432,14 +343,17 @@ const ListView = () => {
)} )}
{canDelete && ( {canDelete && (
<IconButton <IconButton
onClick={() => handleDeleteClick(webhook.id)} onClick={(e) => {
e.stopPropagation();
setWebhooksToDelete([webhook.id]);
setShowModal(true);
}}
label={formatMessage({ label={formatMessage({
id: 'global.delete', id: 'Settings.webhooks.events.delete',
defaultMessage: 'Delete', defaultMessage: 'Delete webhook',
})} })}
icon={<Trash />} icon={<Trash />}
noBorder noBorder
id={`delete-${webhook.id}`}
/> />
)} )}
</Flex> </Flex>
@ -459,7 +373,7 @@ const ListView = () => {
<Button <Button
variant="secondary" variant="secondary"
startIcon={<Plus />} startIcon={<Plus />}
onClick={() => (canCreate ? handleGoTo('create') : {})} onClick={() => (canCreate ? goTo('create') : {})}
> >
{formatMessage({ {formatMessage({
id: 'Settings.webhooks.list.button.add', id: 'Settings.webhooks.list.button.add',
@ -473,8 +387,9 @@ const ListView = () => {
</Main> </Main>
<ConfirmDialog <ConfirmDialog
isOpen={showModal} isOpen={showModal}
onToggleDialog={handleToggleModal} onToggleDialog={() => setShowModal((prev) => !prev)}
onConfirm={handleConfirmDelete} onConfirm={confirmDelete}
isConfirmButtonLoading={deleteMutation.isLoading}
/> />
</Layout> </Layout>
); );

View File

@ -1,72 +0,0 @@
import produce from 'immer';
import set from 'lodash/set';
const initialState = {
webhooks: [],
webhooksToDelete: [],
webhookToDelete: null,
loadingWebhooks: true,
};
const reducer = (state, action) =>
// eslint-disable-next-line consistent-return
produce(state, (draftState) => {
switch (action.type) {
case 'GET_DATA_SUCCEEDED': {
draftState.webhooks = action.data;
draftState.loadingWebhooks = false;
break;
}
case 'TOGGLE_LOADING': {
draftState.loadingWebhooks = !state.loadingWebhooks;
break;
}
case 'SET_WEBHOOK_ENABLED': {
set(draftState, ['webhooks', ...action.keys], action.value);
break;
}
case 'SET_WEBHOOK_TO_DELETE': {
draftState.webhookToDelete = action.id;
break;
}
case 'SET_WEBHOOKS_TO_DELETE': {
if (action.value) {
draftState.webhooksToDelete.push(action.id);
} else {
draftState.webhooksToDelete = state.webhooksToDelete.filter((id) => id !== action.id);
}
break;
}
case 'SET_ALL_WEBHOOKS_TO_DELETE': {
if (state.webhooksToDelete.length === 0) {
draftState.webhooksToDelete = state.webhooks.map((webhook) => webhook.id);
} else {
draftState.webhooksToDelete = [];
}
break;
}
case 'WEBHOOKS_DELETED': {
draftState.webhooks = state.webhooks.filter(
(webhook) => !state.webhooksToDelete.includes(webhook.id)
);
draftState.webhooksToDelete = [];
break;
}
case 'WEBHOOK_DELETED': {
draftState.webhooks = state.webhooks.filter((_, index) => index !== action.index);
draftState.webhookToDelete = null;
break;
}
default:
return draftState;
}
});
export default reducer;
export { initialState };

View File

@ -1,370 +0,0 @@
import reducer from '../reducer';
describe('Admin | containers | Webhooks | ListView | reducer', () => {
const initialState = {
webhooks: [],
webhooksToDelete: [],
webhookToDelete: null,
loadingWebhooks: true,
};
describe('Load webhooks', () => {
it('should update webhooks with received data', () => {
const state = initialState;
const receivedData = [
{
id: 1,
name: 'webhook 1',
url: 'http://localhost:5000',
headers: {},
events: ['entry.create', 'entry.update', 'entry.delete'],
isEnabled: true,
},
{
id: 2,
name: 'webhook 2',
url: 'http://localhost:4000',
headers: {},
events: ['media.create', 'media.update'],
isEnabled: false,
},
];
const action = {
type: 'GET_DATA_SUCCEEDED',
data: receivedData,
};
const expectedState = { ...state, webhooks: receivedData, loadingWebhooks: false };
expect(reducer(state, action)).toEqual(expectedState);
});
});
describe('Update webhook', () => {
it('should toggle isEnabled parameter', () => {
const webhooks = [
{
id: 1,
name: 'webhook 1',
url: 'http://localhost:5000',
headers: {},
events: ['entry.create', 'entry.update', 'entry.delete'],
isEnabled: true,
},
{
id: 2,
name: 'webhook 2',
url: 'http://localhost:4000',
headers: {},
events: ['media.create', 'media.update'],
isEnabled: false,
},
];
const state = { ...initialState, webhooks };
const action = {
type: 'SET_WEBHOOK_ENABLED',
keys: [1, 'isEnabled'],
value: true,
};
const expectedState = {
...state,
webhooks: [
{
id: 1,
name: 'webhook 1',
url: 'http://localhost:5000',
headers: {},
events: ['entry.create', 'entry.update', 'entry.delete'],
isEnabled: true,
},
{
id: 2,
name: 'webhook 2',
url: 'http://localhost:4000',
headers: {},
events: ['media.create', 'media.update'],
isEnabled: true,
},
],
};
expect(reducer(state, action)).toEqual(expectedState);
});
});
describe('Delete webhooks', () => {
it('should set a webhook id to webhookToDelete', () => {
const webhooks = [
{
id: 1,
name: 'webhook 1',
url: 'http://localhost:5000',
headers: {},
events: ['entry.create', 'entry.update', 'entry.delete'],
isEnabled: true,
},
{
id: 2,
name: 'webhook 2',
url: 'http://localhost:4000',
headers: {},
events: ['media.create', 'media.update'],
isEnabled: false,
},
];
const state = { ...initialState, webhooks };
const action = {
type: 'SET_WEBHOOK_TO_DELETE',
id: 1,
};
const expectedState = { ...state, webhookToDelete: 1 };
expect(reducer(state, action)).toEqual(expectedState);
});
it('should add a webhook id to webhooksToDelete if value is true', () => {
const webhooks = [
{
id: 1,
name: 'webhook 1',
url: 'http://localhost:5000',
headers: {},
events: ['entry.create', 'entry.update', 'entry.delete'],
isEnabled: true,
},
{
id: 2,
name: 'webhook 2',
url: 'http://localhost:4000',
headers: {},
events: ['media.create', 'media.update'],
isEnabled: false,
},
];
const state = { ...initialState, webhooks };
const action = {
type: 'SET_WEBHOOKS_TO_DELETE',
id: 1,
value: true,
};
const expectedState = { ...state, webhooksToDelete: [1] };
expect(reducer(state, action)).toEqual(expectedState);
});
it('should remove a webhook id to webhooksToDelete if value is false', () => {
const webhooks = [
{
id: 1,
name: 'webhook 1',
url: 'http://localhost:5000',
headers: {},
events: ['entry.create', 'entry.update', 'entry.delete'],
isEnabled: true,
},
{
id: 2,
name: 'webhook 2',
url: 'http://localhost:4000',
headers: {},
events: ['media.create', 'media.update'],
isEnabled: false,
},
];
const state = { ...initialState, webhooks, webhooksToDelete: [1, 2] };
const action = {
type: 'SET_WEBHOOKS_TO_DELETE',
id: 1,
value: false,
};
const expectedState = { ...state, webhooksToDelete: [2] };
expect(reducer(state, action)).toEqual(expectedState);
});
it('should update webhooks and clear webhooksToDelete', () => {
const webhooks = [
{
id: 1,
name: 'webhook 1',
url: 'http://localhost:5000',
headers: {},
events: ['entry.create', 'entry.update', 'entry.delete'],
isEnabled: true,
},
{
id: 2,
name: 'webhook 2',
url: 'http://localhost:4000',
headers: {},
events: ['media.create', 'media.update'],
isEnabled: false,
},
];
const updatedWebhooks = [
{
id: 2,
name: 'webhook 2',
url: 'http://localhost:4000',
headers: {},
events: ['media.create', 'media.update'],
isEnabled: false,
},
];
const state = { ...initialState, webhooksToDelete: [1], webhooks };
const action = {
type: 'WEBHOOKS_DELETED',
};
const expectedState = { ...state, webhooks: updatedWebhooks, webhooksToDelete: [] };
expect(reducer(state, action)).toEqual(expectedState);
});
it('should update webhooks and clear webhookToDelete', () => {
const webhooks = [
{
id: 3,
name: 'webhook 1',
url: 'http://localhost:5000',
headers: {},
events: ['entry.create', 'entry.update', 'entry.delete'],
isEnabled: true,
},
{
id: 4,
name: 'webhook 2',
url: 'http://localhost:4000',
headers: {},
events: ['media.create', 'media.update'],
isEnabled: false,
},
{
id: 5,
name: 'webhook 2',
url: 'http://localhost:4000',
headers: {},
events: ['media.create', 'media.update'],
isEnabled: false,
},
];
const updatedWebhooks = [
{
id: 3,
name: 'webhook 1',
url: 'http://localhost:5000',
headers: {},
events: ['entry.create', 'entry.update', 'entry.delete'],
isEnabled: true,
},
{
id: 5,
name: 'webhook 2',
url: 'http://localhost:4000',
headers: {},
events: ['media.create', 'media.update'],
isEnabled: false,
},
];
const webhookIdToDelete = 4;
const state = { ...initialState, webhookToDelete: webhookIdToDelete, webhooks };
const action = {
type: 'WEBHOOK_DELETED',
index: 1,
};
const expectedState = { ...state, webhooks: updatedWebhooks, webhookToDelete: null };
expect(reducer(state, action)).toEqual(expectedState);
});
it('should clear webhooksToDelete when webhooksToDelete length > 0', () => {
const webhooks = [
{
id: 3,
name: 'webhook 1',
url: 'http://localhost:5000',
headers: {},
events: ['entry.create', 'entry.update', 'entry.delete'],
isEnabled: true,
},
{
id: 4,
name: 'webhook 2',
url: 'http://localhost:4000',
headers: {},
events: ['media.create', 'media.update'],
isEnabled: false,
},
{
id: 5,
name: 'webhook 2',
url: 'http://localhost:4000',
headers: {},
events: ['media.create', 'media.update'],
isEnabled: false,
},
];
const state = { ...initialState, webhooks, webhooksToDelete: [3] };
const action = {
type: 'SET_ALL_WEBHOOKS_TO_DELETE',
};
const expectedState = { ...state, webhooks, webhooksToDelete: [] };
expect(reducer(state, action)).toEqual(expectedState);
});
it('should add all webhooks in webhooksToDelete when webhooksToDelete length === 0', () => {
const webhooks = [
{
id: 3,
name: 'webhook 1',
url: 'http://localhost:5000',
headers: {},
events: ['entry.create', 'entry.update', 'entry.delete'],
isEnabled: true,
},
{
id: 4,
name: 'webhook 2',
url: 'http://localhost:4000',
headers: {},
events: ['media.create', 'media.update'],
isEnabled: false,
},
{
id: 5,
name: 'webhook 2',
url: 'http://localhost:4000',
headers: {},
events: ['media.create', 'media.update'],
isEnabled: false,
},
];
const state = { ...initialState, webhooks, webhooksToDelete: [] };
const action = {
type: 'SET_ALL_WEBHOOKS_TO_DELETE',
};
const expectedState = { ...state, webhooks, webhooksToDelete: [3, 4, 5] };
expect(reducer(state, action)).toEqual(expectedState);
});
});
});

View File

@ -1,16 +1,48 @@
import { setupServer } from 'msw/node'; import { setupServer } from 'msw/node';
import { rest } from 'msw'; import { rest } from 'msw';
const initialWebhooks = [
{ id: 1, isEnabled: true, name: 'test', url: 'http:://strapi.io' },
{ id: 2, isEnabled: false, name: 'test2', url: 'http://me.io' },
];
let webhooks = initialWebhooks;
export const resetWebhooks = () => {
webhooks = initialWebhooks;
};
const handlers = [ const handlers = [
rest.get('*/webhooks', (req, res, ctx) => { rest.get('*/webhooks', (req, res, ctx) => {
return res( return res(
ctx.delay(100), ctx.delay(100),
ctx.status(200), ctx.status(200),
ctx.json({ ctx.json({
data: [ data: webhooks,
{ id: 1, isEnabled: true, name: 'test', url: 'http:://strapi.io' }, })
{ id: 2, isEnabled: false, name: 'test2', url: 'http://me.io' }, );
], }),
rest.post('*/webhooks/batch-delete', (req, res, ctx) => {
webhooks = webhooks.filter((webhook) => !req.body.ids.includes(webhook.id));
return res(
ctx.status(200),
ctx.json({
data: webhooks,
})
);
}),
rest.put('*/webhooks/:id', (req, res, ctx) => {
const { id } = req.params;
const { isEnabled } = req.body;
webhooks = webhooks.map((webhook) =>
webhook.id === Number(id) ? { ...webhook, isEnabled } : webhook
);
return res(
ctx.status(200),
ctx.json({
data: webhooks,
}) })
); );
}), }),

View File

@ -287,6 +287,7 @@
"Settings.webhooks.event.publish-tooltip": "This event only exists for contents with Draft/Publish system enabled", "Settings.webhooks.event.publish-tooltip": "This event only exists for contents with Draft/Publish system enabled",
"Settings.webhooks.events.create": "Create", "Settings.webhooks.events.create": "Create",
"Settings.webhooks.events.update": "Update", "Settings.webhooks.events.update": "Update",
"Settings.webhooks.events.delete": "Delete webhook",
"Settings.webhooks.form.events": "Events", "Settings.webhooks.form.events": "Events",
"Settings.webhooks.form.headers": "Headers", "Settings.webhooks.form.headers": "Headers",
"Settings.webhooks.form.url": "URL", "Settings.webhooks.form.url": "URL",
@ -299,9 +300,10 @@
"Settings.webhooks.list.empty.title": "There are no webhooks yet", "Settings.webhooks.list.empty.title": "There are no webhooks yet",
"Settings.webhooks.list.th.actions": "actions", "Settings.webhooks.list.th.actions": "actions",
"Settings.webhooks.list.th.status": "status", "Settings.webhooks.list.th.status": "status",
"Settings.webhooks.list.loading.success": "Webhooks have been loaded",
"Settings.webhooks.singular": "webhook", "Settings.webhooks.singular": "webhook",
"Settings.webhooks.title": "Webhooks", "Settings.webhooks.title": "Webhooks",
"Settings.webhooks.to.delete": "{webhooksToDeleteLength, plural, one {# asset} other {# assets}} selected", "Settings.webhooks.to.delete": "{webhooksToDeleteLength, plural, one {# webhook} other {# webhooks}} selected",
"Settings.webhooks.trigger": "Trigger", "Settings.webhooks.trigger": "Trigger",
"Settings.webhooks.trigger.cancel": "Cancel trigger", "Settings.webhooks.trigger.cancel": "Cancel trigger",
"Settings.webhooks.trigger.pending": "Pending…", "Settings.webhooks.trigger.pending": "Pending…",

View File

@ -9,7 +9,7 @@ const createContentTypeSchema = ({
reservedModels = [], reservedModels = [],
singularNames = [], singularNames = [],
pluralNames = [], pluralNames = [],
collectionNames = [] collectionNames = [],
}) => { }) => {
const shape = { const shape = {
displayName: yup displayName: yup

View File

@ -157,7 +157,6 @@ const forms = {
}, },
}, },
contentType: { contentType: {
schema(alreadyTakenNames, isEditing, ctUid, reservedNames, extensions, contentTypes) { schema(alreadyTakenNames, isEditing, ctUid, reservedNames, extensions, contentTypes) {
const singularNames = Object.values(contentTypes).map((contentType) => { const singularNames = Object.values(contentTypes).map((contentType) => {
return contentType.schema.singularName; return contentType.schema.singularName;
@ -197,7 +196,7 @@ const forms = {
reservedModels: reservedNames.models, reservedModels: reservedNames.models,
singularNames: takenSingularNames, singularNames: takenSingularNames,
pluralNames: takenPluralNames, pluralNames: takenPluralNames,
collectionNames collectionNames,
}); });
// FIXME // FIXME