From 5cf4ead7ebd95c2fb2e5e7558d18a12ecc0cc4eb Mon Sep 17 00:00:00 2001 From: Jamie Howard Date: Thu, 11 May 2023 12:49:11 +0100 Subject: [PATCH 01/17] chore(SettingsPage/Webhooks): move formatValue to the component using it --- .../EditView/components/EventInput/index.js | 13 ++++++++++++- .../components/EventInput/tests/formatValue.test.js | 2 +- .../components/EventInput/utils/formatValue.js | 13 ------------- 3 files changed, 13 insertions(+), 15 deletions(-) delete mode 100644 packages/core/admin/admin/src/pages/SettingsPage/pages/Webhooks/EditView/components/EventInput/utils/formatValue.js diff --git a/packages/core/admin/admin/src/pages/SettingsPage/pages/Webhooks/EditView/components/EventInput/index.js b/packages/core/admin/admin/src/pages/SettingsPage/pages/Webhooks/EditView/components/EventInput/index.js index b60c4b1db9..86cc72cc59 100644 --- a/packages/core/admin/admin/src/pages/SettingsPage/pages/Webhooks/EditView/components/EventInput/index.js +++ b/packages/core/admin/admin/src/pages/SettingsPage/pages/Webhooks/EditView/components/EventInput/index.js @@ -5,7 +5,18 @@ import { useFormikContext } from 'formik'; import { useIntl } from 'react-intl'; import styled from 'styled-components'; 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` td { diff --git a/packages/core/admin/admin/src/pages/SettingsPage/pages/Webhooks/EditView/components/EventInput/tests/formatValue.test.js b/packages/core/admin/admin/src/pages/SettingsPage/pages/Webhooks/EditView/components/EventInput/tests/formatValue.test.js index d622adb149..45f2e7f798 100644 --- a/packages/core/admin/admin/src/pages/SettingsPage/pages/Webhooks/EditView/components/EventInput/tests/formatValue.test.js +++ b/packages/core/admin/admin/src/pages/SettingsPage/pages/Webhooks/EditView/components/EventInput/tests/formatValue.test.js @@ -1,4 +1,4 @@ -import formatValue from '../utils/formatValue'; +import { formatValue } from '..'; describe('utils | formatValue', () => { it('should format array to object', () => { diff --git a/packages/core/admin/admin/src/pages/SettingsPage/pages/Webhooks/EditView/components/EventInput/utils/formatValue.js b/packages/core/admin/admin/src/pages/SettingsPage/pages/Webhooks/EditView/components/EventInput/utils/formatValue.js deleted file mode 100644 index 85f7641ab5..0000000000 --- a/packages/core/admin/admin/src/pages/SettingsPage/pages/Webhooks/EditView/components/EventInput/utils/formatValue.js +++ /dev/null @@ -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; From e8189e616a77eb4343c87d32e5c4a0361e70c4ce Mon Sep 17 00:00:00 2001 From: Jamie Howard Date: Fri, 12 May 2023 12:06:27 +0100 Subject: [PATCH 02/17] feat(webhooks): remove reducer from the list view feat(webhooks): refactor data fetching --- .../pages/Webhooks/ListView/index.js | 244 ++- .../pages/Webhooks/ListView/reducer.js | 72 - .../Webhooks/ListView/tests/index.test.js | 1313 +---------------- .../Webhooks/ListView/tests/reducer.test.js | 370 ----- 4 files changed, 138 insertions(+), 1861 deletions(-) delete mode 100644 packages/core/admin/admin/src/pages/SettingsPage/pages/Webhooks/ListView/reducer.js delete mode 100644 packages/core/admin/admin/src/pages/SettingsPage/pages/Webhooks/ListView/tests/reducer.test.js diff --git a/packages/core/admin/admin/src/pages/SettingsPage/pages/Webhooks/ListView/index.js b/packages/core/admin/admin/src/pages/SettingsPage/pages/Webhooks/ListView/index.js index f4eedd593e..58827f3db2 100644 --- a/packages/core/admin/admin/src/pages/SettingsPage/pages/Webhooks/ListView/index.js +++ b/packages/core/admin/admin/src/pages/SettingsPage/pages/Webhooks/ListView/index.js @@ -1,13 +1,9 @@ /* eslint-disable no-nested-ternary */ -/** - * - * ListView - * - */ - -import React, { useEffect, useReducer, useRef, useState } from 'react'; +import React, { useState } from 'react'; import { useHistory, useLocation } from 'react-router-dom'; import { useIntl } from 'react-intl'; +import { useQuery, useMutation } from 'react-query'; + import { useFetchClient, useRBAC, @@ -45,25 +41,22 @@ import { VisuallyHidden, } from '@strapi/design-system'; import { Plus, Pencil, Trash, EmptyDocuments } from '@strapi/icons'; -import reducer, { initialState } from './reducer'; import adminPermissions from '../../../../../permissions'; const ListView = () => { - const { - isLoading, - allowedActions: { canCreate, canRead, canUpdate, canDelete }, - } = useRBAC(adminPermissions.settings.webhooks); - const toggleNotification = useNotification(); - const isMounted = useRef(true); - const { formatMessage } = useIntl(); + const [webhooks, setWebhooks] = useState([]); + const [webhookToDelete, setWebhookToDelete] = useState(null); + const [webhooksToDelete, setWebhooksToDelete] = useState([]); const [showModal, setShowModal] = useState(false); - const [{ webhooks, webhooksToDelete, webhookToDelete, loadingWebhooks }, dispatch] = useReducer( - reducer, - initialState - ); - const { notifyStatus } = useNotifyAT(); + const { formatMessage } = useIntl(); + const toggleNotification = useNotification(); + const { + isLoading: RBACLoading, + allowedActions: { canCreate, canUpdate, canDelete }, + } = useRBAC(adminPermissions.settings.webhooks); const { get, del, post, put } = useFetchClient(); + const { notifyStatus } = useNotifyAT(); useFocusWhenNavigate(); const { push } = useHistory(); @@ -72,53 +65,70 @@ const ListView = () => { const webhooksToDeleteLength = webhooksToDelete.length; const getWebhookIndex = (id) => webhooks.findIndex((webhook) => webhook.id === id); - useEffect(() => { - isMounted.current = true; + const fetchWebHooks = async () => { + try { + const { + data: { data }, + } = await get('/admin/webhooks'); - return () => { - isMounted.current = false; - }; - }, []); + setWebhooks(data); + notifyStatus('webhooks have been loaded'); - /** - * TODO: refactor this, but actually refactor - * the whole component. Needs some love. - */ - useEffect(() => { - const fetchWebHooks = async () => { - try { - const { - data: { data }, - } = await get('/admin/webhooks'); + return data; + } catch (err) { + toggleNotification({ + type: 'warning', + message: { id: 'notification.error' }, + }); - 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(); + return null; } - }, [canRead, get, notifyStatus, toggleNotification]); + }; + const QUERY_KEY = 'webhooks'; + const { isLoading: webhooksLoading } = useQuery(QUERY_KEY, fetchWebHooks); + const loading = RBACLoading || webhooksLoading; + + const { mutateAsync: handleConfirmDeleteOne } = useMutation(async () => { + try { + await del(`/admin/webhooks/${webhookToDelete}`); + + const webhookIndex = getWebhookIndex(webhookToDelete); + + if (webhookIndex !== -1) { + setWebhooks((prev) => { + prev.splice(webhookIndex, 1); + + return [...prev]; + }); + } + } catch (err) { + toggleNotification({ + type: 'warning', + message: { id: 'notification.error' }, + }); + } + setShowModal(false); + }); + + const { mutateAsync: handleConfirmDeleteAll } = useMutation(async () => { + const body = { + ids: webhooksToDelete, + }; + + try { + await post('/admin/webhooks/batch-delete', body); + + setWebhooks((prevWebhooks) => + prevWebhooks.filter((webhook) => !webhooksToDelete.includes(webhook.id)) + ); + } catch (err) { + toggleNotification({ + type: 'warning', + message: { id: 'notification.error' }, + }); + } + setShowModal(false); + }); const handleToggleModal = () => { setShowModal((prev) => !prev); @@ -132,69 +142,17 @@ const ListView = () => { } }; - const handleConfirmDeleteOne = async () => { - try { - await del(`/admin/webhooks/${webhookToDelete}`); - - dispatch({ - type: 'WEBHOOK_DELETED', - index: getWebhookIndex(webhookToDelete), - }); - } catch (err) { - /** - * TODO: especially this. - */ - if (err.code !== 20) { - toggleNotification({ - type: 'warning', - message: { id: 'notification.error' }, - }); - } - } - setShowModal(false); - }; - - const handleConfirmDeleteAll = async () => { - const body = { - ids: webhooksToDelete, - }; - - try { - 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, - }); + setWebhookToDelete(id); } }; const handleEnabledChange = async (value, id) => { const webhookIndex = getWebhookIndex(id); const initialWebhookProps = webhooks[webhookIndex]; - const keys = [webhookIndex, 'isEnabled']; const body = { ...initialWebhookProps, @@ -204,44 +162,28 @@ const ListView = () => { delete body.id; try { - dispatch({ - type: 'SET_WEBHOOK_ENABLED', - keys, - value, - }); - await put(`/admin/webhooks/${id}`, body); - } catch (err) { - if (isMounted.current) { - dispatch({ - type: 'SET_WEBHOOK_ENABLED', - keys, - value: !value, - }); - if (err.code !== 20) { - toggleNotification({ - type: 'warning', - message: { id: 'notification.error' }, - }); - } - } + setWebhooks((prevWebhooks) => + prevWebhooks.map((webhook) => + webhook.id === id ? { ...webhook, isEnabled: value } : webhook + ) + ); + } catch (err) { + toggleNotification({ + type: 'warning', + message: { id: 'notification.error' }, + }); } }; - const handleSelectAllCheckbox = () => { - dispatch({ - type: 'SET_ALL_WEBHOOKS_TO_DELETE', - }); - }; + const handleSelectAllCheckbox = (value) => + value ? setWebhooksToDelete(webhooks.map((webhook) => webhook.id)) : setWebhooksToDelete([]); - const handleSelectOneCheckbox = (value, id) => { - dispatch({ - type: 'SET_WEBHOOKS_TO_DELETE', - value, - id, - }); - }; + const handleSelectOneCheckbox = (value, id) => + value + ? setWebhooksToDelete((prev) => [...prev, id]) + : setWebhooksToDelete((prev) => prev.filter((webhookId) => webhookId !== id)); const handleGoTo = (to) => { push(`${pathname}/${to}`); @@ -250,7 +192,7 @@ const ListView = () => { return ( -
+
{ })} primaryAction={ canCreate && - !loadingWebhooks && ( + !loading && ( } variant="default" to={`${pathname}/create`} size="S"> {formatMessage({ id: 'Settings.webhooks.list.button.add', @@ -299,7 +241,7 @@ const ListView = () => { /> )} - {isLoading || loadingWebhooks ? ( + {loading ? ( diff --git a/packages/core/admin/admin/src/pages/SettingsPage/pages/Webhooks/ListView/reducer.js b/packages/core/admin/admin/src/pages/SettingsPage/pages/Webhooks/ListView/reducer.js deleted file mode 100644 index 3406cce875..0000000000 --- a/packages/core/admin/admin/src/pages/SettingsPage/pages/Webhooks/ListView/reducer.js +++ /dev/null @@ -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 }; diff --git a/packages/core/admin/admin/src/pages/SettingsPage/pages/Webhooks/ListView/tests/index.test.js b/packages/core/admin/admin/src/pages/SettingsPage/pages/Webhooks/ListView/tests/index.test.js index 766beab20a..7cc8107e98 100644 --- a/packages/core/admin/admin/src/pages/SettingsPage/pages/Webhooks/ListView/tests/index.test.js +++ b/packages/core/admin/admin/src/pages/SettingsPage/pages/Webhooks/ListView/tests/index.test.js @@ -1,10 +1,17 @@ import React from 'react'; -import { fireEvent, render, screen, waitFor } from '@testing-library/react'; +import { + fireEvent, + render, + screen, + waitFor, + waitForElementToBeRemoved, +} from '@testing-library/react'; import { IntlProvider } from 'react-intl'; import { ThemeProvider, lightTheme } from '@strapi/design-system'; import { Router } from 'react-router-dom'; import { createMemoryHistory } from 'history'; -import { useRBAC } from '@strapi/helper-plugin'; +import { useRBAC, useNotification } from '@strapi/helper-plugin'; +import { QueryClientProvider, QueryClient } from 'react-query'; import ListView from '../index'; import server from './server'; @@ -17,13 +24,23 @@ jest.mock('@strapi/helper-plugin', () => ({ const history = createMemoryHistory(); +const queryClient = new QueryClient({ + defaultOptions: { + queries: { + refetchOnWindowFocus: false, + }, + }, +}); + const App = ( - - - - - + + + + + + + ); @@ -34,1288 +51,48 @@ describe('Admin | containers | ListView', () => { jest.clearAllMocks(); }); - afterEach(() => server.resetHandlers()); + afterEach(() => { + server.resetHandlers(); + }); afterAll(() => server.close()); - it('renders and matches the snapshot', async () => { + it('should show a loader when data is loading and then display the data', async () => { useRBAC.mockImplementation(() => ({ isLoading: false, - allowedActions: { canUpdate: true, canCreate: true, canRead: true, canDelete: true }, + allowedActions: { canUpdate: true, canCreate: true, canDelete: true }, })); - const { - container: { firstChild }, - } = render(App); + useNotification.mockImplementation(() => jest.fn()); - await waitFor(() => { - expect(screen.getByText('http:://strapi.io')).toBeInTheDocument(); - }); + render(App); - expect(firstChild).toMatchInlineSnapshot(` - .c1 { - padding-bottom: 56px; - } + const loadingElement = await screen.findByText('Loading content.'); - .c4 { - background: #f6f6f9; - padding-top: 40px; - padding-right: 56px; - padding-bottom: 40px; - padding-left: 56px; - } + expect(loadingElement).toBeInTheDocument(); - .c6 { - min-width: 0; - } + await waitForElementToBeRemoved(() => screen.getByText('Loading content.')); - .c17 { - padding-right: 56px; - padding-left: 56px; - } - - .c18 { - background: #ffffff; - border-radius: 4px; - box-shadow: 0px 1px 4px rgba(33,33,52,0.1); - } - - .c20 { - position: relative; - } - - .c22 { - padding-right: 24px; - padding-left: 24px; - } - - .c30 { - width: 20%; - } - - .c32 { - width: 60%; - } - - .c40 { - color: #328048; - padding-left: 8px; - } - - .c42 { - background: #ffffff; - padding: 8px; - border-radius: 4px; - border-width: 0; - border-color: #dcdce4; - width: 2rem; - height: 2rem; - cursor: pointer; - } - - .c46 { - color: #d02b20; - padding-left: 8px; - } - - .c47 { - background: #eaeaef; - } - - .c49 { - background: #f0f0ff; - padding: 20px; - } - - .c51 { - background: #d9d8ff; - } - - .c53 { - padding-left: 12px; - } - - .c8 { - font-weight: 600; - font-size: 2rem; - line-height: 1.25; - color: #32324d; - } - - .c16 { - font-size: 1rem; - line-height: 1.5; - color: #666687; - } - - .c31 { - font-weight: 600; - font-size: 0.6875rem; - line-height: 1.45; - text-transform: uppercase; - color: #666687; - } - - .c35 { - font-size: 0.875rem; - line-height: 1.43; - font-weight: 500; - color: #32324d; - } - - .c36 { - font-size: 0.875rem; - line-height: 1.43; - color: #32324d; - } - - .c54 { - font-size: 0.75rem; - line-height: 1.33; - font-weight: 600; - color: #4945ff; - } - - .c5 { - -webkit-align-items: center; - -webkit-box-align: center; - -ms-flex-align: center; - align-items: center; - display: -webkit-box; - display: -webkit-flex; - display: -ms-flexbox; - display: flex; - -webkit-flex-direction: row; - -ms-flex-direction: row; - flex-direction: row; - -webkit-box-pack: justify; - -webkit-justify-content: space-between; - -ms-flex-pack: justify; - justify-content: space-between; - } - - .c7 { - -webkit-align-items: center; - -webkit-box-align: center; - -ms-flex-align: center; - align-items: center; - display: -webkit-box; - display: -webkit-flex; - display: -ms-flexbox; - display: flex; - -webkit-flex-direction: row; - -ms-flex-direction: row; - flex-direction: row; - } - - .c41 { - -webkit-align-items: center; - -webkit-box-align: center; - -ms-flex-align: center; - align-items: center; - display: -webkit-box; - display: -webkit-flex; - display: -ms-flexbox; - display: flex; - -webkit-flex-direction: row; - -ms-flex-direction: row; - flex-direction: row; - gap: 4px; - } - - .c43 { - -webkit-align-items: center; - -webkit-box-align: center; - -ms-flex-align: center; - align-items: center; - display: -webkit-box; - display: -webkit-flex; - display: -ms-flexbox; - display: flex; - -webkit-flex-direction: row; - -ms-flex-direction: row; - flex-direction: row; - -webkit-box-pack: center; - -webkit-justify-content: center; - -ms-flex-pack: center; - justify-content: center; - } - - .c44 { - position: relative; - outline: none; - } - - .c44 > svg { - height: 12px; - width: 12px; - } - - .c44 > svg > g, - .c44 > svg path { - fill: #ffffff; - } - - .c44[aria-disabled='true'] { - pointer-events: none; - } - - .c44:after { - -webkit-transition-property: all; - transition-property: all; - -webkit-transition-duration: 0.2s; - transition-duration: 0.2s; - border-radius: 8px; - content: ''; - position: absolute; - top: -4px; - bottom: -4px; - left: -4px; - right: -4px; - border: 2px solid transparent; - } - - .c44:focus-visible { - outline: none; - } - - .c44:focus-visible:after { - border-radius: 8px; - content: ''; - position: absolute; - top: -5px; - bottom: -5px; - left: -5px; - right: -5px; - border: 2px solid #4945ff; - } - - .c28 { - height: 18px; - min-width: 18px; - margin: 0; - border-radius: 4px; - border: 1px solid #c0c0cf; - -webkit-appearance: none; - background-color: #ffffff; - cursor: pointer; - } - - .c28:checked { - background-color: #4945ff; - border: 1px solid #4945ff; - } - - .c28:checked:after { - content: ''; - display: block; - position: relative; - background: url(data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMTAiIGhlaWdodD0iOCIgdmlld0JveD0iMCAwIDEwIDgiIGZpbGw9Im5vbmUiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+CiAgPHBhdGgKICAgIGQ9Ik04LjU1MzIzIDAuMzk2OTczQzguNjMxMzUgMC4zMTYzNTUgOC43NjA1MSAwLjMxNTgxMSA4LjgzOTMxIDAuMzk1NzY4TDkuODYyNTYgMS40MzQwN0M5LjkzODkzIDEuNTExNTcgOS45MzkzNSAxLjYzNTkgOS44NjM0OSAxLjcxMzlMNC4wNjQwMSA3LjY3NzI0QzMuOTg1OSA3Ljc1NzU1IDMuODU3MDcgNy43NTgwNSAzLjc3ODM0IDcuNjc4MzRMMC4xMzg2NiAzLjk5MzMzQzAuMDYxNzc5OCAzLjkxNTQ5IDAuMDYxNzEwMiAzLjc5MDMyIDAuMTM4NTA0IDMuNzEyNEwxLjE2MjEzIDIuNjczNzJDMS4yNDAzOCAyLjU5NDMyIDEuMzY4NDMgMi41OTQyMiAxLjQ0NjggMi42NzM0OEwzLjkyMTc0IDUuMTc2NDdMOC41NTMyMyAwLjM5Njk3M1oiCiAgICBmaWxsPSJ3aGl0ZSIKICAvPgo8L3N2Zz4=) no-repeat no-repeat center center; - width: 10px; - height: 10px; - left: 50%; - top: 50%; - -webkit-transform: translateX(-50%) translateY(-50%); - -ms-transform: translateX(-50%) translateY(-50%); - transform: translateX(-50%) translateY(-50%); - } - - .c28:checked:disabled:after { - background: url(data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMTAiIGhlaWdodD0iOCIgdmlld0JveD0iMCAwIDEwIDgiIGZpbGw9Im5vbmUiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+CiAgPHBhdGgKICAgIGQ9Ik04LjU1MzIzIDAuMzk2OTczQzguNjMxMzUgMC4zMTYzNTUgOC43NjA1MSAwLjMxNTgxMSA4LjgzOTMxIDAuMzk1NzY4TDkuODYyNTYgMS40MzQwN0M5LjkzODkzIDEuNTExNTcgOS45MzkzNSAxLjYzNTkgOS44NjM0OSAxLjcxMzlMNC4wNjQwMSA3LjY3NzI0QzMuOTg1OSA3Ljc1NzU1IDMuODU3MDcgNy43NTgwNSAzLjc3ODM0IDcuNjc4MzRMMC4xMzg2NiAzLjk5MzMzQzAuMDYxNzc5OCAzLjkxNTQ5IDAuMDYxNzEwMiAzLjc5MDMyIDAuMTM4NTA0IDMuNzEyNEwxLjE2MjEzIDIuNjczNzJDMS4yNDAzOCAyLjU5NDMyIDEuMzY4NDMgMi41OTQyMiAxLjQ0NjggMi42NzM0OEwzLjkyMTc0IDUuMTc2NDdMOC41NTMyMyAwLjM5Njk3M1oiCiAgICBmaWxsPSIjOEU4RUE5IgogIC8+Cjwvc3ZnPg==) no-repeat no-repeat center center; - } - - .c28:disabled { - background-color: #dcdce4; - border: 1px solid #c0c0cf; - } - - .c28:indeterminate { - background-color: #4945ff; - border: 1px solid #4945ff; - } - - .c28:indeterminate:after { - content: ''; - display: block; - position: relative; - color: white; - height: 2px; - width: 10px; - background-color: #ffffff; - left: 50%; - top: 50%; - -webkit-transform: translateX(-50%) translateY(-50%); - -ms-transform: translateX(-50%) translateY(-50%); - transform: translateX(-50%) translateY(-50%); - } - - .c28:indeterminate:disabled { - background-color: #dcdce4; - border: 1px solid #c0c0cf; - } - - .c28:indeterminate:disabled:after { - background-color: #8e8ea9; - } - - .c33 { - border: 0; - -webkit-clip: rect(0 0 0 0); - clip: rect(0 0 0 0); - height: 1px; - margin: -1px; - overflow: hidden; - padding: 0; - position: absolute; - width: 1px; - } - - .c48 { - height: 1px; - border: none; - -webkit-flex-shrink: 0; - -ms-flex-negative: 0; - flex-shrink: 0; - margin: 0; - } - - .c45 svg > g, - .c45 svg path { - fill: #8e8ea9; - } - - .c45:hover svg > g, - .c45:hover svg path { - fill: #666687; - } - - .c45:active svg > g, - .c45:active svg path { - fill: #a5a5ba; - } - - .c45[aria-disabled='true'] svg path { - fill: #666687; - } - - .c0 { - display: grid; - grid-template-columns: 1fr; - } - - .c2 { - overflow-x: hidden; - } - - .c3:focus-visible { - outline: none; - } - - .c39 { - background: #ee5e52; - border: none; - border-radius: 16px; - position: relative; - height: 1.5rem; - width: 2.5rem; - } - - .c39 span { - font-size: 0; - } - - .c39:before { - content: ''; - background: #ffffff; - width: 1rem; - height: 1rem; - border-radius: 50%; - position: absolute; - -webkit-transition: all 0.5s; - transition: all 0.5s; - left: 4px; - top: 4px; - } - - .c37 { - background: transparent; - padding: 0; - border: none; - } - - .c37[aria-checked='true'] .c38 { - background: #5cb176; - } - - .c37[aria-checked='true'] .c38:before { - -webkit-transform: translateX(1rem); - -ms-transform: translateX(1rem); - transform: translateX(1rem); - } - - .c19 { - overflow: hidden; - border: 1px solid #eaeaef; - } - - .c24 { - width: 100%; - white-space: nowrap; - } - - .c21:before { - background: linear-gradient(90deg,#c0c0cf 0%,rgba(0,0,0,0) 100%); - opacity: 0.2; - position: absolute; - height: 100%; - box-shadow: 0px 1px 4px rgba(33,33,52,0.1); - width: 8px; - left: 0; - } - - .c21:after { - background: linear-gradient(270deg,#c0c0cf 0%,rgba(0,0,0,0) 100%); - opacity: 0.2; - position: absolute; - height: 100%; - box-shadow: 0px 1px 4px rgba(33,33,52,0.1); - width: 8px; - right: 0; - top: 0; - } - - .c23 { - overflow-x: auto; - } - - .c34 tr:last-of-type { - border-bottom: none; - } - - .c25 { - border-bottom: 1px solid #eaeaef; - } - - .c26 { - border-bottom: 1px solid #eaeaef; - } - - .c26 td, - .c26 th { - padding: 16px; - } - - .c26 td:first-of-type, - .c26 th:first-of-type { - padding: 0 4px; - } - - .c26 th { - padding-top: 0; - padding-bottom: 0; - height: 3.5rem; - } - - .c27 { - vertical-align: middle; - text-align: left; - color: #666687; - outline-offset: -4px; - } - - .c27 input { - vertical-align: sub; - } - - .c29 svg { - height: 0.25rem; - } - - .c52 { - height: 1.5rem; - width: 1.5rem; - border-radius: 50%; - display: -webkit-box; - display: -webkit-flex; - display: -ms-flexbox; - display: flex; - -webkit-box-pack: center; - -webkit-justify-content: center; - -ms-flex-pack: center; - justify-content: center; - -webkit-align-items: center; - -webkit-box-align: center; - -ms-flex-align: center; - align-items: center; - } - - .c52 svg { - height: 0.625rem; - width: 0.625rem; - } - - .c52 svg path { - fill: #4945ff; - } - - .c50 { - border-radius: 0 0 4px 4px; - display: block; - width: 100%; - border: none; - } - - .c9 { - background: #4945ff; - padding-top: 8px; - padding-right: 16px; - padding-bottom: 8px; - padding-left: 16px; - border-radius: 4px; - border-color: #4945ff; - border: 1px solid #4945ff; - } - - .c15 { - font-size: 0.75rem; - line-height: 1.33; - font-weight: 600; - color: #ffffff; - } - - .c10 { - -webkit-align-items: center; - -webkit-box-align: center; - -ms-flex-align: center; - align-items: center; - display: -webkit-inline-box; - display: -webkit-inline-flex; - display: -ms-inline-flexbox; - display: inline-flex; - -webkit-flex-direction: row; - -ms-flex-direction: row; - flex-direction: row; - gap: 8px; - } - - .c13 { - -webkit-align-items: center; - -webkit-box-align: center; - -ms-flex-align: center; - align-items: center; - display: -webkit-box; - display: -webkit-flex; - display: -ms-flexbox; - display: flex; - -webkit-flex-direction: row; - -ms-flex-direction: row; - flex-direction: row; - } - - .c11 { - position: relative; - outline: none; - } - - .c11 > svg { - height: 12px; - width: 12px; - } - - .c11 > svg > g, - .c11 > svg path { - fill: #ffffff; - } - - .c11[aria-disabled='true'] { - pointer-events: none; - } - - .c11:after { - -webkit-transition-property: all; - transition-property: all; - -webkit-transition-duration: 0.2s; - transition-duration: 0.2s; - border-radius: 8px; - content: ''; - position: absolute; - top: -4px; - bottom: -4px; - left: -4px; - right: -4px; - border: 2px solid transparent; - } - - .c11:focus-visible { - outline: none; - } - - .c11:focus-visible:after { - border-radius: 8px; - content: ''; - position: absolute; - top: -5px; - bottom: -5px; - left: -5px; - right: -5px; - border: 2px solid #4945ff; - } - - .c12 { - -webkit-text-decoration: none; - text-decoration: none; - } - - .c12[aria-disabled='true'] { - border: 1px solid #dcdce4; - background: #eaeaef; - } - - .c12[aria-disabled='true'] .c14 { - color: #666687; - } - - .c12[aria-disabled='true'] svg > g,.c12[aria-disabled='true'] svg path { - fill: #666687; - } - - .c12[aria-disabled='true']:active { - border: 1px solid #dcdce4; - background: #eaeaef; - } - - .c12[aria-disabled='true']:active .c14 { - color: #666687; - } - - .c12[aria-disabled='true']:active svg > g,.c12[aria-disabled='true']:active svg path { - fill: #666687; - } - - .c12:hover { - border: 1px solid #7b79ff; - background: #7b79ff; - } - - .c12:active { - border: 1px solid #4945ff; - background: #4945ff; - } - - .c12 svg > g, - .c12 svg path { - fill: #ffffff; - } - - @media (prefers-reduced-motion:reduce) { - .c39:before { - -webkit-transition: none; - transition: none; - } - } - -
-
-
-
-
- -

- Get POST changes notifications -

-
-
-
-
-
-
- - - - - - - - - - - - - - - - - - - - - - - - - - -
-
-
- -
- -
-
-
- - Name - - -
-
-
- - URL - - -
-
-
- - Status - - -
-
-
-
- Actions -
- -
-
- - test - - - - http:://strapi.io - - - - - -
- - test2 - - - - http://me.io - - - - - -
-
-
-
-
- -
-
-
-
-
-
- `); + expect(screen.getByText('http:://strapi.io')).toBeInTheDocument(); }); - describe('Shows a loading state', () => { - it('should show a loader when it is loading for the permissions', () => { - useRBAC.mockImplementation(() => ({ - isLoading: true, - allowedActions: { canUpdate: true, canCreate: true, canRead: true, canDelete: true }, - })); + it('should show a loader when permissions are loading', () => { + useRBAC.mockImplementation(() => ({ + isLoading: true, + allowedActions: { canUpdate: true, canCreate: true, canDelete: true }, + })); - render(App); + useNotification.mockImplementation(() => jest.fn()); - expect(screen.getByTestId('loader')).toBeInTheDocument(); - }); + const { getByText } = render(App); - it('should show a loader when it is loading for the data and not for the permissions', () => { - useRBAC.mockImplementation(() => ({ - isLoading: false, - allowedActions: { canUpdate: true, canCreate: true, canRead: true, canDelete: true }, - })); - - render(App); - - expect(screen.getByTestId('loader')).toBeInTheDocument(); - }); + expect(getByText('Loading content.')).toBeInTheDocument(); }); it('should show a list of webhooks', async () => { useRBAC.mockImplementation(() => ({ isLoading: false, - allowedActions: { canUpdate: true, canCreate: true, canRead: true, canDelete: true }, + allowedActions: { canUpdate: true, canCreate: true, canDelete: true }, })); render(App); @@ -1328,7 +105,7 @@ describe('Admin | containers | ListView', () => { it('should show confirmation delete modal', async () => { useRBAC.mockImplementation(() => ({ isLoading: false, - allowedActions: { canUpdate: true, canCreate: true, canRead: true, canDelete: true }, + allowedActions: { canUpdate: true, canCreate: true, canDelete: true }, })); const { container, getByText } = render(App); diff --git a/packages/core/admin/admin/src/pages/SettingsPage/pages/Webhooks/ListView/tests/reducer.test.js b/packages/core/admin/admin/src/pages/SettingsPage/pages/Webhooks/ListView/tests/reducer.test.js deleted file mode 100644 index ea5c60a68d..0000000000 --- a/packages/core/admin/admin/src/pages/SettingsPage/pages/Webhooks/ListView/tests/reducer.test.js +++ /dev/null @@ -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); - }); - }); -}); From ace6bb8a16ecd9559d0472c434bb747c88578204 Mon Sep 17 00:00:00 2001 From: Jamie Howard Date: Fri, 12 May 2023 12:09:29 +0100 Subject: [PATCH 03/17] fix(webhooks): hide delete button when no webhooks to delete --- .../src/pages/SettingsPage/pages/Webhooks/ListView/index.js | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/core/admin/admin/src/pages/SettingsPage/pages/Webhooks/ListView/index.js b/packages/core/admin/admin/src/pages/SettingsPage/pages/Webhooks/ListView/index.js index 58827f3db2..dd03dc0400 100644 --- a/packages/core/admin/admin/src/pages/SettingsPage/pages/Webhooks/ListView/index.js +++ b/packages/core/admin/admin/src/pages/SettingsPage/pages/Webhooks/ListView/index.js @@ -121,6 +121,7 @@ const ListView = () => { setWebhooks((prevWebhooks) => prevWebhooks.filter((webhook) => !webhooksToDelete.includes(webhook.id)) ); + setWebhooksToDelete([]); } catch (err) { toggleNotification({ type: 'warning', From 46d50a6b9025391380f94c0fb5a5b02ed26dd5b3 Mon Sep 17 00:00:00 2001 From: Jamie Howard Date: Fri, 12 May 2023 14:08:33 +0100 Subject: [PATCH 04/17] feat(webhooks): useMutation for enabled change --- .../pages/Webhooks/ListView/index.js | 54 ++++++++++--------- 1 file changed, 28 insertions(+), 26 deletions(-) diff --git a/packages/core/admin/admin/src/pages/SettingsPage/pages/Webhooks/ListView/index.js b/packages/core/admin/admin/src/pages/SettingsPage/pages/Webhooks/ListView/index.js index dd03dc0400..51c9a5e3a8 100644 --- a/packages/core/admin/admin/src/pages/SettingsPage/pages/Webhooks/ListView/index.js +++ b/packages/core/admin/admin/src/pages/SettingsPage/pages/Webhooks/ListView/index.js @@ -88,7 +88,7 @@ const ListView = () => { const { isLoading: webhooksLoading } = useQuery(QUERY_KEY, fetchWebHooks); const loading = RBACLoading || webhooksLoading; - const { mutateAsync: handleConfirmDeleteOne } = useMutation(async () => { + const { mutateAsync: deleteOne } = useMutation(async () => { try { await del(`/admin/webhooks/${webhookToDelete}`); @@ -110,7 +110,7 @@ const ListView = () => { setShowModal(false); }); - const { mutateAsync: handleConfirmDeleteAll } = useMutation(async () => { + const { mutateAsync: deleteSelected } = useMutation(async () => { const body = { ids: webhooksToDelete, }; @@ -131,33 +131,13 @@ const ListView = () => { setShowModal(false); }); - const handleToggleModal = () => { - setShowModal((prev) => !prev); - }; - - const handleConfirmDelete = () => { - if (webhookToDelete) { - handleConfirmDeleteOne(); - } else { - handleConfirmDeleteAll(); - } - }; - - const handleDeleteClick = (id) => { - setShowModal(true); - - if (id !== 'all') { - setWebhookToDelete(id); - } - }; - - const handleEnabledChange = async (value, id) => { + const { mutateAsync: enabledChange } = useMutation(async ({ enabled, id }) => { const webhookIndex = getWebhookIndex(id); const initialWebhookProps = webhooks[webhookIndex]; const body = { ...initialWebhookProps, - isEnabled: value, + isEnabled: enabled, }; delete body.id; @@ -167,7 +147,7 @@ const ListView = () => { setWebhooks((prevWebhooks) => prevWebhooks.map((webhook) => - webhook.id === id ? { ...webhook, isEnabled: value } : webhook + webhook.id === id ? { ...webhook, isEnabled: enabled } : webhook ) ); } catch (err) { @@ -176,6 +156,26 @@ const ListView = () => { message: { id: 'notification.error' }, }); } + }); + + const handleToggleModal = () => { + setShowModal((prev) => !prev); + }; + + const handleConfirmDelete = () => { + if (webhookToDelete) { + deleteOne(); + } else { + deleteSelected(); + } + }; + + const handleDeleteClick = (id) => { + setShowModal(true); + + if (id !== 'all') { + setWebhookToDelete(id); + } }; const handleSelectAllCheckbox = (value) => @@ -353,7 +353,9 @@ const ListView = () => { defaultMessage: 'Status', })}`} selected={webhook.isEnabled} - onChange={() => handleEnabledChange(!webhook.isEnabled, webhook.id)} + onChange={() => + enabledChange({ enabled: !webhook.isEnabled, id: webhook.id }) + } visibleLabels /> From 9330ba872c90d1a12b93bab6cc45d0cd518bf2df Mon Sep 17 00:00:00 2001 From: Jamie Howard Date: Fri, 12 May 2023 16:49:08 +0100 Subject: [PATCH 05/17] feat(webhooks): combine deletion mutations feat(webhooks): use api error handler feat(webhooks): add translations --- .../pages/Webhooks/ListView/index.js | 123 +++++++++--------- .../core/admin/admin/src/translations/en.json | 1 + 2 files changed, 64 insertions(+), 60 deletions(-) diff --git a/packages/core/admin/admin/src/pages/SettingsPage/pages/Webhooks/ListView/index.js b/packages/core/admin/admin/src/pages/SettingsPage/pages/Webhooks/ListView/index.js index 51c9a5e3a8..d410e782cf 100644 --- a/packages/core/admin/admin/src/pages/SettingsPage/pages/Webhooks/ListView/index.js +++ b/packages/core/admin/admin/src/pages/SettingsPage/pages/Webhooks/ListView/index.js @@ -15,6 +15,7 @@ import { onRowClick, stopPropagation, LinkButton, + useAPIErrorHandler, } from '@strapi/helper-plugin'; import { HeaderLayout, @@ -50,17 +51,19 @@ const ListView = () => { const [showModal, setShowModal] = useState(false); const { formatMessage } = useIntl(); + const { formatAPIError } = useAPIErrorHandler(); const toggleNotification = useNotification(); + useFocusWhenNavigate(); + const { push } = useHistory(); + const { pathname } = useLocation(); + const { - isLoading: RBACLoading, + isLoading: isRBACLoading, allowedActions: { canCreate, canUpdate, canDelete }, } = useRBAC(adminPermissions.settings.webhooks); const { get, del, post, put } = useFetchClient(); const { notifyStatus } = useNotifyAT(); - useFocusWhenNavigate(); - const { push } = useHistory(); - const { pathname } = useLocation(); const rowsCount = webhooks.length; const webhooksToDeleteLength = webhooksToDelete.length; const getWebhookIndex = (id) => webhooks.findIndex((webhook) => webhook.id === id); @@ -72,13 +75,18 @@ const ListView = () => { } = await get('/admin/webhooks'); setWebhooks(data); - notifyStatus('webhooks have been loaded'); + notifyStatus( + formatMessage({ + id: 'Settings.webhooks.list.loading.success', + defaultMessage: 'Webhooks have been loaded', + }) + ); return data; - } catch (err) { + } catch (error) { toggleNotification({ type: 'warning', - message: { id: 'notification.error' }, + message: formatAPIError(error), }); return null; @@ -86,52 +94,51 @@ const ListView = () => { }; const QUERY_KEY = 'webhooks'; const { isLoading: webhooksLoading } = useQuery(QUERY_KEY, fetchWebHooks); - const loading = RBACLoading || webhooksLoading; + const isLoading = isRBACLoading || webhooksLoading; - const { mutateAsync: deleteOne } = useMutation(async () => { - try { - await del(`/admin/webhooks/${webhookToDelete}`); + const deleteMutation = useMutation(async () => { + if (!webhookToDelete) { + try { + await post('/admin/webhooks/batch-delete', { + ids: webhooksToDelete, + }); - const webhookIndex = getWebhookIndex(webhookToDelete); - - if (webhookIndex !== -1) { - setWebhooks((prev) => { - prev.splice(webhookIndex, 1); - - return [...prev]; + setWebhooks((prevWebhooks) => + prevWebhooks.filter((webhook) => !webhooksToDelete.includes(webhook.id)) + ); + setWebhooksToDelete([]); + } catch (error) { + toggleNotification({ + type: 'warning', + message: formatAPIError(error), + }); + } + } else { + try { + await del(`/admin/webhooks/${webhookToDelete}`); + + const webhookIndex = getWebhookIndex(webhookToDelete); + + if (webhookIndex !== -1) { + setWebhooks((prev) => { + prev.splice(webhookIndex, 1); + + return [...prev]; + }); + setWebhookToDelete(null); + } + } catch (error) { + toggleNotification({ + type: 'warning', + message: formatAPIError(error), }); } - } catch (err) { - toggleNotification({ - type: 'warning', - message: { id: 'notification.error' }, - }); } + setShowModal(false); }); - const { mutateAsync: deleteSelected } = useMutation(async () => { - const body = { - ids: webhooksToDelete, - }; - - try { - await post('/admin/webhooks/batch-delete', body); - - setWebhooks((prevWebhooks) => - prevWebhooks.filter((webhook) => !webhooksToDelete.includes(webhook.id)) - ); - setWebhooksToDelete([]); - } catch (err) { - toggleNotification({ - type: 'warning', - message: { id: 'notification.error' }, - }); - } - setShowModal(false); - }); - - const { mutateAsync: enabledChange } = useMutation(async ({ enabled, id }) => { + const enabledMutation = useMutation(async ({ enabled, id }) => { const webhookIndex = getWebhookIndex(id); const initialWebhookProps = webhooks[webhookIndex]; @@ -139,7 +146,6 @@ const ListView = () => { ...initialWebhookProps, isEnabled: enabled, }; - delete body.id; try { @@ -150,10 +156,10 @@ const ListView = () => { webhook.id === id ? { ...webhook, isEnabled: enabled } : webhook ) ); - } catch (err) { + } catch (error) { toggleNotification({ type: 'warning', - message: { id: 'notification.error' }, + message: formatAPIError(error), }); } }); @@ -162,13 +168,7 @@ const ListView = () => { setShowModal((prev) => !prev); }; - const handleConfirmDelete = () => { - if (webhookToDelete) { - deleteOne(); - } else { - deleteSelected(); - } - }; + const handleConfirmDelete = async () => deleteMutation.mutateAsync({ webhookToDelete }); const handleDeleteClick = (id) => { setShowModal(true); @@ -193,7 +193,7 @@ const ListView = () => { return ( -
+
{ })} primaryAction={ canCreate && - !loading && ( + !isLoading && ( } variant="default" to={`${pathname}/create`} size="S"> {formatMessage({ id: 'Settings.webhooks.list.button.add', @@ -242,7 +242,7 @@ const ListView = () => { /> )} - {loading ? ( + {isLoading ? ( @@ -353,8 +353,11 @@ const ListView = () => { defaultMessage: 'Status', })}`} selected={webhook.isEnabled} - onChange={() => - enabledChange({ enabled: !webhook.isEnabled, id: webhook.id }) + onChange={async () => + enabledMutation.mutateAsync({ + enabled: !webhook.isEnabled, + id: webhook.id, + }) } visibleLabels /> diff --git a/packages/core/admin/admin/src/translations/en.json b/packages/core/admin/admin/src/translations/en.json index 67c6e24c60..1bd1da08cc 100644 --- a/packages/core/admin/admin/src/translations/en.json +++ b/packages/core/admin/admin/src/translations/en.json @@ -299,6 +299,7 @@ "Settings.webhooks.list.empty.title": "There are no webhooks yet", "Settings.webhooks.list.th.actions": "actions", "Settings.webhooks.list.th.status": "status", + "Settings.webhooks.list.loading.success": "Webhooks have been loaded", "Settings.webhooks.singular": "webhook", "Settings.webhooks.title": "Webhooks", "Settings.webhooks.to.delete": "{webhooksToDeleteLength, plural, one {# asset} other {# assets}} selected", From 182af61050eb543aee5dd3353356dc4f9a33f313 Mon Sep 17 00:00:00 2001 From: Jamie Howard Date: Mon, 15 May 2023 11:11:50 +0100 Subject: [PATCH 06/17] chore: code cleanup --- .../SettingsPage/pages/Webhooks/ListView/index.js | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/packages/core/admin/admin/src/pages/SettingsPage/pages/Webhooks/ListView/index.js b/packages/core/admin/admin/src/pages/SettingsPage/pages/Webhooks/ListView/index.js index d410e782cf..44692c0f14 100644 --- a/packages/core/admin/admin/src/pages/SettingsPage/pages/Webhooks/ListView/index.js +++ b/packages/core/admin/admin/src/pages/SettingsPage/pages/Webhooks/ListView/index.js @@ -138,13 +138,13 @@ const ListView = () => { setShowModal(false); }); - const enabledMutation = useMutation(async ({ enabled, id }) => { + const enabledMutation = useMutation(async ({ isEnabled, id }) => { const webhookIndex = getWebhookIndex(id); const initialWebhookProps = webhooks[webhookIndex]; const body = { ...initialWebhookProps, - isEnabled: enabled, + isEnabled, }; delete body.id; @@ -152,9 +152,7 @@ const ListView = () => { await put(`/admin/webhooks/${id}`, body); setWebhooks((prevWebhooks) => - prevWebhooks.map((webhook) => - webhook.id === id ? { ...webhook, isEnabled: enabled } : webhook - ) + prevWebhooks.map((webhook) => (webhook.id === id ? { ...webhook, isEnabled } : webhook)) ); } catch (error) { toggleNotification({ @@ -168,7 +166,7 @@ const ListView = () => { setShowModal((prev) => !prev); }; - const handleConfirmDelete = async () => deleteMutation.mutateAsync({ webhookToDelete }); + const handleConfirmDelete = async () => deleteMutation.mutateAsync(); const handleDeleteClick = (id) => { setShowModal(true); @@ -355,7 +353,7 @@ const ListView = () => { selected={webhook.isEnabled} onChange={async () => enabledMutation.mutateAsync({ - enabled: !webhook.isEnabled, + isEnabled: !webhook.isEnabled, id: webhook.id, }) } From 76ab105dc42bb6d060abecd1ad0156e4621bf8b7 Mon Sep 17 00:00:00 2001 From: Jamie Howard Date: Mon, 15 May 2023 11:42:33 +0100 Subject: [PATCH 07/17] test(webhooks): webhook deletion --- .../pages/Webhooks/ListView/index.js | 2 + .../Webhooks/ListView/tests/index.test.js | 55 +++++++++++++++++++ .../pages/Webhooks/ListView/tests/server.js | 28 ++++++++-- 3 files changed, 81 insertions(+), 4 deletions(-) diff --git a/packages/core/admin/admin/src/pages/SettingsPage/pages/Webhooks/ListView/index.js b/packages/core/admin/admin/src/pages/SettingsPage/pages/Webhooks/ListView/index.js index 44692c0f14..d53305aeb2 100644 --- a/packages/core/admin/admin/src/pages/SettingsPage/pages/Webhooks/ListView/index.js +++ b/packages/core/admin/admin/src/pages/SettingsPage/pages/Webhooks/ListView/index.js @@ -225,6 +225,7 @@ const ListView = () => { )}