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
!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:
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!
4. Ensure the test suite passes (`yarn test`). Tip: `yarn test --watch TestName` is helpful in development.
5. Format your code with prettier (`yarn prettier`).
4. Ensure the test suite passes (`npm test`). Tip: `npm test -- --watch TestName` is helpful in development.
5. Format your code with prettier (`npm run prettier`).
## Development Setup
You will need [Node.js](http://nodejs.org) at `v8.0.0+` and [Yarn](https://yarnpkg.com/en/) at `v1.2.0+`
You will need [Node.js](http://nodejs.org) at `v12.0.0+`.
After cloning the repo, run:
```bash
$ yarn install # or npm
$ npm install # or npm
```
### Commonly used NPM scripts
``` bash
# dev-server, watch and auto reload playground
$ yarn start
$ npm start
# start playground app in production environment
$ yarn start:prod
$ npm run start:prod
# runt tslint
$ yarn lint
$ npm run lint
# try autofix tslint issues
$ yarn lint --fix
$ npm run lint -- --fix
# run unit tests
$ yarn unit
$ npm run unit
# run e2e tests
$ yarn e2e
$ npm run e2e
# 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
$ yarn cy:open
$ npm run cy:open
# run the full test suite, include linting / unit / e2e
$ yarn test
# run the unit tests (includes linting and license checks)
$ npm test
# prepare bundles
$ yarn bundle
$ npm run bundle
# format the code using prettier
$ yarn prettier
$ npm run prettier
# auto-generate changelog
$ yarn changelog
$ npm run changelog
```
There are some other scripts available in the `scripts` section of the `package.json` file.

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]
jobs:
build:
build-and-unit:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v1
- name: Use Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v1
with:
node-version: 10.x
- name: yarn install, build, and test
run: |
npm install -g yarn
yarn install
yarn bundle
yarn test
- run: npm ci
- run: npm run bundle
- run: npm test

3
.gitignore vendored
View File

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

View File

@ -2,7 +2,6 @@ language: node_js
node_js:
- '10'
cache:
yarn: true
directories:
- "~/.cache"
env:
@ -10,11 +9,9 @@ env:
- GH_REF: github.com/Redocly/redoc.git
- GIT_AUTHOR_EMAIL: redoc-bot@users.noreply.github.com
- 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: 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: Pc86j2/rgCPAEWcjzPbbVFkL2SwNt4CpeXP69zedMi9RomQblMJa9R0wbAT6H5VCnPky2cpmxkzjFWOc91N9crzceg6aOoWkPr6pTTnTv+EL70i6+XSrAFpkgRjszprxnU1Bz+GcYEjP/zR8479fx8ooSl7MwHOaP7XiQyaBQAbY1CPlmpT+b4Ut7Fm5QnD90/NgPjbKkngl0kVUfNFdFOSfJ3QWLyFCUUSQ4DlxccJOTIaOH/n8u9Nz4NTuuHE3XeRuEsuj2SJutJnFBUYwsvugrdPvKWiubkewJfylp6EwABzByENsg6XxW9SIq80lMc3Oi7ld9L2lAgpj+8/42olnbMzH0F0rw/p1ccPAdVwQVV6YFaqCzivK5A5aX5LmGKwJ6SR9k1PgcWP6sKKMIsIEObbyM88Tke3QkreEz+cLg/3jjko7Vpb0tbqh8BtbpWV+exL4rX3r2C5Mb1Es1W597hN5LSczWYFgw0ZETpfbVZg6Ri1iZks0wpsT/E+c0q2scUaBVrdTZseHxUPB7mPDlXL1l9/i4sOxPyBHZtJRAzeTT/fOXfj4vuD+ihspXzoRRLaQbizlb8FpyPA47XdmBDpXi3OBiaIFLwvybEn7qM7rqvWxdz6vvCZv0t/AN3t3Qvh2vHKCshHecaa8NoJQHWrdFMHeecYHyeoujZ8=
addons:
chrome: stable
apt:
@ -35,6 +32,6 @@ deploy:
tags: true
- provider: script
skip_cleanup: true
script: yarn deploy:demo
script: npm run deploy:demo
on:
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)

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 |
## Some Real-life usages
- [Rebilly](https://rebilly.github.io/RebillyAPI)
- [Rebilly](https://rebilly-api.redoc.ly/)
- [Docker Engine](https://docs.docker.com/engine/api/v1.25/)
- [Zuora](https://www.zuora.com/developer/api-reference/)
- [Shopify Draft Orders](https://help.shopify.com/api/draft-orders)
- [Discourse](http://docs.discourse.org)
- [Commbox](https://www.commbox.io/api/)
- [APIs.guru](https://apis.guru/api-doc/)
- [FastAPI](https://github.com/tiangolo/fastapi)
@ -107,14 +107,14 @@ That's all folks!
**IMPORTANT NOTE:** if you work with untrusted user spec, use `untrusted-spec` [option](#redoc-options-object) to prevent XSS security risks.
### 1. Install ReDoc (skip this step for CDN)
Install using [yarn](https://yarnpkg.com):
Install using [npm](https://docs.npmjs.com/getting-started/what-is-npm):
npm i redoc
or using [yarn](https://yarnpkg.com):
yarn add redoc
or using [npm](https://docs.npmjs.com/getting-started/what-is-npm):
npm install redoc --save
### 2. Reference redoc script in HTML
For **CDN**:
```html
@ -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):
* [`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-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-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
@ -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.
* `hideLoading` - do not show loading animation. Useful for small docs.
* `hideSingleRequestSampleTab` - do not show the request sample tab for requests with only one sample.
* `expandSingleSchemaField` - automatically expand single field in a schema
* `jsonSampleExpandLevel` - set the default expand level for JSON payload samples (responses and request body). Special value 'all' expands all levels. The default value is `2`.
* `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.~~
* `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).

View File

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

View File

@ -50,6 +50,11 @@ YargsParser.command(
describe: 'path or URL to your spec',
});
yargs.options('title', {
describe: 'Page Title',
type: 'string',
});
yargs.option('s', {
alias: 'ssr',
describe: 'Enable server-side rendering',
@ -73,6 +78,7 @@ YargsParser.command(
async argv => {
const config: Options = {
ssr: argv.ssr as boolean,
title: argv.title as string,
watch: argv.watch as boolean,
templateFileName: argv.template as string,
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",
"version": "0.9.5",
"version": "0.9.12",
"description": "ReDoc's Command Line Interface",
"main": "index.js",
"bin": "index.js",
@ -11,25 +11,25 @@
"node": ">= 8"
},
"dependencies": {
"chokidar": "^3.0.2",
"handlebars": "^4.1.2",
"chokidar": "^3.4.1",
"handlebars": "^4.7.6",
"isarray": "^2.0.5",
"mkdirp": "^0.5.1",
"mkdirp": "^1.0.4",
"mobx": "^4.2.0",
"node-libs-browser": "^2.2.1",
"react": "^16.8.6",
"react-dom": "^16.8.6",
"redoc": "2.0.0-rc.21",
"styled-components": "^4.3.2",
"tslib": "^1.10.0",
"yargs": "^13.3.0"
"react": "^16.13.1",
"react-dom": "^16.13.1",
"redoc": "^2.0.0-rc.40",
"styled-components": "^5.1.1",
"tslib": "^2.0.0",
"yargs": "^15.4.1"
},
"publishConfig": {
"access": "public"
},
"devDependencies": {
"@types/chokidar": "^2.1.3",
"@types/handlebars": "^4.0.39",
"@types/mkdirp": "^0.5.2"
"@types/handlebars": "^4.1.0",
"@types/mkdirp": "^1.0.1"
}
}

File diff suppressed because it is too large Load Diff

View File

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

View File

@ -12,6 +12,12 @@ Serve local file:
docker run -it --rm -p 80:80 \
-v $(pwd)/demo/swagger.yaml:/usr/share/nginx/html/swagger.yaml \
-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

View File

@ -21,6 +21,13 @@ http {
alias /usr/share/nginx/html/;
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-Methods' 'GET, POST, OPTIONS';
#
@ -36,11 +43,25 @@ http {
return 204;
}
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-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';
}
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-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';

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/zoom.us/2.0.0/swagger.yaml', label: 'Zoom.us' },
{
value: 'https://api.apis.guru/v2/specs/graphhopper.com/1.0/swagger.yaml',
label: 'GraphHopper',
},
{ value: 'https://docs.graphhopper.com/openapi.json', label: 'GraphHopper' },
];
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"
frameBorder="0"
scrolling="0"
width="150px"
width="160px"
height="30px"
/>
</Heading>
<RedocStandalone specUrl={proxiedUrl} options={{ scrollYOffset: 'nav' }} />
<RedocStandalone
specUrl={proxiedUrl}
options={{ scrollYOffset: 'nav', untrustedSpec: true }}
/>
</>
);
}

View File

@ -114,7 +114,7 @@ paths:
- petstore_auth:
- 'write:pets'
- 'read:pets'
x-code-samples:
x-codeSamples:
- lang: 'C#'
source: |
PetStore.v1.Pet pet = new PetStore.v1.Pet();
@ -162,7 +162,7 @@ paths:
- petstore_auth:
- 'write:pets'
- 'read:pets'
x-code-samples:
x-codeSamples:
- lang: PHP
source: |
$form = new \PetStore\Entities\Pet();
@ -489,6 +489,234 @@ paths:
description: Invalid ID supplied
'404':
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:
post:
tags:
@ -955,7 +1183,23 @@ components:
examples:
Order:
value:
quantity: 1,
shipDate: 2018-10-19T16:46:45Z,
status: placed,
quantity: 1
shipDate: '2018-10-19T16:46:45Z'
status: placed
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');
let store;
const options: RedocRawOptions = { nativeScrollbars: false };
const options: RedocRawOptions = { nativeScrollbars: false, maxDisplayedEnumValues: 3 };
async function init() {
const spec = await loadAndBundleSpec(specUrl);

View File

@ -1,5 +1,5 @@
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 { compact } from 'lodash';
import { resolve } from 'path';
@ -7,17 +7,14 @@ import * as webpack from 'webpack';
const VERSION = JSON.stringify(require('../package.json').version);
const REVISION = JSON.stringify(
require('child_process')
.execSync('git rev-parse --short HEAD')
.toString()
.trim(),
require('child_process').execSync('git rev-parse --short HEAD').toString().trim(),
);
function root(filename) {
return resolve(__dirname + '/' + filename);
}
const tsLoader = env => ({
const tsLoader = (env) => ({
loader: 'ts-loader',
options: {
compilerOptions: {
@ -27,7 +24,7 @@ const tsLoader = env => ({
},
});
const babelLoader = mode => ({
const babelLoader = () => ({
loader: 'babel-loader',
options: {
generatorOpts: {
@ -38,13 +35,6 @@ const babelLoader = mode => ({
['@babel/plugin-syntax-decorators', { legacy: true }],
'@babel/plugin-syntax-dynamic-import',
'@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([
mode !== 'production' ? babelHotLoader : undefined,
tsLoader(env),
babelLoader(mode),
babelLoader(),
]),
exclude: [/node_modules/],
},
@ -161,7 +151,9 @@ export default (env: { playground?: boolean; bench?: boolean } = {}, { mode }) =
ignore(/js-yaml\/dumper\.js$/),
ignore(/json-schema-ref-parser\/lib\/dereference\.js/),
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
Extend OpenAPI root [Swagger Object](https://swagger.io/specification/#oasObject)
#### 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
@ -162,13 +162,13 @@ x-traitTag: true
### Operation Object vendor extensions
Extends OpenAPI [Operation Object](http://swagger.io/specification/#operationObject)
#### x-code-samples
#### x-codeSamples
| 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
`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
Operation code sample
@ -306,3 +306,37 @@ Player:
x-additionalPropertiesName: attribute-name
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', () => {
cy.get('.menu-content')
.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', () => {

View File

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

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

View File

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

View File

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

View File

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

View File

@ -5,8 +5,19 @@ import { PropertyNameCell } from './fields-layout';
import { ShelfIcon } from './shelfs';
export const ClickablePropertyNameCell = styled(PropertyNameCell)`
cursor: pointer;
button {
background-color: transparent;
border: 0;
outline: 0;
font-size: 13px;
font-family: ${props => props.theme.typography.code.fontFamily};
cursor: pointer;
padding: 0;
color: ${props => props.theme.colors.text.primary};
&:focus {
font-weight: ${({ theme }) => theme.typography.fontWeightBold};
}
}
${ShelfIcon} {
height: ${({ theme }) => theme.schema.arrow.size};
width: ${({ theme }) => theme.schema.arrow.size};
@ -97,3 +108,14 @@ export const ConstraintItem = styled(FieldLabel)`
}
${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;
width: 20px;
display: inline-block;
outline: 0;
}
${className}:before {
content: '';

View File

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

View File

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

View File

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

View File

@ -26,6 +26,7 @@ class IntShelfIcon extends React.PureComponent<{
x="0"
xmlns="http://www.w3.org/2000/svg"
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 " />
</svg>
@ -50,10 +51,17 @@ export const ShelfIcon = styled(IntShelfIcon)`
export const Badge = styled.span<{ type: string }>`
display: inline-block;
padding: 0 5px;
padding: 2px 8px;
margin: 0;
background-color: ${props => props.theme.colors[props.type].main};
color: ${props => props.theme.colors[props.type].contrastText};
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;
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);
cursor: pointer;
text-align: center;
@ -24,7 +24,7 @@ export const Tabs = styled(ReactTabs)`
color: ${({ theme }) => darken(theme.colors.tonalOffset, theme.rightPanel.textColor)};
margin: 0
${({ 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;
min-width: 60px;
font-size: 0.9em;
@ -33,6 +33,9 @@ export const Tabs = styled(ReactTabs)`
&.react-tabs__tab--selected {
color: ${props => props.theme.colors.text.primary};
background: ${({ theme }) => theme.rightPanel.textColor};
&:focus {
outline: auto;
}
}
&:only-child {
@ -58,7 +61,7 @@ export const Tabs = styled(ReactTabs)`
}
}
> .react-tabs__tab-panel {
background: ${({ theme }) => theme.codeSample.backgroundColor};
background: ${({ theme }) => theme.codeBlock.backgroundColor};
& > div,
& > pre {
padding: ${props => props.theme.spacing.unit * 4}px;

View File

@ -81,7 +81,7 @@ export class ApiInfo extends React.Component<ApiInfoProps> {
<p>
Download OpenAPI specification:
<DownloadButton
download={downloadFilename}
download={downloadFilename || true}
target="_blank"
href={downloadLink}
onClick={this.handleDownloadClick}
@ -100,7 +100,7 @@ export class ApiInfo extends React.Component<ApiInfoProps> {
)) ||
null}
</StyledMarkdownBlock>
<Markdown source={store.spec.info.description} />
<Markdown source={store.spec.info.description} data-role="redoc-description" />
{externalDocs && <ExternalDocumentation externalDocs={externalDocs} />}
</MiddlePanel>
</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 { AdvancedMarkdown } from '../Markdown/AdvancedMarkdown';
import { H1, H2, MiddlePanel, Row, Section, ShareLink } from '../../common-elements';
import { ContentItemModel } from '../../services/MenuBuilder';
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 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 {
const { Label = MimeLabel, Dropdown = SimpleDropdown } = props;
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;
inverted?: boolean;
compact?: boolean;
}
export interface EndpointState {
@ -49,7 +50,9 @@ export class Endpoint extends React.Component<EndpointProps, EndpointState> {
{options => (
<OperationEndpointWrap>
<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>
<ShelfIcon
float={'right'}
@ -59,7 +62,7 @@ export class Endpoint extends React.Component<EndpointProps, EndpointState> {
style={{ marginRight: '-25px' }}
/>
</EndpointInfo>
<ServersOverlay expanded={expanded}>
<ServersOverlay expanded={expanded} aria-hidden={!expanded}>
{operation.servers.map(server => {
const normalizedUrl = options.expandDefaultServerVariables
? expandDefaultServerVariables(server.url, server.variables)

View File

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

View File

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

View File

@ -3,28 +3,62 @@ import { ExampleValue, FieldLabel } from '../../common-elements/fields';
import { l } from '../../services/Labels';
import { OptionsContext } from '../OptionsProvider';
import styled from '../../styled-components';
import { RedocRawOptions } from '../../services/RedocNormalizedOptions';
export interface EnumValuesProps {
values: 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;
private toggle() {
this.setState({ collapsed: !this.state.collapsed });
}
render() {
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) {
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 (
<div>
<FieldLabel>
{type === 'array' ? l('enumArray') : ''}{' '}
{values.length === 1 ? l('enumSingleValue') : l('enum')}:
</FieldLabel>{' '}
{values.map((value, idx) => {
{displayedItems.map((value, idx) => {
const exampleValue = enumSkipQuotes ? value : JSON.stringify(value);
return (
<React.Fragment key={idx}>
@ -32,7 +66,25 @@ export class EnumValues extends React.PureComponent<EnumValuesProps> {
</React.Fragment>
);
})}
{showToggleButton ? (
<ToggleButton
onClick={() => {
this.toggle();
}}
>
{toggleButtonText}
</ToggleButton>
) : null}
</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;
field: FieldModel;
expandByDefault?: boolean;
renderDiscriminatorSwitch?: (opts: FieldProps) => JSX.Element;
}
@ -30,23 +31,42 @@ export interface FieldProps extends SchemaOptions {
@observer
export class Field extends React.Component<FieldProps> {
toggle = () => {
this.props.field.toggle();
if (this.props.field.expanded === undefined && this.props.expandByDefault) {
this.props.field.expanded = false;
} else {
this.props.field.toggle();
}
};
handleKeyPress = e => {
if (e.key === 'Enter') {
e.preventDefault();
this.toggle();
}
};
render() {
const { className, field, isLast } = this.props;
const { name, expanded, deprecated, required, kind } = field;
const { className, field, isLast, expandByDefault } = this.props;
const { name, deprecated, required, kind } = field;
const withSubSchema = !field.schema.isPrimitive && !field.schema.isCircular;
const expanded = field.expanded === undefined ? expandByDefault : field.expanded;
const paramName = withSubSchema ? (
<ClickablePropertyNameCell
onClick={this.toggle}
className={deprecated ? 'deprecated' : ''}
kind={kind}
title={name}
>
<PropertyBullet />
{name}
<ShelfIcon direction={expanded ? 'down' : 'right'} />
<button
onClick={this.toggle}
onKeyPress={this.handleKeyPress}
aria-label="expand properties"
>
{name}
<ShelfIcon direction={expanded ? 'down' : 'right'} />
</button>
{required && <RequiredLabel> required </RequiredLabel>}
</ClickablePropertyNameCell>
) : (
@ -65,7 +85,7 @@ export class Field extends React.Component<FieldProps> {
<FieldDetails {...this.props} />
</PropertyDetailsCell>
</tr>
{field.expanded && withSubSchema && (
{expanded && withSubSchema && (
<tr key={field.name + 'inner'}>
<PropertyCellWithInner colSpan={2}>
<InnerPropertiesWrap>

View File

@ -8,6 +8,7 @@ import {
TypeName,
TypePrefix,
TypeTitle,
ToggleButton,
} from '../../common-elements/fields';
import { serializeParameterValue } from '../../utils/openapi';
import { ExternalDocumentation } from '../ExternalDocumentation/ExternalDocumentation';
@ -23,10 +24,24 @@ import { Badge } from '../../common-elements/';
import { l } from '../../services/Labels';
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;
togglePattern = () => {
this.setState({
patternShown: !this.state.patternShown,
});
};
render() {
const { showExamples, field, renderDiscriminatorSwitch } = this.props;
const { patternShown } = this.state;
const { enumSkipQuotes, hideSchemaTitles } = this.context;
const { schema, description, example, deprecated } = field;
@ -62,7 +77,20 @@ export class FieldDetails extends React.PureComponent<FieldProps> {
{schema.title && !hideSchemaTitles && <TypeTitle> ({schema.title}) </TypeTitle>}
<ConstraintsView constraints={schema.constraints} />
{schema.nullable && <NullableLabel> {l('nullable')} </NullableLabel>}
{schema.pattern && <PatternLabel> {schema.pattern} </PatternLabel>}
{schema.pattern && (
<>
<PatternLabel>
{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>}
</div>
{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>
<SampleControls>
{renderCopyButton()}
<span onClick={this.expandAll}> Expand all </span>
<span onClick={this.collapseAll}> Collapse all </span>
<button onClick={this.expandAll}> Expand all </button>
<button onClick={this.collapseAll}> Collapse all </button>
</SampleControls>
<OptionsContext.Consumer>
{options => (
@ -57,11 +57,10 @@ class Json extends React.PureComponent<JsonProps> {
collapseAll = () => {
const elements = this.node.getElementsByClassName('collapsible');
for (const expanded of Array.prototype.slice.call(elements)) {
// const collapsed = elements[i];
if ((expanded.parentNode as Element)!.classList.contains('redoc-json')) {
continue;
}
// skip first item to avoid collapsing whole object/array
const elementsArr = Array.prototype.slice.call(elements, 1);
for (const expanded of elementsArr) {
(expanded.parentNode as Element)!.classList.add('collapsed');
}
};

View File

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

View File

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

View File

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

View File

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

View File

@ -26,7 +26,7 @@ export interface ParametersProps {
const PARAM_PLACES = ['path', 'query', 'cookie', 'header'];
export class Parameters extends React.PureComponent<ParametersProps> {
orderParams(params: FieldModel[]): Dict<FieldModel[]> {
orderParams(params: FieldModel[]): Record<string, FieldModel[]> {
const res = {};
params.forEach(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;
return (
<MediaTypesSwitch content={content} renderDropdown={DropdownWithinHeader}>

View File

@ -29,7 +29,12 @@ export function ExternalExample({ example, mimeType }: ExampleProps) {
return (
<StyledPre>
Error loading external example: <br />
<a className={'token string'} href={example.externalValueUrl} target="_blank">
<a
className={'token string'}
href={example.externalValueUrl}
target="_blank"
rel="noopener noreferrer"
>
{example.externalValueUrl}
</a>
</StyledPre>

View File

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

View File

@ -1,16 +1,15 @@
// @ts-ignore
import Dropdown from 'react-dropdown';
import { transparentize } from 'polished';
import styled from '../../styled-components';
import { StyledDropdown } from '../../common-elements';
export const MimeLabel = styled.div`
padding: 12px;
padding: 0.9em;
background-color: ${({ theme }) => transparentize(0.6, theme.rightPanel.backgroundColor)};
margin: 0 0 10px 0;
display: block;
font-family: ${({ theme }) => theme.typography.headings.fontFamily};
font-size: 0.929em;
line-height: 1.5em;
`;
export const DropdownLabel = styled.span`
@ -29,31 +28,45 @@ export const DropdownWrapper = styled.div`
`;
export const InvertedSimpleDropdown = styled(StyledDropdown)`
margin-left: 10px;
text-transform: none;
font-size: 0.929em;
margin: 0 0 10px 0;
display: block;
background-color: ${({ theme }) => transparentize(0.6, theme.rightPanel.backgroundColor)};
.Dropdown-control {
margin-top: 0;
}
.Dropdown-control,
.Dropdown-control:hover {
&& {
margin-left: 10px;
text-transform: none;
font-size: 0.929em;
margin: 0 0 10px 0;
display: block;
background-color: ${({ theme }) => transparentize(0.6, theme.rightPanel.backgroundColor)};
font-size: 1em;
border: none;
padding: 0.9em 1.6em 0.9em 0.9em;
background: transparent;
color: ${({ theme }) => theme.rightPanel.textColor};
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};
}
}
.Dropdown-menu {
margin: 0;
margin-top: 2px;
.dropdown-selector-value {
text-overflow: ellipsis;
white-space: nowrap;
overflow: hidden;
color: ${({ theme }) => theme.rightPanel.textColor};
}
.dropdown-selector-content {
margin: 0;
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;
position: relative;
overflow: hidden;
width: calc(100% - ${props => props.theme.menu.width});
width: calc(100% - ${props => props.theme.sidebar.width});
${media.lessThan('small', true)`
width: 100%;
`};
@ -46,7 +46,7 @@ export const BackgroundStub = styled.div`
width: ${({ theme }) => {
if (theme.rightPanel.width.endsWith('%')) {
const percents = parseInt(theme.rightPanel.width, 10);
return `calc((100% - ${theme.menu.width}) * ${percents / 100})`;
return `calc((100% - ${theme.sidebar.width}) * ${percents / 100})`;
} else {
return theme.rightPanel.width;
}

View File

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

View File

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

View File

@ -5,6 +5,10 @@ import styled from '../../styled-components';
import { ResponseTitle } from './ResponseTitle';
export const StyledResponseTitle = styled(ResponseTitle)`
display: block;
border: 0;
width: 100%;
text-align: left;
padding: 10px;
border-radius: 2px;
margin-bottom: 4px;
@ -12,10 +16,13 @@ export const StyledResponseTitle = styled(ResponseTitle)`
background-color: #f2f2f2;
cursor: pointer;
color: ${props => props.theme.colors.responses[props.type].color};
background-color: ${props => props.theme.colors.responses[props.type].backgroundColor};
${props =>
color: ${(props) => props.theme.colors.responses[props.type].color};
background-color: ${(props) => props.theme.colors.responses[props.type].backgroundColor};
&:focus {
outline: auto;
outline-color: ${(props) => props.theme.colors.responses[props.type].color};
}
${(props) =>
(props.empty &&
`
cursor: default;
@ -25,6 +32,10 @@ cursor: default;
width: 1.5em;
text-align: center;
display: inline-block;
vertical-align: top;
}
&:focus {
outline: 0;
}
`) ||
''};
@ -39,3 +50,7 @@ export const HeadersCaption = styled(UnderlinedHeader.withComponent('caption'))`
margin-top: 1em;
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 styled from '../../styled-components';
import {humanizeConstraints} from "../../utils";
const PaddedSchema = styled.div`
padding-left: ${({ theme }) => theme.spacing.unit * 2}px;
@ -12,9 +13,16 @@ const PaddedSchema = styled.div`
export class ArraySchema extends React.PureComponent<SchemaProps> {
render() {
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 (
<div>
<ArrayOpenningLabel> Array </ArrayOpenningLabel>
<ArrayOpenningLabel> Array ({minMaxItems})</ArrayOpenningLabel>
<PaddedSchema>
<Schema {...this.props} schema={itemsSchema} />
</PaddedSchema>

View File

@ -21,7 +21,7 @@ export class DiscriminatorDropdown extends React.Component<{
});
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) => {
return {
value: idx.toString(),
label: subSchema.title,
value: subSchema.title,
idx,
};
});
const activeItem = options[parent.activeOneOf];
const activeValue = options[parent.activeOneOf].value;
this.sortOptions(options, enumValues);
return (
<StyledDropdown value={activeItem} options={options} onChange={this.changeActiveChild} />
<StyledDropdown value={activeValue} options={options} onChange={this.changeActiveChild} />
);
}
changeActiveChild = ({ value }) => {
const idx = parseInt(value, 10);
this.props.parent.activateOneOf(idx);
changeActiveChild = (option: DropdownOption) => {
this.props.parent.activateOneOf(option.idx);
};
}

View File

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

View File

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

View File

@ -80,7 +80,7 @@ export class SchemaDefinition extends React.PureComponent<ObjectDescriptionProps
}
const MediaSamplesWrap = styled.div`
background: ${({ theme }) => theme.codeSample.backgroundColor};
background: ${({ theme }) => theme.codeBlock.backgroundColor};
& > div,
& > pre {
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 { SearchResult } from '../../services/SearchWorker.worker';
import { bind, debounce } from 'decko';
import { PerfectScrollbarWrap } from '../../common-elements/perfect-scrollbar';
import {
ClearIcon,
@ -96,7 +97,6 @@ export class SearchBox extends React.PureComponent<SearchBoxProps, SearchBoxStat
setResults(results: SearchResult[], term: string) {
this.setState({
results,
term,
});
this.props.marker.mark(term);
}

View File

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

View File

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

View File

@ -4,14 +4,20 @@ import { ClipboardService } from '../../services';
export class SelectOnClick extends React.PureComponent {
private child: HTMLDivElement | null;
handleClick = () => {
selectElement = () => {
ClipboardService.selectElement(this.child);
};
render() {
const { children } = this.props;
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}
</div>
);

View File

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

View File

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

View File

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

View File

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

View File

@ -9,7 +9,7 @@ exports[`Components SchemaView discriminator should correctly render discriminat
"deprecated": false,
"description": "",
"example": undefined,
"expanded": false,
"expanded": undefined,
"explode": false,
"in": undefined,
"kind": "field",
@ -59,7 +59,7 @@ exports[`Components SchemaView discriminator should correctly render discriminat
"deprecated": false,
"description": "",
"example": undefined,
"expanded": false,
"expanded": undefined,
"explode": false,
"in": undefined,
"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 './ResponseSamples/ResponseSamples';
export * from './PayloadSamples/PayloadSamples';
export * from './PayloadSamples/styled.elements';
export * from './MediaTypeSwitch/MediaTypesSwitch';
export * from './Parameters/Parameters';
export * from './PayloadSamples/Example';
export * from './DropdownOrLabel/DropdownOrLabel';
export * from './ErrorBoundary';
export * from './StoreBuilder';

View File

@ -1,5 +1,14 @@
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 './utils';

View File

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

View File

@ -8,6 +8,7 @@ export interface LabelsConfig {
nullable: string;
recursive: string;
arrayOf: string;
webhook: string;
}
export type LabelsConfigRaw = Partial<LabelsConfig>;
@ -22,6 +23,7 @@ const labels: LabelsConfig = {
nullable: 'Nullable',
recursive: 'Recursive',
arrayOf: 'Array of ',
webhook: 'Event',
};
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 {
isOperationName,
SECURITY_DEFINITIONS_COMPONENT_NAME,
setSecuritySchemePrefix,
JsonPointer,
} from '../utils';
import { MarkdownRenderer } from './MarkdownRenderer';
import { GroupModel, OperationModel } from './models';
@ -15,12 +24,15 @@ export type TagInfo = OpenAPITag & {
};
export type ExtendedOpenAPIOperation = {
pointer: string;
pathName: string;
httpVerb: string;
pathParameters: Array<Referenced<OpenAPIParameter>>;
pathServers: Array<OpenAPIServer> | undefined;
isWebhook: boolean;
} & OpenAPIOperation;
export type TagsInfoMap = Dict<TagInfo>;
export type TagsInfoMap = Record<string, TagInfo>;
export interface TagGroup {
name: string;
@ -209,41 +221,49 @@ export class MenuBuilder {
tags[tag.name] = { ...tag, operations: [] };
}
const paths = spec.paths;
for (const pathName of Object.keys(paths)) {
const path = paths[pathName];
const operations = Object.keys(path).filter(isOperationName);
for (const operationName of operations) {
const operationInfo = path[operationName];
let operationTags = operationInfo.tags;
getTags(spec.paths);
if (spec['x-webhooks']) {
getTags(spec['x-webhooks'], true);
}
if (!operationTags || !operationTags.length) {
// empty tag
operationTags = [''];
}
function getTags(paths: OpenAPIPaths, isWebhook?: boolean) {
for (const pathName of Object.keys(paths)) {
const path = paths[pathName];
const operations = Object.keys(path).filter(isOperationName);
for (const operationName of operations) {
const operationInfo = path[operationName];
let operationTags = operationInfo.tags;
for (const tagName of operationTags) {
let tag = tags[tagName];
if (tag === undefined) {
tag = {
name: tagName,
operations: [],
};
tags[tagName] = tag;
if (!operationTags || !operationTags.length) {
// empty tag
operationTags = [''];
}
if (tag['x-traitTag']) {
continue;
for (const tagName of operationTags) {
let tag = tags[tagName];
if (tag === undefined) {
tag = {
name: tagName,
operations: [],
};
tags[tagName] = tag;
}
if (tag['x-traitTag']) {
continue;
}
tag.operations.push({
...operationInfo,
pathName,
pointer: JsonPointer.compile(['paths', pathName, operationName]),
httpVerb: operationName,
pathParameters: path.parameters || [],
pathServers: path.servers,
isWebhook: !!isWebhook,
});
}
tag.operations.push({
...operationInfo,
pathName,
httpVerb: operationName,
pathParameters: path.parameters || [],
});
}
}
}
return tags;
}
}

View File

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

View File

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

View File

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

View File

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

View File

@ -14,6 +14,7 @@ export default class Worker {
search: typeof search = search;
toJS = toJS;
load = load;
dispose = dispose;
}
export interface SearchDocument {
@ -29,22 +30,28 @@ export interface SearchResult<T = string> {
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+/;
const builder = new lunr.Builder();
builder.field('title');
builder.field('description');
builder.ref('ref');
let builder: lunr.Builder;
builder.pipeline.add(lunr.trimmer, lunr.stopWordFilter, lunr.stemmer);
let resolveIndex: (v: lunr.Index) => void;
let index: Promise<lunr.Index>;
function initEmpty() {
builder = new lunr.Builder();
builder.field('title');
builder.field('description');
builder.ref('ref');
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, {})) + '*';
@ -70,6 +77,11 @@ export async function load(state: any) {
resolveIndex(lunr.Index.load(state.index));
}
export async function dispose() {
store = [];
initEmpty();
}
export async function search<Meta = string>(
q: string,
limit = 0,

View File

@ -2,6 +2,7 @@ import { OpenAPIExternalDocumentation, OpenAPISpec } from '../types';
import { ContentItemModel, MenuBuilder } from './MenuBuilder';
import { ApiInfoModel } from './models/ApiInfo';
import { WebhookModel } from './models/Webhook';
import { SecuritySchemesModel } from './models/SecuritySchemes';
import { OpenAPIParser } from './OpenAPIParser';
import { RedocNormalizedOptions } from './RedocNormalizedOptions';
@ -15,6 +16,7 @@ export class SpecStore {
externalDocs?: OpenAPIExternalDocumentation;
contentItems: ContentItemModel[];
securitySchemes: SecuritySchemesModel;
webhooks?: WebhookModel;
constructor(
spec: OpenAPISpec,
@ -26,5 +28,6 @@ export class SpecStore {
this.externalDocs = this.parser.spec.externalDocs;
this.contentItems = MenuBuilder.buildStructure(this.parser, this.options);
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;
test('should hoist oneOfs when mergin allOf', () => {
// eslint-disable-next-line @typescript-eslint/no-var-requires
const spec = require('./fixtures/oneOfHoist.json');
parser = new OpenAPIParser(spec, undefined, opts);
expect(parser.mergeAllOf(spec.components.schemas.test)).toMatchSnapshot();

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