pull Redocly/redoc.git master to fork

This commit is contained in:
Lucia Li 2020-09-09 15:36:18 -07:00
commit 02f8819eb1
131 changed files with 24031 additions and 13332 deletions

View File

@ -9,4 +9,4 @@
!webpack.config.ts !webpack.config.ts
!package.json !package.json
!yarn.lock !package-lock.json

52
.eslintrc.js Normal file
View File

@ -0,0 +1,52 @@
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/explicit-module-boundary-types': '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',
'@typescript-eslint/ban-types': ['error', { types: { object: false }, extendDefaults: true }],
'@typescript-eslint/no-var-requires': '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
],
},
],
},
};

View File

@ -15,56 +15,58 @@ 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: Before submitting a pull request, please make sure the following is done:
1. Fork the repository and create your branch from master. 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 youve fixed a bug or added code that should be tested, add tests! 3. If youve 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. 4. Ensure the test suite passes (`npm test`). Tip: `npm test -- --watch TestName` is helpful in development.
5. Format your code with prettier (`yarn prettier`). 5. Format your code with prettier (`npm run prettier`).
## Development Setup ## 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: After cloning the repo, run:
```bash ```bash
$ yarn install # or npm $ npm install # or npm
``` ```
### Commonly used NPM scripts ### Commonly used NPM scripts
``` bash ``` bash
# dev-server, watch and auto reload playground # dev-server, watch and auto reload playground
$ yarn start $ npm start
# start playground app in production environment # start playground app in production environment
$ yarn start:prod $ npm run start:prod
# runt tslint # runt tslint
$ yarn lint $ npm run lint
# try autofix tslint issues # try autofix tslint issues
$ yarn lint --fix $ npm run lint -- --fix
# run unit tests # run unit tests
$ yarn unit $ npm run unit
# run e2e tests # run e2e tests
$ yarn e2e $ npm run e2e
# Make sure you have created bundle before running e2e test
# E.g. run `npm run bundle` and wait for the finishing process.
# open cypress UI to debug e2e test # open cypress UI to debug e2e test
$ yarn cy:open $ npm run cy:open
# run the full test suite, include linting / unit / e2e # run the unit tests (includes linting and license checks)
$ yarn test $ npm test
# prepare bundles # prepare bundles
$ yarn bundle $ npm run bundle
# format the code using prettier # format the code using prettier
$ yarn prettier $ npm run prettier
# auto-generate changelog # auto-generate changelog
$ yarn changelog $ npm run changelog
``` ```
There are some other scripts available in the `scripts` section of the `package.json` file. There are some other scripts available in the `scripts` section of the `package.json` file.

42
.github/workflows/demo-deploy-s3.yml vendored Normal file
View File

@ -0,0 +1,42 @@
name: Redoc demo CI/CD
on:
push:
tags:
- v[0-9]*.[0-9]*.[0-9]*
jobs:
build-and-unit:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v1
- run: npm ci
- run: npm run bundle
- run: npm test
deploy:
needs: build-and-unit
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v1
- name: cache node modules
uses: actions/cache@v1
with:
path: ~/.npm # npm cache files are stored in `~/.npm` on Linux/macOS
key: npm-${{ hashFiles('package-lock.json') }}
restore-keys: |
npm-${{ hashFiles('package-lock.json') }}
npm-
- name: Configure AWS Credentials
uses: aws-actions/configure-aws-credentials@v1
with:
aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
aws-region: us-east-1
- name: Install dependencies
run: npm ci
- name: Build package
run: npm run build:demo
- name: Deploy to S3 bucket
run: npm run deploy:demo
- name: Invalidate
run: aws cloudfront create-invalidation --distribution-id ${{ secrets.CF_DEMO_DISTRIBUTION_ID }} --paths "/*"

View File

@ -3,19 +3,10 @@ name: Unit Tests
on: [push] on: [push]
jobs: jobs:
build: build-and-unit:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v1 - uses: actions/checkout@v1
- name: Use Node.js ${{ matrix.node-version }} - run: npm ci
uses: actions/setup-node@v1 - run: npm run bundle
with: - run: npm test
node-version: 10.x
- name: yarn install, build, and test
run: |
npm install -g yarn
yarn install
yarn bundle
yarn test

3
.gitignore vendored
View File

@ -34,4 +34,5 @@ cli/index.js
/coverage /coverage
.ghpages-tmp .ghpages-tmp
stats.json stats.json
/package-lock.json yarn.lock
.idea

View File

@ -2,7 +2,6 @@ language: node_js
node_js: node_js:
- '10' - '10'
cache: cache:
yarn: true
directories: directories:
- "~/.cache" - "~/.cache"
env: env:
@ -10,11 +9,9 @@ env:
- GH_REF: github.com/Redocly/redoc.git - GH_REF: github.com/Redocly/redoc.git
- GIT_AUTHOR_EMAIL: redoc-bot@users.noreply.github.com - GIT_AUTHOR_EMAIL: redoc-bot@users.noreply.github.com
- GIT_AUTHOR_NAME: RedocBot - GIT_AUTHOR_NAME: RedocBot
- secure: H2GClDJ7TEQaWgnk8d2fIVDpLwG3rTmN8TalUzrCqXGoG6ylCVmlwzKLgfPPWrVgSA7QTdfNV0ab7c2KyPoZBinHmeGMSuKNLVbhOXRc2VFxTBntBTuyJhfCgQEpUKvJesJUuv5RuBn//wC7VZcVVNc06TZDEe8+aDVYQuCRMXZJ4X3e6nSZ64QX2veyVcr+TjnRsZPkeBVcK9hngvuaxLb/hbJ85CvjiseZRt47PGIcrEpMn9n2GMw1m0fNnPoN+MBTSCnIklTmdjPG7t4GUSLmD6H0lNLdXuehYqmQAHgYrLec1aiFlV57QaDoDZrq2hSf4vDmCB/FVydGhD5JunI67pujoV2OnD1V80eUZhYNWOYsJ2Nfp4NxgXsPUcE6zWLYsLfktMPZADhOXInQRACt1cnx8zMYKLnch1RY/ZqjSg0nPtRjLzQ0lNsw5leixvBdBnMjxYHVyAWVwg8WiJMaLO9vog2Qnxg1NTacHO2CsOmm2rw6stpg7ndp/+nOleRlfUKggjt0Tn3FjwCIXeGup2P2EBa+WW2YMAaoMFofYviR5vRlKBgdKo9fsAruaO1r6nm2EdAjOlniyw92bEfU/qOey1nVp/oK2S82uT5In8KB7vl6rF3ak7WAsT9Q5vZUhsrG+eE4PVyIyWNBhs4A7pSwZGHDR/MYtp0E2ug=
- 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: 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: 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: ela1tn4wkJQZ8O4iv+4pIZi5cebxeCStVF1tEUe6qa6WWgJYVXmS2tEv3QQ36NUBFrP58Y6yl10XguPnvj/2BCqcZI4FUBHh3BfiBoUtXxDCVKI5LtlniNiOFGUwfzEeYka8T51zFlcUXSCCaxHkRZbmBsIzeJ39UwTi5fy0qwLv9GgL0czhwm8I8sZ8gyWdGmqpXNFEsb9JP4ZA3mw2qpWkGpGAqQPD9XSCkU3LmX1/ltwsBMAgGYKLLo7vU8d5KV2c8L1Gnxfl6BvfmqUD/dsas/1rnk08rU2nez5ekuQa2tJRkDLOv8bqvrGRLjHSUa3yPuisC6SsDGSU7/3DcozZyYsz7WQ6WI8tYabyjqqeJTF1N8a5T3IbZaZNV1J4JHOO9Cb/y7gIg4edANg6tbe7MzZpdEPRBnw6OkdTdirpNsWQ/jnfpY1hn6mraQZz/q8yaz3W21NjbBJhVnvfh5gWLKQ3YAAziCBhmmrThFhUu0czz+G920MuFo477TBcxvlrE7CaNJ0Q6yYkDehEPOv3jvEs1QVHPwuRrlaLTbBhrlTICKZ58gdX30O8N4i0Xgp/v6qrC03bplnMQc8E/uC61wcVLJixnlZVp8FODpUvPjsxVFkpuNSOIAaiqcERmoiPXx05Epzmr78hjU5rYCx/1MmVoeB4gs9YO+4guD4=
- secure: Pc86j2/rgCPAEWcjzPbbVFkL2SwNt4CpeXP69zedMi9RomQblMJa9R0wbAT6H5VCnPky2cpmxkzjFWOc91N9crzceg6aOoWkPr6pTTnTv+EL70i6+XSrAFpkgRjszprxnU1Bz+GcYEjP/zR8479fx8ooSl7MwHOaP7XiQyaBQAbY1CPlmpT+b4Ut7Fm5QnD90/NgPjbKkngl0kVUfNFdFOSfJ3QWLyFCUUSQ4DlxccJOTIaOH/n8u9Nz4NTuuHE3XeRuEsuj2SJutJnFBUYwsvugrdPvKWiubkewJfylp6EwABzByENsg6XxW9SIq80lMc3Oi7ld9L2lAgpj+8/42olnbMzH0F0rw/p1ccPAdVwQVV6YFaqCzivK5A5aX5LmGKwJ6SR9k1PgcWP6sKKMIsIEObbyM88Tke3QkreEz+cLg/3jjko7Vpb0tbqh8BtbpWV+exL4rX3r2C5Mb1Es1W597hN5LSczWYFgw0ZETpfbVZg6Ri1iZks0wpsT/E+c0q2scUaBVrdTZseHxUPB7mPDlXL1l9/i4sOxPyBHZtJRAzeTT/fOXfj4vuD+ihspXzoRRLaQbizlb8FpyPA47XdmBDpXi3OBiaIFLwvybEn7qM7rqvWxdz6vvCZv0t/AN3t3Qvh2vHKCshHecaa8NoJQHWrdFMHeecYHyeoujZ8=
addons: addons:
chrome: stable chrome: stable
apt: apt:
@ -35,6 +32,6 @@ deploy:
tags: true tags: true
- provider: script - provider: script
skip_cleanup: true skip_cleanup: true
script: yarn deploy:demo script: npm run deploy:demo
on: on:
tags: true tags: true

View File

