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 (
+
+
+
+ }>
+ {formatMessage({
+ id: getTrad('pages.PluginPage.Button.open'),
+ defaultMessage: 'Open Documentation',
+ })}
+
+
+ }
+ />
+
+ {isLoading && Plugin is loading}
+ {data?.docVersions.length ? (
+
+
+
+ |
+
+ {formatMessage({
+ id: getTrad('pages.PluginPage.table.version'),
+ defaultMessage: 'Version',
+ })}
+
+ |
+
+
+ {formatMessage({
+ id: getTrad('pages.PluginPage.table.generated'),
+ defaultMessage: 'Last Generated',
+ })}
+
+ |
+
+
+
+ {data.docVersions
+ .sort((a, b) => (a.generatedDate < b.generatedDate ? 1 : -1))
+ .map(doc => (
+
+ |
+ {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 (
+
+ );
+ }}
+
+ )}
+
+ );
+};
+
+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'] } },
],