mirror of
https://github.com/Redocly/redoc.git
synced 2025-08-08 06:04:56 +03:00
Merge remote-tracking branch 'source/master' into develop
# Conflicts: # .gitignore # package.json # src/components/ApiInfo/ApiInfo.tsx # src/components/SearchBox/styled.elements.tsx # src/components/SideMenu/SideMenu.tsx # src/services/RedocNormalizedOptions.ts # src/theme.ts
This commit is contained in:
commit
5af9c204e3
|
@ -9,4 +9,4 @@
|
|||
!webpack.config.ts
|
||||
|
||||
!package.json
|
||||
!yarn.lock
|
||||
!package-lock.json
|
||||
|
|
49
.eslintrc.js
Normal file
49
.eslintrc.js
Normal file
|
@ -0,0 +1,49 @@
|
|||
module.exports = {
|
||||
env: {
|
||||
browser: true,
|
||||
},
|
||||
parser: '@typescript-eslint/parser',
|
||||
extends: ['plugin:react/recommended', 'plugin:@typescript-eslint/recommended'],
|
||||
parserOptions: {
|
||||
project: 'tsconfig.json',
|
||||
sourceType: 'module',
|
||||
createDefaultProgram: true,
|
||||
ecmaFeatures: {
|
||||
jsx: true,
|
||||
},
|
||||
},
|
||||
settings: {
|
||||
react: {
|
||||
version: 'detect',
|
||||
},
|
||||
},
|
||||
plugins: ['@typescript-eslint', 'import'],
|
||||
rules: {
|
||||
'@typescript-eslint/explicit-function-return-type': 'off',
|
||||
'@typescript-eslint/no-explicit-any': 'off',
|
||||
'@typescript-eslint/no-use-before-define': 'off',
|
||||
'@typescript-eslint/interface-name-prefix': 'off',
|
||||
'@typescript-eslint/no-inferrable-types': 'off',
|
||||
'@typescript-eslint/no-non-null-assertion': 'off',
|
||||
'@typescript-eslint/ban-ts-ignore': 'off',
|
||||
|
||||
'react/prop-types': 'off',
|
||||
|
||||
'import/no-extraneous-dependencies': 'error',
|
||||
'import/no-internal-modules': [
|
||||
'error',
|
||||
{
|
||||
allow: [
|
||||
'prismjs/**',
|
||||
'perfect-scrollbar/**',
|
||||
'react-dom/*',
|
||||
'core-js/**',
|
||||
'memoize-one/**',
|
||||
'unfetch/**',
|
||||
'raf/polyfill',
|
||||
'**/fixtures/**', // for tests
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
38
.github/CONTRIBUTING.md
vendored
38
.github/CONTRIBUTING.md
vendored
|
@ -15,56 +15,56 @@ Hi! We're really excited that you are interested in contributing to ReDoc. Befor
|
|||
Before submitting a pull request, please make sure the following is done:
|
||||
|
||||
1. Fork the repository and create your branch from master.
|
||||
2. Run `yarn` in the repository root.
|
||||
2. Run `npm install` in the repository root.
|
||||
3. If you’ve fixed a bug or added code that should be tested, add tests!
|
||||
4. Ensure the test suite passes (`yarn test`). Tip: `yarn test --watch TestName` is helpful in development.
|
||||
5. Format your code with prettier (`yarn prettier`).
|
||||
4. Ensure the test suite passes (`npm test`). Tip: `npm test -- --watch TestName` is helpful in development.
|
||||
5. Format your code with prettier (`npm run prettier`).
|
||||
|
||||
## Development Setup
|
||||
|
||||
You will need [Node.js](http://nodejs.org) at `v8.0.0+` and [Yarn](https://yarnpkg.com/en/) at `v1.2.0+`
|
||||
You will need [Node.js](http://nodejs.org) at `v12.0.0+`.
|
||||
|
||||
After cloning the repo, run:
|
||||
|
||||
```bash
|
||||
$ yarn install # or npm
|
||||
$ npm install # or npm
|
||||
```
|
||||
|
||||
### Commonly used NPM scripts
|
||||
|
||||
``` bash
|
||||
# dev-server, watch and auto reload playground
|
||||
$ yarn start
|
||||
$ npm start
|
||||
|
||||
# start playground app in production environement
|
||||
$ yarn start:prod
|
||||
# start playground app in production environment
|
||||
$ npm run start:prod
|
||||
|
||||
# runt tslint
|
||||
$ yarn lint
|
||||
$ npm run lint
|
||||
|
||||
# try autofix tslint issues
|
||||
$ yarn lint --fix
|
||||
$ npm run lint -- --fix
|
||||
|
||||
# run unit tests
|
||||
$ yarn unit
|
||||
$ npm run unit
|
||||
|
||||
# run e2e tests
|
||||
$ yarn e2e
|
||||
$ npm run e2e
|
||||
|
||||
# open cypress UI to debug e2e test
|
||||
$ yarn cy:open
|
||||
$ npm run cy:open
|
||||
|
||||
# run the full test suite, include linting / unit / e2e
|
||||
$ yarn test
|
||||
$ npm test
|
||||
|
||||
# prepare bundles
|
||||
$ yarn bundle
|
||||
$ npm run bundle
|
||||
|
||||
# format the code using prettier
|
||||
$ yarn prettier
|
||||
$ npm run prettier
|
||||
|
||||
# auto-generate changelog
|
||||
$ yarn changelog
|
||||
$ npm run changelog
|
||||
```
|
||||
|
||||
There are some other scripts available in the `scripts` section of the `package.json` file.
|
||||
|
@ -84,11 +84,11 @@ There are some other scripts available in the `scripts` section of the `package.
|
|||
|
||||
- **`src`**: contains the source code. The codebase is written in Typescript. CSS styles are managed with [Styled components](https://www.styled-components.com/). State is managed by [MobX](https://github.com/mobxjs/mobx)
|
||||
|
||||
- **`src/common-elements`**: containts common Styled elements or components used in multiple places
|
||||
- **`src/common-elements`**: contains common Styled elements or components used in multiple places
|
||||
- **`src/components`**: contains main visual components
|
||||
- **`src/services`**: contains different services used by ReDoc including MobX stores
|
||||
- **`src/services/models`**: contains classes for OpenAPI entities (e.g. Response, Operations, etc)
|
||||
- **`src/types`**: contains extra typescript typings including OpenAPI doc typings
|
||||
- **`src/utils`**: utility functions
|
||||
- **`src/styled-components.ts`**: - reexprots styled-components with proper typescript annotations using theme
|
||||
- **`src/styled-components.ts`**: - reexports styled-components with proper typescript annotations using theme
|
||||
- **`src/theme.ts`**: - default theme (colors, fonts, etc) used by all the components
|
||||
|
|
20
.github/workflows/unit-tests.yml
vendored
Normal file
20
.github/workflows/unit-tests.yml
vendored
Normal file
|
@ -0,0 +1,20 @@
|
|||
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: npm install, build, and test
|
||||
run: |
|
||||
npm install
|
||||
npm run bundle
|
||||
npm test
|
2
.gitignore
vendored
2
.gitignore
vendored
|
@ -34,7 +34,7 @@ cli/index.js
|
|||
/coverage
|
||||
.ghpages-tmp
|
||||
stats.json
|
||||
/package-lock.json
|
||||
yarn.lock
|
||||
|
||||
.idea/
|
||||
*.iml
|
||||
|
|
16
.travis.yml
16
.travis.yml
|
@ -1,7 +1,9 @@
|
|||
language: node_js
|
||||
node_js:
|
||||
- '8'
|
||||
cache: yarn
|
||||
- '10'
|
||||
cache:
|
||||
directories:
|
||||
- "~/.cache"
|
||||
env:
|
||||
global:
|
||||
- GH_REF: github.com/Redocly/redoc.git
|
||||
|
@ -11,9 +13,12 @@ env:
|
|||
- secure: apiavCfCQngL9Een1m7MIXMf3bqO3rY4YY59TMBl/yFKi80CEsHPHhgVUkl6hC+aM5PeBt/vgjh37rHMX31j/pcSZ4Z8SO/4Bwr36iHfhSxSEuAQog8P07qWqH7wYYWGIVmF682stgl0fYF+GN92sx/6edFVzsWVECf2G7imtICKSTbhKGm3Dhn2JwGnhD7eyfgZ33omgiaswumdu0xABoXDfqSZR+16fC4Ap5rhv3fXO9ndvRNy1STn376nT+my6e86UrQL4aS/S+HNHgIe1BUs+5cOp6Jgw6t0ie7phY0EAiECsRxy9K4e3Dctv9m6+Wma4+vy65MS0zGyrqey6oyV4l827sCOjrD1qcqc9bX6FlMSouVoNfE4ZjINNAbgigTaiLSoDSPcf5I5smkkM2ezzFOMSZwZxNdaNL2LKb97vc8m/ZUkv0sKZyT7oqVL7aJweEivsSHj5l2KR8Z7XrVB1y2eI6GvyTSa/d+CL4dSRzjh8+IRN047YBrdTKD5IkdT0upfoBu14WPUfFmLKxX+iMCslXRWb6kwojhrWNYmZvL65KRAzJ6+eIPDG/W5QUOpYyYT77bLlBQjVo6NmVvl9v3HMECq9CHH0ivKFBGPiKMOx7cJkTax3FuyznOW2WCXB9kTb5Zk9toaiNlSp9L6ll/h2Eyxa6n6sWUgmmM=
|
||||
- secure: vVRg9BKGBwF2MbXQnEccFL+XW0/7RaBmge9k7jbGYScBwkP3XjnQ/Xaj0cvTz2CM2EqXsbpwfvr4Jo+enW/E3MGy5RiEzv5hUe/jIFRR0gfAFbZxSTvg5xiFhTDffqQk0fncO4jXu+wPO5lZ2CMRWzyXz3i1MZhjMcAgoDr1+TRss/EGXLNHxr2RM88tpUW0fV2prIRoyGqhCgnYZtrm7hmr41Ej+itg1MqZLml/Rjkt3KsNgI+z0O5Qn3QSAO8GtPZqeftQxAjevOmxZGcssxY8EJvqbjAujr4y51WncXpEmCRPSY2J9R5+fkgZurqwnJapbQpjwKYemok3ps7EHg2gWkAlmPdQO4LKpbffGkM/o5b+8+HdIuQZugsSWQD9hUSftTAFLcfA1isi7V2lHE1m8bX/vk9zIyDdcPSwIaFe9y+w3PexwFmTjPLq+nia/UY2kARFZMEIFAJby6gkA70DcAJ50QOM86InJu5DSzGbIssgTGAXCn0TPPyGveaurVLw8x61j3yh8LDF46gUHey3rqv6WjpCM9h/vg7X/gq5ve/5Q2KHscUKfs/sA53Mt7qPeqRZY1QCaaRjzqJO/ZraHqWWeKmPKaWhPGR0kYEnkvB+K9GZ+HNSWCltjCO4SJ1xeEl7CRqQxAwdiMATF5SKqyiC+bn5oc35mFgbRF8=
|
||||
- secure: ela1tn4wkJQZ8O4iv+4pIZi5cebxeCStVF1tEUe6qa6WWgJYVXmS2tEv3QQ36NUBFrP58Y6yl10XguPnvj/2BCqcZI4FUBHh3BfiBoUtXxDCVKI5LtlniNiOFGUwfzEeYka8T51zFlcUXSCCaxHkRZbmBsIzeJ39UwTi5fy0qwLv9GgL0czhwm8I8sZ8gyWdGmqpXNFEsb9JP4ZA3mw2qpWkGpGAqQPD9XSCkU3LmX1/ltwsBMAgGYKLLo7vU8d5KV2c8L1Gnxfl6BvfmqUD/dsas/1rnk08rU2nez5ekuQa2tJRkDLOv8bqvrGRLjHSUa3yPuisC6SsDGSU7/3DcozZyYsz7WQ6WI8tYabyjqqeJTF1N8a5T3IbZaZNV1J4JHOO9Cb/y7gIg4edANg6tbe7MzZpdEPRBnw6OkdTdirpNsWQ/jnfpY1hn6mraQZz/q8yaz3W21NjbBJhVnvfh5gWLKQ3YAAziCBhmmrThFhUu0czz+G920MuFo477TBcxvlrE7CaNJ0Q6yYkDehEPOv3jvEs1QVHPwuRrlaLTbBhrlTICKZ58gdX30O8N4i0Xgp/v6qrC03bplnMQc8E/uC61wcVLJixnlZVp8FODpUvPjsxVFkpuNSOIAaiqcERmoiPXx05Epzmr78hjU5rYCx/1MmVoeB4gs9YO+4guD4=
|
||||
- secure: SEqTg6WoGPPpcWzJ03ZfcSBb3nZ2Mdhug0ec2PszuzYO3libCb9usiqi+jils9z6qyXsL6ecz8HYazDGOUepnubhIpI5otLgfn9XiapjMT06Bj//AjbKpH7eu3TJSpJMzoRHZrKIE1y9ZKIBqKwl9Xs7ko+1oa+MLhrLuxXkoi0JqRB5UzkQtJRDoxVNjysnLQn+hsfnm+yuqPHZd2+Loy++q//WHuf9bwJrlkXn2ICYQIX5oQGlxNO6ui+OZklb0YknvyO5GdQeoKaHYru3MMKKCIS6I7AG9wLmPs5Ou3T0Ia0Xx4/7xazs0rH4NCVpIceSYc3v6evR37pp8MsFTC3BzjL1V3slTnmitC1KSNM8ndGRUg1nsCBkJysnR3HpX6SHuCH+UzOuMxEjwiPdSRnzJPEbTHa1HqMfTkTJMbm4zhp7W4/ozX4TtjUB0ql6NoQE2n0Z3aYgR2C78TmzaPQun8EgredWnCID1FedyexaNcw4HyZ2rXlcvG3rBzSwLHH5PePT9skyqy6KtIaL0MlAP556ilgUeyCZfCNdTmzCvPDZuqaeLRezWDdsKnRfTkxIW80QWlmZ6sW0hynJV5JN2Oghk9Tr+QzgV4ZF68FHwoU9YXCTyX4w5iTYq/GjvfTBqB3VSGPOz3PwU7r47tmaYzPj+I44zqktgxyuxDo=
|
||||
- secure: Pc86j2/rgCPAEWcjzPbbVFkL2SwNt4CpeXP69zedMi9RomQblMJa9R0wbAT6H5VCnPky2cpmxkzjFWOc91N9crzceg6aOoWkPr6pTTnTv+EL70i6+XSrAFpkgRjszprxnU1Bz+GcYEjP/zR8479fx8ooSl7MwHOaP7XiQyaBQAbY1CPlmpT+b4Ut7Fm5QnD90/NgPjbKkngl0kVUfNFdFOSfJ3QWLyFCUUSQ4DlxccJOTIaOH/n8u9Nz4NTuuHE3XeRuEsuj2SJutJnFBUYwsvugrdPvKWiubkewJfylp6EwABzByENsg6XxW9SIq80lMc3Oi7ld9L2lAgpj+8/42olnbMzH0F0rw/p1ccPAdVwQVV6YFaqCzivK5A5aX5LmGKwJ6SR9k1PgcWP6sKKMIsIEObbyM88Tke3QkreEz+cLg/3jjko7Vpb0tbqh8BtbpWV+exL4rX3r2C5Mb1Es1W597hN5LSczWYFgw0ZETpfbVZg6Ri1iZks0wpsT/E+c0q2scUaBVrdTZseHxUPB7mPDlXL1l9/i4sOxPyBHZtJRAzeTT/fOXfj4vuD+ihspXzoRRLaQbizlb8FpyPA47XdmBDpXi3OBiaIFLwvybEn7qM7rqvWxdz6vvCZv0t/AN3t3Qvh2vHKCshHecaa8NoJQHWrdFMHeecYHyeoujZ8=
|
||||
addons:
|
||||
chrome: stable
|
||||
apt:
|
||||
packages:
|
||||
- libgconf-2-4
|
||||
before_script: npm run bundle
|
||||
script: npm test && ([ "${TRAVIS_PULL_REQUEST}" = "false" ] && npm run e2e-ci || npm
|
||||
run e2e)
|
||||
|
@ -29,9 +34,6 @@ deploy:
|
|||
tags: true
|
||||
- provider: script
|
||||
skip_cleanup: true
|
||||
script: cd cli && yarn install && yarn ci-publish || true
|
||||
- provider: script
|
||||
skip_cleanup: true
|
||||
script: yarn deploy:demo
|
||||
script: npm run deploy:demo
|
||||
on:
|
||||
tags: true
|
||||
|
|
243
CHANGELOG.md
243
CHANGELOG.md
|
@ -1,3 +1,228 @@
|
|||
# [2.0.0-rc.24](https://github.com/Redocly/redoc/compare/v2.0.0-rc.23...v2.0.0-rc.24) (2020-03-17)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* Add debounce for 300 ms when searching ([#1089](https://github.com/Redocly/redoc/issues/1089)) ([373f018](https://github.com/Redocly/redoc/commit/373f018d0c183f83d07a4dbad4a4e2c9ab159f69))
|
||||
* do not load SearchWorker if disableSearch is `true` ([#1191](https://github.com/Redocly/redoc/issues/1191)) ([af415e8](https://github.com/Redocly/redoc/commit/af415e89e8c074a3f7c84f76f24020a7bd545483)), closes [#764](https://github.com/Redocly/redoc/issues/764)
|
||||
* fix major search performance due to wrong marker element ([8c053cc](https://github.com/Redocly/redoc/commit/8c053cc474e88befc3338307317c0702d212d4c3)), closes [#1109](https://github.com/Redocly/redoc/issues/1109)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* new option expandSingleSchemaField ([7608800](https://github.com/Redocly/redoc/commit/7608800d0acaa2fa0099dc840e17cd5aa90b54ca))
|
||||
|
||||
|
||||
|
||||
# [2.0.0-rc.23](https://github.com/Redocly/redoc/compare/v2.0.0-rc.22...v2.0.0-rc.23) (2020-02-09)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* fix broken sticky sidebar in Chrome 80 ([1a2a7dd](https://github.com/Redocly/redoc/commit/1a2a7dd8331cedd6ced4c18accf0b417549b3ff3)), closes [#1167](https://github.com/Redocly/redoc/issues/1167)
|
||||
|
||||
|
||||
|
||||
# [2.0.0-rc.22](https://github.com/Redocly/redoc/compare/v2.0.0-rc.21...v2.0.0-rc.22) (2020-01-15)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* do not process oneOf if inherited from parent with discriminator ([5248415](https://github.com/Redocly/redoc/commit/52484157912d908daea8255d0b7d684b33258d7a))
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* add HTTP syntax highlighting ([#1157](https://github.com/Redocly/redoc/issues/1157)) ([27a4af7](https://github.com/Redocly/redoc/commit/27a4af707686d56280753473b4294ee4af096534))
|
||||
|
||||
|
||||
|
||||
# [2.0.0-rc.21](https://github.com/Redocly/redoc/compare/v2.0.0-rc.20...v2.0.0-rc.21) (2020-01-10)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* empty servers behaviour per OAS spec ([ed1db0c](https://github.com/Redocly/redoc/commit/ed1db0c9027087ae0ae923e390e3e1d638a647ae)), closes [#1151](https://github.com/Redocly/redoc/issues/1151)
|
||||
* fix duplicated content in tags when using md headings ([a260c84](https://github.com/Redocly/redoc/commit/a260c8414c34a259a70a20ebcd20ecbb06c3d250)), closes [#1150](https://github.com/Redocly/redoc/issues/1150) [#1152](https://github.com/Redocly/redoc/issues/1152)
|
||||
* use mobile menu background color value from theme ([#1144](https://github.com/Redocly/redoc/issues/1144)) ([41a9b3c](https://github.com/Redocly/redoc/commit/41a9b3c18228d236d182d3c15c9abc35ae72a0d5))
|
||||
|
||||
|
||||
|
||||
# [2.0.0-rc.20](https://github.com/Redocly/redoc/compare/v2.0.0-rc.19...v2.0.0-rc.20) (2019-12-13)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* fix missing parameters ([942d782](https://github.com/Redocly/redoc/commit/942d782b5a8d08767a7538741b75587cf1e67f44)), closes [#1142](https://github.com/Redocly/redoc/issues/1142)
|
||||
|
||||
|
||||
|
||||
# [2.0.0-rc.19](https://github.com/Redocly/redoc/compare/v2.0.0-rc.18...v2.0.0-rc.19) (2019-12-13)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* change the title of "Security Scheme Type" to match "HTTP Authorization Scheme" ([#1126](https://github.com/Redocly/redoc/issues/1126)) ([289c8e6](https://github.com/Redocly/redoc/commit/289c8e6ae1ff00371f86d3f2646607c64bc30050))
|
||||
* do not URI-encode parameter values for better readability ([6aeb0bf](https://github.com/Redocly/redoc/commit/6aeb0bf68df3f03f2ca1317f8b5787545bd363f1)), closes [#1138](https://github.com/Redocly/redoc/issues/1138)
|
||||
* fix sortByRequired (stabilise sort) ([#1136](https://github.com/Redocly/redoc/issues/1136)) ([d92434d](https://github.com/Redocly/redoc/commit/d92434d11b08e8b0f6be5453ec69aa1d0e0df79f)), closes [#1104](https://github.com/Redocly/redoc/issues/1104) [#1121](https://github.com/Redocly/redoc/issues/1121) [#1061](https://github.com/Redocly/redoc/issues/1061)
|
||||
* h2 padding on mobile ([7ed1a7e](https://github.com/Redocly/redoc/commit/7ed1a7ef0e7978a0dfb40afcc72c3362466f9624)), closes [#1118](https://github.com/Redocly/redoc/issues/1118)
|
||||
* python comment stripped in headings ([4a25aae](https://github.com/Redocly/redoc/commit/4a25aaef69fad814836392ea7e41eb32c182a261)), closes [#1116](https://github.com/Redocly/redoc/issues/1116)
|
||||
* remove hardcoded fontFamily for oneOf labels ([094ce91](https://github.com/Redocly/redoc/commit/094ce914e3f9cfe567b39db4ea88208014d8b686)), closes [#1120](https://github.com/Redocly/redoc/issues/1120)
|
||||
* search-box use theme ([1bf490c](https://github.com/Redocly/redoc/commit/1bf490c05b343d262f8819bf1ddc433e070be1b9))
|
||||
* support discriminator mapping 1-n ([6e390f9](https://github.com/Redocly/redoc/commit/6e390f9c7909da0b5d1d6fc571ab4ad92e715d6e)), closes [#1111](https://github.com/Redocly/redoc/issues/1111)
|
||||
* wrap json examples in code tag ([#1064](https://github.com/Redocly/redoc/issues/1064)) ([dc5430e](https://github.com/Redocly/redoc/commit/dc5430e53def780a81612d269cc3aea3f8785eea))
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* display `multipleOf` constrains ([#1065](https://github.com/Redocly/redoc/issues/1065)) ([3e90133](https://github.com/Redocly/redoc/commit/3e901336643b988ae45ae86c485005b8865e6e04))
|
||||
* enable menuToggle by default ([5d81abe](https://github.com/Redocly/redoc/commit/5d81abeb28c1e4f2826e41424c10163834c37e45))
|
||||
* new option hideSchemaTitles ([11cc4c4](https://github.com/Redocly/redoc/commit/11cc4c4c3e04a7e5bf3a9ebba20d10fa882a49e5))
|
||||
* new option payloadSampleIdx ([eaaa99d](https://github.com/Redocly/redoc/commit/eaaa99d68e2392273e8d9c0173db3b546e035d5f))
|
||||
* **cli:** Fallback on the spec's title before falling back on… ([#1073](https://github.com/Redocly/redoc/issues/1073)) ([e01eea4](https://github.com/Redocly/redoc/commit/e01eea445c93d74b66533c860d76bb3aff4d6df2))
|
||||
|
||||
|
||||
|
||||
# [2.0.0-rc.18](https://github.com/Redocly/redoc/compare/v2.0.0-rc.17...v2.0.0-rc.18) (2019-10-16)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* add oneOf buttons vertical space when wrapped to new line ([cd9fd61](https://github.com/Redocly/redoc/commit/cd9fd61))
|
||||
* improve mime-type dropdown font ([ce885f8](https://github.com/Redocly/redoc/commit/ce885f8))
|
||||
|
||||
|
||||
|
||||
# [2.0.0-rc.17](https://github.com/Redocly/redoc/compare/v2.0.0-rc.16...v2.0.0-rc.17) (2019-10-16)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* active menu item scroll into view ([0a01e9a](https://github.com/Redocly/redoc/commit/0a01e9a))
|
||||
* changed several components style font-family to monospace ([#1063](https://github.com/Redocly/redoc/issues/1063)) ([0c20e64](https://github.com/Redocly/redoc/commit/0c20e64)), closes [#909](https://github.com/Redocly/redoc/issues/909)
|
||||
* no quotes for default values in header fields. ([#1059](https://github.com/Redocly/redoc/issues/1059)) ([b5af71d](https://github.com/Redocly/redoc/commit/b5af71d))
|
||||
* types over-pluralization ([#1057](https://github.com/Redocly/redoc/issues/1057)) ([4494f80](https://github.com/Redocly/redoc/commit/4494f80)), closes [#1053](https://github.com/Redocly/redoc/issues/1053)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* added support for file paths as --options cli argument ([#1049](https://github.com/Redocly/redoc/issues/1049)) ([4adb927](https://github.com/Redocly/redoc/commit/4adb927))
|
||||
|
||||
|
||||
|
||||
# [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 highlighted ([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)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* fix escaping JSON string values ([58cb20d](https://github.com/Redocly/redoc/commit/58cb20d)), closes [#999](https://github.com/Redocly/redoc/issues/999)
|
||||
* revert expanding default server variables ([7849f7f](https://github.com/Redocly/redoc/commit/7849f7f))
|
||||
|
||||
|
||||
|
||||
# [2.0.0-rc.13](https://github.com/Redocly/redoc/compare/v2.0.0-rc.12...v2.0.0-rc.13) (2019-08-01)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* enum list doesn't wrap ([bfbb0c1](https://github.com/Redocly/redoc/commit/bfbb0c1)), closes [#993](https://github.com/Redocly/redoc/issues/993)
|
||||
* incorrect serialization of some parameter samples ([aba45db](https://github.com/Redocly/redoc/commit/aba45db)), closes [#992](https://github.com/Redocly/redoc/issues/992)
|
||||
* support json serialization for parameter examples ([1367380](https://github.com/Redocly/redoc/commit/1367380)), closes [#934](https://github.com/Redocly/redoc/issues/934)
|
||||
* unify accordion icons for responses section ([2afc2e4](https://github.com/Redocly/redoc/commit/2afc2e4)), closes [#975](https://github.com/Redocly/redoc/issues/975)
|
||||
* update to core.js 3 ([9e3375d](https://github.com/Redocly/redoc/commit/9e3375d)), closes [#997](https://github.com/Redocly/redoc/issues/997)
|
||||
|
||||
|
||||
|
||||
# [2.0.0-rc.12](https://github.com/Redocly/redoc/compare/v2.0.0-rc.11...v2.0.0-rc.12) (2019-07-30)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* rename ObjectDescription to SchemaDefinition as discussed ([4496622](https://github.com/Redocly/redoc/commit/4496622))
|
||||
|
||||
|
||||
|
||||
# [2.0.0-rc.11](https://github.com/Redocly/redoc/compare/v2.0.0-rc.10...v2.0.0-rc.11) (2019-07-30)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* do not add extra slashes to pattern ([70d1ee9](https://github.com/Redocly/redoc/commit/70d1ee9)), closes [#983](https://github.com/Redocly/redoc/issues/983)
|
||||
* dropdown fixes related to object description ([0504ad4](https://github.com/Redocly/redoc/commit/0504ad4))
|
||||
* incorrect serialization of parameter sample with hyphen ([f7dd658](https://github.com/Redocly/redoc/commit/f7dd658))
|
||||
* redoc-cli: Add missing content type header on compressed responses of `/` path
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* menu items from tags + md extension for Schema Definition ([#681](https://github.com/Redocly/redoc/pull/681))
|
||||
* new option `menuToggle` - fold active MenuItem if clicked ([#963](https://github.com/Redocly/redoc/issues/963))
|
||||
* Add option for skipping quotes in enums `enumSkipQuotes` ([#968](https://github.com/Redocly/redoc/issues/968)) ([afc7e36](https://github.com/Redocly/redoc/commit/afc7e36))
|
||||
* add `sampleCollapseLevel` option ([#937](https://github.com/Redocly/redoc/issues/937)) ([d3f1c16](https://github.com/Redocly/redoc/commit/d3f1c16))
|
||||
|
||||
# [2.0.0-rc.10](https://github.com/Redocly/redoc/compare/v2.0.0-rc.9...v2.0.0-rc.10) (2019-07-08)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* broken headings with single quote ([51d3b9b](https://github.com/Redocly/redoc/commit/51d3b9b)), closes [#955](https://github.com/Redocly/redoc/issues/955)
|
||||
* fix fields table overflow if deeply nested with long title ([12b7057](https://github.com/Redocly/redoc/commit/12b7057))
|
||||
* hide empty example when it is not defined ([4bd499f](https://github.com/Redocly/redoc/commit/4bd499f))
|
||||
* markdown in examples descriptions + minor ui tweaks ([f52d9e8](https://github.com/Redocly/redoc/commit/f52d9e8))
|
||||
* organize response examples in dropdown and display description ([995e557](https://github.com/Redocly/redoc/commit/995e557))
|
||||
|
||||
|
||||
|
||||
# [2.0.0-rc.9](https://github.com/Redocly/redoc/compare/v2.0.0-rc.8-1...v2.0.0-rc.9) (2019-06-27)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* fix regression double slashes added to full URL display ([f29a4fe](https://github.com/Redocly/redoc/commit/f29a4fe))
|
||||
* IE11, add missing Object.assign polyfill ([888f04e](https://github.com/Redocly/redoc/commit/888f04e))
|
||||
* serialize parameter example values according to the spec ([#917](https://github.com/Redocly/redoc/issues/917)) ([3939286](https://github.com/Redocly/redoc/commit/3939286))
|
||||
* styled-component style error in tabs ([#946](https://github.com/Redocly/redoc/issues/946)) ([c488bbf](https://github.com/Redocly/redoc/commit/c488bbf))
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* add x-additionalPropertiesName ([#622](https://github.com/Redocly/redoc/issues/622)) ([#944](https://github.com/Redocly/redoc/issues/944)) ([0eb1e66](https://github.com/Redocly/redoc/commit/0eb1e66))
|
||||
|
||||
|
||||
|
||||
# [2.0.0-rc.8-1](https://github.com/Rebilly/ReDoc/compare/v2.0.0-rc.8...v2.0.0-rc.8-1) (2019-05-13)
|
||||
|
||||
|
||||
|
@ -31,7 +256,7 @@
|
|||
|
||||
### Bug Fixes
|
||||
|
||||
* broken schema talbes with long enums ([3a74b74](https://github.com/Rebilly/ReDoc/commit/3a74b74))
|
||||
* broken schema tables with long enums ([3a74b74](https://github.com/Rebilly/ReDoc/commit/3a74b74))
|
||||
* deep linking sometimes not working when sent over messengers ([2491d97](https://github.com/Rebilly/ReDoc/commit/2491d97))
|
||||
|
||||
|
||||
|
@ -49,7 +274,7 @@
|
|||
* IE11 add missing fetch and URL polyfills ([d2ce1bd](https://github.com/Rebilly/ReDoc/commit/d2ce1bd)), closes [#875](https://github.com/Rebilly/ReDoc/issues/875)
|
||||
* ignore empty x-tagGroups array ([#869](https://github.com/Rebilly/ReDoc/issues/869)) ([4366a0d](https://github.com/Rebilly/ReDoc/commit/4366a0d))
|
||||
* incorrect detected schema title for deeply inherited schemas ([7d7b4e3](https://github.com/Rebilly/ReDoc/commit/7d7b4e3))
|
||||
* pluralize arrray of types ([fdcac30](https://github.com/Rebilly/ReDoc/commit/fdcac30))
|
||||
* pluralize array of types ([fdcac30](https://github.com/Rebilly/ReDoc/commit/fdcac30))
|
||||
* remove huge space after Authentication section ([548fae3](https://github.com/Rebilly/ReDoc/commit/548fae3)), closes [#872](https://github.com/Rebilly/ReDoc/issues/872)
|
||||
* remove query string from server URL ([#895](https://github.com/Rebilly/ReDoc/issues/895)) ([64453ff](https://github.com/Rebilly/ReDoc/commit/64453ff))
|
||||
* remove tabs top margin ([5c187f3](https://github.com/Rebilly/ReDoc/commit/5c187f3))
|
||||
|
@ -133,7 +358,7 @@
|
|||
* improve scrolling performance in Chrome with non-wrapped json examples ([a69c402](https://github.com/Rebilly/ReDoc/commit/a69c402))
|
||||
* nested oneOf button spacing ([3673720](https://github.com/Rebilly/ReDoc/commit/3673720)), closes [#719](https://github.com/Rebilly/ReDoc/issues/719)
|
||||
* onLoaded callback not run on spec error ([e77df0c](https://github.com/Rebilly/ReDoc/commit/e77df0c)), closes [#690](https://github.com/Rebilly/ReDoc/issues/690)
|
||||
* theme improvments by [@stasiukanya](https://github.com/stasiukanya) ([e2d0cd5](https://github.com/Rebilly/ReDoc/commit/e2d0cd5))
|
||||
* theme improvements by [@stasiukanya](https://github.com/stasiukanya) ([e2d0cd5](https://github.com/Rebilly/ReDoc/commit/e2d0cd5))
|
||||
* **cli:** old peer dependency issue with styled-components ([#699](https://github.com/Rebilly/ReDoc/issues/699)) ([9e2853c](https://github.com/Rebilly/ReDoc/commit/9e2853c))
|
||||
|
||||
|
||||
|
@ -205,7 +430,7 @@
|
|||
|
||||
### Bug Fixes
|
||||
|
||||
* addd indent to array schema internals ([865f3ce](https://github.com/Rebilly/ReDoc/commit/865f3ce))
|
||||
* add indent to array schema internals ([865f3ce](https://github.com/Rebilly/ReDoc/commit/865f3ce))
|
||||
* fix oneOf/anyOf titles ([39b930d](https://github.com/Rebilly/ReDoc/commit/39b930d)), closes [#618](https://github.com/Rebilly/ReDoc/issues/618) [#621](https://github.com/Rebilly/ReDoc/issues/621)
|
||||
|
||||
|
||||
|
@ -251,7 +476,7 @@
|
|||
### Bug Fixes
|
||||
|
||||
* add some spacing between operation description and parameters ([597688e](https://github.com/Rebilly/ReDoc/commit/597688e))
|
||||
* description is not rendered if doesn't containt markdown headings ([90ed717](https://github.com/Rebilly/ReDoc/commit/90ed717)), closes [#591](https://github.com/Rebilly/ReDoc/issues/591)
|
||||
* description is not rendered if doesn't contain markdown headings ([90ed717](https://github.com/Rebilly/ReDoc/commit/90ed717)), closes [#591](https://github.com/Rebilly/ReDoc/issues/591)
|
||||
* download button downloads index.html instead of spec with CLI ([334f904](https://github.com/Rebilly/ReDoc/commit/334f904)), closes [#594](https://github.com/Rebilly/ReDoc/issues/594)
|
||||
* fix Authentication section is not rendered ([2ecc8bc](https://github.com/Rebilly/ReDoc/commit/2ecc8bc)), closes [#590](https://github.com/Rebilly/ReDoc/issues/590)
|
||||
* fix linebreaks in multiparagraph field descriptions ([8fb9cd6](https://github.com/Rebilly/ReDoc/commit/8fb9cd6))
|
||||
|
@ -562,7 +787,7 @@
|
|||
|
||||
### Bug Fixes
|
||||
|
||||
* Path parameters are not correctly overriden ([c406dc5](https://github.com/Rebilly/ReDoc/commit/c406dc5)), closes [#400](https://github.com/Rebilly/ReDoc/issues/400)
|
||||
* Path parameters are not correctly overridden ([c406dc5](https://github.com/Rebilly/ReDoc/commit/c406dc5)), closes [#400](https://github.com/Rebilly/ReDoc/issues/400)
|
||||
* Use parentNode instead of parentElement to fix IE11 crash ([e8adb60](https://github.com/Rebilly/ReDoc/commit/e8adb60)), closes [#406](https://github.com/Rebilly/ReDoc/issues/406)
|
||||
|
||||
|
||||
|
@ -724,7 +949,7 @@
|
|||
* do not ignore path level parameters ([14f8408](https://github.com/Rebilly/Redoc/commit/14f8408))
|
||||
* improve rendering of types ([17da7b7](https://github.com/Rebilly/Redoc/commit/17da7b7))
|
||||
* move title propagation to the correct place ([0b0bc99](https://github.com/Rebilly/Redoc/commit/0b0bc99))
|
||||
* owerwrite text-align to left ([bfee3ed](https://github.com/Rebilly/Redoc/commit/bfee3ed))
|
||||
* overwrite text-align to left ([bfee3ed](https://github.com/Rebilly/Redoc/commit/bfee3ed))
|
||||
|
||||
|
||||
### Features
|
||||
|
@ -786,7 +1011,7 @@ Complete rewrite also means that this rewrite may introduce issues, but they sho
|
|||
|
||||
### Deprecations
|
||||
|
||||
- Fonts are not loaded by ReDoc so you should load them. Default fonts can be loaded as bellow:
|
||||
- Fonts are not loaded by ReDoc so you should load them. Default fonts can be loaded as below:
|
||||
|
||||
```html
|
||||
<link href="https://fonts.googleapis.com/css?family=Montserrat:300,400,700|Roboto:300,400,700" rel="stylesheet">
|
||||
|
@ -1155,7 +1380,7 @@ closes [#321](https://github.com/Rebilly/ReDoc/issues/321)
|
|||
### Bug fixes
|
||||
* Update webpack to the latest beta ([#143](https://github.com/Rebilly/ReDoc/issues/143))
|
||||
* Fix read-only fields appear in request samples ([#142](https://github.com/Rebilly/ReDoc/issues/142))
|
||||
* A few more minor UI improvemnts
|
||||
* A few more minor UI improvements
|
||||
|
||||
### Features/Improvements
|
||||
* Major performance optimization with new option `lazy-rendering`
|
||||
|
|
60
README.md
60
README.md
|
@ -64,11 +64,11 @@ Additionally, all the 1.x releases are hosted on our GitHub Pages-based CDN **(d
|
|||
| 1.17.x | 2.0 |
|
||||
|
||||
## Some Real-life usages
|
||||
- [Rebilly](https://rebilly.github.io/RebillyAPI)
|
||||
- [Rebilly](https://rebilly-api.redoc.ly/)
|
||||
- [Docker Engine](https://docs.docker.com/engine/api/v1.25/)
|
||||
- [Zuora](https://www.zuora.com/developer/api-reference/)
|
||||
- [Shopify Draft Orders](https://help.shopify.com/api/draft-orders)
|
||||
- [Discourse](http://docs.discourse.org)
|
||||
- [Commbox](https://www.commbox.io/api/)
|
||||
- [APIs.guru](https://apis.guru/api-doc/)
|
||||
- [FastAPI](https://github.com/tiangolo/fastapi)
|
||||
|
||||
|
@ -107,14 +107,14 @@ That's all folks!
|
|||
**IMPORTANT NOTE:** if you work with untrusted user spec, use `untrusted-spec` [option](#redoc-options-object) to prevent XSS security risks.
|
||||
|
||||
### 1. Install ReDoc (skip this step for CDN)
|
||||
Install using [yarn](https://yarnpkg.com):
|
||||
Install using [npm](https://docs.npmjs.com/getting-started/what-is-npm):
|
||||
|
||||
npm i redoc
|
||||
|
||||
or using [yarn](https://yarnpkg.com):
|
||||
|
||||
yarn add redoc
|
||||
|
||||
or using [npm](https://docs.npmjs.com/getting-started/what-is-npm):
|
||||
|
||||
npm install redoc --save
|
||||
|
||||
### 2. Reference redoc script in HTML
|
||||
For **CDN**:
|
||||
```html
|
||||
|
@ -138,7 +138,7 @@ For npm:
|
|||
|
||||
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:
|
||||
|
||||
|
@ -213,7 +213,7 @@ ReDoc makes use of the following [vendor extensions](https://swagger.io/specific
|
|||
* [`x-traitTag`](docs/redoc-vendor-extensions.md#x-traitTag) - useful for handling out common things like Pagination, Rate-Limits, etc
|
||||
* [`x-code-samples`](docs/redoc-vendor-extensions.md#x-code-samples) - specify operation code samples
|
||||
* [`x-examples`](docs/redoc-vendor-extensions.md#x-examples) - specify JSON example for requests
|
||||
* [`x-nullable`](docs/redoc-vendor-extensions.md#nullable) - mark schema param as a nullable
|
||||
* [`x-nullable`](docs/redoc-vendor-extensions.md#x-nullable) - mark schema param as a nullable
|
||||
* [`x-displayName`](docs/redoc-vendor-extensions.md#x-displayname) - specify human-friendly names for the menu categories
|
||||
* [`x-tagGroups`](docs/redoc-vendor-extensions.md#x-tagGroups) - group tags by categories in the side menu
|
||||
* [`x-servers`](docs/redoc-vendor-extensions.md#x-servers) - ability to specify different servers for API (backported from OpenAPI 3.0)
|
||||
|
@ -223,27 +223,33 @@ ReDoc makes use of the following [vendor extensions](https://swagger.io/specific
|
|||
### `<redoc>` options object
|
||||
You can use all of the following options with standalone version on <redoc> tag by kebab-casing them, e.g. `scrollYOffset` becomes `scroll-y-offset` and `expandResponses` becomes `expand-responses`.
|
||||
|
||||
* `untrustedSpec` - if set, the spec is considered untrusted and all HTML/markdown is sanitized to prevent XSS. **Disabled by default** for performance reasons. **Enable this option if you work with untrusted user data!**
|
||||
* `disableSearch` - disable search indexing and search box.
|
||||
* `expandDefaultServerVariables` - enable expanding default server variables, default `false`.
|
||||
* `expandResponses` - specify which responses to expand by default by response codes. Values should be passed as comma-separated list without spaces e.g. `expandResponses="200,201"`. Special value `"all"` expands all responses by default. Be careful: this option can slow-down documentation rendering time.
|
||||
* `hideDownloadButton` - do not show "Download" spec button. **THIS DOESN'T MAKE YOUR SPEC PRIVATE**, it just hides the button.
|
||||
* `hideHostname` - if set, the protocol and hostname is not shown in the operation definition.
|
||||
* `hideLoading` - do not show loading animation. Useful for small docs.
|
||||
* `hideSingleRequestSampleTab` - do not show the request sample tab for requests with only one sample.
|
||||
* `expandSingleSchemaField` - automatically expand single field in a schema
|
||||
* `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`.
|
||||
* `lazyRendering` - _Not implemented yet_ ~~if set, enables lazy rendering mode in ReDoc. This mode is useful for APIs with big number of operations (e.g. > 50). In this mode ReDoc shows initial screen ASAP and then renders the rest operations asynchronously while showing progress bar on the top. Check out the [demo](\\redocly.github.io/redoc) for the example.~~
|
||||
* `menuToggle` - if true clicking second time on expanded menu item will collapse it, default `false`.
|
||||
* `nativeScrollbars` - use native scrollbar for sidemenu instead of perfect-scroll (scrolling performance optimization for big specs).
|
||||
* `noAutoAuth` - do not inject Authentication section automatically.
|
||||
* `onlyRequiredInSamples` - shows only required fields in request samples.
|
||||
* `pathInMiddlePanel` - show path link and HTTP verb in the middle panel instead of the right one.
|
||||
* `requiredPropsFirst` - show required properties first ordered in the same order as in `required` array.
|
||||
* `scrollYOffset` - If set, specifies a vertical scroll-offset. This is often useful when there are fixed positioned elements at the top of the page, such as navbars, headers etc;
|
||||
`scrollYOffset` can be specified in various ways:
|
||||
* **number**: A fixed number of pixels to be used as offset;
|
||||
* **selector**: selector of the element to be used for specifying the offset. The distance from the top of the page to the element's bottom will be used as offset;
|
||||
* **function**: A getter function. Must return a number representing the offset (in pixels);
|
||||
* **number**: A fixed number of pixels to be used as offset.
|
||||
* **selector**: selector of the element to be used for specifying the offset. The distance from the top of the page to the element's bottom will be used as offset.
|
||||
* **function**: A getter function. Must return a number representing the offset (in pixels).
|
||||
* `showExtensions` - show vendor extensions ("x-" fields). Extensions used by ReDoc are ignored. Can be boolean or an array of `string` with names of extensions to display.
|
||||
* `sortPropsAlphabetically` - sort properties alphabetically.
|
||||
* `suppressWarnings` - if set, warnings are not rendered at the top of documentation (they still are logged to the console).
|
||||
* `lazyRendering` - _Not implemented yet_ ~~if set, enables lazy rendering mode in ReDoc. This mode is useful for APIs with big number of operations (e.g. > 50). In this mode ReDoc shows initial screen ASAP and then renders the rest operations asynchronously while showing progress bar on the top. Check out the [demo](\\redocly.github.io/redoc) for the example.~~
|
||||
* `hideHostname` - if set, the protocol and hostname is not shown in the operation definition.
|
||||
* `expandResponses` - specify which responses to expand by default by response codes. Values should be passed as comma-separated list without spaces e.g. `expandResponses="200,201"`. Special value `"all"` expands all responses by default. Be careful: this option can slow-down documentation rendering time.
|
||||
* `requiredPropsFirst` - show required properties first ordered in the same order as in `required` array.
|
||||
* `sortPropsAlphabetically` - sort properties alphabetically
|
||||
* `showExtensions` - show vendor extensions ("x-" fields). Extensions used by ReDoc are ignored. Can be boolean or an array of `string` with names of extensions to display
|
||||
* `noAutoAuth` - do not inject Authentication section automatically
|
||||
* `pathInMiddlePanel` - show path link and HTTP verb in the middle panel instead of the right one
|
||||
* `hideLoading` - do not show loading animation. Useful for small docs
|
||||
* `nativeScrollbars` - use native scrollbar for sidemenu instead of perfect-scroll (scrolling performance optimization for big specs)
|
||||
* `hideDownloadButton` - do not show "Download" spec button. **THIS DOESN'T MAKE YOUR SPEC PRIVATE**, it just hides the button.
|
||||
* `disableSearch` - disable search indexing and search box
|
||||
* `onlyRequiredInSamples` - shows only required fields in request samples.
|
||||
* `theme` - ReDoc theme. Not documented yet. For details check source code: [theme.ts](https://github.com/Redocly/redoc/blob/master/src/theme.ts)
|
||||
* `payloadSampleIdx` - if set, payload sample will be inserted at this index or last. Indexes start from 0.
|
||||
* `theme` - ReDoc theme. Not documented yet. For details check source code: [theme.ts](https://github.com/Redocly/redoc/blob/master/src/theme.ts).
|
||||
* `untrustedSpec` - if set, the spec is considered untrusted and all HTML/markdown is sanitized to prevent XSS. **Disabled by default** for performance reasons. **Enable this option if you work with untrusted user data!**
|
||||
|
||||
## Advanced usage of standalone version
|
||||
Instead of adding `spec-url` attribute to the `<redoc>` element you can initialize ReDoc via globally exposed `Redoc` object:
|
||||
|
|
|
@ -17,7 +17,7 @@ const localDistDir = './benchmark/revisions/local/bundles';
|
|||
sh.rm('-rf', localDistDir);
|
||||
console.log(`Building local dist: ${localDistDir}`);
|
||||
sh.mkdir('-p', localDistDir);
|
||||
exec(`yarn bundle:lib --output-path ${localDistDir}`);
|
||||
exec(`npm run bundle:lib --output-path ${localDistDir}`);
|
||||
|
||||
const revisions = [];
|
||||
for (const arg of args) {
|
||||
|
@ -32,7 +32,7 @@ const configDir = './benchmark/revisions/config.js';
|
|||
console.log(`Writing config "${configDir}"`);
|
||||
fs.writeFileSync(configDir, configFile);
|
||||
|
||||
console.log('Starging benchmark server');
|
||||
console.log('Starting benchmark server');
|
||||
const proc = spawn('npm', ['run', 'start:benchmark']);
|
||||
|
||||
proc.stdout.on('data', data => {
|
||||
|
@ -119,7 +119,7 @@ function buildRevisionDist(revision) {
|
|||
|
||||
const pwd = sh.pwd();
|
||||
sh.cd(buildDir);
|
||||
exec('yarn remove cypress puppeteer && yarn && yarn bundle:lib');
|
||||
exec('npm uninstall cypress puppeteer && npm install && npm run bundle:lib');
|
||||
sh.cd(pwd);
|
||||
return distDir;
|
||||
}
|
||||
|
|
|
@ -7,7 +7,7 @@
|
|||
# To run:
|
||||
# To display the command line options:
|
||||
# $ docker run --rm -it redoc-cli --help
|
||||
# .. will display the comand line help
|
||||
# .. will display the command line help
|
||||
#
|
||||
# To turn `swagger.yml` file in the current directory, to html documentation 'redoc-static.html'
|
||||
# $ docker run --rm -it -v $PWD:/data redoc-cli bundle swagger.yml
|
||||
|
|
88
cli/index.ts
88
cli/index.ts
|
@ -14,7 +14,14 @@ import * as zlib from 'zlib';
|
|||
import { createStore, loadAndBundleSpec, Redoc } from 'redoc';
|
||||
|
||||
import { watch } from 'chokidar';
|
||||
import { createReadStream, existsSync, readFileSync, ReadStream, writeFileSync } from 'fs';
|
||||
import {
|
||||
createReadStream,
|
||||
existsSync,
|
||||
lstatSync,
|
||||
readFileSync,
|
||||
ReadStream,
|
||||
writeFileSync,
|
||||
} from 'fs';
|
||||
import * as mkdirp from 'mkdirp';
|
||||
|
||||
import * as YargsParser from 'yargs';
|
||||
|
@ -25,6 +32,7 @@ interface Options {
|
|||
cdn?: boolean;
|
||||
output?: string;
|
||||
title?: string;
|
||||
disableGoogleFont?: boolean;
|
||||
port?: number;
|
||||
templateFileName?: string;
|
||||
templateOptions?: any;
|
||||
|
@ -68,9 +76,11 @@ YargsParser.command(
|
|||
watch: argv.watch as boolean,
|
||||
templateFileName: argv.template as string,
|
||||
templateOptions: argv.templateOptions || {},
|
||||
redocOptions: argv.options || {},
|
||||
redocOptions: getObjectOrJSON(argv.options),
|
||||
};
|
||||
|
||||
console.log(config);
|
||||
|
||||
try {
|
||||
await serve(argv.port as number, argv.spec as string, config);
|
||||
} catch (e) {
|
||||
|
@ -96,7 +106,12 @@ YargsParser.command(
|
|||
yargs.options('title', {
|
||||
describe: 'Page Title',
|
||||
type: 'string',
|
||||
default: 'ReDoc documentation',
|
||||
});
|
||||
|
||||
yargs.options('disableGoogleFont', {
|
||||
describe: 'Disable Google Font',
|
||||
type: 'boolean',
|
||||
default: false,
|
||||
});
|
||||
|
||||
yargs.option('cdn', {
|
||||
|
@ -108,15 +123,16 @@ YargsParser.command(
|
|||
yargs.demandOption('spec');
|
||||
return yargs;
|
||||
},
|
||||
async argv => {
|
||||
const config: Options = {
|
||||
async (argv: any) => {
|
||||
const config = {
|
||||
ssr: true,
|
||||
output: argv.o as string,
|
||||
cdn: argv.cdn as boolean,
|
||||
title: argv.title as string,
|
||||
disableGoogleFont: argv.disableGoogleFont as boolean,
|
||||
templateFileName: argv.template as string,
|
||||
templateOptions: argv.templateOptions || {},
|
||||
redocOptions: argv.options || {},
|
||||
redocOptions: getObjectOrJSON(argv.options),
|
||||
};
|
||||
|
||||
try {
|
||||
|
@ -156,7 +172,9 @@ async function serve(port: number, pathToSpec: string, options: Options = {}) {
|
|||
},
|
||||
);
|
||||
} else if (request.url === '/') {
|
||||
respondWithGzip(pageHTML, request, response);
|
||||
respondWithGzip(pageHTML, request, response, {
|
||||
'Content-Type': 'text/html',
|
||||
});
|
||||
} else if (request.url === '/spec.json') {
|
||||
const specStr = JSON.stringify(spec, null, 2);
|
||||
respondWithGzip(specStr, request, response, {
|
||||
|
@ -178,14 +196,14 @@ async function serve(port: number, pathToSpec: string, options: Options = {}) {
|
|||
if (options.watch && existsSync(pathToSpec)) {
|
||||
const pathToSpecDirectory = resolve(dirname(pathToSpec));
|
||||
const watchOptions = {
|
||||
ignored: /(^|[\/\\])\../,
|
||||
ignored: [/(^|[\/\\])\../, /___jb_[a-z]+___$/],
|
||||
ignoreInitial: true,
|
||||
};
|
||||
|
||||
const watcher = watch(pathToSpecDirectory, watchOptions);
|
||||
const log = console.log.bind(console);
|
||||
watcher
|
||||
.on('change', async path => {
|
||||
log(`${path} changed, updating docs`);
|
||||
|
||||
const handlePath = async _path => {
|
||||
try {
|
||||
spec = await loadAndBundleSpec(pathToSpec);
|
||||
pageHTML = await getPageHTML(spec, pathToSpec, options);
|
||||
|
@ -193,6 +211,19 @@ async function serve(port: number, pathToSpec: string, options: Options = {}) {
|
|||
} catch (e) {
|
||||
console.error('Error while updating: ', e.message);
|
||||
}
|
||||
};
|
||||
|
||||
watcher
|
||||
.on('change', async path => {
|
||||
log(`${path} changed, updating docs`);
|
||||
handlePath(path);
|
||||
})
|
||||
.on('add', async path => {
|
||||
log(`File ${path} added, updating docs`);
|
||||
handlePath(path);
|
||||
})
|
||||
.on('addDir', path => {
|
||||
log(`↗ Directory ${path} added. Files in here will trigger reload.`);
|
||||
})
|
||||
.on('error', error => console.error(`Watcher error: ${error}`))
|
||||
.on('ready', () => log(`👀 Watching ${pathToSpecDirectory} for changes...`));
|
||||
|
@ -216,7 +247,15 @@ async function bundle(pathToSpec, options: Options = {}) {
|
|||
async function getPageHTML(
|
||||
spec: any,
|
||||
pathToSpec: string,
|
||||
{ ssr, cdn, title, templateFileName, templateOptions, redocOptions = {} }: Options,
|
||||
{
|
||||
ssr,
|
||||
cdn,
|
||||
title,
|
||||
disableGoogleFont,
|
||||
templateFileName,
|
||||
templateOptions,
|
||||
redocOptions = {},
|
||||
}: Options,
|
||||
) {
|
||||
let html;
|
||||
let css;
|
||||
|
@ -258,7 +297,8 @@ async function getPageHTML(
|
|||
? '<script src="https://unpkg.com/redoc@next/bundles/redoc.standalone.js"></script>'
|
||||
: `<script>${redocStandaloneSrc}</script>`) + css
|
||||
: '<script src="redoc.standalone.js"></script>',
|
||||
title,
|
||||
title: title || spec.info.title || 'ReDoc documentation',
|
||||
disableGoogleFont,
|
||||
templateOptions,
|
||||
});
|
||||
}
|
||||
|
@ -321,3 +361,25 @@ function handleError(error: Error) {
|
|||
console.error(error.stack);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
function getObjectOrJSON(options) {
|
||||
switch (typeof options) {
|
||||
case 'object':
|
||||
return options;
|
||||
case 'string':
|
||||
try {
|
||||
if (existsSync(options) && lstatSync(options).isFile()) {
|
||||
return JSON.parse(readFileSync(options, 'utf-8'));
|
||||
} else {
|
||||
return JSON.parse(options);
|
||||
}
|
||||
} catch (e) {
|
||||
console.log(
|
||||
`Encountered error:\n\n${options}\n\nis neither a file with a valid JSON object neither a stringified JSON object.`,
|
||||
);
|
||||
handleError(e);
|
||||
}
|
||||
default:
|
||||
return {};
|
||||
}
|
||||
}
|
||||
|
|
2246
cli/package-lock.json
generated
Normal file
2246
cli/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
|
@ -1,36 +1,35 @@
|
|||
{
|
||||
"name": "redoc-cli",
|
||||
"version": "0.8.4",
|
||||
"version": "0.9.7",
|
||||
"description": "ReDoc's Command Line Interface",
|
||||
"main": "index.js",
|
||||
"bin": "index.js",
|
||||
"repository": "https://github.com/Redocly/redoc",
|
||||
"author": "Roman Hotsiy <gotsijroman@gmail.com>",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 8"
|
||||
},
|
||||
"dependencies": {
|
||||
"chokidar": "^2.0.4",
|
||||
"handlebars": "^4.0.11",
|
||||
"isarray": "^2.0.4",
|
||||
"chokidar": "^3.0.2",
|
||||
"handlebars": "^4.1.2",
|
||||
"isarray": "^2.0.5",
|
||||
"mkdirp": "^0.5.1",
|
||||
"mobx": "^4.2.0",
|
||||
"node-libs-browser": "^2.2.0",
|
||||
"react": "^16.8.4",
|
||||
"react-dom": "^16.8.4",
|
||||
"redoc": "^2.0.0-rc.8-1",
|
||||
"styled-components": "^4.1.3",
|
||||
"tslib": "^1.9.3",
|
||||
"yargs": "^12.0.5"
|
||||
},
|
||||
"scripts": {
|
||||
"ci-publish": "ci-publish"
|
||||
"node-libs-browser": "^2.2.1",
|
||||
"react": "^16.8.6",
|
||||
"react-dom": "^16.8.6",
|
||||
"redoc": "2.0.0-rc.24",
|
||||
"styled-components": "^4.3.2",
|
||||
"tslib": "^1.10.0",
|
||||
"yargs": "^13.3.0"
|
||||
},
|
||||
"publishConfig": {
|
||||
"access": "public"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/chokidar": "^1.7.5",
|
||||
"@types/chokidar": "^2.1.3",
|
||||
"@types/handlebars": "^4.0.39",
|
||||
"@types/mkdirp": "^0.5.2",
|
||||
"ci-publish": "^1.3.1"
|
||||
"@types/mkdirp": "^0.5.2"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -13,7 +13,7 @@
|
|||
}
|
||||
</style>
|
||||
{{{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}}
|
||||
</head>
|
||||
|
||||
<body>
|
||||
|
|
4756
cli/yarn.lock
4756
cli/yarn.lock
File diff suppressed because it is too large
Load Diff
|
@ -11,8 +11,8 @@ RUN apk update && apk add --no-cache git
|
|||
|
||||
# Install dependencies
|
||||
WORKDIR /build
|
||||
COPY package.json yarn.lock /build/
|
||||
RUN yarn install --frozen-lockfile --ignore-optional --ignore-scripts
|
||||
COPY package.json package-lock.json /build/
|
||||
RUN npm ci --no-optional --ignore-scripts
|
||||
|
||||
# copy only required for the build files
|
||||
COPY src /build/src
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
#!/bin/bash
|
||||
|
||||
# DockerHub cd into Dockerfile location before buil
|
||||
# DockerHub cd into Dockerfile location before build
|
||||
# So we have to undo this.
|
||||
cd ../..
|
||||
docker build -f config/docker/Dockerfile -t $IMAGE_NAME .
|
||||
|
|
|
@ -53,7 +53,7 @@
|
|||
},
|
||||
{
|
||||
"name": "Contacts",
|
||||
"description": "Contacts are Customer's address book.\nAll contact information used in Invoices, Subscriptions, Transacions, etc is enlisted here. Hovewer, changing a Contact won't change corresponding contact information in related resources\n"
|
||||
"description": "Contacts are Customer's address book.\nAll contact information used in Invoices, Subscriptions, Transacions, etc is enlisted here. However, changing a Contact won't change corresponding contact information in related resources\n"
|
||||
},
|
||||
{
|
||||
"name": "Coupons",
|
||||
|
@ -93,7 +93,7 @@
|
|||
},
|
||||
{
|
||||
"name": "Files",
|
||||
"description": "A File is an entity that can store a phyiscal file and some metadata. It also provides an easy access to\nits size, mime-type, user-defined tags and description thus allowing easy sorting and searching among stored\nfiles.\nThere are several methods of file uploading available: multipart/form-data encoded form, RAW POST (by sending\nfile contents as POST body), fetching from URL (by providing the file URL via 'url' param)\nAttachment is an entity that is used to link a File to one or multiple objects like Customer, Dispute, Payment,\nTransaction, Subscription, Plan, Product, Invoice, Note. That allows to quickly find and use files related to\nthose specific entities.\n"
|
||||
"description": "A File is an entity that can store a physical file and some metadata. It also provides an easy access to\nits size, mime-type, user-defined tags and description thus allowing easy sorting and searching among stored\nfiles.\nThere are several methods of file uploading available: multipart/form-data encoded form, RAW POST (by sending\nfile contents as POST body), fetching from URL (by providing the file URL via 'url' param)\nAttachment is an entity that is used to link a File to one or multiple objects like Customer, Dispute, Payment,\nTransaction, Subscription, Plan, Product, Invoice, Note. That allows to quickly find and use files related to\nthose specific entities.\n"
|
||||
},
|
||||
{
|
||||
"name": "Gateway Accounts",
|
||||
|
@ -15623,7 +15623,7 @@
|
|||
"description": "Reset user password\n",
|
||||
"responses": {
|
||||
"201": {
|
||||
"description": "Password was reseted successfully",
|
||||
"description": "Password was reset successfully",
|
||||
"headers": {
|
||||
"Rate-Limit-Limit": {
|
||||
"description": "The number of allowed requests in the current period",
|
||||
|
@ -23851,17 +23851,17 @@
|
|||
"type": "string"
|
||||
},
|
||||
"totpRequired": {
|
||||
"description": "The user setting of two-factor authentification",
|
||||
"description": "The user setting of two-factor authentication",
|
||||
"readOnly": true,
|
||||
"type": "boolean"
|
||||
},
|
||||
"totpSecret": {
|
||||
"description": "The user TOTP key for authentification app (if TOTP enabled)",
|
||||
"description": "The user TOTP key for authentication app (if TOTP enabled)",
|
||||
"readOnly": true,
|
||||
"type": "string"
|
||||
},
|
||||
"totpUrl": {
|
||||
"description": "The user link to QR-code for TOTP authentification app (if TOTP enabled)",
|
||||
"description": "The user link to QR-code for TOTP authentication app (if TOTP enabled)",
|
||||
"readOnly": true,
|
||||
"type": "string",
|
||||
"format": "url"
|
||||
|
@ -24250,7 +24250,7 @@
|
|||
},
|
||||
"bodyHtml": {
|
||||
"type": "string",
|
||||
"description": "Leave empty to recieve \"text/plain\" email.\nThe template palceholders are allowed.\n"
|
||||
"description": "Leave empty to receive \"text/plain\" email.\nThe template palceholders are allowed.\n"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
|
@ -26495,15 +26495,15 @@
|
|||
}
|
||||
},
|
||||
"totpRequired": {
|
||||
"description": "The user setting of two-factor authentification",
|
||||
"description": "The user setting of two-factor authentication",
|
||||
"type": "boolean"
|
||||
},
|
||||
"totpSecret": {
|
||||
"description": "The user TOTP key for authentification app (if TOTP enabled)",
|
||||
"description": "The user TOTP key for authentication app (if TOTP enabled)",
|
||||
"type": "string"
|
||||
},
|
||||
"totpUrl": {
|
||||
"description": "The user link to QR-code for TOTP authentification app (if TOTP enabled)",
|
||||
"description": "The user link to QR-code for TOTP authentication app (if TOTP enabled)",
|
||||
"type": "string",
|
||||
"format": "url"
|
||||
},
|
||||
|
@ -26984,7 +26984,7 @@
|
|||
"collectionExpand": {
|
||||
"name": "expand",
|
||||
"in": "query",
|
||||
"description": "Expand response to get full related object intead of ID. See the expand guide for more info.",
|
||||
"description": "Expand response to get full related object instead of ID. See the expand guide for more info.",
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
|
|
|
@ -11,7 +11,7 @@ const demos = [
|
|||
value: 'https://api.apis.guru/v2/specs/googleapis.com/calendar/v3/swagger.yaml',
|
||||
label: 'Google Calendar',
|
||||
},
|
||||
{ value: 'https://api.apis.guru/v2/specs/slack.com/1.0.6/swagger.yaml', label: 'Slack' },
|
||||
{ value: 'https://api.apis.guru/v2/specs/slack.com/1.2.0/swagger.yaml', label: 'Slack' },
|
||||
{ value: 'https://api.apis.guru/v2/specs/zoom.us/2.0.0/swagger.yaml', label: 'Zoom.us' },
|
||||
{
|
||||
value: 'https://api.apis.guru/v2/specs/graphhopper.com/1.0/swagger.yaml',
|
||||
|
|
|
@ -38,7 +38,7 @@ info:
|
|||
OAuth2 - an open protocol to allow secure authorization in a simple
|
||||
and standard method from web, mobile and desktop applications.
|
||||
|
||||
<security-definitions />
|
||||
<SecurityDefinitions />
|
||||
|
||||
version: 1.0.0
|
||||
title: Swagger Petstore
|
||||
|
@ -63,6 +63,14 @@ tags:
|
|||
description: Access to Petstore orders
|
||||
- name: user
|
||||
description: Operations about user
|
||||
- name: pet_model
|
||||
x-displayName: The Pet Model
|
||||
description: |
|
||||
<SchemaDefinition schemaRef="#/components/schemas/Pet" />
|
||||
- name: store_model
|
||||
x-displayName: The Order Model
|
||||
description: |
|
||||
<SchemaDefinition schemaRef="#/components/schemas/Order" exampleRef="#/components/examples/Order" showReadOnly={true} showWriteOnly={true} />
|
||||
x-tagGroups:
|
||||
- name: General
|
||||
tags:
|
||||
|
@ -71,9 +79,21 @@ x-tagGroups:
|
|||
- name: User Management
|
||||
tags:
|
||||
- user
|
||||
- name: Models
|
||||
tags:
|
||||
- pet_model
|
||||
- store_model
|
||||
paths:
|
||||
/pet:
|
||||
parameters:
|
||||
- name: Accept-Language
|
||||
in: header
|
||||
description: "The language you prefer for messages. Supported values are en-AU, en-CA, en-GB, en-US"
|
||||
example: en-US
|
||||
required: false
|
||||
schema:
|
||||
type: string
|
||||
default: en-AU
|
||||
- name: cookieParam
|
||||
in: cookie
|
||||
description: Some cookie
|
||||
|
@ -286,7 +306,7 @@ paths:
|
|||
tags:
|
||||
- pet
|
||||
summary: Finds Pets by status
|
||||
description: Multiple status values can be provided with comma seperated strings
|
||||
description: Multiple status values can be provided with comma separated strings
|
||||
operationId: findPetsByStatus
|
||||
parameters:
|
||||
- name: status
|
||||
|
@ -331,7 +351,7 @@ paths:
|
|||
- pet
|
||||
summary: Finds Pets by tags
|
||||
description: >-
|
||||
Muliple tags can be provided with comma seperated strings. Use tag1,
|
||||
Multiple tags can be provided with comma separated strings. Use tag1,
|
||||
tag2, tag3 for testing.
|
||||
operationId: findPetsByTags
|
||||
deprecated: true
|
||||
|
@ -611,7 +631,7 @@ paths:
|
|||
type: integer
|
||||
format: int32
|
||||
X-Expires-After:
|
||||
description: date in UTC when toekn expires
|
||||
description: date in UTC when token expires
|
||||
schema:
|
||||
type: string
|
||||
format: date-time
|
||||
|
@ -666,6 +686,7 @@ components:
|
|||
type: string
|
||||
description: The measured skill for hunting
|
||||
default: lazy
|
||||
example: adventurous
|
||||
enum:
|
||||
- clueless
|
||||
- lazy
|
||||
|
@ -717,6 +738,7 @@ components:
|
|||
type: number
|
||||
description: Average amount of honey produced per day in ounces
|
||||
example: 3.14
|
||||
multipleOf: .01
|
||||
required:
|
||||
- honeyPerDay
|
||||
Id:
|
||||
|
@ -754,6 +776,11 @@ components:
|
|||
description: Indicates whenever order was completed or not
|
||||
type: boolean
|
||||
default: false
|
||||
readOnly: true
|
||||
requestId:
|
||||
description: Unique Request Id
|
||||
type: string
|
||||
writeOnly: true
|
||||
xml:
|
||||
name: Order
|
||||
Pet:
|
||||
|
@ -866,14 +893,13 @@ components:
|
|||
as well as digits
|
||||
format: password
|
||||
minLength: 8
|
||||
pattern: '(?=.*[a-z])(?=.*[A-Z])(?=.*[0-9])'
|
||||
pattern: '/(?=.*[a-z])(?=.*[A-Z])(?=.*[0-9])/'
|
||||
example: drowssaP123
|
||||
phone:
|
||||
description: User phone number in international format
|
||||
type: string
|
||||
pattern: '^\+(?:[0-9]-?){6,14}[0-9]$'
|
||||
pattern: '/^\+(?:[0-9]-?){6,14}[0-9]$/'
|
||||
example: +1-202-555-0192
|
||||
nullable: true
|
||||
userStatus:
|
||||
description: User status
|
||||
type: integer
|
||||
|
@ -926,3 +952,10 @@ components:
|
|||
type: apiKey
|
||||
name: api_key
|
||||
in: header
|
||||
examples:
|
||||
Order:
|
||||
value:
|
||||
quantity: 1,
|
||||
shipDate: 2018-10-19T16:46:45Z,
|
||||
status: placed,
|
||||
complete: false
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
import * as React from 'react';
|
||||
import { render } from 'react-dom';
|
||||
// tslint:disable-next-line
|
||||
import { AppContainer } from 'react-hot-loader';
|
||||
// import DevTools from 'mobx-react-devtools';
|
||||
|
||||
|
|
|
@ -631,7 +631,7 @@ paths:
|
|||
X-Expires-After:
|
||||
type: string
|
||||
format: date-time
|
||||
description: date in UTC when toekn expires
|
||||
description: date in UTC when token expires
|
||||
'400':
|
||||
description: Invalid username/password supplied
|
||||
/user/logout:
|
||||
|
|
|
@ -38,7 +38,6 @@ const babelLoader = mode => ({
|
|||
['@babel/plugin-syntax-decorators', { legacy: true }],
|
||||
'@babel/plugin-syntax-dynamic-import',
|
||||
'@babel/plugin-syntax-jsx',
|
||||
mode !== 'production' ? 'react-hot-loader/babel' : undefined,
|
||||
[
|
||||
'babel-plugin-styled-components',
|
||||
{
|
||||
|
@ -50,6 +49,13 @@ const babelLoader = mode => ({
|
|||
},
|
||||
});
|
||||
|
||||
const babelHotLoader = {
|
||||
loader: 'babel-loader',
|
||||
options: {
|
||||
plugins: ['react-hot-loader/babel'],
|
||||
},
|
||||
};
|
||||
|
||||
export default (env: { playground?: boolean; bench?: boolean } = {}, { mode }) => ({
|
||||
entry: [
|
||||
root('../src/polyfills.ts'),
|
||||
|
@ -105,7 +111,11 @@ export default (env: { playground?: boolean; bench?: boolean } = {}, { mode }) =
|
|||
{ test: [/\.eot$/, /\.gif$/, /\.woff$/, /\.svg$/, /\.ttf$/], use: 'null-loader' },
|
||||
{
|
||||
test: /\.tsx?$/,
|
||||
use: [tsLoader(env), babelLoader(mode)],
|
||||
use: compact([
|
||||
mode !== 'production' ? babelHotLoader : undefined,
|
||||
tsLoader(env),
|
||||
babelLoader(mode),
|
||||
]),
|
||||
exclude: [/node_modules/],
|
||||
},
|
||||
{
|
||||
|
|
|
@ -6,7 +6,7 @@ describe('Menu', () => {
|
|||
it('should have valid items count', () => {
|
||||
cy.get('.menu-content')
|
||||
.find('li')
|
||||
.should('have.length', 6 + (2 + 8 + 4) + (1 + 8));
|
||||
.should('have.length', 6 + (2 + 8 + 1 + 4 + 2) + (1 + 8));
|
||||
});
|
||||
|
||||
it('should sync active menu items while scroll', () => {
|
||||
|
|
|
@ -42,7 +42,7 @@ describe('Servers', () => {
|
|||
// TODO add cy-data attributes
|
||||
cy.get('[data-section-id="operation/addPet"]').should(
|
||||
'contain',
|
||||
'http://localhost:' + win.location.port + '/e2e/pet',
|
||||
'http://localhost:' + win.location.port + '/pet',
|
||||
);
|
||||
});
|
||||
});
|
||||
|
@ -57,7 +57,7 @@ describe('Servers', () => {
|
|||
// TODO add cy-data attributes
|
||||
cy.get('[data-section-id="operation/addPet"]').should(
|
||||
'contain',
|
||||
'http://localhost:' + win.location.port + '/e2e/pet',
|
||||
'http://localhost:' + win.location.port + '/pet',
|
||||
);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -27,6 +27,8 @@ describe('Search', () => {
|
|||
it('should support arrow navigation', () => {
|
||||
getSearchInput().type('int', { force: true });
|
||||
|
||||
cy.wait(500);
|
||||
|
||||
getSearchInput().type('{downarrow}', { force: true });
|
||||
getResult(0).should('have.class', 'active');
|
||||
|
||||
|
|
17990
package-lock.json
generated
Normal file
17990
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
159
package.json
159
package.json
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "otx-redoc",
|
||||
"version": "2.0.0-rc.2",
|
||||
"name": "redoc",
|
||||
"version": "2.0.0-rc.24",
|
||||
"description": "ReDoc",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
|
@ -28,7 +28,7 @@
|
|||
"start": "webpack-dev-server --mode=development --env.playground --hot --config demo/webpack.config.ts",
|
||||
"start:prod": "webpack-dev-server --env.playground --mode=production --config demo/webpack.config.ts",
|
||||
"start:benchmark": "webpack-dev-server --mode=production --env.bench --config demo/webpack.config.ts",
|
||||
"test": "npm run lint && npm run unit && npm run bundlesize && npm run license-check",
|
||||
"test": "npm run lint && npm run unit && npm run license-check",
|
||||
"unit": "jest --coverage",
|
||||
"e2e": "cypress run",
|
||||
"e2e-ci": "cypress run --record",
|
||||
|
@ -40,9 +40,9 @@
|
|||
"bundle": "npm run bundle:clean && npm run bundle:lib && npm run bundle:standalone",
|
||||
"declarations": "tsc --emitDeclarationOnly -p tsconfig.lib.json && cp -R src/types typings/",
|
||||
"stats": "webpack --env.standalone --json --profile --mode=production > stats.json",
|
||||
"prettier": "prettier --write \"src/**/*.{ts,tsx}\"",
|
||||
"prettier": "prettier --write \"cli/index.ts\" \"src/**/*.{ts,tsx}\"",
|
||||
"changelog": "conventional-changelog -p angular -i CHANGELOG.md -s -r 1",
|
||||
"lint": "tslint --project tsconfig.json",
|
||||
"lint": "eslint 'src/**/*.{js,ts,tsx}'",
|
||||
"benchmark": "node ./benchmark/benchmark.js",
|
||||
"start:demo": "webpack-dev-server --hot --config demo/webpack.config.ts --mode=development",
|
||||
"compile:cli": "tsc custom.d.ts cli/index.ts --target es6 --module commonjs --types yargs",
|
||||
|
@ -52,80 +52,83 @@
|
|||
"docker:build": "docker build -f config/docker/Dockerfile -t redoc ."
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/core": "7.3.4",
|
||||
"@babel/plugin-syntax-decorators": "7.2.0",
|
||||
"@babel/plugin-syntax-dynamic-import": "^7.2.0",
|
||||
"@babel/plugin-syntax-jsx": "7.2.0",
|
||||
"@babel/plugin-syntax-typescript": "7.3.3",
|
||||
"@cypress/webpack-preprocessor": "4.0.3",
|
||||
"@hot-loader/react-dom": "^16.8.4",
|
||||
"@types/chai": "4.1.7",
|
||||
"@types/dompurify": "^0.0.32",
|
||||
"@types/enzyme": "^3.9.0",
|
||||
"@babel/core": "7.8.7",
|
||||
"@babel/plugin-syntax-decorators": "7.8.3",
|
||||
"@babel/plugin-syntax-dynamic-import": "^7.8.3",
|
||||
"@babel/plugin-syntax-jsx": "7.8.3",
|
||||
"@babel/plugin-syntax-typescript": "7.8.3",
|
||||
"@cypress/webpack-preprocessor": "4.1.3",
|
||||
"@hot-loader/react-dom": "^16.12.0",
|
||||
"@types/chai": "4.2.10",
|
||||
"@types/dompurify": "^2.0.1",
|
||||
"@types/enzyme": "^3.10.5",
|
||||
"@types/enzyme-to-json": "^1.5.3",
|
||||
"@types/jest": "^24.0.11",
|
||||
"@types/jest": "^25.1.4",
|
||||
"@types/json-pointer": "^1.0.30",
|
||||
"@types/lodash": "^4.14.122",
|
||||
"@types/lodash": "^4.14.149",
|
||||
"@types/lunr": "^2.3.2",
|
||||
"@types/mark.js": "^8.11.3",
|
||||
"@types/marked": "^0.6.3",
|
||||
"@types/prismjs": "^1.9.1",
|
||||
"@types/prop-types": "^15.7.0",
|
||||
"@types/react": "^16.8.7",
|
||||
"@types/react-dom": "^16.8.2",
|
||||
"@types/react-hot-loader": "^4.1.0",
|
||||
"@types/mark.js": "^8.11.5",
|
||||
"@types/marked": "^0.7.3",
|
||||
"@types/prismjs": "^1.16.0",
|
||||
"@types/prop-types": "^15.7.3",
|
||||
"@types/react": "^16.9.23",
|
||||
"@types/react-dom": "^16.9.5",
|
||||
"@types/react-tabs": "^2.3.1",
|
||||
"@types/styled-components": "^4.1.12",
|
||||
"@types/tapable": "1.0.4",
|
||||
"@types/webpack": "^4.4.25",
|
||||
"@types/webpack-env": "^1.13.9",
|
||||
"@types/yargs": "^12.0.9",
|
||||
"babel-loader": "8.0.5",
|
||||
"babel-plugin-styled-components": "^1.10.0",
|
||||
"@types/styled-components": "^4.4.1",
|
||||
"@types/tapable": "1.0.5",
|
||||
"@types/webpack": "^4.41.7",
|
||||
"@types/webpack-env": "^1.15.1",
|
||||
"@types/yargs": "^13.0.3",
|
||||
"@typescript-eslint/eslint-plugin": "^2.24.0",
|
||||
"@typescript-eslint/parser": "^2.24.0",
|
||||
"babel-loader": "8.0.6",
|
||||
"babel-plugin-styled-components": "^1.10.7",
|
||||
"beautify-benchmark": "^0.2.4",
|
||||
"bundlesize": "^0.17.1",
|
||||
"conventional-changelog-cli": "^2.0.12",
|
||||
"copy-webpack-plugin": "^5.0.0",
|
||||
"core-js": "^2.6.5",
|
||||
"coveralls": "^3.0.3",
|
||||
"css-loader": "^2.1.1",
|
||||
"cypress": "~3.1.5",
|
||||
"deploy-to-gh-pages": "^1.3.6",
|
||||
"enzyme": "^3.9.0",
|
||||
"enzyme-adapter-react-16": "^1.10.0",
|
||||
"enzyme-to-json": "^3.3.5",
|
||||
"fork-ts-checker-webpack-plugin": "1.0.0",
|
||||
"bundlesize": "^0.18.0",
|
||||
"conventional-changelog-cli": "^2.0.31",
|
||||
"copy-webpack-plugin": "^5.1.1",
|
||||
"core-js": "^3.5.0",
|
||||
"coveralls": "^3.0.9",
|
||||
"css-loader": "^3.4.2",
|
||||
"cypress": "~3.7.0",
|
||||
"deploy-to-gh-pages": "^1.3.7",
|
||||
"enzyme": "^3.11.0",
|
||||
"enzyme-adapter-react-16": "^1.15.2",
|
||||
"enzyme-to-json": "^3.4.4",
|
||||
"eslint": "^6.8.0",
|
||||
"eslint-plugin-import": "^2.20.1",
|
||||
"eslint-plugin-react": "^7.19.0",
|
||||
"fork-ts-checker-webpack-plugin": "3.1.1",
|
||||
"html-webpack-plugin": "^3.1.0",
|
||||
"jest": "^24.3.1",
|
||||
"jest": "^24.9.0",
|
||||
"license-checker": "^25.0.1",
|
||||
"lodash": "^4.17.11",
|
||||
"lodash": "^4.17.15",
|
||||
"mobx": "^4.3.1",
|
||||
"prettier": "^1.16.4",
|
||||
"prettier-eslint": "^8.8.2",
|
||||
"prettier": "^1.19.1",
|
||||
"raf": "^3.4.1",
|
||||
"react": "^16.8.4",
|
||||
"react-dom": "^16.8.4",
|
||||
"rimraf": "^2.6.3",
|
||||
"react": "^16.13.0",
|
||||
"react-dom": "^16.13.0",
|
||||
"react-hot-loader": "^4.12.19",
|
||||
"rimraf": "^3.0.2",
|
||||
"shelljs": "^0.8.3",
|
||||
"source-map-loader": "^0.2.4",
|
||||
"style-loader": "^0.23.1",
|
||||
"styled-components": "^4.1.3",
|
||||
"ts-jest": "24.0.0",
|
||||
"ts-loader": "5.3.3",
|
||||
"ts-node": "^8.0.3",
|
||||
"tslint": "^5.13.1",
|
||||
"tslint-react": "^3.4.0",
|
||||
"typescript": "^3.3.3333",
|
||||
"style-loader": "^1.1.3",
|
||||
"styled-components": "^4.4.1",
|
||||
"ts-jest": "24.2.0",
|
||||
"ts-loader": "6.2.1",
|
||||
"ts-node": "^8.6.2",
|
||||
"typescript": "^3.8.3",
|
||||
"unfetch": "^4.1.0",
|
||||
"url-polyfill": "^1.1.5",
|
||||
"webpack": "^4.29.6",
|
||||
"webpack-cli": "^3.2.3",
|
||||
"webpack-dev-server": "^3.2.1",
|
||||
"url-polyfill": "^1.1.8",
|
||||
"webpack": "^4.42.0",
|
||||
"webpack-cli": "^3.3.11",
|
||||
"webpack-dev-server": "^3.10.3",
|
||||
"webpack-node-externals": "^1.6.0",
|
||||
"workerize-loader": "^1.0.4",
|
||||
"workerize-loader": "^1.1.0",
|
||||
"yaml-js": "^0.2.3"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"core-js": "^3.1.4",
|
||||
"mobx": "^4.2.0 || ^5.0.0",
|
||||
"react": "^16.8.4",
|
||||
"react-dom": "^16.8.4",
|
||||
|
@ -134,27 +137,27 @@
|
|||
"dependencies": {
|
||||
"classnames": "^2.2.6",
|
||||
"decko": "^1.2.0",
|
||||
"dompurify": "^1.0.10",
|
||||
"eventemitter3": "^3.0.0",
|
||||
"dompurify": "^2.0.8",
|
||||
"eventemitter3": "^4.0.0",
|
||||
"json-pointer": "^0.6.0",
|
||||
"json-schema-ref-parser": "^6.1.0",
|
||||
"lunr": "2.3.6",
|
||||
"lunr": "2.3.8",
|
||||
"mark.js": "^8.11.1",
|
||||
"marked": "^0.6.1",
|
||||
"memoize-one": "^5.0.0",
|
||||
"mobx-react": "^5.4.3",
|
||||
"openapi-sampler": "1.0.0-beta.14",
|
||||
"marked": "^0.7.0",
|
||||
"memoize-one": "~5.1.1",
|
||||
"mobx-react": "6.1.5",
|
||||
"openapi-sampler": "1.0.0-beta.15",
|
||||
"perfect-scrollbar": "^1.4.0",
|
||||
"polished": "^3.0.3",
|
||||
"prismjs": "^1.15.0",
|
||||
"polished": "^3.4.4",
|
||||
"prismjs": "^1.19.0",
|
||||
"prop-types": "^15.7.2",
|
||||
"react-dropdown": "^1.6.4",
|
||||
"react-hot-loader": "^4.8.0",
|
||||
"react-tabs": "^3.0.0",
|
||||
"slugify": "^1.3.4",
|
||||
"react-dropdown": "^1.7.0",
|
||||
"react-tabs": "^3.1.0",
|
||||
"slugify": "^1.4.0",
|
||||
"stickyfill": "^1.1.1",
|
||||
"swagger2openapi": "^5.2.3",
|
||||
"tslib": "^1.9.3"
|
||||
"swagger2openapi": "^5.3.4",
|
||||
"tslib": "^1.11.1",
|
||||
"url-template": "^2.0.8"
|
||||
},
|
||||
"bundlesize": [
|
||||
{
|
||||
|
|
|
@ -5,11 +5,7 @@ import { ClipboardService } from '../services/ClipboardService';
|
|||
|
||||
export interface CopyButtonWrapperProps {
|
||||
data: any;
|
||||
children: (
|
||||
props: {
|
||||
renderCopyButton: (() => React.ReactNode);
|
||||
},
|
||||
) => React.ReactNode;
|
||||
children: (props: { renderCopyButton: () => React.ReactNode }) => React.ReactNode;
|
||||
}
|
||||
|
||||
export class CopyButtonWrapper extends React.PureComponent<
|
||||
|
|
|
@ -55,7 +55,7 @@ export const StyledDropdown = styled(Dropdown)`
|
|||
display: block;
|
||||
height: 0;
|
||||
position: absolute;
|
||||
right: 0.35em;
|
||||
right: 0.3em;
|
||||
top: 50%;
|
||||
margin-top: -0.125em;
|
||||
width: 0;
|
||||
|
@ -97,7 +97,7 @@ export const StyledDropdown = styled(Dropdown)`
|
|||
export const SimpleDropdown = styled(StyledDropdown)`
|
||||
margin-left: 10px;
|
||||
text-transform: none;
|
||||
font-size: 0.929em;
|
||||
font-size: 0.969em;
|
||||
|
||||
.Dropdown-control {
|
||||
font-size: 1em;
|
||||
|
|
|
@ -63,7 +63,7 @@ export const PropertyNameCell = styled(PropertyCell)`
|
|||
line-height: 20px;
|
||||
white-space: nowrap;
|
||||
font-size: 0.929em;
|
||||
font-family: ${props => props.theme.typography.headings.fontFamily};
|
||||
font-family: ${props => props.theme.typography.code.fontFamily};
|
||||
|
||||
&.deprecated {
|
||||
${deprecatedCss};
|
||||
|
|
|
@ -32,6 +32,7 @@ export const TypeName = styled(FieldLabel)`
|
|||
|
||||
export const TypeTitle = styled(FieldLabel)`
|
||||
color: ${props => props.theme.schema.typeTitleColor};
|
||||
word-break: break-word;
|
||||
`;
|
||||
|
||||
export const TypeFormat = TypeName;
|
||||
|
@ -60,14 +61,6 @@ export const PatternLabel = styled(FieldLabel)`
|
|||
&::after {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
&::before {
|
||||
content: ' /';
|
||||
}
|
||||
|
||||
&::after {
|
||||
content: '/ ';
|
||||
}
|
||||
`;
|
||||
|
||||
export const ExampleValue = styled(FieldLabel)`
|
||||
|
@ -76,11 +69,9 @@ export const ExampleValue = styled(FieldLabel)`
|
|||
background-color: ${transparentize(0.95, theme.colors.text.primary)};
|
||||
color: ${transparentize(0.1, theme.colors.text.primary)};
|
||||
|
||||
margin: ${theme.spacing.unit}px;
|
||||
padding: 0 ${theme.spacing.unit}px;
|
||||
border: 1px solid ${transparentize(0.9, theme.colors.text.primary)};
|
||||
font-family: ${theme.typography.code.fontFamily};
|
||||
color: ${theme.typography.code.color};
|
||||
}`};
|
||||
& + & {
|
||||
margin-left: 0;
|
||||
|
@ -99,6 +90,7 @@ export const ConstraintItem = styled(FieldLabel)`
|
|||
margin: 0 ${theme.spacing.unit}px;
|
||||
padding: 0 ${theme.spacing.unit}px;
|
||||
border: 1px solid ${transparentize(0.9, theme.colors.primary.main)};
|
||||
font-family: ${theme.typography.code.fontFamily};
|
||||
}`};
|
||||
& + & {
|
||||
margin-left: 0;
|
||||
|
|
|
@ -1,14 +1,16 @@
|
|||
import { SECTION_ATTR } from '../services/MenuStore';
|
||||
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});
|
||||
padding: 0 ${props => props.theme.spacing.sectionHorizontal}px;
|
||||
|
||||
${media.lessThan('medium', true)`
|
||||
${({ compact, theme }) =>
|
||||
media.lessThan('medium', true)`
|
||||
width: 100%;
|
||||
padding: ${props =>
|
||||
`${props.theme.spacing.sectionVertical}px ${props.theme.spacing.sectionHorizontal}px`};
|
||||
padding: ${`${compact ? 0 : theme.spacing.sectionVertical}px ${
|
||||
theme.spacing.sectionHorizontal
|
||||
}px`};
|
||||
`};
|
||||
`;
|
||||
|
||||
|
|
|
@ -11,13 +11,14 @@ export const OneOfLabel = styled.span`
|
|||
font-size: 0.9em;
|
||||
margin-right: 10px;
|
||||
color: ${props => props.theme.colors.primary.main};
|
||||
font-family: Montserrat;
|
||||
font-family: ${props => props.theme.typography.headings.fontFamily};
|
||||
}
|
||||
`;
|
||||
|
||||
export const OneOfButton = styled.li<{ active: boolean }>`
|
||||
display: inline-block;
|
||||
margin-right: 10px;
|
||||
margin-bottom: 5px;
|
||||
font-size: 0.8em;
|
||||
cursor: pointer;
|
||||
border: 1px solid ${props => props.theme.colors.primary.main};
|
||||
|
|
|
@ -1,16 +1,19 @@
|
|||
import { observer } from 'mobx-react';
|
||||
import * as React from 'react';
|
||||
import { Constants } from '../../../src/services/Constants';
|
||||
import { DarkRightPanel, StyledLink } from '../../../src/common-elements';
|
||||
import { ContentPanel } from '../RightPanelContent/ContentPanel';
|
||||
|
||||
import { MiddlePanel, Row, Section } from '../../common-elements/';
|
||||
|
||||
import { AppStore } from '../../services/AppStore';
|
||||
|
||||
import { MiddlePanel, Row, Section } from '../../common-elements/';
|
||||
import { ExternalDocumentation } from '../ExternalDocumentation/ExternalDocumentation';
|
||||
import { Markdown } from '../Markdown/Markdown';
|
||||
import { StyledMarkdownBlock } from '../Markdown/styled.elements';
|
||||
import { ApiHeader, DownloadButton, InfoSpan, InfoSpanBox, InfoSpanBoxWrap, } from './styled.elements';
|
||||
import {
|
||||
ApiHeader,
|
||||
DownloadButton,
|
||||
InfoSpan,
|
||||
InfoSpanBox,
|
||||
InfoSpanBoxWrap,
|
||||
} from './styled.elements';
|
||||
|
||||
export interface ApiInfoProps {
|
||||
store: AppStore;
|
||||
|
@ -25,8 +28,8 @@ export class ApiInfo extends React.Component<ApiInfoProps> {
|
|||
};
|
||||
|
||||
render() {
|
||||
const {store} = this.props;
|
||||
const {info, externalDocs} = store.spec;
|
||||
const { store } = this.props;
|
||||
const { info, externalDocs } = store.spec;
|
||||
const hideDownloadButton = store.options.hideDownloadButton;
|
||||
|
||||
const downloadFilename = info.downloadFileName;
|
||||
|
@ -35,26 +38,24 @@ export class ApiInfo extends React.Component<ApiInfoProps> {
|
|||
const license =
|
||||
(info.license && (
|
||||
<InfoSpan>
|
||||
License: <StyledLink href={info.license.url}>{info.license.name}</StyledLink>
|
||||
License: <a href={info.license.url}>{info.license.name}</a>
|
||||
</InfoSpan>
|
||||
)) ||
|
||||
null;
|
||||
|
||||
const website =
|
||||
(info.contact &&
|
||||
info.contact.url && (
|
||||
(info.contact && info.contact.url && (
|
||||
<InfoSpan>
|
||||
URL: <StyledLink href={info.contact.url}>{info.contact.url}</StyledLink>
|
||||
URL: <a href={info.contact.url}>{info.contact.url}</a>
|
||||
</InfoSpan>
|
||||
)) ||
|
||||
null;
|
||||
|
||||
const email =
|
||||
(info.contact &&
|
||||
info.contact.email && (
|
||||
(info.contact && info.contact.email && (
|
||||
<InfoSpan>
|
||||
{info.contact.name || 'E-mail'}:{' '}
|
||||
<StyledLink href={'mailto:' + info.contact.email}>{info.contact.email}</StyledLink>
|
||||
<a href={'mailto:' + info.contact.email}>{info.contact.email}</a>
|
||||
</InfoSpan>
|
||||
)) ||
|
||||
null;
|
||||
|
@ -62,16 +63,12 @@ export class ApiInfo extends React.Component<ApiInfoProps> {
|
|||
const terms =
|
||||
(info.termsOfService && (
|
||||
<InfoSpan>
|
||||
<StyledLink href={info.termsOfService}>Terms of Service</StyledLink>
|
||||
<a href={info.termsOfService}>Terms of Service</a>
|
||||
</InfoSpan>
|
||||
)) ||
|
||||
null;
|
||||
|
||||
const version =
|
||||
(info.version && (
|
||||
<span>({info.version})</span>
|
||||
)) ||
|
||||
null;
|
||||
const version = (info.version && <span>({info.version})</span>) || null;
|
||||
|
||||
return (
|
||||
<Section>
|
||||
|
@ -103,11 +100,9 @@ export class ApiInfo extends React.Component<ApiInfoProps> {
|
|||
)) ||
|
||||
null}
|
||||
</StyledMarkdownBlock>
|
||||
<Markdown source={store.spec.info.description}/>
|
||||
{externalDocs && <ExternalDocumentation externalDocs={externalDocs}/>}
|
||||
<Markdown source={store.spec.info.description} data-role="redoc-description" />
|
||||
{externalDocs && <ExternalDocumentation externalDocs={externalDocs} />}
|
||||
</MiddlePanel>
|
||||
{store.options.showOtherInfoPanel &&
|
||||
<DarkRightPanel><ContentPanel content={info[Constants.OTX_EXTENSION_KEY]}/></DarkRightPanel>}
|
||||
</Row>
|
||||
</Section>
|
||||
);
|
||||
|
|
|
@ -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
|
||||
export class SectionItem extends React.Component<ContentItemProps> {
|
||||
|
@ -71,7 +71,7 @@ export class SectionItem extends React.Component<ContentItemProps> {
|
|||
return (
|
||||
<>
|
||||
<Row>
|
||||
<MiddlePanel>
|
||||
<MiddlePanel compact={false}>
|
||||
<Header>
|
||||
<ShareLink to={this.props.item.id} />
|
||||
{name}
|
||||
|
|
|
@ -5,7 +5,7 @@ import { Markdown } from '../Markdown/Markdown';
|
|||
import { OptionsContext } from '../OptionsProvider';
|
||||
import { SelectOnClick } from '../SelectOnClick/SelectOnClick';
|
||||
|
||||
import { getBasePath } from '../../utils';
|
||||
import { expandDefaultServerVariables, getBasePath } from '../../utils';
|
||||
import {
|
||||
EndpointInfo,
|
||||
HttpVerb,
|
||||
|
@ -60,21 +60,26 @@ export class Endpoint extends React.Component<EndpointProps, EndpointState> {
|
|||
/>
|
||||
</EndpointInfo>
|
||||
<ServersOverlay expanded={expanded}>
|
||||
{operation.servers.map(server => (
|
||||
<ServerItem key={server.url}>
|
||||
{operation.servers.map(server => {
|
||||
const normalizedUrl = options.expandDefaultServerVariables
|
||||
? expandDefaultServerVariables(server.url, server.variables)
|
||||
: server.url;
|
||||
return (
|
||||
<ServerItem key={normalizedUrl}>
|
||||
<Markdown source={server.description || ''} compact={true} />
|
||||
<SelectOnClick>
|
||||
<ServerUrl>
|
||||
<span>
|
||||
{hideHostname || options.hideHostname
|
||||
? getBasePath(server.url)
|
||||
: server.url}
|
||||
? getBasePath(normalizedUrl)
|
||||
: normalizedUrl}
|
||||
</span>
|
||||
{operation.path}
|
||||
</ServerUrl>
|
||||
</SelectOnClick>
|
||||
</ServerItem>
|
||||
))}
|
||||
);
|
||||
})}
|
||||
</ServersOverlay>
|
||||
</OperationEndpointWrap>
|
||||
)}
|
||||
|
|
|
@ -2,6 +2,7 @@ import * as React from 'react';
|
|||
import { ExampleValue, FieldLabel } from '../../common-elements/fields';
|
||||
|
||||
import { l } from '../../services/Labels';
|
||||
import { OptionsContext } from '../OptionsProvider';
|
||||
|
||||
export interface EnumValuesProps {
|
||||
values: string[];
|
||||
|
@ -9,8 +10,10 @@ export interface EnumValuesProps {
|
|||
}
|
||||
|
||||
export class EnumValues extends React.PureComponent<EnumValuesProps> {
|
||||
static contextType = OptionsContext;
|
||||
render() {
|
||||
const { values, type } = this.props;
|
||||
const { enumSkipQuotes } = this.context;
|
||||
if (!values.length) {
|
||||
return null;
|
||||
}
|
||||
|
@ -20,12 +23,15 @@ export class EnumValues extends React.PureComponent<EnumValuesProps> {
|
|||
<FieldLabel>
|
||||
{type === 'array' ? l('enumArray') : ''}{' '}
|
||||
{values.length === 1 ? l('enumSingleValue') : l('enum')}:
|
||||
</FieldLabel>
|
||||
{values.map((value, idx) => (
|
||||
</FieldLabel>{' '}
|
||||
{values.map((value, idx) => {
|
||||
const exampleValue = enumSkipQuotes ? value : JSON.stringify(value);
|
||||
return (
|
||||
<React.Fragment key={idx}>
|
||||
<ExampleValue>{JSON.stringify(value)}</ExampleValue>{' '}
|
||||
<ExampleValue>{exampleValue}</ExampleValue>{' '}
|
||||
</React.Fragment>
|
||||
))}
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -23,6 +23,7 @@ export interface FieldProps extends SchemaOptions {
|
|||
showExamples?: boolean;
|
||||
|
||||
field: FieldModel;
|
||||
expandByDefault?: boolean;
|
||||
|
||||
renderDiscriminatorSwitch?: (opts: FieldProps) => JSX.Element;
|
||||
}
|
||||
|
@ -30,13 +31,19 @@ export interface FieldProps extends SchemaOptions {
|
|||
@observer
|
||||
export class Field extends React.Component<FieldProps> {
|
||||
toggle = () => {
|
||||
if (this.props.field.expanded === undefined && this.props.expandByDefault) {
|
||||
this.props.field.expanded = false;
|
||||
} else {
|
||||
this.props.field.toggle();
|
||||
}
|
||||
};
|
||||
render() {
|
||||
const { className, field, isLast } = this.props;
|
||||
const { name, expanded, deprecated, required, kind } = field;
|
||||
const { className, field, isLast, expandByDefault } = this.props;
|
||||
const { name, deprecated, required, kind } = field;
|
||||
const withSubSchema = !field.schema.isPrimitive && !field.schema.isCircular;
|
||||
|
||||
const expanded = field.expanded === undefined ? expandByDefault : field.expanded;
|
||||
|
||||
const paramName = withSubSchema ? (
|
||||
<ClickablePropertyNameCell
|
||||
onClick={this.toggle}
|
||||
|
@ -65,8 +72,7 @@ export class Field extends React.Component<FieldProps> {
|
|||
<FieldDetails {...this.props} />
|
||||
</PropertyDetailsCell>
|
||||
</tr>
|
||||
{field.expanded &&
|
||||
withSubSchema && (
|
||||
{expanded && withSubSchema && (
|
||||
<tr key={field.name + 'inner'}>
|
||||
<PropertyCellWithInner colSpan={2}>
|
||||
<InnerPropertiesWrap>
|
||||
|
|
|
@ -4,6 +4,7 @@ import { ExampleValue, FieldLabel } from '../../common-elements/fields';
|
|||
export interface FieldDetailProps {
|
||||
value?: any;
|
||||
label: string;
|
||||
raw?: boolean;
|
||||
}
|
||||
|
||||
export class FieldDetail extends React.PureComponent<FieldDetailProps> {
|
||||
|
@ -11,12 +12,12 @@ export class FieldDetail extends React.PureComponent<FieldDetailProps> {
|
|||
if (this.props.value === undefined) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const value = this.props.raw ? this.props.value : JSON.stringify(this.props.value);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<FieldLabel> {this.props.label} </FieldLabel>{' '}
|
||||
<ExampleValue>
|
||||
{JSON.stringify(this.props.value)}
|
||||
</ExampleValue>
|
||||
<FieldLabel> {this.props.label} </FieldLabel> <ExampleValue>{value}</ExampleValue>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -9,6 +9,7 @@ import {
|
|||
TypePrefix,
|
||||
TypeTitle,
|
||||
} from '../../common-elements/fields';
|
||||
import { serializeParameterValue } from '../../utils/openapi';
|
||||
import { ExternalDocumentation } from '../ExternalDocumentation/ExternalDocumentation';
|
||||
import { Markdown } from '../Markdown/Markdown';
|
||||
import { EnumValues } from './EnumValues';
|
||||
|
@ -20,13 +21,31 @@ import { FieldDetail } from './FieldDetail';
|
|||
import { Badge } from '../../common-elements/';
|
||||
|
||||
import { l } from '../../services/Labels';
|
||||
import { OptionsContext } from '../OptionsProvider';
|
||||
|
||||
export class FieldDetails extends React.PureComponent<FieldProps> {
|
||||
static contextType = OptionsContext;
|
||||
render() {
|
||||
const { showExamples, field, renderDiscriminatorSwitch } = this.props;
|
||||
const { enumSkipQuotes, hideSchemaTitles } = this.context;
|
||||
|
||||
const { schema, description, example, deprecated } = field;
|
||||
|
||||
const rawDefault = !!enumSkipQuotes || field.in === 'header'; // having quotes around header field default values is confusing and inappropriate
|
||||
|
||||
let exampleField: JSX.Element | null = null;
|
||||
|
||||
if (showExamples && example !== undefined) {
|
||||
const label = l('example') + ':';
|
||||
if (field.in && (field.style || field.serializationMime)) {
|
||||
// decode for better readability in examples: see https://github.com/Redocly/redoc/issues/1138
|
||||
const serializedValue = decodeURIComponent(serializeParameterValue(field, example));
|
||||
exampleField = <FieldDetail label={label} value={serializedValue} raw={true} />;
|
||||
} else {
|
||||
exampleField = <FieldDetail label={label} value={example} />;
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div>
|
||||
|
@ -40,10 +59,10 @@ export class FieldDetails extends React.PureComponent<FieldProps> {
|
|||
>{' '}
|
||||
</TypeFormat>
|
||||
)}
|
||||
{schema.title && <TypeTitle> ({schema.title}) </TypeTitle>}
|
||||
{schema.title && !hideSchemaTitles && <TypeTitle> ({schema.title}) </TypeTitle>}
|
||||
<ConstraintsView constraints={schema.constraints} />
|
||||
{schema.nullable && <NullableLabel> {l('nullable')} </NullableLabel>}
|
||||
{schema.pattern && <PatternLabel>{schema.pattern}</PatternLabel>}
|
||||
{schema.pattern && <PatternLabel> {schema.pattern} </PatternLabel>}
|
||||
{schema.isCircular && <RecursiveLabel> {l('recursive')} </RecursiveLabel>}
|
||||
</div>
|
||||
{deprecated && (
|
||||
|
@ -51,9 +70,9 @@ export class FieldDetails extends React.PureComponent<FieldProps> {
|
|||
<Badge type="warning"> {l('deprecated')} </Badge>
|
||||
</div>
|
||||
)}
|
||||
<FieldDetail label={l('default') + ':'} value={schema.default} />
|
||||
<FieldDetail raw={rawDefault} label={l('default') + ':'} value={schema.default} />
|
||||
{!renderDiscriminatorSwitch && <EnumValues type={schema.type} values={schema.enum} />}{' '}
|
||||
{showExamples && <FieldDetail label={l('example') + ':'} value={example} />}
|
||||
{exampleField}
|
||||
{<Extensions extensions={{ ...field.extensions, ...schema.extensions }} />}
|
||||
<div>
|
||||
<Markdown compact={true} source={description} />
|
||||
|
|
|
@ -5,6 +5,7 @@ import { SampleControls } from '../../common-elements';
|
|||
import { CopyButtonWrapper } from '../../common-elements/CopyButtonWrapper';
|
||||
import { PrismDiv } from '../../common-elements/PrismDiv';
|
||||
import { jsonToHTML } from '../../utils/jsonToHtml';
|
||||
import { OptionsContext } from '../OptionsProvider';
|
||||
import { jsonStyles } from './style';
|
||||
|
||||
export interface JsonProps {
|
||||
|
@ -32,12 +33,18 @@ class Json extends React.PureComponent<JsonProps> {
|
|||
<span onClick={this.expandAll}> Expand all </span>
|
||||
<span onClick={this.collapseAll}> Collapse all </span>
|
||||
</SampleControls>
|
||||
<OptionsContext.Consumer>
|
||||
{options => (
|
||||
<PrismDiv
|
||||
className={this.props.className}
|
||||
// tslint:disable-next-line
|
||||
ref={node => (this.node = node!)}
|
||||
dangerouslySetInnerHTML={{ __html: jsonToHTML(this.props.data) }}
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: jsonToHTML(this.props.data, options.jsonSampleExpandLevel),
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</OptionsContext.Consumer>
|
||||
</JsonViewerWrap>
|
||||
);
|
||||
|
||||
|
|
|
@ -25,7 +25,7 @@ export class AdvancedMarkdown extends React.Component<AdvancedMarkdownProps> {
|
|||
renderWithOptionsAndStore(options: RedocNormalizedOptions, store?: AppStore) {
|
||||
const { source, htmlWrap = i => i } = this.props;
|
||||
if (!store) {
|
||||
throw new Error('When using componentes in markdown, store prop must be provided');
|
||||
throw new Error('When using components in markdown, store prop must be provided');
|
||||
}
|
||||
|
||||
const renderer = new MarkdownRenderer(options);
|
||||
|
|
|
@ -17,11 +17,12 @@ export type MarkdownProps = BaseMarkdownProps &
|
|||
StylingMarkdownProps & {
|
||||
source: string;
|
||||
className?: string;
|
||||
'data-role'?: string;
|
||||
};
|
||||
|
||||
export class Markdown extends React.Component<MarkdownProps> {
|
||||
render() {
|
||||
const { source, inline, compact, className } = this.props;
|
||||
const { source, inline, compact, className, 'data-role': dataRole } = this.props;
|
||||
const renderer = new MarkdownRenderer();
|
||||
return (
|
||||
<SanitizedMarkdownHTML
|
||||
|
@ -29,6 +30,7 @@ export class Markdown extends React.Component<MarkdownProps> {
|
|||
inline={inline}
|
||||
compact={compact}
|
||||
className={className}
|
||||
data-role={dataRole}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -10,7 +10,7 @@ const StyledMarkdownSpan = StyledMarkdownBlock.withComponent('span');
|
|||
const sanitize = (untrustedSpec, html) => (untrustedSpec ? DOMPurify.sanitize(html) : html);
|
||||
|
||||
export function SanitizedMarkdownHTML(
|
||||
props: StylingMarkdownProps & { html: string; className?: string },
|
||||
props: StylingMarkdownProps & { html: string; className?: string; 'data-role'?: string },
|
||||
) {
|
||||
const Wrap = props.inline ? StyledMarkdownSpan : StyledMarkdownBlock;
|
||||
|
||||
|
@ -22,6 +22,7 @@ export function SanitizedMarkdownHTML(
|
|||
dangerouslySetInnerHTML={{
|
||||
__html: sanitize(options.untrustedSpec, props.html),
|
||||
}}
|
||||
data-role={props['data-role']}
|
||||
{...props}
|
||||
/>
|
||||
)}
|
||||
|
|
|
@ -19,11 +19,13 @@ export const linksCss = css`
|
|||
}
|
||||
`;
|
||||
|
||||
export const StyledMarkdownBlock = styled(PrismDiv as StyledComponent<
|
||||
export const StyledMarkdownBlock = styled(
|
||||
PrismDiv as StyledComponent<
|
||||
'div',
|
||||
ResolvedThemeInterface,
|
||||
{ compact?: boolean; inline?: boolean }
|
||||
>)`
|
||||
>,
|
||||
)`
|
||||
|
||||
font-family: ${props => props.theme.typography.fontFamily};
|
||||
font-weight: ${props => props.theme.typography.fontWeightRegular};
|
||||
|
|
|
@ -3,6 +3,7 @@ import * as React from 'react';
|
|||
|
||||
import { DropdownProps } from '../../common-elements/dropdown';
|
||||
import { MediaContentModel, MediaTypeModel, SchemaModel } from '../../services/models';
|
||||
import { DropdownLabel, DropdownWrapper } from '../PayloadSamples/styled.elements';
|
||||
|
||||
export interface MediaTypeChildProps {
|
||||
schema: SchemaModel;
|
||||
|
@ -11,6 +12,8 @@ export interface MediaTypeChildProps {
|
|||
|
||||
export interface MediaTypesSwitchProps {
|
||||
content?: MediaContentModel;
|
||||
withLabel?: boolean;
|
||||
|
||||
renderDropdown: (props: DropdownProps) => JSX.Element;
|
||||
children: (activeMime: MediaTypeModel) => JSX.Element;
|
||||
}
|
||||
|
@ -37,13 +40,25 @@ export class MediaTypesSwitch extends React.Component<MediaTypesSwitchProps> {
|
|||
};
|
||||
});
|
||||
|
||||
const Wrapper = ({ children }) =>
|
||||
this.props.withLabel ? (
|
||||
<DropdownWrapper>
|
||||
<DropdownLabel>Content type</DropdownLabel>
|
||||
{children}
|
||||
</DropdownWrapper>
|
||||
) : (
|
||||
children
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Wrapper>
|
||||
{this.props.renderDropdown({
|
||||
value: options[activeMimeIdx],
|
||||
options,
|
||||
onChange: this.switchMedia,
|
||||
})}
|
||||
</Wrapper>
|
||||
{this.props.children(content.active)}
|
||||
</>
|
||||
);
|
||||
|
|
|
@ -29,7 +29,12 @@ export function ExternalExample({ example, mimeType }: ExampleProps) {
|
|||
return (
|
||||
<StyledPre>
|
||||
Error loading external example: <br />
|
||||
<a className={'token string'} href={example.externalValueUrl} target="_blank">
|
||||
<a
|
||||
className={'token string'}
|
||||
href={example.externalValueUrl}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
{example.externalValueUrl}
|
||||
</a>
|
||||
</StyledPre>
|
||||
|
|
|
@ -1,17 +1,33 @@
|
|||
import * as React from 'react';
|
||||
|
||||
import { SmallTabs, Tab, TabList, TabPanel } from '../../common-elements';
|
||||
import { MediaTypeModel } from '../../services/models';
|
||||
import styled from '../../styled-components';
|
||||
|
||||
import { DropdownProps } from '../../common-elements';
|
||||
import { MediaTypeModel } from '../../services/models';
|
||||
import { Markdown } from '../Markdown/Markdown';
|
||||
import { Example } from './Example';
|
||||
import { NoSampleLabel } from './styled.elements';
|
||||
import { DropdownLabel, DropdownWrapper, NoSampleLabel } from './styled.elements';
|
||||
|
||||
export interface PayloadSamplesProps {
|
||||
mediaType: MediaTypeModel;
|
||||
renderDropdown: (props: DropdownProps) => JSX.Element;
|
||||
}
|
||||
|
||||
export class MediaTypeSamples extends React.Component<PayloadSamplesProps> {
|
||||
interface MediaTypeSamplesState {
|
||||
activeIdx: number;
|
||||
}
|
||||
|
||||
export class MediaTypeSamples extends React.Component<PayloadSamplesProps, MediaTypeSamplesState> {
|
||||
state = {
|
||||
activeIdx: 0,
|
||||
};
|
||||
switchMedia = ({ value }) => {
|
||||
this.setState({
|
||||
activeIdx: parseInt(value, 10),
|
||||
});
|
||||
};
|
||||
render() {
|
||||
const { activeIdx } = this.state;
|
||||
const examples = this.props.mediaType.examples || {};
|
||||
const mimeType = this.props.mediaType.name;
|
||||
|
||||
|
@ -21,28 +37,46 @@ export class MediaTypeSamples extends React.Component<PayloadSamplesProps> {
|
|||
if (examplesNames.length === 0) {
|
||||
return noSample;
|
||||
}
|
||||
|
||||
if (examplesNames.length > 1) {
|
||||
const options = examplesNames.map((name, idx) => {
|
||||
return {
|
||||
label: examples[name].summary || name,
|
||||
value: idx.toString(),
|
||||
};
|
||||
});
|
||||
|
||||
const example = examples[examplesNames[activeIdx]];
|
||||
const description = example.description;
|
||||
|
||||
return (
|
||||
<SmallTabs defaultIndex={0}>
|
||||
<TabList>
|
||||
{examplesNames.map(name => (
|
||||
<Tab key={name}> {examples[name].summary || name} </Tab>
|
||||
))}
|
||||
</TabList>
|
||||
{examplesNames.map(name => (
|
||||
<TabPanel key={name}>
|
||||
<Example example={examples[name]} mimeType={mimeType} />
|
||||
</TabPanel>
|
||||
))}
|
||||
</SmallTabs>
|
||||
<SamplesWrapper>
|
||||
<DropdownWrapper>
|
||||
<DropdownLabel>Example</DropdownLabel>
|
||||
{this.props.renderDropdown({
|
||||
value: options[activeIdx],
|
||||
options,
|
||||
onChange: this.switchMedia,
|
||||
})}
|
||||
</DropdownWrapper>
|
||||
<div>
|
||||
{description && <Markdown source={description} />}
|
||||
<Example example={example} mimeType={mimeType} />
|
||||
</div>
|
||||
</SamplesWrapper>
|
||||
);
|
||||
} else {
|
||||
const name = examplesNames[0];
|
||||
const example = examples[examplesNames[0]];
|
||||
return (
|
||||
<div>
|
||||
<Example example={examples[name]} mimeType={mimeType} />
|
||||
</div>
|
||||
<SamplesWrapper>
|
||||
{example.description && <Markdown source={example.description} />}
|
||||
<Example example={example} mimeType={mimeType} />
|
||||
</SamplesWrapper>
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const SamplesWrapper = styled.div`
|
||||
margin-top: 15px;
|
||||
`;
|
||||
|
|
|
@ -1,11 +1,14 @@
|
|||
// @ts-ignore
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
import ReactDropdown from 'react-dropdown';
|
||||
|
||||
import { observer } from 'mobx-react';
|
||||
import * as React from 'react';
|
||||
import { MediaTypeSamples } from './MediaTypeSamples';
|
||||
|
||||
import { MediaTypesSwitch } from '../MediaTypeSwitch/MediaTypesSwitch';
|
||||
|
||||
import { MediaContentModel } from '../../services/models';
|
||||
import { DropdownOrLabel } from '../DropdownOrLabel/DropdownOrLabel';
|
||||
import { MediaTypesSwitch } from '../MediaTypeSwitch/MediaTypesSwitch';
|
||||
import { InvertedSimpleDropdown, MimeLabel } from './styled.elements';
|
||||
|
||||
export interface PayloadSamplesProps {
|
||||
|
@ -21,8 +24,14 @@ export class PayloadSamples extends React.Component<PayloadSamplesProps> {
|
|||
}
|
||||
|
||||
return (
|
||||
<MediaTypesSwitch content={mimeContent} renderDropdown={this.renderDropdown}>
|
||||
{mediaType => <MediaTypeSamples key="samples" mediaType={mediaType} />}
|
||||
<MediaTypesSwitch content={mimeContent} renderDropdown={this.renderDropdown} withLabel={true}>
|
||||
{mediaType => (
|
||||
<MediaTypeSamples
|
||||
key="samples"
|
||||
mediaType={mediaType}
|
||||
renderDropdown={this.renderDropdown}
|
||||
/>
|
||||
)}
|
||||
</MediaTypesSwitch>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -13,8 +13,7 @@ export function useExternalExample(example: ExampleModel, mimeType: string) {
|
|||
|
||||
prevRef.current = example;
|
||||
|
||||
useEffect(
|
||||
() => {
|
||||
useEffect(() => {
|
||||
const load = async () => {
|
||||
setIsLoading(true);
|
||||
try {
|
||||
|
@ -26,9 +25,7 @@ export function useExternalExample(example: ExampleModel, mimeType: string) {
|
|||
};
|
||||
|
||||
load();
|
||||
},
|
||||
[example, mimeType],
|
||||
);
|
||||
}, [example, mimeType]);
|
||||
|
||||
return value.current;
|
||||
}
|
||||
|
|
|
@ -1,29 +1,49 @@
|
|||
// @ts-ignore
|
||||
import Dropdown from 'react-dropdown';
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
import ReactDropdown from 'react-dropdown';
|
||||
|
||||
import { transparentize } from 'polished';
|
||||
import styled from '../../styled-components';
|
||||
|
||||
import { StyledDropdown } from '../../common-elements';
|
||||
|
||||
export const MimeLabel = styled.div`
|
||||
border-bottom: 1px solid rgba(255, 255, 255, 0.9);
|
||||
padding: 12px;
|
||||
background-color: ${({ theme }) => transparentize(0.6, theme.rightPanel.backgroundColor)};
|
||||
margin: 0 0 10px 0;
|
||||
display: block;
|
||||
color: rgba(255, 255, 255, 0.8);
|
||||
`;
|
||||
|
||||
export const DropdownLabel = styled.span`
|
||||
font-family: ${({ theme }) => theme.typography.headings.fontFamily};
|
||||
font-size: 12px;
|
||||
position: absolute;
|
||||
z-index: 1;
|
||||
top: -11px;
|
||||
left: 12px;
|
||||
font-weight: ${({ theme }) => theme.typography.fontWeightBold};
|
||||
color: ${({ theme }) => transparentize(0.6, theme.rightPanel.textColor)};
|
||||
`;
|
||||
|
||||
export const DropdownWrapper = styled.div`
|
||||
position: relative;
|
||||
`;
|
||||
|
||||
export const InvertedSimpleDropdown = styled(StyledDropdown)`
|
||||
margin-left: 10px;
|
||||
text-transform: none;
|
||||
font-size: 0.929em;
|
||||
border-bottom: 1px solid ${({ theme }) => theme.rightPanel.textColor};
|
||||
margin: 0 0 10px 0;
|
||||
display: block;
|
||||
|
||||
background-color: ${({ theme }) => transparentize(0.6, theme.rightPanel.backgroundColor)};
|
||||
.Dropdown-control {
|
||||
margin-top: 0;
|
||||
}
|
||||
.Dropdown-control,
|
||||
.Dropdown-control:hover {
|
||||
font-size: 1em;
|
||||
border: none;
|
||||
padding: 0 1.2em 0 0;
|
||||
padding: 0.9em 1.6em 0.9em 0.9em;
|
||||
background: transparent;
|
||||
color: ${({ theme }) => theme.rightPanel.textColor};
|
||||
box-shadow: none;
|
||||
|
@ -34,6 +54,7 @@ export const InvertedSimpleDropdown = styled(StyledDropdown)`
|
|||
}
|
||||
.Dropdown-menu {
|
||||
margin: 0;
|
||||
margin-top: 2px;
|
||||
}
|
||||
`;
|
||||
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import { observer } from 'mobx-react';
|
||||
import * as React from 'react';
|
||||
import { OperationModel, RedocNormalizedOptions } from '../../services';
|
||||
import { isPayloadSample, OperationModel, RedocNormalizedOptions } from '../../services';
|
||||
import { PayloadSamples } from '../PayloadSamples/PayloadSamples';
|
||||
import { SourceCodeWithCopy } from '../SourceCode/SourceCode';
|
||||
|
||||
|
@ -19,15 +19,10 @@ export class RequestSamples extends React.Component<RequestSamplesProps> {
|
|||
|
||||
render() {
|
||||
const { operation } = this.props;
|
||||
const requestBodyContent = operation.requestBody && operation.requestBody.content;
|
||||
const hasBodySample = requestBodyContent && requestBodyContent.hasSample;
|
||||
const samples = operation.codeSamples;
|
||||
|
||||
const hasSamples = hasBodySample || samples.length > 0;
|
||||
const hideTabList =
|
||||
samples.length + (hasBodySample ? 1 : 0) === 1
|
||||
? this.context.hideSingleRequestSampleTab
|
||||
: false;
|
||||
const hasSamples = samples.length > 0;
|
||||
const hideTabList = samples.length === 1 ? this.context.hideSingleRequestSampleTab : false;
|
||||
return (
|
||||
(hasSamples && (
|
||||
<div>
|
||||
|
@ -35,23 +30,21 @@ export class RequestSamples extends React.Component<RequestSamplesProps> {
|
|||
|
||||
<Tabs defaultIndex={0}>
|
||||
<TabList hidden={hideTabList}>
|
||||
{hasBodySample && <Tab key="payload"> Payload </Tab>}
|
||||
{samples.map(sample => (
|
||||
<Tab key={sample.lang + '_' + (sample.label || '')}>
|
||||
{sample.label !== undefined ? sample.label : sample.lang}
|
||||
</Tab>
|
||||
))}
|
||||
</TabList>
|
||||
{hasBodySample && (
|
||||
<TabPanel key="payload">
|
||||
<div>
|
||||
<PayloadSamples content={requestBodyContent!} />
|
||||
</div>
|
||||
</TabPanel>
|
||||
)}
|
||||
{samples.map(sample => (
|
||||
<TabPanel key={sample.lang}>
|
||||
<TabPanel key={sample.lang + '_' + (sample.label || '')}>
|
||||
{isPayloadSample(sample) ? (
|
||||
<div>
|
||||
<PayloadSamples content={sample.requestBodyContent} />
|
||||
</div>
|
||||
) : (
|
||||
<SourceCodeWithCopy lang={sample.lang} source={sample.source} />
|
||||
)}
|
||||
</TabPanel>
|
||||
))}
|
||||
</Tabs>
|
||||
|
|
|
@ -28,8 +28,7 @@ export class ResponseView extends React.Component<{ response: ResponseModel }> {
|
|||
code={code}
|
||||
opened={expanded}
|
||||
/>
|
||||
{expanded &&
|
||||
!empty && (
|
||||
{expanded && !empty && (
|
||||
<ResponseDetailsWrap>
|
||||
<ResponseDetails response={this.props.response} />
|
||||
</ResponseDetailsWrap>
|
||||
|
|
|
@ -22,7 +22,7 @@ export class ResponseTitle extends React.PureComponent<ResponseTitleProps> {
|
|||
<ShelfIcon
|
||||
size={'1.5em'}
|
||||
color={type}
|
||||
direction={opened ? 'up' : 'down'}
|
||||
direction={opened ? 'down' : 'right'}
|
||||
float={'left'}
|
||||
/>
|
||||
)}
|
||||
|
|
|
@ -9,6 +9,7 @@ import { DiscriminatorDropdown } from './DiscriminatorDropdown';
|
|||
import { SchemaProps } from './Schema';
|
||||
|
||||
import { mapWithLast } from '../../utils';
|
||||
import { OptionsContext } from '../OptionsProvider';
|
||||
|
||||
export interface ObjectSchemaProps extends SchemaProps {
|
||||
discriminator?: {
|
||||
|
@ -19,6 +20,8 @@ export interface ObjectSchemaProps extends SchemaProps {
|
|||
|
||||
@observer
|
||||
export class ObjectSchema extends React.Component<ObjectSchemaProps> {
|
||||
static contextType = OptionsContext;
|
||||
|
||||
get parentSchema() {
|
||||
return this.props.discriminator!.parentSchema;
|
||||
}
|
||||
|
@ -34,13 +37,15 @@ export class ObjectSchema extends React.Component<ObjectSchemaProps> {
|
|||
|
||||
const filteredFields = needFilter
|
||||
? fields.filter(item => {
|
||||
return (
|
||||
(this.props.skipReadOnly && !item.schema.readOnly) ||
|
||||
(this.props.skipWriteOnly && !item.schema.writeOnly)
|
||||
return !(
|
||||
(this.props.skipReadOnly && item.schema.readOnly) ||
|
||||
(this.props.skipWriteOnly && item.schema.writeOnly)
|
||||
);
|
||||
})
|
||||
: fields;
|
||||
|
||||
const expandByDefault = this.context.expandSingleSchemaField && filteredFields.length === 1;
|
||||
|
||||
return (
|
||||
<PropertiesTable>
|
||||
{showTitle && <PropertiesTableCaption>{this.props.schema.title}</PropertiesTableCaption>}
|
||||
|
@ -51,6 +56,7 @@ export class ObjectSchema extends React.Component<ObjectSchemaProps> {
|
|||
key={field.name}
|
||||
isLast={isLast}
|
||||
field={field}
|
||||
expandByDefault={expandByDefault}
|
||||
renderDiscriminatorSwitch={
|
||||
(discriminator &&
|
||||
discriminator.fieldName === field.name &&
|
||||
|
|
|
@ -44,9 +44,7 @@ export class Schema extends React.Component<Partial<SchemaProps>> {
|
|||
if (discriminatorProp !== undefined) {
|
||||
if (!oneOf || !oneOf.length) {
|
||||
throw new Error(
|
||||
`Looks like you are using discriminator wrong: you don't have any definition inherited from the ${
|
||||
schema.title
|
||||
}`,
|
||||
`Looks like you are using discriminator wrong: you don't have any definition inherited from the ${schema.title}`,
|
||||
);
|
||||
}
|
||||
return (
|
||||
|
@ -66,9 +64,9 @@ export class Schema extends React.Component<Partial<SchemaProps>> {
|
|||
|
||||
switch (type) {
|
||||
case 'object':
|
||||
return <ObjectSchema {...this.props as any} />;
|
||||
return <ObjectSchema {...(this.props as any)} />;
|
||||
case 'array':
|
||||
return <ArraySchema {...this.props as any} />;
|
||||
return <ArraySchema {...(this.props as any)} />;
|
||||
}
|
||||
|
||||
// TODO: maybe adjust FieldDetails to accept schema
|
||||
|
|
93
src/components/SchemaDefinition/SchemaDefinition.tsx
Normal file
93
src/components/SchemaDefinition/SchemaDefinition.tsx
Normal file
|
@ -0,0 +1,93 @@
|
|||
import * as React from 'react';
|
||||
|
||||
import { DarkRightPanel, MiddlePanel, MimeLabel, Row, Section } from '../../common-elements';
|
||||
import { MediaTypeModel, OpenAPIParser, RedocNormalizedOptions } from '../../services';
|
||||
import styled from '../../styled-components';
|
||||
import { OpenAPIMediaType } from '../../types';
|
||||
import { DropdownOrLabel } from '../DropdownOrLabel/DropdownOrLabel';
|
||||
import { MediaTypeSamples } from '../PayloadSamples/MediaTypeSamples';
|
||||
import { InvertedSimpleDropdown } from '../PayloadSamples/styled.elements';
|
||||
import { Schema } from '../Schema';
|
||||
|
||||
export interface ObjectDescriptionProps {
|
||||
schemaRef: string;
|
||||
exampleRef?: string;
|
||||
showReadOnly?: boolean;
|
||||
showWriteOnly?: boolean;
|
||||
parser: OpenAPIParser;
|
||||
options: RedocNormalizedOptions;
|
||||
}
|
||||
|
||||
export class SchemaDefinition extends React.PureComponent<ObjectDescriptionProps> {
|
||||
private static getMediaType(schemaRef: string, exampleRef?: string): OpenAPIMediaType {
|
||||
if (!schemaRef) {
|
||||
return {};
|
||||
}
|
||||
|
||||
const info: OpenAPIMediaType = {
|
||||
schema: { $ref: schemaRef },
|
||||
};
|
||||
|
||||
if (exampleRef) {
|
||||
info.examples = { example: { $ref: exampleRef } };
|
||||
}
|
||||
|
||||
return info;
|
||||
}
|
||||
|
||||
private _mediaModel: MediaTypeModel;
|
||||
|
||||
private get mediaModel() {
|
||||
const { parser, schemaRef, exampleRef, options } = this.props;
|
||||
if (!this._mediaModel) {
|
||||
this._mediaModel = new MediaTypeModel(
|
||||
parser,
|
||||
'json',
|
||||
false,
|
||||
SchemaDefinition.getMediaType(schemaRef, exampleRef),
|
||||
options,
|
||||
);
|
||||
}
|
||||
|
||||
return this._mediaModel;
|
||||
}
|
||||
|
||||
render() {
|
||||
const { showReadOnly = true, showWriteOnly = false } = this.props;
|
||||
return (
|
||||
<Section>
|
||||
<Row>
|
||||
<MiddlePanel>
|
||||
<Schema
|
||||
skipWriteOnly={!showWriteOnly}
|
||||
skipReadOnly={!showReadOnly}
|
||||
schema={this.mediaModel.schema}
|
||||
/>
|
||||
</MiddlePanel>
|
||||
<DarkRightPanel>
|
||||
<MediaSamplesWrap>
|
||||
<MediaTypeSamples renderDropdown={this.renderDropdown} mediaType={this.mediaModel} />
|
||||
</MediaSamplesWrap>
|
||||
</DarkRightPanel>
|
||||
</Row>
|
||||
</Section>
|
||||
);
|
||||
}
|
||||
|
||||
private renderDropdown = props => {
|
||||
return <DropdownOrLabel Label={MimeLabel} Dropdown={InvertedSimpleDropdown} {...props} />;
|
||||
};
|
||||
}
|
||||
|
||||
const MediaSamplesWrap = styled.div`
|
||||
background: ${({ theme }) => theme.codeSample.backgroundColor};
|
||||
& > div,
|
||||
& > pre {
|
||||
padding: ${props => props.theme.spacing.unit * 4}px;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
& > div > pre {
|
||||
padding: 0;
|
||||
}
|
||||
`;
|
|
@ -7,6 +7,7 @@ import { MenuItem } from '../SideMenu/MenuItem';
|
|||
import { MarkerService } from '../../services/MarkerService';
|
||||
import { SearchResult } from '../../services/SearchWorker.worker';
|
||||
|
||||
import { bind, debounce } from 'decko';
|
||||
import { PerfectScrollbarWrap } from '../../common-elements/perfect-scrollbar';
|
||||
import {
|
||||
ClearIcon,
|
||||
|
@ -94,11 +95,18 @@ export class SearchBox extends React.PureComponent<SearchBoxProps, SearchBoxStat
|
|||
setResults(results: SearchResult[], term: string) {
|
||||
this.setState({
|
||||
results,
|
||||
term,
|
||||
});
|
||||
this.props.marker.mark(term);
|
||||
}
|
||||
|
||||
@bind
|
||||
@debounce(400)
|
||||
searchCallback(searchTerm: string) {
|
||||
this.props.search.search(searchTerm).then(res => {
|
||||
this.setResults(res, searchTerm);
|
||||
});
|
||||
}
|
||||
|
||||
search = (event: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const q = event.target.value;
|
||||
if (q.length < 3) {
|
||||
|
@ -106,13 +114,12 @@ export class SearchBox extends React.PureComponent<SearchBoxProps, SearchBoxStat
|
|||
return;
|
||||
}
|
||||
|
||||
this.setState({
|
||||
this.setState(
|
||||
{
|
||||
term: q,
|
||||
});
|
||||
|
||||
this.props.search.search(event.target.value).then(res => {
|
||||
this.setResults(res, q);
|
||||
});
|
||||
},
|
||||
() => this.searchCallback(this.state.term),
|
||||
);
|
||||
};
|
||||
|
||||
render() {
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
import { darken } from 'polished';
|
||||
import * as React from 'react';
|
||||
|
||||
import { darken, getLuminance, lighten } from 'polished';
|
||||
import styled from '../../styled-components';
|
||||
import { MenuItemLabel } from '../SideMenu/styled.elements';
|
||||
|
||||
|
@ -16,9 +17,15 @@ export const SearchInput = styled.input.attrs(() => ({
|
|||
padding: 5px ${props => props.theme.spacing.unit * 2}px 5px
|
||||
${props => props.theme.spacing.unit * 4}px;
|
||||
border: 0;
|
||||
border-bottom: 1px solid ${({theme}) => darken(0.1, theme.menu.backgroundColor)};
|
||||
font-family: ${({theme}) => theme.typography.fontFamily};
|
||||
font-size: 1em;
|
||||
border-bottom: 1px solid
|
||||
${({ theme }) =>
|
||||
(getLuminance(theme.menu.backgroundColor) > 0.5 ? darken : lighten)(
|
||||
0.1,
|
||||
theme.menu.backgroundColor,
|
||||
)};
|
||||
font-family: ${({ theme }) => theme.typography.fontFamily};
|
||||
font-weight: bold;
|
||||
font-size: 13px;
|
||||
color: ${props => props.theme.menu.textColor};
|
||||
background-color: transparent;
|
||||
outline: none;
|
||||
|
@ -26,23 +33,14 @@ export const SearchInput = styled.input.attrs(() => ({
|
|||
|
||||
export const SearchIcon = styled((props: { className?: string }) => (
|
||||
<svg
|
||||
version="1.1"
|
||||
id="Layer_1"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
x="0px"
|
||||
y="0px"
|
||||
className={props.className}
|
||||
viewBox="0 0 24 24"
|
||||
xmlSpace="preserve"
|
||||
version="1.1"
|
||||
viewBox="0 0 1000 1000"
|
||||
x="0px"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
y="0px"
|
||||
>
|
||||
<g>
|
||||
<path
|
||||
className="st0"
|
||||
d="M22.7,21.5l-5.1-5c1.5-1.7,2.4-4,2.4-6.5c0-5.5-4.5-10-10-10S0,4.5,0,10s4.5,10,10,10c2.3,0,4.4-0.8,6.1-2.1
|
||||
l5.2,5.1c0.2,0.2,0.4,0.3,0.7,0.3c0.3,0,0.5-0.1,0.7-0.3C23.1,22.5,23.1,21.9,22.7,21.5z M10,18c-4.4,0-8-3.6-8-8s3.6-8,8-8
|
||||
s8,3.6,8,8S14.4,18,10,18z"
|
||||
/>
|
||||
</g>
|
||||
<path d="M968.2,849.4L667.3,549c83.9-136.5,66.7-317.4-51.7-435.6C477.1-25,252.5-25,113.9,113.4c-138.5,138.3-138.5,362.6,0,501C219.2,730.1,413.2,743,547.6,666.5l301.9,301.4c43.6,43.6,76.9,14.9,104.2-12.4C981,928.3,1011.8,893,968.2,849.4z M524.5,522c-88.9,88.7-233,88.7-321.8,0c-88.9-88.7-88.9-232.6,0-321.3c88.9-88.7,233-88.7,321.8,0C613.4,289.4,613.4,433.3,524.5,522z" />
|
||||
</svg>
|
||||
)).attrs({
|
||||
className: 'search-icon',
|
||||
|
@ -59,11 +57,12 @@ export const SearchIcon = styled((props: { className?: string }) => (
|
|||
|
||||
export const SearchResultsBox = styled.div`
|
||||
padding: ${props => props.theme.spacing.unit}px 0;
|
||||
background-color: #ededed;
|
||||
background-color: ${({ theme }) => darken(0.05, theme.menu.backgroundColor)}};
|
||||
color: ${props => props.theme.menu.textColor};
|
||||
min-height: 150px;
|
||||
max-height: 250px;
|
||||
border-top: 1px solid #e1e1e1;
|
||||
border-bottom: 1px solid #e1e1e1;
|
||||
border-top: ${({ theme }) => darken(0.1, theme.menu.backgroundColor)}};
|
||||
border-bottom: ${({ theme }) => darken(0.1, theme.menu.backgroundColor)}};
|
||||
margin-top: 10px;
|
||||
line-height: 1.4;
|
||||
font-size: 0.9em;
|
||||
|
@ -72,17 +71,14 @@ export const SearchResultsBox = styled.div`
|
|||
padding-top: 6px;
|
||||
padding-bottom: 6px;
|
||||
|
||||
&:hover {
|
||||
background-color: #e1e1e1;
|
||||
&:hover,
|
||||
&.active {
|
||||
background-color: ${({ theme }) => darken(0.1, theme.menu.backgroundColor)};
|
||||
}
|
||||
|
||||
> svg {
|
||||
display: none;
|
||||
}
|
||||
|
||||
&.active {
|
||||
background-color: #e1e1e1;
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
|
|
|
@ -49,7 +49,7 @@ export class OAuthFlow extends React.PureComponent<OAuthFlowProps> {
|
|||
<strong> Scopes: </strong>
|
||||
</div>
|
||||
<ul>
|
||||
{Object.keys(flow!.scopes).map(scope => (
|
||||
{Object.keys(flow!.scopes || {}).map(scope => (
|
||||
<li key={scope}>
|
||||
<code>{scope}</code> - <Markdown inline={true} source={flow!.scopes[scope] || ''} />
|
||||
</li>
|
||||
|
@ -80,7 +80,7 @@ export class SecurityDefs extends React.PureComponent<SecurityDefsProps> {
|
|||
<table className="security-details">
|
||||
<tbody>
|
||||
<tr>
|
||||
<th> Security scheme type: </th>
|
||||
<th> Security Scheme Type </th>
|
||||
<td> {AUTH_TYPES[scheme.type] || scheme.type} </td>
|
||||
</tr>
|
||||
{scheme.apiKey ? (
|
||||
|
@ -97,7 +97,7 @@ export class SecurityDefs extends React.PureComponent<SecurityDefsProps> {
|
|||
scheme.http.scheme === 'bearer' && scheme.http.bearerFormat && (
|
||||
<tr key="bearer">
|
||||
<th> Bearer format </th>
|
||||
<td> "{scheme.http.bearerFormat}" </td>
|
||||
<td> "{scheme.http.bearerFormat}" </td>
|
||||
</tr>
|
||||
),
|
||||
]
|
||||
|
@ -105,7 +105,11 @@ export class SecurityDefs extends React.PureComponent<SecurityDefsProps> {
|
|||
<tr>
|
||||
<th> Connect URL </th>
|
||||
<td>
|
||||
<a target="_blank" href={scheme.openId.connectUrl}>
|
||||
<a
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
href={scheme.openId.connectUrl}
|
||||
>
|
||||
{scheme.openId.connectUrl}
|
||||
</a>
|
||||
</td>
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
// import { observe } from 'mobx';
|
||||
import { observer } from 'mobx-react';
|
||||
import * as React from 'react';
|
||||
|
||||
|
@ -15,7 +16,7 @@ export interface MenuItemProps {
|
|||
|
||||
@observer
|
||||
export class MenuItem extends React.Component<MenuItemProps> {
|
||||
ref: Element | null;
|
||||
ref = React.createRef<HTMLLabelElement>();
|
||||
|
||||
activate = (evt: React.MouseEvent<HTMLElement>) => {
|
||||
this.props.onActivate!(this.props.item);
|
||||
|
@ -31,42 +32,30 @@ export class MenuItem extends React.Component<MenuItemProps> {
|
|||
}
|
||||
|
||||
scrollIntoViewIfActive() {
|
||||
if (this.props.item.active && this.ref) {
|
||||
this.ref.scrollIntoViewIfNeeded();
|
||||
if (this.props.item.active && this.ref.current) {
|
||||
this.ref.current.scrollIntoViewIfNeeded();
|
||||
}
|
||||
}
|
||||
|
||||
saveRef = ref => {
|
||||
this.ref = ref;
|
||||
};
|
||||
|
||||
render() {
|
||||
const { item, withoutChildren } = this.props;
|
||||
return (
|
||||
<MenuItemLi
|
||||
onClick={this.activate}
|
||||
depth={item.depth}
|
||||
ref={this.saveRef}
|
||||
data-item-id={item.id}
|
||||
>
|
||||
<MenuItemLi onClick={this.activate} depth={item.depth} data-item-id={item.id}>
|
||||
{item.type === 'operation' ? (
|
||||
<OperationMenuItemContent {...this.props} item={item as OperationModel} />
|
||||
) : (
|
||||
<MenuItemLabel depth={item.depth} active={item.active} type={item.type}>
|
||||
<MenuItemLabel depth={item.depth} active={item.active} type={item.type} ref={this.ref}>
|
||||
<MenuItemTitle title={item.name}>
|
||||
{item.name}
|
||||
{this.props.children}
|
||||
</MenuItemTitle>
|
||||
{(item.depth > 0 &&
|
||||
item.items.length > 0 && (
|
||||
{(item.depth > 0 && item.items.length > 0 && (
|
||||
<ShelfIcon float={'right'} direction={item.expanded ? 'down' : 'right'} />
|
||||
)) ||
|
||||
null}
|
||||
</MenuItemLabel>
|
||||
)}
|
||||
{!withoutChildren &&
|
||||
item.items &&
|
||||
item.items.length > 0 && (
|
||||
{!withoutChildren && item.items && item.items.length > 0 && (
|
||||
<MenuItems
|
||||
expanded={item.expanded}
|
||||
items={item.items}
|
||||
|
@ -83,11 +72,24 @@ export interface OperationMenuItemContentProps {
|
|||
}
|
||||
|
||||
@observer
|
||||
class OperationMenuItemContent extends React.Component<OperationMenuItemContentProps> {
|
||||
export class OperationMenuItemContent extends React.Component<OperationMenuItemContentProps> {
|
||||
ref = React.createRef<HTMLLabelElement>();
|
||||
|
||||
componentDidUpdate() {
|
||||
if (this.props.item.active && this.ref.current) {
|
||||
this.ref.current.scrollIntoViewIfNeeded();
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
const { item } = this.props;
|
||||
return (
|
||||
<MenuItemLabel depth={item.depth} active={item.active} deprecated={item.deprecated}>
|
||||
<MenuItemLabel
|
||||
depth={item.depth}
|
||||
active={item.active}
|
||||
deprecated={item.deprecated}
|
||||
ref={this.ref}
|
||||
>
|
||||
<OperationBadge type={item.httpVerb}>{shortenHTTPVerb(item.httpVerb)}</OperationBadge>
|
||||
<MenuItemTitle width="calc(100% - 38px)">
|
||||
{item.name}
|
||||
|
|
|
@ -1,15 +1,16 @@
|
|||
import { observer } from 'mobx-react';
|
||||
import * as React from 'react';
|
||||
import { StyledLink } from '../../../src/common-elements';
|
||||
|
||||
import { PerfectScrollbarWrap } from '../../common-elements/perfect-scrollbar';
|
||||
|
||||
import { IMenuItem, MenuStore } from '../../services/MenuStore';
|
||||
import { OptionsContext } from '../OptionsProvider';
|
||||
import { MenuItems } from './MenuItems';
|
||||
|
||||
import { PerfectScrollbarWrap } from '../../common-elements/perfect-scrollbar';
|
||||
import { RedocAttribution } from './styled.elements';
|
||||
|
||||
@observer
|
||||
export class SideMenu extends React.Component<{ menu: MenuStore; className?: string }> {
|
||||
static contextType = OptionsContext;
|
||||
private _updateScroll?: () => void;
|
||||
|
||||
render() {
|
||||
|
@ -22,17 +23,21 @@ export class SideMenu extends React.Component<{ menu: MenuStore; className?: str
|
|||
wheelPropagation: false,
|
||||
}}
|
||||
>
|
||||
<MenuItems items={store.items} onActivate={this.activate} root={true}/>
|
||||
<MenuItems items={store.items} onActivate={this.activate} root={true} />
|
||||
<RedocAttribution>
|
||||
<StyledLink href="https://www.opentext.com/" target={'_blank'}>
|
||||
<a target="_blank" rel="noopener noreferrer" href="https://www.opentext.com/">
|
||||
© Copyright 2019 OpenText Corp
|
||||
</StyledLink>
|
||||
</a>
|
||||
</RedocAttribution>
|
||||
</PerfectScrollbarWrap>
|
||||
);
|
||||
}
|
||||
|
||||
activate = (item: IMenuItem) => {
|
||||
if (item && item.active && this.context.menuToggle) {
|
||||
return item.expanded ? item.collapse() : item.expand();
|
||||
}
|
||||
|
||||
this.props.menu.activateAndScroll(item, true);
|
||||
setTimeout(() => {
|
||||
if (this._updateScroll) {
|
||||
|
|
|
@ -104,7 +104,7 @@ export const menuItemDepth = {
|
|||
font-size: 0.929em;
|
||||
text-transform: ${({ theme }) => theme.menu.level1Items.textTransform};
|
||||
&:hover {
|
||||
color: ${props => props.theme.colors.primary.main};
|
||||
color: ${props => props.theme.menu.activeTextColor};
|
||||
}
|
||||
`,
|
||||
2: css`
|
||||
|
@ -126,7 +126,7 @@ export const MenuItemLabel = styled.label.attrs((props: MenuItemLabelType) => ({
|
|||
}),
|
||||
}))<MenuItemLabelType>`
|
||||
cursor: pointer;
|
||||
color: ${props => (props.active ? props.theme.colors.primary.main : props.theme.menu.textColor)};
|
||||
color: ${props => (props.active ? props.theme.menu.activeTextColor : props.theme.menu.textColor)};
|
||||
margin: 0;
|
||||
padding: 12.5px ${props => props.theme.spacing.unit * 4}px;
|
||||
${({ depth, type, theme }) =>
|
||||
|
|
|
@ -19,6 +19,10 @@ export interface StickySidebarProps {
|
|||
menu: MenuStore;
|
||||
}
|
||||
|
||||
export interface StickySidebarState {
|
||||
offsetTop?: string;
|
||||
}
|
||||
|
||||
const stickyfill = Stickyfill && Stickyfill();
|
||||
|
||||
const StyledStickySidebar = styled.div<{ open?: boolean }>`
|
||||
|
@ -29,7 +33,7 @@ const StyledStickySidebar = styled.div<{ open?: boolean }>`
|
|||
flex-direction: column;
|
||||
|
||||
backface-visibility: hidden;
|
||||
contain: strict;
|
||||
/* contain: strict; TODO: breaks layout since Chrome 80*/
|
||||
|
||||
height: 100vh;
|
||||
position: sticky;
|
||||
|
@ -40,7 +44,7 @@ const StyledStickySidebar = styled.div<{ open?: boolean }>`
|
|||
position: fixed;
|
||||
z-index: 20;
|
||||
width: 100%;
|
||||
background: #ffffff;
|
||||
background: ${({ theme }) => theme.menu.backgroundColor};
|
||||
display: ${props => (props.open ? 'flex' : 'none')};
|
||||
`};
|
||||
|
||||
|
@ -77,13 +81,26 @@ const FloatingButton = styled.div`
|
|||
`;
|
||||
|
||||
@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;
|
||||
|
||||
componentDidMount() {
|
||||
if (stickyfill) {
|
||||
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() {
|
||||
|
@ -92,7 +109,7 @@ export class StickyResponsiveSidebar extends React.Component<StickySidebarProps>
|
|||
}
|
||||
}
|
||||
|
||||
getScrollYOffset(options) {
|
||||
getScrollYOffset(options: RedocNormalizedOptions) {
|
||||
let top;
|
||||
if (this.props.scrollYOffset !== undefined) {
|
||||
top = RedocNormalizedOptions.normalizeScrollYOffset(this.props.scrollYOffset)();
|
||||
|
@ -105,22 +122,17 @@ export class StickyResponsiveSidebar extends React.Component<StickySidebarProps>
|
|||
render() {
|
||||
const open = this.props.menu.sideBarOpened;
|
||||
|
||||
const style = options => {
|
||||
const top = this.getScrollYOffset(options);
|
||||
return {
|
||||
top,
|
||||
height: `calc(100vh - ${top})`,
|
||||
};
|
||||
};
|
||||
const top = this.state.offsetTop;
|
||||
|
||||
return (
|
||||
<OptionsContext.Consumer>
|
||||
{options => (
|
||||
<>
|
||||
<StyledStickySidebar
|
||||
open={open}
|
||||
className={this.props.className}
|
||||
style={style(options)}
|
||||
style={{
|
||||
top,
|
||||
height: `calc(100vh - ${top})`,
|
||||
}}
|
||||
// tslint:disable-next-line
|
||||
ref={el => {
|
||||
this.stickyElement = el as any;
|
||||
|
@ -132,16 +144,10 @@ export class StickyResponsiveSidebar extends React.Component<StickySidebarProps>
|
|||
<AnimatedChevronButton open={open} />
|
||||
</FloatingButton>
|
||||
</>
|
||||
)}
|
||||
</OptionsContext.Consumer>
|
||||
);
|
||||
}
|
||||
|
||||
private toggleNavMenu = () => {
|
||||
this.props.menu.toggleSidebar();
|
||||
};
|
||||
|
||||
// private closeNavMenu = () => {
|
||||
// this.setState({ open: false });
|
||||
// };
|
||||
}
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import memoize from 'memoize-one';
|
||||
import * as memoize from 'memoize-one/dist/memoize-one.cjs'; // fixme: https://github.com/alexreardon/memoize-one/issues/37
|
||||
import { Component, createContext } from 'react';
|
||||
|
||||
import { AppStore } from '../services/';
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
/* eslint-disable import/no-internal-modules */
|
||||
/* tslint:disable:no-implicit-dependencies */
|
||||
|
||||
import { shallow } from 'enzyme';
|
||||
|
|
|
@ -24,14 +24,14 @@ describe('Components', () => {
|
|||
});
|
||||
|
||||
test('should collapse/uncollapse', () => {
|
||||
expect(component.html()).not.toContain('class="hoverable"'); // all are collapesed by default
|
||||
expect(component.html()).not.toContain('class="hoverable"'); // all are collapsed by default
|
||||
const expandAll = component.find('div > span[children=" Expand all "]');
|
||||
expandAll.simulate('click');
|
||||
expect(component.html()).toContain('class="hoverable"'); // all are collapesed
|
||||
expect(component.html()).toContain('class="hoverable"'); // all are collapsed
|
||||
|
||||
const collapseAll = component.find('div > span[children=" Collapse all "]');
|
||||
collapseAll.simulate('click');
|
||||
expect(component.html()).not.toContain('class="hoverable"'); // all are collapesed
|
||||
expect(component.html()).not.toContain('class="hoverable"'); // all are collapsed
|
||||
});
|
||||
|
||||
test('should collapse/uncollapse', () => {
|
||||
|
|
|
@ -9,7 +9,8 @@ exports[`Components SchemaView discriminator should correctly render discriminat
|
|||
"deprecated": false,
|
||||
"description": "",
|
||||
"example": undefined,
|
||||
"expanded": false,
|
||||
"expanded": undefined,
|
||||
"explode": false,
|
||||
"in": undefined,
|
||||
"kind": "field",
|
||||
"name": "packSize",
|
||||
|
@ -58,7 +59,8 @@ exports[`Components SchemaView discriminator should correctly render discriminat
|
|||
"deprecated": false,
|
||||
"description": "",
|
||||
"example": undefined,
|
||||
"expanded": false,
|
||||
"expanded": undefined,
|
||||
"explode": false,
|
||||
"in": undefined,
|
||||
"kind": "field",
|
||||
"name": "type",
|
||||
|
|
|
@ -28,3 +28,5 @@ export * from './OptionsProvider';
|
|||
export * from './SideMenu/';
|
||||
export * from './StickySidebar/StickyResponsiveSidebar';
|
||||
export * from './SearchBox/SearchBox';
|
||||
export * from './SchemaDefinition/SchemaDefinition';
|
||||
export * from './SourceCode/SourceCode';
|
||||
|
|
|
@ -1,11 +1,12 @@
|
|||
import 'core-js/es6/promise';
|
||||
import 'core-js/fn/array/find';
|
||||
import 'core-js/fn/object/assign';
|
||||
import 'core-js/fn/string/ends-with';
|
||||
import 'core-js/fn/string/starts-with';
|
||||
import 'core-js/es/promise';
|
||||
|
||||
import 'core-js/es6/map';
|
||||
import 'core-js/es6/symbol';
|
||||
import 'core-js/es/array/find';
|
||||
import 'core-js/es/object/assign';
|
||||
import 'core-js/es/string/ends-with';
|
||||
import 'core-js/es/string/starts-with';
|
||||
|
||||
import 'core-js/es/map';
|
||||
import 'core-js/es/symbol';
|
||||
|
||||
import 'unfetch/polyfill/index';
|
||||
import 'url-polyfill';
|
||||
|
|
|
@ -10,8 +10,15 @@ import { RedocNormalizedOptions, RedocRawOptions } from './RedocNormalizedOption
|
|||
import { ScrollService } from './ScrollService';
|
||||
import { SearchStore } from './SearchStore';
|
||||
|
||||
import { SchemaDefinition } from '../components/SchemaDefinition/SchemaDefinition';
|
||||
import { SecurityDefs } from '../components/SecuritySchemes/SecuritySchemes';
|
||||
import { SECURITY_DEFINITIONS_COMPONENT_NAME } from '../utils/openapi';
|
||||
import {
|
||||
SCHEMA_DEFINITION_JSX_NAME,
|
||||
SECURITY_DEFINITIONS_COMPONENT_NAME,
|
||||
SECURITY_DEFINITIONS_JSX_NAME,
|
||||
} from '../utils/openapi';
|
||||
|
||||
import { IS_BROWSER } from '../utils';
|
||||
|
||||
export interface StoreState {
|
||||
menu: {
|
||||
|
@ -96,6 +103,9 @@ export class AppStore {
|
|||
dispose() {
|
||||
this.scroll.dispose();
|
||||
this.menu.dispose();
|
||||
if (this.search) {
|
||||
this.search.dispose();
|
||||
}
|
||||
if (this.disposer != null) {
|
||||
this.disposer();
|
||||
}
|
||||
|
@ -126,16 +136,16 @@ export class AppStore {
|
|||
|
||||
const elements: Element[] = [];
|
||||
for (let i = start; i < end; i++) {
|
||||
let elem = this.menu.getElementAt(i);
|
||||
const elem = this.menu.getElementAt(i);
|
||||
if (!elem) {
|
||||
continue;
|
||||
}
|
||||
if (this.menu.flatItems[i].type === 'section') {
|
||||
elem = elem.parentElement!.parentElement;
|
||||
}
|
||||
if (elem) {
|
||||
elements.push(elem);
|
||||
}
|
||||
|
||||
if (idx === -1 && IS_BROWSER) {
|
||||
const $description = document.querySelector('[data-role="redoc-description"]');
|
||||
if ($description) elements.push($description);
|
||||
}
|
||||
|
||||
this.marker.addOnly(elements);
|
||||
|
@ -151,5 +161,18 @@ const DEFAULT_OPTIONS: RedocRawOptions = {
|
|||
securitySchemes: store.spec.securitySchemes,
|
||||
}),
|
||||
},
|
||||
[SECURITY_DEFINITIONS_JSX_NAME]: {
|
||||
component: SecurityDefs,
|
||||
propsSelector: (store: AppStore) => ({
|
||||
securitySchemes: store.spec.securitySchemes,
|
||||
}),
|
||||
},
|
||||
[SCHEMA_DEFINITION_JSX_NAME]: {
|
||||
component: SchemaDefinition,
|
||||
propsSelector: (store: AppStore) => ({
|
||||
parser: store.spec.parser,
|
||||
options: store.options,
|
||||
}),
|
||||
},
|
||||
},
|
||||
};
|
||||
|
|
|
@ -28,7 +28,10 @@ export class ClipboardService {
|
|||
if ((document as any).selection) {
|
||||
(document as any).selection.empty();
|
||||
} else if (window.getSelection) {
|
||||
window.getSelection().removeAllRanges();
|
||||
const selection = window.getSelection();
|
||||
if (selection) {
|
||||
selection.removeAllRanges();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import * as marked from 'marked';
|
||||
|
||||
import { highlight, safeSlugify } from '../utils';
|
||||
import { highlight, safeSlugify, unescapeHTMLChars } from '../utils';
|
||||
import { AppStore } from './AppStore';
|
||||
import { RedocNormalizedOptions } from './RedocNormalizedOptions';
|
||||
|
||||
|
@ -45,6 +45,14 @@ export class MarkdownRenderer {
|
|||
return compRegexp.test(rawText);
|
||||
}
|
||||
|
||||
static getTextBeforeHading(md: string, heading: string): string {
|
||||
const headingLinePos = md.search(new RegExp(`^##?\\s+${heading}`, 'm'));
|
||||
if (headingLinePos > -1) {
|
||||
return md.substring(0, headingLinePos);
|
||||
}
|
||||
return md;
|
||||
}
|
||||
|
||||
headings: MarkdownHeading[] = [];
|
||||
currentTopHeading: MarkdownHeading;
|
||||
|
||||
|
@ -65,6 +73,7 @@ export class MarkdownRenderer {
|
|||
container: MarkdownHeading[] = this.headings,
|
||||
parentId?: string,
|
||||
): MarkdownHeading {
|
||||
name = unescapeHTMLChars(name);
|
||||
const item = {
|
||||
id: parentId ? `${parentId}/${safeSlugify(name)}` : `section/${safeSlugify(name)}`,
|
||||
name,
|
||||
|
@ -88,7 +97,7 @@ export class MarkdownRenderer {
|
|||
}
|
||||
|
||||
attachHeadingsDescriptions(rawText: string) {
|
||||
const buildRegexp = heading => {
|
||||
const buildRegexp = (heading: MarkdownHeading) => {
|
||||
return new RegExp(`##?\\s+${heading.name.replace(/[-\/\\^$*+?.()|[\]{}]/g, '\\$&')}`);
|
||||
};
|
||||
|
||||
|
@ -118,7 +127,12 @@ export class MarkdownRenderer {
|
|||
.trim();
|
||||
}
|
||||
|
||||
headingRule = (text: string, level: number, raw: string, slugger: marked.Slugger) => {
|
||||
headingRule = (
|
||||
text: string,
|
||||
level: 1 | 2 | 3 | 4 | 5 | 6,
|
||||
raw: string,
|
||||
slugger: marked.Slugger,
|
||||
) => {
|
||||
if (level === 1) {
|
||||
this.currentTopHeading = this.saveHeading(text, level);
|
||||
} else if (level === 2) {
|
||||
|
|
|
@ -42,7 +42,7 @@ export class MenuBuilder {
|
|||
|
||||
const items: ContentItemModel[] = [];
|
||||
const tagsMap = MenuBuilder.getTagsWithOperations(spec);
|
||||
items.push(...MenuBuilder.addMarkdownItems(spec.info.description || '', options));
|
||||
items.push(...MenuBuilder.addMarkdownItems(spec.info.description || '', undefined, 1, options));
|
||||
if (spec['x-tagGroups'] && spec['x-tagGroups'].length > 0) {
|
||||
items.push(
|
||||
...MenuBuilder.getTagGroupsItems(parser, undefined, spec['x-tagGroups'], tagsMap, options),
|
||||
|
@ -59,14 +59,23 @@ export class MenuBuilder {
|
|||
*/
|
||||
static addMarkdownItems(
|
||||
description: string,
|
||||
parent: GroupModel | undefined,
|
||||
initialDepth: number,
|
||||
options: RedocNormalizedOptions,
|
||||
): ContentItemModel[] {
|
||||
const renderer = new MarkdownRenderer(options);
|
||||
const headings = renderer.extractHeadings(description || '');
|
||||
|
||||
const mapHeadingsDeep = (parent, items, depth = 1) =>
|
||||
if (headings.length && parent && parent.description) {
|
||||
parent.description = MarkdownRenderer.getTextBeforeHading(
|
||||
parent.description,
|
||||
headings[0].name,
|
||||
);
|
||||
}
|
||||
|
||||
const mapHeadingsDeep = (_parent, items, depth = 1) =>
|
||||
items.map(heading => {
|
||||
const group = new GroupModel('section', heading, parent);
|
||||
const group = new GroupModel('section', heading, _parent);
|
||||
group.depth = depth;
|
||||
if (heading.items) {
|
||||
group.items = mapHeadingsDeep(group, heading.items, depth + 1);
|
||||
|
@ -82,11 +91,11 @@ export class MenuBuilder {
|
|||
return group;
|
||||
});
|
||||
|
||||
return mapHeadingsDeep(undefined, headings);
|
||||
return mapHeadingsDeep(parent, headings, initialDepth);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns array of OperationsGroup items for the tag groups (x-tagGroups vendor extenstion)
|
||||
* Returns array of OperationsGroup items for the tag groups (x-tagGroups vendor extension)
|
||||
* @param tags value of `x-tagGroups` vendor extension
|
||||
*/
|
||||
static getTagGroupsItems(
|
||||
|
@ -144,15 +153,22 @@ export class MenuBuilder {
|
|||
}
|
||||
const item = new GroupModel('tag', tag, parent);
|
||||
item.depth = GROUP_DEPTH + 1;
|
||||
item.items = this.getOperationsItems(parser, item, tag, item.depth + 1, options);
|
||||
|
||||
// don't put empty tag into content, instead put its operations
|
||||
if (tag.name === '') {
|
||||
const items = this.getOperationsItems(parser, undefined, tag, item.depth + 1, options);
|
||||
const items = [
|
||||
...MenuBuilder.addMarkdownItems(tag.description || '', item, item.depth + 1, options),
|
||||
...this.getOperationsItems(parser, undefined, tag, item.depth + 1, options),
|
||||
];
|
||||
res.push(...items);
|
||||
continue;
|
||||
}
|
||||
|
||||
item.items = [
|
||||
...MenuBuilder.addMarkdownItems(tag.description || '', item, item.depth + 1, options),
|
||||
...this.getOperationsItems(parser, item, tag, item.depth + 1, options),
|
||||
];
|
||||
|
||||
res.push(item);
|
||||
}
|
||||
return res;
|
||||
|
|
|
@ -116,7 +116,7 @@ export class MenuStore {
|
|||
}
|
||||
|
||||
if (isScrolledDown) {
|
||||
const el = this.getElementAt(itemIdx + 1);
|
||||
const el = this.getElementAtOrFirstChild(itemIdx + 1);
|
||||
if (this.scroll.isElementBellow(el)) {
|
||||
break;
|
||||
}
|
||||
|
@ -163,6 +163,18 @@ export class MenuStore {
|
|||
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
|
||||
*/
|
||||
|
@ -178,7 +190,7 @@ export class MenuStore {
|
|||
* activate menu item
|
||||
* @param item item to activate
|
||||
* @param updateLocation [true] whether to update location
|
||||
* @param rewriteHistory [false] whether to rewrite browser history (do not create new enrty)
|
||||
* @param rewriteHistory [false] whether to rewrite browser history (do not create new entry)
|
||||
*/
|
||||
@action
|
||||
activate(
|
||||
|
@ -189,6 +201,11 @@ export class MenuStore {
|
|||
if ((this.activeItem && this.activeItem.id) === (item && item.id)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (item && item.type === 'group') {
|
||||
return;
|
||||
}
|
||||
|
||||
this.deactivate(this.activeItem);
|
||||
if (!item) {
|
||||
this.history.replace('', rewriteHistory);
|
||||
|
|
|
@ -4,7 +4,11 @@ import { OpenAPIRef, OpenAPISchema, OpenAPISpec, Referenced } from '../types';
|
|||
|
||||
import { appendToMdHeading, IS_BROWSER } from '../utils/';
|
||||
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 { RedocNormalizedOptions } from './RedocNormalizedOptions';
|
||||
|
||||
|
@ -40,6 +44,7 @@ class RefCounter {
|
|||
export class OpenAPIParser {
|
||||
specUrl?: string;
|
||||
spec: OpenAPISpec;
|
||||
mergeRefs: Set<string>;
|
||||
|
||||
private _refCounter: RefCounter = new RefCounter();
|
||||
|
||||
|
@ -53,6 +58,8 @@ export class OpenAPIParser {
|
|||
|
||||
this.spec = spec;
|
||||
|
||||
this.mergeRefs = new Set();
|
||||
|
||||
const href = IS_BROWSER ? window.location.href : '';
|
||||
if (typeof specUrl === 'string') {
|
||||
this.specUrl = urlResolve(href, specUrl);
|
||||
|
@ -74,7 +81,10 @@ export class OpenAPIParser {
|
|||
) {
|
||||
// Automatically inject Authentication section with SecurityDefinitions component
|
||||
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);
|
||||
spec.info.description = appendToMdHeading(description, 'Authentication', comment);
|
||||
}
|
||||
|
@ -102,7 +112,7 @@ export class OpenAPIParser {
|
|||
};
|
||||
|
||||
/**
|
||||
* checks if the objectt is OpenAPI reference (containts $ref property)
|
||||
* checks if the object is OpenAPI reference (contains $ref property)
|
||||
*/
|
||||
isRef(obj: any): obj is OpenAPIRef {
|
||||
if (!obj) {
|
||||
|
@ -112,7 +122,7 @@ export class OpenAPIParser {
|
|||
}
|
||||
|
||||
/**
|
||||
* resets visited enpoints. should be run after
|
||||
* resets visited endpoints. should be run after
|
||||
*/
|
||||
resetVisited() {
|
||||
if (process.env.NODE_ENV !== 'production') {
|
||||
|
@ -136,9 +146,9 @@ export class OpenAPIParser {
|
|||
/**
|
||||
* Resolve given reference object or return as is if it is not a reference
|
||||
* @param obj object to dereference
|
||||
* @param forceCircular whether to dereference even if it is cirular ref
|
||||
* @param forceCircular whether to dereference even if it is circular ref
|
||||
*/
|
||||
deref<T extends object>(obj: OpenAPIRef | T, forceCircular: boolean = false): T {
|
||||
deref<T extends object>(obj: OpenAPIRef | T, forceCircular = false): T {
|
||||
if (this.isRef(obj)) {
|
||||
const resolved = this.byRef<T>(obj.$ref)!;
|
||||
const visited = this._refCounter.visited(obj.$ref);
|
||||
|
@ -167,16 +177,21 @@ export class OpenAPIParser {
|
|||
}
|
||||
|
||||
/**
|
||||
* Merge allOf contsraints.
|
||||
* Merge allOf constraints.
|
||||
* @param schema schema with allOF
|
||||
* @param $ref pointer of the schema
|
||||
* @param forceCircular whether to dereference children even if it is a cirular ref
|
||||
* @param forceCircular whether to dereference children even if it is a circular ref
|
||||
*/
|
||||
mergeAllOf(
|
||||
schema: OpenAPISchema,
|
||||
$ref?: string,
|
||||
forceCircular: boolean = false,
|
||||
used$Refs = new Set<string>(),
|
||||
): MergedOpenAPISchema {
|
||||
if ($ref) {
|
||||
used$Refs.add($ref);
|
||||
}
|
||||
|
||||
schema = this.hoistOneOfs(schema);
|
||||
|
||||
if (schema.allOf === undefined) {
|
||||
|
@ -198,16 +213,25 @@ export class OpenAPIParser {
|
|||
receiver.items = { ...receiver.items };
|
||||
}
|
||||
|
||||
const allOfSchemas = schema.allOf.map(subSchema => {
|
||||
const allOfSchemas = schema.allOf
|
||||
.map(subSchema => {
|
||||
if (subSchema && subSchema.$ref && used$Refs.has(subSchema.$ref)) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const resolved = this.deref(subSchema, forceCircular);
|
||||
const subRef = subSchema.$ref || undefined;
|
||||
const subMerged = this.mergeAllOf(resolved, subRef, forceCircular);
|
||||
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) {
|
||||
if (
|
||||
|
@ -251,13 +275,13 @@ export class OpenAPIParser {
|
|||
}
|
||||
|
||||
// merge rest of constraints
|
||||
// TODO: do more intelegent merge
|
||||
// TODO: do more intelligent merge
|
||||
receiver = { ...subSchema, ...receiver };
|
||||
|
||||
if (subSchemaRef) {
|
||||
receiver.parentRefs!.push(subSchemaRef);
|
||||
if (receiver.title === undefined && isNamedDefinition(subSchemaRef)) {
|
||||
// this is not so correct behaviour. comented out for now
|
||||
// this is not so correct behaviour. commented out for now
|
||||
// ref: https://github.com/Redocly/redoc/issues/601
|
||||
// receiver.title = JsonPointer.baseName(subSchemaRef);
|
||||
}
|
||||
|
@ -272,8 +296,8 @@ export class OpenAPIParser {
|
|||
* returns map of definition pointer to definition name
|
||||
* @param $refs array of references to find derived from
|
||||
*/
|
||||
findDerived($refs: string[]): Dict<string> {
|
||||
const res: Dict<string> = {};
|
||||
findDerived($refs: string[]): Dict<string[] | string> {
|
||||
const res: Dict<string[]> = {};
|
||||
const schemas = (this.spec.components && this.spec.components.schemas) || {};
|
||||
for (const defName in schemas) {
|
||||
const def = this.deref(schemas[defName]);
|
||||
|
@ -281,7 +305,7 @@ export class OpenAPIParser {
|
|||
def.allOf !== undefined &&
|
||||
def.allOf.find(obj => obj.$ref !== undefined && $refs.indexOf(obj.$ref) > -1)
|
||||
) {
|
||||
res['#/components/schemas/' + defName] = def['x-discriminator-value'] || defName;
|
||||
res['#/components/schemas/' + defName] = [def['x-discriminator-value'] || defName];
|
||||
}
|
||||
}
|
||||
return res;
|
||||
|
|
|
@ -21,22 +21,30 @@ export interface RedocRawOptions {
|
|||
disableSearch?: boolean | string;
|
||||
onlyRequiredInSamples?: boolean | string;
|
||||
showExtensions?: boolean | string | string[];
|
||||
showOtherInfoPanel?: boolean;
|
||||
hideSingleRequestSampleTab?: boolean | string;
|
||||
menuToggle?: boolean | string;
|
||||
jsonSampleExpandLevel?: number | string | 'all';
|
||||
hideSchemaTitles?: boolean | string;
|
||||
payloadSampleIdx?: number;
|
||||
expandSingleSchemaField?: boolean | string;
|
||||
|
||||
unstable_ignoreMimeParameters?: boolean;
|
||||
|
||||
allowedMdComponents?: Dict<MDXComponentMeta>;
|
||||
|
||||
labels?: LabelsConfigRaw;
|
||||
|
||||
enumSkipQuotes?: boolean | string;
|
||||
|
||||
expandDefaultServerVariables?: boolean;
|
||||
}
|
||||
|
||||
function argValueToBoolean(val?: string | boolean): boolean {
|
||||
function argValueToBoolean(val?: string | boolean, defaultValue?: boolean): boolean {
|
||||
if (val === undefined) {
|
||||
return false;
|
||||
return defaultValue || false;
|
||||
}
|
||||
if (typeof val === 'string') {
|
||||
return true;
|
||||
return val === 'false' ? false : true;
|
||||
}
|
||||
return val;
|
||||
}
|
||||
|
@ -111,6 +119,28 @@ export class RedocNormalizedOptions {
|
|||
return value;
|
||||
}
|
||||
|
||||
static normalizePayloadSampleIdx(value: RedocRawOptions['payloadSampleIdx']): number {
|
||||
if (typeof value === 'number') {
|
||||
return Math.max(0, value); // always greater or equal than 0
|
||||
}
|
||||
|
||||
if (typeof value === 'string') {
|
||||
return isFinite(value) ? parseInt(value, 10) : 0;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
private static normalizeJsonSampleExpandLevel(level?: number | string | 'all'): number {
|
||||
if (level === 'all') {
|
||||
return +Infinity;
|
||||
}
|
||||
if (!isNaN(Number(level))) {
|
||||
return Math.ceil(Number(level));
|
||||
}
|
||||
return 2;
|
||||
}
|
||||
|
||||
theme: ResolvedThemeInterface;
|
||||
scrollYOffset: () => number;
|
||||
hideHostname: boolean;
|
||||
|
@ -126,12 +156,19 @@ export class RedocNormalizedOptions {
|
|||
onlyRequiredInSamples: boolean;
|
||||
showExtensions: boolean | string[];
|
||||
hideSingleRequestSampleTab: boolean;
|
||||
showOtherInfoPanel: boolean;
|
||||
menuToggle: boolean;
|
||||
jsonSampleExpandLevel: number;
|
||||
enumSkipQuotes: boolean;
|
||||
hideSchemaTitles: boolean;
|
||||
payloadSampleIdx: number;
|
||||
expandSingleSchemaField: boolean;
|
||||
|
||||
/* tslint:disable-next-line */
|
||||
unstable_ignoreMimeParameters: boolean;
|
||||
allowedMdComponents: Dict<MDXComponentMeta>;
|
||||
|
||||
expandDefaultServerVariables: boolean;
|
||||
|
||||
constructor(raw: RedocRawOptions, defaults: RedocRawOptions = {}) {
|
||||
raw = { ...defaults, ...raw };
|
||||
const hook = raw.theme && raw.theme.extensionsHook;
|
||||
|
@ -157,11 +194,21 @@ export class RedocNormalizedOptions {
|
|||
this.disableSearch = argValueToBoolean(raw.disableSearch);
|
||||
this.onlyRequiredInSamples = argValueToBoolean(raw.onlyRequiredInSamples);
|
||||
this.showExtensions = RedocNormalizedOptions.normalizeShowExtensions(raw.showExtensions);
|
||||
this.showOtherInfoPanel = argValueToBoolean(raw.showOtherInfoPanel);
|
||||
this.hideSingleRequestSampleTab = argValueToBoolean(raw.hideSingleRequestSampleTab);
|
||||
this.menuToggle = argValueToBoolean(raw.menuToggle, true);
|
||||
this.jsonSampleExpandLevel = RedocNormalizedOptions.normalizeJsonSampleExpandLevel(
|
||||
raw.jsonSampleExpandLevel,
|
||||
);
|
||||
this.enumSkipQuotes = argValueToBoolean(raw.enumSkipQuotes);
|
||||
this.hideSchemaTitles = argValueToBoolean(raw.hideSchemaTitles);
|
||||
this.payloadSampleIdx = RedocNormalizedOptions.normalizePayloadSampleIdx(raw.payloadSampleIdx);
|
||||
this.expandSingleSchemaField = argValueToBoolean(raw.expandSingleSchemaField);
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/camelcase
|
||||
this.unstable_ignoreMimeParameters = argValueToBoolean(raw.unstable_ignoreMimeParameters);
|
||||
|
||||
this.allowedMdComponents = raw.allowedMdComponents || {};
|
||||
|
||||
this.expandDefaultServerVariables = argValueToBoolean(raw.expandDefaultServerVariables);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -9,7 +9,7 @@ const EVENT = 'scroll';
|
|||
export class ScrollService {
|
||||
private _scrollParent: Window | HTMLElement | undefined;
|
||||
private _emiter: EventEmitter;
|
||||
private _prevOffsetY: number = 0;
|
||||
private _prevOffsetY = 0;
|
||||
constructor(private options: RedocNormalizedOptions) {
|
||||
this._scrollParent = IS_BROWSER ? window : undefined;
|
||||
this._emiter = new EventEmitter();
|
||||
|
|
|
@ -4,21 +4,23 @@ import { OperationModel } from './models';
|
|||
|
||||
import Worker from './SearchWorker.worker';
|
||||
|
||||
let worker: new () => Worker;
|
||||
|
||||
if (IS_BROWSER) {
|
||||
function getWorker() {
|
||||
let worker: new () => Worker;
|
||||
if (IS_BROWSER) {
|
||||
try {
|
||||
// tslint:disable-next-line
|
||||
worker = require('workerize-loader?inline&fallback=false!./SearchWorker.worker');
|
||||
} catch (e) {
|
||||
worker = require('./SearchWorker.worker').default;
|
||||
}
|
||||
} else {
|
||||
} else {
|
||||
worker = require('./SearchWorker.worker').default;
|
||||
}
|
||||
return new worker();
|
||||
}
|
||||
|
||||
export class SearchStore<T> {
|
||||
searchWorker = new worker();
|
||||
searchWorker = getWorker();
|
||||
|
||||
indexItems(groups: Array<IMenuItem | OperationModel>) {
|
||||
const recurse = items => {
|
||||
|
@ -38,6 +40,10 @@ export class SearchStore<T> {
|
|||
this.searchWorker.add(title, body, meta);
|
||||
}
|
||||
|
||||
dispose() {
|
||||
(this.searchWorker as any).terminate();
|
||||
}
|
||||
|
||||
search(q: string) {
|
||||
return this.searchWorker.search<T>(q);
|
||||
}
|
||||
|
|
|
@ -2,7 +2,7 @@ import * as lunr from 'lunr';
|
|||
|
||||
try {
|
||||
// tslint:disable-next-line
|
||||
require('core-js/es6/promise'); // bundle into worker
|
||||
require('core-js/es/promise'); // bundle into worker
|
||||
} catch (_) {
|
||||
// nope
|
||||
}
|
||||
|
|
|
@ -6,7 +6,7 @@ import { SecuritySchemesModel } from './models/SecuritySchemes';
|
|||
import { OpenAPIParser } from './OpenAPIParser';
|
||||
import { RedocNormalizedOptions } from './RedocNormalizedOptions';
|
||||
/**
|
||||
* Store that containts all the specification related information in the form of tree
|
||||
* Store that contains all the specification related information in the form of tree
|
||||
*/
|
||||
export class SpecStore {
|
||||
parser: OpenAPIParser;
|
||||
|
|
|
@ -8,6 +8,7 @@ describe('Models', () => {
|
|||
let parser;
|
||||
|
||||
test('should hoist oneOfs when mergin allOf', () => {
|
||||
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
||||
const spec = require('./fixtures/oneOfHoist.json');
|
||||
parser = new OpenAPIParser(spec, undefined, opts);
|
||||
expect(parser.mergeAllOf(spec.components.schemas.test)).toMatchSnapshot();
|
||||
|
|
|
@ -10,6 +10,13 @@
|
|||
"in": "path",
|
||||
"name": "test_name",
|
||||
"schema": { "type": "string" }
|
||||
},
|
||||
"serializationParam": {
|
||||
"in": "query",
|
||||
"name": "serialization_test_name",
|
||||
"schema": { "type": "array" },
|
||||
"style": "form",
|
||||
"explode": true
|
||||
}
|
||||
},
|
||||
"headers": {
|
||||
|
|
|
@ -13,7 +13,7 @@ describe('History service', () => {
|
|||
expect(fn).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test('History subscribe should return unsubsribe function', () => {
|
||||
test('History subscribe should return unsubscribe function', () => {
|
||||
const fn = jest.fn();
|
||||
const unsubscribe = history.subscribe(fn);
|
||||
history.emit();
|
||||
|
|
|
@ -6,9 +6,9 @@ const opts = new RedocNormalizedOptions({});
|
|||
|
||||
describe('Models', () => {
|
||||
describe('FieldModel', () => {
|
||||
let parser;
|
||||
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
||||
const spec = require('../fixtures/fields.json');
|
||||
parser = new OpenAPIParser(spec, undefined, opts);
|
||||
const parser = new OpenAPIParser(spec, undefined, opts);
|
||||
|
||||
test('basic field details', () => {
|
||||
const field = new FieldModel(
|
||||
|
@ -26,6 +26,23 @@ describe('Models', () => {
|
|||
expect(field.schema.type).toEqual('string');
|
||||
});
|
||||
|
||||
test('field details relevant for parameter serialization', () => {
|
||||
const field = new FieldModel(
|
||||
parser,
|
||||
{
|
||||
$ref: '#/components/parameters/serializationParam',
|
||||
},
|
||||
'#/components/parameters/serializationParam',
|
||||
opts,
|
||||
);
|
||||
|
||||
expect(field.name).toEqual('serialization_test_name');
|
||||
expect(field.in).toEqual('query');
|
||||
expect(field.schema.type).toEqual('array');
|
||||
expect(field.style).toEqual('form');
|
||||
expect(field.explode).toEqual(true);
|
||||
});
|
||||
|
||||
test('field name should populated from name even if $ref (headers)', () => {
|
||||
const field = new FieldModel(
|
||||
parser,
|
||||
|
|
|
@ -22,7 +22,7 @@ describe('Models', () => {
|
|||
expect(resp.type).toEqual('error');
|
||||
});
|
||||
|
||||
test('default should be sucessful by default', () => {
|
||||
test('default should be successful by default', () => {
|
||||
const resp = new ResponseModel(parser, 'default', false, {}, opts);
|
||||
expect(resp.type).toEqual('success');
|
||||
});
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
/* eslint-disable @typescript-eslint/no-var-requires */
|
||||
import { SchemaModel } from '../../models/Schema';
|
||||
import { OpenAPIParser } from '../../OpenAPIParser';
|
||||
import { RedocNormalizedOptions } from '../../RedocNormalizedOptions';
|
||||
|
|
|
@ -17,6 +17,7 @@ export class ApiInfoModel implements OpenAPIInfo {
|
|||
constructor(private parser: OpenAPIParser) {
|
||||
Object.assign(this, parser.spec.info);
|
||||
this.description = parser.spec.info.description || '';
|
||||
|
||||
const firstHeadingLinePos = this.description.search(/^##?\s+/m);
|
||||
if (firstHeadingLinePos > -1) {
|
||||
this.description = this.description.substring(0, firstHeadingLinePos);
|
||||
|
|
|
@ -1,18 +1,36 @@
|
|||
import { action, observable } from 'mobx';
|
||||
|
||||
import { OpenAPIParameter, Referenced } from '../../types';
|
||||
import {
|
||||
OpenAPIParameter,
|
||||
OpenAPIParameterLocation,
|
||||
OpenAPIParameterStyle,
|
||||
Referenced,
|
||||
} from '../../types';
|
||||
import { RedocNormalizedOptions } from '../RedocNormalizedOptions';
|
||||
|
||||
import { extractExtensions } from '../../utils/openapi';
|
||||
import { OpenAPIParser } from '../OpenAPIParser';
|
||||
import { SchemaModel } from './Schema';
|
||||
|
||||
function getDefaultStyleValue(parameterLocation: OpenAPIParameterLocation): OpenAPIParameterStyle {
|
||||
switch (parameterLocation) {
|
||||
case 'header':
|
||||
return 'simple';
|
||||
case 'query':
|
||||
return 'form';
|
||||
case 'path':
|
||||
return 'simple';
|
||||
default:
|
||||
return 'form';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Field or Parameter model ready to be used by components
|
||||
*/
|
||||
export class FieldModel {
|
||||
@observable
|
||||
expanded: boolean = false;
|
||||
expanded: boolean | undefined;
|
||||
|
||||
schema: SchemaModel;
|
||||
name: string;
|
||||
|
@ -20,9 +38,13 @@ export class FieldModel {
|
|||
description: string;
|
||||
example?: string;
|
||||
deprecated: boolean;
|
||||
in?: string;
|
||||
in?: OpenAPIParameterLocation;
|
||||
kind: string;
|
||||
extensions?: Dict<any>;
|
||||
explode: boolean;
|
||||
style?: OpenAPIParameterStyle;
|
||||
|
||||
serializationMime?: string;
|
||||
|
||||
constructor(
|
||||
parser: OpenAPIParser,
|
||||
|
@ -35,11 +57,29 @@ export class FieldModel {
|
|||
this.name = infoOrRef.name || info.name;
|
||||
this.in = info.in;
|
||||
this.required = !!info.required;
|
||||
this.schema = new SchemaModel(parser, info.schema || {}, pointer, options);
|
||||
|
||||
let fieldSchema = info.schema;
|
||||
let serializationMime = '';
|
||||
if (!fieldSchema && info.in && info.content) {
|
||||
serializationMime = Object.keys(info.content)[0];
|
||||
fieldSchema = info.content[serializationMime] && info.content[serializationMime].schema;
|
||||
}
|
||||
|
||||
this.schema = new SchemaModel(parser, fieldSchema || {}, pointer, options);
|
||||
this.description =
|
||||
info.description === undefined ? this.schema.description || '' : info.description;
|
||||
this.example = info.example || this.schema.example;
|
||||
|
||||
if (serializationMime) {
|
||||
this.serializationMime = serializationMime;
|
||||
} else if (info.style) {
|
||||
this.style = info.style;
|
||||
} else if (this.in) {
|
||||
this.style = getDefaultStyleValue(this.in);
|
||||
}
|
||||
|
||||
this.explode = !!info.explode;
|
||||
|
||||
this.deprecated = info.deprecated === undefined ? !!this.schema.deprecated : info.deprecated;
|
||||
parser.exitRef(infoOrRef);
|
||||
|
||||
|
|
|
@ -2,7 +2,7 @@ import { action, observable } from 'mobx';
|
|||
|
||||
import { OpenAPIExternalDocumentation, OpenAPITag } from '../../types';
|
||||
import { safeSlugify } from '../../utils';
|
||||
import { MarkdownHeading } from '../MarkdownRenderer';
|
||||
import { MarkdownHeading, MarkdownRenderer } from '../MarkdownRenderer';
|
||||
import { ContentItemModel } from '../MenuBuilder';
|
||||
import { IMenuItem, MenuItemGroupType } from '../MenuStore';
|
||||
|
||||
|
@ -40,7 +40,15 @@ export class GroupModel implements IMenuItem {
|
|||
this.type = type;
|
||||
this.name = tagOrGroup['x-displayName'] || tagOrGroup.name;
|
||||
this.level = (tagOrGroup as MarkdownHeading).level || 1;
|
||||
|
||||
// remove sections from markdown, same as in ApiInfo
|
||||
this.description = tagOrGroup.description || '';
|
||||
|
||||
const items = (tagOrGroup as MarkdownHeading).items;
|
||||
if (items && items.length) {
|
||||
this.description = MarkdownRenderer.getTextBeforeHading(this.description, items[0].name);
|
||||
}
|
||||
|
||||
this.parent = parent;
|
||||
this.externalDocs = (tagOrGroup as OpenAPITag).externalDocs;
|
||||
|
||||
|
|
|
@ -21,7 +21,7 @@ export class MediaContentModel {
|
|||
* @param isRequestType needed to know if skipe RO/RW fields in objects
|
||||
*/
|
||||
constructor(
|
||||
public parser: OpenAPIParser,
|
||||
parser: OpenAPIParser,
|
||||
info: Dict<OpenAPIMediaType>,
|
||||
public isRequestType: boolean,
|
||||
options: RedocNormalizedOptions,
|
||||
|
|
|
@ -27,9 +27,23 @@ import { ContentItemModel, ExtendedOpenAPIOperation } from '../MenuBuilder';
|
|||
import { OpenAPIParser } from '../OpenAPIParser';
|
||||
import { RedocNormalizedOptions } from '../RedocNormalizedOptions';
|
||||
import { FieldModel } from './Field';
|
||||
import { MediaContentModel } from './MediaContent';
|
||||
import { RequestBodyModel } from './RequestBody';
|
||||
import { ResponseModel } from './Response';
|
||||
|
||||
interface XPayloadSample {
|
||||
lang: 'payload';
|
||||
label: string;
|
||||
requestBodyContent: MediaContentModel;
|
||||
source: string;
|
||||
}
|
||||
|
||||
export function isPayloadSample(
|
||||
sample: XPayloadSample | OpenAPIXCodeSample,
|
||||
): sample is XPayloadSample {
|
||||
return sample.lang === 'payload' && (sample as any).requestBodyContent;
|
||||
}
|
||||
|
||||
/**
|
||||
* Operation model ready to be used by components
|
||||
*/
|
||||
|
@ -62,7 +76,6 @@ export class OperationModel implements IMenuItem {
|
|||
path: string;
|
||||
servers: OpenAPIServer[];
|
||||
security: SecurityRequirementModel[];
|
||||
codeSamples: OpenAPIXCodeSample[];
|
||||
extensions: Dict<any>;
|
||||
|
||||
constructor(
|
||||
|
@ -89,7 +102,6 @@ export class OperationModel implements IMenuItem {
|
|||
this.httpVerb = operationSpec.httpVerb;
|
||||
this.deprecated = !!operationSpec.deprecated;
|
||||
this.operationId = operationSpec.operationId;
|
||||
this.codeSamples = operationSpec['x-code-samples'] || [];
|
||||
this.path = operationSpec.pathName;
|
||||
|
||||
const pathInfo = parser.byRef<OpenAPIPath>(
|
||||
|
@ -144,6 +156,30 @@ export class OperationModel implements IMenuItem {
|
|||
);
|
||||
}
|
||||
|
||||
@memoize
|
||||
get codeSamples() {
|
||||
let samples: Array<OpenAPIXCodeSample | XPayloadSample> =
|
||||
this.operationSpec['x-code-samples'] || [];
|
||||
|
||||
const requestBodyContent = this.requestBody && this.requestBody.content;
|
||||
if (requestBodyContent && requestBodyContent.hasSample) {
|
||||
const insertInx = Math.min(samples.length, this.options.payloadSampleIdx);
|
||||
|
||||
samples = [
|
||||
...samples.slice(0, insertInx),
|
||||
{
|
||||
lang: 'payload',
|
||||
label: 'Payload',
|
||||
source: '',
|
||||
requestBodyContent,
|
||||
},
|
||||
...samples.slice(insertInx),
|
||||
];
|
||||
}
|
||||
|
||||
return samples;
|
||||
}
|
||||
|
||||
@memoize
|
||||
get parameters() {
|
||||
const _parameters = mergeParams(
|
||||
|
@ -154,11 +190,12 @@ export class OperationModel implements IMenuItem {
|
|||
).map(paramOrRef => new FieldModel(this.parser, paramOrRef, this.pointer, this.options));
|
||||
|
||||
if (this.options.sortPropsAlphabetically) {
|
||||
sortByField(_parameters, 'name');
|
||||
return sortByField(_parameters, 'name');
|
||||
}
|
||||
if (this.options.requiredPropsFirst) {
|
||||
sortByRequired(_parameters);
|
||||
return sortByRequired(_parameters);
|
||||
}
|
||||
|
||||
return _parameters;
|
||||
}
|
||||
|
||||
|
|
|
@ -75,6 +75,7 @@ export class SchemaModel {
|
|||
this.pointer = schemaOrRef.$ref || pointer || '';
|
||||
this.rawSchema = parser.deref(schemaOrRef);
|
||||
this.schema = parser.mergeAllOf(this.rawSchema, this.pointer, isChild);
|
||||
|
||||
this.init(parser, isChild);
|
||||
|
||||
parser.exitRef(schemaOrRef);
|
||||
|
@ -125,6 +126,13 @@ export class SchemaModel {
|
|||
if (!isChild && getDiscriminator(schema) !== undefined) {
|
||||
this.initDiscriminator(schema, parser);
|
||||
return;
|
||||
} else if (
|
||||
isChild &&
|
||||
Array.isArray(schema.oneOf) &&
|
||||
schema.oneOf.find(s => s.$ref === this.pointer)
|
||||
) {
|
||||
// we hit allOf of the schema with the parent discriminator
|
||||
delete schema.oneOf;
|
||||
}
|
||||
|
||||
if (schema.oneOf !== undefined) {
|
||||
|
@ -216,7 +224,10 @@ export class SchemaModel {
|
|||
) {
|
||||
const discriminator = getDiscriminator(schema)!;
|
||||
this.discriminatorProp = discriminator.propertyName;
|
||||
const derived = parser.findDerived([...(schema.parentRefs || []), this.pointer]);
|
||||
const implicitInversedMapping = parser.findDerived([
|
||||
...(schema.parentRefs || []),
|
||||
this.pointer,
|
||||
]);
|
||||
|
||||
if (schema.oneOf) {
|
||||
for (const variant of schema.oneOf) {
|
||||
|
@ -224,19 +235,41 @@ export class SchemaModel {
|
|||
continue;
|
||||
}
|
||||
const name = JsonPointer.baseName(variant.$ref);
|
||||
derived[variant.$ref] = name;
|
||||
implicitInversedMapping[variant.$ref] = name;
|
||||
}
|
||||
}
|
||||
|
||||
const mapping = discriminator.mapping || {};
|
||||
const explicitInversedMapping = {};
|
||||
for (const name in mapping) {
|
||||
derived[mapping[name]] = name;
|
||||
const $ref = mapping[name];
|
||||
|
||||
if (Array.isArray(explicitInversedMapping[$ref])) {
|
||||
explicitInversedMapping[$ref].push(name);
|
||||
} else {
|
||||
// overrides implicit mapping here
|
||||
explicitInversedMapping[$ref] = [name];
|
||||
}
|
||||
}
|
||||
|
||||
const refs = Object.keys(derived);
|
||||
this.oneOf = refs.map(ref => {
|
||||
const innerSchema = new SchemaModel(parser, parser.byRef(ref)!, ref, this.options, true);
|
||||
innerSchema.title = derived[ref];
|
||||
const inversedMapping = { ...implicitInversedMapping, ...explicitInversedMapping };
|
||||
|
||||
const refs: Array<{ $ref; name }> = [];
|
||||
|
||||
for (const $ref of Object.keys(inversedMapping)) {
|
||||
const names = inversedMapping[$ref];
|
||||
if (Array.isArray(names)) {
|
||||
for (const name of names) {
|
||||
refs.push({ $ref, name });
|
||||
}
|
||||
} else {
|
||||
refs.push({ $ref, name: names });
|
||||
}
|
||||
}
|
||||
|
||||
this.oneOf = refs.map(({ $ref, name }) => {
|
||||
const innerSchema = new SchemaModel(parser, parser.byRef($ref)!, $ref, this.options, true);
|
||||
innerSchema.title = name;
|
||||
return innerSchema;
|
||||
});
|
||||
}
|
||||
|
@ -251,7 +284,7 @@ function buildFields(
|
|||
const props = schema.properties || {};
|
||||
const additionalProps = schema.additionalProperties;
|
||||
const defaults = schema.default || {};
|
||||
const fields = Object.keys(props || []).map(fieldName => {
|
||||
let fields = Object.keys(props || []).map(fieldName => {
|
||||
let field = props[fieldName];
|
||||
|
||||
if (!field) {
|
||||
|
@ -280,11 +313,11 @@ function buildFields(
|
|||
});
|
||||
|
||||
if (options.sortPropsAlphabetically) {
|
||||
sortByField(fields, 'name');
|
||||
fields = sortByField(fields, 'name');
|
||||
}
|
||||
if (options.requiredPropsFirst) {
|
||||
// if not sort alphabetically sort in the order from required keyword
|
||||
sortByRequired(fields, !options.sortPropsAlphabetically ? schema.required : undefined);
|
||||
fields = sortByRequired(fields, !options.sortPropsAlphabetically ? schema.required : undefined);
|
||||
}
|
||||
|
||||
if (typeof additionalProps === 'object' || additionalProps === true) {
|
||||
|
|
61
src/theme.ts
61
src/theme.ts
|
@ -3,8 +3,8 @@ import { darken, desaturate, lighten, readableColor, transparentize } from 'poli
|
|||
const defaultTheme: ThemeInterface = {
|
||||
spacing: {
|
||||
unit: 5,
|
||||
sectionHorizontal: ({spacing}) => spacing.unit * 8,
|
||||
sectionVertical: ({spacing}) => spacing.unit * 8,
|
||||
sectionHorizontal: ({ spacing }) => spacing.unit * 8,
|
||||
sectionVertical: ({ spacing }) => spacing.unit * 8,
|
||||
},
|
||||
breakpoints: {
|
||||
small: '50rem',
|
||||
|
@ -15,31 +15,31 @@ const defaultTheme: ThemeInterface = {
|
|||
tonalOffset: 0.3,
|
||||
primary: {
|
||||
main: '#232E72',
|
||||
light: ({colors}) => lighten(colors.tonalOffset, colors.primary.main),
|
||||
dark: ({colors}) => darken(colors.tonalOffset, colors.primary.main),
|
||||
contrastText: ({colors}) => readableColor(colors.primary.main),
|
||||
light: ({ colors }) => lighten(colors.tonalOffset, colors.primary.main),
|
||||
dark: ({ colors }) => darken(colors.tonalOffset, colors.primary.main),
|
||||
contrastText: ({ colors }) => readableColor(colors.primary.main),
|
||||
},
|
||||
success: {
|
||||
main: '#00aa13',
|
||||
light: ({colors}) => lighten(colors.tonalOffset, colors.success.main),
|
||||
dark: ({colors}) => darken(colors.tonalOffset, colors.success.main),
|
||||
contrastText: ({colors}) => readableColor(colors.success.main),
|
||||
light: ({ colors }) => lighten(colors.tonalOffset, colors.success.main),
|
||||
dark: ({ colors }) => darken(colors.tonalOffset, colors.success.main),
|
||||
contrastText: ({ colors }) => readableColor(colors.success.main),
|
||||
},
|
||||
warning: {
|
||||
main: '#d4ad03',
|
||||
light: ({colors}) => lighten(colors.tonalOffset, colors.warning.main),
|
||||
dark: ({colors}) => darken(colors.tonalOffset, colors.warning.main),
|
||||
light: ({ colors }) => lighten(colors.tonalOffset, colors.warning.main),
|
||||
dark: ({ colors }) => darken(colors.tonalOffset, colors.warning.main),
|
||||
contrastText: '#ffffff',
|
||||
},
|
||||
error: {
|
||||
main: '#e53935',
|
||||
light: ({colors}) => lighten(colors.tonalOffset, colors.error.main),
|
||||
dark: ({colors}) => darken(colors.tonalOffset, colors.error.main),
|
||||
contrastText: ({colors}) => readableColor(colors.error.main),
|
||||
light: ({ colors }) => lighten(colors.tonalOffset, colors.error.main),
|
||||
dark: ({ colors }) => darken(colors.tonalOffset, colors.error.main),
|
||||
contrastText: ({ colors }) => readableColor(colors.error.main),
|
||||
},
|
||||
text: {
|
||||
primary: '#333333',
|
||||
secondary: ({colors}) => lighten(colors.tonalOffset, colors.text.primary),
|
||||
secondary: ({ colors }) => lighten(colors.tonalOffset, colors.text.primary),
|
||||
},
|
||||
border: {
|
||||
dark: 'rgba(0,0,0, 0.1)',
|
||||
|
@ -47,20 +47,20 @@ const defaultTheme: ThemeInterface = {
|
|||
},
|
||||
responses: {
|
||||
success: {
|
||||
color: ({colors}) => colors.success.main,
|
||||
backgroundColor: ({colors}) => transparentize(0.9, colors.success.main),
|
||||
color: ({ colors }) => colors.success.main,
|
||||
backgroundColor: ({ colors }) => transparentize(0.9, colors.success.main),
|
||||
},
|
||||
error: {
|
||||
color: ({colors}) => colors.error.main,
|
||||
backgroundColor: ({colors}) => transparentize(0.9, colors.error.main),
|
||||
color: ({ colors }) => colors.error.main,
|
||||
backgroundColor: ({ colors }) => transparentize(0.9, colors.error.main),
|
||||
},
|
||||
redirect: {
|
||||
color: '#ffa500',
|
||||
backgroundColor: ({colors}) => transparentize(0.9, colors.responses.redirect.color),
|
||||
backgroundColor: ({ colors }) => transparentize(0.9, colors.responses.redirect.color),
|
||||
},
|
||||
info: {
|
||||
color: '#87ceeb',
|
||||
backgroundColor: ({colors}) => transparentize(0.9, colors.responses.info.color),
|
||||
backgroundColor: ({ colors }) => transparentize(0.9, colors.responses.info.color),
|
||||
},
|
||||
},
|
||||
http: {
|
||||
|
@ -110,22 +110,26 @@ const defaultTheme: ThemeInterface = {
|
|||
code: {
|
||||
fontSize: '13px',
|
||||
fontFamily: 'Courier, monospace',
|
||||
lineHeight: ({typography}) => typography.lineHeight,
|
||||
fontWeight: ({typography}) => typography.fontWeightRegular,
|
||||
lineHeight: ({ typography }) => typography.lineHeight,
|
||||
fontWeight: ({ typography }) => typography.fontWeightRegular,
|
||||
color: '#e53935',
|
||||
backgroundColor: 'rgba(38, 50, 56, 0.05)',
|
||||
wrap: false,
|
||||
},
|
||||
links: {
|
||||
color: ({colors}) => colors.primary.main,
|
||||
visited: ({typography}) => typography.links.color,
|
||||
hover: ({typography}) => lighten(0.2, typography.links.color),
|
||||
color: ({ colors }) => colors.primary.main,
|
||||
visited: ({ typography }) => typography.links.color,
|
||||
hover: ({ typography }) => lighten(0.2, typography.links.color),
|
||||
},
|
||||
},
|
||||
menu: {
|
||||
width: '260px',
|
||||
backgroundColor: '#F3F6FB',
|
||||
textColor: '#232E72',
|
||||
activeTextColor: theme =>
|
||||
theme.menu.textColor !== defaultTheme.menu!.textColor
|
||||
? theme.menu.textColor
|
||||
: theme.colors.primary.main,
|
||||
groupItems: {
|
||||
textTransform: 'uppercase',
|
||||
},
|
||||
|
@ -138,8 +142,8 @@ const defaultTheme: ThemeInterface = {
|
|||
},
|
||||
},
|
||||
logo: {
|
||||
maxHeight: ({menu}) => menu.width,
|
||||
maxWidth: ({menu}) => menu.width,
|
||||
maxHeight: ({ menu }) => menu.width,
|
||||
maxWidth: ({ menu }) => menu.width,
|
||||
gutter: '2px',
|
||||
},
|
||||
rightPanel: {
|
||||
|
@ -148,7 +152,7 @@ const defaultTheme: ThemeInterface = {
|
|||
textColor: '#ffffff',
|
||||
},
|
||||
codeSample: {
|
||||
backgroundColor: ({rightPanel}) => darken(0.1, rightPanel.backgroundColor),
|
||||
backgroundColor: ({ rightPanel }) => darken(0.1, rightPanel.backgroundColor),
|
||||
},
|
||||
};
|
||||
|
||||
|
@ -308,6 +312,7 @@ export interface ResolvedThemeInterface {
|
|||
width: string;
|
||||
backgroundColor: string;
|
||||
textColor: string;
|
||||
activeTextColor: string;
|
||||
groupItems: {
|
||||
textTransform: string;
|
||||
};
|
||||
|
|
5
src/types/components.d.ts
vendored
5
src/types/components.d.ts
vendored
|
@ -1,5 +0,0 @@
|
|||
import { AppStore } from '../services/AppStore';
|
||||
|
||||
export interface BaseContainerProps {
|
||||
store: AppStore;
|
||||
}
|
|
@ -8,7 +8,11 @@ describe('Utils', () => {
|
|||
const fn = (...args) => args;
|
||||
|
||||
const actual = mapWithLast(arr, fn);
|
||||
const expected = [[1, false], [2, false], [3, true]];
|
||||
const expected = [
|
||||
[1, false],
|
||||
[2, false],
|
||||
[3, true],
|
||||
];
|
||||
expect(actual).toEqual(expected);
|
||||
});
|
||||
|
||||
|
@ -60,7 +64,7 @@ describe('Utils', () => {
|
|||
test('should behave like Object.assign on the top level', () => {
|
||||
const obj1 = { a: { a1: 'A1' }, c: 'C' };
|
||||
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', () => {
|
||||
const obj1 = { a: ['A', 'B'] };
|
||||
|
|
|
@ -8,10 +8,13 @@ import {
|
|||
mergeParams,
|
||||
normalizeServers,
|
||||
pluralizeType,
|
||||
serializeParameterValue,
|
||||
sortByRequired,
|
||||
} from '../';
|
||||
|
||||
import { OpenAPIParser } from '../../services';
|
||||
import { OpenAPIParameter } from '../../types';
|
||||
import { FieldModel, OpenAPIParser, RedocNormalizedOptions } from '../../services';
|
||||
import { OpenAPIParameter, OpenAPIParameterLocation, OpenAPIParameterStyle } from '../../types';
|
||||
import { expandDefaultServerVariables } from '../openapi';
|
||||
|
||||
describe('Utils', () => {
|
||||
describe('openapi getStatusCode', () => {
|
||||
|
@ -247,7 +250,7 @@ describe('Utils', () => {
|
|||
expect(res).toEqual([{ url: 'http://base.com/sandbox/test', description: '' }]);
|
||||
});
|
||||
|
||||
it('should correcly resolve url with server relative path', () => {
|
||||
it('should correctly resolve url with server relative path', () => {
|
||||
const res = normalizeServers('http://base.com/subpath/spec.yaml', [
|
||||
{
|
||||
url: '/sandbox/test',
|
||||
|
@ -256,7 +259,7 @@ describe('Utils', () => {
|
|||
expect(res).toEqual([{ url: 'http://base.com/sandbox/test', description: '' }]);
|
||||
});
|
||||
|
||||
it('should correcly resolve url with relative path', () => {
|
||||
it('should correctly resolve url with relative path', () => {
|
||||
const res = normalizeServers('http://base.com/subpath/spec.yaml', [
|
||||
{
|
||||
url: 'sandbox/test',
|
||||
|
@ -296,11 +299,8 @@ describe('Utils', () => {
|
|||
it('should expand variables', () => {
|
||||
const servers = normalizeServers('', [
|
||||
{
|
||||
url: '{protocol}{host}{basePath}',
|
||||
url: 'http://{host}{basePath}',
|
||||
variables: {
|
||||
protocol: {
|
||||
default: 'http://',
|
||||
},
|
||||
host: {
|
||||
default: '127.0.0.1',
|
||||
},
|
||||
|
@ -318,9 +318,15 @@ describe('Utils', () => {
|
|||
},
|
||||
]);
|
||||
|
||||
expect(servers[0].url).toEqual('http://127.0.0.1/path/to/endpoint');
|
||||
expect(servers[1].url).toEqual('http://127.0.0.2:{port}');
|
||||
expect(servers[2].url).toEqual('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',
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -328,7 +334,8 @@ describe('Utils', () => {
|
|||
const itemConstraintSchema = (
|
||||
min: number | undefined = undefined,
|
||||
max: number | undefined = undefined,
|
||||
) => ({ type: 'array', minItems: min, maxItems: max });
|
||||
multipleOf: number | undefined = undefined,
|
||||
) => ({ type: 'array', minItems: min, maxItems: max, multipleOf });
|
||||
|
||||
it('should not have a humanized constraint without schema constraints', () => {
|
||||
expect(humanizeConstraints(itemConstraintSchema())).toHaveLength(0);
|
||||
|
@ -350,9 +357,21 @@ describe('Utils', () => {
|
|||
expect(humanizeConstraints(itemConstraintSchema(7, 7))).toContain('7 items');
|
||||
});
|
||||
|
||||
it('should have a humazined constraint when justMinItems is set, and it is equal to 1', () => {
|
||||
it('should have a humanized constraint when justMinItems is set, and it is equal to 1', () => {
|
||||
expect(humanizeConstraints(itemConstraintSchema(1))).toContain('non-empty');
|
||||
});
|
||||
|
||||
it('should have a humanized constraint when multipleOf is set, and it is in format of /^0\\.0*1$/', () => {
|
||||
expect(humanizeConstraints(itemConstraintSchema(undefined, undefined, 0.01))).toContain(
|
||||
'decimal places <= 2',
|
||||
);
|
||||
});
|
||||
|
||||
it('should have a humanized constraint when multipleOf is set, and it is in format other than /^0\\.0*1$/', () => {
|
||||
expect(humanizeConstraints(itemConstraintSchema(undefined, undefined, 0.5))).toContain(
|
||||
'multiple of 0.5',
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('OpenAPI pluralizeType', () => {
|
||||
|
@ -365,16 +384,623 @@ describe('Utils', () => {
|
|||
expect(pluralizeType('array')).toEqual('arrays');
|
||||
});
|
||||
|
||||
it('should pluralize complex dislay types', () => {
|
||||
it('should pluralize complex display types', () => {
|
||||
expect(pluralizeType('object (Pet)')).toEqual('objects (Pet)');
|
||||
expect(pluralizeType('string <email>')).toEqual('strings <email>');
|
||||
});
|
||||
|
||||
it('should pluralize oneOf-ed dislay types', () => {
|
||||
it('should pluralize oneOf-ed display types', () => {
|
||||
expect(pluralizeType('object or string')).toEqual('objects or strings');
|
||||
expect(pluralizeType('object (Pet) or number <int64>')).toEqual(
|
||||
'objects (Pet) or numbers <int64>',
|
||||
);
|
||||
});
|
||||
|
||||
it('should not pluralize display types that are already pluralized', () => {
|
||||
expect(pluralizeType('strings')).toEqual('strings');
|
||||
expect(pluralizeType('objects (Pet)')).toEqual('objects (Pet)');
|
||||
expect(pluralizeType('strings <email>')).toEqual('strings <email>');
|
||||
expect(pluralizeType('objects or strings')).toEqual('objects or strings');
|
||||
expect(pluralizeType('objects (Pet) or numbers <int64>')).toEqual(
|
||||
'objects (Pet) or numbers <int64>',
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('openapi serializeParameter', () => {
|
||||
interface TestCase {
|
||||
style: OpenAPIParameterStyle;
|
||||
explode: boolean;
|
||||
expected: string;
|
||||
}
|
||||
|
||||
interface TestValueTypeGroup {
|
||||
value: any;
|
||||
description: string;
|
||||
cases: TestCase[];
|
||||
}
|
||||
interface TestLocationGroup {
|
||||
location: OpenAPIParameterLocation;
|
||||
name: string;
|
||||
description: string;
|
||||
cases: TestValueTypeGroup[];
|
||||
}
|
||||
const testCases: TestLocationGroup[] = [
|
||||
{
|
||||
location: 'path',
|
||||
name: 'id',
|
||||
description: 'path parameters',
|
||||
cases: [
|
||||
{
|
||||
value: 5,
|
||||
description: 'primitive values',
|
||||
cases: [
|
||||
{ style: 'simple', explode: false, expected: '5' },
|
||||
{ style: 'simple', explode: true, expected: '5' },
|
||||
{ style: 'label', explode: false, expected: '.5' },
|
||||
{ style: 'label', explode: true, expected: '.5' },
|
||||
{ style: 'matrix', explode: false, expected: ';id=5' },
|
||||
{ style: 'matrix', explode: true, expected: ';id=5' },
|
||||
],
|
||||
},
|
||||
{
|
||||
value: [3, 4, 5],
|
||||
description: 'array values',
|
||||
cases: [
|
||||
{ style: 'simple', explode: false, expected: '3,4,5' },
|
||||
{ style: 'simple', explode: true, expected: '3,4,5' },
|
||||
{ style: 'label', explode: false, expected: '.3,4,5' },
|
||||
{ style: 'label', explode: true, expected: '.3.4.5' },
|
||||
{ style: 'matrix', explode: false, expected: ';id=3,4,5' },
|
||||
{ style: 'matrix', explode: true, expected: ';id=3;id=4;id=5' },
|
||||
],
|
||||
},
|
||||
{
|
||||
value: { role: 'admin', firstName: 'Alex' },
|
||||
description: 'object values',
|
||||
cases: [
|
||||
{ style: 'simple', explode: false, 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: true, expected: '.role=admin.firstName=Alex' },
|
||||
{ style: 'matrix', explode: false, expected: ';id=role,admin,firstName,Alex' },
|
||||
{ style: 'matrix', explode: true, expected: ';role=admin;firstName=Alex' },
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
location: 'query',
|
||||
name: 'id',
|
||||
description: 'query parameters',
|
||||
cases: [
|
||||
{
|
||||
value: 5,
|
||||
description: 'primitive values',
|
||||
cases: [
|
||||
{ style: 'form', explode: true, expected: 'id=5' },
|
||||
{ style: 'form', explode: false, expected: 'id=5' },
|
||||
],
|
||||
},
|
||||
{
|
||||
value: [3, 4, 5],
|
||||
description: 'array values',
|
||||
cases: [
|
||||
{ style: 'form', explode: true, expected: 'id=3&id=4&id=5' },
|
||||
{ style: 'form', explode: false, expected: 'id=3,4,5' },
|
||||
{ style: 'spaceDelimited', explode: true, expected: 'id=3&id=4&id=5' },
|
||||
{ style: 'spaceDelimited', explode: false, expected: 'id=3%204%205' },
|
||||
{ style: 'pipeDelimited', explode: true, expected: 'id=3&id=4&id=5' },
|
||||
{ style: 'pipeDelimited', explode: false, expected: 'id=3|4|5' },
|
||||
],
|
||||
},
|
||||
{
|
||||
value: { role: 'admin', firstName: 'Alex' },
|
||||
description: 'object values',
|
||||
cases: [
|
||||
{ style: 'form', explode: true, expected: 'role=admin&firstName=Alex' },
|
||||
{ style: 'form', explode: false, expected: 'id=role,admin,firstName,Alex' },
|
||||
{ style: 'deepObject', explode: true, expected: 'id[role]=admin&id[firstName]=Alex' },
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
location: 'cookie',
|
||||
name: 'id',
|
||||
description: 'cookie parameters',
|
||||
cases: [
|
||||
{
|
||||
value: 5,
|
||||
description: 'primitive values',
|
||||
cases: [
|
||||
{ style: 'form', explode: true, expected: 'id=5' },
|
||||
{ style: 'form', explode: false, expected: 'id=5' },
|
||||
],
|
||||
},
|
||||
{
|
||||
value: [3, 4, 5],
|
||||
description: 'array values',
|
||||
cases: [
|
||||
{ style: 'form', explode: true, expected: 'id=3&id=4&id=5' },
|
||||
{ style: 'form', explode: false, expected: 'id=3,4,5' },
|
||||
],
|
||||
},
|
||||
{
|
||||
value: { role: 'admin', firstName: 'Alex' },
|
||||
description: 'object values',
|
||||
cases: [
|
||||
{ style: 'form', explode: true, expected: 'role=admin&firstName=Alex' },
|
||||
{ style: 'form', explode: false, expected: 'id=role,admin,firstName,Alex' },
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
location: 'header',
|
||||
name: 'x-id',
|
||||
description: 'header parameters',
|
||||
cases: [
|
||||
{
|
||||
value: 5,
|
||||
description: 'primitive values',
|
||||
cases: [
|
||||
{ style: 'simple', explode: false, expected: '5' },
|
||||
{ style: 'simple', explode: true, expected: '5' },
|
||||
],
|
||||
},
|
||||
{
|
||||
value: [3, 4, 5],
|
||||
description: 'array values',
|
||||
cases: [
|
||||
{ style: 'simple', explode: false, expected: '3,4,5' },
|
||||
{ style: 'simple', explode: true, expected: '3,4,5' },
|
||||
],
|
||||
},
|
||||
{
|
||||
value: { role: 'admin', firstName: 'Alex' },
|
||||
description: 'object values',
|
||||
cases: [
|
||||
{ style: 'simple', explode: false, expected: 'role,admin,firstName,Alex' },
|
||||
{ style: 'simple', explode: true, expected: 'role=admin,firstName=Alex' },
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
testCases.forEach(locationTestGroup => {
|
||||
describe(locationTestGroup.description, () => {
|
||||
locationTestGroup.cases.forEach(valueTypeTestGroup => {
|
||||
describe(valueTypeTestGroup.description, () => {
|
||||
valueTypeTestGroup.cases.forEach(testCase => {
|
||||
it(`should serialize correctly when style is ${testCase.style} and explode is ${testCase.explode}`, () => {
|
||||
const parameter: OpenAPIParameter = {
|
||||
name: locationTestGroup.name,
|
||||
in: locationTestGroup.location,
|
||||
style: testCase.style,
|
||||
explode: testCase.explode,
|
||||
};
|
||||
const serialized = serializeParameterValue(parameter, valueTypeTestGroup.value);
|
||||
|
||||
expect(serialized).toEqual(testCase.expected);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('advanced serialization', () => {
|
||||
it('should serialize correctly query parameter with content with application/json', () => {
|
||||
const parameter: OpenAPIParameter = {
|
||||
name: 'id',
|
||||
in: 'query',
|
||||
content: {
|
||||
'application/json': {
|
||||
schema: {
|
||||
type: 'string',
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const parser = new OpenAPIParser({ openapi: '3.0' } as any);
|
||||
const opts = new RedocNormalizedOptions({});
|
||||
|
||||
const field = new FieldModel(parser, parameter, '', opts);
|
||||
expect(serializeParameterValue(field, { name: 'test', age: 23 })).toEqual(
|
||||
'id={"name":"test","age":23}',
|
||||
);
|
||||
});
|
||||
|
||||
it('should serialize correctly header parameter with content with application/json', () => {
|
||||
const parameter: OpenAPIParameter = {
|
||||
name: 'x-header',
|
||||
in: 'header',
|
||||
content: {
|
||||
'application/json': {
|
||||
schema: {
|
||||
type: 'string',
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const parser = new OpenAPIParser({ openapi: '3.0' } as any);
|
||||
const opts = new RedocNormalizedOptions({});
|
||||
|
||||
const field = new FieldModel(parser, parameter, '', opts);
|
||||
expect(serializeParameterValue(field, { name: 'test', age: 23 })).toEqual(
|
||||
'{"name":"test","age":23}',
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('OpenAPI sortByRequired', () => {
|
||||
it('should equal to the old data when all items have no required props', () => {
|
||||
const fields = [
|
||||
{
|
||||
name: 'loginName',
|
||||
required: false,
|
||||
},
|
||||
{
|
||||
name: 'displayName',
|
||||
required: false,
|
||||
},
|
||||
{
|
||||
name: 'email',
|
||||
required: false,
|
||||
},
|
||||
{
|
||||
name: 'space',
|
||||
required: false,
|
||||
},
|
||||
{
|
||||
name: 'type',
|
||||
required: false,
|
||||
},
|
||||
{
|
||||
name: 'depIds',
|
||||
required: false,
|
||||
},
|
||||
{
|
||||
name: 'depNames',
|
||||
required: false,
|
||||
},
|
||||
{
|
||||
name: 'password',
|
||||
required: false,
|
||||
},
|
||||
{
|
||||
name: 'pwdControl',
|
||||
required: false,
|
||||
},
|
||||
{
|
||||
name: 'csfLevel',
|
||||
required: false,
|
||||
},
|
||||
{
|
||||
name: 'priority',
|
||||
required: false,
|
||||
},
|
||||
{
|
||||
name: 'siteId',
|
||||
required: false,
|
||||
},
|
||||
];
|
||||
expect(sortByRequired(fields as FieldModel[])).toEqual(fields);
|
||||
});
|
||||
|
||||
it('other item should be the same order when some of items are required', () => {
|
||||
const fields = [
|
||||
{
|
||||
name: 'loginName',
|
||||
required: true,
|
||||
},
|
||||
{
|
||||
name: 'displayName',
|
||||
required: false,
|
||||
},
|
||||
{
|
||||
name: 'email',
|
||||
required: true,
|
||||
},
|
||||
{
|
||||
name: 'space',
|
||||
required: false,
|
||||
},
|
||||
{
|
||||
name: 'type',
|
||||
required: false,
|
||||
},
|
||||
{
|
||||
name: 'depIds',
|
||||
required: false,
|
||||
},
|
||||
{
|
||||
name: 'depNames',
|
||||
required: false,
|
||||
},
|
||||
{
|
||||
name: 'password',
|
||||
required: false,
|
||||
},
|
||||
{
|
||||
name: 'pwdControl',
|
||||
required: false,
|
||||
},
|
||||
{
|
||||
name: 'csfLevel',
|
||||
required: false,
|
||||
},
|
||||
{
|
||||
name: 'priority',
|
||||
required: false,
|
||||
},
|
||||
{
|
||||
name: 'siteId',
|
||||
required: false,
|
||||
},
|
||||
];
|
||||
const sortedFields = [
|
||||
{
|
||||
name: 'loginName',
|
||||
required: true,
|
||||
},
|
||||
{
|
||||
name: 'email',
|
||||
required: true,
|
||||
},
|
||||
{
|
||||
name: 'displayName',
|
||||
required: false,
|
||||
},
|
||||
{
|
||||
name: 'space',
|
||||
required: false,
|
||||
},
|
||||
{
|
||||
name: 'type',
|
||||
required: false,
|
||||
},
|
||||
{
|
||||
name: 'depIds',
|
||||
required: false,
|
||||
},
|
||||
{
|
||||
name: 'depNames',
|
||||
required: false,
|
||||
},
|
||||
{
|
||||
name: 'password',
|
||||
required: false,
|
||||
},
|
||||
{
|
||||
name: 'pwdControl',
|
||||
required: false,
|
||||
},
|
||||
{
|
||||
name: 'csfLevel',
|
||||
required: false,
|
||||
},
|
||||
{
|
||||
name: 'priority',
|
||||
required: false,
|
||||
},
|
||||
{
|
||||
name: 'siteId',
|
||||
required: false,
|
||||
},
|
||||
];
|
||||
expect(sortByRequired(fields as FieldModel[])).toEqual(sortedFields);
|
||||
});
|
||||
|
||||
it('should the order of required items is as same as the order parameter ', () => {
|
||||
const fields = [
|
||||
{
|
||||
name: 'loginName',
|
||||
required: true,
|
||||
},
|
||||
{
|
||||
name: 'displayName',
|
||||
required: true,
|
||||
},
|
||||
{
|
||||
name: 'email',
|
||||
required: true,
|
||||
},
|
||||
{
|
||||
name: 'space',
|
||||
required: false,
|
||||
},
|
||||
{
|
||||
name: 'type',
|
||||
required: false,
|
||||
},
|
||||
{
|
||||
name: 'depIds',
|
||||
required: false,
|
||||
},
|
||||
{
|
||||
name: 'depNames',
|
||||
required: false,
|
||||
},
|
||||
{
|
||||
name: 'password',
|
||||
required: false,
|
||||
},
|
||||
{
|
||||
name: 'pwdControl',
|
||||
required: false,
|
||||
},
|
||||
{
|
||||
name: 'csfLevel',
|
||||
required: false,
|
||||
},
|
||||
{
|
||||
name: 'priority',
|
||||
required: false,
|
||||
},
|
||||
{
|
||||
name: 'siteId',
|
||||
required: false,
|
||||
},
|
||||
];
|
||||
expect(
|
||||
sortByRequired(fields as FieldModel[], ['siteId', 'displayName', 'loginName', 'email']),
|
||||
).toEqual([
|
||||
{
|
||||
name: 'displayName',
|
||||
required: true,
|
||||
},
|
||||
{
|
||||
name: 'loginName',
|
||||
required: true,
|
||||
},
|
||||
{
|
||||
name: 'email',
|
||||
required: true,
|
||||
},
|
||||
{
|
||||
name: 'space',
|
||||
required: false,
|
||||
},
|
||||
{
|
||||
name: 'type',
|
||||
required: false,
|
||||
},
|
||||
{
|
||||
name: 'depIds',
|
||||
required: false,
|
||||
},
|
||||
{
|
||||
name: 'depNames',
|
||||
required: false,
|
||||
},
|
||||
{
|
||||
name: 'password',
|
||||
required: false,
|
||||
},
|
||||
{
|
||||
name: 'pwdControl',
|
||||
required: false,
|
||||
},
|
||||
{
|
||||
name: 'csfLevel',
|
||||
required: false,
|
||||
},
|
||||
{
|
||||
name: 'priority',
|
||||
required: false,
|
||||
},
|
||||
{
|
||||
name: 'siteId',
|
||||
required: false,
|
||||
},
|
||||
]);
|
||||
expect(sortByRequired(fields as FieldModel[], ['email', 'displayName'])).toEqual([
|
||||
{
|
||||
name: 'email',
|
||||
required: true,
|
||||
},
|
||||
{
|
||||
name: 'displayName',
|
||||
required: true,
|
||||
},
|
||||
{
|
||||
name: 'loginName',
|
||||
required: true,
|
||||
},
|
||||
{
|
||||
name: 'space',
|
||||
required: false,
|
||||
},
|
||||
{
|
||||
name: 'type',
|
||||
required: false,
|
||||
},
|
||||
{
|
||||
name: 'depIds',
|
||||
required: false,
|
||||
},
|
||||
{
|
||||
name: 'depNames',
|
||||
required: false,
|
||||
},
|
||||
{
|
||||
name: 'password',
|
||||
required: false,
|
||||
},
|
||||
{
|
||||
name: 'pwdControl',
|
||||
required: false,
|
||||
},
|
||||
{
|
||||
name: 'csfLevel',
|
||||
required: false,
|
||||
},
|
||||
{
|
||||
name: 'priority',
|
||||
required: false,
|
||||
},
|
||||
{
|
||||
name: 'siteId',
|
||||
required: false,
|
||||
},
|
||||
]);
|
||||
|
||||
expect(sortByRequired(fields as FieldModel[], ['displayName'])).toEqual([
|
||||
{
|
||||
name: 'displayName',
|
||||
required: true,
|
||||
},
|
||||
{
|
||||
name: 'loginName',
|
||||
required: true,
|
||||
},
|
||||
{
|
||||
name: 'email',
|
||||
required: true,
|
||||
},
|
||||
{
|
||||
name: 'space',
|
||||
required: false,
|
||||
},
|
||||
{
|
||||
name: 'type',
|
||||
required: false,
|
||||
},
|
||||
{
|
||||
name: 'depIds',
|
||||
required: false,
|
||||
},
|
||||
{
|
||||
name: 'depNames',
|
||||
required: false,
|
||||
},
|
||||
{
|
||||
name: 'password',
|
||||
required: false,
|
||||
},
|
||||
{
|
||||
name: 'pwdControl',
|
||||
required: false,
|
||||
},
|
||||
{
|
||||
name: 'csfLevel',
|
||||
required: false,
|
||||
},
|
||||
{
|
||||
name: 'priority',
|
||||
required: false,
|
||||
},
|
||||
{
|
||||
name: 'siteId',
|
||||
required: false,
|
||||
},
|
||||
]);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -16,6 +16,7 @@ function throttle(func, wait) {
|
|||
const now = new Date().getTime();
|
||||
const remaining = wait - (now - previous);
|
||||
context = this;
|
||||
// eslint-disable-next-line prefer-rest-params
|
||||
args = arguments;
|
||||
if (remaining <= 0 || remaining > wait) {
|
||||
if (timeout) {
|
||||
|
|
|
@ -2,7 +2,7 @@ import slugify from 'slugify';
|
|||
import { format, parse } from 'url';
|
||||
|
||||
/**
|
||||
* Maps over array passing `isLast` bool to iterator as the second arguemnt
|
||||
* Maps over array passing `isLast` bool to iterator as the second argument
|
||||
*/
|
||||
export function mapWithLast<T, P>(array: T[], iteratee: (item: T, isLast: boolean) => P) {
|
||||
const res: P[] = [];
|
||||
|
@ -83,7 +83,7 @@ export function appendToMdHeading(md: string, heading: string, content: string)
|
|||
}
|
||||
|
||||
// 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) {
|
||||
return target;
|
||||
}
|
||||
|
@ -118,7 +118,7 @@ const isMergebleObject = (item): boolean => {
|
|||
|
||||
/**
|
||||
* slugify() returns empty string when failed to slugify.
|
||||
* so try to return minimun slugified-string with failed one which keeps original value
|
||||
* so try to return minimum slugified-string with failed one which keeps original value
|
||||
* the regex codes are referenced with https://gist.github.com/mathewbyrne/1280286
|
||||
*/
|
||||
export function safeSlugify(value: string): string {
|
||||
|
@ -189,8 +189,12 @@ export function removeQueryString(serverUrl: string): string {
|
|||
function parseURL(url: string) {
|
||||
if (typeof URL === 'undefined') {
|
||||
// node
|
||||
return new (require('url')).URL(url);
|
||||
return new (require('url').URL)(url);
|
||||
} else {
|
||||
return new URL(url);
|
||||
}
|
||||
}
|
||||
|
||||
export function unescapeHTMLChars(str: string): string {
|
||||
return str.replace(/&#(\d+);/g, (_m, code) => String.fromCharCode(parseInt(code, 10)));
|
||||
}
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user