mirror of
https://github.com/strapi/strapi.git
synced 2025-12-27 15:13:21 +00:00
Merge pull request #11268 from strapi/v4/migrate-plugin-documentation
[v4] migrate the documentation plugin
This commit is contained in:
commit
a639e92022
@ -14,6 +14,13 @@ module.exports = () => ({
|
||||
},
|
||||
},
|
||||
},
|
||||
documentation: {
|
||||
config: {
|
||||
info: {
|
||||
version: '2.0.0',
|
||||
},
|
||||
},
|
||||
},
|
||||
myplugin: {
|
||||
enabled: true,
|
||||
resolve: `./src/plugins/myplugin`, // From the root of the project
|
||||
|
||||
@ -46,7 +46,7 @@
|
||||
"axios": "^0.21.1",
|
||||
"babel-loader": "8.2.2",
|
||||
"babel-plugin-styled-components": "1.12.0",
|
||||
"bcryptjs": "^2.4.3",
|
||||
"bcryptjs": "2.4.3",
|
||||
"chalk": "^4.1.1",
|
||||
"chokidar": "^3.5.1",
|
||||
"classnames": "^2.3.1",
|
||||
|
||||
@ -193,13 +193,13 @@ describe('Role CRUD End to End', () => {
|
||||
"action": "plugin::documentation.settings.regenerate",
|
||||
"displayName": "Regenerate",
|
||||
"plugin": "documentation",
|
||||
"subCategory": "settings",
|
||||
"subCategory": "general",
|
||||
},
|
||||
Object {
|
||||
"action": "plugin::documentation.settings.update",
|
||||
"displayName": "Update and delete",
|
||||
"plugin": "documentation",
|
||||
"subCategory": "settings",
|
||||
"subCategory": "general",
|
||||
},
|
||||
Object {
|
||||
"action": "plugin::upload.assets.copy-link",
|
||||
@ -407,6 +407,12 @@ describe('Role CRUD End to End', () => {
|
||||
"displayName": "Update",
|
||||
"subCategory": "general",
|
||||
},
|
||||
Object {
|
||||
"action": "plugin::documentation.settings.read",
|
||||
"category": "documentation",
|
||||
"displayName": "Access the documentation settings page",
|
||||
"subCategory": "general",
|
||||
},
|
||||
Object {
|
||||
"action": "plugin::email.settings.read",
|
||||
"category": "email",
|
||||
@ -666,13 +672,13 @@ describe('Role CRUD End to End', () => {
|
||||
"action": "plugin::documentation.settings.regenerate",
|
||||
"displayName": "Regenerate",
|
||||
"plugin": "documentation",
|
||||
"subCategory": "settings",
|
||||
"subCategory": "general",
|
||||
},
|
||||
Object {
|
||||
"action": "plugin::documentation.settings.update",
|
||||
"displayName": "Update and delete",
|
||||
"plugin": "documentation",
|
||||
"subCategory": "settings",
|
||||
"subCategory": "general",
|
||||
},
|
||||
Object {
|
||||
"action": "plugin::upload.assets.copy-link",
|
||||
@ -892,6 +898,12 @@ describe('Role CRUD End to End', () => {
|
||||
"displayName": "Update",
|
||||
"subCategory": "general",
|
||||
},
|
||||
Object {
|
||||
"action": "plugin::documentation.settings.read",
|
||||
"category": "documentation",
|
||||
"displayName": "Access the documentation settings page",
|
||||
"subCategory": "general",
|
||||
},
|
||||
Object {
|
||||
"action": "plugin::email.settings.read",
|
||||
"category": "email",
|
||||
|
||||
@ -41,7 +41,6 @@ module.exports = (dir, initialConfig = {}) => {
|
||||
|
||||
const rootConfig = {
|
||||
launchedAt: Date.now(),
|
||||
appPath: dir,
|
||||
paths: CONFIG_PATHS,
|
||||
serveAdminPanel,
|
||||
autoReload,
|
||||
|
||||
@ -34,8 +34,10 @@ describe('Session middleware', () => {
|
||||
},
|
||||
use: jest.fn(),
|
||||
},
|
||||
dirs: {
|
||||
root: __dirname,
|
||||
},
|
||||
config: {
|
||||
appPath: __dirname,
|
||||
database: {
|
||||
connections: {
|
||||
mysql: {},
|
||||
|
||||
@ -9,7 +9,7 @@ const session = require('koa-session');
|
||||
*/
|
||||
module.exports = strapi => {
|
||||
const requireStore = store => {
|
||||
return require(path.resolve(strapi.config.appPath, 'node_modules', 'koa-' + store));
|
||||
return require(path.resolve(strapi.dirs.root, 'node_modules', 'koa-' + store));
|
||||
};
|
||||
|
||||
const defineStore = session => {
|
||||
|
||||
@ -1,26 +0,0 @@
|
||||
import styled from 'styled-components';
|
||||
|
||||
const Wrapper = styled.div`
|
||||
margin-bottom: 28px;
|
||||
background: #ffffff;
|
||||
padding: 22px 28px 18px;
|
||||
border-radius: 2px;
|
||||
box-shadow: 0 2px 4px #e3e9f3;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
`;
|
||||
|
||||
const Title = styled.div`
|
||||
padding-top: 0px;
|
||||
line-height: 18px;
|
||||
> span {
|
||||
font-weight: 600;
|
||||
color: #333740;
|
||||
font-size: 18px;
|
||||
}
|
||||
> p {
|
||||
color: #787e8f;
|
||||
font-size: 13px;
|
||||
}
|
||||
`;
|
||||
|
||||
export { Wrapper, Title };
|
||||
@ -1,39 +0,0 @@
|
||||
/**
|
||||
*
|
||||
* Block
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { FormattedMessage } from 'react-intl';
|
||||
import { Wrapper, Title } from './components';
|
||||
|
||||
const renderMsg = msg => <p>{msg}</p>;
|
||||
|
||||
const Block = ({ children, description, style, title }) => (
|
||||
<div className="col-md-12">
|
||||
<Wrapper style={style}>
|
||||
<Title>
|
||||
<FormattedMessage id={title} />
|
||||
<FormattedMessage id={description}>{renderMsg}</FormattedMessage>
|
||||
</Title>
|
||||
{children}
|
||||
</Wrapper>
|
||||
</div>
|
||||
);
|
||||
|
||||
Block.defaultProps = {
|
||||
children: null,
|
||||
description: 'app.utils.defaultMessage',
|
||||
style: {},
|
||||
title: 'app.utils.defaultMessage',
|
||||
};
|
||||
|
||||
Block.propTypes = {
|
||||
children: PropTypes.any,
|
||||
description: PropTypes.string,
|
||||
style: PropTypes.object,
|
||||
title: PropTypes.string,
|
||||
};
|
||||
|
||||
export default Block;
|
||||
@ -1,36 +0,0 @@
|
||||
import React from 'react';
|
||||
import { auth, InputsIndex as Input, useNotification } from '@strapi/helper-plugin';
|
||||
import { CopyToClipboard } from 'react-copy-to-clipboard';
|
||||
import getTrad from '../../utils/getTrad';
|
||||
|
||||
const Copy = () => {
|
||||
const toggleNotification = useNotification();
|
||||
|
||||
const handleCopy = () => {
|
||||
toggleNotification({
|
||||
type: 'info',
|
||||
message: { id: getTrad('containers.HomePage.copied') },
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<CopyToClipboard text={auth.getToken()} onCopy={handleCopy}>
|
||||
<div className="row" style={{ zIndex: '99' }}>
|
||||
<Input
|
||||
style={{ zIndex: '9', cursor: 'pointer' }}
|
||||
inputStyle={{ cursor: 'pointer' }}
|
||||
name="jwtToken"
|
||||
value={auth.getToken()}
|
||||
type="string"
|
||||
onChange={() => {}}
|
||||
label={{ id: getTrad('containers.HomePage.form.jwtToken') }}
|
||||
inputDescription={{
|
||||
id: getTrad('containers.HomePage.form.jwtToken.description'),
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</CopyToClipboard>
|
||||
);
|
||||
};
|
||||
|
||||
export default Copy;
|
||||
@ -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;
|
||||
@ -1,72 +0,0 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { useIntl } from 'react-intl';
|
||||
import { Header as BaseHeader } from '@buffetjs/custom';
|
||||
import { Button } from '@buffetjs/core';
|
||||
import { CheckPermissions } from '@strapi/helper-plugin';
|
||||
import openWithNewTab from '../../utils/openWithNewTab';
|
||||
import pluginPermissions from '../../permissions';
|
||||
import getTrad from '../../utils/getTrad';
|
||||
|
||||
const Header = ({ currentDocVersion, docPrefixURL }) => {
|
||||
const { formatMessage } = useIntl();
|
||||
const headerActions = [
|
||||
{
|
||||
color: 'none',
|
||||
label: formatMessage({
|
||||
id: getTrad('containers.HomePage.Button.open'),
|
||||
defaultMessage: 'Open the documentation',
|
||||
}),
|
||||
className: 'buttonOutline',
|
||||
onClick: () => {
|
||||
const slash = docPrefixURL.startsWith('/') ? '' : '/';
|
||||
|
||||
return openWithNewTab(`${slash}${docPrefixURL}/v${currentDocVersion}`);
|
||||
},
|
||||
type: 'button',
|
||||
key: 'button-open',
|
||||
Component: props => (
|
||||
<CheckPermissions permissions={pluginPermissions.open}>
|
||||
<Button {...props} />
|
||||
</CheckPermissions>
|
||||
),
|
||||
},
|
||||
{
|
||||
label: formatMessage({
|
||||
id: getTrad('containers.HomePage.Button.update'),
|
||||
defaultMessage: 'Update',
|
||||
}),
|
||||
color: 'success',
|
||||
type: 'submit',
|
||||
key: 'button-submit',
|
||||
Component: props => (
|
||||
<CheckPermissions permissions={pluginPermissions.update}>
|
||||
<Button {...props} />
|
||||
</CheckPermissions>
|
||||
),
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<BaseHeader
|
||||
actions={headerActions}
|
||||
content={formatMessage({
|
||||
id: getTrad('containers.HomePage.PluginHeader.description'),
|
||||
defaultMessage: 'Configure the documentation plugin',
|
||||
})}
|
||||
title={{
|
||||
label: formatMessage({
|
||||
id: getTrad('containers.HomePage.PluginHeader.title'),
|
||||
defaultMessage: 'Documentation - Settings',
|
||||
}),
|
||||
}}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
Header.propTypes = {
|
||||
currentDocVersion: PropTypes.string.isRequired,
|
||||
docPrefixURL: PropTypes.string.isRequired,
|
||||
};
|
||||
|
||||
export default Header;
|
||||
@ -1,67 +0,0 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { FormattedMessage } from 'react-intl';
|
||||
import { CheckPermissions } from '@strapi/helper-plugin';
|
||||
import pluginPermissions from '../../permissions';
|
||||
import openWithNewTab from '../../utils/openWithNewTab';
|
||||
import { StyledButton } from './components';
|
||||
|
||||
const ButtonContainer = ({ currentDocVersion, isHeader, onClick, onClickDelete, version }) => {
|
||||
if (isHeader) {
|
||||
return <div />;
|
||||
}
|
||||
|
||||
const handleClickGenerate = e => {
|
||||
e.preventDefault();
|
||||
onClick(version);
|
||||
};
|
||||
|
||||
const handleDelete = e => {
|
||||
e.preventDefault();
|
||||
onClickDelete(version);
|
||||
};
|
||||
|
||||
return (
|
||||
<div>
|
||||
<StyledButton
|
||||
type="openDocumentation"
|
||||
onClick={e => {
|
||||
e.preventDefault();
|
||||
openWithNewTab(`/documentation/v${version}`);
|
||||
}}
|
||||
>
|
||||
<FormattedMessage id="documentation.components.Row.open" />
|
||||
</StyledButton>
|
||||
<CheckPermissions permissions={pluginPermissions.regenerate}>
|
||||
<StyledButton
|
||||
// FIXME when new DS ready
|
||||
type="generateDocumentation"
|
||||
onClick={handleClickGenerate}
|
||||
>
|
||||
<FormattedMessage id="documentation.components.Row.regenerate" />
|
||||
</StyledButton>
|
||||
</CheckPermissions>
|
||||
<CheckPermissions permissions={pluginPermissions.update}>
|
||||
<StyledButton type={version === currentDocVersion ? '' : 'trash'} onClick={handleDelete} />
|
||||
</CheckPermissions>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
ButtonContainer.defaultProps = {
|
||||
currentDocVersion: '1.0.0',
|
||||
isHeader: false,
|
||||
onClick: () => {},
|
||||
onClickDelete: () => {},
|
||||
version: '',
|
||||
};
|
||||
|
||||
ButtonContainer.propTypes = {
|
||||
currentDocVersion: PropTypes.string,
|
||||
isHeader: PropTypes.bool,
|
||||
onClick: PropTypes.func,
|
||||
onClickDelete: PropTypes.func,
|
||||
version: PropTypes.string,
|
||||
};
|
||||
|
||||
export default ButtonContainer;
|
||||
@ -1,83 +0,0 @@
|
||||
import styled, { css } from 'styled-components';
|
||||
import { Button } from '@strapi/helper-plugin';
|
||||
|
||||
const Wrapper = styled.div`
|
||||
height: 54px;
|
||||
display: flex;
|
||||
line-height: 53px;
|
||||
margin: 0 28px 0 36px;
|
||||
justify-content: space-between;
|
||||
border-bottom: 1px solid rgba(14, 22, 34, 0.04);
|
||||
font-size: 13px;
|
||||
color: #333740;
|
||||
> div:first-child {
|
||||
flex: 0 0 70px;
|
||||
font-weight: 500;
|
||||
}
|
||||
> div:nth-child(2) {
|
||||
flex: 0 0 160px;
|
||||
text-align: left;
|
||||
font-weight: 500 !important;
|
||||
}
|
||||
> div:last-child {
|
||||
flex: 0 0 400px;
|
||||
align-self: center;
|
||||
text-align: right;
|
||||
}
|
||||
-webkit-font-smoothing: antialiased;
|
||||
`;
|
||||
|
||||
const StyledButton = styled(Button)`
|
||||
height: 26px;
|
||||
margin: 0;
|
||||
padding: 0 15px;
|
||||
line-height: initial;
|
||||
font-size: 13px;
|
||||
font-weight: 500;
|
||||
${({ type }) => {
|
||||
if (type === 'openDocumentation') {
|
||||
return css`
|
||||
margin-right: 10px;
|
||||
border: 1px solid #dfe0e1;
|
||||
&:before {
|
||||
margin-right: 10px;
|
||||
content: '\f08e';
|
||||
font-family: 'FontAwesome';
|
||||
font-size: 10px;
|
||||
}
|
||||
`;
|
||||
}
|
||||
if (type === 'generateDocumentation') {
|
||||
return css`
|
||||
background: #e6f0fb;
|
||||
border: 1px solid #aed4fb;
|
||||
color: #007eff;
|
||||
&:before {
|
||||
margin-right: 10px;
|
||||
content: '\f021';
|
||||
font-family: 'FontAwesome';
|
||||
font-size: 10px;
|
||||
}
|
||||
`;
|
||||
}
|
||||
if (type === 'trash') {
|
||||
return css`
|
||||
margin-left: 25px;
|
||||
font-weight: 400;
|
||||
&:before {
|
||||
margin-right: 10px;
|
||||
content: '\f2ed';
|
||||
font-family: 'FontAwesome';
|
||||
font-size: 12 px;
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
||||
return css`
|
||||
margin-left: 45px;
|
||||
font-weight: 400;
|
||||
`;
|
||||
}}
|
||||
`;
|
||||
|
||||
export { Wrapper, StyledButton };
|
||||
@ -1,51 +0,0 @@
|
||||
/**
|
||||
* Row
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { FormattedMessage } from 'react-intl';
|
||||
import PropTypes from 'prop-types';
|
||||
import ButtonContainer from './ButtonContainer';
|
||||
import { Wrapper } from './components';
|
||||
|
||||
function Row({ currentDocVersion, data, isHeader, onClickDelete, onUpdateDoc }) {
|
||||
const { version, generatedDate } = data;
|
||||
|
||||
return (
|
||||
<Wrapper>
|
||||
<div>{version}</div>
|
||||
<div>
|
||||
{isHeader ? (
|
||||
<FormattedMessage id="documentation.components.Row.generatedDate" />
|
||||
) : (
|
||||
<span>{generatedDate}</span>
|
||||
)}
|
||||
</div>
|
||||
<ButtonContainer
|
||||
currentDocVersion={currentDocVersion}
|
||||
isHeader={isHeader}
|
||||
version={version}
|
||||
onClickDelete={onClickDelete}
|
||||
onClick={onUpdateDoc}
|
||||
/>
|
||||
</Wrapper>
|
||||
);
|
||||
}
|
||||
|
||||
Row.defaultProps = {
|
||||
currentDocVersion: '1.0.0',
|
||||
data: {},
|
||||
isHeader: false,
|
||||
onClickDelete: () => {},
|
||||
onUpdateDoc: () => {},
|
||||
};
|
||||
|
||||
Row.propTypes = {
|
||||
currentDocVersion: PropTypes.string,
|
||||
data: PropTypes.object,
|
||||
isHeader: PropTypes.bool,
|
||||
onClickDelete: PropTypes.func,
|
||||
onUpdateDoc: PropTypes.func,
|
||||
};
|
||||
|
||||
export default Row;
|
||||
@ -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 => {
|
||||
|
||||
@ -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 (
|
||||
<Layout>
|
||||
<Main>
|
||||
<HeaderLayout
|
||||
title={formatMessage({
|
||||
id: getTrad('plugin.name'),
|
||||
defaultMessage: 'Documentation',
|
||||
})}
|
||||
/>
|
||||
<ContentLayout>
|
||||
<NoContent
|
||||
content={{
|
||||
id: getTrad('coming.soon'),
|
||||
defaultMessage:
|
||||
'This content is currently under construction and will be back in a few weeks!',
|
||||
}}
|
||||
/>
|
||||
</ContentLayout>
|
||||
</Main>
|
||||
</Layout>
|
||||
);
|
||||
};
|
||||
|
||||
function App() {
|
||||
return (
|
||||
<CheckPagePermissions permissions={pluginPermissions.main}>
|
||||
{/* <HomePage /> */}
|
||||
<ComingSoon />
|
||||
</CheckPagePermissions>
|
||||
);
|
||||
}
|
||||
|
||||
export default App;
|
||||
@ -1,59 +0,0 @@
|
||||
import styled from 'styled-components';
|
||||
|
||||
const ContainerFluid = styled.div`
|
||||
padding: 18px 30px;
|
||||
> div:first-child {
|
||||
max-height: 33px;
|
||||
}
|
||||
|
||||
.buttonOutline {
|
||||
height: 30px;
|
||||
padding: 0 15px;
|
||||
border: 1px solid #dfe0e1;
|
||||
font-weight: 500;
|
||||
font-size: 13px;
|
||||
&:before {
|
||||
margin-right: 10px;
|
||||
content: '\f08e';
|
||||
font-family: 'FontAwesome';
|
||||
font-size: 10px;
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
const StyledRow = styled.div`
|
||||
padding-top: 11px;
|
||||
> div:last-child {
|
||||
> div {
|
||||
padding-bottom: 0 !important;
|
||||
}
|
||||
}
|
||||
> div:first-child {
|
||||
> div {
|
||||
margin-bottom: 22px !important;
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
const VersionWrapper = styled.div`
|
||||
margin-top: -9px;
|
||||
margin-left: -28px;
|
||||
margin-right: -28px;
|
||||
> div:first-child {
|
||||
height: 18px;
|
||||
line-height: 16px;
|
||||
border-bottom: 0;
|
||||
|
||||
> div:nth-child(2) {
|
||||
font-weight: 600;
|
||||
color: #9ea7b8;
|
||||
}
|
||||
}
|
||||
> div:last-child {
|
||||
> div {
|
||||
border-bottom: none;
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
export { ContainerFluid, StyledRow, VersionWrapper };
|
||||
@ -1,175 +0,0 @@
|
||||
/*
|
||||
*
|
||||
* HomePage
|
||||
*
|
||||
*/
|
||||
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import flatten from 'lodash/flatten';
|
||||
import {
|
||||
PopUpWarning,
|
||||
LoadingIndicatorPage,
|
||||
CheckPermissions,
|
||||
SizedInput,
|
||||
getYupInnerErrors,
|
||||
} from '@strapi/helper-plugin';
|
||||
import pluginPermissions from '../../permissions';
|
||||
import getTrad from '../../utils/getTrad';
|
||||
import Block from '../../components/Block';
|
||||
import Copy from '../../components/Copy';
|
||||
import Header from '../../components/Header';
|
||||
import Row from '../../components/Row';
|
||||
import { ContainerFluid, StyledRow, VersionWrapper } from './components';
|
||||
import useHomePage from './useHomePage';
|
||||
import schema from './utils/schema';
|
||||
|
||||
const HomePage = () => {
|
||||
const [versionToDelete, setVersionToDelete] = useState(null);
|
||||
|
||||
const [{ formErrors, modifiedData }, setState] = useState({
|
||||
formErrors: null,
|
||||
modifiedData: {
|
||||
restrictedAccess: true,
|
||||
password: '',
|
||||
},
|
||||
});
|
||||
|
||||
const { data, isLoading, deleteMutation, submitMutation, regenerateDocMutation } = useHomePage();
|
||||
|
||||
useEffect(() => {
|
||||
if (data?.form) {
|
||||
const initialData = flatten(data.form).reduce((acc, current) => {
|
||||
acc[current.name] = current.value;
|
||||
|
||||
return acc;
|
||||
}, {});
|
||||
setState({ formErrors: null, modifiedData: initialData });
|
||||
}
|
||||
}, [data]);
|
||||
|
||||
const handleChange = ({ target: { name, value } }) => {
|
||||
setState(prev => ({
|
||||
...prev,
|
||||
modifiedData: {
|
||||
...prev.modifiedData,
|
||||
[name]: value,
|
||||
},
|
||||
}));
|
||||
};
|
||||
|
||||
const handleDeleteDoc = version => {
|
||||
setVersionToDelete(version);
|
||||
};
|
||||
|
||||
const handleConfirmDeleteDoc = () => {
|
||||
deleteMutation.mutate({ prefix: data.prefix, version: versionToDelete });
|
||||
toggleModal();
|
||||
};
|
||||
|
||||
const handleSubmit = async e => {
|
||||
e.preventDefault();
|
||||
|
||||
try {
|
||||
await schema.validate(modifiedData, { abortEarly: false });
|
||||
|
||||
setState(prev => ({ ...prev, formErrors: null }));
|
||||
|
||||
submitMutation.mutate({ body: modifiedData, prefix: data.prefix });
|
||||
} catch (err) {
|
||||
const errors = getYupInnerErrors(err);
|
||||
|
||||
setState(prev => ({ ...prev, formErrors: errors }));
|
||||
}
|
||||
};
|
||||
|
||||
const handleUpdateDoc = version => {
|
||||
regenerateDocMutation.mutate({ version, prefix: data.prefix });
|
||||
};
|
||||
|
||||
const toggleModal = () => {
|
||||
setVersionToDelete(null);
|
||||
};
|
||||
|
||||
console.log(data);
|
||||
|
||||
if (isLoading) {
|
||||
return <LoadingIndicatorPage />;
|
||||
}
|
||||
|
||||
// FIXME
|
||||
if (!data) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<ContainerFluid className="container-fluid">
|
||||
<PopUpWarning
|
||||
isOpen={versionToDelete !== null}
|
||||
toggleModal={toggleModal}
|
||||
content={{
|
||||
title: 'components.popUpWarning.title',
|
||||
message: getTrad('containers.HomePage.PopUpWarning.message'),
|
||||
cancel: 'app.components.Button.cancel',
|
||||
confirm: getTrad('containers.HomePage.PopUpWarning.confirm'),
|
||||
}}
|
||||
popUpWarningType="danger"
|
||||
onConfirm={handleConfirmDeleteDoc}
|
||||
/>
|
||||
<form onSubmit={handleSubmit}>
|
||||
<Header currentDocVersion={data.currentVersion} docPrefixURL={data.prefix} />
|
||||
<StyledRow className="row">
|
||||
<Block>
|
||||
<Copy />
|
||||
</Block>
|
||||
<CheckPermissions permissions={pluginPermissions.update}>
|
||||
<Block>
|
||||
<div className="row">
|
||||
<SizedInput
|
||||
description={getTrad(
|
||||
'containers.HomePage.form.restrictedAccess.inputDescription'
|
||||
)}
|
||||
label={getTrad('containers.HomePage.form.restrictedAccess')}
|
||||
name="restrictedAccess"
|
||||
onChange={handleChange}
|
||||
size={{ xs: 6 }}
|
||||
type="bool"
|
||||
value={modifiedData.restrictedAccess}
|
||||
/>
|
||||
{modifiedData.restrictedAccess && (
|
||||
<SizedInput
|
||||
description={getTrad('containers.HomePage.form.password.inputDescription')}
|
||||
label={getTrad('containers.HomePage.form.password')}
|
||||
error={formErrors?.password}
|
||||
name="password"
|
||||
onChange={handleChange}
|
||||
size={{ xs: 6 }}
|
||||
type="password"
|
||||
value={modifiedData.password}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</Block>
|
||||
</CheckPermissions>
|
||||
<Block title={getTrad('containers.HomePage.Block.title')}>
|
||||
<VersionWrapper>
|
||||
<Row isHeader />
|
||||
{data.docVersions.map(doc => {
|
||||
return (
|
||||
<Row
|
||||
key={doc.generatedDate}
|
||||
data={doc}
|
||||
currentDocVersion={data.currentVersion}
|
||||
onClickDelete={handleDeleteDoc}
|
||||
onUpdateDoc={handleUpdateDoc}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</VersionWrapper>
|
||||
</Block>
|
||||
</StyledRow>
|
||||
</form>
|
||||
</ContainerFluid>
|
||||
);
|
||||
};
|
||||
|
||||
export default HomePage;
|
||||
@ -1,56 +0,0 @@
|
||||
import { useQuery, useMutation, useQueryClient } from 'react-query';
|
||||
import { useNotification } from '@strapi/helper-plugin';
|
||||
import { fetchData, deleteDoc, regenerateDoc, submit } from './utils/api';
|
||||
import getTrad from '../../utils/getTrad';
|
||||
|
||||
const useHomePage = () => {
|
||||
const queryClient = useQueryClient();
|
||||
const toggleNotification = useNotification();
|
||||
const { isLoading, data } = useQuery('get-documentation', () => fetchData(toggleNotification));
|
||||
|
||||
const handleError = err => {
|
||||
toggleNotification({
|
||||
type: 'warning',
|
||||
message: err.response.payload.message,
|
||||
});
|
||||
};
|
||||
|
||||
const deleteMutation = useMutation(deleteDoc, {
|
||||
onSuccess: async () => {
|
||||
await queryClient.invalidateQueries('get-documentation');
|
||||
toggleNotification({
|
||||
type: 'info',
|
||||
message: { id: getTrad('notification.delete.success') },
|
||||
});
|
||||
},
|
||||
onError: handleError,
|
||||
});
|
||||
|
||||
const submitMutation = useMutation(submit, {
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries('get-documentation');
|
||||
|
||||
toggleNotification({
|
||||
type: 'success',
|
||||
message: { id: getTrad('notification.update.success') },
|
||||
});
|
||||
},
|
||||
onError: handleError,
|
||||
});
|
||||
|
||||
const regenerateDocMutation = useMutation(regenerateDoc, {
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries('get-documentation');
|
||||
|
||||
toggleNotification({
|
||||
type: 'info',
|
||||
message: { id: getTrad('notification.generate.success') },
|
||||
});
|
||||
},
|
||||
onError: handleError,
|
||||
});
|
||||
|
||||
return { data, isLoading, deleteMutation, submitMutation, regenerateDocMutation };
|
||||
};
|
||||
|
||||
export default useHomePage;
|
||||
195
packages/plugins/documentation/admin/src/pages/PluginPage/index.js
Executable file
195
packages/plugins/documentation/admin/src/pages/PluginPage/index.js
Executable file
@ -0,0 +1,195 @@
|
||||
/**
|
||||
*
|
||||
* 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,
|
||||
useFocusWhenNavigate,
|
||||
} 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 = () => {
|
||||
useFocusWhenNavigate();
|
||||
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 (
|
||||
<Layout>
|
||||
<Main>
|
||||
<HeaderLayout
|
||||
title={formatMessage({
|
||||
id: getTrad('plugin.name'),
|
||||
defaultMessage: 'Documentation',
|
||||
})}
|
||||
subtitle={formatMessage({
|
||||
id: getTrad('pages.PluginPage.header.description'),
|
||||
defaultMessage: 'Configure the documentation plugin',
|
||||
})}
|
||||
primaryAction={
|
||||
// eslint-disable-next-line
|
||||
<CheckPermissions permissions={permissions.open}>
|
||||
<Button onClick={openDocVersion} startIcon={<Show />}>
|
||||
{formatMessage({
|
||||
id: getTrad('pages.PluginPage.Button.open'),
|
||||
defaultMessage: 'Open Documentation',
|
||||
})}
|
||||
</Button>
|
||||
</CheckPermissions>
|
||||
}
|
||||
/>
|
||||
<ContentLayout>
|
||||
{isLoading && <LoadingIndicatorPage>Plugin is loading</LoadingIndicatorPage>}
|
||||
{data?.docVersions.length ? (
|
||||
<Table colCount={colCount} rowCount={rowCount}>
|
||||
<Thead>
|
||||
<Tr>
|
||||
<Th>
|
||||
<TableLabel textColor="neutral600">
|
||||
{formatMessage({
|
||||
id: getTrad('pages.PluginPage.table.version'),
|
||||
defaultMessage: 'Version',
|
||||
})}
|
||||
</TableLabel>
|
||||
</Th>
|
||||
<Th>
|
||||
<TableLabel textColor="neutral600">
|
||||
{formatMessage({
|
||||
id: getTrad('pages.PluginPage.table.generated'),
|
||||
defaultMessage: 'Last Generated',
|
||||
})}
|
||||
</TableLabel>
|
||||
</Th>
|
||||
</Tr>
|
||||
</Thead>
|
||||
<Tbody>
|
||||
{data.docVersions
|
||||
.sort((a, b) => (a.generatedDate < b.generatedDate ? 1 : -1))
|
||||
.map(doc => (
|
||||
<Tr key={doc.version}>
|
||||
<Td width="50%">
|
||||
<Text>{doc.version}</Text>
|
||||
</Td>
|
||||
<Td width="50%">
|
||||
<Text>{doc.generatedDate}</Text>
|
||||
</Td>
|
||||
<Td>
|
||||
<Row justifyContent="end" {...stopPropagation}>
|
||||
<IconButton
|
||||
onClick={openDocVersion}
|
||||
noBorder
|
||||
icon={<Show />}
|
||||
label={formatMessage(
|
||||
{
|
||||
id: getTrad('pages.PluginPage.table.icon.show'),
|
||||
defaultMessage: 'Open {target}',
|
||||
},
|
||||
{ target: `${doc.version}` }
|
||||
)}
|
||||
/>
|
||||
<CheckPermissions permissions={permissions.regenerate}>
|
||||
<IconButton
|
||||
onClick={() => handleRegenerateDoc(doc.version)}
|
||||
noBorder
|
||||
icon={<Reload />}
|
||||
label={formatMessage(
|
||||
{
|
||||
id: getTrad('pages.PluginPage.table.icon.regenerate'),
|
||||
defaultMessage: 'Regnerate {target}',
|
||||
},
|
||||
{ target: `${doc.version}` }
|
||||
)}
|
||||
/>
|
||||
</CheckPermissions>
|
||||
<CheckPermissions permissions={permissions.update}>
|
||||
{doc.version !== data.currentVersion && (
|
||||
<IconButton
|
||||
onClick={() => handleClickDelete(doc.version)}
|
||||
noBorder
|
||||
icon={<DeleteIcon />}
|
||||
label={formatMessage(
|
||||
{
|
||||
id: getTrad('pages.PluginPage.table.icon.delete'),
|
||||
defaultMessage: 'Delete {target}',
|
||||
},
|
||||
{ target: `${doc.version}` }
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
</CheckPermissions>
|
||||
</Row>
|
||||
</Td>
|
||||
</Tr>
|
||||
))}
|
||||
</Tbody>
|
||||
</Table>
|
||||
) : (
|
||||
<EmptyStateLayout />
|
||||
)}
|
||||
</ContentLayout>
|
||||
<ConfirmDialog
|
||||
isConfirmButtonLoading={isConfirmButtonLoading}
|
||||
onConfirm={handleConfirmDelete}
|
||||
onToggleDialog={handleShowConfirmDelete}
|
||||
isOpen={showConfirmDelete}
|
||||
/>
|
||||
</Main>
|
||||
</Layout>
|
||||
);
|
||||
};
|
||||
|
||||
export default PluginPage;
|
||||
File diff suppressed because one or more lines are too long
@ -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;
|
||||
@ -0,0 +1,181 @@
|
||||
import React, { useState } from 'react';
|
||||
import { useIntl } from 'react-intl';
|
||||
import { Formik } from 'formik';
|
||||
import {
|
||||
CheckPermissions,
|
||||
Form,
|
||||
LoadingIndicatorPage,
|
||||
useFocusWhenNavigate,
|
||||
} 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 = () => {
|
||||
useFocusWhenNavigate();
|
||||
const { formatMessage } = useIntl();
|
||||
const { submitMutation, data, isLoading } = useReactQuery();
|
||||
const [passwordShown, setPasswordShown] = useState(false);
|
||||
|
||||
const handleUpdateSettingsSubmit = body => {
|
||||
submitMutation.mutate({
|
||||
prefix: data?.prefix,
|
||||
body,
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<Main>
|
||||
{isLoading ? (
|
||||
<LoadingIndicatorPage>Plugin settings are loading</LoadingIndicatorPage>
|
||||
) : (
|
||||
<Formik
|
||||
initialValues={{
|
||||
restrictedAccess: data?.documentationAccess.restrictedAccess || false,
|
||||
password: '',
|
||||
}}
|
||||
onSubmit={handleUpdateSettingsSubmit}
|
||||
validationSchema={schema}
|
||||
>
|
||||
{({ handleSubmit, values, handleChange, errors, setFieldTouched, setFieldValue }) => {
|
||||
return (
|
||||
<Form noValidate onSubmit={handleSubmit}>
|
||||
<HeaderLayout
|
||||
title={formatMessage({
|
||||
id: getTrad('plugin.name'),
|
||||
defaultMessage: 'Documentation',
|
||||
})}
|
||||
subtitle={formatMessage({
|
||||
id: getTrad('pages.SettingsPage.header.description'),
|
||||
defaultMessage: 'Configure the documentation plugin',
|
||||
})}
|
||||
primaryAction={
|
||||
<CheckPermissions permissions={permissions.update}>
|
||||
<Button type="submit" startIcon={<Check />}>
|
||||
{formatMessage({
|
||||
id: getTrad('pages.SettingsPage.Button.save'),
|
||||
defaultMessage: 'Save',
|
||||
})}
|
||||
</Button>
|
||||
</CheckPermissions>
|
||||
}
|
||||
/>
|
||||
<ContentLayout>
|
||||
<Box
|
||||
background="neutral0"
|
||||
hasRadius
|
||||
shadow="filterShadow"
|
||||
paddingTop={6}
|
||||
paddingBottom={6}
|
||||
paddingLeft={7}
|
||||
paddingRight={7}
|
||||
>
|
||||
<Stack size={4}>
|
||||
<H3 as="h2">
|
||||
{formatMessage({
|
||||
id: getTrad('pages.SettingsPage.title'),
|
||||
defaultMessage: 'Settings',
|
||||
})}
|
||||
</H3>
|
||||
<Grid gap={4}>
|
||||
<GridItem col={6} s={12}>
|
||||
<ToggleInput
|
||||
name="restrictedAccess"
|
||||
label={formatMessage({
|
||||
id: getTrad('pages.SettingsPage.toggle.label'),
|
||||
defaultMessage: 'Restricted Access',
|
||||
})}
|
||||
hint={formatMessage({
|
||||
id: getTrad('pages.SettingsPage.toggle.hint'),
|
||||
defaultMessage: 'Make the documentation endpoint private',
|
||||
})}
|
||||
checked={values.restrictedAccess}
|
||||
onChange={() => {
|
||||
if (values.restrictedAccess === true) {
|
||||
setFieldValue('password', '', false);
|
||||
setFieldTouched('password', false, false);
|
||||
}
|
||||
|
||||
setFieldValue('restrictedAccess', !values.restrictedAccess, false);
|
||||
}}
|
||||
onLabel="On"
|
||||
offLabel="Off"
|
||||
/>
|
||||
</GridItem>
|
||||
{values.restrictedAccess && (
|
||||
<GridItem col={6} s={12}>
|
||||
<TextInput
|
||||
label={formatMessage({
|
||||
id: getTrad('pages.SettingsPage.password.label'),
|
||||
defaultMessage: 'Password',
|
||||
})}
|
||||
name="password"
|
||||
placeholder="**********"
|
||||
type={passwordShown ? 'text' : 'password'}
|
||||
value={values.password}
|
||||
onChange={handleChange}
|
||||
error={
|
||||
errors.password
|
||||
? formatMessage({
|
||||
id: errors.password,
|
||||
defaultMessage: 'Invalid value',
|
||||
})
|
||||
: null
|
||||
}
|
||||
endAction={
|
||||
<FieldActionWrapper
|
||||
onClick={e => {
|
||||
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 ? <Show /> : <Hide />}
|
||||
</FieldActionWrapper>
|
||||
}
|
||||
/>
|
||||
</GridItem>
|
||||
)}
|
||||
</Grid>
|
||||
</Stack>
|
||||
</Box>
|
||||
</ContentLayout>
|
||||
</Form>
|
||||
);
|
||||
}}
|
||||
</Formik>
|
||||
)}
|
||||
</Main>
|
||||
);
|
||||
};
|
||||
|
||||
export default SettingsPage;
|
||||
File diff suppressed because one or more lines are too long
@ -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;
|
||||
@ -1,11 +1,11 @@
|
||||
import { request } from '@strapi/helper-plugin';
|
||||
import pluginId from '../../../pluginId';
|
||||
import pluginId from '../../pluginId';
|
||||
|
||||
const deleteDoc = ({ prefix, version }) => {
|
||||
return request(`${prefix}/deleteDoc/${version}`, { method: 'DELETE' });
|
||||
};
|
||||
|
||||
const fetchData = async toggleNotification => {
|
||||
const fetchDocumentationVersions = async toggleNotification => {
|
||||
try {
|
||||
const data = await request(`/${pluginId}/getInfos`, { method: 'GET' });
|
||||
|
||||
@ -25,6 +25,7 @@ const regenerateDoc = ({ prefix, version }) => {
|
||||
return request(`${prefix}/regenerateDoc`, { method: 'POST', body: { version } });
|
||||
};
|
||||
|
||||
const submit = ({ prefix, body }) => request(`${prefix}/updateSettings`, { method: 'PUT', body });
|
||||
const updateSettings = ({ prefix, body }) =>
|
||||
request(`${prefix}/updateSettings`, { method: 'PUT', body });
|
||||
|
||||
export { deleteDoc, fetchData, regenerateDoc, submit };
|
||||
export { deleteDoc, fetchDocumentationVersions, regenerateDoc, updateSettings };
|
||||
@ -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;
|
||||
@ -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"
|
||||
}
|
||||
}
|
||||
@ -14,12 +14,14 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@strapi/helper-plugin": "3.6.8",
|
||||
"bcryptjs": "2.4.3",
|
||||
"cheerio": "^1.0.0-rc.5",
|
||||
"fs-extra": "^9.1.0",
|
||||
"koa-session": "6.2.0",
|
||||
"koa-static": "^5.0.0",
|
||||
"lodash": "4.17.21",
|
||||
"moment": "^2.29.1",
|
||||
"path-to-regexp": "^3.1.0",
|
||||
"path-to-regexp": "6.2.0",
|
||||
"pluralize": "8.0.0",
|
||||
"react": "^17.0.2",
|
||||
"react-copy-to-clipboard": "^5.0.3",
|
||||
"react-dom": "^17.0.2",
|
||||
|
||||
127
packages/plugins/documentation/server/bootstrap.js
vendored
127
packages/plugins/documentation/server/bootstrap.js
vendored
@ -1,10 +1,6 @@
|
||||
/* eslint-disable no-unreachable */
|
||||
'use strict';
|
||||
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const _ = require('lodash');
|
||||
|
||||
// Add permissions
|
||||
const RBAC_ACTIONS = [
|
||||
{
|
||||
@ -17,123 +13,44 @@ const RBAC_ACTIONS = [
|
||||
section: 'plugins',
|
||||
displayName: 'Update and delete',
|
||||
uid: 'settings.update',
|
||||
subCategory: 'settings',
|
||||
pluginName: 'documentation',
|
||||
},
|
||||
{
|
||||
section: 'plugins',
|
||||
displayName: 'Regenerate',
|
||||
uid: 'settings.regenerate',
|
||||
subCategory: 'settings',
|
||||
pluginName: 'documentation',
|
||||
},
|
||||
{
|
||||
section: 'settings',
|
||||
displayName: 'Access the documentation settings page',
|
||||
uid: 'settings.read',
|
||||
pluginName: 'documentation',
|
||||
category: 'documentation',
|
||||
},
|
||||
];
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {{strapi: import("@strapi/strapi").Strapi}} args
|
||||
*/
|
||||
module.exports = async ({ strapi }) => {
|
||||
await strapi.admin.services.permission.actionProvider.registerMany(RBAC_ACTIONS);
|
||||
|
||||
return;
|
||||
const pluginStore = strapi.store({
|
||||
environment: '',
|
||||
type: 'plugin',
|
||||
name: 'documentation',
|
||||
});
|
||||
|
||||
// Check if the plugin users-permissions is installed because the documentation needs it
|
||||
if (Object.keys(strapi.plugins).indexOf('users-permissions') === -1) {
|
||||
throw new Error(
|
||||
'In order to make the documentation plugin works the users-permissions one is required'
|
||||
);
|
||||
}
|
||||
const config = await pluginStore.get({ key: 'config' });
|
||||
|
||||
const pluginStore = strapi.store({ type: 'plugin', name: 'documentation' });
|
||||
|
||||
const restrictedAccess = await pluginStore.get({ key: 'config' });
|
||||
|
||||
if (!restrictedAccess) {
|
||||
if (!config) {
|
||||
pluginStore.set({ key: 'config', value: { restrictedAccess: false } });
|
||||
}
|
||||
|
||||
let shouldUpdateFullDoc = false;
|
||||
const services = strapi.plugins['documentation'].services.documentation;
|
||||
// Generate plugins' documentation
|
||||
const pluginsWithDocumentationNeeded = services.getPluginsWithDocumentationNeeded();
|
||||
|
||||
pluginsWithDocumentationNeeded.forEach(plugin => {
|
||||
const isDocExisting = services.checkIfPluginDocumentationFolderExists(plugin);
|
||||
|
||||
if (!isDocExisting) {
|
||||
services.createDocumentationDirectory(services.getPluginDocumentationPath(plugin));
|
||||
// create the overrides directory
|
||||
services.createDocumentationDirectory(services.getPluginOverrideDocumentationPath(plugin));
|
||||
services.createPluginDocumentationFile(plugin);
|
||||
shouldUpdateFullDoc = true;
|
||||
} else {
|
||||
const needToUpdatePluginDoc = services.checkIfPluginDocNeedsUpdate(plugin);
|
||||
|
||||
if (needToUpdatePluginDoc) {
|
||||
services.createPluginDocumentationFile(plugin);
|
||||
shouldUpdateFullDoc = true;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Retrieve all the apis from the apis directory
|
||||
const apis = services.getApis();
|
||||
// Generate APIS' documentation
|
||||
apis.forEach(api => {
|
||||
const isDocExisting = services.checkIfDocumentationFolderExists(api);
|
||||
|
||||
if (!isDocExisting) {
|
||||
// If the documentation directory doesn't exist create it
|
||||
services.createDocumentationDirectory(services.getDocumentationPath(api));
|
||||
// Create the overrides directory
|
||||
services.createDocumentationDirectory(services.getDocumentationOverridesPath(api));
|
||||
// Create the documentation files per version
|
||||
services.createDocumentationFile(api); // Then create the {api}.json documentation file
|
||||
shouldUpdateFullDoc = true;
|
||||
} else {
|
||||
const needToUpdateAPIDoc = services.checkIfAPIDocNeedsUpdate(api);
|
||||
|
||||
if (needToUpdateAPIDoc) {
|
||||
services.createDocumentationFile(api);
|
||||
shouldUpdateFullDoc = true;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
const fullDoc = services.generateFullDoc();
|
||||
// Verify that the correct documentation folder exists in the documentation plugin
|
||||
const isMergedDocumentationExists = services.checkIfMergedDocumentationFolderExists();
|
||||
const documentationPath = services.getMergedDocumentationPath();
|
||||
|
||||
if (isMergedDocumentationExists) {
|
||||
/**
|
||||
* Retrieve all tags from the documentation and join them
|
||||
* @param {Object} documentation
|
||||
* @returns {String}
|
||||
*/
|
||||
const getDocTagsToString = documentation => {
|
||||
return _.get(documentation, 'tags', [])
|
||||
.map(tag => {
|
||||
return tag.name.toLowerCase();
|
||||
})
|
||||
.sort((a, b) => a - b)
|
||||
.join('.');
|
||||
};
|
||||
const oldDoc = require(path.resolve(documentationPath, 'full_documentation.json'));
|
||||
const oldDocTags = getDocTagsToString(oldDoc);
|
||||
const currentDocTags = getDocTagsToString(fullDoc);
|
||||
|
||||
// If the tags are different (an api has been deleted) we need to rebuild the documentation
|
||||
if (oldDocTags !== currentDocTags) {
|
||||
shouldUpdateFullDoc = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (!isMergedDocumentationExists || shouldUpdateFullDoc) {
|
||||
// Create the folder
|
||||
services.createDocumentationDirectory(documentationPath);
|
||||
// Write the file
|
||||
fs.writeFileSync(
|
||||
path.resolve(documentationPath, 'full_documentation.json'),
|
||||
JSON.stringify(fullDoc, null, 2),
|
||||
'utf8'
|
||||
);
|
||||
}
|
||||
await strapi
|
||||
.plugin('documentation')
|
||||
.service('documentation')
|
||||
.generateFullDoc();
|
||||
};
|
||||
|
||||
12
packages/plugins/documentation/server/config/default-config.js
Executable file → Normal file
12
packages/plugins/documentation/server/config/default-config.js
Executable file → Normal file
@ -21,10 +21,11 @@ module.exports = {
|
||||
path: '/documentation',
|
||||
showGeneratedFiles: true,
|
||||
generateDefaultResponse: true,
|
||||
plugins: ['email', 'upload'],
|
||||
},
|
||||
servers: [
|
||||
{
|
||||
url: 'http://localhost:1337',
|
||||
url: 'http://localhost:1337/api',
|
||||
description: 'Development server',
|
||||
},
|
||||
{
|
||||
@ -45,4 +46,13 @@ module.exports = {
|
||||
bearerAuth: [],
|
||||
},
|
||||
],
|
||||
components: {
|
||||
securitySchemes: {
|
||||
bearerAuth: {
|
||||
type: 'http',
|
||||
scheme: 'bearer',
|
||||
bearerFormat: 'JWT',
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
@ -1,8 +1,16 @@
|
||||
'use strict';
|
||||
|
||||
const defaultConfig = require('./default-config');
|
||||
const defaultDocumentationConfig = require('./default-config');
|
||||
|
||||
const defaultConfig = {
|
||||
...defaultDocumentationConfig,
|
||||
session: {
|
||||
key: 'plugin::documentation.sess',
|
||||
maxAge: 'session',
|
||||
secretKeys: ['mySecret'],
|
||||
},
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
default: defaultConfig,
|
||||
validator() {},
|
||||
};
|
||||
|
||||
@ -8,6 +8,7 @@
|
||||
|
||||
// Core dependencies.
|
||||
const path = require('path');
|
||||
const bcrypt = require('bcryptjs');
|
||||
|
||||
// Public dependencies.
|
||||
const fs = require('fs-extra');
|
||||
@ -17,15 +18,15 @@ const koaStatic = require('koa-static');
|
||||
module.exports = {
|
||||
async getInfos(ctx) {
|
||||
try {
|
||||
const service = strapi.plugins.documentation.services.documentation;
|
||||
const docVersions = service.retrieveDocumentationVersions();
|
||||
const form = await service.retrieveFrontForm();
|
||||
const docService = strapi.plugin('documentation').service('documentation');
|
||||
const docVersions = docService.getDocumentationVersions();
|
||||
const documentationAccess = await docService.getDocumentationAccess();
|
||||
|
||||
ctx.send({
|
||||
docVersions,
|
||||
currentVersion: service.getDocumentationVersion(),
|
||||
prefix: strapi.config.get('plugin.documentation.x-strapi-config').path,
|
||||
form,
|
||||
currentVersion: docService.getDocumentationVersion(),
|
||||
prefix: strapi.plugin('documentation').config('x-strapi-config').path,
|
||||
documentationAccess,
|
||||
});
|
||||
} catch (err) {
|
||||
ctx.badRequest(null, err.message);
|
||||
@ -42,10 +43,13 @@ 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,
|
||||
'extensions',
|
||||
strapi.dirs.extensions,
|
||||
'documentation',
|
||||
'documentation',
|
||||
version,
|
||||
@ -65,8 +69,7 @@ module.exports = {
|
||||
|
||||
try {
|
||||
const layoutPath = path.resolve(
|
||||
strapi.config.appPath,
|
||||
'extensions',
|
||||
strapi.dirs.extensions,
|
||||
'documentation',
|
||||
'public',
|
||||
'index.html'
|
||||
@ -78,12 +81,7 @@ module.exports = {
|
||||
ctx.url = path.basename(`${ctx.url}/index.html`);
|
||||
|
||||
try {
|
||||
const staticFolder = path.resolve(
|
||||
strapi.config.appPath,
|
||||
'extensions',
|
||||
'documentation',
|
||||
'public'
|
||||
);
|
||||
const staticFolder = path.resolve(strapi.dirs.extensions, 'documentation', 'public');
|
||||
return koaStatic(staticFolder)(ctx, next);
|
||||
} catch (e) {
|
||||
strapi.log.error(e);
|
||||
@ -118,8 +116,7 @@ module.exports = {
|
||||
|
||||
try {
|
||||
const layoutPath = path.resolve(
|
||||
strapi.config.appPath,
|
||||
'extensions',
|
||||
strapi.dirs.extensions,
|
||||
'documentation',
|
||||
'public',
|
||||
'login.html'
|
||||
@ -130,12 +127,7 @@ module.exports = {
|
||||
ctx.url = path.basename(`${ctx.url}/login.html`);
|
||||
|
||||
try {
|
||||
const staticFolder = path.resolve(
|
||||
strapi.config.appPath,
|
||||
'extensions',
|
||||
'documentation',
|
||||
'public'
|
||||
);
|
||||
const staticFolder = path.resolve(strapi.dirs.extensions, 'documentation', 'public');
|
||||
return koaStatic(staticFolder)(ctx, next);
|
||||
} catch (e) {
|
||||
strapi.log.error(e);
|
||||
@ -153,18 +145,19 @@ module.exports = {
|
||||
body: { password },
|
||||
} = ctx.request;
|
||||
|
||||
const { password: storedPassword } = await strapi
|
||||
const { password: hash } = await strapi
|
||||
.store({ type: 'plugin', name: 'documentation', key: 'config' })
|
||||
.get();
|
||||
|
||||
const isValid = await strapi.plugins['users-permissions'].services.user.validatePassword(
|
||||
password,
|
||||
storedPassword
|
||||
);
|
||||
const isValid = await bcrypt.compare(password, hash);
|
||||
|
||||
let querystring = '?error=password';
|
||||
|
||||
if (isValid) {
|
||||
ctx.session.documentation = password;
|
||||
ctx.session.documentation = {
|
||||
logged: true,
|
||||
};
|
||||
|
||||
querystring = '';
|
||||
}
|
||||
|
||||
@ -176,126 +169,71 @@ module.exports = {
|
||||
},
|
||||
|
||||
async regenerateDoc(ctx) {
|
||||
const service = strapi.plugins.documentation.services.documentation;
|
||||
const documentationVersions = service.retrieveDocumentationVersions().map(el => el.version);
|
||||
const {
|
||||
request: {
|
||||
body: { version },
|
||||
admin,
|
||||
},
|
||||
} = ctx;
|
||||
const { version } = ctx.request.body;
|
||||
|
||||
const service = strapi.service('plugin::documentation.documentation');
|
||||
|
||||
const documentationVersions = service.getDocumentationVersions().map(el => el.version);
|
||||
|
||||
if (_.isEmpty(version)) {
|
||||
return ctx.badRequest(
|
||||
null,
|
||||
admin ? 'documentation.error.noVersion' : 'Please provide a version.'
|
||||
);
|
||||
return ctx.badRequest('Please provide a version.');
|
||||
}
|
||||
|
||||
if (!documentationVersions.includes(version)) {
|
||||
return ctx.badRequest(
|
||||
null,
|
||||
admin
|
||||
? 'documentation.error.regenerateDoc.versionMissing'
|
||||
: 'The version you are trying to generate does not exist.'
|
||||
);
|
||||
return ctx.badRequest('The version you are trying to generate does not exist.');
|
||||
}
|
||||
|
||||
try {
|
||||
strapi.reload.isWatching = false;
|
||||
const fullDoc = service.generateFullDoc(version);
|
||||
const documentationPath = service.getMergedDocumentationPath(version);
|
||||
// Write the file
|
||||
fs.writeFileSync(
|
||||
path.resolve(documentationPath, 'full_documentation.json'),
|
||||
JSON.stringify(fullDoc, null, 2),
|
||||
'utf8'
|
||||
);
|
||||
await service.generateFullDoc(version);
|
||||
ctx.send({ ok: true });
|
||||
} catch (err) {
|
||||
ctx.badRequest(null, admin ? 'documentation.error.regenerateDoc' : 'An error occured');
|
||||
} finally {
|
||||
strapi.reload.isWatching = true;
|
||||
}
|
||||
},
|
||||
|
||||
async deleteDoc(ctx) {
|
||||
strapi.reload.isWatching = false;
|
||||
const service = strapi.plugins.documentation.services.documentation;
|
||||
const documentationVersions = service.retrieveDocumentationVersions().map(el => el.version);
|
||||
const { version } = ctx.params;
|
||||
|
||||
const {
|
||||
params: { version },
|
||||
request: { admin },
|
||||
} = ctx;
|
||||
const service = strapi.service('plugin::documentation.documentation');
|
||||
|
||||
const documentationVersions = service.getDocumentationVersions().map(el => el.version);
|
||||
|
||||
if (_.isEmpty(version)) {
|
||||
return ctx.badRequest(
|
||||
null,
|
||||
admin ? 'documentation.error.noVersion' : 'Please provide a version.'
|
||||
);
|
||||
return ctx.badRequest('Please provide a version.');
|
||||
}
|
||||
|
||||
if (!documentationVersions.includes(version)) {
|
||||
return ctx.badRequest(
|
||||
null,
|
||||
admin
|
||||
? 'documentation.error.deleteDoc.versionMissing'
|
||||
: 'The version you are trying to delete does not exist.'
|
||||
);
|
||||
return ctx.badRequest('The version you are trying to delete does not exist.');
|
||||
}
|
||||
|
||||
try {
|
||||
strapi.reload.isWatching = false;
|
||||
await service.deleteDocumentation(version);
|
||||
ctx.send({ ok: true });
|
||||
} catch (err) {
|
||||
ctx.badRequest(null, admin ? 'notification.error' : err.message);
|
||||
} finally {
|
||||
strapi.reload.isWatching = true;
|
||||
}
|
||||
},
|
||||
|
||||
async updateSettings(ctx) {
|
||||
const {
|
||||
admin,
|
||||
body: { restrictedAccess, password },
|
||||
} = ctx.request;
|
||||
|
||||
const usersPermService = strapi.plugins['users-permissions'].services;
|
||||
const { restrictedAccess, password } = ctx.request.body;
|
||||
|
||||
const pluginStore = strapi.store({ type: 'plugin', name: 'documentation' });
|
||||
|
||||
const prevConfig = await pluginStore.get({ key: 'config' });
|
||||
const config = {
|
||||
restrictedAccess: Boolean(restrictedAccess),
|
||||
};
|
||||
|
||||
if (restrictedAccess && _.isEmpty(password)) {
|
||||
return ctx.badRequest(
|
||||
null,
|
||||
admin ? 'users-permissions.Auth.form.error.password.provide' : 'Please provide a password'
|
||||
);
|
||||
if (restrictedAccess) {
|
||||
if (_.isEmpty(password)) {
|
||||
return ctx.badRequest('Please provide a password');
|
||||
}
|
||||
|
||||
config.password = await bcrypt.hash(password, 10);
|
||||
}
|
||||
|
||||
const isNewPassword = !_.isEmpty(password) && password !== prevConfig.password;
|
||||
|
||||
if (isNewPassword && usersPermService.user.isHashed(password)) {
|
||||
// Throw an error if the password selected by the user
|
||||
// contains more than two times the symbol '$'.
|
||||
return ctx.badRequest(
|
||||
null,
|
||||
admin
|
||||
? 'users-permissions.Auth.form.error.password.format'
|
||||
: 'our password cannot contain more than three times the symbol `$`.'
|
||||
);
|
||||
}
|
||||
|
||||
if (isNewPassword) {
|
||||
prevConfig.password = await usersPermService.user.hashPassword({
|
||||
password,
|
||||
});
|
||||
}
|
||||
|
||||
_.set(prevConfig, 'restrictedAccess', restrictedAccess);
|
||||
|
||||
await pluginStore.set({ key: 'config', value: prevConfig });
|
||||
await pluginStore.set({ key: 'config', value: config });
|
||||
|
||||
return ctx.send({ ok: true });
|
||||
},
|
||||
|
||||
17
packages/plugins/documentation/server/index.js
Normal file
17
packages/plugins/documentation/server/index.js
Normal file
@ -0,0 +1,17 @@
|
||||
'use strict';
|
||||
|
||||
const bootstrap = require('./bootstrap');
|
||||
const services = require('./services');
|
||||
const routes = require('./routes');
|
||||
const controllers = require('./controllers');
|
||||
const register = require('./register');
|
||||
const config = require('./config');
|
||||
|
||||
module.exports = () => ({
|
||||
bootstrap,
|
||||
config,
|
||||
routes,
|
||||
controllers,
|
||||
register,
|
||||
services,
|
||||
});
|
||||
@ -1,61 +1,30 @@
|
||||
'use strict';
|
||||
|
||||
const path = require('path');
|
||||
const _ = require('lodash');
|
||||
const koaStatic = require('koa-static');
|
||||
const session = require('koa-session');
|
||||
const swaggerUi = require('swagger-ui-dist');
|
||||
|
||||
const initialRoutes = [];
|
||||
module.exports = async ({ strapi }) => {
|
||||
const sessionConfig = strapi.config.get('plugin.documentation').session;
|
||||
strapi.server.app.keys = sessionConfig.secretKeys;
|
||||
strapi.server.app.use(session(sessionConfig, strapi.server.app));
|
||||
|
||||
// TODO: delete when refactoring documentation plugin for v4
|
||||
module.exports = {
|
||||
defaults: { documentation: { enabled: true } },
|
||||
load: {
|
||||
beforeInitialize() {
|
||||
strapi.config.middleware.load.before.push('documentation');
|
||||
strapi.server.routes([
|
||||
{
|
||||
method: 'GET',
|
||||
path: '/plugins/documentation/(.*)',
|
||||
async handler(ctx, next) {
|
||||
ctx.url = path.basename(ctx.url);
|
||||
|
||||
initialRoutes.push(..._.cloneDeep(strapi.plugins.documentation.routes));
|
||||
return koaStatic(swaggerUi.getAbsoluteFSPath(), {
|
||||
maxage: 86400000,
|
||||
defer: true,
|
||||
})(ctx, next);
|
||||
},
|
||||
config: {
|
||||
auth: false,
|
||||
},
|
||||
},
|
||||
|
||||
initialize() {
|
||||
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;
|
||||
}
|
||||
);
|
||||
|
||||
strapi.server.routes([
|
||||
{
|
||||
method: 'GET',
|
||||
path: '/plugins/documentation/(.*)',
|
||||
async handler(ctx, next) {
|
||||
ctx.url = path.basename(ctx.url);
|
||||
|
||||
return koaStatic(swaggerUi.getAbsoluteFSPath(), {
|
||||
maxage: strapi.config.middleware.settings.public.maxAge,
|
||||
defer: true,
|
||||
})(ctx, next);
|
||||
},
|
||||
},
|
||||
]);
|
||||
},
|
||||
},
|
||||
]);
|
||||
};
|
||||
|
||||
@ -9,7 +9,7 @@ module.exports = async (ctx, next) => {
|
||||
return next();
|
||||
}
|
||||
|
||||
if (!ctx.session.documentation) {
|
||||
if (!ctx.session.documentation || !ctx.session.documentation.logged) {
|
||||
const querystring = ctx.querystring ? `?${ctx.querystring}` : '';
|
||||
|
||||
return ctx.redirect(
|
||||
@ -18,15 +18,7 @@ module.exports = async (ctx, next) => {
|
||||
}/login${querystring}`
|
||||
);
|
||||
}
|
||||
const isValid = await strapi.plugins['users-permissions'].services.user.validatePassword(
|
||||
ctx.session.documentation,
|
||||
config.password
|
||||
);
|
||||
|
||||
if (!isValid) {
|
||||
ctx.session.documentation = null;
|
||||
}
|
||||
|
||||
// Execute the action.
|
||||
await next();
|
||||
return next();
|
||||
};
|
||||
@ -1,7 +0,0 @@
|
||||
'use strict';
|
||||
|
||||
const index = require('./index-policy');
|
||||
|
||||
module.exports = {
|
||||
index,
|
||||
};
|
||||
11
packages/plugins/documentation/server/register.js
Normal file
11
packages/plugins/documentation/server/register.js
Normal file
@ -0,0 +1,11 @@
|
||||
'use strict';
|
||||
|
||||
const registerDocumentationMiddleWare = require('./middlewares/documentation');
|
||||
|
||||
/**
|
||||
* Register upload plugin
|
||||
* @param {{ strapi: import('@strapi/strapi').Strapi }}
|
||||
*/
|
||||
module.exports = async ({ strapi }) => {
|
||||
await registerDocumentationMiddleWare({ strapi });
|
||||
};
|
||||
@ -1,4 +1,5 @@
|
||||
'use strict';
|
||||
const restrictAccess = require('../middlewares/restrict-access');
|
||||
|
||||
module.exports = [
|
||||
{
|
||||
@ -6,9 +7,15 @@ module.exports = [
|
||||
path: '/',
|
||||
handler: 'documentation.index',
|
||||
config: {
|
||||
auth: false,
|
||||
middlewares: [restrictAccess],
|
||||
policies: [
|
||||
'plugin::documentation.index',
|
||||
{ name: 'admin::hasPermissions', config: { actions: ['plugin::documentation.read'] } },
|
||||
{
|
||||
name: 'admin::hasPermissions',
|
||||
config: {
|
||||
actions: ['plugin::documentation.read'],
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
@ -17,9 +24,15 @@ module.exports = [
|
||||
path: '/v:major(\\d+).:minor(\\d+).:patch(\\d+)',
|
||||
handler: 'documentation.index',
|
||||
config: {
|
||||
auth: false,
|
||||
middlewares: [restrictAccess],
|
||||
policies: [
|
||||
'plugin::documentation.index',
|
||||
{ name: 'admin::hasPermissions', config: { actions: ['plugin::documentation.read'] } },
|
||||
{
|
||||
name: 'admin::hasPermissions',
|
||||
config: {
|
||||
actions: ['plugin::documentation.read'],
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
@ -28,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'],
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
@ -38,6 +57,7 @@ module.exports = [
|
||||
path: '/login',
|
||||
handler: 'documentation.login',
|
||||
config: {
|
||||
auth: false,
|
||||
policies: [
|
||||
{ name: 'admin::hasPermissions', config: { actions: ['plugin::documentation.read'] } },
|
||||
],
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@ -1,29 +0,0 @@
|
||||
[
|
||||
[
|
||||
{
|
||||
"name": "restrictedAccess",
|
||||
"label": {
|
||||
"id": "documentation.containers.HomePage.form.restrictedAccess"
|
||||
},
|
||||
"type": "toggle",
|
||||
"inputDescription": {
|
||||
"id": "documentation.containers.HomePage.form.restrictedAccess.inputDescription"
|
||||
},
|
||||
"inputDescriptionStyle": { "marginBottom": "2px"},
|
||||
"value": false
|
||||
},
|
||||
{
|
||||
"name": "password",
|
||||
"label": {
|
||||
"id": "documentation.containers.HomePage.form.password"
|
||||
},
|
||||
"customBootstrapClass": "col-md-4",
|
||||
"type": "password",
|
||||
"inputDescription": {
|
||||
"id": "documentation.containers.HomePage.form.password.inputDescription"
|
||||
},
|
||||
"validations": { "required": true },
|
||||
"value": ""
|
||||
}
|
||||
]
|
||||
]
|
||||
@ -0,0 +1,174 @@
|
||||
'use strict';
|
||||
|
||||
const _ = require('lodash');
|
||||
const pathToRegexp = require('path-to-regexp');
|
||||
|
||||
const queryParams = require('../query-params');
|
||||
const buildApiRequests = require('./build-api-requests');
|
||||
const buildApiResponses = require('./build-api-responses');
|
||||
|
||||
/**
|
||||
* @description Parses a route with ':variable'
|
||||
*
|
||||
* @param {string} routePath - The route's path property
|
||||
* @returns {string}
|
||||
*/
|
||||
const parsePathWithVariables = routePath => {
|
||||
return pathToRegexp
|
||||
.parse(routePath)
|
||||
.map(token => {
|
||||
if (_.isObject(token)) {
|
||||
return token.prefix + '{' + token.name + '}';
|
||||
}
|
||||
|
||||
return token;
|
||||
})
|
||||
.join('');
|
||||
};
|
||||
|
||||
/**
|
||||
* @description Builds the required object for a path parameter
|
||||
*
|
||||
* @param {string} routePath - The route's path property
|
||||
*
|
||||
* @returns {object } Swagger path params object
|
||||
*/
|
||||
const getPathParams = routePath => {
|
||||
return pathToRegexp
|
||||
.parse(routePath)
|
||||
.filter(token => _.isObject(token))
|
||||
.map(param => {
|
||||
return {
|
||||
name: param.name,
|
||||
in: 'path',
|
||||
description: '',
|
||||
deprecated: false,
|
||||
required: true,
|
||||
schema: { type: 'string' },
|
||||
};
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {string} prefix - The route prefix
|
||||
* @param {string} path - The route path
|
||||
*
|
||||
* @returns {string}
|
||||
*/
|
||||
const getPathWithPrefix = (prefix, path) => {
|
||||
if (path.includes('localizations')) {
|
||||
return path;
|
||||
}
|
||||
|
||||
if (path.endsWith('/')) {
|
||||
return prefix;
|
||||
}
|
||||
|
||||
return prefix.concat(path);
|
||||
};
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {object} api - Information about the api
|
||||
* @param {object} api.routeInfo - The routes for a given api or plugin
|
||||
* @param {string} api.routeInfo.prefix - The prefix for all routes
|
||||
* @param {array} api.routeInfo.routes - The routes for the current api
|
||||
* @param {object} api.attributes - The attributes for a given api or plugin
|
||||
* @param {string} api.tag - A descriptor for OpenAPI
|
||||
*
|
||||
* @returns {object}
|
||||
*/
|
||||
const getPaths = ({ routeInfo, attributes, tag }) => {
|
||||
const paths = routeInfo.routes.reduce((acc, route) => {
|
||||
// TODO: Find a more reliable way to determine list of entities vs a single entity
|
||||
const isListOfEntities = route.handler.split('.').pop() === 'find';
|
||||
const methodVerb = route.method.toLowerCase();
|
||||
|
||||
const hasPathParams = route.path.includes('/:');
|
||||
const pathWithPrefix = routeInfo.prefix
|
||||
? getPathWithPrefix(routeInfo.prefix, route.path)
|
||||
: route.path;
|
||||
const routePath = hasPathParams ? parsePathWithVariables(pathWithPrefix) : pathWithPrefix;
|
||||
|
||||
const { responses } = buildApiResponses(attributes, route, isListOfEntities);
|
||||
|
||||
const swaggerConfig = {
|
||||
responses,
|
||||
tags: [_.upperFirst(tag)],
|
||||
parameters: [],
|
||||
requestBody: {},
|
||||
};
|
||||
|
||||
if (isListOfEntities) {
|
||||
swaggerConfig.parameters.push(...queryParams);
|
||||
}
|
||||
|
||||
if (hasPathParams) {
|
||||
const pathParams = getPathParams(route.path);
|
||||
swaggerConfig.parameters.push(...pathParams);
|
||||
}
|
||||
|
||||
if (['post', 'put'].includes(methodVerb)) {
|
||||
const { requestBody } = buildApiRequests(attributes, route);
|
||||
|
||||
swaggerConfig.requestBody = requestBody;
|
||||
}
|
||||
|
||||
_.set(acc, `${routePath}.${methodVerb}`, swaggerConfig);
|
||||
|
||||
return acc;
|
||||
}, {});
|
||||
|
||||
return { paths };
|
||||
};
|
||||
|
||||
/**
|
||||
* @description - Builds the Swagger paths object for each api
|
||||
*
|
||||
* @param {object} api - Information about the current api
|
||||
* @property {string} api.name - The name of the api
|
||||
* @property {string} api.getter - The getter for the api (api | plugin)
|
||||
* @property {array} api.ctNames - The name of all contentTypes found on the api
|
||||
*
|
||||
* @returns {object}
|
||||
*/
|
||||
module.exports = api => {
|
||||
if (!api.ctNames.length && api.getter === 'plugin') {
|
||||
// Set arbitrary attributes
|
||||
const attributes = { foo: { type: 'string' } };
|
||||
const routeInfo = strapi.plugin(api.name).routes['admin'];
|
||||
|
||||
const apiInfo = {
|
||||
routeInfo,
|
||||
attributes,
|
||||
tag: api.name,
|
||||
};
|
||||
|
||||
return getPaths(apiInfo);
|
||||
}
|
||||
|
||||
// An api could have multiple contentTypes
|
||||
for (const contentTypeName of api.ctNames) {
|
||||
// Get the attributes found on the api's contentType
|
||||
const uid = `${api.getter}::${api.name}.${contentTypeName}`;
|
||||
const ct = strapi.contentType(uid);
|
||||
const attributes = ct.attributes;
|
||||
|
||||
// Get the routes for the current api
|
||||
const routeInfo =
|
||||
api.getter === 'plugin'
|
||||
? strapi.plugin(api.name).routes['content-api']
|
||||
: strapi.api[api.name].routes[contentTypeName];
|
||||
|
||||
// Parse an identifier for OpenAPI tag if the api name and contentType name don't match
|
||||
const tag = api.name === contentTypeName ? api.name : `${api.name} - ${contentTypeName}`;
|
||||
const apiInfo = {
|
||||
routeInfo,
|
||||
attributes,
|
||||
tag,
|
||||
};
|
||||
|
||||
return getPaths(apiInfo);
|
||||
}
|
||||
};
|
||||
@ -0,0 +1,41 @@
|
||||
'use strict';
|
||||
|
||||
const cleanSchemaAttributes = require('../clean-schema-attributes');
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {object} attributes - The attributes found on a contentType
|
||||
* @param {object} route - The current route
|
||||
*
|
||||
* @returns The Swagger requestBody
|
||||
*/
|
||||
module.exports = (attributes, route) => {
|
||||
const requiredAttributes = Object.entries(attributes)
|
||||
.filter(([, attribute]) => attribute.required)
|
||||
.map(([attributeName, attribute]) => {
|
||||
return { [attributeName]: attribute };
|
||||
});
|
||||
|
||||
const requestAttributes =
|
||||
route.method === 'POST' && requiredAttributes.length
|
||||
? Object.assign({}, ...requiredAttributes)
|
||||
: attributes;
|
||||
|
||||
return {
|
||||
requestBody: {
|
||||
required: true,
|
||||
content: {
|
||||
'application/json': {
|
||||
schema: {
|
||||
properties: {
|
||||
data: {
|
||||
type: 'object',
|
||||
properties: cleanSchemaAttributes(requestAttributes, { isRequest: true }),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
};
|
||||
@ -0,0 +1,108 @@
|
||||
'use strict';
|
||||
|
||||
const getSchemaData = require('../get-schema-data');
|
||||
const cleanSchemaAttributes = require('../clean-schema-attributes');
|
||||
const errorResponse = require('../error-response');
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {boolean} isSingleEntity - Checks for a single entity
|
||||
* @returns {object} The correctly formatted meta object
|
||||
*/
|
||||
const getMeta = isListOfEntities => {
|
||||
if (isListOfEntities) {
|
||||
return {
|
||||
type: 'object',
|
||||
properties: {
|
||||
pagination: {
|
||||
properties: {
|
||||
page: { type: 'integer' },
|
||||
pageSize: { type: 'integer', minimum: 25 },
|
||||
pageCount: { type: 'integer', maximum: 1 },
|
||||
total: { type: 'integer' },
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
return { type: 'object' };
|
||||
};
|
||||
|
||||
/**
|
||||
* @description - Builds the Swagger response object for a given api
|
||||
*
|
||||
* @param {object} attributes - The attributes found on a contentType
|
||||
* @param {object} route - The current route
|
||||
* @param {boolean} isListOfEntities - Checks for a list of entitities
|
||||
*
|
||||
* @returns The Swagger responses
|
||||
*/
|
||||
module.exports = (attributes, route, isListOfEntities = false) => {
|
||||
let schema;
|
||||
if (route.method === 'DELETE') {
|
||||
schema = {
|
||||
type: 'integer',
|
||||
format: 'int64',
|
||||
};
|
||||
} else {
|
||||
schema = {
|
||||
properties: {
|
||||
data: getSchemaData(isListOfEntities, cleanSchemaAttributes(attributes)),
|
||||
meta: getMeta(isListOfEntities),
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
responses: {
|
||||
'200': {
|
||||
content: {
|
||||
'application/json': {
|
||||
schema,
|
||||
},
|
||||
},
|
||||
},
|
||||
'400': {
|
||||
description: 'Bad Request',
|
||||
content: {
|
||||
'application/json': {
|
||||
schema: errorResponse,
|
||||
},
|
||||
},
|
||||
},
|
||||
'401': {
|
||||
description: 'Unauthorized',
|
||||
content: {
|
||||
'application/json': {
|
||||
schema: errorResponse,
|
||||
},
|
||||
},
|
||||
},
|
||||
'403': {
|
||||
description: 'Forbidden',
|
||||
content: {
|
||||
'application/json': {
|
||||
schema: errorResponse,
|
||||
},
|
||||
},
|
||||
},
|
||||
'404': {
|
||||
description: 'Not Found',
|
||||
content: {
|
||||
'application/json': {
|
||||
schema: errorResponse,
|
||||
},
|
||||
},
|
||||
},
|
||||
'500': {
|
||||
description: 'Internal Server Error',
|
||||
content: {
|
||||
'application/json': {
|
||||
schema: errorResponse,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
};
|
||||
@ -0,0 +1,11 @@
|
||||
'use strict';
|
||||
|
||||
const buildApiResponses = require('./build-api-responses');
|
||||
const buildApiRequests = require('./build-api-requests');
|
||||
const builApiEndpointPath = require('./build-api-endpoint-path');
|
||||
|
||||
module.exports = {
|
||||
buildApiResponses,
|
||||
buildApiRequests,
|
||||
builApiEndpointPath,
|
||||
};
|
||||
@ -0,0 +1,205 @@
|
||||
'use strict';
|
||||
|
||||
const _ = require('lodash');
|
||||
const getSchemaData = require('./get-schema-data');
|
||||
|
||||
/**
|
||||
* @description - Converts types found on attributes to OpenAPI specific data types
|
||||
*
|
||||
* @param {object} attributes - The attributes found on a contentType
|
||||
* @param {{ typeMap: Map, isRequest: boolean }} opts
|
||||
* @returns Attributes using OpenAPI acceptable data types
|
||||
*/
|
||||
|
||||
const cleanSchemaAttributes = (attributes, { typeMap = new Map(), isRequest = false } = {}) => {
|
||||
const attributesCopy = _.cloneDeep(attributes);
|
||||
|
||||
for (const prop in attributesCopy) {
|
||||
const attribute = attributesCopy[prop];
|
||||
if (attribute.default) {
|
||||
delete attributesCopy[prop].default;
|
||||
}
|
||||
|
||||
switch (attribute.type) {
|
||||
case 'password': {
|
||||
if (!isRequest) {
|
||||
delete attributesCopy[prop];
|
||||
break;
|
||||
}
|
||||
|
||||
attributesCopy[prop] = { type: 'string', format: 'password', example: '*******' };
|
||||
break;
|
||||
}
|
||||
case 'email': {
|
||||
attributesCopy[prop] = { type: 'string', format: 'email' };
|
||||
break;
|
||||
}
|
||||
case 'string':
|
||||
case 'text':
|
||||
case 'richtext': {
|
||||
attributesCopy[prop] = { type: 'string' };
|
||||
break;
|
||||
}
|
||||
case 'timestamp': {
|
||||
attributesCopy[prop] = { type: 'string', format: 'timestamp', example: Date.now() };
|
||||
break;
|
||||
}
|
||||
case 'time': {
|
||||
attributesCopy[prop] = { type: 'string', format: 'time', example: '12:54.000' };
|
||||
break;
|
||||
}
|
||||
case 'date': {
|
||||
attributesCopy[prop] = { type: 'string', format: 'date' };
|
||||
break;
|
||||
}
|
||||
case 'datetime': {
|
||||
attributesCopy[prop] = { type: 'string', format: 'date-time' };
|
||||
break;
|
||||
}
|
||||
case 'boolean': {
|
||||
attributesCopy[prop] = { type: 'boolean' };
|
||||
break;
|
||||
}
|
||||
case 'enumeration': {
|
||||
attributesCopy[prop] = { type: 'string', enum: attribute.enum };
|
||||
break;
|
||||
}
|
||||
case 'decimal':
|
||||
case 'float': {
|
||||
attributesCopy[prop] = { type: 'number', format: 'float' };
|
||||
break;
|
||||
}
|
||||
case 'integer': {
|
||||
attributesCopy[prop] = { type: 'integer' };
|
||||
break;
|
||||
}
|
||||
case 'biginteger': {
|
||||
attributesCopy[prop] = { type: 'string', pattern: '^\\d*$', example: '123456789' };
|
||||
break;
|
||||
}
|
||||
case 'json': {
|
||||
attributesCopy[prop] = {};
|
||||
break;
|
||||
}
|
||||
case 'uid': {
|
||||
attributesCopy[prop] = { type: 'string' };
|
||||
break;
|
||||
}
|
||||
case 'component': {
|
||||
const componentAttributes = strapi.components[attribute.component].attributes;
|
||||
|
||||
if (attribute.repeatable) {
|
||||
attributesCopy[prop] = {
|
||||
type: 'array',
|
||||
items: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
...(isRequest ? {} : { id: { type: 'string' } }),
|
||||
...cleanSchemaAttributes(componentAttributes, { typeMap, isRequest }),
|
||||
},
|
||||
},
|
||||
};
|
||||
} else {
|
||||
attributesCopy[prop] = {
|
||||
type: 'object',
|
||||
properties: {
|
||||
...(isRequest ? {} : { id: { type: 'string' } }),
|
||||
...cleanSchemaAttributes(componentAttributes, {
|
||||
typeMap,
|
||||
isRequest,
|
||||
}),
|
||||
},
|
||||
};
|
||||
}
|
||||
break;
|
||||
}
|
||||
case 'dynamiczone': {
|
||||
const components = attribute.components.map(component => {
|
||||
const componentAttributes = strapi.components[component].attributes;
|
||||
return {
|
||||
type: 'object',
|
||||
properties: {
|
||||
...(isRequest ? {} : { id: { type: 'string' } }),
|
||||
__component: { type: 'string' },
|
||||
...cleanSchemaAttributes(componentAttributes, { typeMap, isRequest }),
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
attributesCopy[prop] = {
|
||||
type: 'array',
|
||||
items: {
|
||||
anyOf: components,
|
||||
},
|
||||
};
|
||||
break;
|
||||
}
|
||||
case 'media': {
|
||||
const imageAttributes = strapi.contentType('plugin::upload.file').attributes;
|
||||
const isListOfEntities = attribute.multiple;
|
||||
|
||||
if (isRequest) {
|
||||
const oneOfType = {
|
||||
oneOf: [{ type: 'integer' }, { type: 'string' }],
|
||||
example: 'string or id',
|
||||
};
|
||||
|
||||
attributesCopy[prop] = isListOfEntities ? { type: 'array', items: oneOfType } : oneOfType;
|
||||
break;
|
||||
}
|
||||
|
||||
attributesCopy[prop] = {
|
||||
type: 'object',
|
||||
properties: {
|
||||
data: getSchemaData(isListOfEntities, cleanSchemaAttributes(imageAttributes)),
|
||||
},
|
||||
};
|
||||
break;
|
||||
}
|
||||
|
||||
case 'relation': {
|
||||
const isListOfEntities = attribute.relation.includes('ToMany');
|
||||
|
||||
if (isRequest) {
|
||||
const oneOfType = {
|
||||
oneOf: [{ type: 'integer' }, { type: 'string' }],
|
||||
example: 'string or id',
|
||||
};
|
||||
|
||||
attributesCopy[prop] = isListOfEntities ? { type: 'array', items: oneOfType } : oneOfType;
|
||||
break;
|
||||
}
|
||||
|
||||
if (!attribute.target || typeMap.has(attribute.target)) {
|
||||
attributesCopy[prop] = {
|
||||
type: 'object',
|
||||
properties: { data: getSchemaData(isListOfEntities, {}) },
|
||||
};
|
||||
break;
|
||||
}
|
||||
|
||||
typeMap.set(attribute.target, true);
|
||||
const targetAttributes = strapi.contentType(attribute.target).attributes;
|
||||
|
||||
attributesCopy[prop] = {
|
||||
type: 'object',
|
||||
properties: {
|
||||
data: getSchemaData(
|
||||
isListOfEntities,
|
||||
cleanSchemaAttributes(targetAttributes, { typeMap, isRequest })
|
||||
),
|
||||
},
|
||||
};
|
||||
|
||||
break;
|
||||
}
|
||||
default: {
|
||||
throw new Error(`Invalid type ${attribute.type} while generating open api schema.`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return attributesCopy;
|
||||
};
|
||||
|
||||
module.exports = cleanSchemaAttributes;
|
||||
@ -0,0 +1,22 @@
|
||||
'use strict';
|
||||
|
||||
module.exports = {
|
||||
type: 'object',
|
||||
required: ['error'],
|
||||
properties: {
|
||||
error: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
status: {
|
||||
type: 'integer',
|
||||
},
|
||||
name: {
|
||||
type: 'string',
|
||||
},
|
||||
message: {
|
||||
type: 'string',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
@ -0,0 +1,32 @@
|
||||
'use strict';
|
||||
|
||||
/**
|
||||
* @description Determines the format of the data response
|
||||
*
|
||||
* @param {boolean} isListOfEntities - Checks for a multiple entities
|
||||
* @param {object} attributes - The attributes found on a contentType
|
||||
|
||||
* @returns object | array of attributes
|
||||
*/
|
||||
module.exports = (isListOfEntities, attributes) => {
|
||||
if (isListOfEntities) {
|
||||
return {
|
||||
type: 'array',
|
||||
items: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
id: { type: 'string' },
|
||||
attributes: { type: 'object', properties: attributes },
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
type: 'object',
|
||||
properties: {
|
||||
id: { type: 'string' },
|
||||
attributes: { type: 'object', properties: attributes },
|
||||
},
|
||||
};
|
||||
};
|
||||
84
packages/plugins/documentation/server/utils/query-params.js
Normal file
84
packages/plugins/documentation/server/utils/query-params.js
Normal file
@ -0,0 +1,84 @@
|
||||
'use strict';
|
||||
|
||||
module.exports = [
|
||||
{
|
||||
name: 'sort',
|
||||
in: 'query',
|
||||
description: 'Sort by attributes ascending (asc) or descending (desc)',
|
||||
deprecated: false,
|
||||
required: false,
|
||||
schema: {
|
||||
type: 'string',
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'pagination[withCount]',
|
||||
in: 'query',
|
||||
description: 'Retun page/pageSize (default: true)',
|
||||
deprecated: false,
|
||||
required: false,
|
||||
schema: {
|
||||
type: 'boolean',
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'pagination[page]',
|
||||
in: 'query',
|
||||
description: 'Page number (default: 0)',
|
||||
deprecated: false,
|
||||
required: false,
|
||||
schema: {
|
||||
type: 'integer',
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'pagination[pageSize]',
|
||||
in: 'query',
|
||||
description: 'Page size (default: 25)',
|
||||
deprecated: false,
|
||||
required: false,
|
||||
schema: {
|
||||
type: 'integer',
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'pagination[start]',
|
||||
in: 'query',
|
||||
description: 'Offset value (default: 0)',
|
||||
deprecated: false,
|
||||
required: false,
|
||||
schema: {
|
||||
type: 'integer',
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'pagination[limit]',
|
||||
in: 'query',
|
||||
description: 'Number of entities to return (default: 25)',
|
||||
deprecated: false,
|
||||
required: false,
|
||||
schema: {
|
||||
type: 'integer',
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'fields',
|
||||
in: 'query',
|
||||
description: 'Fields to return (ex: title,author)',
|
||||
deprecated: false,
|
||||
required: false,
|
||||
schema: {
|
||||
type: 'string',
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'populate',
|
||||
in: 'query',
|
||||
description: 'Relations to return',
|
||||
deprecated: false,
|
||||
required: false,
|
||||
schema: {
|
||||
type: 'string',
|
||||
},
|
||||
},
|
||||
];
|
||||
@ -1,21 +1,3 @@
|
||||
'use strict';
|
||||
|
||||
const bootstrap = require('./server/bootstrap');
|
||||
const policies = require('./server/policies');
|
||||
const services = require('./server/services');
|
||||
const routes = require('./server/routes');
|
||||
const controllers = require('./server/controllers');
|
||||
const middlewares = require('./server/middlewares');
|
||||
const config = require('./server/config');
|
||||
|
||||
module.exports = () => {
|
||||
return {
|
||||
bootstrap,
|
||||
config,
|
||||
routes,
|
||||
controllers,
|
||||
middlewares,
|
||||
policies,
|
||||
services,
|
||||
};
|
||||
};
|
||||
module.exports = require('./server');
|
||||
|
||||
@ -17,7 +17,7 @@
|
||||
"@purest/providers": "^1.0.2",
|
||||
"@strapi/helper-plugin": "3.6.8",
|
||||
"@strapi/utils": "3.6.8",
|
||||
"bcryptjs": "^2.4.3",
|
||||
"bcryptjs": "2.4.3",
|
||||
"grant-koa": "5.4.8",
|
||||
"jsonwebtoken": "^8.1.0",
|
||||
"koa2-ratelimit": "^0.9.0",
|
||||
|
||||
17
yarn.lock
17
yarn.lock
@ -7206,7 +7206,7 @@ bcrypt-pbkdf@^1.0.0, bcrypt-pbkdf@^1.0.2:
|
||||
dependencies:
|
||||
tweetnacl "^0.14.3"
|
||||
|
||||
bcryptjs@2.4.3, bcryptjs@^2.4.3:
|
||||
bcryptjs@2.4.3:
|
||||
version "2.4.3"
|
||||
resolved "https://registry.yarnpkg.com/bcryptjs/-/bcryptjs-2.4.3.tgz#9ab5627b93e60621ff7cdac5da9733027df1d0cb"
|
||||
integrity sha1-mrVie5PmBiH/fNrF2pczAn3x0Ms=
|
||||
@ -18476,6 +18476,11 @@ path-to-regexp@0.1.7:
|
||||
resolved "https://registry.yarnpkg.com/path-to-regexp/-/path-to-regexp-0.1.7.tgz#df604178005f522f15eb4490e7247a1bfaa67f8c"
|
||||
integrity sha1-32BBeABfUi8V60SQ5yR6G/qmf4w=
|
||||
|
||||
path-to-regexp@6.2.0, path-to-regexp@^6.1.0:
|
||||
version "6.2.0"
|
||||
resolved "https://registry.yarnpkg.com/path-to-regexp/-/path-to-regexp-6.2.0.tgz#f7b3803336104c346889adece614669230645f38"
|
||||
integrity sha512-f66KywYG6+43afgE/8j/GoiNyygk/bnoCbps++3ErRKsIYkGGupyv07R2Ok5m9i67Iqc+T2g1eAUGUPzWhYTyg==
|
||||
|
||||
path-to-regexp@^1.7.0:
|
||||
version "1.8.0"
|
||||
resolved "https://registry.yarnpkg.com/path-to-regexp/-/path-to-regexp-1.8.0.tgz#887b3ba9d84393e87a0a0b9f4cb756198b53548a"
|
||||
@ -18483,16 +18488,6 @@ path-to-regexp@^1.7.0:
|
||||
dependencies:
|
||||
isarray "0.0.1"
|
||||
|
||||
path-to-regexp@^3.1.0:
|
||||
version "3.2.0"
|
||||
resolved "https://registry.yarnpkg.com/path-to-regexp/-/path-to-regexp-3.2.0.tgz#fa7877ecbc495c601907562222453c43cc204a5f"
|
||||
integrity sha512-jczvQbCUS7XmS7o+y1aEO9OBVFeZBQ1MDSEqmO7xSoPgOPoowY/SxLpZ6Vh97/8qHZOteiCKb7gkG9gA2ZUxJA==
|
||||
|
||||
path-to-regexp@^6.1.0:
|
||||
version "6.2.0"
|
||||
resolved "https://registry.yarnpkg.com/path-to-regexp/-/path-to-regexp-6.2.0.tgz#f7b3803336104c346889adece614669230645f38"
|
||||
integrity sha512-f66KywYG6+43afgE/8j/GoiNyygk/bnoCbps++3ErRKsIYkGGupyv07R2Ok5m9i67Iqc+T2g1eAUGUPzWhYTyg==
|
||||
|
||||
path-type@^1.0.0:
|
||||
version "1.1.0"
|
||||
resolved "https://registry.yarnpkg.com/path-type/-/path-type-1.1.0.tgz#59c44f7ee491da704da415da5a4070ba4f8fe441"
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user