diff --git a/packages/plugins/documentation/admin/src/components/FieldActionWrapper/index.js b/packages/plugins/documentation/admin/src/components/FieldActionWrapper/index.js new file mode 100644 index 0000000000..2d9395557d --- /dev/null +++ b/packages/plugins/documentation/admin/src/components/FieldActionWrapper/index.js @@ -0,0 +1,14 @@ +import styled from 'styled-components'; +import { FieldAction } from '@strapi/parts/Field'; + +const FieldActionWrapper = styled(FieldAction)` + svg { + height: 1rem; + width: 1rem; + path { + fill: ${({ theme }) => theme.colors.neutral600}; + } + } +`; + +export default FieldActionWrapper; diff --git a/packages/plugins/documentation/admin/src/index.js b/packages/plugins/documentation/admin/src/index.js index 66c12f66c2..99a39c806a 100644 --- a/packages/plugins/documentation/admin/src/index.js +++ b/packages/plugins/documentation/admin/src/index.js @@ -25,7 +25,9 @@ export default { }, permissions: pluginPermissions.main, Component: async () => { - const component = await import(/* webpackChunkName: "documentation-page" */ './pages/App'); + const component = await import( + /* webpackChunkName: "documentation-page" */ './pages/PluginPage' + ); return component; }, @@ -41,7 +43,24 @@ export default { pluginLogo, }); }, - bootstrap() {}, + bootstrap(app) { + app.addSettingsLink('global', { + intlLabel: { + id: `${pluginId}.plugin.name`, + defaultMessage: 'Documentation', + }, + id: 'documentation', + to: `/settings/${pluginId}`, + Component: async () => { + const component = await import( + /* webpackChunkName: "documentation-settings" */ './pages/SettingsPage' + ); + + return component; + }, + permissions: pluginPermissions.main, + }); + }, async registerTrads({ locales }) { const importedTrads = await Promise.all( locales.map(locale => { diff --git a/packages/plugins/documentation/admin/src/pages/App/index.js b/packages/plugins/documentation/admin/src/pages/App/index.js deleted file mode 100755 index b1ae95eff0..0000000000 --- a/packages/plugins/documentation/admin/src/pages/App/index.js +++ /dev/null @@ -1,52 +0,0 @@ -/** - * - * This component is the skeleton around the actual pages, and should only - * contain code that should be seen on all pages. (e.g. navigation bar) - * - */ - -import React from 'react'; -import { useIntl } from 'react-intl'; -import { CheckPagePermissions, NoContent } from '@strapi/helper-plugin'; -import { Layout, HeaderLayout, ContentLayout } from '@strapi/parts/Layout'; -import { Main } from '@strapi/parts/Main'; -import pluginPermissions from '../../permissions'; -import { getTrad } from '../../utils'; -// import HomePage from '../HomePage'; - -const ComingSoon = () => { - const { formatMessage } = useIntl(); - - return ( - -
- - - - -
-
- ); -}; - -function App() { - return ( - - {/* */} - - - ); -} - -export default App; diff --git a/packages/plugins/documentation/admin/src/pages/HomePage/index.js b/packages/plugins/documentation/admin/src/pages/HomePage/index.js index 1eedc0c039..a7d03cabca 100755 --- a/packages/plugins/documentation/admin/src/pages/HomePage/index.js +++ b/packages/plugins/documentation/admin/src/pages/HomePage/index.js @@ -90,8 +90,6 @@ const HomePage = () => { setVersionToDelete(null); }; - console.log(data); - if (isLoading) { return ; } diff --git a/packages/plugins/documentation/admin/src/pages/PluginPage/index.js b/packages/plugins/documentation/admin/src/pages/PluginPage/index.js new file mode 100755 index 0000000000..908fc50de2 --- /dev/null +++ b/packages/plugins/documentation/admin/src/pages/PluginPage/index.js @@ -0,0 +1,193 @@ +/** + * + * This component is the skeleton around the actual pages, and should only + * contain code that should be seen on all pages. (e.g. navigation bar) + * + */ + +import React, { useState } from 'react'; +import { useIntl } from 'react-intl'; +import { + CheckPermissions, + ConfirmDialog, + LoadingIndicatorPage, + stopPropagation, + EmptyStateLayout, +} from '@strapi/helper-plugin'; +import { Button } from '@strapi/parts/Button'; +import { Layout, HeaderLayout, ContentLayout } from '@strapi/parts/Layout'; +import { Main } from '@strapi/parts/Main'; +import { IconButton } from '@strapi/parts/IconButton'; +import { Text, TableLabel } from '@strapi/parts/Text'; +import { Row } from '@strapi/parts/Row'; +import { Table, Tr, Thead, Th, Tbody, Td } from '@strapi/parts/Table'; + +import DeleteIcon from '@strapi/icons/DeleteIcon'; +import Show from '@strapi/icons/Show'; +import Reload from '@strapi/icons/Reload'; + +import permissions from '../../permissions'; +import { getTrad } from '../../utils'; +import openWithNewTab from '../../utils/openWithNewTab'; +import useReactQuery from '../utils/useReactQuery'; + +const PluginPage = () => { + const { formatMessage } = useIntl(); + const { data, isLoading, deleteMutation, regenerateDocMutation } = useReactQuery(); + const [showConfirmDelete, setShowConfirmDelete] = useState(false); + const [isConfirmButtonLoading, setIsConfirmButtonLoading] = useState(false); + const [versionToDelete, setVersionToDelete] = useState(); + + const colCount = 4; + const rowCount = (data?.docVersions?.length || 0) + 1; + + const openDocVersion = () => { + const slash = data?.prefix.startsWith('/') ? '' : '/'; + openWithNewTab(`${slash}${data?.prefix}/v${data?.currentVersion}`); + }; + + const handleRegenerateDoc = version => { + regenerateDocMutation.mutate({ version, prefix: data?.prefix }); + }; + + const handleShowConfirmDelete = () => { + setShowConfirmDelete(!showConfirmDelete); + }; + + const handleConfirmDelete = async () => { + setIsConfirmButtonLoading(true); + await deleteMutation.mutateAsync({ prefix: data?.prefix, version: versionToDelete }); + setShowConfirmDelete(!showConfirmDelete); + setIsConfirmButtonLoading(false); + }; + + const handleClickDelete = version => { + setVersionToDelete(version); + setShowConfirmDelete(!showConfirmDelete); + }; + + return ( + +
+ + + + } + /> + + {isLoading && Plugin is loading} + {data?.docVersions.length ? ( + + + + + + + + + {data.docVersions + .sort((a, b) => (a.generatedDate < b.generatedDate ? 1 : -1)) + .map(doc => ( + + + + + + ))} + +
+ + {formatMessage({ + id: getTrad('pages.PluginPage.table.version'), + defaultMessage: 'Version', + })} + + + + {formatMessage({ + id: getTrad('pages.PluginPage.table.generated'), + defaultMessage: 'Last Generated', + })} + +
+ {doc.version} + + {doc.generatedDate} + + + } + label={formatMessage( + { + id: getTrad('pages.PluginPage.table.icon.show'), + defaultMessage: 'Open {target}', + }, + { target: `${doc.version}` } + )} + /> + + handleRegenerateDoc(doc.version)} + noBorder + icon={} + label={formatMessage( + { + id: getTrad('pages.PluginPage.table.icon.regenerate'), + defaultMessage: 'Regnerate {target}', + }, + { target: `${doc.version}` } + )} + /> + + + {doc.version !== data.currentVersion && ( + handleClickDelete(doc.version)} + noBorder + icon={} + label={formatMessage( + { + id: getTrad('pages.PluginPage.table.icon.delete'), + defaultMessage: 'Delete {target}', + }, + { target: `${doc.version}` } + )} + /> + )} + + +
+ ) : ( + + )} +
+ +
+
+ ); +}; + +export default PluginPage; diff --git a/packages/plugins/documentation/admin/src/pages/PluginPage/tests/index.test.js b/packages/plugins/documentation/admin/src/pages/PluginPage/tests/index.test.js new file mode 100644 index 0000000000..b269ae5310 --- /dev/null +++ b/packages/plugins/documentation/admin/src/pages/PluginPage/tests/index.test.js @@ -0,0 +1,546 @@ +import React from 'react'; +import { render, screen, waitFor } from '@testing-library/react'; +import { IntlProvider } from 'react-intl'; +import { QueryClient, QueryClientProvider } from 'react-query'; +import { ThemeProvider, lightTheme } from '@strapi/parts'; +import { Router } from 'react-router-dom'; +import { createMemoryHistory } from 'history'; + +import PluginPage from '../index'; +import server from './server'; + +jest.mock('@strapi/helper-plugin', () => ({ + ...jest.requireActual('@strapi/helper-plugin'), + useNotification: jest.fn(), + CheckPermissions: jest.fn(({ children }) => children), +})); + +const client = new QueryClient({ + defaultOptions: { + queries: { + retry: false, + }, + }, +}); + +const makeApp = history => ( + + + + + + + + + +); + +describe('Plugin | Documentation | PluginPage', () => { + beforeAll(() => server.listen()); + + beforeEach(() => { + jest.clearAllMocks(); + }); + + afterEach(() => server.resetHandlers()); + + afterAll(() => server.close()); + + it('renders and matches the snapshot', () => { + const history = createMemoryHistory(); + const App = makeApp(history); + const { + container: { firstChild }, + } = render(App); + + expect(firstChild).toMatchInlineSnapshot(` + .c14 { + font-weight: 500; + font-size: 0.75rem; + line-height: 1.33; + color: #32324d; + } + + .c11 { + padding-right: 8px; + } + + .c8 { + display: -webkit-box; + display: -webkit-flex; + display: -ms-flexbox; + display: flex; + cursor: pointer; + padding: 8px; + border-radius: 4px; + background: #ffffff; + border: 1px solid #dcdce4; + position: relative; + outline: none; + } + + .c8 svg { + height: 12px; + width: 12px; + } + + .c8 svg > g, + .c8 svg path { + fill: #ffffff; + } + + .c8[aria-disabled='true'] { + pointer-events: none; + } + + .c8: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; + } + + .c8:focus-visible { + outline: none; + } + + .c8:focus-visible:after { + border-radius: 8px; + content: ''; + position: absolute; + top: -5px; + bottom: -5px; + left: -5px; + right: -5px; + border: 2px solid #4945ff; + } + + .c12 { + height: 100%; + } + + .c9 { + -webkit-align-items: center; + -webkit-box-align: center; + -ms-flex-align: center; + align-items: center; + padding: 8px 16px; + background: #4945ff; + border: none; + border: 1px solid #4945ff; + background: #4945ff; + } + + .c9 .c10 { + display: -webkit-box; + display: -webkit-flex; + display: -ms-flexbox; + display: flex; + -webkit-align-items: center; + -webkit-box-align: center; + -ms-flex-align: center; + align-items: center; + } + + .c9 .c13 { + color: #ffffff; + } + + .c9[aria-disabled='true'] { + border: 1px solid #dcdce4; + background: #eaeaef; + } + + .c9[aria-disabled='true'] .c13 { + color: #666687; + } + + .c9[aria-disabled='true'] svg > g, + .c9[aria-disabled='true'] svg path { + fill: #666687; + } + + .c9[aria-disabled='true']:active { + border: 1px solid #dcdce4; + background: #eaeaef; + } + + .c9[aria-disabled='true']:active .c13 { + color: #666687; + } + + .c9[aria-disabled='true']:active svg > g, + .c9[aria-disabled='true']:active svg path { + fill: #666687; + } + + .c9:hover { + border: 1px solid #7b79ff; + background: #7b79ff; + } + + .c9:active { + border: 1px solid #4945ff; + background: #4945ff; + } + + .c27 { + font-weight: 500; + font-size: 1rem; + line-height: 1.25; + color: #666687; + } + + .c22 { + background: #ffffff; + padding: 64px; + border-radius: 4px; + box-shadow: 0px 1px 4px rgba(33,33,52,0.1); + } + + .c24 { + padding-bottom: 24px; + } + + .c26 { + padding-bottom: 16px; + } + + .c23 { + display: -webkit-box; + display: -webkit-flex; + display: -ms-flexbox; + display: flex; + -webkit-flex-direction: column; + -ms-flex-direction: column; + flex-direction: column; + -webkit-align-items: center; + -webkit-box-align: center; + -ms-flex-align: center; + align-items: center; + text-align: center; + } + + .c25 svg { + height: 5.5rem; + } + + .c20 { + 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; + } + + .c21 { + -webkit-animation: gzYjWD 1s infinite linear; + animation: gzYjWD 1s infinite linear; + } + + .c18 { + 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: space-around; + -webkit-justify-content: space-around; + -ms-flex-pack: space-around; + justify-content: space-around; + -webkit-align-items: center; + -webkit-box-align: center; + -ms-flex-align: center; + align-items: center; + } + + .c19 { + height: 100vh; + } + + .c1 { + padding-bottom: 56px; + } + + .c4 { + background: #f6f6f9; + padding-top: 56px; + padding-right: 56px; + padding-bottom: 56px; + padding-left: 56px; + } + + .c17 { + padding-right: 56px; + padding-left: 56px; + } + + .c0 { + display: grid; + grid-template-columns: 1fr; + } + + .c2 { + overflow-x: hidden; + } + + .c5 { + 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; + -webkit-align-items: center; + -webkit-box-align: center; + -ms-flex-align: center; + align-items: center; + } + + .c6 { + display: -webkit-box; + display: -webkit-flex; + display: -ms-flexbox; + display: flex; + -webkit-flex-direction: row; + -ms-flex-direction: row; + flex-direction: row; + -webkit-align-items: center; + -webkit-box-align: center; + -ms-flex-align: center; + align-items: center; + } + + .c7 { + font-weight: 600; + font-size: 2rem; + line-height: 1.25; + color: #32324d; + } + + .c15 { + font-weight: 400; + font-size: 0.875rem; + line-height: 1.43; + color: #666687; + } + + .c16 { + font-size: 1rem; + line-height: 1.5; + } + + .c3 { + outline: none; + } + +
+
+
+
+
+
+
+

+ Documentation +

+
+ +
+

+ Configure the documentation plugin +

+
+
+
+
+
+
+ Plugin is loading +
+ +
+
+
+ +
+

+ You don't have any content yet... +

+
+
+
+
+
+
+ `); + }); + + it('should show a loader when fetching data', () => { + const history = createMemoryHistory(); + const App = makeApp(history); + render(App); + + expect(screen.getByTestId('loader')).toBeInTheDocument(); + }); + + it('should show a list of versions', async () => { + const history = createMemoryHistory(); + const App = makeApp(history); + render(App); + + await waitFor(() => expect(screen.getByText('1.0.0')).toBeInTheDocument()); + }); +}); diff --git a/packages/plugins/documentation/admin/src/pages/PluginPage/tests/server.js b/packages/plugins/documentation/admin/src/pages/PluginPage/tests/server.js new file mode 100644 index 0000000000..ee98d34760 --- /dev/null +++ b/packages/plugins/documentation/admin/src/pages/PluginPage/tests/server.js @@ -0,0 +1,23 @@ +import { setupServer } from 'msw/node'; +import { rest } from 'msw'; + +const handlers = [ + rest.get('*/getInfos', (req, res, ctx) => { + return res( + ctx.delay(1000), + ctx.status(200), + ctx.json({ + currentVersion: '1.0.0', + docVersions: [ + { version: '1.0.0', generatedDoc: '10/05/2021 2:52:44 PM' }, + { version: '1.2.0', generatedDoc: '11/05/2021 3:00:00 PM' }, + ], + prefix: '/documentation', + }) + ); + }), +]; + +const server = setupServer(...handlers); + +export default server; diff --git a/packages/plugins/documentation/admin/src/pages/SettingsPage/index.js b/packages/plugins/documentation/admin/src/pages/SettingsPage/index.js new file mode 100644 index 0000000000..306dd1e69c --- /dev/null +++ b/packages/plugins/documentation/admin/src/pages/SettingsPage/index.js @@ -0,0 +1,169 @@ +import React, { useState } from 'react'; +import { useIntl } from 'react-intl'; +import { Formik } from 'formik'; +import { CheckPermissions, Form, LoadingIndicatorPage } from '@strapi/helper-plugin'; + +// Strapi Parts +import { ContentLayout, HeaderLayout } from '@strapi/parts/Layout'; +import { Main } from '@strapi/parts/Main'; +import { Button } from '@strapi/parts/Button'; +import { Box } from '@strapi/parts/Box'; +import { Stack } from '@strapi/parts/Stack'; +import { H3 } from '@strapi/parts/Text'; +import { ToggleInput } from '@strapi/parts/ToggleInput'; +import { TextInput } from '@strapi/parts/TextInput'; +import { Grid, GridItem } from '@strapi/parts/Grid'; + +// Strapi Icons +import Show from '@strapi/icons/Show'; +import Hide from '@strapi/icons/Hide'; +import Check from '@strapi/icons/Check'; + +import permissions from '../../permissions'; +import { getTrad } from '../../utils'; +import useReactQuery from '../utils/useReactQuery'; +import FieldActionWrapper from '../../components/FieldActionWrapper'; +import schema from '../utils/schema'; + +const SettingsPage = () => { + const { formatMessage } = useIntl(); + const { submitMutation, data, isLoading } = useReactQuery(); + const [passwordShown, setPasswordShown] = useState(false); + + const handleUpdateSettingsSubmit = body => { + submitMutation.mutate({ + prefix: data?.prefix, + body, + }); + }; + + return ( +
+ {isLoading ? ( + Plugin settings are loading + ) : ( + + {({ handleSubmit, values, handleChange, errors }) => { + return ( +
+ + + + } + /> + + + +

+ {formatMessage({ + id: getTrad('pages.SettingsPage.title'), + defaultMessage: 'Settings', + })} +

+ + + + + {values.restrictedAccess && ( + + { + e.stopPropagation(); + setPasswordShown(prev => !prev); + }} + label={formatMessage( + passwordShown + ? { + id: 'Auth.form.password.show-password', + defaultMessage: 'Show password', + } + : { + id: 'Auth.form.password.hide-password', + defaultMessage: 'Hide password', + } + )} + > + {passwordShown ? : } + + } + /> + + )} + +
+
+
+ + ); + }} +
+ )} +
+ ); +}; + +export default SettingsPage; diff --git a/packages/plugins/documentation/admin/src/pages/SettingsPage/tests/index.test.js b/packages/plugins/documentation/admin/src/pages/SettingsPage/tests/index.test.js new file mode 100644 index 0000000000..f9c07638c0 --- /dev/null +++ b/packages/plugins/documentation/admin/src/pages/SettingsPage/tests/index.test.js @@ -0,0 +1,127 @@ +import React from 'react'; +import { render } from '@testing-library/react'; +import { IntlProvider } from 'react-intl'; +import { QueryClient, QueryClientProvider } from 'react-query'; +import { ThemeProvider, lightTheme } from '@strapi/parts'; +import { Router } from 'react-router-dom'; +import { createMemoryHistory } from 'history'; + +import SettingsPage from '../index'; +import server from './server'; + +jest.mock('@strapi/helper-plugin', () => ({ + ...jest.requireActual('@strapi/helper-plugin'), + useNotification: jest.fn(), + CheckPermissions: jest.fn(({ children }) => children), +})); + +const client = new QueryClient({ + defaultOptions: { + queries: { + retry: false, + }, + }, +}); + +const makeApp = history => ( + + + + + + + + + +); + +describe('Plugin | Documentation | SettingsPage', () => { + beforeAll(() => server.listen()); + + beforeEach(() => jest.clearAllMocks()); + + afterEach(() => server.resetHandlers()); + + afterAll(() => server.close()); + + it('renders and matches the snapshot', () => { + const history = createMemoryHistory(); + const App = makeApp(history); + const { + container: { firstChild }, + } = render(App); + + expect(firstChild).toMatchInlineSnapshot(` + .c3 { + 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; + } + + .c4 { + -webkit-animation: gzYjWD 1s infinite linear; + animation: gzYjWD 1s infinite linear; + } + + .c1 { + 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: space-around; + -webkit-justify-content: space-around; + -ms-flex-pack: space-around; + justify-content: space-around; + -webkit-align-items: center; + -webkit-box-align: center; + -ms-flex-align: center; + align-items: center; + } + + .c2 { + height: 100vh; + } + + .c0 { + outline: none; + } + +
+
+
+
+ Plugin settings are loading +
+ +
+
+
+ `); + }); +}); diff --git a/packages/plugins/documentation/admin/src/pages/SettingsPage/tests/server.js b/packages/plugins/documentation/admin/src/pages/SettingsPage/tests/server.js new file mode 100644 index 0000000000..d4c6c67f6f --- /dev/null +++ b/packages/plugins/documentation/admin/src/pages/SettingsPage/tests/server.js @@ -0,0 +1,18 @@ +import { setupServer } from 'msw/node'; +import { rest } from 'msw'; + +const handlers = [ + rest.get('*/getInfos', (req, res, ctx) => { + return res( + ctx.delay(1000), + ctx.status(200), + ctx.json({ + documentationAccess: { restrictedAccess: false, password: '' }, + }) + ); + }), +]; + +const server = setupServer(...handlers); + +export default server; diff --git a/packages/plugins/documentation/admin/src/pages/utils/api.js b/packages/plugins/documentation/admin/src/pages/utils/api.js new file mode 100644 index 0000000000..62347f94cc --- /dev/null +++ b/packages/plugins/documentation/admin/src/pages/utils/api.js @@ -0,0 +1,31 @@ +import { request } from '@strapi/helper-plugin'; +import pluginId from '../../pluginId'; + +const deleteDoc = ({ prefix, version }) => { + return request(`${prefix}/deleteDoc/${version}`, { method: 'DELETE' }); +}; + +const fetchDocumentationVersions = async toggleNotification => { + try { + const data = await request(`/${pluginId}/getInfos`, { method: 'GET' }); + + return data; + } catch (err) { + toggleNotification({ + type: 'warning', + message: { id: 'notification.error' }, + }); + + // FIXME + return null; + } +}; + +const regenerateDoc = ({ prefix, version }) => { + return request(`${prefix}/regenerateDoc`, { method: 'POST', body: { version } }); +}; + +const updateSettings = ({ prefix, body }) => + request(`${prefix}/updateSettings`, { method: 'PUT', body }); + +export { deleteDoc, fetchDocumentationVersions, regenerateDoc, updateSettings }; diff --git a/packages/plugins/documentation/admin/src/pages/utils/schema.js b/packages/plugins/documentation/admin/src/pages/utils/schema.js new file mode 100644 index 0000000000..7aba547d89 --- /dev/null +++ b/packages/plugins/documentation/admin/src/pages/utils/schema.js @@ -0,0 +1,11 @@ +import { translatedErrors } from '@strapi/helper-plugin'; +import * as yup from 'yup'; + +const schema = yup.object().shape({ + restrictedAccess: yup.boolean(), + password: yup.string().when('restrictedAccess', (value, initSchema) => { + return value ? initSchema.required(translatedErrors.required) : initSchema; + }), +}); + +export default schema; diff --git a/packages/plugins/documentation/admin/src/pages/utils/useReactQuery.js b/packages/plugins/documentation/admin/src/pages/utils/useReactQuery.js new file mode 100644 index 0000000000..4df81f145b --- /dev/null +++ b/packages/plugins/documentation/admin/src/pages/utils/useReactQuery.js @@ -0,0 +1,46 @@ +import { useQuery, useMutation, useQueryClient } from 'react-query'; +import { useNotification } from '@strapi/helper-plugin'; +import { fetchDocumentationVersions, deleteDoc, regenerateDoc, updateSettings } from './api'; +import getTrad from '../../utils/getTrad'; + +const useReactQuery = () => { + const queryClient = useQueryClient(); + const toggleNotification = useNotification(); + const { isLoading, data } = useQuery('get-documentation', () => + fetchDocumentationVersions(toggleNotification) + ); + + const handleError = err => { + toggleNotification({ + type: 'warning', + message: err.response.payload.message, + }); + }; + + const handleSuccess = (type, tradId) => { + queryClient.invalidateQueries('get-documentation'); + toggleNotification({ + type, + message: { id: getTrad(tradId) }, + }); + }; + + const deleteMutation = useMutation(deleteDoc, { + onSuccess: () => handleSuccess('info', 'notification.delete.success'), + onError: error => handleError(error), + }); + + const submitMutation = useMutation(updateSettings, { + onSuccess: () => handleSuccess('success', 'notification.update.success'), + onError: handleError, + }); + + const regenerateDocMutation = useMutation(regenerateDoc, { + onSuccess: () => handleSuccess('info', 'notification.generate.success'), + onError: error => handleError(error), + }); + + return { data, isLoading, deleteMutation, submitMutation, regenerateDocMutation }; +}; + +export default useReactQuery; diff --git a/packages/plugins/documentation/admin/src/translations/en.json b/packages/plugins/documentation/admin/src/translations/en.json index 9d927e6b1e..03cd139fd1 100755 --- a/packages/plugins/documentation/admin/src/translations/en.json +++ b/packages/plugins/documentation/admin/src/translations/en.json @@ -1,12 +1,9 @@ { "coming-soon": "This content is currently under construction and will be back in a few weeks!", - "components.Row.generatedDate": "Last generation", "components.Row.open": "Open", "components.Row.regenerate": "Regenerate", "containers.HomePage.Block.title": "Versions", - "containers.HomePage.Button.open": "Open the documentation", "containers.HomePage.Button.update": "Update", - "containers.HomePage.PluginHeader.description": "Configure the documentation plugin", "containers.HomePage.PluginHeader.title": "Documentation - Settings", "containers.HomePage.PopUpWarning.confirm": "I understand", "containers.HomePage.PopUpWarning.message": "Are you sure you want to delete this version?", @@ -26,7 +23,20 @@ "notification.delete.success": "Doc deleted", "notification.generate.success": "Doc generated", "notification.update.success": "Settings updated successfully", + "pages.PluginPage.Button.open": "Open documentation", + "pages.PluginPage.header.description": "Configure the documentation plugin", + "pages.PluginPage.table.generated": "Last generated", + "pages.PluginPage.table.icon.delete": "Delete {target}", + "pages.PluginPage.table.icon.regnerate": "Regenerate {target}", + "pages.PluginPage.table.icon.show": "Open {target}", + "pages.PluginPage.table.version": "Version", + "pages.SettingPage.title": "Settings", + "pages.SettingsPage.Button.description": "Configure the documentation plugin", + "pages.SettingsPage.header.save": "Save", + "pages.SettingsPage.password.label": "Password", + "pages.SettingsPage.toggle.label": "Restricted Access", + "pages.SettingsPage.toggle.hint": "Make the documentation endpoint private", "plugin.description.long": "Create an OpenAPI Document and visualize your API with SWAGGER UI.", "plugin.description.short": "Create an OpenAPI Document and visualize your API with SWAGGER UI.", "plugin.name": "Documentation" -} +} \ No newline at end of file diff --git a/packages/plugins/documentation/package.json b/packages/plugins/documentation/package.json index b321420009..f5cd125515 100644 --- a/packages/plugins/documentation/package.json +++ b/packages/plugins/documentation/package.json @@ -20,6 +20,8 @@ "lodash": "4.17.21", "moment": "^2.29.1", "path-to-regexp": "6.2.0", + "pluralize": "8.0.0", + "koa-session": "6.2.0", "react": "^17.0.2", "react-copy-to-clipboard": "^5.0.3", "react-dom": "^17.0.2", diff --git a/packages/plugins/documentation/server/config/index.js b/packages/plugins/documentation/server/config/index.js index 4916383c4e..38327da289 100644 --- a/packages/plugins/documentation/server/config/index.js +++ b/packages/plugins/documentation/server/config/index.js @@ -1,8 +1,9 @@ 'use strict'; const defaultConfig = require('./default-config'); +const sessionConfig = require('./session-config'); module.exports = { - default: defaultConfig, + default: { ...defaultConfig, ...sessionConfig }, validator() {}, }; diff --git a/packages/plugins/documentation/server/config/session-config.js b/packages/plugins/documentation/server/config/session-config.js new file mode 100644 index 0000000000..e29c0e6ac7 --- /dev/null +++ b/packages/plugins/documentation/server/config/session-config.js @@ -0,0 +1,19 @@ +'use strict'; + +module.exports = { + session: { + client: 'cookie', + key: 'strapi.sid', + prefix: 'strapi:sess:', + ttl: 864000000, + rolling: false, + secretKeys: ['mySecretKey1', 'mySecretKey2'], + cookie: { + path: '/', + httpOnly: true, + maxAge: 864000000, + rewrite: true, + signed: false, + }, + }, +}; diff --git a/packages/plugins/documentation/server/controllers/documentation.js b/packages/plugins/documentation/server/controllers/documentation.js index a7d9cf2fd5..387d8bc386 100644 --- a/packages/plugins/documentation/server/controllers/documentation.js +++ b/packages/plugins/documentation/server/controllers/documentation.js @@ -42,7 +42,11 @@ module.exports = { const version = major && minor && patch ? `${major}.${minor}.${patch}` - : strapi.plugins.documentation.config.info.version; + : strapi + .plugin('documentation') + .service('documentation') + .getDocumentationVersion(); + const openAPISpecsPath = path.join( strapi.config.appPath, 'src', diff --git a/packages/plugins/documentation/server/middlewares/documentation.js b/packages/plugins/documentation/server/middlewares/documentation.js index 689f1ad51f..1efacf1933 100755 --- a/packages/plugins/documentation/server/middlewares/documentation.js +++ b/packages/plugins/documentation/server/middlewares/documentation.js @@ -1,39 +1,15 @@ 'use strict'; const path = require('path'); -const _ = require('lodash'); const koaStatic = require('koa-static'); - -const initialRoutes = []; +const session = require('koa-session'); +const swaggerUi = require('swagger-ui-dist'); // TODO: delete when refactoring documentation plugin for v4 module.exports = async ({ strapi }) => { - // strapi.config.middleware.load.before.push('documentation'); - - initialRoutes.push(..._.cloneDeep(strapi.plugins.documentation.routes)); - - const swaggerUi = require('swagger-ui-dist'); - - // Find the plugins routes. - strapi.plugins.documentation.routes = strapi.plugins.documentation.routes.map((route, index) => { - if (route.handler === 'Documentation.getInfos') { - return route; - } - - if (route.handler === 'Documentation.index' || route.path === '/login') { - route.config.policies = initialRoutes[index].config.policies; - } - - // Set prefix to empty to be able to customise it. - if (strapi.config.has('plugins.documentation.x-strapi-config.path')) { - route.config.prefix = ''; - route.path = `/${strapi.config.get('plugin.documentation.x-strapi-config').path}${ - route.path - }`.replace('//', '/'); - } - - return route; - }); + const sessionConfig = strapi.config.get('plugin.documentation').session; + strapi.server.app.keys = sessionConfig.secretKeys; + strapi.server.app.use(session(sessionConfig, strapi.server.app)); strapi.server.routes([ { @@ -43,7 +19,7 @@ module.exports = async ({ strapi }) => { ctx.url = path.basename(ctx.url); return koaStatic(swaggerUi.getAbsoluteFSPath(), { - maxage: 6000, + maxage: sessionConfig.cookie.maxAge, defer: true, })(ctx, next); }, diff --git a/packages/plugins/documentation/server/routes/index.js b/packages/plugins/documentation/server/routes/index.js index dcae99067b..8b7c8fd354 100644 --- a/packages/plugins/documentation/server/routes/index.js +++ b/packages/plugins/documentation/server/routes/index.js @@ -1,4 +1,5 @@ 'use strict'; +const restrictAccess = require('../middlewares/restrict-access'); module.exports = [ { @@ -7,10 +8,15 @@ module.exports = [ handler: 'documentation.index', config: { auth: false, - // middlewares: [restrictAccess], - // policies: [ - // { name: 'admin::hasPermissions', options: { actions: ['plugin::documentation.read'] } }, - // ], + middlewares: [restrictAccess], + policies: [ + { + name: 'admin::hasPermissions', + config: { + actions: ['plugin::documentation.read'], + }, + }, + ], }, }, { @@ -19,10 +25,15 @@ module.exports = [ handler: 'documentation.index', config: { auth: false, - // middlewares: [restrictAccess], - // policies: [ - // { name: 'admin::hasPermissions', options: { actions: ['plugin::documentation.read'] } }, - // ], + middlewares: [restrictAccess], + policies: [ + { + name: 'admin::hasPermissions', + config: { + actions: ['plugin::documentation.read'], + }, + }, + ], }, }, { @@ -30,8 +41,14 @@ module.exports = [ path: '/login', handler: 'documentation.loginView', config: { + auth: false, policies: [ - { name: 'admin::hasPermissions', config: { actions: ['plugin::documentation.read'] } }, + { + name: 'admin::hasPermissions', + config: { + actions: ['plugin::documentation.read'], + }, + }, ], }, }, @@ -40,6 +57,7 @@ module.exports = [ path: '/login', handler: 'documentation.login', config: { + auth: false, policies: [ { name: 'admin::hasPermissions', config: { actions: ['plugin::documentation.read'] } }, ],