mirror of
https://github.com/Redocly/redoc.git
synced 2025-08-08 06:04:56 +03:00
Merge branch 'master-with-console' of https://github.com/raha1923/redoc into master-with-console
This commit is contained in:
commit
7e25ddec33
21
.github/workflows/unit-tests.yml
vendored
Normal file
21
.github/workflows/unit-tests.yml
vendored
Normal file
|
@ -0,0 +1,21 @@
|
||||||
|
name: Unit Tests
|
||||||
|
|
||||||
|
on: [push]
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build:
|
||||||
|
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v1
|
||||||
|
- name: Use Node.js ${{ matrix.node-version }}
|
||||||
|
uses: actions/setup-node@v1
|
||||||
|
with:
|
||||||
|
node-version: 10.x
|
||||||
|
- name: yarn install, build, and test
|
||||||
|
run: |
|
||||||
|
npm install -g yarn
|
||||||
|
yarn install
|
||||||
|
yarn bundle
|
||||||
|
yarn test
|
33
CHANGELOG.md
33
CHANGELOG.md
|
@ -1,3 +1,36 @@
|
||||||
|
# [2.0.0-rc.16](https://github.com/Redocly/redoc/compare/v2.0.0-rc.15...v2.0.0-rc.16) (2019-09-30)
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* fix scrollYOffset when SSR ([d09c1c1](https://github.com/Redocly/redoc/commit/d09c1c1))
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
# [2.0.0-rc.15](https://github.com/Redocly/redoc/compare/v2.0.0-rc.14...v2.0.0-rc.15) (2019-09-30)
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* auth section appears twice ([5aa7784](https://github.com/Redocly/redoc/commit/5aa7784)), closes [#818](https://github.com/Redocly/redoc/issues/818)
|
||||||
|
* clicking on group title breaks first tag ([4649683](https://github.com/Redocly/redoc/commit/4649683)), closes [#1034](https://github.com/Redocly/redoc/issues/1034)
|
||||||
|
* do not crash on empty scopes ([e787d9e](https://github.com/Redocly/redoc/commit/e787d9e)), closes [#1044](https://github.com/Redocly/redoc/issues/1044)
|
||||||
|
* false-positive recursive detection with allOf at the same level ([faa74d6](https://github.com/Redocly/redoc/commit/faa74d6))
|
||||||
|
* fix scrollYOffset when SSR ([21258a5](https://github.com/Redocly/redoc/commit/21258a5))
|
||||||
|
* left menu item before group is not highligted ([67e2a8f](https://github.com/Redocly/redoc/commit/67e2a8f)), closes [#1033](https://github.com/Redocly/redoc/issues/1033)
|
||||||
|
* remove excessive whitespace between md sections on small screens ([e318fb3](https://github.com/Redocly/redoc/commit/e318fb3)), closes [#874](https://github.com/Redocly/redoc/issues/874)
|
||||||
|
* use url-template dependency ([#1008](https://github.com/Redocly/redoc/issues/1008)) ([32a464a](https://github.com/Redocly/redoc/commit/32a464a)), closes [#1007](https://github.com/Redocly/redoc/issues/1007)
|
||||||
|
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
* **cli:** added support for JSON string value for --options CLI argument ([#1047](https://github.com/Redocly/redoc/issues/1047)) ([2a28130](https://github.com/Redocly/redoc/commit/2a28130)), closes [#797](https://github.com/Redocly/redoc/issues/797)
|
||||||
|
* **cli:** add `disableGoogleFont` parameter to cli ([#1045](https://github.com/Redocly/redoc/issues/1045)) ([aceb343](https://github.com/Redocly/redoc/commit/aceb343))
|
||||||
|
* new option expandDefaultServerVariables ([#1014](https://github.com/Redocly/redoc/issues/1014)) ([0360dce](https://github.com/Redocly/redoc/commit/0360dce))
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
# [2.0.0-rc.14](https://github.com/Redocly/redoc/compare/v2.0.0-rc.13...v2.0.0-rc.14) (2019-08-07)
|
# [2.0.0-rc.14](https://github.com/Redocly/redoc/compare/v2.0.0-rc.13...v2.0.0-rc.14) (2019-08-07)
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -139,7 +139,7 @@ For npm:
|
||||||
|
|
||||||
Install peer dependencies required by ReDoc if you don't have them installed already:
|
Install peer dependencies required by ReDoc if you don't have them installed already:
|
||||||
|
|
||||||
npm i react react-dom mobx@^4.2.0 styled-components
|
npm i react react-dom mobx@^4.2.0 styled-components core-js
|
||||||
|
|
||||||
Import `RedocStandalone` component from 'redoc' module:
|
Import `RedocStandalone` component from 'redoc' module:
|
||||||
|
|
||||||
|
@ -246,6 +246,7 @@ You can use all of the following options with standalone version on <redoc> tag
|
||||||
* `onlyRequiredInSamples` - shows only required fields in request samples.
|
* `onlyRequiredInSamples` - shows only required fields in request samples.
|
||||||
* `jsonSampleExpandLevel` - set the default expand level for JSON payload samples (responses and request body). Special value 'all' expands all levels. The default value is `2`.
|
* `jsonSampleExpandLevel` - set the default expand level for JSON payload samples (responses and request body). Special value 'all' expands all levels. The default value is `2`.
|
||||||
* `menuToggle` - if true clicking second time on expanded menu item will collapse it, default `false`
|
* `menuToggle` - if true clicking second time on expanded menu item will collapse it, default `false`
|
||||||
|
* `expandDefaultServerVariables` - enable expanding default server variables, default `false`
|
||||||
* `theme` - ReDoc theme. Not documented yet. For details check source code: [theme.ts](https://github.com/Redocly/redoc/blob/master/src/theme.ts)
|
* `theme` - ReDoc theme. Not documented yet. For details check source code: [theme.ts](https://github.com/Redocly/redoc/blob/master/src/theme.ts)
|
||||||
|
|
||||||
## Advanced usage of standalone version
|
## Advanced usage of standalone version
|
||||||
|
|
70
cli/index.ts
70
cli/index.ts
|
@ -25,6 +25,7 @@ interface Options {
|
||||||
cdn?: boolean;
|
cdn?: boolean;
|
||||||
output?: string;
|
output?: string;
|
||||||
title?: string;
|
title?: string;
|
||||||
|
disableGoogleFont?: boolean;
|
||||||
port?: number;
|
port?: number;
|
||||||
templateFileName?: string;
|
templateFileName?: string;
|
||||||
templateOptions?: any;
|
templateOptions?: any;
|
||||||
|
@ -68,9 +69,11 @@ YargsParser.command(
|
||||||
watch: argv.watch as boolean,
|
watch: argv.watch as boolean,
|
||||||
templateFileName: argv.template as string,
|
templateFileName: argv.template as string,
|
||||||
templateOptions: argv.templateOptions || {},
|
templateOptions: argv.templateOptions || {},
|
||||||
redocOptions: argv.options || {},
|
redocOptions: getObjectOrJSON(argv.options),
|
||||||
};
|
};
|
||||||
|
|
||||||
|
console.log(config);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await serve(argv.port as number, argv.spec as string, config);
|
await serve(argv.port as number, argv.spec as string, config);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
@ -99,6 +102,12 @@ YargsParser.command(
|
||||||
default: 'ReDoc documentation',
|
default: 'ReDoc documentation',
|
||||||
});
|
});
|
||||||
|
|
||||||
|
yargs.options('disableGoogleFont', {
|
||||||
|
describe: 'Disable Google Font',
|
||||||
|
type: 'boolean',
|
||||||
|
default: false,
|
||||||
|
});
|
||||||
|
|
||||||
yargs.option('cdn', {
|
yargs.option('cdn', {
|
||||||
describe: 'Do not include ReDoc source code into html page, use link to CDN instead',
|
describe: 'Do not include ReDoc source code into html page, use link to CDN instead',
|
||||||
type: 'boolean',
|
type: 'boolean',
|
||||||
|
@ -108,15 +117,16 @@ YargsParser.command(
|
||||||
yargs.demandOption('spec');
|
yargs.demandOption('spec');
|
||||||
return yargs;
|
return yargs;
|
||||||
},
|
},
|
||||||
async argv => {
|
async (argv: any) => {
|
||||||
const config: Options = {
|
const config = {
|
||||||
ssr: true,
|
ssr: true,
|
||||||
output: argv.o as string,
|
output: argv.o as string,
|
||||||
cdn: argv.cdn as boolean,
|
cdn: argv.cdn as boolean,
|
||||||
title: argv.title as string,
|
title: argv.title as string,
|
||||||
|
disableGoogleFont: argv.disableGoogleFont as boolean,
|
||||||
templateFileName: argv.template as string,
|
templateFileName: argv.template as string,
|
||||||
templateOptions: argv.templateOptions || {},
|
templateOptions: argv.templateOptions || {},
|
||||||
redocOptions: argv.options || {},
|
redocOptions: getObjectOrJSON(argv.options),
|
||||||
};
|
};
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
@ -180,21 +190,34 @@ async function serve(port: number, pathToSpec: string, options: Options = {}) {
|
||||||
if (options.watch && existsSync(pathToSpec)) {
|
if (options.watch && existsSync(pathToSpec)) {
|
||||||
const pathToSpecDirectory = resolve(dirname(pathToSpec));
|
const pathToSpecDirectory = resolve(dirname(pathToSpec));
|
||||||
const watchOptions = {
|
const watchOptions = {
|
||||||
ignored: /(^|[\/\\])\../,
|
ignored: [/(^|[\/\\])\../, /___jb_[a-z]+___$/],
|
||||||
|
ignoreInitial: true,
|
||||||
};
|
};
|
||||||
|
|
||||||
const watcher = watch(pathToSpecDirectory, watchOptions);
|
const watcher = watch(pathToSpecDirectory, watchOptions);
|
||||||
const log = console.log.bind(console);
|
const log = console.log.bind(console);
|
||||||
|
|
||||||
|
const handlePath = async path => {
|
||||||
|
try {
|
||||||
|
spec = await loadAndBundleSpec(pathToSpec);
|
||||||
|
pageHTML = await getPageHTML(spec, pathToSpec, options);
|
||||||
|
log('Updated successfully');
|
||||||
|
} catch (e) {
|
||||||
|
console.error('Error while updating: ', e.message);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
watcher
|
watcher
|
||||||
.on('change', async path => {
|
.on('change', async path => {
|
||||||
log(`${path} changed, updating docs`);
|
log(`${path} changed, updating docs`);
|
||||||
try {
|
handlePath(path);
|
||||||
spec = await loadAndBundleSpec(pathToSpec);
|
})
|
||||||
pageHTML = await getPageHTML(spec, pathToSpec, options);
|
.on('add', async path => {
|
||||||
log('Updated successfully');
|
log(`File ${path} added, updating docs`);
|
||||||
} catch (e) {
|
handlePath(path);
|
||||||
console.error('Error while updating: ', e.message);
|
})
|
||||||
}
|
.on('addDir', path => {
|
||||||
|
log(`↗ Directory ${path} added. Files in here will trigger reload.`);
|
||||||
})
|
})
|
||||||
.on('error', error => console.error(`Watcher error: ${error}`))
|
.on('error', error => console.error(`Watcher error: ${error}`))
|
||||||
.on('ready', () => log(`👀 Watching ${pathToSpecDirectory} for changes...`));
|
.on('ready', () => log(`👀 Watching ${pathToSpecDirectory} for changes...`));
|
||||||
|
@ -218,7 +241,15 @@ async function bundle(pathToSpec, options: Options = {}) {
|
||||||
async function getPageHTML(
|
async function getPageHTML(
|
||||||
spec: any,
|
spec: any,
|
||||||
pathToSpec: string,
|
pathToSpec: string,
|
||||||
{ ssr, cdn, title, templateFileName, templateOptions, redocOptions = {} }: Options,
|
{
|
||||||
|
ssr,
|
||||||
|
cdn,
|
||||||
|
title,
|
||||||
|
disableGoogleFont,
|
||||||
|
templateFileName,
|
||||||
|
templateOptions,
|
||||||
|
redocOptions = {},
|
||||||
|
}: Options,
|
||||||
) {
|
) {
|
||||||
let html;
|
let html;
|
||||||
let css;
|
let css;
|
||||||
|
@ -261,6 +292,7 @@ async function getPageHTML(
|
||||||
: `<script>${redocStandaloneSrc}</script>`) + css
|
: `<script>${redocStandaloneSrc}</script>`) + css
|
||||||
: '<script src="redoc.standalone.js"></script>',
|
: '<script src="redoc.standalone.js"></script>',
|
||||||
title,
|
title,
|
||||||
|
disableGoogleFont,
|
||||||
templateOptions,
|
templateOptions,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -323,3 +355,15 @@ function handleError(error: Error) {
|
||||||
console.error(error.stack);
|
console.error(error.stack);
|
||||||
process.exit(1);
|
process.exit(1);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function getObjectOrJSON(options) {
|
||||||
|
try {
|
||||||
|
return options && typeof options === 'string'
|
||||||
|
? JSON.parse(options) : options
|
||||||
|
? options
|
||||||
|
: {};
|
||||||
|
} catch (e) {
|
||||||
|
console.log(`Encountered error:\n${options}\nis not a valid JSON.`);
|
||||||
|
handleError(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "redoc-cli",
|
"name": "redoc-cli",
|
||||||
"version": "0.8.6",
|
"version": "0.9.2",
|
||||||
"description": "ReDoc's Command Line Interface",
|
"description": "ReDoc's Command Line Interface",
|
||||||
"main": "index.js",
|
"main": "index.js",
|
||||||
"bin": "index.js",
|
"bin": "index.js",
|
||||||
|
@ -19,7 +19,7 @@
|
||||||
"node-libs-browser": "^2.2.1",
|
"node-libs-browser": "^2.2.1",
|
||||||
"react": "^16.8.6",
|
"react": "^16.8.6",
|
||||||
"react-dom": "^16.8.6",
|
"react-dom": "^16.8.6",
|
||||||
"redoc": "2.0.0-rc.13",
|
"redoc": "2.0.0-rc.16",
|
||||||
"styled-components": "^4.3.2",
|
"styled-components": "^4.3.2",
|
||||||
"tslib": "^1.10.0",
|
"tslib": "^1.10.0",
|
||||||
"yargs": "^13.3.0"
|
"yargs": "^13.3.0"
|
||||||
|
|
|
@ -13,7 +13,7 @@
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
{{{redocHead}}}
|
{{{redocHead}}}
|
||||||
<link href="https://fonts.googleapis.com/css?family=Montserrat:300,400,700|Roboto:300,400,700" rel="stylesheet">
|
{{#unless disableGoogleFont}}<link href="https://fonts.googleapis.com/css?family=Montserrat:300,400,700|Roboto:300,400,700" rel="stylesheet">{{/unless}}
|
||||||
<link href="https://cdn.fontcdn.ir/Font/Persian/Vazir/Vazir.css" rel="stylesheet">
|
<link href="https://cdn.fontcdn.ir/Font/Persian/Vazir/Vazir.css" rel="stylesheet">
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
|
|
|
@ -629,10 +629,10 @@ domain-browser@^1.1.1:
|
||||||
resolved "https://registry.yarnpkg.com/domain-browser/-/domain-browser-1.2.0.tgz#3d31f50191a6749dd1375a7f522e823d42e54eda"
|
resolved "https://registry.yarnpkg.com/domain-browser/-/domain-browser-1.2.0.tgz#3d31f50191a6749dd1375a7f522e823d42e54eda"
|
||||||
integrity sha512-jnjyiM6eRyZl2H+W8Q/zLMA481hzi0eszAaBUzIVnmYVDBbnLxVNnfu1HgEBvCbL+71FrxMl3E6lpKH7Ge3OXA==
|
integrity sha512-jnjyiM6eRyZl2H+W8Q/zLMA481hzi0eszAaBUzIVnmYVDBbnLxVNnfu1HgEBvCbL+71FrxMl3E6lpKH7Ge3OXA==
|
||||||
|
|
||||||
dompurify@^1.0.11:
|
dompurify@^2.0.3:
|
||||||
version "1.0.11"
|
version "2.0.3"
|
||||||
resolved "https://registry.yarnpkg.com/dompurify/-/dompurify-1.0.11.tgz#fe0f4a40d147f7cebbe31a50a1357539cfc1eb4d"
|
resolved "https://registry.yarnpkg.com/dompurify/-/dompurify-2.0.3.tgz#5cc4965a487d54aedba6ba9634b137cfbd7eb50d"
|
||||||
integrity sha512-XywCTXZtc/qCX3iprD1pIklRVk/uhl8BKpkTxr+ZyMVUzSUg7wkQXRBp/euJ5J5moa1QvfpvaPQVP71z1O59dQ==
|
integrity sha512-q006uOkD2JGSJgF0qBt7rVhUvUPBWCxpGayALmHvXx2iNlMfNVz7PDGeXEUjNGgIDjADz59VZCv6UE3U8XRWVw==
|
||||||
|
|
||||||
elliptic@^6.0.0:
|
elliptic@^6.0.0:
|
||||||
version "6.5.0"
|
version "6.5.0"
|
||||||
|
@ -1092,7 +1092,7 @@ mem@^4.0.0:
|
||||||
mimic-fn "^2.0.0"
|
mimic-fn "^2.0.0"
|
||||||
p-is-promise "^2.0.0"
|
p-is-promise "^2.0.0"
|
||||||
|
|
||||||
memoize-one@^5.0.0, memoize-one@^5.0.5:
|
memoize-one@^5.0.0, memoize-one@~5.0.5:
|
||||||
version "5.0.5"
|
version "5.0.5"
|
||||||
resolved "https://registry.yarnpkg.com/memoize-one/-/memoize-one-5.0.5.tgz#8cd3809555723a07684afafcd6f756072ac75d7e"
|
resolved "https://registry.yarnpkg.com/memoize-one/-/memoize-one-5.0.5.tgz#8cd3809555723a07684afafcd6f756072ac75d7e"
|
||||||
integrity sha512-ey6EpYv0tEaIbM/nTDOpHciXUvd+ackQrJgEzBwemhZZIWZjcyodqEcrmqDy2BKRTM3a65kKBV4WtLXJDt26SQ==
|
integrity sha512-ey6EpYv0tEaIbM/nTDOpHciXUvd+ackQrJgEzBwemhZZIWZjcyodqEcrmqDy2BKRTM3a65kKBV4WtLXJDt26SQ==
|
||||||
|
@ -1161,10 +1161,10 @@ mobx-react-lite@1.4.0:
|
||||||
resolved "https://registry.yarnpkg.com/mobx-react-lite/-/mobx-react-lite-1.4.0.tgz#193beb5fdddf17ae61542f65ff951d84db402351"
|
resolved "https://registry.yarnpkg.com/mobx-react-lite/-/mobx-react-lite-1.4.0.tgz#193beb5fdddf17ae61542f65ff951d84db402351"
|
||||||
integrity sha512-5xCuus+QITQpzKOjAOIQ/YxNhOl/En+PlNJF+5QU4Qxn9gnNMJBbweAdEW3HnuVQbfqDYEUnkGs5hmkIIStehg==
|
integrity sha512-5xCuus+QITQpzKOjAOIQ/YxNhOl/En+PlNJF+5QU4Qxn9gnNMJBbweAdEW3HnuVQbfqDYEUnkGs5hmkIIStehg==
|
||||||
|
|
||||||
mobx-react@^6.1.1:
|
mobx-react@^6.1.3:
|
||||||
version "6.1.1"
|
version "6.1.3"
|
||||||
resolved "https://registry.yarnpkg.com/mobx-react/-/mobx-react-6.1.1.tgz#24a2c8a3393890fa732b4efd34cc6dcccf6e0e7a"
|
resolved "https://registry.yarnpkg.com/mobx-react/-/mobx-react-6.1.3.tgz#ad07880ea60cdcdb2a7e2a0d54e01379710cf00a"
|
||||||
integrity sha512-hjACWCTpxZf9Sv1YgWF/r6HS6Nsly1SYF22qBJeUE3j+FMfoptgjf8Zmcx2d6uzA07Cezwap5Cobq9QYa0MKUw==
|
integrity sha512-eT/jO9dYIoB1AlZwI2VC3iX0gPOeOIqZsiwg7tDJV1B7Z69h+TZZL3dgOE0UeS2zoHhGeKbP+K+OLeLMnnkGnA==
|
||||||
dependencies:
|
dependencies:
|
||||||
mobx-react-lite "1.4.0"
|
mobx-react-lite "1.4.0"
|
||||||
|
|
||||||
|
@ -1540,10 +1540,10 @@ react-dropdown@^1.6.4:
|
||||||
dependencies:
|
dependencies:
|
||||||
classnames "^2.2.3"
|
classnames "^2.2.3"
|
||||||
|
|
||||||
react-hot-loader@^4.12.10:
|
react-hot-loader@^4.12.14:
|
||||||
version "4.12.10"
|
version "4.12.14"
|
||||||
resolved "https://registry.yarnpkg.com/react-hot-loader/-/react-hot-loader-4.12.10.tgz#b3457c0f733423c4827c6d2672e50c9f8bedaf6b"
|
resolved "https://registry.yarnpkg.com/react-hot-loader/-/react-hot-loader-4.12.14.tgz#81ca06ffda0b90aad15d6069339f73ed6428340a"
|
||||||
integrity sha512-dX+ZUigxQijWLsKPnxc0khuCt2sYiZ1W59LgSBMOLeGSG3+HkknrTlnJu6BCNdhYxbEQkGvBsr7zXlNWYUIhAQ==
|
integrity sha512-ecxH4eBvEaJ9onT8vkEmK1FAAJUh1PqzGqds9S3k+GeihSp7nKAp4fOxytO+Ghr491LiBD38jaKyDXYnnpI9pQ==
|
||||||
dependencies:
|
dependencies:
|
||||||
fast-levenshtein "^2.0.6"
|
fast-levenshtein "^2.0.6"
|
||||||
global "^4.3.0"
|
global "^4.3.0"
|
||||||
|
@ -1602,35 +1602,35 @@ readdirp@^3.1.1:
|
||||||
dependencies:
|
dependencies:
|
||||||
picomatch "^2.0.4"
|
picomatch "^2.0.4"
|
||||||
|
|
||||||
redoc@2.0.0-rc.13:
|
redoc@2.0.0-rc.16:
|
||||||
version "2.0.0-rc.13"
|
version "2.0.0-rc.16"
|
||||||
resolved "https://registry.yarnpkg.com/redoc/-/redoc-2.0.0-rc.13.tgz#243e4d003ca9bd45006c215d8856a3b1229ca8bb"
|
resolved "https://registry.yarnpkg.com/redoc/-/redoc-2.0.0-rc.16.tgz#01d5dafba6ae266a5934dc9904b87bc8a175b222"
|
||||||
integrity sha512-t0vlss1TIUknYXTI9RIZ1nRMyIW/pjo4KMMDFOMdRq5/8jopkNyf37q25BwBuAJfDxQV+tIUoy6o+rAAffeDkQ==
|
integrity sha512-5YWk7NBebYZ8xMbKXA1sD++QsSh7NbnB2sStJRKLeP/rU4oX586SIqHXl+MW1OhIZW44mYFMHpYzxpZKCllk9w==
|
||||||
dependencies:
|
dependencies:
|
||||||
classnames "^2.2.6"
|
classnames "^2.2.6"
|
||||||
decko "^1.2.0"
|
decko "^1.2.0"
|
||||||
dompurify "^1.0.11"
|
dompurify "^2.0.3"
|
||||||
eventemitter3 "^4.0.0"
|
eventemitter3 "^4.0.0"
|
||||||
json-pointer "^0.6.0"
|
json-pointer "^0.6.0"
|
||||||
json-schema-ref-parser "^6.1.0"
|
json-schema-ref-parser "^6.1.0"
|
||||||
lunr "2.3.6"
|
lunr "2.3.6"
|
||||||
mark.js "^8.11.1"
|
mark.js "^8.11.1"
|
||||||
marked "^0.7.0"
|
marked "^0.7.0"
|
||||||
memoize-one "^5.0.5"
|
memoize-one "~5.0.5"
|
||||||
mobx-react "^6.1.1"
|
mobx-react "^6.1.3"
|
||||||
openapi-sampler "1.0.0-beta.15"
|
openapi-sampler "1.0.0-beta.15"
|
||||||
perfect-scrollbar "^1.4.0"
|
perfect-scrollbar "^1.4.0"
|
||||||
polished "^3.4.1"
|
polished "^3.4.1"
|
||||||
prismjs "^1.17.1"
|
prismjs "^1.17.1"
|
||||||
prop-types "^15.7.2"
|
prop-types "^15.7.2"
|
||||||
react-dropdown "^1.6.4"
|
react-dropdown "^1.6.4"
|
||||||
react-hot-loader "^4.12.10"
|
react-hot-loader "^4.12.14"
|
||||||
react-tabs "^3.0.0"
|
react-tabs "^3.0.0"
|
||||||
slugify "^1.3.4"
|
slugify "^1.3.5"
|
||||||
stickyfill "^1.1.1"
|
stickyfill "^1.1.1"
|
||||||
swagger2openapi "^5.3.1"
|
swagger2openapi "^5.3.1"
|
||||||
tslib "^1.10.0"
|
tslib "^1.10.0"
|
||||||
uri-template-lite "^19.4.0"
|
url-template "^2.0.8"
|
||||||
|
|
||||||
reftools@^1.0.8:
|
reftools@^1.0.8:
|
||||||
version "1.0.8"
|
version "1.0.8"
|
||||||
|
@ -1782,10 +1782,10 @@ signal-exit@^3.0.0:
|
||||||
resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-3.0.2.tgz#b5fdc08f1287ea1178628e415e25132b73646c6d"
|
resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-3.0.2.tgz#b5fdc08f1287ea1178628e415e25132b73646c6d"
|
||||||
integrity sha1-tf3AjxKH6hF4Yo5BXiUTK3NkbG0=
|
integrity sha1-tf3AjxKH6hF4Yo5BXiUTK3NkbG0=
|
||||||
|
|
||||||
slugify@^1.3.4:
|
slugify@^1.3.5:
|
||||||
version "1.3.4"
|
version "1.3.5"
|
||||||
resolved "https://registry.yarnpkg.com/slugify/-/slugify-1.3.4.tgz#78d2792d7222b55cd9fc81fa018df99af779efeb"
|
resolved "https://registry.yarnpkg.com/slugify/-/slugify-1.3.5.tgz#90210678818b6d533cb060083aed0e8238133508"
|
||||||
integrity sha512-KP0ZYk5hJNBS8/eIjGkFDCzGQIoZ1mnfQRYS5WM3273z+fxGWXeN0fkwf2ebEweydv9tioZIHGZKoF21U07/nw==
|
integrity sha512-5VCnH7aS13b0UqWOs7Ef3E5rkhFe8Od+cp7wybFv5mv/sYSRkucZlJX0bamAJky7b2TTtGvrJBWVdpdEicsSrA==
|
||||||
|
|
||||||
source-map@^0.5.0:
|
source-map@^0.5.0:
|
||||||
version "0.5.7"
|
version "0.5.7"
|
||||||
|
@ -2002,10 +2002,10 @@ uglify-js@^3.1.4:
|
||||||
commander "~2.20.0"
|
commander "~2.20.0"
|
||||||
source-map "~0.6.1"
|
source-map "~0.6.1"
|
||||||
|
|
||||||
uri-template-lite@^19.4.0:
|
url-template@^2.0.8:
|
||||||
version "19.4.0"
|
version "2.0.8"
|
||||||
resolved "https://registry.yarnpkg.com/uri-template-lite/-/uri-template-lite-19.4.0.tgz#cbc2c072cf4931428a2f9d3aea36b8254a33cce5"
|
resolved "https://registry.yarnpkg.com/url-template/-/url-template-2.0.8.tgz#fc565a3cccbff7730c775f5641f9555791439f21"
|
||||||
integrity sha512-VY8dgwyMwnCztkzhq0cA/YhNmO+YZqow//5FdmgE2fZU/JPi+U0rPL7MRDi0F+Ch4vJ7nYidWzeWAeY7uywe9g==
|
integrity sha1-/FZaPMy/93MMd19WQflVV5FDnyE=
|
||||||
|
|
||||||
url@^0.11.0:
|
url@^0.11.0:
|
||||||
version "0.11.0"
|
version "0.11.0"
|
||||||
|
|
72
package.json
72
package.json
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "redoc",
|
"name": "redoc",
|
||||||
"version": "2.0.0-rc.14",
|
"version": "2.0.0-rc.16",
|
||||||
"description": "ReDoc",
|
"description": "ReDoc",
|
||||||
"repository": {
|
"repository": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
|
@ -52,20 +52,20 @@
|
||||||
"docker:build": "docker build -f config/docker/Dockerfile -t redoc ."
|
"docker:build": "docker build -f config/docker/Dockerfile -t redoc ."
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@babel/core": "7.5.5",
|
"@babel/core": "7.6.2",
|
||||||
"@babel/plugin-syntax-decorators": "7.2.0",
|
"@babel/plugin-syntax-decorators": "7.2.0",
|
||||||
"@babel/plugin-syntax-dynamic-import": "^7.2.0",
|
"@babel/plugin-syntax-dynamic-import": "^7.2.0",
|
||||||
"@babel/plugin-syntax-jsx": "7.2.0",
|
"@babel/plugin-syntax-jsx": "7.2.0",
|
||||||
"@babel/plugin-syntax-typescript": "7.3.3",
|
"@babel/plugin-syntax-typescript": "7.3.3",
|
||||||
"@cypress/webpack-preprocessor": "4.1.0",
|
"@cypress/webpack-preprocessor": "4.1.0",
|
||||||
"@hot-loader/react-dom": "^16.8.6",
|
"@hot-loader/react-dom": "^16.9.0",
|
||||||
"@types/chai": "4.1.7",
|
"@types/chai": "4.2.3",
|
||||||
"@types/dompurify": "^0.0.33",
|
"@types/dompurify": "^0.0.33",
|
||||||
"@types/enzyme": "^3.10.3",
|
"@types/enzyme": "^3.10.3",
|
||||||
"@types/enzyme-to-json": "^1.5.3",
|
"@types/enzyme-to-json": "^1.5.3",
|
||||||
"@types/jest": "^24.0.15",
|
"@types/jest": "^24.0.18",
|
||||||
"@types/json-pointer": "^1.0.30",
|
"@types/json-pointer": "^1.0.30",
|
||||||
"@types/lodash": "^4.14.136",
|
"@types/lodash": "^4.14.141",
|
||||||
"@types/lunr": "^2.3.2",
|
"@types/lunr": "^2.3.2",
|
||||||
"@types/mark.js": "^8.11.4",
|
"@types/mark.js": "^8.11.4",
|
||||||
"@types/marked": "^0.6.5",
|
"@types/marked": "^0.6.5",
|
||||||
|
@ -77,54 +77,54 @@
|
||||||
"@types/react-dom": "^16.8.5",
|
"@types/react-dom": "^16.8.5",
|
||||||
"@types/react-hot-loader": "^4.1.0",
|
"@types/react-hot-loader": "^4.1.0",
|
||||||
"@types/react-tabs": "^2.3.1",
|
"@types/react-tabs": "^2.3.1",
|
||||||
"@types/styled-components": "^4.1.18",
|
"@types/styled-components": "^4.1.19",
|
||||||
"@types/tapable": "1.0.4",
|
"@types/tapable": "1.0.4",
|
||||||
"@types/webpack": "^4.32.1",
|
"@types/webpack": "^4.39.2",
|
||||||
"@types/webpack-env": "^1.14.0",
|
"@types/webpack-env": "^1.14.0",
|
||||||
"@types/yargs": "^13.0.0",
|
"@types/yargs": "^13.0.3",
|
||||||
"babel-loader": "8.0.6",
|
"babel-loader": "8.0.6",
|
||||||
"babel-plugin-styled-components": "^1.10.6",
|
"babel-plugin-styled-components": "^1.10.6",
|
||||||
"beautify-benchmark": "^0.2.4",
|
"beautify-benchmark": "^0.2.4",
|
||||||
"bundlesize": "^0.18.0",
|
"bundlesize": "^0.18.0",
|
||||||
"conventional-changelog-cli": "^2.0.23",
|
"conventional-changelog-cli": "^2.0.23",
|
||||||
"copy-webpack-plugin": "^5.0.4",
|
"copy-webpack-plugin": "^5.0.4",
|
||||||
"core-js": "^3.1.4",
|
"core-js": "^3.2.1",
|
||||||
"coveralls": "^3.0.5",
|
"coveralls": "^3.0.6",
|
||||||
"css-loader": "^3.1.0",
|
"css-loader": "^3.2.0",
|
||||||
"cypress": "~3.4.0",
|
"cypress": "~3.4.1",
|
||||||
"deploy-to-gh-pages": "^1.3.7",
|
"deploy-to-gh-pages": "^1.3.7",
|
||||||
"enzyme": "^3.10.0",
|
"enzyme": "^3.10.0",
|
||||||
"enzyme-adapter-react-16": "^1.14.0",
|
"enzyme-adapter-react-16": "^1.14.0",
|
||||||
"enzyme-to-json": "^3.3.5",
|
"enzyme-to-json": "^3.4.0",
|
||||||
"fork-ts-checker-webpack-plugin": "1.4.3",
|
"fork-ts-checker-webpack-plugin": "1.5.0",
|
||||||
"html-webpack-plugin": "^3.1.0",
|
"html-webpack-plugin": "^3.1.0",
|
||||||
"jest": "^24.8.0",
|
"jest": "^24.9.0",
|
||||||
"license-checker": "^25.0.1",
|
"license-checker": "^25.0.1",
|
||||||
"lodash": "^4.17.15",
|
"lodash": "^4.17.15",
|
||||||
"mobx": "^4.3.1",
|
"mobx": "^4.3.1",
|
||||||
"prettier": "^1.18.2",
|
"prettier": "^1.18.2",
|
||||||
"prettier-eslint": "^9.0.0",
|
"prettier-eslint": "^9.0.0",
|
||||||
"raf": "^3.4.1",
|
"raf": "^3.4.1",
|
||||||
"react": "^16.8.6",
|
"react": "^16.10.1",
|
||||||
"react-dom": "^16.8.6",
|
"react-dom": "^16.10.1",
|
||||||
"rimraf": "^2.6.3",
|
"rimraf": "^3.0.0",
|
||||||
"shelljs": "^0.8.3",
|
"shelljs": "^0.8.3",
|
||||||
"source-map-loader": "^0.2.4",
|
"source-map-loader": "^0.2.4",
|
||||||
"style-loader": "^0.23.1",
|
"style-loader": "^1.0.0",
|
||||||
"styled-components": "^4.3.2",
|
"styled-components": "^4.4.0",
|
||||||
"ts-jest": "24.0.2",
|
"ts-jest": "24.1.0",
|
||||||
"ts-loader": "6.0.4",
|
"ts-loader": "6.2.0",
|
||||||
"ts-node": "^8.3.0",
|
"ts-node": "^8.4.1",
|
||||||
"tslint": "^5.18.0",
|
"tslint": "^5.20.0",
|
||||||
"tslint-react": "^4.0.0",
|
"tslint-react": "^4.1.0",
|
||||||
"typescript": "^3.5.3",
|
"typescript": "^3.6.3",
|
||||||
"unfetch": "^4.1.0",
|
"unfetch": "^4.1.0",
|
||||||
"url-polyfill": "^1.1.7",
|
"url-polyfill": "^1.1.7",
|
||||||
"webpack": "^4.38.0",
|
"webpack": "^4.41.0",
|
||||||
"webpack-cli": "^3.3.6",
|
"webpack-cli": "^3.3.9",
|
||||||
"webpack-dev-server": "^3.7.2",
|
"webpack-dev-server": "^3.8.1",
|
||||||
"webpack-node-externals": "^1.6.0",
|
"webpack-node-externals": "^1.6.0",
|
||||||
"workerize-loader": "^1.0.4",
|
"workerize-loader": "^1.1.0",
|
||||||
"yaml-js": "^0.2.3"
|
"yaml-js": "^0.2.3"
|
||||||
},
|
},
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
|
@ -140,15 +140,15 @@
|
||||||
"brace": "^0.11.1",
|
"brace": "^0.11.1",
|
||||||
"classnames": "^2.2.6",
|
"classnames": "^2.2.6",
|
||||||
"decko": "^1.2.0",
|
"decko": "^1.2.0",
|
||||||
"dompurify": "^1.0.11",
|
"dompurify": "^2.0.3",
|
||||||
"eventemitter3": "^4.0.0",
|
"eventemitter3": "^4.0.0",
|
||||||
"json-pointer": "^0.6.0",
|
"json-pointer": "^0.6.0",
|
||||||
"json-schema-ref-parser": "^6.1.0",
|
"json-schema-ref-parser": "^6.1.0",
|
||||||
"lunr": "2.3.6",
|
"lunr": "2.3.6",
|
||||||
"mark.js": "^8.11.1",
|
"mark.js": "^8.11.1",
|
||||||
"marked": "^0.7.0",
|
"marked": "^0.7.0",
|
||||||
"memoize-one": "^5.0.5",
|
"memoize-one": "~5.0.5",
|
||||||
"mobx-react": "^6.1.1",
|
"mobx-react": "^6.1.3",
|
||||||
"openapi-sampler": "1.0.0-beta.15",
|
"openapi-sampler": "1.0.0-beta.15",
|
||||||
"perfect-scrollbar": "^1.4.0",
|
"perfect-scrollbar": "^1.4.0",
|
||||||
"polished": "^3.4.1",
|
"polished": "^3.4.1",
|
||||||
|
@ -160,11 +160,11 @@
|
||||||
"react-hot-loader": "^4.12.10",
|
"react-hot-loader": "^4.12.10",
|
||||||
"react-switch": "^5.0.1",
|
"react-switch": "^5.0.1",
|
||||||
"react-tabs": "^3.0.0",
|
"react-tabs": "^3.0.0",
|
||||||
"slugify": "^1.3.4",
|
"slugify": "^1.3.5",
|
||||||
"stickyfill": "^1.1.1",
|
"stickyfill": "^1.1.1",
|
||||||
"swagger2openapi": "^5.3.1",
|
"swagger2openapi": "^5.3.1",
|
||||||
"tslib": "^1.10.0",
|
"tslib": "^1.10.0",
|
||||||
"uri-template-lite": "^19.4.0"
|
"url-template": "^2.0.8"
|
||||||
},
|
},
|
||||||
"bundlesize": [
|
"bundlesize": [
|
||||||
{
|
{
|
||||||
|
|
|
@ -1,16 +1,18 @@
|
||||||
import { SECTION_ATTR } from '../services/MenuStore';
|
import { SECTION_ATTR } from '../services/MenuStore';
|
||||||
import styled, { media } from '../styled-components';
|
import styled, { media } from '../styled-components';
|
||||||
|
|
||||||
export const MiddlePanel = styled.div`
|
export const MiddlePanel = styled.div<{ compact?: boolean }>`
|
||||||
width: calc(100% - ${props => props.theme.rightPanel.width});
|
width: calc(100% - ${props => props.theme.rightPanel.width});
|
||||||
padding: 0 ${props => props.theme.spacing.sectionHorizontal}px;
|
padding: 0 ${props => props.theme.spacing.sectionHorizontal}px;
|
||||||
direction: ${props => props.theme.typography.direction || 'ltr'};
|
direction: ${props => props.theme.typography.direction || 'ltr'};
|
||||||
text-align: ${props => (props.theme.typography.direction === 'rtl') ? 'right' : 'inherit'};
|
text-align: ${props => (props.theme.typography.direction === 'rtl') ? 'right' : 'inherit'};
|
||||||
|
|
||||||
${media.lessThan('medium', true)`
|
${({ compact, theme }) =>
|
||||||
|
media.lessThan('medium', true)`
|
||||||
width: 100%;
|
width: 100%;
|
||||||
padding: ${props =>
|
padding: ${`${compact ? 0 : theme.spacing.sectionVertical}px ${
|
||||||
`${props.theme.spacing.sectionVertical}px ${props.theme.spacing.sectionHorizontal}px`};
|
theme.spacing.sectionHorizontal
|
||||||
|
}px`};
|
||||||
`};
|
`};
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
|
|
@ -60,7 +60,7 @@ export class ContentItem extends React.Component<ContentItemProps> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const middlePanelWrap = component => <MiddlePanel>{component}</MiddlePanel>;
|
const middlePanelWrap = component => <MiddlePanel compact={true}>{component}</MiddlePanel>;
|
||||||
|
|
||||||
@observer
|
@observer
|
||||||
export class SectionItem extends React.Component<ContentItemProps> {
|
export class SectionItem extends React.Component<ContentItemProps> {
|
||||||
|
@ -71,7 +71,7 @@ export class SectionItem extends React.Component<ContentItemProps> {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Row>
|
<Row>
|
||||||
<MiddlePanel>
|
<MiddlePanel compact={level !== 1}>
|
||||||
<Header>
|
<Header>
|
||||||
<ShareLink to={this.props.item.id} />
|
<ShareLink to={this.props.item.id} />
|
||||||
{name}
|
{name}
|
||||||
|
|
|
@ -5,7 +5,7 @@ import { Markdown } from '../Markdown/Markdown';
|
||||||
import { OptionsContext } from '../OptionsProvider';
|
import { OptionsContext } from '../OptionsProvider';
|
||||||
import { SelectOnClick } from '../SelectOnClick/SelectOnClick';
|
import { SelectOnClick } from '../SelectOnClick/SelectOnClick';
|
||||||
|
|
||||||
import { getBasePath } from '../../utils';
|
import { expandDefaultServerVariables, getBasePath } from '../../utils';
|
||||||
import {
|
import {
|
||||||
EndpointInfo,
|
EndpointInfo,
|
||||||
HttpVerb,
|
HttpVerb,
|
||||||
|
@ -61,15 +61,18 @@ export class Endpoint extends React.Component<EndpointProps, EndpointState> {
|
||||||
</EndpointInfo>
|
</EndpointInfo>
|
||||||
<ServersOverlay expanded={expanded}>
|
<ServersOverlay expanded={expanded}>
|
||||||
{operation.servers.map(server => {
|
{operation.servers.map(server => {
|
||||||
|
const normalizedUrl = options.expandDefaultServerVariables
|
||||||
|
? expandDefaultServerVariables(server.url, server.variables)
|
||||||
|
: server.url;
|
||||||
return (
|
return (
|
||||||
<ServerItem key={server.url}>
|
<ServerItem key={normalizedUrl}>
|
||||||
<Markdown source={server.description || ''} compact={true} />
|
<Markdown source={server.description || ''} compact={true} />
|
||||||
<SelectOnClick>
|
<SelectOnClick>
|
||||||
<ServerUrl>
|
<ServerUrl>
|
||||||
<span>
|
<span>
|
||||||
{hideHostname || options.hideHostname
|
{hideHostname || options.hideHostname
|
||||||
? getBasePath(server.url)
|
? getBasePath(normalizedUrl)
|
||||||
: server.url}
|
: normalizedUrl}
|
||||||
</span>
|
</span>
|
||||||
{operation.path}
|
{operation.path}
|
||||||
</ServerUrl>
|
</ServerUrl>
|
||||||
|
|
|
@ -44,9 +44,7 @@ export class Schema extends React.Component<Partial<SchemaProps>> {
|
||||||
if (discriminatorProp !== undefined) {
|
if (discriminatorProp !== undefined) {
|
||||||
if (!oneOf || !oneOf.length) {
|
if (!oneOf || !oneOf.length) {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
`Looks like you are using discriminator wrong: you don't have any definition inherited from the ${
|
`Looks like you are using discriminator wrong: you don't have any definition inherited from the ${schema.title}`,
|
||||||
schema.title
|
|
||||||
}`,
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
return (
|
return (
|
||||||
|
@ -66,9 +64,9 @@ export class Schema extends React.Component<Partial<SchemaProps>> {
|
||||||
|
|
||||||
switch (type) {
|
switch (type) {
|
||||||
case 'object':
|
case 'object':
|
||||||
return <ObjectSchema {...this.props as any} />;
|
return <ObjectSchema {...(this.props as any)} />;
|
||||||
case 'array':
|
case 'array':
|
||||||
return <ArraySchema {...this.props as any} />;
|
return <ArraySchema {...(this.props as any)} />;
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: maybe adjust FieldDetails to accept schema
|
// TODO: maybe adjust FieldDetails to accept schema
|
||||||
|
|
|
@ -52,7 +52,7 @@ export class OAuthFlow extends React.PureComponent<OAuthFlowProps> {
|
||||||
<strong> Scopes: </strong>
|
<strong> Scopes: </strong>
|
||||||
</div>
|
</div>
|
||||||
<ul>
|
<ul>
|
||||||
{Object.keys(flow!.scopes).map(scope => (
|
{Object.keys(flow!.scopes || {}).map(scope => (
|
||||||
<li key={scope}>
|
<li key={scope}>
|
||||||
<code>{scope}</code> - <Markdown inline={true} source={flow!.scopes[scope] || ''} />
|
<code>{scope}</code> - <Markdown inline={true} source={flow!.scopes[scope] || ''} />
|
||||||
</li>
|
</li>
|
||||||
|
|
|
@ -19,6 +19,10 @@ export interface StickySidebarProps {
|
||||||
menu: MenuStore;
|
menu: MenuStore;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface StickySidebarState {
|
||||||
|
offsetTop?: string;
|
||||||
|
}
|
||||||
|
|
||||||
const stickyfill = Stickyfill && Stickyfill();
|
const stickyfill = Stickyfill && Stickyfill();
|
||||||
|
|
||||||
const StyledStickySidebar = styled.div<{ open?: boolean }>`
|
const StyledStickySidebar = styled.div<{ open?: boolean }>`
|
||||||
|
@ -77,13 +81,26 @@ const FloatingButton = styled.div`
|
||||||
`;
|
`;
|
||||||
|
|
||||||
@observer
|
@observer
|
||||||
export class StickyResponsiveSidebar extends React.Component<StickySidebarProps> {
|
export class StickyResponsiveSidebar extends React.Component<
|
||||||
|
StickySidebarProps,
|
||||||
|
StickySidebarState
|
||||||
|
> {
|
||||||
|
static contextType = OptionsContext;
|
||||||
|
context!: React.ContextType<typeof OptionsContext>;
|
||||||
|
state: StickySidebarState = { offsetTop: '0px' };
|
||||||
|
|
||||||
stickyElement: Element;
|
stickyElement: Element;
|
||||||
|
|
||||||
componentDidMount() {
|
componentDidMount() {
|
||||||
if (stickyfill) {
|
if (stickyfill) {
|
||||||
stickyfill.add(this.stickyElement);
|
stickyfill.add(this.stickyElement);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// rerender when hydrating from SSR
|
||||||
|
// see https://github.com/facebook/react/issues/8017#issuecomment-256351955
|
||||||
|
this.setState({
|
||||||
|
offsetTop: this.getScrollYOffset(this.context),
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
componentWillUnmount() {
|
componentWillUnmount() {
|
||||||
|
@ -92,7 +109,7 @@ export class StickyResponsiveSidebar extends React.Component<StickySidebarProps>
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
getScrollYOffset(options) {
|
getScrollYOffset(options: RedocNormalizedOptions) {
|
||||||
let top;
|
let top;
|
||||||
if (this.props.scrollYOffset !== undefined) {
|
if (this.props.scrollYOffset !== undefined) {
|
||||||
top = RedocNormalizedOptions.normalizeScrollYOffset(this.props.scrollYOffset)();
|
top = RedocNormalizedOptions.normalizeScrollYOffset(this.props.scrollYOffset)();
|
||||||
|
@ -105,43 +122,32 @@ export class StickyResponsiveSidebar extends React.Component<StickySidebarProps>
|
||||||
render() {
|
render() {
|
||||||
const open = this.props.menu.sideBarOpened;
|
const open = this.props.menu.sideBarOpened;
|
||||||
|
|
||||||
const style = options => {
|
const top = this.state.offsetTop;
|
||||||
const top = this.getScrollYOffset(options);
|
|
||||||
return {
|
|
||||||
top,
|
|
||||||
height: `calc(100vh - ${top})`,
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<OptionsContext.Consumer>
|
<>
|
||||||
{options => (
|
<StyledStickySidebar
|
||||||
<>
|
open={open}
|
||||||
<StyledStickySidebar
|
className={this.props.className}
|
||||||
open={open}
|
style={{
|
||||||
className={this.props.className}
|
top,
|
||||||
style={style(options)}
|
height: `calc(100vh - ${top})`,
|
||||||
// tslint:disable-next-line
|
}}
|
||||||
ref={el => {
|
// tslint:disable-next-line
|
||||||
this.stickyElement = el as any;
|
ref={el => {
|
||||||
}}
|
this.stickyElement = el as any;
|
||||||
>
|
}}
|
||||||
{this.props.children}
|
>
|
||||||
</StyledStickySidebar>
|
{this.props.children}
|
||||||
<FloatingButton onClick={this.toggleNavMenu}>
|
</StyledStickySidebar>
|
||||||
<AnimatedChevronButton open={open} />
|
<FloatingButton onClick={this.toggleNavMenu}>
|
||||||
</FloatingButton>
|
<AnimatedChevronButton open={open} />
|
||||||
</>
|
</FloatingButton>
|
||||||
)}
|
</>
|
||||||
</OptionsContext.Consumer>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
private toggleNavMenu = () => {
|
private toggleNavMenu = () => {
|
||||||
this.props.menu.toggleSidebar();
|
this.props.menu.toggleSidebar();
|
||||||
};
|
};
|
||||||
|
|
||||||
// private closeNavMenu = () => {
|
|
||||||
// this.setState({ open: false });
|
|
||||||
// };
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -116,7 +116,7 @@ export class MenuStore {
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isScrolledDown) {
|
if (isScrolledDown) {
|
||||||
const el = this.getElementAt(itemIdx + 1);
|
const el = this.getElementAtOrFirstChild(itemIdx + 1);
|
||||||
if (this.scroll.isElementBellow(el)) {
|
if (this.scroll.isElementBellow(el)) {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
@ -163,6 +163,18 @@ export class MenuStore {
|
||||||
return (item && querySelector(`[${SECTION_ATTR}="${item.id}"]`)) || null;
|
return (item && querySelector(`[${SECTION_ATTR}="${item.id}"]`)) || null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* get section/operation DOM Node related to the item or if it is group item, returns first item of the group
|
||||||
|
* @param idx item absolute index
|
||||||
|
*/
|
||||||
|
getElementAtOrFirstChild(idx: number): Element | null {
|
||||||
|
let item = this.flatItems[idx];
|
||||||
|
if (item && item.type === 'group') {
|
||||||
|
item = item.items[0];
|
||||||
|
}
|
||||||
|
return (item && querySelector(`[${SECTION_ATTR}="${item.id}"]`)) || null;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* current active item
|
* current active item
|
||||||
*/
|
*/
|
||||||
|
@ -189,6 +201,11 @@ export class MenuStore {
|
||||||
if ((this.activeItem && this.activeItem.id) === (item && item.id)) {
|
if ((this.activeItem && this.activeItem.id) === (item && item.id)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (item && item.type === 'group') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
this.deactivate(this.activeItem);
|
this.deactivate(this.activeItem);
|
||||||
if (!item) {
|
if (!item) {
|
||||||
this.history.replace('', rewriteHistory);
|
this.history.replace('', rewriteHistory);
|
||||||
|
|
|
@ -4,7 +4,11 @@ import { OpenAPIRef, OpenAPISchema, OpenAPISpec, Referenced } from '../types';
|
||||||
|
|
||||||
import { appendToMdHeading, IS_BROWSER } from '../utils/';
|
import { appendToMdHeading, IS_BROWSER } from '../utils/';
|
||||||
import { JsonPointer } from '../utils/JsonPointer';
|
import { JsonPointer } from '../utils/JsonPointer';
|
||||||
import { isNamedDefinition, SECURITY_DEFINITIONS_COMPONENT_NAME } from '../utils/openapi';
|
import {
|
||||||
|
isNamedDefinition,
|
||||||
|
SECURITY_DEFINITIONS_COMPONENT_NAME,
|
||||||
|
SECURITY_DEFINITIONS_JSX_NAME,
|
||||||
|
} from '../utils/openapi';
|
||||||
import { buildComponentComment, MarkdownRenderer } from './MarkdownRenderer';
|
import { buildComponentComment, MarkdownRenderer } from './MarkdownRenderer';
|
||||||
import { RedocNormalizedOptions } from './RedocNormalizedOptions';
|
import { RedocNormalizedOptions } from './RedocNormalizedOptions';
|
||||||
|
|
||||||
|
@ -40,6 +44,7 @@ class RefCounter {
|
||||||
export class OpenAPIParser {
|
export class OpenAPIParser {
|
||||||
specUrl?: string;
|
specUrl?: string;
|
||||||
spec: OpenAPISpec;
|
spec: OpenAPISpec;
|
||||||
|
mergeRefs: Set<string>;
|
||||||
|
|
||||||
private _refCounter: RefCounter = new RefCounter();
|
private _refCounter: RefCounter = new RefCounter();
|
||||||
|
|
||||||
|
@ -53,6 +58,8 @@ export class OpenAPIParser {
|
||||||
|
|
||||||
this.spec = spec;
|
this.spec = spec;
|
||||||
|
|
||||||
|
this.mergeRefs = new Set();
|
||||||
|
|
||||||
const href = IS_BROWSER ? window.location.href : '';
|
const href = IS_BROWSER ? window.location.href : '';
|
||||||
if (typeof specUrl === 'string') {
|
if (typeof specUrl === 'string') {
|
||||||
this.specUrl = urlResolve(href, specUrl);
|
this.specUrl = urlResolve(href, specUrl);
|
||||||
|
@ -74,7 +81,10 @@ export class OpenAPIParser {
|
||||||
) {
|
) {
|
||||||
// Automatically inject Authentication section with SecurityDefinitions component
|
// Automatically inject Authentication section with SecurityDefinitions component
|
||||||
const description = spec.info.description || '';
|
const description = spec.info.description || '';
|
||||||
if (!MarkdownRenderer.containsComponent(description, SECURITY_DEFINITIONS_COMPONENT_NAME)) {
|
if (
|
||||||
|
!MarkdownRenderer.containsComponent(description, SECURITY_DEFINITIONS_COMPONENT_NAME) &&
|
||||||
|
!MarkdownRenderer.containsComponent(description, SECURITY_DEFINITIONS_JSX_NAME)
|
||||||
|
) {
|
||||||
const comment = buildComponentComment(SECURITY_DEFINITIONS_COMPONENT_NAME);
|
const comment = buildComponentComment(SECURITY_DEFINITIONS_COMPONENT_NAME);
|
||||||
spec.info.description = appendToMdHeading(description, 'Authentication', comment);
|
spec.info.description = appendToMdHeading(description, 'Authentication', comment);
|
||||||
}
|
}
|
||||||
|
@ -176,7 +186,12 @@ export class OpenAPIParser {
|
||||||
schema: OpenAPISchema,
|
schema: OpenAPISchema,
|
||||||
$ref?: string,
|
$ref?: string,
|
||||||
forceCircular: boolean = false,
|
forceCircular: boolean = false,
|
||||||
|
used$Refs = new Set<string>(),
|
||||||
): MergedOpenAPISchema {
|
): MergedOpenAPISchema {
|
||||||
|
if ($ref) {
|
||||||
|
used$Refs.add($ref);
|
||||||
|
}
|
||||||
|
|
||||||
schema = this.hoistOneOfs(schema);
|
schema = this.hoistOneOfs(schema);
|
||||||
|
|
||||||
if (schema.allOf === undefined) {
|
if (schema.allOf === undefined) {
|
||||||
|
@ -198,16 +213,25 @@ export class OpenAPIParser {
|
||||||
receiver.items = { ...receiver.items };
|
receiver.items = { ...receiver.items };
|
||||||
}
|
}
|
||||||
|
|
||||||
const allOfSchemas = schema.allOf.map(subSchema => {
|
const allOfSchemas = schema.allOf
|
||||||
const resolved = this.deref(subSchema, forceCircular);
|
.map(subSchema => {
|
||||||
const subRef = subSchema.$ref || undefined;
|
if (subSchema && subSchema.$ref && used$Refs.has(subSchema.$ref)) {
|
||||||
const subMerged = this.mergeAllOf(resolved, subRef, forceCircular);
|
return undefined;
|
||||||
receiver.parentRefs!.push(...(subMerged.parentRefs || []));
|
}
|
||||||
return {
|
|
||||||
$ref: subRef,
|
const resolved = this.deref(subSchema, forceCircular);
|
||||||
schema: subMerged,
|
const subRef = subSchema.$ref || undefined;
|
||||||
};
|
const subMerged = this.mergeAllOf(resolved, subRef, forceCircular, used$Refs);
|
||||||
});
|
receiver.parentRefs!.push(...(subMerged.parentRefs || []));
|
||||||
|
return {
|
||||||
|
$ref: subRef,
|
||||||
|
schema: subMerged,
|
||||||
|
};
|
||||||
|
})
|
||||||
|
.filter(child => child !== undefined) as Array<{
|
||||||
|
$ref: string | undefined;
|
||||||
|
schema: MergedOpenAPISchema;
|
||||||
|
}>;
|
||||||
|
|
||||||
for (const { $ref: subSchemaRef, schema: subSchema } of allOfSchemas) {
|
for (const { $ref: subSchemaRef, schema: subSchema } of allOfSchemas) {
|
||||||
if (
|
if (
|
||||||
|
|
|
@ -37,7 +37,10 @@ export interface RedocRawOptions {
|
||||||
allowedMdComponents?: Dict<MDXComponentMeta>;
|
allowedMdComponents?: Dict<MDXComponentMeta>;
|
||||||
|
|
||||||
labels?: LabelsConfigRaw;
|
labels?: LabelsConfigRaw;
|
||||||
|
|
||||||
enumSkipQuotes?: boolean | string;
|
enumSkipQuotes?: boolean | string;
|
||||||
|
|
||||||
|
expandDefaultServerVariables?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
function argValueToBoolean(val?: string | boolean): boolean {
|
function argValueToBoolean(val?: string | boolean): boolean {
|
||||||
|
@ -159,6 +162,8 @@ export class RedocNormalizedOptions {
|
||||||
unstable_ignoreMimeParameters: boolean;
|
unstable_ignoreMimeParameters: boolean;
|
||||||
allowedMdComponents: Dict<MDXComponentMeta>;
|
allowedMdComponents: Dict<MDXComponentMeta>;
|
||||||
|
|
||||||
|
expandDefaultServerVariables: boolean;
|
||||||
|
|
||||||
constructor(raw: RedocRawOptions, defaults: RedocRawOptions = {}) {
|
constructor(raw: RedocRawOptions, defaults: RedocRawOptions = {}) {
|
||||||
raw = { ...defaults, ...raw };
|
raw = { ...defaults, ...raw };
|
||||||
const hook = raw.theme && raw.theme.extensionsHook;
|
const hook = raw.theme && raw.theme.extensionsHook;
|
||||||
|
@ -200,5 +205,7 @@ export class RedocNormalizedOptions {
|
||||||
this.unstable_ignoreMimeParameters = argValueToBoolean(raw.unstable_ignoreMimeParameters);
|
this.unstable_ignoreMimeParameters = argValueToBoolean(raw.unstable_ignoreMimeParameters);
|
||||||
|
|
||||||
this.allowedMdComponents = raw.allowedMdComponents || {};
|
this.allowedMdComponents = raw.allowedMdComponents || {};
|
||||||
|
|
||||||
|
this.expandDefaultServerVariables = argValueToBoolean(raw.expandDefaultServerVariables);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -60,7 +60,7 @@ describe('Utils', () => {
|
||||||
test('should behave like Object.assign on the top level', () => {
|
test('should behave like Object.assign on the top level', () => {
|
||||||
const obj1 = { a: { a1: 'A1' }, c: 'C' };
|
const obj1 = { a: { a1: 'A1' }, c: 'C' };
|
||||||
const obj2 = { a: undefined, b: { b1: 'B1' } };
|
const obj2 = { a: undefined, b: { b1: 'B1' } };
|
||||||
expect(mergeObjects({}, obj1, obj2)).toEqual(Object.assign({}, obj1, obj2));
|
expect(mergeObjects({}, obj1, obj2)).toEqual({ ...obj1, ...obj2 });
|
||||||
});
|
});
|
||||||
test('should not merge array values, just override', () => {
|
test('should not merge array values, just override', () => {
|
||||||
const obj1 = { a: ['A', 'B'] };
|
const obj1 = { a: ['A', 'B'] };
|
||||||
|
|
|
@ -13,6 +13,7 @@ import {
|
||||||
|
|
||||||
import { FieldModel, OpenAPIParser, RedocNormalizedOptions } from '../../services';
|
import { FieldModel, OpenAPIParser, RedocNormalizedOptions } from '../../services';
|
||||||
import { OpenAPIParameter, OpenAPIParameterLocation, OpenAPIParameterStyle } from '../../types';
|
import { OpenAPIParameter, OpenAPIParameterLocation, OpenAPIParameterStyle } from '../../types';
|
||||||
|
import { expandDefaultServerVariables } from '../openapi';
|
||||||
|
|
||||||
describe('Utils', () => {
|
describe('Utils', () => {
|
||||||
describe('openapi getStatusCode', () => {
|
describe('openapi getStatusCode', () => {
|
||||||
|
@ -293,6 +294,39 @@ describe('Utils', () => {
|
||||||
]);
|
]);
|
||||||
expect(res).toEqual([{ url: 'https://base.com/sandbox/test', description: 'test' }]);
|
expect(res).toEqual([{ url: 'https://base.com/sandbox/test', description: 'test' }]);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should expand variables', () => {
|
||||||
|
const servers = normalizeServers('', [
|
||||||
|
{
|
||||||
|
url: 'http://{host}{basePath}',
|
||||||
|
variables: {
|
||||||
|
host: {
|
||||||
|
default: '127.0.0.1',
|
||||||
|
},
|
||||||
|
basePath: {
|
||||||
|
default: '/path/to/endpoint',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
url: 'http://127.0.0.2:{port}',
|
||||||
|
variables: {},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
url: 'http://127.0.0.3',
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
|
||||||
|
expect(expandDefaultServerVariables(servers[0].url, servers[0].variables)).toEqual(
|
||||||
|
'http://127.0.0.1/path/to/endpoint',
|
||||||
|
);
|
||||||
|
expect(expandDefaultServerVariables(servers[1].url, servers[1].variables)).toEqual(
|
||||||
|
'http://127.0.0.2:{port}',
|
||||||
|
);
|
||||||
|
expect(expandDefaultServerVariables(servers[2].url, servers[2].variables)).toEqual(
|
||||||
|
'http://127.0.0.3',
|
||||||
|
);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('openapi humanizeConstraints', () => {
|
describe('openapi humanizeConstraints', () => {
|
||||||
|
@ -404,7 +438,7 @@ describe('Utils', () => {
|
||||||
{ style: 'simple', explode: false, expected: 'role,admin,firstName,Alex' },
|
{ style: 'simple', explode: false, expected: 'role,admin,firstName,Alex' },
|
||||||
{ style: 'simple', explode: true, expected: 'role=admin,firstName=Alex' },
|
{ style: 'simple', explode: true, expected: 'role=admin,firstName=Alex' },
|
||||||
{ style: 'label', explode: false, expected: '.role,admin,firstName,Alex' },
|
{ style: 'label', explode: false, expected: '.role,admin,firstName,Alex' },
|
||||||
{ style: 'label', explode: true, expected: '.role=admin,firstName=Alex' },
|
{ style: 'label', explode: true, expected: '.role=admin.firstName=Alex' },
|
||||||
{ style: 'matrix', explode: false, expected: ';id=role,admin,firstName,Alex' },
|
{ style: 'matrix', explode: false, expected: ';id=role,admin,firstName,Alex' },
|
||||||
{ style: 'matrix', explode: true, expected: ';role=admin;firstName=Alex' },
|
{ style: 'matrix', explode: true, expected: ';role=admin;firstName=Alex' },
|
||||||
],
|
],
|
||||||
|
@ -516,9 +550,7 @@ describe('Utils', () => {
|
||||||
locationTestGroup.cases.forEach(valueTypeTestGroup => {
|
locationTestGroup.cases.forEach(valueTypeTestGroup => {
|
||||||
describe(valueTypeTestGroup.description, () => {
|
describe(valueTypeTestGroup.description, () => {
|
||||||
valueTypeTestGroup.cases.forEach(testCase => {
|
valueTypeTestGroup.cases.forEach(testCase => {
|
||||||
it(`should serialize correctly when style is ${testCase.style} and explode is ${
|
it(`should serialize correctly when style is ${testCase.style} and explode is ${testCase.explode}`, () => {
|
||||||
testCase.explode
|
|
||||||
}`, () => {
|
|
||||||
const parameter: OpenAPIParameter = {
|
const parameter: OpenAPIParameter = {
|
||||||
name: locationTestGroup.name,
|
name: locationTestGroup.name,
|
||||||
in: locationTestGroup.location,
|
in: locationTestGroup.location,
|
||||||
|
|
|
@ -83,7 +83,7 @@ export function appendToMdHeading(md: string, heading: string, content: string)
|
||||||
}
|
}
|
||||||
|
|
||||||
// credits https://stackoverflow.com/a/46973278/1749888
|
// credits https://stackoverflow.com/a/46973278/1749888
|
||||||
export const mergeObjects = <T extends object = object>(target: T, ...sources: T[]): T => {
|
export const mergeObjects = (target: any, ...sources: any[]): any => {
|
||||||
if (!sources.length) {
|
if (!sources.length) {
|
||||||
return target;
|
return target;
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import { dirname } from 'path';
|
import { dirname } from 'path';
|
||||||
import { URI } from 'uri-template-lite';
|
const URLtemplate = require('url-template');
|
||||||
|
|
||||||
import { OpenAPIParser } from '../services/OpenAPIParser';
|
import { OpenAPIParser } from '../services/OpenAPIParser';
|
||||||
import {
|
import {
|
||||||
|
@ -168,7 +168,7 @@ function serializeFormValue(name: string, explode: boolean, value: any) {
|
||||||
// e.g. URI.template doesn't parse names with hypen (-) which are valid query param names
|
// e.g. URI.template doesn't parse names with hypen (-) which are valid query param names
|
||||||
const safeName = '__redoc_param_name__';
|
const safeName = '__redoc_param_name__';
|
||||||
const suffix = explode ? '*' : '';
|
const suffix = explode ? '*' : '';
|
||||||
const template = new URI.Template(`{?${safeName}${suffix}}`);
|
const template = URLtemplate.parse(`{?${safeName}${suffix}}`);
|
||||||
return template
|
return template
|
||||||
.expand({ [safeName]: value })
|
.expand({ [safeName]: value })
|
||||||
.substring(1)
|
.substring(1)
|
||||||
|
@ -227,7 +227,7 @@ function serializePathParameter(
|
||||||
// Use RFC6570 safe name ([a-zA-Z0-9_]) and replace with our name later
|
// Use RFC6570 safe name ([a-zA-Z0-9_]) and replace with our name later
|
||||||
// e.g. URI.template doesn't parse names with hypen (-) which are valid query param names
|
// e.g. URI.template doesn't parse names with hypen (-) which are valid query param names
|
||||||
const safeName = '__redoc_param_name__';
|
const safeName = '__redoc_param_name__';
|
||||||
const template = new URI.Template(`{${prefix}${safeName}${suffix}}`);
|
const template = URLtemplate.parse(`{${prefix}${safeName}${suffix}}`);
|
||||||
|
|
||||||
return template.expand({ [safeName]: value }).replace(/__redoc_param_name__/g, name);
|
return template.expand({ [safeName]: value }).replace(/__redoc_param_name__/g, name);
|
||||||
}
|
}
|
||||||
|
@ -263,7 +263,7 @@ function serializeQueryParameter(
|
||||||
return `${name}=${value.join('|')}`;
|
return `${name}=${value.join('|')}`;
|
||||||
case 'deepObject':
|
case 'deepObject':
|
||||||
if (!explode || Array.isArray(value) || typeof value !== 'object') {
|
if (!explode || Array.isArray(value) || typeof value !== 'object') {
|
||||||
console.warn('The style deepObject is only applicable for objects with expolde=true');
|
console.warn('The style deepObject is only applicable for objects with explode=true');
|
||||||
return '';
|
return '';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -285,7 +285,7 @@ function serializeHeaderParameter(
|
||||||
|
|
||||||
// name is not important here, so use RFC6570 safe name ([a-zA-Z0-9_])
|
// name is not important here, so use RFC6570 safe name ([a-zA-Z0-9_])
|
||||||
const name = '__redoc_param_name__';
|
const name = '__redoc_param_name__';
|
||||||
const template = new URI.Template(`{${name}${suffix}}`);
|
const template = URLtemplate.parse(`{${name}${suffix}}`);
|
||||||
return decodeURIComponent(template.expand({ [name]: value }));
|
return decodeURIComponent(template.expand({ [name]: value }));
|
||||||
default:
|
default:
|
||||||
console.warn('Unexpected style for header: ' + style);
|
console.warn('Unexpected style for header: ' + style);
|
||||||
|
@ -487,6 +487,13 @@ export function mergeSimilarMediaTypes(types: Dict<OpenAPIMediaType>): Dict<Open
|
||||||
return mergedTypes;
|
return mergedTypes;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function expandDefaultServerVariables(url: string, variables: object = {}) {
|
||||||
|
return url.replace(
|
||||||
|
/(?:{)(\w+)(?:})/g,
|
||||||
|
(match, name) => (variables[name] && variables[name].default) || match,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
export function normalizeServers(
|
export function normalizeServers(
|
||||||
specUrl: string | undefined,
|
specUrl: string | undefined,
|
||||||
servers: OpenAPIServer[],
|
servers: OpenAPIServer[],
|
||||||
|
|
Loading…
Reference in New Issue
Block a user