Merge remote-tracking branch 'redoc/master' into sections-at-the-end

This commit is contained in:
Roberto Fernández 2021-03-02 15:23:53 +01:00
commit 45a087c9cd
104 changed files with 12054 additions and 9706 deletions

View File

@ -20,12 +20,15 @@ module.exports = {
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',

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

@ -7,6 +7,6 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v1
- run: npm install
- run: npm ci
- run: npm run bundle
- run: npm test

2
.gitignore vendored
View File

@ -35,3 +35,5 @@ cli/index.js
.ghpages-tmp
stats.json
yarn.lock
.idea
.vscode

View File

@ -9,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:
@ -32,8 +30,3 @@ deploy:
api_key: "$NPM_TOKEN"
on:
tags: true
- provider: script
skip_cleanup: true
script: npm run deploy:demo
on:
tags: true

View File

@ -1,4 +0,0 @@
{
"editor.formatOnSave": true,
"typescript.tsdk": "node_modules/typescript/lib"
}

View File

@ -1,3 +1,226 @@
# [2.0.0-rc.50](https://github.com/Redocly/redoc/compare/v2.0.0-rc.49...v2.0.0-rc.50) (2021-02-15)
### Bug Fixes
* add includes polyfill ([3ba622f](https://github.com/Redocly/redoc/commit/3ba622f3ab9e28c954fe05f42e7b90862fc3d544)), closes [#1530](https://github.com/Redocly/redoc/issues/1530)
* background-color in search results ([#1531](https://github.com/Redocly/redoc/issues/1531)) ([d288165](https://github.com/Redocly/redoc/commit/d288165a4ea04aedc23dba12020a73e86f20755b))
* false-positive recursive tag case when using oneOf + allOf ([#1534](https://github.com/Redocly/redoc/issues/1534)) ([8270481](https://github.com/Redocly/redoc/commit/8270481e9f0f381b392f7921d21cb06e0e673b6d))
# [2.0.0-rc.49](https://github.com/Redocly/redoc/compare/v2.0.0-rc.48...v2.0.0-rc.49) (2021-01-30)
### Bug Fixes
* crash on multiple examples on parameter object ([0dce880](https://github.com/Redocly/redoc/commit/0dce880dce1e489c7e8963e352d97603262f4b86)), closes [#1485](https://github.com/Redocly/redoc/issues/1485)
* fix SourceCodeWithCopy component to be non-pure ([040ce72](https://github.com/Redocly/redoc/commit/040ce72a8ae0c1ca7504e10e44d0b2ac7ba04977))
* pass boolean and number values as a string in highlight function ([#1512](https://github.com/Redocly/redoc/issues/1512)) ([c874a59](https://github.com/Redocly/redoc/commit/c874a5942c3bf9f6a2dc5909e31d57925d40aa86))
# [2.0.0-rc.48](https://github.com/Redocly/redoc/compare/v2.0.0-rc.47...v2.0.0-rc.48) (2020-11-30)
### Bug Fixes
* add missed labels to elements ([#1445](https://github.com/Redocly/redoc/issues/1445)) ([8c559bc](https://github.com/Redocly/redoc/commit/8c559bcbcde39efee7f1570b88840468bfdfb17c))
### Features
* add new option hideSchemaPattern ([#1475](https://github.com/Redocly/redoc/issues/1475)) ([bb4594e](https://github.com/Redocly/redoc/commit/bb4594ee58d89819c975bdb575083c0667e3d940))
* support multiple examples for parameters ([#1470](https://github.com/Redocly/redoc/issues/1470)) ([d12e410](https://github.com/Redocly/redoc/commit/d12e410d99a988948b359093159df79572bc78ab))
# [2.0.0-rc.46](https://github.com/Redocly/redoc/compare/v2.0.0-rc.45...v2.0.0-rc.46) (2020-11-05)
### Bug Fixes
* fix arrow color in responses ([#1452](https://github.com/Redocly/redoc/issues/1452)) ([6bedcf9](https://github.com/Redocly/redoc/commit/6bedcf94b26d820101ab510b28d2b76a38999eea))
* remove duplicated slash if hideHostname option enabled ([#1448](https://github.com/Redocly/redoc/issues/1448)) ([4729fc3](https://github.com/Redocly/redoc/commit/4729fc3d8fc83f4af087cd7932adf500b45bab4e))
* use shrinkwrap for cli package ([#1446](https://github.com/Redocly/redoc/issues/1446)) ([4567534](https://github.com/Redocly/redoc/commit/4567534cbb26f13a72a64d49faca64fc992d6dd8))
### Features
* add tabTextColor option for responses ([#1451](https://github.com/Redocly/redoc/issues/1451)) ([702fea0](https://github.com/Redocly/redoc/commit/702fea0f410499101efc554983c6db58acc84889))
# [2.0.0-rc.45](https://github.com/Redocly/redoc/compare/v2.0.0-rc.43...v2.0.0-rc.45) (2020-10-27)
### Bug Fixes
* fix the name of OpenID Connect security scheme ([#1425](https://github.com/Redocly/redoc/issues/1425)) ([c11f679](https://github.com/Redocly/redoc/commit/c11f679f82586a96225488c8a96d0c908bfd2e09))
* increase colors contrast to make them more accessible ([#1433](https://github.com/Redocly/redoc/issues/1433)) ([e2de5b0](https://github.com/Redocly/redoc/commit/e2de5b065eabd00d301ea61106ddafc65bd83afa))
### Features
* add field constraint indicator for uniqueItems ([#1423](https://github.com/Redocly/redoc/issues/1423)) ([c0ae9de](https://github.com/Redocly/redoc/commit/c0ae9de60758aa7561ce8a04b6e0060d0bc4a258)), closes [#1353](https://github.com/Redocly/redoc/issues/1353)
* new extensions hook PropertyDetailsCell + wrap property name into span ([0703f73](https://github.com/Redocly/redoc/commit/0703f73f79a1cabafdc1a908ebb0c5ab142ca825))
# [2.0.0-rc.44](https://github.com/Redocly/redoc/compare/v2.0.0-rc.43...v2.0.0-rc.44) (2020-10-16)
### Features
* new extensions hook PropertyDetailsCell + wrap property name into span ([0fae030](https://github.com/Redocly/redoc/commit/0fae03099645bd9d3795709175640583b08dfc3d))
# [2.0.0-rc.43](https://github.com/Redocly/redoc/compare/v2.0.0-rc.42...v2.0.0-rc.43) (2020-10-13)
### Bug Fixes
* fix broken observable after mobx upgrade ([#1415](https://github.com/Redocly/redoc/issues/1415)) ([26c407b](https://github.com/Redocly/redoc/commit/26c407bd0f2bc1ec9881e0a3668e09e645fc0cc0))
# [2.0.0-rc.42](https://github.com/Redocly/redoc/compare/v2.0.0-rc.41...v2.0.0-rc.42) (2020-10-13)
### Bug Fixes
* hide dropdown input on IE 11 ([#1403](https://github.com/Redocly/redoc/issues/1403)) ([6632d84](https://github.com/Redocly/redoc/commit/6632d844536532227cb92290f9fc2b6b2f913270))
* make samples accessible by keyboard ([#1401](https://github.com/Redocly/redoc/issues/1401)) ([146b38c](https://github.com/Redocly/redoc/commit/146b38c9d0b926765d8e00dd37204c30bf3ac4e0))
* make schema layout more responsive on small screen ([#1411](https://github.com/Redocly/redoc/issues/1411)) ([84ab95d](https://github.com/Redocly/redoc/commit/84ab95ddc7b5dc159098aecf82ad922ffd4a3093))
# [2.0.0-rc.41](https://github.com/Redocly/redoc/compare/v2.0.0-rc.40...v2.0.0-rc.41) (2020-09-24)
### Bug Fixes
* display response code at the top after adding a line break ([#1374](https://github.com/Redocly/redoc/issues/1374)) ([c801b87](https://github.com/Redocly/redoc/commit/c801b87d2aea5e17d35093e2548e1f51f42b1ee3))
* fix displaying response title ([#1376](https://github.com/Redocly/redoc/issues/1376)) ([f3e8ab4](https://github.com/Redocly/redoc/commit/f3e8ab4f8e5522c9ea1ddedb143e23c7d62f5807))
* fix displaying top-level object without any properties ([a5468fb](https://github.com/Redocly/redoc/commit/a5468fb7bb99fcfe33724af939b1a589c1219052))
* show long pattern and add toggle button ([#1375](https://github.com/Redocly/redoc/issues/1375)) ([a6b41aa](https://github.com/Redocly/redoc/commit/a6b41aa00b7592512fdaa7532d9f5d85238db29b))
### Features
* load external search index ([346b10f](https://github.com/Redocly/redoc/commit/346b10f1739d6b44066bdf1f6aac39d5ee3567d2))
* support for ignoring specified named schemas ([9730c4e](https://github.com/Redocly/redoc/commit/9730c4ee1c274c5775966959b69c209c40034b11))
# [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)

View File

@ -3,7 +3,7 @@
**OpenAPI/Swagger-generated API Reference Documentation**
[![Build Status](https://travis-ci.org/Redocly/redoc.svg?branch=master)](https://travis-ci.org/Redocly/redoc) [![Coverage Status](https://coveralls.io/repos/Redocly/redoc/badge.svg?branch=master&service=github)](https://coveralls.io/github/Redocly/redoc?branch=master) [![dependencies Status](https://david-dm.org/Redocly/redoc/status.svg)](https://david-dm.org/Redocly/redoc) [![devDependencies Status](https://david-dm.org/Redocly/redoc/dev-status.svg)](https://david-dm.org/Redocly/redoc#info=devDependencies) [![npm](http://img.shields.io/npm/v/redoc.svg)](https://www.npmjs.com/package/redoc) [![License](https://img.shields.io/npm/l/redoc.svg)](https://github.com/Redocly/redoc/blob/master/LICENSE)
[![Build Status](https://travis-ci.com/Redocly/redoc.svg?branch=master)](https://travis-ci.com/Redocly/redoc) [![Coverage Status](https://coveralls.io/repos/Redocly/redoc/badge.svg?branch=master&service=github)](https://coveralls.io/github/Redocly/redoc?branch=master) [![dependencies Status](https://david-dm.org/Redocly/redoc/status.svg)](https://david-dm.org/Redocly/redoc) [![devDependencies Status](https://david-dm.org/Redocly/redoc/dev-status.svg)](https://david-dm.org/Redocly/redoc#info=devDependencies) [![npm](http://img.shields.io/npm/v/redoc.svg)](https://www.npmjs.com/package/redoc) [![License](https://img.shields.io/npm/l/redoc.svg)](https://github.com/Redocly/redoc/blob/master/LICENSE)
[![bundle size](http://img.badgesize.io/https://cdn.jsdelivr.net/npm/redoc/bundles/redoc.standalone.js?compression=gzip&max=300000)](https://cdn.jsdelivr.net/npm/redoc/bundles/redoc.standalone.js) [![npm](https://img.shields.io/npm/dm/redoc.svg)](https://www.npmjs.com/package/redoc) [![](https://data.jsdelivr.com/v1/package/npm/redoc/badge)](https://www.jsdelivr.com/package/npm/redoc) [![Docker Build Status](https://img.shields.io/docker/build/redocly/redoc.svg)](https://hub.docker.com/r/redocly/redoc/)
@ -64,7 +64,7 @@ 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-api.redoc.ly/)
- [Rebilly](https://api-reference.rebilly.com/)
- [Docker Engine](https://docs.docker.com/engine/api/v1.25/)
- [Zuora](https://www.zuora.com/developer/api-reference/)
- [Discourse](http://docs.discourse.org)
@ -138,7 +138,7 @@ For npm:
Install peer dependencies required by ReDoc if you don't have them installed already:
npm i react react-dom mobx@^4.2.0 styled-components core-js
npm i react react-dom mobx styled-components core-js
Import `RedocStandalone` component from 'redoc' module:
@ -219,6 +219,9 @@ ReDoc makes use of the following [vendor extensions](https://swagger.io/specific
* [`x-servers`](docs/redoc-vendor-extensions.md#x-servers) - ability to specify different servers for API (backported from OpenAPI 3.0)
* [`x-ignoredHeaderParameters`](docs/redoc-vendor-extensions.md#x-ignoredHeaderParameters) - ability to specify header parameter names to ignore
* [`x-additionalPropertiesName`](docs/redoc-vendor-extensions.md#x-additionalPropertiesName) - ability to supply a descriptive name for the additional property keys
* [`x-summary`](docs/redoc-vendor-extensions.md#x-summary) - For Response object, use as the response button text, with description rendered under the button
* [`x-extendedDiscriminator`](docs/redoc-vendor-extensions.md#x-extendedDiscriminator) - In Schemas, uses this to solve name-clash issues with the standard discriminator
* [`x-explicitMappingOnly`](docs/redoc-vendor-extensions.md#x-explicitMappingOnly) - In Schemas, display a more descriptive property name in objects with additionalProperties when viewing the property list with an object
### `<redoc>` options object
You can use all of the following options with standalone version on <redoc> tag by kebab-casing them, e.g. `scrollYOffset` becomes `scroll-y-offset` and `expandResponses` becomes `expand-responses`.
@ -226,14 +229,18 @@ You can use all of the following options with standalone version on <redoc> tag
* `disableSearch` - disable search indexing and search box.
* `expandDefaultServerVariables` - enable expanding default server variables, default `false`.
* `expandResponses` - specify which responses to expand by default by response codes. Values should be passed as comma-separated list without spaces e.g. `expandResponses="200,201"`. Special value `"all"` expands all responses by default. Be careful: this option can slow-down documentation rendering time.
* `maxDisplayedEnumValues` - display only specified number of enum values. hide rest values under spoiler.
* `hideDownloadButton` - do not show "Download" spec button. **THIS DOESN'T MAKE YOUR SPEC PRIVATE**, it just hides the button.
* `hideHostname` - if set, the protocol and hostname is not shown in the operation definition.
* `hideLoading` - do not show loading animation. Useful for small docs.
* `hideSchemaPattern` - if set, the pattern is not shown in the schema.
* `hideSingleRequestSampleTab` - do not show the request sample tab for requests with only one sample.
* `expandSingleSchemaField` - automatically expand single field in a schema
* `jsonSampleExpandLevel` - set the default expand level for JSON payload samples (responses and request body). Special value 'all' expands all levels. The default value is `2`.
* `jsonSampleExpandLevel` - set the default expand level for JSON payload samples (responses and request body). Special value `"all"` expands all levels. The default value is `2`.
* `hideSchemaTitles` - do not display schema `title` next to to the type
* `simpleOneOfTypeLabel` - show only unique oneOf types in the label without titles
* `lazyRendering` - _Not implemented yet_ ~~if set, enables lazy rendering mode in ReDoc. This mode is useful for APIs with big number of operations (e.g. > 50). In this mode ReDoc shows initial screen ASAP and then renders the rest operations asynchronously while showing progress bar on the top. Check out the [demo](\\redocly.github.io/redoc) for the example.~~
* `menuToggle` - if true clicking second time on expanded menu item will collapse it, default `false`.
* `menuToggle` - if true clicking second time on expanded menu item will collapse it, default `true`.
* `nativeScrollbars` - use native scrollbar for sidemenu instead of perfect-scroll (scrolling performance optimization for big specs).
* `noAutoAuth` - do not inject Authentication section automatically.
* `onlyRequiredInSamples` - shows only required fields in request samples.
@ -246,11 +253,67 @@ You can use all of the following options with standalone version on <redoc> tag
* **function**: A getter function. Must return a number representing the offset (in pixels).
* `showExtensions` - show vendor extensions ("x-" fields). Extensions used by ReDoc are ignored. Can be boolean or an array of `string` with names of extensions to display.
* `sortPropsAlphabetically` - sort properties alphabetically.
* `suppressWarnings` - if set, warnings are not rendered at the top of documentation (they still are logged to the console).
* `payloadSampleIdx` - if set, payload sample will be inserted at this index or last. Indexes start from 0.
* `theme` - ReDoc theme. Not documented yet. For details check source code: [theme.ts](https://github.com/Redocly/redoc/blob/master/src/theme.ts).
* `theme` - ReDoc theme. For details check [theme docs](#redoc-theme-object).
* `untrustedSpec` - if set, the spec is considered untrusted and all HTML/markdown is sanitized to prevent XSS. **Disabled by default** for performance reasons. **Enable this option if you work with untrusted user data!**
### `<redoc>` theme object
* `spacing`
* `unit`: 5 # main spacing unit used in autocomputed theme values later
* `sectionHorizontal`: 40 # Horizontal section padding. COMPUTED: spacing.unit * 8
* `sectionVertical`: 40 # Horizontal section padding. COMPUTED: spacing.unit * 8
* `breakpoints` # breakpoints for switching three/two and mobile view layouts
* `small`: '50rem'
* `medium`: '85rem'
* `large`: '105rem'
* `colors`
* `tonalOffset`: 0.3 # default tonal offset used in computations
* `typography`
* `fontSize`: '14px'
* `lineHeight`: '1.5em'
* `fontWeightRegular`: '400'
* `fontWeightBold`: '600'
* `fontWeightLight`: '300'
* `fontFamily`: 'Roboto, sans-serif'
* `smoothing`: 'antialiased'
* `optimizeSpeed`: true
* `headings`
* `fontFamily`: 'Montserrat, sans-serif'
* `fontWeight`: '400'
* `lineHeight`: '1.6em'
* `code` # inline code styling
* `fontSize`: '13px'
* `fontFamily`: 'Courier, monospace'
* `lineHeight`: # COMPUTED: typography.lineHeight
* `fontWeight`: # COMPUTED: typography.fontWeightRegular
* `color`: '#e53935'
* `backgroundColor`: 'rgba(38, 50, 56, 0.05)'
* `wrap`: false # whether to break word for inline blocks (otherwise they can overflow)
* `links`
* `color`: # COMPUTED: colors.primary.main
* `visited`: # COMPUTED: typography.links.color
* `hover`: # COMPUTED: lighten(0.2 typography.links.color)
* `menu`
* `width`: '260px'
* `backgroundColor`: '#fafafa'
* `textColor`: '#333333'
* `activeTextColor`: # COMPUTED: theme.menu.textColor (if set by user) or theme.colors.primary.main
* `groupItems` # Group headings
* `textTransform`: 'uppercase'
* `level1Items` # Level 1 items like tags or section 1st level items
* `textTransform`: 'none'
* `arrow` # menu arrow
* `size`: '1.5em'
* `color`: # COMPUTED: theme.menu.textColor
* `logo`
* `maxHeight`: # COMPUTED: menu.width
* `maxWidth`: # COMPUTED: menu.width
* `gutter`: '2px' # logo image padding
* `rightPanel`
* `backgroundColor`: '#263238'
* `width`: '40%'
* `textColor`: '#ffffff'
## Advanced usage of standalone version
Instead of adding `spec-url` attribute to the `<redoc>` element you can initialize ReDoc via globally exposed `Redoc` object:
```js

View File

@ -293,7 +293,7 @@ async function getPageHTML(
var container = document.getElementById('redoc');
Redoc.${
ssr
? 'hydrate(__redoc_state, container);'
? 'hydrate(__redoc_state, container)'
: `init("spec.json", ${JSON.stringify(redocOptions)}, container)`
};

File diff suppressed because it is too large Load Diff

View File

@ -1,6 +1,6 @@
{
"name": "redoc-cli",
"version": "0.9.8",
"version": "0.10.4",
"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",
"mobx": "^4.2.0",
"mkdirp": "^1.0.4",
"mobx": "^6.0.4",
"node-libs-browser": "^2.2.1",
"react": "^16.8.6",
"react-dom": "^16.8.6",
"redoc": "2.0.0-rc.29",
"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.50",
"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"
}
}

View File

@ -36,6 +36,18 @@ COPY demo/favicon.png /usr/share/nginx/html/
COPY config/docker/nginx.conf /etc/nginx/
COPY config/docker/docker-run.sh /usr/local/bin
# Provide rights to the root group to write to nginx repositories (needed to run in OpenShift)
RUN chgrp -R 0 /etc/nginx && \
chgrp -R 0 /usr/share/nginx/html && \
chgrp -R 0 /var/cache/nginx && \
chgrp -R 0 /var/log/nginx && \
chgrp -R 0 /var/run && \
chmod -R g+rwX /etc/nginx && \
chmod -R g+rwX /usr/share/nginx/html && \
chmod -R g+rwX /var/cache/nginx && \
chmod -R g+rwX /var/log/nginx && \
chmod -R g+rwX /var/run
EXPOSE 80
CMD ["sh", "/usr/local/bin/docker-run.sh"]

View File

@ -2,6 +2,8 @@
## Usage
### Docker
Serve remote spec by URL:
docker run -it --rm -p 80:80 \
@ -12,13 +14,33 @@ 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
### OpenShift
To quote [OpenShift Container Platform-Specific Guidelines](https://docs.openshift.com/container-platform/3.11/creating_images/guidelines.html#openshift-specific-guidelines):
> Support Arbitrary User IDs
>
> By default, OpenShift Container Platform runs containers using an arbitrarily assigned user ID. This provides additional security against processes escaping the container due to a container engine vulnerability and thereby achieving escalated permissions on the host node.
>
> For an image to support running as an arbitrary user, directories and files that may be written to by processes in the image should be owned by the root group and be read/writable by that group. Files to be executed should also have group execute permissions.
To comply with those requirements the `Dockerfile` contains instructions to adapt the rights for the folders:
- `/etc/nginx` because the `docker-run.sh` script modifies it at startup time
- `/usr/share/nginx/html` because the `docker-run.sh` script modifies it at startup time
- `/var/cache/nginx` because the Nginx process writes to it
- `/var/log/nginx` because the Nginx process writes to it
- `/var/run` because the Nginx process writes to it
Another issue with OpenShift is that the default exposed port `80` cannot be used as it is restricted. So one needs to use another port like `8080` (using the `PORT` configuration as described below), and then to configure the `container spec` accordingly.
## Runtime configuration options
- `PAGE_TITLE` (default `"ReDoc"`) - page title

View File

@ -211,6 +211,7 @@ export default class ComboBox extends React.Component<ComboBoxProps, ComboBoxSta
onFocus={this.open}
onBlur={this.handleBlur}
onKeyDown={this.handleKeyPress}
aria-label="URL to an OpenAPI definition to try"
/>
<Button onClick={this.handleTryItClick}> TRY IT </Button>
{open && <DropDownList>{options.map(this.renderOption)}</DropDownList>}

View File

@ -7,8 +7,11 @@ import ComboBox from './ComboBox';
const demos = [
{ value: 'https://api.apis.guru/v2/specs/instagram.com/1.0.0/swagger.yaml', label: 'Instagram' },
{ value: 'https://api.apis.guru/v2/specs/googleapis.com/calendar/v3/swagger.yaml', label: 'Google Calendar' },
{ value: 'https://api.apis.guru/v2/specs/slack.com/1.2.0/swagger.yaml', label: 'Slack' },
{
value: 'https://api.apis.guru/v2/specs/googleapis.com/calendar/v3/openapi.yaml',
label: 'Google Calendar',
},
{ value: 'https://api.apis.guru/v2/specs/slack.com/1.5.0/openapi.yaml', label: 'Slack' },
{ value: 'https://api.apis.guru/v2/specs/zoom.us/2.0.0/swagger.yaml', label: 'Zoom.us' },
{ value: 'https://docs.graphhopper.com/openapi.json', label: 'GraphHopper' },
];
@ -76,7 +79,10 @@ class DemoApp extends React.Component<
<>
<Heading>
<a href=".">
<Logo src="https://github.com/Redocly/redoc/raw/master/docs/images/redoc-logo.png" />
<Logo
src="https://github.com/Redocly/redoc/raw/master/docs/images/redoc-logo.png"
alt="Redoc logo"
/>
</a>
<ControlsContainer>
<ComboBox
@ -94,7 +100,7 @@ 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>

View File

@ -1187,3 +1187,19 @@ components:
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

@ -1,8 +1,34 @@
# ReDoc vendor extensions
ReDoc makes use of the following [vendor extensions](https://swagger.io/specification/#specificationExtensions)
### Swagger Object vendor extensions
Extend OpenAPI root [Swagger Object](https://swagger.io/specification/#oasObject)
You can use the following [vendor extensions](https://swagger.io/specification/#specificationExtensions) with Redoc.
- [Swagger Object](#swagger-object)
- [x-servers](#x-servers)
- [x-tagGroups](#x-taggroups)
- [Tag Group Object](#a-nametaggroupobjectatag-group-object)
- [x-ignoredHeaderParameters](#x-ignoredheaderparameters)
- [Info Object](#info-object)
- [x-logo](#x-logo)
- [Logo Object](#a-namelogoobjectalogo-object)
- [Tag Object](#tag-object)
- [x-traitTag](#x-traittag)
- [x-displayName](#x-displayname)
- [Operation Object](#operation-object-vendor-extensions)
- [x-codeSamples](#x-codesamples)
- [Code Sample Object](#a-namecodesampleobjectacode-sample-object)
- [Parameter Object](#parameter-object)
- [x-examples](#x-examples)
- [Response Object vendor extensions](#response-object-vendor-extensions)
- [x-summary](#x-summary)
- [Schema Object](#schema-object)
- [x-nullable](#x-nullable)
- [x-extendedDiscriminator](#x-extendeddiscriminator)
- [x-additionalPropertiesName](#x-additionalpropertiesname)
- [x-explicitMappingOnly](#x-explicitmappingonly)
### Swagger Object
Extends the OpenAPI root [OpenAPI Object](https://swagger.io/specification/#oasObject)
#### x-servers
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.
@ -12,9 +38,9 @@ Backported from OpenAPI 3.0 [`servers`](https://github.com/OAI/OpenAPI-Specifica
| :------------- | :-----------: | :---------- |
| x-tagGroups | [ [Tag Group Object](#tagGroupObject) ] | A list of tag groups |
###### Usage in Redoc
###### How to use with Redoc
`x-tagGroups` is used to group tags in the side menu.
If you are going to use `x-tagGroups`, please make sure you **add all tags to a group**, since a tag that is not in a group, **will not be displayed** at all!
Before you use `x-tagGroups`, make sure you **add all tags to a group**, since a tag that is not in a group, **will not be displayed** at all!
#### <a name="tagGroupObject"></a>Tag Group Object
Information about tags group
@ -62,8 +88,8 @@ x-tagGroups:
| x-ignoredHeaderParameters | [ string ] | A list of ignored headers |
###### Usage in Redoc
`x-ignoredHeaderParameters` is used to specify header parameter names which are ignored by ReDoc
###### How to use with Redoc
Use `x-ignoredHeaderParameters` to specify header parameter names which are ignored by ReDoc.
###### x-ignoredHeaderParameters example
```yaml
@ -77,19 +103,20 @@ x-ignoredHeaderParameters:
- X-Test-Header
```
### Info Object vendor extensions
Extends OpenAPI [Info Object](http://swagger.io/specification/#infoObject)
### Info Object
Extends the OpenAPI [Info Object](http://swagger.io/specification/#infoObject)
#### x-logo
| Field Name | Type | Description |
| :------------- | :-----------: | :---------- |
| x-logo | [Logo Object](#logoObject) | The information about API logo |
###### Usage in Redoc
`x-logo` is used to specify API logo. The corresponding image are displayed just above side-menu.
###### How to use with Redoc
`x-logo` is used to specify API logo. The corresponding image is displayed just above the side-menu.
#### <a name="logoObject"></a>Logo Object
The information about API logo
###### Fixed fields
| Field Name | Type | Description |
| :-------------- | :------: | :---------- |
@ -125,17 +152,16 @@ info:
altText: "Petstore logo"
```
### Tag Object
Extends the OpenAPI [Tag Object](http://swagger.io/specification/#tagObject)
### Tag Object vendor extensions
Extends OpenAPI [Tag Object](http://swagger.io/specification/#tagObject)
#### x-traitTag
| Field Name | Type | Description |
| :------------- | :------: | :---------- |
| x-traitTag | boolean | In Swagger two operations can have multiple tags. This property distinguishes between tags that are used to group operations (default) from tags that are used to mark operation with certain trait (`true` value) |
###### Usage in Redoc
Tags that have `x-traitTag` set to `true` are listed in side-menu but don't have any subitems (operations). Tag `description` is rendered as well.
###### How to use with Redoc
Tags that have `x-traitTag` set to `true` are listed in the side-menu but don't have any subitems (operations). It also renders the `description` tag.
This is useful for handling out common things like Pagination, Rate-Limits, etc.
###### x-traitTag example
@ -161,17 +187,19 @@ x-traitTag: true
| x-displayName | string | Defines the text that is used for this tag in the menu and in section headings |
### Operation Object vendor extensions
Extends OpenAPI [Operation Object](http://swagger.io/specification/#operationObject)
Extends the OpenAPI [Operation Object](http://swagger.io/specification/#operationObject)
#### x-codeSamples
| Field Name | Type | Description |
| :------------- | :------: | :---------- |
| x-codeSamples | [ [Code Sample Object](#codeSampleObject) ] | A list of code samples associated with operation |
###### Usage in ReDoc
`x-codeSamples` are rendered on the right panel of ReDoc
###### How to use with ReDoc
`x-codeSamples` are rendered on the right panel in ReDoc.
#### <a name="codeSampleObject"></a>Code Sample Object
Operation code sample
###### Fixed fields
| Field Name | Type | Description |
| :---------- | :------: | :----------- |
@ -194,49 +222,51 @@ lang: JavaScript
source: console.log('Hello World');
```
### Parameter Object vendor extensions
Extends OpenAPI [Parameter Object](http://swagger.io/specification/#parameterObject)
### Parameter Object
Extends the OpenAPI [Parameter Object](http://swagger.io/specification/#parameterObject)
#### x-examples
| Field Name | Type | Description |
| :------------- | :------: | :---------- |
| x-examples | [Example Object](http://swagger.io/specification/#exampleObject) | Object that contains examples for the request. Applies when `in` is `body` and mime-type is `application/json` |
###### Usage in ReDoc
`x-examples` are rendered in the JSON tab on the right panel of ReDoc.
###### How to use with ReDoc
`x-examples` are rendered in the JSON tab on the right panel in ReDoc.
### Response Object vendor extensions
Extends OpenAPI [Response Object](https://swagger.io/specification/#responseObject)
Extends the OpenAPI [Response Object](https://swagger.io/specification/#responseObject)
#### x-summary
| Field Name | Type | Description |
| :------------- | :------: | :---------- |
| x-summary | string | a short summary of the response |
###### Usage in ReDoc
If specified, `x-summary` is used as the response button text. Description is rendered under the button.
###### How to use with ReDoc
If specified, you can use `x-summary` as the response button text, with description rendered under the button.
### Schema Object
Extends the OpenAPI [Schema Object](http://swagger.io/specification/#schemaObject)
### Schema Object vendor extensions
Extends OpenAPI [Schema Object](http://swagger.io/specification/#schemaObject)
#### x-nullable
| Field Name | Type | Description |
| :------------- | :------: | :---------- |
| x-nullable | boolean | marks schema as a nullable |
###### Usage in ReDoc
###### How to use with ReDoc
Schemas marked as `x-nullable` are marked in ReDoc with the label Nullable
#### x-extendedDiscriminator
**ATTENTION**: This is ReDoc-specific vendor extension. It won't be supported by other tools.
**ATTENTION**: This is a ReDoc-specific vendor extension, and is not supported by other tools.
| Field Name | Type | Description |
| :------------- | :------: | :---------- |
| x-extendedDiscriminator | string | specifies extended discriminator |
###### Usage in ReDoc
###### How to use with ReDoc
ReDoc uses this vendor extension to solve name-clash issues with the standard `discriminator`.
Value of this field specifies the field which will be used as a extended discriminator.
ReDoc displays definition with selectpicker using which user can select value of the `x-extendedDiscriminator`-marked field.
ReDoc displays the definition which is derived from the current (using `allOf`) and has `enum` with only one value which is the same as the selected value of the field specified as `x-extendedDiscriminator`.
ReDoc displays the definition derived from the current (using `allOf`) and has `enum` with only one value which is the same as the selected value of the field specified as `x-extendedDiscriminator`.
###### x-extendedDiscriminator example
@ -276,11 +306,10 @@ PayPalPayment:
type: string
```
In the example above the names of definitions (`PayPalPayment`) are named differently than
names in the payload (`paypal`) which is not supported by default `discriminator`.
In the example above, the names of definitions (`PayPalPayment`) are named differently than names in the payload (`paypal`) which is not supported by default `discriminator`.
#### x-additionalPropertiesName
**ATTENTION**: This is ReDoc-specific vendor extension. It won't be supported by other tools.
**ATTENTION**: This is a ReDoc-specific vendor extension, and is not supported by other tools.
Extends the `additionalProperties` property of the schema object.
@ -288,7 +317,7 @@ Extends the `additionalProperties` property of the schema object.
| :------------- | :------: | :---------- |
| x-additionalPropertiesName | string | descriptive name of additional properties keys |
###### Usage in ReDoc
###### How to use with ReDoc
ReDoc uses this extension to display a more descriptive property name in objects with `additionalProperties` when viewing the property list with an `object`.
###### x-additionalPropertiesName example
@ -308,7 +337,7 @@ Player:
```
#### x-explicitMappingOnly
**ATTENTION**: This is ReDoc-specific vendor extension. It won't be supported by other tools.
**ATTENTION**: This is ReDoc-specific vendor extension, and is not supported by other tools.
Extends the `discriminator` property of the schema object.
@ -316,10 +345,9 @@ Extends the `discriminator` property of the schema object.
| :------------- | :------: | :---------- |
| x-explicitMappingOnly | boolean | limit the discriminator selectpicker to the explicit mappings only |
###### Usage in ReDoc
###### How to use with 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.
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
@ -338,5 +366,4 @@ Pet:
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.
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) + 1);
.should('have.length', 34);
});
it('should sync active menu items while scroll', () => {

18428
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -1,6 +1,6 @@
{
"name": "redoc",
"version": "2.0.0-rc.30",
"version": "2.0.0-rc.50",
"description": "ReDoc",
"repository": {
"type": "git",
@ -47,117 +47,116 @@
"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.8.7",
"@babel/plugin-syntax-decorators": "7.8.3",
"@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.8.3",
"@babel/plugin-syntax-typescript": "7.8.3",
"@cypress/webpack-preprocessor": "4.1.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.10",
"@types/dompurify": "^2.0.1",
"@types/chai": "^4.2.12",
"@types/dompurify": "^2.0.2",
"@types/enzyme": "^3.10.5",
"@types/enzyme-to-json": "^1.5.3",
"@types/jest": "^25.1.4",
"@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.3",
"@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.23",
"@types/react-dom": "^16.9.5",
"@types/react-tabs": "^2.3.1",
"@types/styled-components": "^4.4.1",
"@types/tapable": "1.0.5",
"@types/webpack": "^4.41.7",
"@types/webpack-env": "^1.15.1",
"@types/yargs": "^13.0.3",
"@typescript-eslint/eslint-plugin": "^2.24.0",
"@typescript-eslint/parser": "^2.24.0",
"babel-loader": "8.0.6",
"@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.31",
"copy-webpack-plugin": "^5.1.1",
"core-js": "^3.5.0",
"coveralls": "^3.0.9",
"css-loader": "^3.4.2",
"cypress": "~3.7.0",
"deploy-to-gh-pages": "^1.3.7",
"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",
"enzyme": "^3.11.0",
"enzyme-adapter-react-16": "^1.15.2",
"enzyme-to-json": "^3.4.4",
"eslint": "^6.8.0",
"eslint-plugin-import": "^2.20.1",
"eslint-plugin-react": "^7.19.0",
"fork-ts-checker-webpack-plugin": "3.1.1",
"html-webpack-plugin": "^3.1.0",
"jest": "^24.9.0",
"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",
"lodash": "^4.17.19",
"mobx": "^6.0.4",
"prettier": "^2.0.5",
"raf": "^3.4.1",
"react": "^16.13.0",
"react-dom": "^16.13.0",
"react-hot-loader": "^4.12.19",
"react": "^16.13.1",
"react-dom": "^16.13.1",
"react-hot-loader": "^4.12.21",
"rimraf": "^3.0.2",
"shelljs": "^0.8.3",
"source-map-loader": "^0.2.4",
"style-loader": "^1.1.3",
"styled-components": "^4.4.1",
"ts-jest": "24.2.0",
"ts-loader": "6.2.1",
"ts-node": "^8.6.2",
"typescript": "^3.8.3",
"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.8",
"webpack": "^4.42.0",
"webpack-cli": "^3.3.11",
"webpack-dev-server": "^3.10.3",
"webpack-node-externals": "^1.6.0",
"workerize-loader": "^1.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": {
"core-js": "^3.1.4",
"mobx": "^4.2.0 || ^5.0.0",
"mobx": "^6.0.4",
"react": "^16.8.4",
"react-dom": "^16.8.4",
"styled-components": "^4.1.1"
"styled-components": "^4.1.1 || ^5.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.8",
"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.5",
"openapi-sampler": "1.0.0-beta.15",
"mobx-react": "^7.0.5",
"openapi-sampler": "^1.0.0-beta.18",
"perfect-scrollbar": "^1.4.0",
"polished": "^3.4.4",
"prismjs": "^1.19.0",
"polished": "^3.6.5",
"prismjs": "^1.22.0",
"prop-types": "^15.7.2",
"react-dropdown": "^1.7.0",
"react-tabs": "^3.1.0",
"slugify": "^1.4.0",
"react-tabs": "^3.1.1",
"slugify": "^1.4.4",
"stickyfill": "^1.1.1",
"swagger2openapi": "^5.3.4",
"tslib": "^1.11.1",
"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

@ -65,7 +65,7 @@ export const PrismDiv = styled.div`
}
.token.boolean {
color: firebrick;
color: #e64441;
}
.token.selector,
@ -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,136 @@
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;
background-color: transparent;
}
}
`;
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

@ -1,6 +1,6 @@
// import { transparentize } from 'polished';
import styled, { extensionsHook } from '../styled-components';
import styled, { extensionsHook, media } from '../styled-components';
import { deprecatedCss } from './mixins';
export const PropertiesTableCaption = styled.caption`
@ -16,6 +16,11 @@ export const PropertyCell = styled.td<{ kind?: string }>`
position: relative;
padding: 10px 10px 10px 0;
${media.lessThan('small')`
display: block;
overflow: hidden;
`}
tr:first-of-type > &,
tr.last > & {
border-left-width: 0;
@ -62,8 +67,8 @@ export const PropertyNameCell = styled(PropertyCell)`
vertical-align: top;
line-height: 20px;
white-space: nowrap;
font-size: 0.929em;
font-family: ${props => props.theme.typography.code.fontFamily};
font-size: 13px;
font-family: ${(props) => props.theme.typography.code.fontFamily};
&.deprecated {
${deprecatedCss};
@ -77,12 +82,24 @@ export const PropertyNameCell = styled(PropertyCell)`
export const PropertyDetailsCell = styled.td`
border-bottom: 1px solid #9fb4be;
padding: 10px 0;
width: ${props => props.theme.schema.defaultDetailsWidth};
width: ${(props) => props.theme.schema.defaultDetailsWidth};
box-sizing: border-box;
tr.expanded & {
border-bottom: none;
}
${media.lessThan('small')`
padding: 0 20px;
border-bottom: none;
border-left: 1px solid ${(props) => props.theme.schema.linesColor};
tr.last > & {
border-left: none;
}
`}
${extensionsHook('PropertyDetailsCell')};
`;
export const PropertyBullet = styled.span`
@ -125,6 +142,20 @@ export const PropertiesTable = styled.table`
vertical-align: middle;
}
${media.lessThan('small')`
display: block;
> tr, > tbody > tr {
display: block;
}
`}
${media.lessThan('small', false, ' and (-ms-high-contrast:none)')`
td {
float: left;
width: 100%;
}
`}
&
${InnerPropertiesWrap},
&

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};
@ -23,7 +34,7 @@ export const FieldLabel = styled.span`
`;
export const TypePrefix = styled(FieldLabel)`
color: ${props => transparentize(0.2, props.theme.schema.typeNameColor)};
color: ${props => transparentize(0.1, props.theme.schema.typeNameColor)};
`;
export const TypeName = styled(FieldLabel)`
@ -51,12 +62,12 @@ export const RecursiveLabel = styled(FieldLabel)`
`;
export const NullableLabel = styled(FieldLabel)`
color: #3195a6;
color: #0e7c86;
font-size: 13px;
`;
export const PatternLabel = styled(FieldLabel)`
color: #3195a6;
color: #0e7c86;
&::before,
&::after {
font-weight: bold;
@ -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: '';
@ -55,6 +56,7 @@ export class Link extends React.Component<{ to: string; className?: string; chil
className={this.props.className}
href={store!.menu.history.linkForId(this.props.to)}
onClick={this.navigate.bind(this, store!.menu.history)}
aria-label={this.props.to}
>
{this.props.children}
</a>

View File

@ -2,5 +2,5 @@ import { css } from '../styled-components';
export const deprecatedCss = css`
text-decoration: line-through;
color: #bdccd3;
color: #707070;
`;

View File

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

View File

@ -2,16 +2,25 @@ import styled from '../styled-components';
import { PrismDiv } from './PrismDiv';
export const SampleControls = styled.div`
opacity: 0.4;
opacity: 0.7;
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>
@ -42,18 +43,24 @@ export const ShelfIcon = styled(IntShelfIcon)`
transform: rotateZ(${props => directionMap[props.direction || 'down']});
polygon {
fill: ${props =>
(props.color && props.theme.colors[props.color] && props.theme.colors[props.color].main) ||
props.color};
fill: ${({ color, theme }) =>
(color && theme.colors.responses[color] && theme.colors.responses[color].color) || color};
}
`;
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

@ -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 {
@ -41,19 +44,19 @@ export const Tabs = styled(ReactTabs)`
}
&.tab-success {
color: ${props => props.theme.colors.responses.success.color};
color: ${props => props.theme.colors.responses.success.tabTextColor};
}
&.tab-redirect {
color: ${props => props.theme.colors.responses.redirect.color};
color: ${props => props.theme.colors.responses.redirect.tabTextColor};
}
&.tab-info {
color: ${props => props.theme.colors.responses.info.color};
color: ${props => props.theme.colors.responses.info.tabTextColor};
}
&.tab-error {
color: ${props => props.theme.colors.responses.error.color};
color: ${props => props.theme.colors.responses.error.tabTextColor};
}
}
}

View File

@ -43,8 +43,8 @@ export class CallbackSamples extends React.Component<CallbackSamplesProps> {
const dropdownOptions = operations.map((callback, idx) => {
return {
label: `${callback.httpVerb.toUpperCase()}: ${callback.name}`,
value: idx.toString(),
value: `${callback.httpVerb.toUpperCase()}: ${callback.name}`,
idx,
};
});

View File

@ -32,7 +32,10 @@ export class CallbackTitle extends React.PureComponent<CallbackTitleProps> {
}
}
const CallbackTitleWrapper = styled.div`
const CallbackTitleWrapper = styled.button`
border: 0;
width: 100%;
text-align: left;
& > * {
vertical-align: middle;
}
@ -45,7 +48,7 @@ const CallbackTitleWrapper = styled.div`
`;
const CallbackName = styled.span<{ deprecated?: boolean }>`
text-decoration: ${props => (props.deprecated ? 'line-through' : 'none')};
text-decoration: ${(props) => (props.deprecated ? 'line-through' : 'none')};
margin-right: 8px;
`;

View File

@ -1,5 +1,6 @@
import styled from '../../styled-components';
import { CallbackTitle } from './CallbackTitle';
import { darken } from 'polished';
export const StyledCallbackTitle = styled(CallbackTitle)`
padding: 10px;
@ -8,6 +9,7 @@ export const StyledCallbackTitle = styled(CallbackTitle)`
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`

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

@ -62,11 +62,12 @@ 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)
: server.url;
const basePath = getBasePath(normalizedUrl);
return (
<ServerItem key={normalizedUrl}>
<Markdown source={server.description || ''} compact={true} />
@ -74,7 +75,9 @@ export class Endpoint extends React.Component<EndpointProps, EndpointState> {
<ServerUrl>
<span>
{hideHostname || options.hideHostname
? getBasePath(normalizedUrl)
? basePath === '/'
? ''
: basePath
: normalizedUrl}
</span>
{operation.path}

View File

@ -14,7 +14,12 @@ 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 =>
@ -32,6 +37,9 @@ 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; compact?: boolean }) => ({
@ -59,7 +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

@ -37,6 +37,14 @@ export class Field extends React.Component<FieldProps> {
this.props.field.toggle();
}
};
handleKeyPress = e => {
if (e.key === 'Enter') {
e.preventDefault();
this.toggle();
}
};
render() {
const { className, field, isLast, expandByDefault } = this.props;
const { name, deprecated, required, kind } = field;
@ -46,20 +54,25 @@ export class Field extends React.Component<FieldProps> {
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"
>
<span>{name}</span>
<ShelfIcon direction={expanded ? 'down' : 'right'} />
</button>
{required && <RequiredLabel> required </RequiredLabel>}
</ClickablePropertyNameCell>
) : (
<PropertyNameCell className={deprecated ? 'deprecated' : undefined} kind={kind} title={name}>
<PropertyBullet />
{name}
<span>{name}</span>
{required && <RequiredLabel> required </RequiredLabel>}
</PropertyNameCell>
);

View File

@ -8,6 +8,9 @@ import {
TypeName,
TypePrefix,
TypeTitle,
ToggleButton,
FieldLabel,
ExampleValue,
} from '../../common-elements/fields';
import { serializeParameterValue } from '../../utils/openapi';
import { ExternalDocumentation } from '../ExternalDocumentation/ExternalDocumentation';
@ -22,29 +25,42 @@ import { Badge } from '../../common-elements/';
import { l } from '../../services/Labels';
import { OptionsContext } from '../OptionsProvider';
import { FieldModel } from '../../services/models/Field';
import styled from '../../styled-components';
const MAX_PATTERN_LENGTH = 45;
export class FieldDetails extends React.PureComponent<FieldProps> {
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 { enumSkipQuotes, hideSchemaTitles } = this.context;
const { patternShown } = this.state;
const { enumSkipQuotes, hideSchemaTitles, hideSchemaPattern } = this.context;
const { schema, description, example, deprecated } = field;
const { schema, description, example, deprecated, examples } = field;
const rawDefault = !!enumSkipQuotes || field.in === 'header'; // having quotes around header field default values is confusing and inappropriate
let exampleField: JSX.Element | null = null;
let renderedExamples: JSX.Element | null = null;
if (showExamples && example !== undefined) {
const label = l('example') + ':';
if (field.in && (field.style || field.serializationMime)) {
// decode for better readability in examples: see https://github.com/Redocly/redoc/issues/1138
const serializedValue = decodeURIComponent(serializeParameterValue(field, example));
exampleField = <FieldDetail label={label} value={serializedValue} raw={true} />;
if (showExamples && (example !== undefined || examples !== undefined)) {
if (examples !== undefined) {
renderedExamples = <Examples field={field} />;
} else {
exampleField = <FieldDetail label={label} value={example} />;
const label = l('example') + ':';
const raw = !!field.in;
renderedExamples = <FieldDetail label={label} value={getSerializedValue(field, field.example)} raw={raw} />;
}
}
@ -64,8 +80,19 @@ 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 && schema.pattern.length < MAX_PATTERN_LENGTH && (
<PatternLabel> {schema.pattern} </PatternLabel>
{schema.pattern && !hideSchemaPattern && (
<>
<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>
@ -76,7 +103,7 @@ export class FieldDetails extends React.PureComponent<FieldProps> {
)}
<FieldDetail raw={rawDefault} label={l('default') + ':'} value={schema.default} />
{!renderDiscriminatorSwitch && <EnumValues type={schema.type} values={schema.enum} />}{' '}
{exampleField}
{renderedExamples}
{<Extensions extensions={{ ...field.extensions, ...schema.extensions }} />}
<div>
<Markdown compact={true} source={description} />
@ -89,3 +116,40 @@ export class FieldDetails extends React.PureComponent<FieldProps> {
);
}
}
function Examples({ field }: { field: FieldModel }) {
if (!field.examples) {
return null;
}
return (
<>
<FieldLabel> {l('examples')}: </FieldLabel>
<ExamplesList>
{Object.values(field.examples).map((example, idx) => {
return (
<li key={idx}>
<ExampleValue>{getSerializedValue(field, example.value)}</ExampleValue> - {example.summary || example.description}
</li>
);
})}
</ExamplesList>
</>
);
}
function getSerializedValue(field: FieldModel, example: any) {
if (field.in) {
// decode for better readability in examples: see https://github.com/Redocly/redoc/issues/1138
return decodeURIComponent(serializeParameterValue(field, example));
} else {
return example;
}
}
const ExamplesList = styled.ul`
margin-top: 1em;
padding-left: 0;
list-style-position: inside;
`;

View File

@ -32,10 +32,10 @@ export class GenericChildrenSwitcher<T> extends React.Component<
};
}
switchItem = ({ value }) => {
switchItem = ({ idx }) => {
if (this.props.items) {
this.setState({
activeItemIdx: parseInt(value, 10),
activeItemIdx: idx,
});
}
};
@ -61,9 +61,10 @@ export class GenericChildrenSwitcher<T> extends React.Component<
<>
<Wrapper>
{this.props.renderDropdown({
value: this.props.options[this.state.activeItemIdx],
value: this.props.options[this.state.activeItemIdx].value,
options: this.props.options,
onChange: this.switchItem,
ariaLabel: this.props.label || 'Callback',
})}
</Wrapper>

View File

@ -30,15 +30,15 @@ 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 => (
{(options) => (
<PrismDiv
className={this.props.className}
// tslint:disable-next-line
ref={node => (this.node = node!)}
ref={(node) => (this.node = node!)}
dangerouslySetInnerHTML={{
__html: jsonToHTML(this.props.data, options.jsonSampleExpandLevel),
}}
@ -51,7 +51,9 @@ class Json extends React.PureComponent<JsonProps> {
expandAll = () => {
const elements = this.node.getElementsByClassName('collapsible');
for (const collapsed of Array.prototype.slice.call(elements)) {
(collapsed.parentNode as Element)!.classList.remove('collapsed');
const parentNode = collapsed.parentNode as Element;
parentNode.classList.remove('collapsed');
parentNode.querySelector('.collapser')!.setAttribute('aria-label', 'collapse');
}
};
@ -61,29 +63,44 @@ class Json extends React.PureComponent<JsonProps> {
const elementsArr = Array.prototype.slice.call(elements, 1);
for (const expanded of elementsArr) {
(expanded.parentNode as Element)!.classList.add('collapsed');
const parentNode = expanded.parentNode as Element;
parentNode.classList.add('collapsed');
parentNode.querySelector('.collapser')!.setAttribute('aria-label', 'expand');
}
};
clickListener = (event: MouseEvent) => {
collapseElement = (target: HTMLElement) => {
let collapsed;
const target = event.target as HTMLElement;
if (target.className === 'collapser') {
collapsed = target.parentElement!.getElementsByClassName('collapsible')[0];
if (collapsed.parentElement.classList.contains('collapsed')) {
collapsed.parentElement.classList.remove('collapsed');
target.setAttribute('aria-label', 'collapse');
} else {
collapsed.parentElement.classList.add('collapsed');
target.setAttribute('aria-label', 'expand');
}
}
};
clickListener = (event: MouseEvent) => {
this.collapseElement(event.target as HTMLElement);
};
focusListener = (event: KeyboardEvent) => {
if (event.key === 'Enter') {
this.collapseElement(event.target as HTMLElement);
}
};
componentDidMount() {
this.node!.addEventListener('click', this.clickListener);
this.node!.addEventListener('focus', this.focusListener);
}
componentWillUnmount() {
this.node!.removeEventListener('click', this.clickListener);
this.node!.removeEventListener('focus', this.focusListener);
}
}

View File

@ -1,12 +1,13 @@
import { css } from '../../styled-components';
export const jsonStyles = css`
.redoc-json > .collapser {
.redoc-json code > .collapser {
display: none;
pointer-events: none;
}
font-family: ${props => props.theme.typography.code.fontFamily};
font-size: ${props => props.theme.typography.code.fontSize};
font-family: ${(props) => props.theme.typography.code.fontFamily};
font-size: ${(props) => props.theme.typography.code.fontSize};
white-space: ${({ theme }) => (theme.typography.code.wrap ? 'pre-wrap' : 'pre')};
contain: content;
@ -47,8 +48,32 @@ export const jsonStyles = css`
}
.collapser {
background-color: transparent;
border: 0;
color: #fff;
font-family: ${(props) => props.theme.typography.code.fontFamily};
font-size: ${(props) => props.theme.typography.code.fontSize};
padding-right: 6px;
padding-left: 6px;
padding-top: 0;
padding-bottom: 0;
display: flex;
align-items: center;
justify-content: center;
width: 15px;
height: 15px;
position: absolute;
top: 4px;
left: -1.5em;
cursor: default;
user-select: none;
-webkit-user-select: none;
padding: 2px;
&:focus {
outline-color: #fff;
outline-style: dotted;
outline-width: 1px;
}
}
ul {
@ -83,13 +108,4 @@ export const jsonStyles = css`
.collapsed > .ellipsis {
display: inherit;
}
.collapser {
position: absolute;
top: 1px;
left: -1.5em;
cursor: default;
user-select: none;
-webkit-user-select: none;
}
`;

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

@ -37,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,7 +66,7 @@ export class Operation extends React.Component<OperationProps> {
<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} />

View File

@ -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

@ -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,7 +1,3 @@
// @ts-ignore
// eslint-disable-next-line @typescript-eslint/no-unused-vars
import ReactDropdown from 'react-dropdown';
import { observer } from 'mobx-react';
import * as React from 'react';
import { MediaTypeSamples } from './MediaTypeSamples';

View File

@ -1,7 +1,3 @@
// @ts-ignore
// eslint-disable-next-line @typescript-eslint/no-unused-vars
import ReactDropdown from 'react-dropdown';
import { transparentize } from 'polished';
import styled from '../../styled-components';
import { StyledDropdown } from '../../common-elements';
@ -24,7 +20,7 @@ export const DropdownLabel = styled.span`
top: -11px;
left: 12px;
font-weight: ${({ theme }) => theme.typography.fontWeightBold};
color: ${({ theme }) => transparentize(0.6, theme.rightPanel.textColor)};
color: ${({ theme }) => transparentize(0.3, theme.rightPanel.textColor)};
`;
export const DropdownWrapper = styled.div`
@ -32,40 +28,44 @@ 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-placeholder {
text-overflow: ellipsis;
white-space: nowrap;
overflow: hidden;
}
.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-option {
.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

@ -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

@ -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,26 @@ 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}
ariaLabel="Example"
/>
);
}
changeActiveChild = ({ value }) => {
const idx = parseInt(value, 10);
this.props.parent.activateOneOf(idx);
changeActiveChild = (option: DropdownOption) => {
this.props.parent.activateOneOf(option.idx);
};
}

View File

@ -65,7 +65,10 @@ export class Schema extends React.Component<Partial<SchemaProps>> {
switch (type) {
case 'object':
return <ObjectSchema {...(this.props as any)} />;
if (schema.fields?.length) {
return <ObjectSchema {...(this.props as any)} />;
}
break;
case 'array':
return <ArraySchema {...(this.props as any)} />;
}

View File

@ -139,6 +139,7 @@ export class SearchBox extends React.PureComponent<SearchBoxProps, SearchBoxStat
value={this.state.term}
onKeyDown={this.handleKeyDown}
placeholder="Search..."
aria-label="Search"
type="text"
onChange={this.search}
/>

View File

@ -66,6 +66,10 @@ export const SearchResultsBox = styled.div`
margin-top: 10px;
line-height: 1.4;
font-size: 0.9em;
li {
background-color: inherit;
}
${MenuItemLabel} {
padding-top: 6px;

View File

@ -1,7 +1,7 @@
// import { transparentize } from 'polished';
import * as React from 'react';
import styled from '../../styled-components';
import styled, { media } from '../../styled-components';
import { Link, UnderlinedHeader } from '../../common-elements/';
import { SecurityRequirementModel } from '../../services/models/SecurityRequirement';
@ -85,11 +85,14 @@ export class SecurityRequirement extends React.PureComponent<SecurityRequirement
}
const AuthHeaderColumn = styled.div`
flex: 1;
flex: 1 1 auto;
`;
const SecuritiesColumn = styled.div`
width: ${props => props.theme.schema.defaultDetailsWidth};
${media.lessThan('small')`
margin-top: 10px;
`}
`;
const AuthHeader = styled(UnderlinedHeader)`
@ -101,6 +104,10 @@ const Wrap = styled.div`
width: 100%;
display: flex;
margin: 1em 0;
${media.lessThan('small')`
flex-direction: column;
`}
`;
export interface SecurityRequirementsProps {

View File

@ -12,7 +12,7 @@ const AUTH_TYPES = {
oauth2: 'OAuth2',
apiKey: 'API Key',
http: 'HTTP',
openIdConnect: 'Open ID Connect',
openIdConnect: 'OpenID Connect',
};
export interface OAuthFlowProps {

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;
@ -95,7 +96,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

@ -7,7 +7,7 @@ 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,6 +60,10 @@ 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 }: { theme: ResolvedThemeInterface }): string {

View File

@ -16,7 +16,7 @@ export class SourceCode extends React.PureComponent<SourceCodeProps> {
}
}
export class SourceCodeWithCopy extends React.PureComponent<SourceCodeProps> {
export class SourceCodeWithCopy extends React.Component<SourceCodeProps> {
render() {
return (
<CopyButtonWrapper data={this.props.source}>

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

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

@ -1,7 +1,10 @@
import 'core-js/es/promise';
import 'core-js/es/array/find';
import 'core-js/es/array/includes';
import 'core-js/es/object/assign';
import 'core-js/es/object/entries';
import 'core-js/es/object/is';
import 'core-js/es/string/ends-with';
import 'core-js/es/string/starts-with';

View File

@ -5,9 +5,11 @@ export interface LabelsConfig {
default: string;
deprecated: string;
example: string;
examples: string;
nullable: string;
recursive: string;
arrayOf: string;
webhook: string;
}
export type LabelsConfigRaw = Partial<LabelsConfig>;
@ -19,9 +21,11 @@ const labels: LabelsConfig = {
default: 'Default',
deprecated: 'Deprecated',
example: 'Example',
examples: 'Examples',
nullable: 'Nullable',
recursive: 'Recursive',
arrayOf: 'Array of ',
webhook: 'Event',
};
export function setRedocLabels(_labels?: LabelsConfigRaw) {

View File

@ -5,6 +5,7 @@ import {
OpenAPITag,
Referenced,
OpenAPIServer,
OpenAPIPaths,
} from '../types';
import {
isOperationName,
@ -28,6 +29,7 @@ export type ExtendedOpenAPIOperation = {
httpVerb: string;
pathParameters: Array<Referenced<OpenAPIParameter>>;
pathServers: Array<OpenAPIServer> | undefined;
isWebhook: boolean;
} & OpenAPIOperation;
export type TagsInfoMap = Record<string, TagInfo>;
@ -236,43 +238,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,
pointer: JsonPointer.compile(['paths', pathName, operationName]),
httpVerb: operationName,
pathParameters: path.parameters || [],
pathServers: path.servers,
});
}
}
}
return tags;
}
}

View File

@ -1,4 +1,4 @@
import { action, observable } from 'mobx';
import { action, observable, makeObservable } from 'mobx';
import { querySelector } from '../utils/dom';
import { SpecStore } from './models';
@ -77,6 +77,8 @@ export class MenuStore {
* @param scroll scroll service instance used by this menu
*/
constructor(spec: SpecStore, public scroll: ScrollService, public history: HistoryService) {
makeObservable(this);
this.items = spec.contentItems;
this.flatItems = flattenByProp(this.items || [], 'items');

View File

@ -5,6 +5,7 @@ import { OpenAPIRef, OpenAPISchema, OpenAPISpec, Referenced } from '../types';
import { appendToMdHeading, IS_BROWSER } from '../utils/';
import { JsonPointer } from '../utils/JsonPointer';
import {
getDefinitionName,
isNamedDefinition,
SECURITY_DEFINITIONS_COMPONENT_NAME,
SECURITY_DEFINITIONS_JSX_NAME,
@ -150,6 +151,11 @@ export class OpenAPIParser {
*/
deref<T extends object>(obj: OpenAPIRef | T, forceCircular = false): T {
if (this.isRef(obj)) {
const schemaName = getDefinitionName(obj.$ref);
if (schemaName && this.options.ignoreNamedSchemas.has(schemaName)) {
return { type: 'object', title: schemaName } as T;
}
const resolved = this.byRef<T>(obj.$ref)!;
const visited = this._refCounter.visited(obj.$ref);
this._refCounter.visit(obj.$ref);
@ -202,7 +208,7 @@ export class OpenAPIParser {
...schema,
allOf: undefined,
parentRefs: [],
title: schema.title || (isNamedDefinition($ref) ? JsonPointer.baseName($ref) : undefined),
title: schema.title || getDefinitionName($ref),
};
// avoid mutating inner objects
@ -214,7 +220,7 @@ export class OpenAPIParser {
}
const allOfSchemas = schema.allOf
.map(subSchema => {
.map((subSchema) => {
if (subSchema && subSchema.$ref && used$Refs.has(subSchema.$ref)) {
return undefined;
}
@ -255,10 +261,12 @@ export class OpenAPIParser {
receiver.properties[prop] = subSchema.properties[prop];
} else {
// merge inner properties
receiver.properties[prop] = this.mergeAllOf(
const mergedProp = this.mergeAllOf(
{ allOf: [receiver.properties[prop], subSchema.properties[prop]] },
$ref + '/properties/' + prop,
);
receiver.properties[prop] = mergedProp
this.exitParents(mergedProp); // every prop resolution should have separate recursive stack
}
}
}

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,6 +26,7 @@ export interface RedocRawOptions {
menuToggle?: boolean | string;
jsonSampleExpandLevel?: number | string | 'all';
hideSchemaTitles?: boolean | string;
simpleOneOfTypeLabel?: boolean | string;
payloadSampleIdx?: number;
expandSingleSchemaField?: boolean | string;
sectionsAtTheEnd?: string | string[];
@ -38,6 +40,9 @@ export interface RedocRawOptions {
enumSkipQuotes?: boolean | string;
expandDefaultServerVariables?: boolean;
maxDisplayedEnumValues?: number;
ignoreNamedSchemas?: string[] | string;
hideSchemaPattern?: boolean;
}
function argValueToBoolean(val?: string | boolean, defaultValue?: boolean): boolean {
@ -50,6 +55,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') {
@ -57,7 +72,7 @@ export class RedocNormalizedOptions {
}
if (typeof value === 'string') {
const res = {};
value.split(',').forEach(code => {
value.split(',').forEach((code) => {
res[code.trim()] = true;
});
return res;
@ -123,7 +138,7 @@ export class RedocNormalizedOptions {
case 'false':
return false;
default:
return value.split(',').map(ext => ext.trim());
return value.split(',').map((ext) => ext.trim());
}
}
@ -163,6 +178,7 @@ export class RedocNormalizedOptions {
expandResponses: { [code: string]: boolean } | 'all';
requiredPropsFirst: boolean;
sortPropsAlphabetically: boolean;
sortEnumValuesAlphabetically: boolean;
noAutoAuth: boolean;
nativeScrollbars: boolean;
pathInMiddlePanel: boolean;
@ -176,6 +192,7 @@ export class RedocNormalizedOptions {
jsonSampleExpandLevel: number;
enumSkipQuotes: boolean;
hideSchemaTitles: boolean;
simpleOneOfTypeLabel: boolean;
payloadSampleIdx: number;
expandSingleSchemaField: boolean;
sectionsAtTheEnd: string[];
@ -185,6 +202,10 @@ export class RedocNormalizedOptions {
allowedMdComponents: Record<string, MDXComponentMeta>;
expandDefaultServerVariables: boolean;
maxDisplayedEnumValues?: number;
ignoreNamedSchemas: Set<string>;
hideSchemaPattern: boolean;
constructor(raw: RedocRawOptions, defaults: RedocRawOptions = {}) {
raw = { ...defaults, ...raw };
@ -215,6 +236,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);
@ -230,15 +252,21 @@ 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.sectionsAtTheEnd = RedocNormalizedOptions.normalizeSectionsAtTheEnd(raw.sectionsAtTheEnd);
// eslint-disable-next-line @typescript-eslint/camelcase
this.unstable_ignoreMimeParameters = argValueToBoolean(raw.unstable_ignoreMimeParameters);
this.allowedMdComponents = raw.allowedMdComponents || {};
this.expandDefaultServerVariables = argValueToBoolean(raw.expandDefaultServerVariables);
this.maxDisplayedEnumValues = argValueToNumber(raw.maxDisplayedEnumValues);
const ignoreNamedSchemas = Array.isArray(raw.ignoreNamedSchemas)
? raw.ignoreNamedSchemas
: raw.ignoreNamedSchemas?.split(',').map((s) => s.trim());
this.ignoreNamedSchemas = new Set(ignoreNamedSchemas);
this.hideSchemaPattern = argValueToBoolean(raw.hideSchemaPattern);
}
}

View File

@ -56,4 +56,10 @@ export class SearchStore<T> {
load(state: any) {
this.searchWorker.load(state);
}
fromExternalJS(path?: string, exportName?: string) {
if (path && exportName) {
this.searchWorker.fromExternalJS(path, exportName)
}
}
}

View File

@ -15,6 +15,7 @@ export default class Worker {
toJS = toJS;
load = load;
dispose = dispose;
fromExternalJS = fromExternalJS;
}
export interface SearchDocument {
@ -72,6 +73,19 @@ export async function toJS() {
};
}
export async function fromExternalJS(path: string, exportName: string) {
try {
importScripts(path);
if (!self[exportName]) {
throw new Error('Broken index file format');
}
load(self[exportName]);
} catch (e) {
console.error('Failed to load search index: ' + e.message);
}
}
export async function load(state: any) {
store = state.store;
resolveIndex(lunr.Index.load(state.index));
@ -95,6 +109,7 @@ export async function search<Meta = string>(
.toLowerCase()
.split(/\s+/)
.forEach(term => {
if (term.length === 1) return;
const exp = expandTerm(term);
t.term(exp, {});
});

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

@ -17,6 +17,21 @@
"schema": { "type": "array" },
"style": "form",
"explode": true
},
"queryParamWithNoStyle": {
"in": "query",
"name": "serialization_test_name",
"schema": { "type": "array" }
},
"pathParamWithNoStyle": {
"in": "path",
"name": "serialization_test_name",
"schema": { "type": "array" }
},
"cookieParamWithNoStyle": {
"in": "cookie",
"name": "serialization_test_name",
"schema": { "type": "array" }
}
},
"headers": {

View File

@ -20,7 +20,7 @@ describe('Models', () => {
);
expect(callback.name).toEqual('Test.Callback');
expect(callback.operations.length).toEqual(0);
expect(callback.expanded).toBeUndefined();
expect(callback.expanded).toBeFalsy();
});
});
});

View File

@ -43,6 +43,57 @@ describe('Models', () => {
expect(field.explode).toEqual(true);
});
test('field details relevant for default path param serialization', () => {
const field = new FieldModel(
parser,
{
$ref: '#/components/parameters/pathParamWithNoStyle',
},
'#/components/parameters/pathParamWithNoStyle',
opts,
);
expect(field.name).toEqual('serialization_test_name');
expect(field.in).toEqual('path');
expect(field.schema.type).toEqual('array');
expect(field.style).toEqual('simple');
expect(field.explode).toEqual(false);
});
test('field details relevant for default query parameter serialization', () => {
const field = new FieldModel(
parser,
{
$ref: '#/components/parameters/queryParamWithNoStyle',
},
'#/components/parameters/queryParamWithNoStyle',
opts,
);
expect(field.name).toEqual('serialization_test_name');
expect(field.in).toEqual('query');
expect(field.schema.type).toEqual('array');
expect(field.style).toEqual('form');
expect(field.explode).toEqual(true);
});
test('field details relevant for default cookie parameter serialization', () => {
const field = new FieldModel(
parser,
{
$ref: '#/components/parameters/queryParamWithNoStyle',
},
'#/components/parameters/queryParamWithNoStyle',
opts,
);
expect(field.name).toEqual('serialization_test_name');
expect(field.in).toEqual('query');
expect(field.schema.type).toEqual('array');
expect(field.style).toEqual('form');
expect(field.explode).toEqual(true);
});
test('field name should populated from name even if $ref (headers)', () => {
const field = new FieldModel(
parser,

View File

@ -1,4 +1,4 @@
import { action, observable } from 'mobx';
import { action, observable, makeObservable } from 'mobx';
import { OpenAPICallback, Referenced } from '../../types';
import { isOperationName, JsonPointer } from '../../utils';
@ -8,7 +8,8 @@ import { RedocNormalizedOptions } from '../RedocNormalizedOptions';
export class CallbackModel {
@observable
expanded: boolean;
expanded: boolean = false;
name: string;
operations: OperationModel[] = [];
@ -19,6 +20,8 @@ export class CallbackModel {
pointer: string,
options: RedocNormalizedOptions,
) {
makeObservable(this);
this.name = name;
const paths = parser.deref<OpenAPICallback>(infoOrRef);
parser.exitRef(infoOrRef);

View File

@ -1,4 +1,4 @@
import { action, observable } from 'mobx';
import { action, observable, makeObservable } from 'mobx';
import {
OpenAPIParameter,
@ -11,32 +11,44 @@ import { RedocNormalizedOptions } from '../RedocNormalizedOptions';
import { extractExtensions } from '../../utils/openapi';
import { OpenAPIParser } from '../OpenAPIParser';
import { SchemaModel } from './Schema';
import { ExampleModel } from './Example';
import { mapValues } from '../../utils/helpers';
function getDefaultStyleValue(parameterLocation: OpenAPIParameterLocation): OpenAPIParameterStyle {
switch (parameterLocation) {
case 'header':
return 'simple';
case 'query':
return 'form';
case 'path':
return 'simple';
default:
return 'form';
}
}
const DEFAULT_SERIALIZATION: Record<
OpenAPIParameterLocation,
{ explode: boolean; style: OpenAPIParameterStyle }
> = {
path: {
style: 'simple',
explode: false,
},
query: {
style: 'form',
explode: true,
},
header: {
style: 'simple',
explode: false,
},
cookie: {
style: 'form',
explode: true,
},
};
/**
* Field or Parameter model ready to be used by components
*/
export class FieldModel {
@observable
expanded: boolean | undefined;
expanded: boolean | undefined = false;
schema: SchemaModel;
name: string;
required: boolean;
description: string;
example?: string;
examples?: Record<string, ExampleModel>;
deprecated: boolean;
in?: OpenAPIParameterLocation;
kind: string;
@ -52,6 +64,8 @@ export class FieldModel {
pointer: string,
options: RedocNormalizedOptions,
) {
makeObservable(this);
const info = parser.deref<OpenAPIParameter>(infoOrRef);
this.kind = infoOrRef.kind || 'field';
this.name = infoOrRef.name || info.name;
@ -70,15 +84,26 @@ export class FieldModel {
info.description === undefined ? this.schema.description || '' : info.description;
this.example = info.example || this.schema.example;
if (info.examples !== undefined) {
this.examples = mapValues(
info.examples,
(example, name) => new ExampleModel(parser, example, name, info.encoding),
);
}
if (serializationMime) {
this.serializationMime = serializationMime;
} else if (info.style) {
this.style = info.style;
} else if (this.in) {
this.style = getDefaultStyleValue(this.in);
this.style = DEFAULT_SERIALIZATION[this.in]?.style ?? 'form'; // fallback to from in case "in" is invalid
}
this.explode = !!info.explode;
if (info.explode === undefined && this.in) {
this.explode = DEFAULT_SERIALIZATION[this.in]?.explode ?? true;
} else {
this.explode = !!info.explode;
}
this.deprecated = info.deprecated === undefined ? !!this.schema.deprecated : info.deprecated;
parser.exitRef(infoOrRef);

View File

@ -1,4 +1,4 @@
import { action, observable } from 'mobx';
import { action, observable, makeObservable } from 'mobx';
import { OpenAPIExternalDocumentation, OpenAPITag } from '../../types';
import { safeSlugify } from '../../utils';
@ -36,6 +36,8 @@ export class GroupModel implements IMenuItem {
tagOrGroup: OpenAPITag | MarkdownHeading,
parent?: GroupModel,
) {
makeObservable(this);
// markdown headings already have ids calculated as they are needed for heading anchors
this.id = (tagOrGroup as MarkdownHeading).id || type + '/' + safeSlugify(tagOrGroup.name);
this.type = type;

View File

@ -1,4 +1,4 @@
import { action, computed, observable } from 'mobx';
import { action, computed, observable, makeObservable } from 'mobx';
import { OpenAPIMediaType } from '../../types';
import { MediaTypeModel } from './MediaType';
@ -26,6 +26,8 @@ export class MediaContentModel {
public isRequestType: boolean,
options: RedocNormalizedOptions,
) {
makeObservable(this);
if (options.unstable_ignoreMimeParameters) {
info = mergeSimilarMediaTypes(info);
}

View File

@ -53,6 +53,7 @@ export class MediaTypeModel {
skipReadOnly: this.isRequestType,
skipNonRequired: this.isRequestType && this.onlyRequiredInSamples,
skipWriteOnly: !this.isRequestType,
maxSampleDepth: 10,
};
if (this.schema && this.schema.oneOf) {
this.examples = {};

View File

@ -1,4 +1,4 @@
import { action, observable } from 'mobx';
import { action, observable, makeObservable } from 'mobx';
import { IMenuItem } from '../MenuStore';
import { GroupModel } from './Group.model';
@ -50,7 +50,7 @@ export class OperationModel implements IMenuItem {
absoluteIdx?: number;
name: string;
description?: string;
type = 'operation' as 'operation';
type = 'operation' as const;
parent?: GroupModel;
externalDocs?: OpenAPIExternalDocumentation;
@ -75,6 +75,7 @@ export class OperationModel implements IMenuItem {
security: SecurityRequirementModel[];
extensions: Record<string, any>;
isCallback: boolean;
isWebhook: boolean;
topMargin: boolean;
constructor(
@ -84,6 +85,8 @@ export class OperationModel implements IMenuItem {
private options: RedocNormalizedOptions,
isCallback: boolean = false,
) {
makeObservable(this);
this.pointer = operationSpec.pointer;
this.description = operationSpec.description;
@ -96,6 +99,7 @@ export class OperationModel implements IMenuItem {
this.operationId = operationSpec.operationId;
this.path = operationSpec.pathName;
this.isCallback = isCallback;
this.isWebhook = !!operationSpec.isWebhook;
this.name = getOperationSummary(operationSpec);
@ -103,7 +107,7 @@ export class OperationModel implements IMenuItem {
// NOTE: Callbacks by default should not inherit the specification's global `security` definition.
// Can be defined individually per-callback in the specification. Defaults to none.
this.security = (operationSpec.security || []).map(
security => new SecurityRequirementModel(security, parser),
(security) => new SecurityRequirementModel(security, parser),
);
// TODO: update getting pathInfo for overriding servers on path level
@ -117,7 +121,7 @@ export class OperationModel implements IMenuItem {
: this.pointer;
this.security = (operationSpec.security || parser.spec.security || []).map(
security => new SecurityRequirementModel(security, parser),
(security) => new SecurityRequirementModel(security, parser),
);
this.servers = normalizeServers(
@ -209,7 +213,7 @@ export class OperationModel implements IMenuItem {
this.operationSpec.pathParameters,
this.operationSpec.parameters,
// TODO: fix pointer
).map(paramOrRef => new FieldModel(this.parser, paramOrRef, this.pointer, this.options));
).map((paramOrRef) => new FieldModel(this.parser, paramOrRef, this.pointer, this.options));
if (this.options.sortPropsAlphabetically) {
return sortByField(_parameters, 'name');
@ -225,7 +229,7 @@ export class OperationModel implements IMenuItem {
get responses() {
let hasSuccessResponses = false;
return Object.keys(this.operationSpec.responses || [])
.filter(code => {
.filter((code) => {
if (code === 'default') {
return true;
}
@ -236,7 +240,7 @@ export class OperationModel implements IMenuItem {
return isStatusCode(code);
}) // filter out other props (e.g. x-props)
.map(code => {
.map((code) => {
return new ResponseModel(
this.parser,
code,
@ -249,7 +253,7 @@ export class OperationModel implements IMenuItem {
@memoize
get callbacks() {
return Object.keys(this.operationSpec.callbacks || []).map(callbackEventName => {
return Object.keys(this.operationSpec.callbacks || []).map((callbackEventName) => {
return new CallbackModel(
this.parser,
callbackEventName,

View File

@ -1,4 +1,4 @@
import { action, observable } from 'mobx';
import { action, observable, makeObservable } from 'mobx';
import { OpenAPIResponse, Referenced } from '../../types';
@ -10,7 +10,7 @@ import { MediaContentModel } from './MediaContent';
export class ResponseModel {
@observable
expanded: boolean;
expanded: boolean = false;
content?: MediaContentModel;
code: string;
@ -26,6 +26,8 @@ export class ResponseModel {
infoOrRef: Referenced<OpenAPIResponse>,
options: RedocNormalizedOptions,
) {
makeObservable(this);
this.expanded = options.expandResponses === 'all' || options.expandResponses[code];
const info = parser.deref(infoOrRef);

View File

@ -1,4 +1,4 @@
import { action, observable } from 'mobx';
import { action, observable, makeObservable } from 'mobx';
import { OpenAPIExternalDocumentation, OpenAPISchema, Referenced } from '../../types';
@ -72,6 +72,8 @@ export class SchemaModel {
private options: RedocNormalizedOptions,
isChild: boolean = false,
) {
makeObservable(this);
this.pointer = schemaOrRef.$ref || pointer || '';
this.rawSchema = parser.deref(schemaOrRef);
this.schema = parser.mergeAllOf(this.rawSchema, this.pointer, isChild);
@ -129,7 +131,7 @@ export class SchemaModel {
} else if (
isChild &&
Array.isArray(schema.oneOf) &&
schema.oneOf.find(s => s.$ref === this.pointer)
schema.oneOf.find((s) => s.$ref === this.pointer)
) {
// we hit allOf of the schema with the parent discriminator
delete schema.oneOf;
@ -168,6 +170,10 @@ export class SchemaModel {
this.enum = this.items.enum;
}
}
if (this.enum.length && this.options.sortEnumValuesAlphabetically) {
this.enum.sort();
}
}
private initOneOf(oneOf: OpenAPISchema[], parser: OpenAPIParser) {
@ -203,17 +209,22 @@ export class SchemaModel {
return schema;
});
this.displayType = this.oneOf
.map(schema => {
let name =
schema.typePrefix +
(schema.title ? `${schema.title} (${schema.displayType})` : schema.displayType);
if (name.indexOf(' or ') > -1) {
name = `(${name})`;
}
return name;
})
.join(' or ');
if (this.options.simpleOneOfTypeLabel) {
const types = collectUniqueOneOfTypesDeep(this);
this.displayType = types.join(' or ');
} else {
this.displayType = this.oneOf
.map((schema) => {
let name =
schema.typePrefix +
(schema.title ? `${schema.title} (${schema.displayType})` : schema.displayType);
if (name.indexOf(' or ') > -1) {
name = `(${name})`;
}
return name;
})
.join(' or ');
}
}
private initDiscriminator(
@ -324,7 +335,7 @@ function buildFields(
const props = schema.properties || {};
const additionalProps = schema.additionalProperties;
const defaults = schema.default || {};
let fields = Object.keys(props || []).map(fieldName => {
let fields = Object.keys(props || []).map((fieldName) => {
let field = props[fieldName];
if (!field) {
@ -385,3 +396,23 @@ function buildFields(
function getDiscriminator(schema: OpenAPISchema): OpenAPISchema['discriminator'] {
return schema.discriminator || schema['x-discriminator'];
}
function collectUniqueOneOfTypesDeep(schema: SchemaModel) {
const uniqueTypes = new Set();
function crawl(schema: SchemaModel) {
for (const oneOfType of schema.oneOf || []) {
if (oneOfType.oneOf) {
crawl(oneOfType);
continue;
}
if (oneOfType.type) {
uniqueTypes.add(oneOfType.type);
}
}
}
crawl(schema);
return Array.from(uniqueTypes.values());
}

View File

@ -0,0 +1,38 @@
import { OpenAPIPath, Referenced } from '../../types';
import { OpenAPIParser } from '../OpenAPIParser';
import { OperationModel } from './Operation';
import { isOperationName } from '../..';
import { RedocNormalizedOptions } from '../RedocNormalizedOptions';
export class WebhookModel {
operations: OperationModel[] = [];
constructor(
parser: OpenAPIParser,
options: RedocNormalizedOptions,
infoOrRef?: Referenced<OpenAPIPath>,
) {
const webhooks = parser.deref<OpenAPIPath>(infoOrRef || {});
parser.exitRef(infoOrRef);
for (const webhookName of Object.keys(webhooks)) {
const webhook = webhooks[webhookName];
const operations = Object.keys(webhook).filter(isOperationName);
for (const operationName of operations) {
const operationInfo = webhook[operationName];
const operation = new OperationModel(
parser,
{
...operationInfo,
httpVerb: operationName,
},
undefined,
options,
false,
);
this.operations.push(operation);
}
}
}
}

View File

@ -1,11 +1,16 @@
import * as React from 'react';
import { hydrate as hydrateComponent, render } from 'react-dom';
import { configure } from "mobx"
import { Redoc, RedocStandalone } from './components/';
import { AppStore, StoreState } from './services/AppStore';
import { debugTime, debugTimeEnd } from './utils/debug';
import { querySelector } from './utils/dom';
configure({
useProxies: 'ifavailable'
})
export { Redoc, AppStore } from '.';
export const version = __REDOC_VERSION__;

View File

@ -13,10 +13,10 @@ const {
} = styledComponents as styledComponents.ThemedStyledComponentsModule<ResolvedThemeInterface>;
export const media = {
lessThan(breakpoint, print?: boolean) {
lessThan(breakpoint, print?: boolean, extra?: string) {
return (...args) => css`
@media ${print ? 'print, ' : ''} screen and (max-width: ${props =>
props.theme.breakpoints[breakpoint]}) {
props.theme.breakpoints[breakpoint]})${extra || ''} {
${(css as any)(...args)};
}
`;

View File

@ -12,7 +12,7 @@ const defaultTheme: ThemeInterface = {
large: '105rem',
},
colors: {
tonalOffset: 0.3,
tonalOffset: 0.2,
primary: {
main: '#32329f',
light: ({ colors }) => lighten(colors.tonalOffset, colors.primary.main),
@ -20,7 +20,7 @@ const defaultTheme: ThemeInterface = {
contrastText: ({ colors }) => readableColor(colors.primary.main),
},
success: {
main: '#37d247',
main: '#1d8127',
light: ({ colors }) => lighten(colors.tonalOffset * 2, colors.success.main),
dark: ({ colors }) => darken(colors.tonalOffset, colors.success.main),
contrastText: ({ colors }) => readableColor(colors.success.main),
@ -32,7 +32,7 @@ const defaultTheme: ThemeInterface = {
contrastText: '#ffffff',
},
error: {
main: '#e53935',
main: '#d41f1c',
light: ({ colors }) => lighten(colors.tonalOffset, colors.error.main),
dark: ({ colors }) => darken(colors.tonalOffset, colors.error.main),
contrastText: ({ colors }) => readableColor(colors.error.main),
@ -52,31 +52,35 @@ const defaultTheme: ThemeInterface = {
responses: {
success: {
color: ({ colors }) => colors.success.main,
backgroundColor: ({ colors }) => transparentize(0.9, colors.success.main),
backgroundColor: ({ colors }) => transparentize(0.93, colors.success.main),
tabTextColor: ({ colors }) => colors.responses.success.color,
},
error: {
color: ({ colors }) => colors.error.main,
backgroundColor: ({ colors }) => transparentize(0.9, colors.error.main),
backgroundColor: ({ colors }) => transparentize(0.93, colors.error.main),
tabTextColor: ({ colors }) => colors.responses.error.color,
},
redirect: {
color: ({ colors }) => colors.warning.main,
backgroundColor: ({ colors }) => transparentize(0.9, colors.responses.redirect.color),
tabTextColor: ({ colors }) => colors.responses.redirect.color,
},
info: {
color: '#87ceeb',
backgroundColor: ({ colors }) => transparentize(0.9, colors.responses.info.color),
tabTextColor: ({ colors }) => colors.responses.info.color,
},
},
http: {
get: '#6bbd5b',
post: '#248fb2',
put: '#9b708b',
options: '#d3ca12',
patch: '#e09d43',
delete: '#e27a7a',
basic: '#999',
link: '#31bbb6',
head: '#c167e4',
get: '#2F8132',
post: '#186FAF',
put: '#95507c',
options: '#947014',
patch: '#bf581d',
delete: '#cc3333',
basic: '#707070',
link: '#07818F',
head: '#A23DAD',
},
},
schema: {
@ -146,8 +150,8 @@ const defaultTheme: ThemeInterface = {
},
},
logo: {
maxHeight: ({ sidebar: menu }) => menu.width,
maxWidth: ({ sidebar: menu }) => menu.width,
maxHeight: ({ sidebar }) => sidebar.width,
maxWidth: ({ sidebar }) => sidebar.width,
gutter: '2px',
},
rightPanel: {
@ -206,6 +210,7 @@ export interface ColorSetting {
export interface HTTPResponseColos {
color: string;
backgroundColor: string;
tabTextColor: string;
}
export interface FontSettings {

View File

@ -9,6 +9,7 @@ export interface OpenAPISpec {
security?: OpenAPISecurityRequirement[];
tags?: OpenAPITag[];
externalDocs?: OpenAPIExternalDocumentation;
'x-webhooks'?: OpenAPIPaths;
}
export interface OpenAPIInfo {
@ -94,6 +95,7 @@ export interface OpenAPIParameter {
example?: any;
examples?: { [media: string]: Referenced<OpenAPIExample> };
content?: { [media: string]: OpenAPIMediaType };
encoding?: Record<string, OpenAPIEncoding>;
}
export interface OpenAPIExample {

View File

@ -335,7 +335,8 @@ describe('Utils', () => {
min: number | undefined = undefined,
max: number | undefined = undefined,
multipleOf: number | undefined = undefined,
) => ({ type: 'array', minItems: min, maxItems: max, multipleOf });
uniqueItems?: boolean,
) => ({ type: 'array', minItems: min, maxItems: max, multipleOf, uniqueItems });
it('should not have a humanized constraint without schema constraints', () => {
expect(humanizeConstraints(itemConstraintSchema())).toHaveLength(0);
@ -372,6 +373,12 @@ describe('Utils', () => {
'multiple of 0.5',
);
});
it('should have a humanized constraint when uniqueItems is set', () => {
expect(humanizeConstraints(itemConstraintSchema(undefined, undefined, undefined, true))).toContain(
'unique',
);
});
});
describe('OpenAPI pluralizeType', () => {

View File

@ -68,11 +68,11 @@ export function mapLang(lang: string): string {
* @param lang highlight language
* @return highlighted source code as **html string**
*/
export function highlight(source: string, lang: string = DEFAULT_LANG): string {
export function highlight(source: string | number | boolean, lang: string = DEFAULT_LANG): string {
lang = lang.toLowerCase();
let grammar = Prism.languages[lang];
if (!grammar) {
grammar = Prism.languages[mapLang(lang)];
}
return Prism.highlight(source, grammar, lang);
return Prism.highlight(source.toString(), grammar, lang);
}

View File

@ -73,9 +73,9 @@ function valueToHTML(value, maxExpandLevel: number) {
function arrayToHTML(json, maxExpandLevel: number) {
const collapsed = level > maxExpandLevel ? 'collapsed' : '';
let output = `<div class="collapser"></div>${punctuation(
'[',
)}<span class="ellipsis"></span><ul class="array collapsible">`;
let output = `<button class="collapser" aria-label="${
level > maxExpandLevel + 1 ? 'expand' : 'collapse'
}"></button>${punctuation('[')}<span class="ellipsis"></span><ul class="array collapsible">`;
let hasContents = false;
const length = json.length;
for (let i = 0; i < length; i++) {
@ -98,9 +98,9 @@ function objectToHTML(json, maxExpandLevel: number) {
const collapsed = level > maxExpandLevel ? 'collapsed' : '';
const keys = Object.keys(json);
const length = keys.length;
let output = `<div class="collapser"></div>${punctuation(
'{',
)}<span class="ellipsis"></span><ul class="obj collapsible">`;
let output = `<button class="collapser" aria-label="${
level > maxExpandLevel + 1 ? 'expand' : 'collapse'
}"></button>${punctuation('{')}<span class="ellipsis"></span><ul class="obj collapsible">`;
let hasContents = false;
for (let i = 0; i < length; i++) {
const key = keys[i];

View File

@ -19,7 +19,7 @@ export async function loadAndBundleSpec(specUrlOrObject: object | string): Promi
export function convertSwagger2OpenAPI(spec: any): Promise<OpenAPISpec> {
console.warn('[ReDoc Compatibility mode]: Converting OpenAPI 2.0 to OpenAPI 3.0');
return new Promise<OpenAPISpec>((resolve, reject) =>
convertObj(spec, { patch: true, warnOnly: true, text: '{}' }, (err, res) => {
convertObj(spec, { patch: true, warnOnly: true, text: '{}', anchors: true }, (err, res) => {
// TODO: log any warnings
if (err) {
return reject(err);

View File

@ -369,6 +369,12 @@ export function isNamedDefinition(pointer?: string): boolean {
return /^#\/components\/schemas\/[^\/]+$/.test(pointer || '');
}
export function getDefinitionName(pointer?: string): string | undefined {
if (!pointer) return undefined;
const match = pointer.match(/^#\/components\/schemas\/([^\/]+)$/);
return match === null ? undefined : match[1]
}
function humanizeMultipleOfConstraint(multipleOf: number | undefined): string | undefined {
if (multipleOf === undefined) {
return;
@ -442,6 +448,10 @@ export function humanizeConstraints(schema: OpenAPISchema): string[] {
res.push(numberRange);
}
if (schema.uniqueItems) {
res.push('unique');
}
return res;
}
@ -512,7 +522,7 @@ export function mergeSimilarMediaTypes(
export function expandDefaultServerVariables(url: string, variables: object = {}) {
return url.replace(
/(?:{)(\w+)(?:})/g,
/(?:{)([\w-.]+)(?:})/g,
(match, name) => (variables[name] && variables[name].default) || match,
);
}

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