@ -1,3 +1,221 @@
# [2.0.0-rc.40](https://github.com/Redocly/redoc/compare/v2.0.0-rc.39...v2.0.0-rc.40) (2020-08-24)
### Bug Fixes
* invalid discriminator dropdown behaviour with enum ([be07197](https://github.com/Redocly/redoc/commit/be07197e6d1e85a3fd3e61189a36b288751c077d))
# [2.0.0-rc.39](https://github.com/Redocly/redoc/compare/v2.0.0-rc.38...v2.0.0-rc.39) (2020-08-22)
### Bug Fixes
* fix broken dropdowns with SSR by using forked react-dropdown-aria ([c322639](https://github.com/Redocly/redoc/commit/c322639f7c3e7efbbd623ae83afb88faa91d9e67))
* make callbacks expandable by keyboard ([#1354](https://github.com/Redocly/redoc/issues/1354)) ([46eee7b](https://github.com/Redocly/redoc/commit/46eee7b70c8ee9da0d8857a823c4df39a5f18b53))
# [2.0.0-rc.38](https://github.com/Redocly/redoc/compare/v2.0.0-rc.37...v2.0.0-rc.38) (2020-08-20)
### Bug Fixes
* do not crash for invalid parameter.in value ([addf895](https://github.com/Redocly/redoc/commit/addf8956e33654a1586a8ac6ed7325519cd99da8)), closes [#1340](https://github.com/Redocly/redoc/issues/1340)
* scale sideMenu labels according to computed font size ([#1356](https://github.com/Redocly/redoc/issues/1356)) ([fed9a06](https://github.com/Redocly/redoc/commit/fed9a061d59592ec17cedbe4fd392e1f74c21527))
# [2.0.0-rc.37](https://github.com/Redocly/redoc/compare/v2.0.0-rc.36...v2.0.0-rc.37) (2020-08-14)
### Features
* add webhooks support ([#1304](https://github.com/Redocly/redoc/issues/1304)) ([41f81b4](https://github.com/Redocly/redoc/commit/41f81b4d96648fec6bf0c39799c0aa2dded48749))
# [2.0.0-rc.36](https://github.com/Redocly/redoc/compare/v2.0.0-rc.35...v2.0.0-rc.36) (2020-08-04)
### Bug Fixes
* highlight json keys using different color ([#1287](https://github.com/Redocly/redoc/issues/1287)) ([c9596d4](https://github.com/Redocly/redoc/commit/c9596d4b6cd9dced9fdee77525e0da90960c562a))
* make elements accessible by keyboard navigation tools ([#1339](https://github.com/Redocly/redoc/issues/1339)) ([2ce7189](https://github.com/Redocly/redoc/commit/2ce71895bc14f9189b4e6cbdb6d838898717823f))
### Features
* new option simpleOneOfTypeLabel ([7af2efe](https://github.com/Redocly/redoc/commit/7af2efe731cdb16ebe5de6cb3e96f80cceb7d98d))
# [2.0.0-rc.35](https://github.com/Redocly/redoc/compare/v2.0.0-rc.34...v2.0.0-rc.35) (2020-07-24)
### Bug Fixes
* update EnumValues component ([#1324](https://github.com/Redocly/redoc/issues/1324)) ([de27ac0](https://github.com/Redocly/redoc/commit/de27ac03081d55967f5a479fb1352a83b8ceb8b2))
# [2.0.0-rc.34](https://github.com/Redocly/redoc/compare/v2.0.0-rc.33...v2.0.0-rc.34) (2020-07-24)
Same as rc.33 by mistake
# [2.0.0-rc.33](https://github.com/Redocly/redoc/compare/v2.0.0-rc.31...v2.0.0-rc.33) (2020-07-21)
### Bug Fixes
* default style and explode for params ([633d712](https://github.com/Redocly/redoc/commit/633d71293fa9af2bda3bf456a9258625ee2b94a1)), closes [#1016](https://github.com/Redocly/redoc/issues/1016)
* fix contrast ratio for response titles ([47c6319](https://github.com/Redocly/redoc/commit/47c63192062d87b2b3205b915472930eaff6cc03))
* fix expand variable for vars with hyphens or dots ([0904b3f](https://github.com/Redocly/redoc/commit/0904b3fec24edc56c4a4951501fe02ae22fd852b)), closes [#926](https://github.com/Redocly/redoc/issues/926)
* make dropdowns accessible by keyboard ([e8a0d10](https://github.com/Redocly/redoc/commit/e8a0d105ca52204b0d6fd61f5e909d9dbbe6f147))
* make endpoint dropdown accessible ([3d25005](https://github.com/Redocly/redoc/commit/3d25005f084f06ac01b8fa13eb1d69092e99fd27))
* make properties focusable ([05fd754](https://github.com/Redocly/redoc/commit/05fd7543a29e0aeb364c1ba3f2d736656de7b3b7))
* make response sections focusable ([442014c](https://github.com/Redocly/redoc/commit/442014c06d6a7d2260adf7bc5798dd29869f10c9))
* make sample controls focusable ([006031c](https://github.com/Redocly/redoc/commit/006031c51787b617f2b0aed80a4b8486c5d2d3ca))
* update focus styling ([30a27c1](https://github.com/Redocly/redoc/commit/30a27c116b366428570d0b5516b5b2b4bcd0c5fc))
### Features
* add maxDisplayedEnumValues config and buttons for show/hide enums ([#1322](https://github.com/Redocly/redoc/issues/1322)) ([a2b018d](https://github.com/Redocly/redoc/commit/a2b018d393ee25fb8e9233f8123c29d14ab054c7))
* array size info based on min max Items properties ([#1308](https://github.com/Redocly/redoc/issues/1308)) ([644e96a](https://github.com/Redocly/redoc/commit/644e96ae457047ce09f55aa1f14a42c41dbc1dc8))
* new option sortEnumValuesAlphabetically ([#1321](https://github.com/Redocly/redoc/issues/1321)) ([a96a11a](https://github.com/Redocly/redoc/commit/a96a11a4dc8a509c6c3fba67dc4e065b66624e18))
# [2.0.0-rc.32](https://github.com/Redocly/redoc/compare/v2.0.0-rc.31...v2.0.0-rc.32) (2020-07-21)
Same as rc.31 by mistake
# [2.0.0-rc.31](https://github.com/Redocly/redoc/compare/v2.0.0-rc.30...v2.0.0-rc.31) (2020-06-25)
### Bug Fixes
* do not display long regexps ([#1295](https://github.com/Redocly/redoc/issues/1295)) ([2ede22c](https://github.com/Redocly/redoc/commit/2ede22c45cc970ea1ac296adbae1f6032744f823))
* prevent body scrolling when user scrolls side menu ([#1300](https://github.com/Redocly/redoc/issues/1300)) ([865a56a](https://github.com/Redocly/redoc/commit/865a56a2a9a105ef7b3b9150767399ca7339195a))
# [2.0.0-rc.30](https://github.com/Redocly/redoc/compare/v2.0.0-rc.29...v2.0.0-rc.30) (2020-05-25)
### Bug Fixes
* add security headers to Docker nginx config ([#1244](https://github.com/Redocly/redoc/issues/1244)) ([4512436](https://github.com/Redocly/redoc/commit/4512436f1d88bd99558fe5f8384b37aa62562480))
* keep 3-column layout on 13-inch mbp ([8d1d4c8](https://github.com/Redocly/redoc/commit/8d1d4c82e1377aecf936985ac13fa9bf5257562a))
* proper search-index dispose ([9dd129d](https://github.com/Redocly/redoc/commit/9dd129d90b87f24ad20f084c44d48be50d750c94))
# [2.0.0-rc.29](https://github.com/Redocly/redoc/compare/v2.0.0-rc.28...v2.0.0-rc.29) (2020-05-10)
### Bug Fixes
* depreacate x-code-samples, rename to x-codeSamples for consistency ([becc2f5](https://github.com/Redocly/redoc/commit/becc2f58568388b6500e6476874f27f62ff58ba9))
* do not crash on incompatible allOf, console.warn instead ([6e607b9](https://github.com/Redocly/redoc/commit/6e607b9a2928b062c7705087432c0f0d88e74f5d)), closes [#1156](https://github.com/Redocly/redoc/issues/1156)
* download button opens in new tab instead of downloading ([b59faad](https://github.com/Redocly/redoc/commit/b59faada8210a4c8f61fa0e850b7d844574a46d1)), closes [#1247](https://github.com/Redocly/redoc/issues/1247)
* fix broken md headings with ampersand ([8460659](https://github.com/Redocly/redoc/commit/846065916d58cf628f0bc93c74be429ecdea12e7)), closes [#1173](https://github.com/Redocly/redoc/issues/1173)
### Features
* **cli:** add the --title option to the serve subcommand ([#1160](https://github.com/Redocly/redoc/issues/1160)) ([10414fc](https://github.com/Redocly/redoc/commit/10414fc6d5c0f91b5e93b1ed2326e4e508611324))
# [2.0.0-rc.28](https://github.com/Redocly/redoc/compare/v2.0.0-rc.27...v2.0.0-rc.28) (2020-04-27)
### Bug Fixes
* encode URLs in json samples linkify (xss) ([62c01da](https://github.com/Redocly/redoc/commit/62c01da420fca2137674ae562d4ecba54db97da9)), thanks to @masatokinugawa
# [2.0.0-rc.27](https://github.com/Redocly/redoc/compare/v2.0.0-rc.26...v2.0.0-rc.27) (2020-04-20)
### Features
* add callbacks support ([#1224](https://github.com/Redocly/redoc/issues/1224)) ([57e93ec](https://github.com/Redocly/redoc/commit/57e93ec4355de2659fcb5449b14b7ed738c6c276))
# [2.0.0-rc.26](https://github.com/Redocly/redoc/compare/v2.0.0-rc.25...v2.0.0-rc.26) (2020-03-29)
### Bug Fixes
* crash to wrong spelling in localeCompare ([3908a7c](https://github.com/Redocly/redoc/commit/3908a7c46448d277b82318659cdea65db52f9e70)), closes [#1218](https://github.com/Redocly/redoc/issues/1218)
# [2.0.0-rc.25](https://github.com/Redocly/redoc/compare/v2.0.0-rc.24...v2.0.0-rc.25) (2020-03-27)
### Bug Fixes
* do not collapse top level on Collapse All in json samples ([#1209](https://github.com/Redocly/redoc/issues/1209)) ([830371b](https://github.com/Redocly/redoc/commit/830371b5d1edf4ba7a138b3b3d78148d020e0349))
* fix passing boolean value to showExtensions options ([#1211](https://github.com/Redocly/redoc/issues/1211)) ([c6eaa02](https://github.com/Redocly/redoc/commit/c6eaa0281bb0f62b019c865e4aefb863ce84d628))
* improve names for some theme settings ([a0bd27c](https://github.com/Redocly/redoc/commit/a0bd27c75427a39abc9c753b0654678eed2f3851))
* sort discriminator entries by mapping order ([#1216](https://github.com/Redocly/redoc/issues/1216)) ([ac4f915](https://github.com/Redocly/redoc/commit/ac4f915494f289d1c97ffdfe3af59efd94734f8c))
### Features
* add x-explicitMappingOnly extension ([#1215](https://github.com/Redocly/redoc/issues/1215)) ([ea5b0aa](https://github.com/Redocly/redoc/commit/ea5b0aabf9133d11d3a8fcb79f9515d21e0d7ac0))
# [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) # [2.0.0-rc.21](https://github.com/Redocly/redoc/compare/v2.0.0-rc.20...v2.0.0-rc.21) (2020-01-10)

View File

@ -64,11 +64,11 @@ Additionally, all the 1.x releases are hosted on our GitHub Pages-based CDN **(d
| 1.17.x | 2.0 | | 1.17.x | 2.0 |
## Some Real-life usages ## 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/) - [Docker Engine](https://docs.docker.com/engine/api/v1.25/)
- [Zuora](https://www.zuora.com/developer/api-reference/) - [Zuora](https://www.zuora.com/developer/api-reference/)
- [Shopify Draft Orders](https://help.shopify.com/api/draft-orders)
- [Discourse](http://docs.discourse.org) - [Discourse](http://docs.discourse.org)
- [Commbox](https://www.commbox.io/api/)
- [APIs.guru](https://apis.guru/api-doc/) - [APIs.guru](https://apis.guru/api-doc/)
- [FastAPI](https://github.com/tiangolo/fastapi) - [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. **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) ### 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 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 ### 2. Reference redoc script in HTML
For **CDN**: For **CDN**:
```html ```html
@ -211,7 +211,7 @@ You can inject Security Definitions widget into any place of your specification
ReDoc makes use of the following [vendor extensions](https://swagger.io/specification/#specificationExtensions): ReDoc makes use of the following [vendor extensions](https://swagger.io/specification/#specificationExtensions):
* [`x-logo`](docs/redoc-vendor-extensions.md#x-logo) - is used to specify API logo * [`x-logo`](docs/redoc-vendor-extensions.md#x-logo) - is used to specify API logo
* [`x-traitTag`](docs/redoc-vendor-extensions.md#x-traitTag) - useful for handling out common things like Pagination, Rate-Limits, etc * [`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-codeSamples`](docs/redoc-vendor-extensions.md#x-codeSamples) - specify operation code samples
* [`x-examples`](docs/redoc-vendor-extensions.md#x-examples) - specify JSON example for requests * [`x-examples`](docs/redoc-vendor-extensions.md#x-examples) - specify JSON example for requests
* [`x-nullable`](docs/redoc-vendor-extensions.md#x-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-displayName`](docs/redoc-vendor-extensions.md#x-displayname) - specify human-friendly names for the menu categories
@ -230,7 +230,10 @@ You can use all of the following options with standalone version on <redoc> tag
* `hideHostname` - if set, the protocol and hostname is not shown in the operation definition. * `hideHostname` - if set, the protocol and hostname is not shown in the operation definition.
* `hideLoading` - do not show loading animation. Useful for small docs. * `hideLoading` - do not show loading animation. Useful for small docs.
* `hideSingleRequestSampleTab` - do not show the request sample tab for requests with only one sample. * `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`. * `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`.
* `hideSchemaTitles` - do not display schema `title` next to to the type
* `simpleOneOfTypeLabel` - show only unique oneOf types in the label without titles
* `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.~~ * `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`. * `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). * `nativeScrollbars` - use native scrollbar for sidemenu instead of perfect-scroll (scrolling performance optimization for big specs).

View File

@ -17,7 +17,7 @@ const localDistDir = './benchmark/revisions/local/bundles';
sh.rm('-rf', localDistDir); sh.rm('-rf', localDistDir);
console.log(`Building local dist: ${localDistDir}`); console.log(`Building local dist: ${localDistDir}`);
sh.mkdir('-p', localDistDir); sh.mkdir('-p', localDistDir);
exec(`yarn bundle:lib --output-path ${localDistDir}`); exec(`npm run bundle:lib --output-path ${localDistDir}`);
const revisions = []; const revisions = [];
for (const arg of args) { for (const arg of args) {
@ -119,7 +119,7 @@ function buildRevisionDist(revision) {
const pwd = sh.pwd(); const pwd = sh.pwd();
sh.cd(buildDir); 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); sh.cd(pwd);
return distDir; return distDir;
} }

View File

@ -50,6 +50,11 @@ YargsParser.command(
describe: 'path or URL to your spec', describe: 'path or URL to your spec',
}); });
yargs.options('title', {
describe: 'Page Title',
type: 'string',
});
yargs.option('s', { yargs.option('s', {
alias: 'ssr', alias: 'ssr',
describe: 'Enable server-side rendering', describe: 'Enable server-side rendering',
@ -73,6 +78,7 @@ YargsParser.command(
async argv => { async argv => {
const config: Options = { const config: Options = {
ssr: argv.ssr as boolean, ssr: argv.ssr as boolean,
title: argv.title as string,
watch: argv.watch as boolean, watch: argv.watch as boolean,
templateFileName: argv.template as string, templateFileName: argv.template as string,
templateOptions: argv.templateOptions || {}, templateOptions: argv.templateOptions || {},

1946
cli/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

View File

@ -1,6 +1,6 @@
{ {
"name": "redoc-cli", "name": "redoc-cli",
"version": "0.9.5", "version": "0.9.12",
"description": "ReDoc's Command Line Interface", "description": "ReDoc's Command Line Interface",
"main": "index.js", "main": "index.js",
"bin": "index.js", "bin": "index.js",
@ -11,25 +11,25 @@
"node": ">= 8" "node": ">= 8"
}, },
"dependencies": { "dependencies": {
"chokidar": "^3.0.2", "chokidar": "^3.4.1",
"handlebars": "^4.1.2", "handlebars": "^4.7.6",
"isarray": "^2.0.5", "isarray": "^2.0.5",
"mkdirp": "^0.5.1", "mkdirp": "^1.0.4",
"mobx": "^4.2.0", "mobx": "^4.2.0",
"node-libs-browser": "^2.2.1", "node-libs-browser": "^2.2.1",
"react": "^16.8.6", "react": "^16.13.1",
"react-dom": "^16.8.6", "react-dom": "^16.13.1",
"redoc": "2.0.0-rc.21", "redoc": "^2.0.0-rc.40",
"styled-components": "^4.3.2", "styled-components": "^5.1.1",
"tslib": "^1.10.0", "tslib": "^2.0.0",
"yargs": "^13.3.0" "yargs": "^15.4.1"
}, },
"publishConfig": { "publishConfig": {
"access": "public" "access": "public"
}, },
"devDependencies": { "devDependencies": {
"@types/chokidar": "^2.1.3", "@types/chokidar": "^2.1.3",
"@types/handlebars": "^4.0.39", "@types/handlebars": "^4.1.0",
"@types/mkdirp": "^0.5.2" "@types/mkdirp": "^1.0.1"
} }
} }

File diff suppressed because it is too large Load Diff

View File

@ -11,8 +11,8 @@ RUN apk update && apk add --no-cache git
# Install dependencies # Install dependencies
WORKDIR /build WORKDIR /build
COPY package.json yarn.lock /build/ COPY package.json package-lock.json /build/
RUN yarn install --frozen-lockfile --ignore-optional --ignore-scripts RUN npm ci --no-optional --ignore-scripts
# copy only required for the build files # copy only required for the build files
COPY src /build/src COPY src /build/src

View File

@ -13,6 +13,12 @@ Serve local file:
-v $(pwd)/demo/swagger.yaml:/usr/share/nginx/html/swagger.yaml \ -v $(pwd)/demo/swagger.yaml:/usr/share/nginx/html/swagger.yaml \
-e SPEC_URL=swagger.yaml redocly/redoc -e SPEC_URL=swagger.yaml redocly/redoc
Serve local file and watch for updates:
docker run -it --rm -p 80:80 \
-v $(pwd)/demo/:/usr/share/nginx/html/swagger/ \
-e SPEC_URL=swagger/swagger.yaml redocly/redoc
## Runtime configuration options ## Runtime configuration options
- `PAGE_TITLE` (default `"ReDoc"`) - page title - `PAGE_TITLE` (default `"ReDoc"`) - page title

View File

@ -21,6 +21,13 @@ http {
alias /usr/share/nginx/html/; alias /usr/share/nginx/html/;
if ($request_method = 'OPTIONS') { if ($request_method = 'OPTIONS') {
# Add security headers
add_header 'X-Frame-Options' 'deny always';
add_header 'X-XSS-Protection' '"1; mode=block" always';
add_header 'X-Content-Type-Options' 'nosniff always';
add_header 'Referrer-Policy' 'strict-origin-when-cross-origin';
# Set access control header
add_header 'Access-Control-Allow-Origin' '*'; add_header 'Access-Control-Allow-Origin' '*';
add_header 'Access-Control-Allow-Methods' 'GET, POST, OPTIONS'; add_header 'Access-Control-Allow-Methods' 'GET, POST, OPTIONS';
# #
@ -36,11 +43,25 @@ http {
return 204; return 204;
} }
if ($request_method = 'POST') { if ($request_method = 'POST') {
# Add security headers
add_header 'X-Frame-Options' 'deny always';
add_header 'X-XSS-Protection' '"1; mode=block" always';
add_header 'X-Content-Type-Options' 'nosniff always';
add_header 'Referrer-Policy' 'strict-origin-when-cross-origin';
# Set access control header
add_header 'Access-Control-Allow-Origin' '*'; add_header 'Access-Control-Allow-Origin' '*';
add_header 'Access-Control-Allow-Methods' 'GET, POST, OPTIONS'; add_header 'Access-Control-Allow-Methods' 'GET, POST, OPTIONS';
add_header 'Access-Control-Allow-Headers' 'DNT,X-CustomHeader,Keep-Alive,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type'; add_header 'Access-Control-Allow-Headers' 'DNT,X-CustomHeader,Keep-Alive,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type';
} }
if ($request_method = 'GET') { if ($request_method = 'GET') {
# Add security headers
add_header 'X-Frame-Options' 'deny always';
add_header 'X-XSS-Protection' '"1; mode=block" always';
add_header 'X-Content-Type-Options' 'nosniff always';
add_header 'Referrer-Policy' 'strict-origin-when-cross-origin';
# Set access control header
add_header 'Access-Control-Allow-Origin' '*'; add_header 'Access-Control-Allow-Origin' '*';
add_header 'Access-Control-Allow-Methods' 'GET, POST, OPTIONS'; add_header 'Access-Control-Allow-Methods' 'GET, POST, OPTIONS';
add_header 'Access-Control-Allow-Headers' 'DNT,X-CustomHeader,Keep-Alive,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type'; add_header 'Access-Control-Allow-Headers' 'DNT,X-CustomHeader,Keep-Alive,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type';

View File

@ -13,10 +13,7 @@ const demos = [
}, },
{ value: 'https://api.apis.guru/v2/specs/slack.com/1.2.0/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/zoom.us/2.0.0/swagger.yaml', label: 'Zoom.us' },
{ { value: 'https://docs.graphhopper.com/openapi.json', label: 'GraphHopper' },
value: 'https://api.apis.guru/v2/specs/graphhopper.com/1.0/swagger.yaml',
label: 'GraphHopper',
},
]; ];
const DEFAULT_SPEC = 'openapi.yaml'; const DEFAULT_SPEC = 'openapi.yaml';
@ -100,11 +97,14 @@ class DemoApp extends React.Component<
src="https://ghbtns.com/github-btn.html?user=Redocly&amp;repo=redoc&amp;type=star&amp;count=true&amp;size=large" src="https://ghbtns.com/github-btn.html?user=Redocly&amp;repo=redoc&amp;type=star&amp;count=true&amp;size=large"
frameBorder="0" frameBorder="0"
scrolling="0" scrolling="0"
width="150px" width="160px"
height="30px" height="30px"
/> />
</Heading> </Heading>
<RedocStandalone specUrl={proxiedUrl} options={{ scrollYOffset: 'nav' }} /> <RedocStandalone
specUrl={proxiedUrl}
options={{ scrollYOffset: 'nav', untrustedSpec: true }}
/>
</> </>
); );
} }

View File

@ -114,7 +114,7 @@ paths:
- petstore_auth: - petstore_auth:
- 'write:pets' - 'write:pets'
- 'read:pets' - 'read:pets'
x-code-samples: x-codeSamples:
- lang: 'C#' - lang: 'C#'
source: | source: |
PetStore.v1.Pet pet = new PetStore.v1.Pet(); PetStore.v1.Pet pet = new PetStore.v1.Pet();
@ -162,7 +162,7 @@ paths:
- petstore_auth: - petstore_auth:
- 'write:pets' - 'write:pets'
- 'read:pets' - 'read:pets'
x-code-samples: x-codeSamples:
- lang: PHP - lang: PHP
source: | source: |
$form = new \PetStore\Entities\Pet(); $form = new \PetStore\Entities\Pet();
@ -489,6 +489,234 @@ paths:
description: Invalid ID supplied description: Invalid ID supplied
'404': '404':
description: Order not found description: Order not found
/store/subscribe:
post:
tags:
- store
summary: Subscribe to the Store events
description: Add subscription for a store events
requestBody:
content:
application/json:
schema:
type: object
properties:
callbackUrl:
type: string
format: uri
description: This URL will be called by the server when the desired event will occur
example: https://myserver.com/send/callback/here
eventName:
type: string
description: Event name for the subscription
enum:
- orderInProgress
- orderShipped
- orderDelivered
example: orderInProgress
required:
- callbackUrl
- eventName
responses:
'201':
description: Subscription added
content:
application/json:
schema:
type: object
properties:
subscriptionId:
type: string
example: AAA-123-BBB-456
callbacks:
orderInProgress:
'{$request.body#/callbackUrl}?event={$request.body#/eventName}':
servers:
- url: //callback-url.path-level/v1
description: Path level server 1
- url: //callback-url.path-level/v2
description: Path level server 2
post:
summary: Order in Progress (Summary)
description: A callback triggered every time an Order is updated status to "inProgress" (Description)
externalDocs:
description: Find out more
url: 'https://more-details.com/demo'
requestBody:
content:
application/json:
schema:
type: object
properties:
orderId:
type: string
example: '123'
timestamp:
type: string
format: date-time
example: '2018-10-19T16:46:45Z'
status:
type: string
example: 'inProgress'
application/xml:
schema:
type: object
properties:
orderId:
type: string
example: '123'
example: |
<?xml version="1.0" encoding="UTF-8"?>
<root>
<orderId>123</orderId>
<status>inProgress</status>
<timestamp>2018-10-19T16:46:45Z</timestamp>
</root>
responses:
'200':
description: Callback successfully processed and no retries will be performed
content:
application/json:
schema:
type: object
properties:
someProp:
type: string
example: '123'
'299':
description: Response for cancelling subscription
'500':
description: Callback processing failed and retries will be performed
x-codeSamples:
- lang: 'C#'
source: |
PetStore.v1.Pet pet = new PetStore.v1.Pet();
pet.setApiKey("your api key");
pet.petType = PetStore.v1.Pet.TYPE_DOG;
pet.name = "Rex";
// set other fields
PetStoreResponse response = pet.create();
if (response.statusCode == HttpStatusCode.Created)
{
// Successfully created
}
else
{
// Something wrong -- check response for errors
Console.WriteLine(response.getRawResponse());
}
- lang: PHP
source: |
$form = new \PetStore\Entities\Pet();
$form->setPetType("Dog");
$form->setName("Rex");
// set other fields
try {
$pet = $client->pets()->create($form);
} catch (UnprocessableEntityException $e) {
var_dump($e->getErrors());
}
put:
description: Order in Progress (Only Description)
servers:
- url: //callback-url.operation-level/v1
description: Operation level server 1 (Operation override)
- url: //callback-url.operation-level/v2
description: Operation level server 2 (Operation override)
requestBody:
content:
application/json:
schema:
type: object
properties:
orderId:
type: string
example: '123'
timestamp:
type: string
format: date-time
example: '2018-10-19T16:46:45Z'
status:
type: string
example: 'inProgress'
application/xml:
schema:
type: object
properties:
orderId:
type: string
example: '123'
example: |
<?xml version="1.0" encoding="UTF-8"?>
<root>
<orderId>123</orderId>
<status>inProgress</status>
<timestamp>2018-10-19T16:46:45Z</timestamp>
</root>
responses:
'200':
description: Callback successfully processed and no retries will be performed
content:
application/json:
schema:
type: object
properties:
someProp:
type: string
example: '123'
orderShipped:
'{$request.body#/callbackUrl}?event={$request.body#/eventName}':
post:
description: |
Very long description
Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor
incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis
nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.
Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu
fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in
culpa qui officia deserunt mollit anim id est laborum.
requestBody:
content:
application/json:
schema:
type: object
properties:
orderId:
type: string
example: '123'
timestamp:
type: string
format: date-time
example: '2018-10-19T16:46:45Z'
estimatedDeliveryDate:
type: string
format: date-time
example: '2018-11-11T16:00:00Z'
responses:
'200':
description: Callback successfully processed and no retries will be performed
orderDelivered:
'http://notificationServer.com?url={$request.body#/callbackUrl}&event={$request.body#/eventName}':
post:
deprecated: true
summary: Order delivered
description: A callback triggered every time an Order is delivered to the recipient
requestBody:
content:
application/json:
schema:
type: object
properties:
orderId:
type: string
example: '123'
timestamp:
type: string
format: date-time
example: '2018-10-19T16:46:45Z'
responses:
'200':
description: Callback successfully processed and no retries will be performed
/user: /user:
post: post:
tags: tags:
@ -955,7 +1183,23 @@ components:
examples: examples:
Order: Order:
value: value:
quantity: 1, quantity: 1
shipDate: 2018-10-19T16:46:45Z, shipDate: '2018-10-19T16:46:45Z'
status: placed, status: placed
complete: false complete: false
x-webhooks:
newPet:
post:
summary: New pet
description: Information about a new pet in the systems
operationId: newPet
tags:
- pet
requestBody:
content:
application/json:
schema:
$ref: "#/components/schemas/Pet"
responses:
"200":
description: Return a 200 status to indicate that the data was received successfully

View File

@ -26,7 +26,7 @@ const specUrl =
(userUrl && userUrl[1]) || (swagger ? 'swagger.yaml' : big ? 'big-openapi.json' : 'openapi.yaml'); (userUrl && userUrl[1]) || (swagger ? 'swagger.yaml' : big ? 'big-openapi.json' : 'openapi.yaml');
let store; let store;
const options: RedocRawOptions = { nativeScrollbars: false }; const options: RedocRawOptions = { nativeScrollbars: false, maxDisplayedEnumValues: 3 };
async function init() { async function init() {
const spec = await loadAndBundleSpec(specUrl); const spec = await loadAndBundleSpec(specUrl);

View File

@ -1,5 +1,5 @@
import * as CopyWebpackPlugin from 'copy-webpack-plugin'; import * as CopyWebpackPlugin from 'copy-webpack-plugin';
import * as ForkTsCheckerWebpackPlugin from 'fork-ts-checker-webpack-plugin'; import ForkTsCheckerWebpackPlugin = require('fork-ts-checker-webpack-plugin');
import * as HtmlWebpackPlugin from 'html-webpack-plugin'; import * as HtmlWebpackPlugin from 'html-webpack-plugin';
import { compact } from 'lodash'; import { compact } from 'lodash';
import { resolve } from 'path'; import { resolve } from 'path';
@ -7,17 +7,14 @@ import * as webpack from 'webpack';
const VERSION = JSON.stringify(require('../package.json').version); const VERSION = JSON.stringify(require('../package.json').version);
const REVISION = JSON.stringify( const REVISION = JSON.stringify(
require('child_process') require('child_process').execSync('git rev-parse --short HEAD').toString().trim(),
.execSync('git rev-parse --short HEAD')
.toString()
.trim(),
); );
function root(filename) { function root(filename) {
return resolve(__dirname + '/' + filename); return resolve(__dirname + '/' + filename);
} }
const tsLoader = env => ({ const tsLoader = (env) => ({
loader: 'ts-loader', loader: 'ts-loader',
options: { options: {
compilerOptions: { compilerOptions: {
@ -27,7 +24,7 @@ const tsLoader = env => ({
}, },
}); });
const babelLoader = mode => ({ const babelLoader = () => ({
loader: 'babel-loader', loader: 'babel-loader',
options: { options: {
generatorOpts: { generatorOpts: {
@ -38,13 +35,6 @@ const babelLoader = mode => ({
['@babel/plugin-syntax-decorators', { legacy: true }], ['@babel/plugin-syntax-decorators', { legacy: true }],
'@babel/plugin-syntax-dynamic-import', '@babel/plugin-syntax-dynamic-import',
'@babel/plugin-syntax-jsx', '@babel/plugin-syntax-jsx',
[
'babel-plugin-styled-components',
{
minify: true,
displayName: mode !== 'production',
},
],
]), ]),
}, },
}); });
@ -114,7 +104,7 @@ export default (env: { playground?: boolean; bench?: boolean } = {}, { mode }) =
use: compact([ use: compact([
mode !== 'production' ? babelHotLoader : undefined, mode !== 'production' ? babelHotLoader : undefined,
tsLoader(env), tsLoader(env),
babelLoader(mode), babelLoader(),
]), ]),
exclude: [/node_modules/], exclude: [/node_modules/],
}, },
@ -161,7 +151,9 @@ export default (env: { playground?: boolean; bench?: boolean } = {}, { mode }) =
ignore(/js-yaml\/dumper\.js$/), ignore(/js-yaml\/dumper\.js$/),
ignore(/json-schema-ref-parser\/lib\/dereference\.js/), ignore(/json-schema-ref-parser\/lib\/dereference\.js/),
ignore(/^\.\/SearchWorker\.worker$/), ignore(/^\.\/SearchWorker\.worker$/),
new CopyWebpackPlugin(['demo/openapi.yaml']), new CopyWebpackPlugin({
patterns: ['demo/openapi.yaml'],
}),
], ],
}); });

View File

@ -4,7 +4,7 @@ ReDoc makes use of the following [vendor extensions](https://swagger.io/specific
### Swagger Object vendor extensions ### Swagger Object vendor extensions
Extend OpenAPI root [Swagger Object](https://swagger.io/specification/#oasObject) Extend OpenAPI root [Swagger Object](https://swagger.io/specification/#oasObject)
#### x-servers #### x-servers
Backported from OpenAPI 3.0 [`servers`](https://github.com/OAI/OpenAPI-Specification/blob/OpenAPI.next/versions/3.0.0.md#server-object). Currently doesn't support templates. Backported from OpenAPI 3.0 [`servers`](https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.0.md#serverObject). Currently doesn't support templates.
#### x-tagGroups #### x-tagGroups
@ -162,13 +162,13 @@ x-traitTag: true
### Operation Object vendor extensions ### Operation Object vendor extensions
Extends OpenAPI [Operation Object](http://swagger.io/specification/#operationObject) Extends OpenAPI [Operation Object](http://swagger.io/specification/#operationObject)
#### x-code-samples #### x-codeSamples
| Field Name | Type | Description | | Field Name | Type | Description |
| :------------- | :------: | :---------- | | :------------- | :------: | :---------- |
| x-code-samples | [ [Code Sample Object](#codeSampleObject) ] | A list of code samples associated with operation | | x-codeSamples | [ [Code Sample Object](#codeSampleObject) ] | A list of code samples associated with operation |
###### Usage in ReDoc ###### Usage in ReDoc
`x-code-samples` are rendered on the right panel of ReDoc `x-codeSamples` are rendered on the right panel of ReDoc
#### <a name="codeSampleObject"></a>Code Sample Object #### <a name="codeSampleObject"></a>Code Sample Object
Operation code sample Operation code sample
@ -306,3 +306,37 @@ Player:
x-additionalPropertiesName: attribute-name x-additionalPropertiesName: attribute-name
type: string type: string
``` ```
#### x-explicitMappingOnly
**ATTENTION**: This is ReDoc-specific vendor extension. It won't be supported by other tools.
Extends the `discriminator` property of the schema object.
| Field Name | Type | Description |
| :------------- | :------: | :---------- |
| x-explicitMappingOnly | boolean | limit the discriminator selectpicker to the explicit mappings only |
###### Usage in ReDoc
ReDoc uses this extension to filter the `discriminator` mappings shown in the selectpicker.
When set to `true`, the selectpicker will only list the the explicitly defined mappings. When `false`,
the default behavior is kept, i.e. explicit and implicit mappings will be shown.
###### x-explicitMappingOnly example
```yaml
Pet:
type: object
required:
- name
- photoUrls
discriminator:
propertyName: petType
x-explicitMappingOnly: true
mapping:
cat: "#/components/schemas/Cat"
bee: "#/components/schemas/HoneyBee"
```
Will show in the selectpicker only the items `cat` and `bee`, even though the `Dog` class inherits from
the `Pet` class.

View File

@ -6,7 +6,7 @@ describe('Menu', () => {
it('should have valid items count', () => { it('should have valid items count', () => {
cy.get('.menu-content') cy.get('.menu-content')
.find('li') .find('li')
.should('have.length', 6 + (2 + 8 + 1 + 4 + 2) + (1 + 8)); .should('have.length', 34);
}); });
it('should sync active menu items while scroll', () => { it('should sync active menu items while scroll', () => {

View File

@ -27,6 +27,8 @@ describe('Search', () => {
it('should support arrow navigation', () => { it('should support arrow navigation', () => {
getSearchInput().type('int', { force: true }); getSearchInput().type('int', { force: true });
cy.wait(500);
getSearchInput().type('{downarrow}', { force: true }); getSearchInput().type('{downarrow}', { force: true });
getResult(0).should('have.class', 'active'); getResult(0).should('have.class', 'active');

19504
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

View File

@ -1,6 +1,6 @@
{ {
"name": "redoc", "name": "redoc",
"version": "2.0.0-rc.21", "version": "2.0.0-rc.40",
"description": "ReDoc", "description": "ReDoc",
"repository": { "repository": {
"type": "git", "type": "git",
@ -42,87 +42,89 @@
"stats": "webpack --env.standalone --json --profile --mode=production > stats.json", "stats": "webpack --env.standalone --json --profile --mode=production > stats.json",
"prettier": "prettier --write \"cli/index.ts\" \"src/**/*.{ts,tsx}\"", "prettier": "prettier --write \"cli/index.ts\" \"src/**/*.{ts,tsx}\"",
"changelog": "conventional-changelog -p angular -i CHANGELOG.md -s -r 1", "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", "benchmark": "node ./benchmark/benchmark.js",
"start:demo": "webpack-dev-server --hot --config demo/webpack.config.ts --mode=development", "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", "compile:cli": "tsc custom.d.ts cli/index.ts --target es6 --module commonjs --types yargs",
"build:demo": "webpack --mode=production --config demo/webpack.config.ts", "build:demo": "webpack --mode=production --config demo/webpack.config.ts",
"deploy:demo": "npm run build:demo && deploy-to-gh-pages --update demo/dist", "deploy:demo": "aws s3 sync demo/dist s3://production-redoc-demo --acl=public-read",
"license-check": "license-checker --production --onlyAllow 'MIT;ISC;Apache-2.0;BSD;BSD-2-Clause;BSD-3-Clause' --summary", "license-check": "license-checker --production --onlyAllow 'MIT;ISC;Apache-2.0;BSD;BSD-2-Clause;BSD-3-Clause' --summary",
"docker:build": "docker build -f config/docker/Dockerfile -t redoc ." "docker:build": "docker build -f config/docker/Dockerfile -t redoc ."
}, },
"devDependencies": { "devDependencies": {
"@babel/core": "7.7.5", "@babel/core": "^7.10.5",
"@babel/plugin-syntax-decorators": "7.7.4", "@babel/plugin-syntax-decorators": "^7.10.4",
"@babel/plugin-syntax-dynamic-import": "^7.7.4", "@babel/plugin-syntax-dynamic-import": "^7.8.3",
"@babel/plugin-syntax-jsx": "7.7.4", "@babel/plugin-syntax-jsx": "^7.10.4",
"@babel/plugin-syntax-typescript": "7.7.4", "@babel/plugin-syntax-typescript": "^7.10.4",
"@cypress/webpack-preprocessor": "4.1.1", "@cypress/webpack-preprocessor": "^5.4.2",
"@hot-loader/react-dom": "^16.11.0", "@hot-loader/react-dom": "^16.12.0",
"@types/chai": "4.2.7", "@types/chai": "^4.2.12",
"@types/dompurify": "^2.0.0", "@types/dompurify": "^2.0.2",
"@types/enzyme": "^3.10.4", "@types/enzyme": "^3.10.5",
"@types/enzyme-to-json": "^1.5.3", "@types/enzyme-to-json": "^1.5.3",
"@types/jest": "^24.0.23", "@types/jest": "^26.0.7",
"@types/json-pointer": "^1.0.30", "@types/json-pointer": "^1.0.30",
"@types/lodash": "^4.14.149", "@types/lodash": "^4.14.158",
"@types/lunr": "^2.3.2", "@types/lunr": "^2.3.3",
"@types/mark.js": "^8.11.5", "@types/mark.js": "^8.11.5",
"@types/marked": "^0.7.2", "@types/marked": "^1.1.0",
"@types/prismjs": "^1.16.0", "@types/prismjs": "^1.16.1",
"@types/prop-types": "^15.7.3", "@types/prop-types": "^15.7.3",
"@types/react": "^16.9.16", "@types/react": "^16.9.43",
"@types/react-dom": "^16.9.4", "@types/react-dom": "^16.9.8",
"@types/react-tabs": "^2.3.1", "@types/react-tabs": "^2.3.2",
"@types/styled-components": "^4.4.1", "@types/styled-components": "^5.1.1",
"@types/tapable": "1.0.4", "@types/tapable": "^1.0.6",
"@types/webpack": "^4.41.0", "@types/webpack": "^4.41.21",
"@types/webpack-env": "^1.14.1", "@types/webpack-env": "^1.15.2",
"@types/yargs": "^13.0.3", "@types/yargs": "^15.0.5",
"babel-loader": "8.0.6", "@typescript-eslint/eslint-plugin": "^3.7.0",
"babel-plugin-styled-components": "^1.10.6", "@typescript-eslint/parser": "^3.7.0",
"babel-loader": "^8.1.0",
"babel-plugin-styled-components": "^1.10.7",
"beautify-benchmark": "^0.2.4", "beautify-benchmark": "^0.2.4",
"bundlesize": "^0.18.0", "bundlesize": "^0.18.0",
"conventional-changelog-cli": "^2.0.28", "conventional-changelog-cli": "^2.0.34",
"copy-webpack-plugin": "^5.1.1", "copy-webpack-plugin": "^6.0.3",
"core-js": "^3.5.0", "core-js": "^3.6.5",
"coveralls": "^3.0.9", "coveralls": "^3.1.0",
"css-loader": "^3.3.0", "css-loader": "^3.6.0",
"cypress": "~3.7.0", "cypress": "^4.11.0",
"deploy-to-gh-pages": "^1.3.7", "deploy-to-gh-pages": "^1.3.7",
"enzyme": "^3.10.0", "enzyme": "^3.11.0",
"enzyme-adapter-react-16": "^1.15.1", "enzyme-adapter-react-16": "^1.15.2",
"enzyme-to-json": "^3.4.3", "enzyme-to-json": "^3.5.0",
"fork-ts-checker-webpack-plugin": "3.1.1", "eslint": "^7.5.0",
"html-webpack-plugin": "^3.1.0", "eslint-plugin-import": "^2.22.0",
"jest": "^24.9.0", "eslint-plugin-react": "^7.20.3",
"fork-ts-checker-webpack-plugin": "^5.0.11",
"html-webpack-plugin": "^4.3.0",
"jest": "^26.1.0",
"license-checker": "^25.0.1", "license-checker": "^25.0.1",
"lodash": "^4.17.15", "lodash": "^4.17.19",
"mobx": "^4.3.1", "mobx": "^5.15.4",
"prettier": "^1.19.1", "prettier": "^2.0.5",
"prettier-eslint": "^9.0.1",
"raf": "^3.4.1", "raf": "^3.4.1",
"react": "^16.12.0", "react": "^16.13.1",
"react-hot-loader": "^4.12.18", "react-dom": "^16.13.1",
"react-dom": "^16.12.0", "react-hot-loader": "^4.12.21",
"rimraf": "^3.0.0", "rimraf": "^3.0.2",
"shelljs": "^0.8.3", "shelljs": "^0.8.4",
"source-map-loader": "^0.2.4", "source-map-loader": "^1.0.1",
"style-loader": "^1.0.1", "style-loader": "^1.2.1",
"styled-components": "^4.4.1", "styled-components": "^5.1.1",
"ts-jest": "24.2.0", "ts-jest": "^26.1.3",
"ts-loader": "6.2.1", "ts-loader": "^8.0.1",
"ts-node": "^8.5.4", "ts-node": "^8.10.2",
"tslint": "^5.20.1", "typescript": "^3.9.7",
"tslint-react": "^4.1.0",
"typescript": "^3.7.3",
"unfetch": "^4.1.0", "unfetch": "^4.1.0",
"url-polyfill": "^1.1.7", "url-polyfill": "^1.1.10",
"webpack": "^4.41.2", "webpack": "^4.44.0",
"webpack-cli": "^3.3.10", "webpack-cli": "^3.3.12",
"webpack-dev-server": "^3.9.0", "webpack-dev-server": "^3.11.0",
"webpack-node-externals": "^1.6.0", "webpack-node-externals": "^2.5.0",
"workerize-loader": "^1.1.0", "workerize-loader": "^1.3.0",
"yaml-js": "^0.2.3" "yaml-js": "^0.2.3"
}, },
"peerDependencies": { "peerDependencies": {
@ -133,28 +135,29 @@
"styled-components": "^4.1.1" "styled-components": "^4.1.1"
}, },
"dependencies": { "dependencies": {
"@redocly/react-dropdown-aria": "^2.0.11",
"@types/node": "^13.11.1",
"classnames": "^2.2.6", "classnames": "^2.2.6",
"decko": "^1.2.0", "decko": "^1.2.0",
"dompurify": "^2.0.7", "dompurify": "^2.0.12",
"eventemitter3": "^4.0.0", "eventemitter3": "^4.0.4",
"json-pointer": "^0.6.0", "json-pointer": "^0.6.0",
"json-schema-ref-parser": "^6.1.0", "json-schema-ref-parser": "^6.1.0",
"lunr": "2.3.8", "lunr": "2.3.8",
"mark.js": "^8.11.1", "mark.js": "^8.11.1",
"marked": "^0.7.0", "marked": "^0.7.0",
"memoize-one": "~5.1.1", "memoize-one": "~5.1.1",
"mobx-react": "^6.1.4", "mobx-react": "^6.2.2",
"openapi-sampler": "1.0.0-beta.15", "openapi-sampler": "^1.0.0-beta.16",
"perfect-scrollbar": "^1.4.0", "perfect-scrollbar": "^1.4.0",
"polished": "^3.4.2", "polished": "^3.6.5",
"prismjs": "^1.17.1", "prismjs": "^1.20.0",
"prop-types": "^15.7.2", "prop-types": "^15.7.2",
"react-dropdown": "^1.6.4", "react-tabs": "^3.1.1",
"react-tabs": "^3.0.0", "slugify": "^1.4.4",
"slugify": "^1.3.6",
"stickyfill": "^1.1.1", "stickyfill": "^1.1.1",
"swagger2openapi": "^5.3.1", "swagger2openapi": "^6.2.1",
"tslib": "^1.10.0", "tslib": "^2.0.0",
"url-template": "^2.0.8" "url-template": "^2.0.8"
}, },
"bundlesize": [ "bundlesize": [

View File

@ -34,14 +34,14 @@ export class CopyButtonWrapper extends React.PureComponent<
renderCopyButton = () => { renderCopyButton = () => {
return ( return (
<span onClick={this.copy}> <button onClick={this.copy}>
<Tooltip <Tooltip
title={ClipboardService.isSupported() ? 'Copied' : 'Not supported in your browser'} title={ClipboardService.isSupported() ? 'Copied' : 'Not supported in your browser'}
open={this.state.tooltipShown} open={this.state.tooltipShown}
> >
Copy Copy
</Tooltip> </Tooltip>
</span> </button>
); );
}; };

View File

@ -82,9 +82,9 @@ export const PrismDiv = styled.div`
} }
} }
/* .property.token.string { .token.property.string {
color: white; color: white;
} */ }
.token.operator, .token.operator,
.token.entity, .token.entity,

View File

@ -1,70 +1,81 @@
import Dropdown from 'react-dropdown'; import Dropdown from '@redocly/react-dropdown-aria';
import styled from '../styled-components'; import styled from '../styled-components';
export interface DropdownOption { export interface DropdownOption {
label: string; idx: number;
value: string; value: string;
} }
export interface DropdownProps { export interface DropdownProps {
options: DropdownOption[]; options: DropdownOption[];
value: DropdownOption; value: string;
onChange: (val: DropdownOption) => void; onChange: (option: DropdownOption) => void;
ariaLabel: string;
} }
export const StyledDropdown = styled(Dropdown)` export const StyledDropdown = styled(Dropdown)`
&& {
box-sizing: border-box;
min-width: 100px; min-width: 100px;
outline: none;
display: inline-block; display: inline-block;
border-radius: 2px;
border: 1px solid rgba(38, 50, 56, 0.5);
vertical-align: bottom;
padding: 2px 0px 2px 6px;
position: relative; position: relative;
width: auto; width: auto;
font-family: ${props => props.theme.typography.headings.fontFamily}; background: white;
color: #263238;
.Dropdown-control { font-family: ${(props) => props.theme.typography.headings.fontFamily};
font-family: ${props => props.theme.typography.headings.fontFamily}; font-size: 0.929em;
line-height: 1.5em;
cursor: pointer;
transition: border 0.25s ease, color 0.25s ease, box-shadow 0.25s ease;
&:hover,
&:focus-within {
border: 1px solid ${(props) => props.theme.colors.primary.main};
color: ${(props) => props.theme.colors.primary.main};
box-shadow: 0px 0px 0px 1px ${(props) => props.theme.colors.primary.main};
}
.dropdown-selector {
display: inline-flex;
padding: 0;
height: auto;
padding-right: 20px;
position: relative;
margin-bottom: 5px;
}
.dropdown-selector-value {
font-family: ${(props) => props.theme.typography.headings.fontFamily};
position: relative; position: relative;
font-size: 0.929em; font-size: 0.929em;
width: 100%; width: 100%;
line-height: 1.5em; line-height: 1;
vertical-align: middle; vertical-align: middle;
cursor: pointer;
border-color: rgba(38, 50, 56, 0.5);
color: #263238; color: #263238;
outline: none; left: 0;
padding: 0.15em 1.5em 0.2em 0.5em; transition: color 0.25s ease, text-shadow 0.25s ease;
border-radius: 2px;
border-width: 1px;
border-style: solid;
margin-top: 5px;
background: white;
box-sizing: border-box;
&:hover {
border-color: ${props => props.theme.colors.primary.main};
color: ${props => props.theme.colors.primary.main};
box-shadow: 0px 2px 4px 0px rgba(34, 36, 38, 0.12);
} }
} .dropdown-arrow {
position: absolute;
.Dropdown-arrow { right: 3px;
border-color: ${props => props.theme.colors.primary.main} transparent transparent; top: 50%;
transform: translateY(-50%);
border-color: ${(props) => props.theme.colors.primary.main} transparent transparent;
border-style: solid; border-style: solid;
border-width: 0.35em 0.35em 0; border-width: 0.35em 0.35em 0;
content: ' ';
display: block;
height: 0;
position: absolute;
right: 0.3em;
top: 50%;
margin-top: -0.125em;
width: 0; width: 0;
svg {
display: none;
}
} }
.Dropdown-menu { .dropdown-selector-content {
position: absolute; position: absolute;
margin-top: 2px; margin-top: 2px;
left: 0; left: -2px;
right: 0; right: 0;
z-index: 10; z-index: 10;
@ -78,13 +89,14 @@ export const StyledDropdown = styled(Dropdown)`
overflow: auto; overflow: auto;
} }
.Dropdown-option { .dropdown-option {
font-size: 0.9em; font-size: 0.9em;
color: #263238; color: #263238;
cursor: pointer; cursor: pointer;
padding: 0.4em; padding: 0.4em;
background-color: #ffffff;
&.is-selected { &[aria-selected='true'] {
background-color: rgba(0, 0, 0, 0.05); background-color: rgba(0, 0, 0, 0.05);
} }
@ -92,22 +104,32 @@ export const StyledDropdown = styled(Dropdown)`
background-color: rgba(38, 50, 56, 0.12); background-color: rgba(38, 50, 56, 0.12);
} }
} }
input {
cursor: pointer;
height: 1px;
}
}
`; `;
export const SimpleDropdown = styled(StyledDropdown)` export const SimpleDropdown = styled(StyledDropdown)`
&& {
margin-left: 10px; margin-left: 10px;
text-transform: none; text-transform: none;
font-size: 0.969em; font-size: 0.969em;
.Dropdown-control {
font-size: 1em; font-size: 1em;
border: none; border: none;
padding: 0 1.2em 0 0; padding: 0 1.2em 0 0;
background: transparent; background: transparent;
&:hover { &:hover,
color: ${props => props.theme.colors.primary.main}; &:focus-within {
border: none;
box-shadow: none; box-shadow: none;
.dropdown-selector-value {
color: ${(props) => props.theme.colors.primary.main};
text-shadow: 0px 0px 0px ${(props) => props.theme.colors.primary.main};
}
} }
} }
`; `;

View File

@ -62,7 +62,7 @@ export const PropertyNameCell = styled(PropertyCell)`
vertical-align: top; vertical-align: top;
line-height: 20px; line-height: 20px;
white-space: nowrap; white-space: nowrap;
font-size: 0.929em; font-size: 13px;
font-family: ${props => props.theme.typography.code.fontFamily}; font-family: ${props => props.theme.typography.code.fontFamily};
&.deprecated { &.deprecated {

View File

@ -5,8 +5,19 @@ import { PropertyNameCell } from './fields-layout';
import { ShelfIcon } from './shelfs'; import { ShelfIcon } from './shelfs';
export const ClickablePropertyNameCell = styled(PropertyNameCell)` export const ClickablePropertyNameCell = styled(PropertyNameCell)`
button {
background-color: transparent;
border: 0;
outline: 0;
font-size: 13px;
font-family: ${props => props.theme.typography.code.fontFamily};
cursor: pointer; cursor: pointer;
padding: 0;
color: ${props => props.theme.colors.text.primary};
&:focus {
font-weight: ${({ theme }) => theme.typography.fontWeightBold};
}
}
${ShelfIcon} { ${ShelfIcon} {
height: ${({ theme }) => theme.schema.arrow.size}; height: ${({ theme }) => theme.schema.arrow.size};
width: ${({ theme }) => theme.schema.arrow.size}; width: ${({ theme }) => theme.schema.arrow.size};
@ -97,3 +108,14 @@ export const ConstraintItem = styled(FieldLabel)`
} }
${extensionsHook('ConstraintItem')}; ${extensionsHook('ConstraintItem')};
`; `;
export const ToggleButton = styled.button`
background-color: transparent;
border: 0;
color: ${({ theme }) => theme.colors.text.secondary};
margin-left: ${({ theme }) => theme.spacing.unit}px;
border-radius: 2px;
cursor: pointer;
outline-color: ${({ theme }) => theme.colors.text.secondary};
font-size: 12px;
`;

View File

@ -14,6 +14,7 @@ export const linkifyMixin = className => css`
line-height: 1; line-height: 1;
width: 20px; width: 20px;
display: inline-block; display: inline-block;
outline: 0;
} }
${className}:before { ${className}:before {
content: ''; content: '';

View File

@ -80,6 +80,7 @@ export function PerfectScrollbarWrap(
<div <div
style={{ style={{
overflow: 'auto', overflow: 'auto',
overscrollBehavior: 'contain',
msOverflowStyle: '-ms-autohiding-scrollbar', msOverflowStyle: '-ms-autohiding-scrollbar',
}} }}
> >

View File

@ -5,13 +5,22 @@ export const SampleControls = styled.div`
opacity: 0.4; opacity: 0.4;
transition: opacity 0.3s ease; transition: opacity 0.3s ease;
text-align: right; text-align: right;
&:focus-within {
> span { opacity: 1;
display: inline-block; }
> button {
background-color: transparent;
border: 0;
color: inherit;
padding: 2px 10px; padding: 2px 10px;
font-family: ${({ theme }) => theme.typography.fontFamily};
font-size: ${({ theme }) => theme.typography.fontSize};
line-height: ${({ theme }) => theme.typography.lineHeight};
cursor: pointer; cursor: pointer;
outline: 0;
:hover { :hover,
:focus {
background: rgba(255, 255, 255, 0.1); background: rgba(255, 255, 255, 0.1);
} }
} }

View File

@ -1,9 +1,8 @@
import styled from '../styled-components'; import styled from '../styled-components';
import { darken } from 'polished';
export const OneOfList = styled.ul` export const OneOfList = styled.div`
margin: 0 0 3px 0; margin: 0 0 3px 0;
padding: 0;
list-style: none;
display: inline-block; display: inline-block;
`; `;
@ -15,7 +14,7 @@ export const OneOfLabel = styled.span`
} }
`; `;
export const OneOfButton = styled.li<{ active: boolean }>` export const OneOfButton = styled.button<{ active: boolean }>`
display: inline-block; display: inline-block;
margin-right: 10px; margin-right: 10px;
margin-bottom: 5px; margin-bottom: 5px;
@ -23,12 +22,21 @@ export const OneOfButton = styled.li<{ active: boolean }>`
cursor: pointer; cursor: pointer;
border: 1px solid ${props => props.theme.colors.primary.main}; border: 1px solid ${props => props.theme.colors.primary.main};
padding: 2px 10px; padding: 2px 10px;
line-height: 1.5em;
outline: none;
&:focus {
box-shadow: 0 0 0 1px ${props => props.theme.colors.primary.main};
}
${props => { ${props => {
if (props.active) { if (props.active) {
return ` return `
color: white; color: white;
background-color: ${props.theme.colors.primary.main}; background-color: ${props.theme.colors.primary.main};
&:focus {
box-shadow: none;
background-color: ${darken(0.15, props.theme.colors.primary.main)};
}
`; `;
} else { } else {
return ` return `

View File

@ -26,6 +26,7 @@ class IntShelfIcon extends React.PureComponent<{
x="0" x="0"
xmlns="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg"
y="0" y="0"
aria-hidden="true"
> >
<polygon points="17.3 8.3 12 13.6 6.7 8.3 5.3 9.7 12 16.4 18.7 9.7 " /> <polygon points="17.3 8.3 12 13.6 6.7 8.3 5.3 9.7 12 16.4 18.7 9.7 " />
</svg> </svg>
@ -50,10 +51,17 @@ export const ShelfIcon = styled(IntShelfIcon)`
export const Badge = styled.span<{ type: string }>` export const Badge = styled.span<{ type: string }>`
display: inline-block; display: inline-block;
padding: 0 5px; padding: 2px 8px;
margin: 0; margin: 0;
background-color: ${props => props.theme.colors[props.type].main}; background-color: ${props => props.theme.colors[props.type].main};
color: ${props => props.theme.colors[props.type].contrastText}; color: ${props => props.theme.colors[props.type].contrastText};
font-size: ${props => props.theme.typography.code.fontSize}; font-size: ${props => props.theme.typography.code.fontSize};
vertical-align: text-top; vertical-align: middle;
line-height: 1.6;
border-radius: 4px;
font-weight: ${({ theme }) => theme.typography.fontWeightBold};
font-size: 12px;
+ span[type] {
margin-left: 4px;
}
`; `;

View File

@ -16,7 +16,7 @@ export const Tabs = styled(ReactTabs)`
padding: 5px 10px; padding: 5px 10px;
display: inline-block; display: inline-block;
background-color: ${({ theme }) => theme.codeSample.backgroundColor}; background-color: ${({ theme }) => theme.codeBlock.backgroundColor};
border-bottom: 1px solid rgba(0, 0, 0, 0.5); border-bottom: 1px solid rgba(0, 0, 0, 0.5);
cursor: pointer; cursor: pointer;
text-align: center; text-align: center;
@ -24,7 +24,7 @@ export const Tabs = styled(ReactTabs)`
color: ${({ theme }) => darken(theme.colors.tonalOffset, theme.rightPanel.textColor)}; color: ${({ theme }) => darken(theme.colors.tonalOffset, theme.rightPanel.textColor)};
margin: 0 margin: 0
${({ theme }) => `${theme.spacing.unit}px ${theme.spacing.unit}px ${theme.spacing.unit}px`}; ${({ theme }) => `${theme.spacing.unit}px ${theme.spacing.unit}px ${theme.spacing.unit}px`};
border: 1px solid ${({ theme }) => darken(0.05, theme.codeSample.backgroundColor)}; border: 1px solid ${({ theme }) => darken(0.05, theme.codeBlock.backgroundColor)};
border-radius: 5px; border-radius: 5px;
min-width: 60px; min-width: 60px;
font-size: 0.9em; font-size: 0.9em;
@ -33,6 +33,9 @@ export const Tabs = styled(ReactTabs)`
&.react-tabs__tab--selected { &.react-tabs__tab--selected {
color: ${props => props.theme.colors.text.primary}; color: ${props => props.theme.colors.text.primary};
background: ${({ theme }) => theme.rightPanel.textColor}; background: ${({ theme }) => theme.rightPanel.textColor};
&:focus {
outline: auto;
}
} }
&:only-child { &:only-child {
@ -58,7 +61,7 @@ export const Tabs = styled(ReactTabs)`
} }
} }
> .react-tabs__tab-panel { > .react-tabs__tab-panel {
background: ${({ theme }) => theme.codeSample.backgroundColor}; background: ${({ theme }) => theme.codeBlock.backgroundColor};
& > div, & > div,
& > pre { & > pre {
padding: ${props => props.theme.spacing.unit * 4}px; padding: ${props => props.theme.spacing.unit * 4}px;

View File

@ -81,7 +81,7 @@ export class ApiInfo extends React.Component<ApiInfoProps> {
<p> <p>
Download OpenAPI specification: Download OpenAPI specification:
<DownloadButton <DownloadButton
download={downloadFilename} download={downloadFilename || true}
target="_blank" target="_blank"
href={downloadLink} href={downloadLink}
onClick={this.handleDownloadClick} onClick={this.handleDownloadClick}
@ -100,7 +100,7 @@ export class ApiInfo extends React.Component<ApiInfoProps> {
)) || )) ||
null} null}
</StyledMarkdownBlock> </StyledMarkdownBlock>
<Markdown source={store.spec.info.description} /> <Markdown source={store.spec.info.description} data-role="redoc-description" />
{externalDocs && <ExternalDocumentation externalDocs={externalDocs} />} {externalDocs && <ExternalDocumentation externalDocs={externalDocs} />}
</MiddlePanel> </MiddlePanel>
</Row> </Row>

View File

@ -0,0 +1,35 @@
import * as React from 'react';
import styled from '../../styled-components';
import { DropdownProps } from '../../common-elements';
import { PayloadSamples } from '../PayloadSamples/PayloadSamples';
import { OperationModel } from '../../services/models';
import { XPayloadSample } from '../../services/models/Operation';
import { isPayloadSample } from '../../services';
export interface PayloadSampleProps {
callback: OperationModel;
renderDropdown: (props: DropdownProps) => JSX.Element;
}
export class CallbackPayloadSample extends React.Component<PayloadSampleProps> {
render() {
const payloadSample = this.props.callback.codeSamples.find(sample =>
isPayloadSample(sample),
) as XPayloadSample | undefined;
if (!payloadSample) {
return null;
}
return (
<PayloadSampleWrapper>
<PayloadSamples content={payloadSample.requestBodyContent} />
</PayloadSampleWrapper>
);
}
}
export const PayloadSampleWrapper = styled.div`
margin-top: 15px;
`;

View File

@ -0,0 +1,79 @@
import { observer } from 'mobx-react';
import * as React from 'react';
import styled from '../../styled-components';
import { RightPanelHeader } from '../../common-elements';
import { RedocNormalizedOptions } from '../../services';
import { CallbackModel } from '../../services/models';
import { OptionsContext } from '../OptionsProvider';
import { GenericChildrenSwitcher } from '../GenericChildrenSwitcher/GenericChildrenSwitcher';
import { DropdownOrLabel } from '../DropdownOrLabel/DropdownOrLabel';
import { InvertedSimpleDropdown, MimeLabel } from '../PayloadSamples/styled.elements';
import { CallbackPayloadSample } from './CallbackReqSamples';
export interface CallbackSamplesProps {
callbacks: CallbackModel[];
}
@observer
export class CallbackSamples extends React.Component<CallbackSamplesProps> {
static contextType = OptionsContext;
context: RedocNormalizedOptions;
private renderDropdown = props => {
return <DropdownOrLabel Label={MimeLabel} Dropdown={InvertedSimpleDropdown} {...props} />;
};
render() {
const { callbacks } = this.props;
if (!callbacks || callbacks.length === 0) {
return null;
}
const operations = callbacks
.map(callback => callback.operations.map(operation => operation))
.reduce((a, b) => a.concat(b), []);
const hasSamples = operations.some(operation => operation.codeSamples.length > 0);
if (!hasSamples) {
return null;
}
const dropdownOptions = operations.map((callback, idx) => {
return {
value: `${callback.httpVerb.toUpperCase()}: ${callback.name}`,
idx,
};
});
return (
<div>
<RightPanelHeader> Callback payload samples </RightPanelHeader>
<SamplesWrapper>
<GenericChildrenSwitcher
items={operations}
renderDropdown={this.renderDropdown}
label={'Callback'}
options={dropdownOptions}
>
{callback => (
<CallbackPayloadSample
key="callbackPayloadSample"
callback={callback}
renderDropdown={this.renderDropdown}
/>
)}
</GenericChildrenSwitcher>
</SamplesWrapper>
</div>
);
}
}
export const SamplesWrapper = styled.div`
background: ${({ theme }) => theme.codeBlock.backgroundColor};
padding: ${props => props.theme.spacing.unit * 4}px;
`;

View File

@ -0,0 +1,46 @@
import { observer } from 'mobx-react';
import * as React from 'react';
import { OperationModel } from '../../services/models';
import styled from '../../styled-components';
import { Endpoint } from '../Endpoint/Endpoint';
import { ExternalDocumentation } from '../ExternalDocumentation/ExternalDocumentation';
import { Extensions } from '../Fields/Extensions';
import { Markdown } from '../Markdown/Markdown';
import { Parameters } from '../Parameters/Parameters';
import { ResponsesList } from '../Responses/ResponsesList';
import { SecurityRequirements } from '../SecurityRequirement/SecurityRequirement';
import { CallbackDetailsWrap } from './styled.elements';
export interface CallbackDetailsProps {
operation: OperationModel;
}
@observer
export class CallbackDetails extends React.Component<CallbackDetailsProps> {
render() {
const { operation } = this.props;
const { description, externalDocs } = operation;
const hasDescription = !!(description || externalDocs);
return (
<CallbackDetailsWrap>
{hasDescription && (
<Description>
{description !== undefined && <Markdown source={description} />}
{externalDocs && <ExternalDocumentation externalDocs={externalDocs} />}
</Description>
)}
<Endpoint operation={this.props.operation} inverted={true} compact={true} />
<Extensions extensions={operation.extensions} />
<SecurityRequirements securities={operation.security} />
<Parameters parameters={operation.parameters} body={operation.requestBody} />
<ResponsesList responses={operation.responses} isCallback={operation.isCallback} />
</CallbackDetailsWrap>
);
}
}
const Description = styled.div`
margin-bottom: ${({ theme }) => theme.spacing.unit * 3}px;
`;

View File

@ -0,0 +1,30 @@
import { observer } from 'mobx-react';
import * as React from 'react';
import { OperationModel } from '../../services/models';
import { StyledCallbackTitle } from './styled.elements';
import { CallbackDetails } from './CallbackDetails';
@observer
export class CallbackOperation extends React.Component<{ callbackOperation: OperationModel }> {
toggle = () => {
this.props.callbackOperation.toggle();
};
render() {
const { name, expanded, httpVerb, deprecated } = this.props.callbackOperation;
return (
<>
<StyledCallbackTitle
onClick={this.toggle}
name={name}
opened={expanded}
httpVerb={httpVerb}
deprecated={deprecated}
/>
{expanded && <CallbackDetails operation={this.props.callbackOperation} />}
</>
);
}
}

View File

@ -0,0 +1,57 @@
import * as React from 'react';
import { darken } from 'polished';
import { ShelfIcon } from '../../common-elements';
import { OperationBadge } from '../SideMenu/styled.elements';
import { shortenHTTPVerb } from '../../utils/openapi';
import styled from '../../styled-components';
import { Badge } from '../../common-elements/';
import { l } from '../../services/Labels';
export interface CallbackTitleProps {
name: string;
opened?: boolean;
httpVerb: string;
deprecated?: boolean;
className?: string;
onClick?: () => void;
}
export class CallbackTitle extends React.PureComponent<CallbackTitleProps> {
render() {
const { name, opened, className, onClick, httpVerb, deprecated } = this.props;
return (
<CallbackTitleWrapper className={className} onClick={onClick || undefined}>
<OperationBadgeStyled type={httpVerb}>{shortenHTTPVerb(httpVerb)}</OperationBadgeStyled>
<ShelfIcon size={'1.5em'} direction={opened ? 'down' : 'right'} float={'left'} />
<CallbackName deprecated={deprecated}>{name}</CallbackName>
{deprecated ? <Badge type="warning"> {l('deprecated')} </Badge> : null}
</CallbackTitleWrapper>
);
}
}
const CallbackTitleWrapper = styled.button`
border: 0;
width: 100%;
text-align: left;
& > * {
vertical-align: middle;
}
${ShelfIcon} {
polygon {
fill: ${({ theme }) => darken(theme.colors.tonalOffset, theme.colors.gray[100])};
}
}
`;
const CallbackName = styled.span<{ deprecated?: boolean }>`
text-decoration: ${(props) => (props.deprecated ? 'line-through' : 'none')};
margin-right: 8px;
`;
const OperationBadgeStyled = styled(OperationBadge)`
margin: 0px 5px 0px 0px;
`;

View File

@ -0,0 +1,40 @@
import * as React from 'react';
import { CallbackModel } from '../../services/models';
import styled from '../../styled-components';
import { CallbackOperation } from './CallbackOperation';
export interface CallbacksListProps {
callbacks: CallbackModel[];
}
export class CallbacksList extends React.PureComponent<CallbacksListProps> {
render() {
const { callbacks } = this.props;
if (!callbacks || callbacks.length === 0) {
return null;
}
return (
<div>
<CallbacksHeader> Callbacks </CallbacksHeader>
{callbacks.map(callback => {
return callback.operations.map((operation, index) => {
return (
<CallbackOperation key={`${callback.name}_${index}`} callbackOperation={operation} />
);
});
})}
</div>
);
}
}
const CallbacksHeader = styled.h3`
font-size: 1.3em;
padding: 0.2em 0;
margin: 3em 0 1.1em;
color: ${({ theme }) => theme.colors.text.primary};
font-weight: normal;
`;

View File

@ -0,0 +1,3 @@
export * from './CallbackOperation';
export * from './CallbackTitle';
export * from './CallbacksList';

View File

@ -0,0 +1,20 @@
import styled from '../../styled-components';
import { CallbackTitle } from './CallbackTitle';
import { darken } from 'polished';
export const StyledCallbackTitle = styled(CallbackTitle)`
padding: 10px;
border-radius: 2px;
margin-bottom: 4px;
line-height: 1.5em;
background-color: ${({ theme }) => theme.colors.gray[100]};
cursor: pointer;
outline-color: ${({ theme }) => darken(theme.colors.tonalOffset, theme.colors.gray[100])};
`;
export const CallbackDetailsWrap = styled.div`
padding: 10px 25px;
background-color: ${({ theme }) => theme.colors.gray[50]};
margin-bottom: 5px;
margin-top: 5px;
`;

View File

@ -3,7 +3,6 @@ import * as React from 'react';
import { ExternalDocumentation } from '../ExternalDocumentation/ExternalDocumentation'; import { ExternalDocumentation } from '../ExternalDocumentation/ExternalDocumentation';
import { AdvancedMarkdown } from '../Markdown/AdvancedMarkdown'; import { AdvancedMarkdown } from '../Markdown/AdvancedMarkdown';
import { H1, H2, MiddlePanel, Row, Section, ShareLink } from '../../common-elements'; import { H1, H2, MiddlePanel, Row, Section, ShareLink } from '../../common-elements';
import { ContentItemModel } from '../../services/MenuBuilder'; import { ContentItemModel } from '../../services/MenuBuilder';
import { GroupModel, OperationModel } from '../../services/models'; import { GroupModel, OperationModel } from '../../services/models';
@ -24,6 +23,9 @@ export class ContentItems extends React.Component<{
return item.type == "tag" ? item.name == `${activeSelection}` : true; return item.type == "tag" ? item.name == `${activeSelection}` : true;
}); });
return filteredItems.map(item => <ContentItem item={item} key={item.id} />); return filteredItems.map(item => <ContentItem item={item} key={item.id} />);
// return items.map(item => {
// return <ContentItem key={item.id} item={item} />;
// });
} }
} }

View File

@ -10,7 +10,7 @@ export interface DropdownOrLabelProps extends DropdownProps {
export function DropdownOrLabel(props: DropdownOrLabelProps): JSX.Element { export function DropdownOrLabel(props: DropdownOrLabelProps): JSX.Element {
const { Label = MimeLabel, Dropdown = SimpleDropdown } = props; const { Label = MimeLabel, Dropdown = SimpleDropdown } = props;
if (props.options.length === 1) { if (props.options.length === 1) {
return <Label>{props.options[0].label}</Label>; return <Label>{props.options[0].value}</Label>;
} }
return <Dropdown {...props} />; return <Dropdown {...props} searchable={false} />;
} }

View File

@ -21,6 +21,7 @@ export interface EndpointProps {
hideHostname?: boolean; hideHostname?: boolean;
inverted?: boolean; inverted?: boolean;
compact?: boolean;
} }
export interface EndpointState { export interface EndpointState {
@ -49,7 +50,9 @@ export class Endpoint extends React.Component<EndpointProps, EndpointState> {
{options => ( {options => (
<OperationEndpointWrap> <OperationEndpointWrap>
<EndpointInfo onClick={this.toggle} expanded={expanded} inverted={inverted}> <EndpointInfo onClick={this.toggle} expanded={expanded} inverted={inverted}>
<HttpVerb type={operation.httpVerb}> {operation.httpVerb}</HttpVerb>{' '} <HttpVerb type={operation.httpVerb} compact={this.props.compact}>
{operation.httpVerb}
</HttpVerb>
<ServerRelativeURL>{operation.path}</ServerRelativeURL> <ServerRelativeURL>{operation.path}</ServerRelativeURL>
<ShelfIcon <ShelfIcon
float={'right'} float={'right'}
@ -59,7 +62,7 @@ export class Endpoint extends React.Component<EndpointProps, EndpointState> {
style={{ marginRight: '-25px' }} style={{ marginRight: '-25px' }}
/> />
</EndpointInfo> </EndpointInfo>
<ServersOverlay expanded={expanded}> <ServersOverlay expanded={expanded} aria-hidden={!expanded}>
{operation.servers.map(server => { {operation.servers.map(server => {
const normalizedUrl = options.expandDefaultServerVariables const normalizedUrl = options.expandDefaultServerVariables
? expandDefaultServerVariables(server.url, server.variables) ? expandDefaultServerVariables(server.url, server.variables)

View File

@ -14,11 +14,16 @@ export const ServerRelativeURL = styled.span`
text-overflow: ellipsis; text-overflow: ellipsis;
`; `;
export const EndpointInfo = styled.div<{ expanded?: boolean; inverted?: boolean }>` export const EndpointInfo = styled.button<{ expanded?: boolean; inverted?: boolean }>`
outline: 0;
color: inherit;
width: 100%;
text-align: left;
cursor: pointer;
padding: 10px 30px 10px ${props => (props.inverted ? '10px' : '20px')}; padding: 10px 30px 10px ${props => (props.inverted ? '10px' : '20px')};
border-radius: ${props => (props.inverted ? '0' : '4px 4px 0 0')}; border-radius: ${props => (props.inverted ? '0' : '4px 4px 0 0')};
background-color: ${props => background-color: ${props =>
props.inverted ? 'transparent' : props.theme.codeSample.backgroundColor}; props.inverted ? 'transparent' : props.theme.codeBlock.backgroundColor};
display: flex; display: flex;
white-space: nowrap; white-space: nowrap;
align-items: center; align-items: center;
@ -32,16 +37,19 @@ export const EndpointInfo = styled.div<{ expanded?: boolean; inverted?: boolean
.${ServerRelativeURL} { .${ServerRelativeURL} {
color: ${props => (props.inverted ? props.theme.colors.text.primary : '#ffffff')} color: ${props => (props.inverted ? props.theme.colors.text.primary : '#ffffff')}
} }
&:focus {
box-shadow: inset 0 2px 2px rgba(0, 0, 0, 0.45), 0 2px 0 rgba(128, 128, 128, 0.25);
}
`; `;
export const HttpVerb = styled.span.attrs((props: { type: string }) => ({ export const HttpVerb = styled.span.attrs((props: { type: string; compact?: boolean }) => ({
className: `http-verb ${props.type}`, className: `http-verb ${props.type}`,
}))<{ type: string }>` }))<{ type: string; compact?: boolean }>`
font-size: 0.929em; font-size: ${props => (props.compact ? '0.8em' : '0.929em')};
line-height: 20px; line-height: ${props => (props.compact ? '18px' : '20px')};
background-color: ${(props: any) => props.theme.colors.http[props.type] || '#999999'}; background-color: ${props => props.theme.colors.http[props.type] || '#999999'};
color: #ffffff; color: #ffffff;
padding: 3px 10px; padding: ${props => (props.compact ? '2px 8px' : '3px 10px')};
text-transform: uppercase; text-transform: uppercase;
font-family: ${props => props.theme.typography.headings.fontFamily}; font-family: ${props => props.theme.typography.headings.fontFamily};
margin: 0; margin: 0;
@ -59,8 +67,8 @@ export const ServersOverlay = styled.div<{ expanded: boolean }>`
border-bottom-left-radius: 4px; border-bottom-left-radius: 4px;
border-bottom-right-radius: 4px; border-bottom-right-radius: 4px;
transition: all 0.25s ease; transition: all 0.25s ease;
visibility: hidden;
${props => (props.expanded ? '' : 'transform: translateY(-50%) scaleY(0);')} ${props => (props.expanded ? 'visibility: visible;' : 'transform: translateY(-50%) scaleY(0);')}
`; `;
export const ServerItem = styled.div` export const ServerItem = styled.div`

View File

@ -6,7 +6,10 @@ const ErrorWrapper = styled.div`
color: red; color: red;
`; `;
export class ErrorBoundary extends React.Component<{}, { error?: Error }> { export class ErrorBoundary extends React.Component<
React.PropsWithChildren<unknown>,
{ error?: Error }
> {
constructor(props) { constructor(props) {
super(props); super(props);
this.state = { error: undefined }; this.state = { error: undefined };

View File

@ -3,28 +3,62 @@ import { ExampleValue, FieldLabel } from '../../common-elements/fields';
import { l } from '../../services/Labels'; import { l } from '../../services/Labels';
import { OptionsContext } from '../OptionsProvider'; import { OptionsContext } from '../OptionsProvider';
import styled from '../../styled-components';
import { RedocRawOptions } from '../../services/RedocNormalizedOptions';
export interface EnumValuesProps { export interface EnumValuesProps {
values: string[]; values: string[];
type: string; type: string;
} }
export class EnumValues extends React.PureComponent<EnumValuesProps> { export interface EnumValuesState {
collapsed: boolean;
}
export class EnumValues extends React.PureComponent<EnumValuesProps, EnumValuesState> {
state: EnumValuesState = {
collapsed: true,
};
static contextType = OptionsContext; static contextType = OptionsContext;
private toggle() {
this.setState({ collapsed: !this.state.collapsed });
}
render() { render() {
const { values, type } = this.props; const { values, type } = this.props;
const { enumSkipQuotes } = this.context; const { collapsed } = this.state;
// TODO: provide context interface in more elegant way
const { enumSkipQuotes, maxDisplayedEnumValues } = this.context as RedocRawOptions;
if (!values.length) { if (!values.length) {
return null; return null;
} }
const displayedItems =
this.state.collapsed && maxDisplayedEnumValues
? values.slice(0, maxDisplayedEnumValues)
: values;
const showToggleButton = maxDisplayedEnumValues
? values.length > maxDisplayedEnumValues
: false;
const toggleButtonText = maxDisplayedEnumValues
? collapsed
? `${values.length - maxDisplayedEnumValues} more`
: 'Hide'
: '';
return ( return (
<div> <div>
<FieldLabel> <FieldLabel>
{type === 'array' ? l('enumArray') : ''}{' '} {type === 'array' ? l('enumArray') : ''}{' '}
{values.length === 1 ? l('enumSingleValue') : l('enum')}: {values.length === 1 ? l('enumSingleValue') : l('enum')}:
</FieldLabel>{' '} </FieldLabel>{' '}
{values.map((value, idx) => { {displayedItems.map((value, idx) => {
const exampleValue = enumSkipQuotes ? value : JSON.stringify(value); const exampleValue = enumSkipQuotes ? value : JSON.stringify(value);
return ( return (
<React.Fragment key={idx}> <React.Fragment key={idx}>
@ -32,7 +66,25 @@ export class EnumValues extends React.PureComponent<EnumValuesProps> {
</React.Fragment> </React.Fragment>
); );
})} })}
{showToggleButton ? (
<ToggleButton
onClick={() => {
this.toggle();
}}
>
{toggleButtonText}
</ToggleButton>
) : null}
</div> </div>
); );
} }
} }
const ToggleButton = styled.span`
color: ${props => props.theme.colors.primary.main};
vertical-align: middle;
font-size: 13px;
line-height: 20px;
padding: 0 5px;
cursor: pointer;
`;

View File

@ -23,6 +23,7 @@ export interface FieldProps extends SchemaOptions {
showExamples?: boolean; showExamples?: boolean;
field: FieldModel; field: FieldModel;
expandByDefault?: boolean;
renderDiscriminatorSwitch?: (opts: FieldProps) => JSX.Element; renderDiscriminatorSwitch?: (opts: FieldProps) => JSX.Element;
} }
@ -30,23 +31,42 @@ export interface FieldProps extends SchemaOptions {
@observer @observer
export class Field extends React.Component<FieldProps> { export class Field extends React.Component<FieldProps> {
toggle = () => { toggle = () => {
if (this.props.field.expanded === undefined && this.props.expandByDefault) {
this.props.field.expanded = false;
} else {
this.props.field.toggle(); this.props.field.toggle();
}
}; };
handleKeyPress = e => {
if (e.key === 'Enter') {
e.preventDefault();
this.toggle();
}
};
render() { render() {
const { className, field, isLast } = this.props; const { className, field, isLast, expandByDefault } = this.props;
const { name, expanded, deprecated, required, kind } = field; const { name, deprecated, required, kind } = field;
const withSubSchema = !field.schema.isPrimitive && !field.schema.isCircular; const withSubSchema = !field.schema.isPrimitive && !field.schema.isCircular;
const expanded = field.expanded === undefined ? expandByDefault : field.expanded;
const paramName = withSubSchema ? ( const paramName = withSubSchema ? (
<ClickablePropertyNameCell <ClickablePropertyNameCell
onClick={this.toggle}
className={deprecated ? 'deprecated' : ''} className={deprecated ? 'deprecated' : ''}
kind={kind} kind={kind}
title={name} title={name}
> >
<PropertyBullet /> <PropertyBullet />
<button
onClick={this.toggle}
onKeyPress={this.handleKeyPress}
aria-label="expand properties"
>
{name} {name}
<ShelfIcon direction={expanded ? 'down' : 'right'} /> <ShelfIcon direction={expanded ? 'down' : 'right'} />
</button>
{required && <RequiredLabel> required </RequiredLabel>} {required && <RequiredLabel> required </RequiredLabel>}
</ClickablePropertyNameCell> </ClickablePropertyNameCell>
) : ( ) : (
@ -65,7 +85,7 @@ export class Field extends React.Component<FieldProps> {
<FieldDetails {...this.props} /> <FieldDetails {...this.props} />
</PropertyDetailsCell> </PropertyDetailsCell>
</tr> </tr>
{field.expanded && withSubSchema && ( {expanded && withSubSchema && (
<tr key={field.name + 'inner'}> <tr key={field.name + 'inner'}>
<PropertyCellWithInner colSpan={2}> <PropertyCellWithInner colSpan={2}>
<InnerPropertiesWrap> <InnerPropertiesWrap>

View File

@ -8,6 +8,7 @@ import {
TypeName, TypeName,
TypePrefix, TypePrefix,
TypeTitle, TypeTitle,
ToggleButton,
} from '../../common-elements/fields'; } from '../../common-elements/fields';
import { serializeParameterValue } from '../../utils/openapi'; import { serializeParameterValue } from '../../utils/openapi';
import { ExternalDocumentation } from '../ExternalDocumentation/ExternalDocumentation'; import { ExternalDocumentation } from '../ExternalDocumentation/ExternalDocumentation';
@ -23,10 +24,24 @@ import { Badge } from '../../common-elements/';
import { l } from '../../services/Labels'; import { l } from '../../services/Labels';
import { OptionsContext } from '../OptionsProvider'; import { OptionsContext } from '../OptionsProvider';
export class FieldDetails extends React.PureComponent<FieldProps> { const MAX_PATTERN_LENGTH = 45;
export class FieldDetails extends React.PureComponent<FieldProps, { patternShown: boolean }> {
state = {
patternShown: false,
};
static contextType = OptionsContext; static contextType = OptionsContext;
togglePattern = () => {
this.setState({
patternShown: !this.state.patternShown,
});
};
render() { render() {
const { showExamples, field, renderDiscriminatorSwitch } = this.props; const { showExamples, field, renderDiscriminatorSwitch } = this.props;
const { patternShown } = this.state;
const { enumSkipQuotes, hideSchemaTitles } = this.context; const { enumSkipQuotes, hideSchemaTitles } = this.context;
const { schema, description, example, deprecated } = field; const { schema, description, example, deprecated } = field;
@ -62,7 +77,20 @@ export class FieldDetails extends React.PureComponent<FieldProps> {
{schema.title && !hideSchemaTitles && <TypeTitle> ({schema.title}) </TypeTitle>} {schema.title && !hideSchemaTitles && <TypeTitle> ({schema.title}) </TypeTitle>}
<ConstraintsView constraints={schema.constraints} /> <ConstraintsView constraints={schema.constraints} />
{schema.nullable && <NullableLabel> {l('nullable')} </NullableLabel>} {schema.nullable && <NullableLabel> {l('nullable')} </NullableLabel>}
{schema.pattern && <PatternLabel> {schema.pattern} </PatternLabel>} {schema.pattern && (
<>
<PatternLabel>
{patternShown || schema.pattern.length < MAX_PATTERN_LENGTH
? schema.pattern
: `${schema.pattern.substr(0, MAX_PATTERN_LENGTH)}...`}
</PatternLabel>
{schema.pattern.length > MAX_PATTERN_LENGTH && (
<ToggleButton onClick={this.togglePattern}>
{patternShown ? 'Hide pattern' : 'Show pattern'}
</ToggleButton>
)}
</>
)}
{schema.isCircular && <RecursiveLabel> {l('recursive')} </RecursiveLabel>} {schema.isCircular && <RecursiveLabel> {l('recursive')} </RecursiveLabel>}
</div> </div>
{deprecated && ( {deprecated && (

View File

@ -0,0 +1,75 @@
import { observer } from 'mobx-react';
import * as React from 'react';
import { DropdownProps, DropdownOption } from '../../common-elements/dropdown';
import { DropdownLabel, DropdownWrapper } from '../PayloadSamples/styled.elements';
export interface GenericChildrenSwitcherProps<T> {
items?: T[];
options: DropdownOption[];
label?: string;
renderDropdown: (props: DropdownProps) => JSX.Element;
children: (activeItem: T) => JSX.Element;
}
export interface GenericChildrenSwitcherState {
activeItemIdx: number;
}
/**
* TODO: Refactor this component:
* Implement rendering dropdown/label directly in this component
* Accept as a parameter mapper-function for building dropdown option labels
*/
@observer
export class GenericChildrenSwitcher<T> extends React.Component<
GenericChildrenSwitcherProps<T>,
GenericChildrenSwitcherState
> {
constructor(props) {
super(props);
this.state = {
activeItemIdx: 0,
};
}
switchItem = ({ idx }) => {
if (this.props.items) {
this.setState({
activeItemIdx: idx,
});
}
};
render() {
const { items } = this.props;
if (!items || !items.length) {
return null;
}
const Wrapper = ({ children }) =>
this.props.label ? (
<DropdownWrapper>
<DropdownLabel>{this.props.label}</DropdownLabel>
{children}
</DropdownWrapper>
) : (
children
);
return (
<>
<Wrapper>
{this.props.renderDropdown({
value: this.props.options[this.state.activeItemIdx].value,
options: this.props.options,
onChange: this.switchItem,
ariaLabel: this.props.label || 'Callback',
})}
</Wrapper>
{this.props.children(items[this.state.activeItemIdx])}
</>
);
}
}

View File

@ -30,8 +30,8 @@ class Json extends React.PureComponent<JsonProps> {
<JsonViewerWrap> <JsonViewerWrap>
<SampleControls> <SampleControls>
{renderCopyButton()} {renderCopyButton()}
<span onClick={this.expandAll}> Expand all </span> <button onClick={this.expandAll}> Expand all </button>
<span onClick={this.collapseAll}> Collapse all </span> <button onClick={this.collapseAll}> Collapse all </button>
</SampleControls> </SampleControls>
<OptionsContext.Consumer> <OptionsContext.Consumer>
{options => ( {options => (
@ -57,11 +57,10 @@ class Json extends React.PureComponent<JsonProps> {
collapseAll = () => { collapseAll = () => {
const elements = this.node.getElementsByClassName('collapsible'); const elements = this.node.getElementsByClassName('collapsible');
for (const expanded of Array.prototype.slice.call(elements)) { // skip first item to avoid collapsing whole object/array
// const collapsed = elements[i]; const elementsArr = Array.prototype.slice.call(elements, 1);
if ((expanded.parentNode as Element)!.classList.contains('redoc-json')) {
continue; for (const expanded of elementsArr) {
}
(expanded.parentNode as Element)!.classList.add('collapsed'); (expanded.parentNode as Element)!.classList.add('collapsed');
} }
}; };

View File

@ -17,11 +17,12 @@ export type MarkdownProps = BaseMarkdownProps &
StylingMarkdownProps & { StylingMarkdownProps & {
source: string; source: string;
className?: string; className?: string;
'data-role'?: string;
}; };
export class Markdown extends React.Component<MarkdownProps> { export class Markdown extends React.Component<MarkdownProps> {
render() { render() {
const { source, inline, compact, className } = this.props; const { source, inline, compact, className, 'data-role': dataRole } = this.props;
const renderer = new MarkdownRenderer(); const renderer = new MarkdownRenderer();
return ( return (
<SanitizedMarkdownHTML <SanitizedMarkdownHTML
@ -29,6 +30,7 @@ export class Markdown extends React.Component<MarkdownProps> {
inline={inline} inline={inline}
compact={compact} compact={compact}
className={className} className={className}
data-role={dataRole}
/> />
); );
} }

View File

@ -10,7 +10,7 @@ const StyledMarkdownSpan = StyledMarkdownBlock.withComponent('span');
const sanitize = (untrustedSpec, html) => (untrustedSpec ? DOMPurify.sanitize(html) : html); const sanitize = (untrustedSpec, html) => (untrustedSpec ? DOMPurify.sanitize(html) : html);
export function SanitizedMarkdownHTML( export function SanitizedMarkdownHTML(
props: StylingMarkdownProps & { html: string; className?: string }, props: StylingMarkdownProps & { html: string; className?: string; 'data-role'?: string },
) { ) {
const Wrap = props.inline ? StyledMarkdownSpan : StyledMarkdownBlock; const Wrap = props.inline ? StyledMarkdownSpan : StyledMarkdownBlock;
@ -22,6 +22,7 @@ export function SanitizedMarkdownHTML(
dangerouslySetInnerHTML={{ dangerouslySetInnerHTML={{
__html: sanitize(options.untrustedSpec, props.html), __html: sanitize(options.untrustedSpec, props.html),
}} }}
data-role={props['data-role']}
{...props} {...props}
/> />
)} )}

View File

@ -19,11 +19,13 @@ export const linksCss = css`
} }
`; `;
export const StyledMarkdownBlock = styled(PrismDiv as StyledComponent< export const StyledMarkdownBlock = styled(
PrismDiv as StyledComponent<
'div', 'div',
ResolvedThemeInterface, ResolvedThemeInterface,
{ compact?: boolean; inline?: boolean } { compact?: boolean; inline?: boolean }
>)` >,
)`
font-family: ${props => props.theme.typography.fontFamily}; font-family: ${props => props.theme.typography.fontFamily};
font-weight: ${props => props.theme.typography.fontWeightRegular}; font-weight: ${props => props.theme.typography.fontWeightRegular};
@ -80,7 +82,7 @@ export const StyledMarkdownBlock = styled(PrismDiv as StyledComponent<
pre { pre {
font-family: ${props => props.theme.typography.code.fontFamily}; font-family: ${props => props.theme.typography.code.fontFamily};
white-space:${({ theme }) => (theme.typography.code.wrap ? 'pre-wrap' : 'pre')}; white-space:${({ theme }) => (theme.typography.code.wrap ? 'pre-wrap' : 'pre')};
background-color: #263238; background-color: ${({ theme }) => theme.codeBlock.backgroundColor};
color: white; color: white;
padding: ${props => props.theme.spacing.unit * 4}px; padding: ${props => props.theme.spacing.unit * 4}px;
overflow-x: auto; overflow-x: auto;

View File

@ -20,9 +20,9 @@ export interface MediaTypesSwitchProps {
@observer @observer
export class MediaTypesSwitch extends React.Component<MediaTypesSwitchProps> { export class MediaTypesSwitch extends React.Component<MediaTypesSwitchProps> {
switchMedia = ({ value }) => { switchMedia = ({ idx }) => {
if (this.props.content) { if (this.props.content) {
this.props.content.activate(parseInt(value, 10)); this.props.content.activate(idx);
} }
}; };
@ -35,8 +35,8 @@ export class MediaTypesSwitch extends React.Component<MediaTypesSwitchProps> {
const options = content.mediaTypes.map((mime, idx) => { const options = content.mediaTypes.map((mime, idx) => {
return { return {
label: mime.name, value: mime.name,
value: idx.toString(), idx,
}; };
}); });
@ -54,9 +54,10 @@ export class MediaTypesSwitch extends React.Component<MediaTypesSwitchProps> {
<> <>
<Wrapper> <Wrapper>
{this.props.renderDropdown({ {this.props.renderDropdown({
value: options[activeMimeIdx], value: options[activeMimeIdx].value,
options, options,
onChange: this.switchMedia, onChange: this.switchMedia,
ariaLabel: 'Content type',
})} })}
</Wrapper> </Wrapper>
{this.props.children(content.active)} {this.props.children(content.active)}

View File

@ -1,29 +1,26 @@
import * as React from 'react';
import { SecurityRequirements } from '../SecurityRequirement/SecurityRequirement';
import { observer } from 'mobx-react'; import { observer } from 'mobx-react';
import * as React from 'react';
import { Badge, DarkRightPanel, H2, MiddlePanel, Row } from '../../common-elements'; import { Badge, DarkRightPanel, H2, MiddlePanel, Row } from '../../common-elements';
import { OptionsContext } from '../OptionsProvider';
import { ShareLink } from '../../common-elements/linkify'; import { ShareLink } from '../../common-elements/linkify';
import { OperationModel } from '../../services/models';
import styled from '../../styled-components';
import { CallbacksList } from '../Callbacks';
import { CallbackSamples } from '../CallbackSamples/CallbackSamples';
import { Endpoint } from '../Endpoint/Endpoint'; import { Endpoint } from '../Endpoint/Endpoint';
import { ExternalDocumentation } from '../ExternalDocumentation/ExternalDocumentation'; import { ExternalDocumentation } from '../ExternalDocumentation/ExternalDocumentation';
import { Extensions } from '../Fields/Extensions';
import { Markdown } from '../Markdown/Markdown'; import { Markdown } from '../Markdown/Markdown';
import { OptionsContext } from '../OptionsProvider';
import { Parameters } from '../Parameters/Parameters'; import { Parameters } from '../Parameters/Parameters';
import { RequestSamples } from '../RequestSamples/RequestSamples'; import { RequestSamples } from '../RequestSamples/RequestSamples';
import { ResponsesList } from '../Responses/ResponsesList'; import { ResponsesList } from '../Responses/ResponsesList';
import { ResponseSamples } from '../ResponseSamples/ResponseSamples'; import { ResponseSamples } from '../ResponseSamples/ResponseSamples';
import { SecurityRequirements } from '../SecurityRequirement/SecurityRequirement';
import { OperationModel as OperationType } from '../../services/models';
import styled from '../../styled-components';
import { Extensions } from '../Fields/Extensions';
const OperationRow = styled(Row)` const OperationRow = styled(Row)`
backface-visibility: hidden; backface-visibility: hidden;
contain: content; contain: content;
overflow: hidden; overflow: hidden;
`; `;
@ -32,7 +29,7 @@ const Description = styled.div`
`; `;
export interface OperationProps { export interface OperationProps {
operation: OperationType; operation: OperationModel;
} }
@observer @observer
@ -40,19 +37,22 @@ export class Operation extends React.Component<OperationProps> {
render() { render() {
const { operation } = this.props; const { operation } = this.props;
const { name: summary, description, deprecated, externalDocs } = operation; const { name: summary, description, deprecated, externalDocs, isWebhook } = operation;
const hasDescription = !!(description || externalDocs); const hasDescription = !!(description || externalDocs);
return ( return (
<OptionsContext.Consumer> <OptionsContext.Consumer>
{options => ( {(options) => (
<OperationRow> <OperationRow>
<MiddlePanel> <MiddlePanel>
<H2> <H2>
<ShareLink to={operation.id} /> <ShareLink to={operation.id} />
{summary} {deprecated && <Badge type="warning"> Deprecated </Badge>} {summary} {deprecated && <Badge type="warning"> Deprecated </Badge>}
{isWebhook && <Badge type="primary"> Webhook </Badge>}
</H2> </H2>
{options.pathInMiddlePanel && <Endpoint operation={operation} inverted={true} />} {options.pathInMiddlePanel && !isWebhook && (
<Endpoint operation={operation} inverted={true} />
)}
{hasDescription && ( {hasDescription && (
<Description> <Description>
{description !== undefined && <Markdown source={description} />} {description !== undefined && <Markdown source={description} />}
@ -63,11 +63,13 @@ export class Operation extends React.Component<OperationProps> {
<SecurityRequirements securities={operation.security} /> <SecurityRequirements securities={operation.security} />
<Parameters parameters={operation.parameters} body={operation.requestBody} /> <Parameters parameters={operation.parameters} body={operation.requestBody} />
<ResponsesList responses={operation.responses} /> <ResponsesList responses={operation.responses} />
<CallbacksList callbacks={operation.callbacks} />
</MiddlePanel> </MiddlePanel>
<DarkRightPanel> <DarkRightPanel>
{!options.pathInMiddlePanel && <Endpoint operation={operation} />} {!options.pathInMiddlePanel && !isWebhook && <Endpoint operation={operation} />}
<RequestSamples operation={operation} /> <RequestSamples operation={operation} />
<ResponseSamples operation={operation} /> <ResponseSamples operation={operation} />
<CallbackSamples callbacks={operation.callbacks} />
</DarkRightPanel> </DarkRightPanel>
</OperationRow> </OperationRow>
)} )}

View File

@ -26,7 +26,7 @@ export interface ParametersProps {
const PARAM_PLACES = ['path', 'query', 'cookie', 'header']; const PARAM_PLACES = ['path', 'query', 'cookie', 'header'];
export class Parameters extends React.PureComponent<ParametersProps> { export class Parameters extends React.PureComponent<ParametersProps> {
orderParams(params: FieldModel[]): Dict<FieldModel[]> { orderParams(params: FieldModel[]): Record<string, FieldModel[]> {
const res = {}; const res = {};
params.forEach(param => { params.forEach(param => {
safePush(res, param.in, param); safePush(res, param.in, param);
@ -67,7 +67,7 @@ function DropdownWithinHeader(props) {
); );
} }
function BodyContent(props: { content: MediaContentModel; description?: string }): JSX.Element { export function BodyContent(props: { content: MediaContentModel; description?: string }): JSX.Element {
const { content, description } = props; const { content, description } = props;
return ( return (
<MediaTypesSwitch content={content} renderDropdown={DropdownWithinHeader}> <MediaTypesSwitch content={content} renderDropdown={DropdownWithinHeader}>

View File

@ -29,7 +29,12 @@ export function ExternalExample({ example, mimeType }: ExampleProps) {
return ( return (
<StyledPre> <StyledPre>
Error loading external example: <br /> 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} {example.externalValueUrl}
</a> </a>
</StyledPre> </StyledPre>

View File

@ -21,9 +21,9 @@ export class MediaTypeSamples extends React.Component<PayloadSamplesProps, Media
state = { state = {
activeIdx: 0, activeIdx: 0,
}; };
switchMedia = ({ value }) => { switchMedia = ({ idx }) => {
this.setState({ this.setState({
activeIdx: parseInt(value, 10), activeIdx: idx,
}); });
}; };
render() { render() {
@ -41,8 +41,8 @@ export class MediaTypeSamples extends React.Component<PayloadSamplesProps, Media
if (examplesNames.length > 1) { if (examplesNames.length > 1) {
const options = examplesNames.map((name, idx) => { const options = examplesNames.map((name, idx) => {
return { return {
label: examples[name].summary || name, value: examples[name].summary || name,
value: idx.toString(), idx,
}; };
}); });
@ -54,9 +54,10 @@ export class MediaTypeSamples extends React.Component<PayloadSamplesProps, Media
<DropdownWrapper> <DropdownWrapper>
<DropdownLabel>Example</DropdownLabel> <DropdownLabel>Example</DropdownLabel>
{this.props.renderDropdown({ {this.props.renderDropdown({
value: options[activeIdx], value: options[activeIdx].value,
options, options,
onChange: this.switchMedia, onChange: this.switchMedia,
ariaLabel: 'Example',
})} })}
</DropdownWrapper> </DropdownWrapper>
<div> <div>

View File

@ -1,16 +1,15 @@
// @ts-ignore
import Dropdown from 'react-dropdown';
import { transparentize } from 'polished'; import { transparentize } from 'polished';
import styled from '../../styled-components'; import styled from '../../styled-components';
import { StyledDropdown } from '../../common-elements'; import { StyledDropdown } from '../../common-elements';
export const MimeLabel = styled.div` export const MimeLabel = styled.div`
padding: 12px; padding: 0.9em;
background-color: ${({ theme }) => transparentize(0.6, theme.rightPanel.backgroundColor)}; background-color: ${({ theme }) => transparentize(0.6, theme.rightPanel.backgroundColor)};
margin: 0 0 10px 0; margin: 0 0 10px 0;
display: block; display: block;
font-family: ${({ theme }) => theme.typography.headings.fontFamily};
font-size: 0.929em;
line-height: 1.5em;
`; `;
export const DropdownLabel = styled.span` export const DropdownLabel = styled.span`
@ -29,31 +28,45 @@ export const DropdownWrapper = styled.div`
`; `;
export const InvertedSimpleDropdown = styled(StyledDropdown)` export const InvertedSimpleDropdown = styled(StyledDropdown)`
&& {
margin-left: 10px; margin-left: 10px;
text-transform: none; text-transform: none;
font-size: 0.929em; font-size: 0.929em;
margin: 0 0 10px 0; margin: 0 0 10px 0;
display: block; display: block;
background-color: ${({ theme }) => transparentize(0.6, theme.rightPanel.backgroundColor)}; background-color: ${({ theme }) => transparentize(0.6, theme.rightPanel.backgroundColor)};
.Dropdown-control {
margin-top: 0;
}
.Dropdown-control,
.Dropdown-control:hover {
font-size: 1em; font-size: 1em;
border: none; border: none;
padding: 0.9em 1.6em 0.9em 0.9em; padding: 0.9em 1.6em 0.9em 0.9em;
background: transparent;
color: ${({ theme }) => theme.rightPanel.textColor};
box-shadow: none; box-shadow: none;
&:hover,
&:focus-within {
border: none;
box-shadow: none;
}
&:focus-within {
background-color: ${({ theme }) => transparentize(0.3, theme.rightPanel.backgroundColor)};
}
.Dropdown-arrow { .dropdown-arrow {
border-top-color: ${({ theme }) => theme.rightPanel.textColor}; border-top-color: ${({ theme }) => theme.rightPanel.textColor};
} }
.dropdown-selector-value {
text-overflow: ellipsis;
white-space: nowrap;
overflow: hidden;
color: ${({ theme }) => theme.rightPanel.textColor};
} }
.Dropdown-menu {
.dropdown-selector-content {
margin: 0; margin: 0;
margin-top: 2px; margin-top: 2px;
.dropdown-option {
text-overflow: ellipsis;
white-space: nowrap;
overflow: hidden;
}
}
} }
`; `;

View File

@ -29,7 +29,7 @@ export const ApiContentWrap = styled.div`
z-index: 1; z-index: 1;
position: relative; position: relative;
overflow: hidden; overflow: hidden;
width: calc(100% - ${props => props.theme.menu.width}); width: calc(100% - ${props => props.theme.sidebar.width});
${media.lessThan('small', true)` ${media.lessThan('small', true)`
width: 100%; width: 100%;
`}; `};
@ -46,7 +46,7 @@ export const BackgroundStub = styled.div`
width: ${({ theme }) => { width: ${({ theme }) => {
if (theme.rightPanel.width.endsWith('%')) { if (theme.rightPanel.width.endsWith('%')) {
const percents = parseInt(theme.rightPanel.width, 10); const percents = parseInt(theme.rightPanel.width, 10);
return `calc((100% - ${theme.menu.width}) * ${percents / 100})`; return `calc((100% - ${theme.sidebar.width}) * ${percents / 100})`;
} else { } else {
return theme.rightPanel.width; return theme.rightPanel.width;
} }

View File

@ -1,5 +1,6 @@
import * as React from 'react'; import * as React from 'react';
import { Code } from './styled.elements';
import { ShelfIcon } from '../../common-elements'; import { ShelfIcon } from '../../common-elements';
import { Markdown } from '../Markdown/Markdown'; import { Markdown } from '../Markdown/Markdown';
@ -17,7 +18,12 @@ export class ResponseTitle extends React.PureComponent<ResponseTitleProps> {
render() { render() {
const { title, type, empty, code, opened, className, onClick } = this.props; const { title, type, empty, code, opened, className, onClick } = this.props;
return ( return (
<div className={className} onClick={(!empty && onClick) || undefined}> <button
className={className}
onClick={(!empty && onClick) || undefined}
aria-expanded={opened}
disabled={empty}
>
{!empty && ( {!empty && (
<ShelfIcon <ShelfIcon
size={'1.5em'} size={'1.5em'}
@ -26,9 +32,9 @@ export class ResponseTitle extends React.PureComponent<ResponseTitleProps> {
float={'left'} float={'left'}
/> />
)} )}
<strong>{code} </strong> <Code>{code} </Code>
<Markdown compact={true} inline={true} source={title} /> <Markdown compact={true} inline={true} source={title} />
</div> </button>
); );
} }
} }

View File

@ -4,20 +4,21 @@ import styled from '../../styled-components';
import { ResponseView } from './Response'; import { ResponseView } from './Response';
const ResponsesHeader = styled.h3` const ResponsesHeader = styled.h3`
font-size: 18px; font-size: 1.3em;
padding: 0.2em 0; padding: 0.2em 0;
margin: 3em 0 1.1em; margin: 3em 0 1.1em;
color: #253137; color: ${({ theme }) => theme.colors.text.primary};
font-weight: normal; font-weight: normal;
`; `;
export interface ResponseListProps { export interface ResponseListProps {
responses: ResponseModel[]; responses: ResponseModel[];
isCallback?: boolean;
} }
export class ResponsesList extends React.PureComponent<ResponseListProps> { export class ResponsesList extends React.PureComponent<ResponseListProps> {
render() { render() {
const { responses } = this.props; const { responses, isCallback } = this.props;
if (!responses || responses.length === 0) { if (!responses || responses.length === 0) {
return null; return null;
@ -25,7 +26,7 @@ export class ResponsesList extends React.PureComponent<ResponseListProps> {
return ( return (
<div> <div>
<ResponsesHeader> Responses </ResponsesHeader> <ResponsesHeader>{isCallback ? 'Callback responses' : 'Responses'}</ResponsesHeader>
{responses.map(response => { {responses.map(response => {
return <ResponseView key={response.code} response={response} />; return <ResponseView key={response.code} response={response} />;
})} })}

View File

@ -5,6 +5,10 @@ import styled from '../../styled-components';
import { ResponseTitle } from './ResponseTitle'; import { ResponseTitle } from './ResponseTitle';
export const StyledResponseTitle = styled(ResponseTitle)` export const StyledResponseTitle = styled(ResponseTitle)`
display: block;
border: 0;
width: 100%;
text-align: left;
padding: 10px; padding: 10px;
border-radius: 2px; border-radius: 2px;
margin-bottom: 4px; margin-bottom: 4px;
@ -12,10 +16,13 @@ export const StyledResponseTitle = styled(ResponseTitle)`
background-color: #f2f2f2; background-color: #f2f2f2;
cursor: pointer; cursor: pointer;
color: ${props => props.theme.colors.responses[props.type].color}; color: ${(props) => props.theme.colors.responses[props.type].color};
background-color: ${props => props.theme.colors.responses[props.type].backgroundColor}; background-color: ${(props) => props.theme.colors.responses[props.type].backgroundColor};
&:focus {
${props => outline: auto;
outline-color: ${(props) => props.theme.colors.responses[props.type].color};
}
${(props) =>
(props.empty && (props.empty &&
` `
cursor: default; cursor: default;
@ -25,6 +32,10 @@ cursor: default;
width: 1.5em; width: 1.5em;
text-align: center; text-align: center;
display: inline-block; display: inline-block;
vertical-align: top;
}
&:focus {
outline: 0;
} }
`) || `) ||
''}; ''};
@ -39,3 +50,7 @@ export const HeadersCaption = styled(UnderlinedHeader.withComponent('caption'))`
margin-top: 1em; margin-top: 1em;
caption-side: top; caption-side: top;
`; `;
export const Code = styled.strong`
vertical-align: top;
`;

View File

@ -4,6 +4,7 @@ import { Schema, SchemaProps } from './Schema';
import { ArrayClosingLabel, ArrayOpenningLabel } from '../../common-elements'; import { ArrayClosingLabel, ArrayOpenningLabel } from '../../common-elements';
import styled from '../../styled-components'; import styled from '../../styled-components';
import {humanizeConstraints} from "../../utils";
const PaddedSchema = styled.div` const PaddedSchema = styled.div`
padding-left: ${({ theme }) => theme.spacing.unit * 2}px; padding-left: ${({ theme }) => theme.spacing.unit * 2}px;
@ -12,9 +13,16 @@ const PaddedSchema = styled.div`
export class ArraySchema extends React.PureComponent<SchemaProps> { export class ArraySchema extends React.PureComponent<SchemaProps> {
render() { render() {
const itemsSchema = this.props.schema.items!; const itemsSchema = this.props.schema.items!;
const itemConstraintSchema = (
min: number | undefined = undefined,
max: number | undefined = undefined,
) => ({ type: 'array', minItems: min, maxItems: max });
const minMaxItems = humanizeConstraints(itemConstraintSchema(itemsSchema.schema.minItems, itemsSchema.schema.maxItems));
return ( return (
<div> <div>
<ArrayOpenningLabel> Array </ArrayOpenningLabel> <ArrayOpenningLabel> Array ({minMaxItems})</ArrayOpenningLabel>
<PaddedSchema> <PaddedSchema>
<Schema {...this.props} schema={itemsSchema} /> <Schema {...this.props} schema={itemsSchema} />
</PaddedSchema> </PaddedSchema>

View File

@ -21,7 +21,7 @@ export class DiscriminatorDropdown extends React.Component<{
}); });
options.sort((a, b) => { options.sort((a, b) => {
return enumOrder[a.label] > enumOrder[b.label] ? 1 : -1; return enumOrder[a.value] > enumOrder[b.value] ? 1 : -1;
}); });
} }
@ -33,21 +33,21 @@ export class DiscriminatorDropdown extends React.Component<{
const options = parent.oneOf.map((subSchema, idx) => { const options = parent.oneOf.map((subSchema, idx) => {
return { return {
value: idx.toString(), value: subSchema.title,
label: subSchema.title, idx,
}; };
}); });
const activeItem = options[parent.activeOneOf]; const activeValue = options[parent.activeOneOf].value;
this.sortOptions(options, enumValues); this.sortOptions(options, enumValues);
return ( return (
<StyledDropdown value={activeItem} options={options} onChange={this.changeActiveChild} /> <StyledDropdown value={activeValue} options={options} onChange={this.changeActiveChild} />
); );
} }
changeActiveChild = ({ value }) => { changeActiveChild = (option: DropdownOption) => {
const idx = parseInt(value, 10); this.props.parent.activateOneOf(option.idx);
this.props.parent.activateOneOf(idx);
}; };
} }

View File

@ -9,6 +9,7 @@ import { DiscriminatorDropdown } from './DiscriminatorDropdown';
import { SchemaProps } from './Schema'; import { SchemaProps } from './Schema';
import { mapWithLast } from '../../utils'; import { mapWithLast } from '../../utils';
import { OptionsContext } from '../OptionsProvider';
export interface ObjectSchemaProps extends SchemaProps { export interface ObjectSchemaProps extends SchemaProps {
discriminator?: { discriminator?: {
@ -19,6 +20,8 @@ export interface ObjectSchemaProps extends SchemaProps {
@observer @observer
export class ObjectSchema extends React.Component<ObjectSchemaProps> { export class ObjectSchema extends React.Component<ObjectSchemaProps> {
static contextType = OptionsContext;
get parentSchema() { get parentSchema() {
return this.props.discriminator!.parentSchema; return this.props.discriminator!.parentSchema;
} }
@ -41,6 +44,8 @@ export class ObjectSchema extends React.Component<ObjectSchemaProps> {
}) })
: fields; : fields;
const expandByDefault = this.context.expandSingleSchemaField && filteredFields.length === 1;
return ( return (
<PropertiesTable> <PropertiesTable>
{showTitle && <PropertiesTableCaption>{this.props.schema.title}</PropertiesTableCaption>} {showTitle && <PropertiesTableCaption>{this.props.schema.title}</PropertiesTableCaption>}
@ -51,6 +56,7 @@ export class ObjectSchema extends React.Component<ObjectSchemaProps> {
key={field.name} key={field.name}
isLast={isLast} isLast={isLast}
field={field} field={field}
expandByDefault={expandByDefault}
renderDiscriminatorSwitch={ renderDiscriminatorSwitch={
(discriminator && (discriminator &&
discriminator.fieldName === field.name && discriminator.fieldName === field.name &&

View File

@ -43,9 +43,10 @@ export class Schema extends React.Component<Partial<SchemaProps>> {
if (discriminatorProp !== undefined) { if (discriminatorProp !== undefined) {
if (!oneOf || !oneOf.length) { if (!oneOf || !oneOf.length) {
throw new Error( console.warn(
`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 null;
} }
return ( return (
<ObjectSchema <ObjectSchema

View File

@ -80,7 +80,7 @@ export class SchemaDefinition extends React.PureComponent<ObjectDescriptionProps
} }
const MediaSamplesWrap = styled.div` const MediaSamplesWrap = styled.div`
background: ${({ theme }) => theme.codeSample.backgroundColor}; background: ${({ theme }) => theme.codeBlock.backgroundColor};
& > div, & > div,
& > pre { & > pre {
padding: ${props => props.theme.spacing.unit * 4}px; padding: ${props => props.theme.spacing.unit * 4}px;

View File

@ -7,6 +7,7 @@ import { MenuItem } from '../SideMenu/MenuItem';
import { MarkerService } from '../../services/MarkerService'; import { MarkerService } from '../../services/MarkerService';
import { SearchResult } from '../../services/SearchWorker.worker'; import { SearchResult } from '../../services/SearchWorker.worker';
import { bind, debounce } from 'decko';
import { PerfectScrollbarWrap } from '../../common-elements/perfect-scrollbar'; import { PerfectScrollbarWrap } from '../../common-elements/perfect-scrollbar';
import { import {
ClearIcon, ClearIcon,
@ -96,7 +97,6 @@ export class SearchBox extends React.PureComponent<SearchBoxProps, SearchBoxStat
setResults(results: SearchResult[], term: string) { setResults(results: SearchResult[], term: string) {
this.setState({ this.setState({
results, results,
term,
}); });
this.props.marker.mark(term); this.props.marker.mark(term);
} }

View File

@ -19,14 +19,14 @@ export const SearchInput = styled.input.attrs(() => ({
border: 0; border: 0;
border-bottom: 1px solid border-bottom: 1px solid
${({ theme }) => ${({ theme }) =>
(getLuminance(theme.menu.backgroundColor) > 0.5 ? darken : lighten)( (getLuminance(theme.sidebar.backgroundColor) > 0.5 ? darken : lighten)(
0.1, 0.1,
theme.menu.backgroundColor, theme.sidebar.backgroundColor,
)}; )};
font-family: ${({ theme }) => theme.typography.fontFamily}; font-family: ${({ theme }) => theme.typography.fontFamily};
font-weight: bold; font-weight: bold;
font-size: 13px; font-size: 13px;
color: ${props => props.theme.menu.textColor}; color: ${props => props.theme.sidebar.textColor};
background-color: transparent; background-color: transparent;
outline: none; outline: none;
`; `;
@ -51,18 +51,18 @@ export const SearchIcon = styled((props: { className?: string }) => (
width: 0.9em; width: 0.9em;
path { path {
fill: ${props => props.theme.menu.textColor}; fill: ${props => props.theme.sidebar.textColor};
} }
`; `;
export const SearchResultsBox = styled.div` export const SearchResultsBox = styled.div`
padding: ${props => props.theme.spacing.unit}px 0; padding: ${props => props.theme.spacing.unit}px 0;
background-color: ${({ theme }) => darken(0.05, theme.menu.backgroundColor)}}; background-color: ${({ theme }) => darken(0.05, theme.sidebar.backgroundColor)}};
color: ${props => props.theme.menu.textColor}; color: ${props => props.theme.sidebar.textColor};
min-height: 150px; min-height: 150px;
max-height: 250px; max-height: 250px;
border-top: ${({ theme }) => darken(0.1, theme.menu.backgroundColor)}}; border-top: ${({ theme }) => darken(0.1, theme.sidebar.backgroundColor)}};
border-bottom: ${({ theme }) => darken(0.1, theme.menu.backgroundColor)}}; border-bottom: ${({ theme }) => darken(0.1, theme.sidebar.backgroundColor)}};
margin-top: 10px; margin-top: 10px;
line-height: 1.4; line-height: 1.4;
font-size: 0.9em; font-size: 0.9em;
@ -73,7 +73,7 @@ export const SearchResultsBox = styled.div`
&:hover, &:hover,
&.active { &.active {
background-color: ${({ theme }) => darken(0.1, theme.menu.backgroundColor)}; background-color: ${({ theme }) => darken(0.1, theme.sidebar.backgroundColor)};
} }
> svg { > svg {

View File

@ -97,7 +97,7 @@ export class SecurityDefs extends React.PureComponent<SecurityDefsProps> {
scheme.http.scheme === 'bearer' && scheme.http.bearerFormat && ( scheme.http.scheme === 'bearer' && scheme.http.bearerFormat && (
<tr key="bearer"> <tr key="bearer">
<th> Bearer format </th> <th> Bearer format </th>
<td> "{scheme.http.bearerFormat}" </td> <td> &quot;{scheme.http.bearerFormat}&quot; </td>
</tr> </tr>
), ),
] ]
@ -105,7 +105,11 @@ export class SecurityDefs extends React.PureComponent<SecurityDefsProps> {
<tr> <tr>
<th> Connect URL </th> <th> Connect URL </th>
<td> <td>
<a target="_blank" href={scheme.openId.connectUrl}> <a
target="_blank"
rel="noopener noreferrer"
href={scheme.openId.connectUrl}
>
{scheme.openId.connectUrl} {scheme.openId.connectUrl}
</a> </a>
</td> </td>

View File

@ -4,14 +4,20 @@ import { ClipboardService } from '../../services';
export class SelectOnClick extends React.PureComponent { export class SelectOnClick extends React.PureComponent {
private child: HTMLDivElement | null; private child: HTMLDivElement | null;
handleClick = () => { selectElement = () => {
ClipboardService.selectElement(this.child); ClipboardService.selectElement(this.child);
}; };
render() { render() {
const { children } = this.props; const { children } = this.props;
return ( return (
<div ref={el => (this.child = el)} onClick={this.handleClick}> <div
ref={el => (this.child = el)}
onClick={this.selectElement}
onFocus={this.selectElement}
tabIndex={0}
role="button"
>
{children} {children}
</div> </div>
); );

View File

@ -7,6 +7,7 @@ import { IMenuItem, OperationModel } from '../../services';
import { shortenHTTPVerb } from '../../utils/openapi'; import { shortenHTTPVerb } from '../../utils/openapi';
import { MenuItems } from './MenuItems'; import { MenuItems } from './MenuItems';
import { MenuItemLabel, MenuItemLi, MenuItemTitle, OperationBadge } from './styled.elements'; import { MenuItemLabel, MenuItemLi, MenuItemTitle, OperationBadge } from './styled.elements';
import { l } from '../../services/Labels';
export interface MenuItemProps { export interface MenuItemProps {
item: IMenuItem; item: IMenuItem;
@ -103,7 +104,11 @@ export class OperationMenuItemContent extends React.Component<OperationMenuItemC
deprecated={item.deprecated} deprecated={item.deprecated}
ref={this.ref} ref={this.ref}
> >
{item.isWebhook ? (
<OperationBadge type="hook">{l('webhook')}</OperationBadge>
) : (
<OperationBadge type={item.httpVerb}>{shortenHTTPVerb(item.httpVerb)}</OperationBadge> <OperationBadge type={item.httpVerb}>{shortenHTTPVerb(item.httpVerb)}</OperationBadge>
)}
<MenuItemTitle width="calc(100% - 38px)"> <MenuItemTitle width="calc(100% - 38px)">
{item.name} {item.name}
{this.props.children} {this.props.children}

View File

@ -25,7 +25,7 @@ export class SideMenu extends React.Component<{ menu: MenuStore; className?: str
> >
<MenuItems items={store.items} onActivate={this.activate} root={true} setActiveSelection={this.props.setActiveSelection} /> <MenuItems items={store.items} onActivate={this.activate} root={true} setActiveSelection={this.props.setActiveSelection} />
<RedocAttribution> <RedocAttribution>
<a target="_blank" href="https://github.com/Redocly/redoc"> <a target="_blank" rel="noopener noreferrer" href="https://github.com/Redocly/redoc">
Documentation Powered by ReDoc Documentation Powered by ReDoc
</a> </a>
</RedocAttribution> </RedocAttribution>

View File

@ -2,12 +2,12 @@ import * as classnames from 'classnames';
import { darken } from 'polished'; import { darken } from 'polished';
import { deprecatedCss, ShelfIcon } from '../../common-elements'; import { deprecatedCss, ShelfIcon } from '../../common-elements';
import styled, { css } from '../../styled-components'; import styled, { css, ResolvedThemeInterface } from '../../styled-components';
export const OperationBadge = styled.span.attrs((props: { type: string }) => ({ export const OperationBadge = styled.span.attrs((props: { type: string }) => ({
className: `operation-type ${props.type}`, className: `operation-type ${props.type}`,
}))<{ type: string }>` }))<{ type: string }>`
width: 32px; width: 9ex;
display: inline-block; display: inline-block;
height: ${props => props.theme.typography.code.fontSize}; height: ${props => props.theme.typography.code.fontSize};
line-height: ${props => props.theme.typography.code.fontSize}; line-height: ${props => props.theme.typography.code.fontSize};
@ -16,7 +16,7 @@ export const OperationBadge = styled.span.attrs((props: { type: string }) => ({
background-repeat: no-repeat; background-repeat: no-repeat;
background-position: 6px 4px; background-position: 6px 4px;
font-size: 7px; font-size: 7px;
font-family: Verdana; // web-safe font-family: Verdana, sans-serif; // web-safe
color: white; color: white;
text-transform: uppercase; text-transform: uppercase;
text-align: center; text-align: center;
@ -60,13 +60,17 @@ export const OperationBadge = styled.span.attrs((props: { type: string }) => ({
&.head { &.head {
background-color: ${props => props.theme.colors.http.head}; background-color: ${props => props.theme.colors.http.head};
} }
&.hook {
background-color: ${props => props.theme.colors.primary.main};
}
`; `;
function menuItemActiveBg(depth, { theme }): string { function menuItemActiveBg(depth, { theme }: { theme: ResolvedThemeInterface }): string {
if (depth > 1) { if (depth > 1) {
return darken(0.1, theme.menu.backgroundColor); return darken(0.1, theme.sidebar.backgroundColor);
} else if (depth === 1) { } else if (depth === 1) {
return darken(0.05, theme.menu.backgroundColor); return darken(0.05, theme.sidebar.backgroundColor);
} else { } else {
return ''; return '';
} }
@ -94,21 +98,21 @@ export const MenuItemLi = styled.li<{ depth: number }>`
export const menuItemDepth = { export const menuItemDepth = {
0: css` 0: css`
opacity: 0.7; opacity: 0.7;
text-transform: ${({ theme }) => theme.menu.groupItems.textTransform}; text-transform: ${({ theme }) => theme.sidebar.groupItems.textTransform};
font-size: 0.8em; font-size: 0.8em;
padding-bottom: 0; padding-bottom: 0;
cursor: default; cursor: default;
color: ${props => props.theme.menu.textColor}; color: ${props => props.theme.sidebar.textColor};
`, `,
1: css` 1: css`
font-size: 0.929em; font-size: 0.929em;
text-transform: ${({ theme }) => theme.menu.level1Items.textTransform}; text-transform: ${({ theme }) => theme.sidebar.level1Items.textTransform};
&:hover { &:hover {
color: ${props => props.theme.menu.activeTextColor}; color: ${props => props.theme.sidebar.activeTextColor};
} }
`, `,
2: css` 2: css`
color: ${props => props.theme.menu.textColor}; color: ${props => props.theme.sidebar.textColor};
`, `,
}; };
@ -126,7 +130,8 @@ export const MenuItemLabel = styled.label.attrs((props: MenuItemLabelType) => ({
}), }),
}))<MenuItemLabelType>` }))<MenuItemLabelType>`
cursor: pointer; cursor: pointer;
color: ${props => (props.active ? props.theme.menu.activeTextColor : props.theme.menu.textColor)}; color: ${props =>
props.active ? props.theme.sidebar.activeTextColor : props.theme.sidebar.textColor};
margin: 0; margin: 0;
padding: 12.5px ${props => props.theme.spacing.unit * 4}px; padding: 12.5px ${props => props.theme.spacing.unit * 4}px;
${({ depth, type, theme }) => ${({ depth, type, theme }) =>
@ -144,10 +149,10 @@ export const MenuItemLabel = styled.label.attrs((props: MenuItemLabelType) => ({
} }
${ShelfIcon} { ${ShelfIcon} {
height: ${({ theme }) => theme.menu.arrow.size}; height: ${({ theme }) => theme.sidebar.arrow.size};
width: ${({ theme }) => theme.menu.arrow.size}; width: ${({ theme }) => theme.sidebar.arrow.size};
polygon { polygon {
fill: ${({ theme }) => theme.menu.arrow.color}; fill: ${({ theme }) => theme.sidebar.arrow.color};
} }
} }
`; `;
@ -172,8 +177,8 @@ export const RedocAttribution = styled.div`
a, a,
a:visited, a:visited,
a:hover { a:hover {
color: ${theme.menu.textColor} !important; color: ${theme.sidebar.textColor} !important;
border-top: 1px solid ${darken(0.1, theme.menu.backgroundColor)}; border-top: 1px solid ${darken(0.1, theme.sidebar.backgroundColor)};
padding: ${theme.spacing.unit}px 0; padding: ${theme.spacing.unit}px 0;
display: block; display: block;
} }

View File

@ -26,14 +26,14 @@ export interface StickySidebarState {
const stickyfill = Stickyfill && Stickyfill(); const stickyfill = Stickyfill && Stickyfill();
const StyledStickySidebar = styled.div<{ open?: boolean }>` const StyledStickySidebar = styled.div<{ open?: boolean }>`
width: ${props => props.theme.menu.width}; width: ${props => props.theme.sidebar.width};
background-color: ${props => props.theme.menu.backgroundColor}; background-color: ${props => props.theme.sidebar.backgroundColor};
overflow: hidden; overflow: hidden;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
backface-visibility: hidden; backface-visibility: hidden;
contain: strict; /* contain: strict; TODO: breaks layout since Chrome 80*/
height: 100vh; height: 100vh;
position: sticky; position: sticky;
@ -44,7 +44,7 @@ const StyledStickySidebar = styled.div<{ open?: boolean }>`
position: fixed; position: fixed;
z-index: 20; z-index: 20;
width: 100%; width: 100%;
background: ${({ theme }) => theme.menu.backgroundColor}; background: ${({ theme }) => theme.sidebar.backgroundColor};
display: ${props => (props.open ? 'flex' : 'none')}; display: ${props => (props.open ? 'flex' : 'none')};
`}; `};

View File

@ -0,0 +1,59 @@
/* tslint:disable:no-implicit-dependencies */
import { shallow } from 'enzyme';
import * as React from 'react';
import { OpenAPIParser } from '../../services';
import { CallbackModel } from '../../services/models/Callback';
import { RedocNormalizedOptions } from '../../services/RedocNormalizedOptions';
import { CallbacksList, CallbackTitle, CallbackOperation } from '../Callbacks';
import * as simpleCallbackFixture from './fixtures/simple-callback.json';
const options = new RedocNormalizedOptions({});
describe('Components', () => {
describe('Callbacks', () => {
it('should correctly render CallbackView', () => {
const parser = new OpenAPIParser(simpleCallbackFixture, undefined, options);
const callback = new CallbackModel(
parser,
'Test.Callback',
{ $ref: '#/components/callbacks/Test' },
'',
options,
);
// There should be 1 operation defined in simple-callback.json, just get it manually for readability.
const callbackViewElement = shallow(
<CallbackOperation key={callback.name} callbackOperation={callback.operations[0]} />,
).getElement();
expect(callbackViewElement.props).toBeDefined();
expect(callbackViewElement.props.children).toBeDefined();
expect(callbackViewElement.props.children.length).toBeGreaterThan(0);
});
it('should correctly render CallbackTitle', () => {
const callbackTitleViewElement = shallow(
<CallbackTitle name={'Test'} className={'.test'} onClick={undefined} httpVerb={'get'} />,
).getElement();
expect(callbackTitleViewElement.props).toBeDefined();
expect(callbackTitleViewElement.props.className).toEqual('.test');
expect(callbackTitleViewElement.props.onClick).toBeUndefined();
});
it('should correctly render CallbacksList', () => {
const parser = new OpenAPIParser(simpleCallbackFixture, undefined, options);
const callback = new CallbackModel(
parser,
'Test.Callback',
{ $ref: '#/components/callbacks/Test' },
'',
options,
);
const callbacksListViewElement = shallow(
<CallbacksList callbacks={[callback]} />,
).getElement();
expect(callbacksListViewElement.props).toBeDefined();
expect(callbacksListViewElement.props.children).toBeDefined();
expect(callbacksListViewElement.props.children.length).toBeGreaterThan(0);
});
});
});

View File

@ -1,3 +1,4 @@
/* eslint-disable import/no-internal-modules */
/* tslint:disable:no-implicit-dependencies */ /* tslint:disable:no-implicit-dependencies */
import { shallow } from 'enzyme'; import { shallow } from 'enzyme';

View File

@ -25,11 +25,11 @@ describe('Components', () => {
test('should collapse/uncollapse', () => { test('should collapse/uncollapse', () => {
expect(component.html()).not.toContain('class="hoverable"'); // all are collapsed by default expect(component.html()).not.toContain('class="hoverable"'); // all are collapsed by default
const expandAll = component.find('div > span[children=" Expand all "]'); const expandAll = component.find('div > button[children=" Expand all "]');
expandAll.simulate('click'); expandAll.simulate('click');
expect(component.html()).toContain('class="hoverable"'); // all are collapsed expect(component.html()).toContain('class="hoverable"'); // all are collapsed
const collapseAll = component.find('div > span[children=" Collapse all "]'); const collapseAll = component.find('div > button[children=" Collapse all "]');
collapseAll.simulate('click'); collapseAll.simulate('click');
expect(component.html()).not.toContain('class="hoverable"'); // all are collapsed expect(component.html()).not.toContain('class="hoverable"'); // all are collapsed
}); });
@ -37,7 +37,7 @@ describe('Components', () => {
test('should collapse/uncollapse', () => { test('should collapse/uncollapse', () => {
ClipboardService.copySelected = jest.fn(); ClipboardService.copySelected = jest.fn();
const copy = component.find('span[onClick]').first(); const copy = component.find('button[onClick]').first();
copy.simulate('click'); copy.simulate('click');
expect(ClipboardService.copySelected as jest.Mock).toHaveBeenCalled(); expect(ClipboardService.copySelected as jest.Mock).toHaveBeenCalled();

View File

@ -9,7 +9,7 @@ exports[`Components SchemaView discriminator should correctly render discriminat
"deprecated": false, "deprecated": false,
"description": "", "description": "",
"example": undefined, "example": undefined,
"expanded": false, "expanded": undefined,
"explode": false, "explode": false,
"in": undefined, "in": undefined,
"kind": "field", "kind": "field",
@ -59,7 +59,7 @@ exports[`Components SchemaView discriminator should correctly render discriminat
"deprecated": false, "deprecated": false,
"description": "", "description": "",
"example": undefined, "example": undefined,
"expanded": false, "expanded": undefined,
"explode": false, "explode": false,
"in": undefined, "in": undefined,
"kind": "field", "kind": "field",

View File

@ -0,0 +1,66 @@
{
"openapi": "3.0.0",
"info": {
"version": "1.0",
"title": "Foo"
},
"components": {
"callbacks": {
"Test": {
"/test": {
"post": {
"operationId": "testCallback",
"description": "Test callback.",
"requestBody": {
"content": {
"application/json": {
"schema": {
"title": "TestTitle",
"type": "object",
"description": "Test description",
"properties": {
"type": {
"type": "string",
"description": "The type of response.",
"enum": [
"TestResponse.Complete"
]
},
"status": {
"type": "string",
"enum": [
"FAILURE",
"SUCCESS"
]
}
},
"required": [
"status"
]
}
}
}
},
"parameters": [
{
"name": "X-Test-Header",
"in": "header",
"required": true,
"example": "1",
"description": "This is a test header parameter",
"schema": {
"type": "string"
}
}
],
"responses": {
"204": {
"description": "Test response."
}
}
}
}
}
}
}
}

View File

@ -20,7 +20,11 @@ export * from './Responses/ResponsesList';
export * from './Responses/ResponseTitle'; export * from './Responses/ResponseTitle';
export * from './ResponseSamples/ResponseSamples'; export * from './ResponseSamples/ResponseSamples';
export * from './PayloadSamples/PayloadSamples'; export * from './PayloadSamples/PayloadSamples';
export * from './PayloadSamples/styled.elements';
export * from './MediaTypeSwitch/MediaTypesSwitch'; export * from './MediaTypeSwitch/MediaTypesSwitch';
export * from './Parameters/Parameters';
export * from './PayloadSamples/Example';
export * from './DropdownOrLabel/DropdownOrLabel';
export * from './ErrorBoundary'; export * from './ErrorBoundary';
export * from './StoreBuilder'; export * from './StoreBuilder';

View File

@ -1,5 +1,14 @@
export * from './components'; export * from './components';
export { MiddlePanel, Row, RightPanel, Section } from './common-elements/'; export {
MiddlePanel,
Row,
RightPanel,
Section,
StyledDropdown,
SimpleDropdown,
DropdownOption,
} from './common-elements/';
export { OpenAPIEncoding } from './types';
export * from './services'; export * from './services';
export * from './utils'; export * from './utils';

View File

@ -18,6 +18,8 @@ import {
SECURITY_DEFINITIONS_JSX_NAME, SECURITY_DEFINITIONS_JSX_NAME,
} from '../utils/openapi'; } from '../utils/openapi';
import { IS_BROWSER } from '../utils';
export interface StoreState { export interface StoreState {
menu: { menu: {
activeItemIdx: number; activeItemIdx: number;
@ -101,6 +103,9 @@ export class AppStore {
dispose() { dispose() {
this.scroll.dispose(); this.scroll.dispose();
this.menu.dispose(); this.menu.dispose();
if (this.search) {
this.search.dispose();
}
if (this.disposer != null) { if (this.disposer != null) {
this.disposer(); this.disposer();
} }
@ -131,16 +136,16 @@ export class AppStore {
const elements: Element[] = []; const elements: Element[] = [];
for (let i = start; i < end; i++) { for (let i = start; i < end; i++) {
let elem = this.menu.getElementAt(i); const elem = this.menu.getElementAt(i);
if (!elem) { if (!elem) {
continue; continue;
} }
if (this.menu.flatItems[i].type === 'section') {
elem = elem.parentElement!.parentElement;
}
if (elem) {
elements.push(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); this.marker.addOnly(elements);

View File

@ -8,6 +8,7 @@ export interface LabelsConfig {
nullable: string; nullable: string;
recursive: string; recursive: string;
arrayOf: string; arrayOf: string;
webhook: string;
} }
export type LabelsConfigRaw = Partial<LabelsConfig>; export type LabelsConfigRaw = Partial<LabelsConfig>;
@ -22,6 +23,7 @@ const labels: LabelsConfig = {
nullable: 'Nullable', nullable: 'Nullable',
recursive: 'Recursive', recursive: 'Recursive',
arrayOf: 'Array of ', arrayOf: 'Array of ',
webhook: 'Event',
}; };
export function setRedocLabels(_labels?: LabelsConfigRaw) { export function setRedocLabels(_labels?: LabelsConfigRaw) {

View File

@ -1,8 +1,17 @@
import { OpenAPIOperation, OpenAPIParameter, OpenAPISpec, OpenAPITag, Referenced } from '../types'; import {
OpenAPIOperation,
OpenAPIParameter,
OpenAPISpec,
OpenAPITag,
Referenced,
OpenAPIServer,
OpenAPIPaths,
} from '../types';
import { import {
isOperationName, isOperationName,
SECURITY_DEFINITIONS_COMPONENT_NAME, SECURITY_DEFINITIONS_COMPONENT_NAME,
setSecuritySchemePrefix, setSecuritySchemePrefix,
JsonPointer,
} from '../utils'; } from '../utils';
import { MarkdownRenderer } from './MarkdownRenderer'; import { MarkdownRenderer } from './MarkdownRenderer';
import { GroupModel, OperationModel } from './models'; import { GroupModel, OperationModel } from './models';
@ -15,12 +24,15 @@ export type TagInfo = OpenAPITag & {
}; };
export type ExtendedOpenAPIOperation = { export type ExtendedOpenAPIOperation = {
pointer: string;
pathName: string; pathName: string;
httpVerb: string; httpVerb: string;
pathParameters: Array<Referenced<OpenAPIParameter>>; pathParameters: Array<Referenced<OpenAPIParameter>>;
pathServers: Array<OpenAPIServer> | undefined;
isWebhook: boolean;
} & OpenAPIOperation; } & OpenAPIOperation;
export type TagsInfoMap = Dict<TagInfo>; export type TagsInfoMap = Record<string, TagInfo>;
export interface TagGroup { export interface TagGroup {
name: string; name: string;
@ -209,7 +221,12 @@ export class MenuBuilder {
tags[tag.name] = { ...tag, operations: [] }; tags[tag.name] = { ...tag, operations: [] };
} }
const paths = spec.paths; getTags(spec.paths);
if (spec['x-webhooks']) {
getTags(spec['x-webhooks'], true);
}
function getTags(paths: OpenAPIPaths, isWebhook?: boolean) {
for (const pathName of Object.keys(paths)) { for (const pathName of Object.keys(paths)) {
const path = paths[pathName]; const path = paths[pathName];
const operations = Object.keys(path).filter(isOperationName); const operations = Object.keys(path).filter(isOperationName);
@ -237,13 +254,16 @@ export class MenuBuilder {
tag.operations.push({ tag.operations.push({
...operationInfo, ...operationInfo,
pathName, pathName,
pointer: JsonPointer.compile(['paths', pathName, operationName]),
httpVerb: operationName, httpVerb: operationName,
pathParameters: path.parameters || [], pathParameters: path.parameters || [],
pathServers: path.servers,
isWebhook: !!isWebhook,
}); });
} }
} }
} }
}
return tags; return tags;
} }
} }

View File

@ -148,7 +148,7 @@ export class OpenAPIParser {
* @param obj object to dereference * @param obj object to dereference
* @param forceCircular whether to dereference even if it is circular 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)) { if (this.isRef(obj)) {
const resolved = this.byRef<T>(obj.$ref)!; const resolved = this.byRef<T>(obj.$ref)!;
const visited = this._refCounter.visited(obj.$ref); const visited = this._refCounter.visited(obj.$ref);
@ -239,7 +239,9 @@ export class OpenAPIParser {
receiver.type !== undefined && receiver.type !== undefined &&
subSchema.type !== undefined subSchema.type !== undefined
) { ) {
throw new Error(`Incompatible types in allOf at "${$ref}"`); console.warn(
`Incompatible types in allOf at "${$ref}": "${receiver.type}" and "${subSchema.type}"`,
);
} }
if (subSchema.type !== undefined) { if (subSchema.type !== undefined) {
@ -296,8 +298,8 @@ export class OpenAPIParser {
* returns map of definition pointer to definition name * returns map of definition pointer to definition name
* @param $refs array of references to find derived from * @param $refs array of references to find derived from
*/ */
findDerived($refs: string[]): Dict<string[]> { findDerived($refs: string[]): Record<string, string[] | string> {
const res: Dict<string[]> = {}; const res: Record<string, string[]> = {};
const schemas = (this.spec.components && this.spec.components.schemas) || {}; const schemas = (this.spec.components && this.spec.components.schemas) || {};
for (const defName in schemas) { for (const defName in schemas) {
const def = this.deref(schemas[defName]); const def = this.deref(schemas[defName]);

View File

@ -12,6 +12,7 @@ export interface RedocRawOptions {
expandResponses?: string | 'all'; expandResponses?: string | 'all';
requiredPropsFirst?: boolean | string; requiredPropsFirst?: boolean | string;
sortPropsAlphabetically?: boolean | string; sortPropsAlphabetically?: boolean | string;
sortEnumValuesAlphabetically?: boolean | string;
noAutoAuth?: boolean | string; noAutoAuth?: boolean | string;
nativeScrollbars?: boolean | string; nativeScrollbars?: boolean | string;
pathInMiddlePanel?: boolean | string; pathInMiddlePanel?: boolean | string;
@ -25,17 +26,20 @@ export interface RedocRawOptions {
menuToggle?: boolean | string; menuToggle?: boolean | string;
jsonSampleExpandLevel?: number | string | 'all'; jsonSampleExpandLevel?: number | string | 'all';
hideSchemaTitles?: boolean | string; hideSchemaTitles?: boolean | string;
simpleOneOfTypeLabel?: boolean | string;
payloadSampleIdx?: number; payloadSampleIdx?: number;
expandSingleSchemaField?: boolean | string;
unstable_ignoreMimeParameters?: boolean; unstable_ignoreMimeParameters?: boolean;
allowedMdComponents?: Dict<MDXComponentMeta>; allowedMdComponents?: Record<string, MDXComponentMeta>;
labels?: LabelsConfigRaw; labels?: LabelsConfigRaw;
enumSkipQuotes?: boolean | string; enumSkipQuotes?: boolean | string;
expandDefaultServerVariables?: boolean; expandDefaultServerVariables?: boolean;
maxDisplayedEnumValues?: number;
} }
function argValueToBoolean(val?: string | boolean, defaultValue?: boolean): boolean { function argValueToBoolean(val?: string | boolean, defaultValue?: boolean): boolean {
@ -48,6 +52,16 @@ function argValueToBoolean(val?: string | boolean, defaultValue?: boolean): bool
return val; return val;
} }
function argValueToNumber(value: number | string | undefined): number | undefined {
if (typeof value === 'string') {
return parseInt(value, 10);
}
if (typeof value === 'number') {
return value;
}
}
export class RedocNormalizedOptions { export class RedocNormalizedOptions {
static normalizeExpandResponses(value: RedocRawOptions['expandResponses']) { static normalizeExpandResponses(value: RedocRawOptions['expandResponses']) {
if (value === 'all') { if (value === 'all') {
@ -55,7 +69,7 @@ export class RedocNormalizedOptions {
} }
if (typeof value === 'string') { if (typeof value === 'string') {
const res = {}; const res = {};
value.split(',').forEach(code => { value.split(',').forEach((code) => {
res[code.trim()] = true; res[code.trim()] = true;
}); });
return res; return res;
@ -111,11 +125,18 @@ export class RedocNormalizedOptions {
return true; return true;
} }
if (typeof value === 'string') { if (typeof value !== 'string') {
return value.split(',').map(ext => ext.trim()); return value;
} }
return value; switch (value) {
case 'true':
return true;
case 'false':
return false;
default:
return value.split(',').map((ext) => ext.trim());
}
} }
static normalizePayloadSampleIdx(value: RedocRawOptions['payloadSampleIdx']): number { static normalizePayloadSampleIdx(value: RedocRawOptions['payloadSampleIdx']): number {
@ -146,6 +167,7 @@ export class RedocNormalizedOptions {
expandResponses: { [code: string]: boolean } | 'all'; expandResponses: { [code: string]: boolean } | 'all';
requiredPropsFirst: boolean; requiredPropsFirst: boolean;
sortPropsAlphabetically: boolean; sortPropsAlphabetically: boolean;
sortEnumValuesAlphabetically: boolean;
noAutoAuth: boolean; noAutoAuth: boolean;
nativeScrollbars: boolean; nativeScrollbars: boolean;
pathInMiddlePanel: boolean; pathInMiddlePanel: boolean;
@ -159,17 +181,32 @@ export class RedocNormalizedOptions {
jsonSampleExpandLevel: number; jsonSampleExpandLevel: number;
enumSkipQuotes: boolean; enumSkipQuotes: boolean;
hideSchemaTitles: boolean; hideSchemaTitles: boolean;
simpleOneOfTypeLabel: boolean;
payloadSampleIdx: number; payloadSampleIdx: number;
expandSingleSchemaField: boolean;
/* tslint:disable-next-line */ /* tslint:disable-next-line */
unstable_ignoreMimeParameters: boolean; unstable_ignoreMimeParameters: boolean;
allowedMdComponents: Dict<MDXComponentMeta>; allowedMdComponents: Record<string, MDXComponentMeta>;
expandDefaultServerVariables: boolean; expandDefaultServerVariables: boolean;
maxDisplayedEnumValues?: number;
constructor(raw: RedocRawOptions, defaults: RedocRawOptions = {}) { constructor(raw: RedocRawOptions, defaults: RedocRawOptions = {}) {
raw = { ...defaults, ...raw }; raw = { ...defaults, ...raw };
const hook = raw.theme && raw.theme.extensionsHook; const hook = raw.theme && raw.theme.extensionsHook;
// migrate from old theme
if ((raw.theme as any)?.menu && !raw.theme?.sidebar) {
console.warn('Theme setting "menu" is deprecated. Rename to "sidebar"');
raw.theme!.sidebar = (raw.theme as any).menu;
}
if ((raw.theme as any)?.codeSample && !raw.theme?.codeBlock) {
console.warn('Theme setting "codeSample" is deprecated. Rename to "codeBlock"');
raw.theme!.codeBlock = (raw.theme as any).codeSample;
}
this.theme = resolveTheme( this.theme = resolveTheme(
mergeObjects({} as any, defaultTheme, { ...raw.theme, extensionsHook: undefined }), mergeObjects({} as any, defaultTheme, { ...raw.theme, extensionsHook: undefined }),
); );
@ -184,6 +221,7 @@ export class RedocNormalizedOptions {
this.expandResponses = RedocNormalizedOptions.normalizeExpandResponses(raw.expandResponses); this.expandResponses = RedocNormalizedOptions.normalizeExpandResponses(raw.expandResponses);
this.requiredPropsFirst = argValueToBoolean(raw.requiredPropsFirst); this.requiredPropsFirst = argValueToBoolean(raw.requiredPropsFirst);
this.sortPropsAlphabetically = argValueToBoolean(raw.sortPropsAlphabetically); this.sortPropsAlphabetically = argValueToBoolean(raw.sortPropsAlphabetically);
this.sortEnumValuesAlphabetically = argValueToBoolean(raw.sortEnumValuesAlphabetically);
this.noAutoAuth = argValueToBoolean(raw.noAutoAuth); this.noAutoAuth = argValueToBoolean(raw.noAutoAuth);
this.nativeScrollbars = argValueToBoolean(raw.nativeScrollbars); this.nativeScrollbars = argValueToBoolean(raw.nativeScrollbars);
this.pathInMiddlePanel = argValueToBoolean(raw.pathInMiddlePanel); this.pathInMiddlePanel = argValueToBoolean(raw.pathInMiddlePanel);
@ -199,12 +237,15 @@ export class RedocNormalizedOptions {
); );
this.enumSkipQuotes = argValueToBoolean(raw.enumSkipQuotes); this.enumSkipQuotes = argValueToBoolean(raw.enumSkipQuotes);
this.hideSchemaTitles = argValueToBoolean(raw.hideSchemaTitles); this.hideSchemaTitles = argValueToBoolean(raw.hideSchemaTitles);
this.simpleOneOfTypeLabel = argValueToBoolean(raw.simpleOneOfTypeLabel);
this.payloadSampleIdx = RedocNormalizedOptions.normalizePayloadSampleIdx(raw.payloadSampleIdx); this.payloadSampleIdx = RedocNormalizedOptions.normalizePayloadSampleIdx(raw.payloadSampleIdx);
this.expandSingleSchemaField = argValueToBoolean(raw.expandSingleSchemaField);
this.unstable_ignoreMimeParameters = argValueToBoolean(raw.unstable_ignoreMimeParameters); this.unstable_ignoreMimeParameters = argValueToBoolean(raw.unstable_ignoreMimeParameters);
this.allowedMdComponents = raw.allowedMdComponents || {}; this.allowedMdComponents = raw.allowedMdComponents || {};
this.expandDefaultServerVariables = argValueToBoolean(raw.expandDefaultServerVariables); this.expandDefaultServerVariables = argValueToBoolean(raw.expandDefaultServerVariables);
this.maxDisplayedEnumValues = argValueToNumber(raw.maxDisplayedEnumValues);
} }
} }

View File

@ -9,7 +9,7 @@ const EVENT = 'scroll';
export class ScrollService { export class ScrollService {
private _scrollParent: Window | HTMLElement | undefined; private _scrollParent: Window | HTMLElement | undefined;
private _emiter: EventEmitter; private _emiter: EventEmitter;
private _prevOffsetY: number = 0; private _prevOffsetY = 0;
constructor(private options: RedocNormalizedOptions) { constructor(private options: RedocNormalizedOptions) {
this._scrollParent = IS_BROWSER ? window : undefined; this._scrollParent = IS_BROWSER ? window : undefined;
this._emiter = new EventEmitter(); this._emiter = new EventEmitter();

View File

@ -4,8 +4,8 @@ import { OperationModel } from './models';
import Worker from './SearchWorker.worker'; import Worker from './SearchWorker.worker';
function getWorker() {
let worker: new () => Worker; let worker: new () => Worker;
if (IS_BROWSER) { if (IS_BROWSER) {
try { try {
// tslint:disable-next-line // tslint:disable-next-line
@ -16,9 +16,11 @@ if (IS_BROWSER) {
} else { } else {
worker = require('./SearchWorker.worker').default; worker = require('./SearchWorker.worker').default;
} }
return new worker();
}
export class SearchStore<T> { export class SearchStore<T> {
searchWorker = new worker(); searchWorker = getWorker();
indexItems(groups: Array<IMenuItem | OperationModel>) { indexItems(groups: Array<IMenuItem | OperationModel>) {
const recurse = items => { const recurse = items => {
@ -38,6 +40,11 @@ export class SearchStore<T> {
this.searchWorker.add(title, body, meta); this.searchWorker.add(title, body, meta);
} }
dispose() {
(this.searchWorker as any).terminate();
(this.searchWorker as any).dispose();
}
search(q: string) { search(q: string) {
return this.searchWorker.search<T>(q); return this.searchWorker.search<T>(q);
} }

View File

@ -14,6 +14,7 @@ export default class Worker {
search: typeof search = search; search: typeof search = search;
toJS = toJS; toJS = toJS;
load = load; load = load;
dispose = dispose;
} }
export interface SearchDocument { export interface SearchDocument {
@ -29,23 +30,29 @@ export interface SearchResult<T = string> {
let store: any[] = []; let store: any[] = [];
let resolveIndex: (v: lunr.Index) => void = () => {
throw new Error('Should not be called');
};
const index: Promise<lunr.Index> = new Promise(resolve => {
resolveIndex = resolve;
});
lunr.tokenizer.separator = /\s+/; lunr.tokenizer.separator = /\s+/;
const builder = new lunr.Builder(); let builder: lunr.Builder;
let resolveIndex: (v: lunr.Index) => void;
let index: Promise<lunr.Index>;
function initEmpty() {
builder = new lunr.Builder();
builder.field('title'); builder.field('title');
builder.field('description'); builder.field('description');
builder.ref('ref'); builder.ref('ref');
builder.pipeline.add(lunr.trimmer, lunr.stopWordFilter, lunr.stemmer); builder.pipeline.add(lunr.trimmer, lunr.stopWordFilter, lunr.stemmer);
index = new Promise(resolve => {
resolveIndex = resolve;
});
}
initEmpty();
const expandTerm = term => '*' + lunr.stemmer(new lunr.Token(term, {})) + '*'; const expandTerm = term => '*' + lunr.stemmer(new lunr.Token(term, {})) + '*';
export function add<T>(title: string, description: string, meta?: T) { export function add<T>(title: string, description: string, meta?: T) {
@ -70,6 +77,11 @@ export async function load(state: any) {
resolveIndex(lunr.Index.load(state.index)); resolveIndex(lunr.Index.load(state.index));
} }
export async function dispose() {
store = [];
initEmpty();
}
export async function search<Meta = string>( export async function search<Meta = string>(
q: string, q: string,
limit = 0, limit = 0,

View File

@ -2,6 +2,7 @@ import { OpenAPIExternalDocumentation, OpenAPISpec } from '../types';
import { ContentItemModel, MenuBuilder } from './MenuBuilder'; import { ContentItemModel, MenuBuilder } from './MenuBuilder';
import { ApiInfoModel } from './models/ApiInfo'; import { ApiInfoModel } from './models/ApiInfo';
import { WebhookModel } from './models/Webhook';
import { SecuritySchemesModel } from './models/SecuritySchemes'; import { SecuritySchemesModel } from './models/SecuritySchemes';
import { OpenAPIParser } from './OpenAPIParser'; import { OpenAPIParser } from './OpenAPIParser';
import { RedocNormalizedOptions } from './RedocNormalizedOptions'; import { RedocNormalizedOptions } from './RedocNormalizedOptions';
@ -15,6 +16,7 @@ export class SpecStore {
externalDocs?: OpenAPIExternalDocumentation; externalDocs?: OpenAPIExternalDocumentation;
contentItems: ContentItemModel[]; contentItems: ContentItemModel[];
securitySchemes: SecuritySchemesModel; securitySchemes: SecuritySchemesModel;
webhooks?: WebhookModel;
constructor( constructor(
spec: OpenAPISpec, spec: OpenAPISpec,
@ -26,5 +28,6 @@ export class SpecStore {
this.externalDocs = this.parser.spec.externalDocs; this.externalDocs = this.parser.spec.externalDocs;
this.contentItems = MenuBuilder.buildStructure(this.parser, this.options); this.contentItems = MenuBuilder.buildStructure(this.parser, this.options);
this.securitySchemes = new SecuritySchemesModel(this.parser); this.securitySchemes = new SecuritySchemesModel(this.parser);
this.webhooks = new WebhookModel(this.parser, options, this.parser.spec['x-webhooks']);
} }
} }

View File

@ -8,6 +8,7 @@ describe('Models', () => {
let parser; let parser;
test('should hoist oneOfs when mergin allOf', () => { test('should hoist oneOfs when mergin allOf', () => {
// eslint-disable-next-line @typescript-eslint/no-var-requires
const spec = require('./fixtures/oneOfHoist.json'); const spec = require('./fixtures/oneOfHoist.json');
parser = new OpenAPIParser(spec, undefined, opts); parser = new OpenAPIParser(spec, undefined, opts);
expect(parser.mergeAllOf(spec.components.schemas.test)).toMatchSnapshot(); expect(parser.mergeAllOf(spec.components.schemas.test)).toMatchSnapshot();

View File

@ -0,0 +1,64 @@
{
"openapi": "3.0.0",
"info": {
"version": "1.0",
"title": "Foo"
},
"components": {
"callbacks": {
"Test": {
"post": {
"operationId": "testCallback",
"description": "Test callback.",
"requestBody": {
"content": {
"application/json": {
"schema": {
"title": "TestTitle",
"type": "object",
"description": "Test description",
"properties": {
"type": {
"type": "string",
"description": "The type of response.",
"enum": [
"TestResponse.Complete"
]
},
"status": {
"type": "string",
"enum": [
"FAILURE",
"SUCCESS"
]
}
},
"required": [
"status"
]
}
}
}
},
"parameters": [
{
"name": "X-Test-Header",
"in": "header",
"required": true,
"example": "1",
"description": "This is a test header parameter",
"schema": {
"type": "string"
}
}
],
"responses": {
"204": {
"description": "Test response."
}
}
}
}
}
}
}

Some files were not shown because too many files have changed in this diff Show More