Merge remote-tracking branch 'source/master' into develop

# Conflicts:
#	.gitignore
#	package.json
#	src/components/ApiInfo/ApiInfo.tsx
#	src/components/SearchBox/styled.elements.tsx
#	src/components/SideMenu/SideMenu.tsx
#	src/services/RedocNormalizedOptions.ts
#	src/theme.ts
This commit is contained in:
akumarsingh 2020-03-18 14:31:46 -04:00
commit 5af9c204e3
108 changed files with 22742 additions and 15742 deletions

View File

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

49
.eslintrc.js Normal file
View File

@ -0,0 +1,49 @@
module.exports = {
env: {
browser: true,
},
parser: '@typescript-eslint/parser',
extends: ['plugin:react/recommended', 'plugin:@typescript-eslint/recommended'],
parserOptions: {
project: 'tsconfig.json',
sourceType: 'module',
createDefaultProgram: true,
ecmaFeatures: {
jsx: true,
},
},
settings: {
react: {
version: 'detect',
},
},
plugins: ['@typescript-eslint', 'import'],
rules: {
'@typescript-eslint/explicit-function-return-type': 'off',
'@typescript-eslint/no-explicit-any': 'off',
'@typescript-eslint/no-use-before-define': 'off',
'@typescript-eslint/interface-name-prefix': 'off',
'@typescript-eslint/no-inferrable-types': 'off',
'@typescript-eslint/no-non-null-assertion': 'off',
'@typescript-eslint/ban-ts-ignore': 'off',
'react/prop-types': 'off',
'import/no-extraneous-dependencies': 'error',
'import/no-internal-modules': [
'error',
{
allow: [
'prismjs/**',
'perfect-scrollbar/**',
'react-dom/*',
'core-js/**',
'memoize-one/**',
'unfetch/**',
'raf/polyfill',
'**/fixtures/**', // for tests
],
},
],
},
};

View File

@ -15,56 +15,56 @@ Hi! We're really excited that you are interested in contributing to ReDoc. Befor
Before submitting a pull request, please make sure the following is done:
1. Fork the repository and create your branch from master.
2. Run `yarn` in the repository root.
2. Run `npm install` in the repository root.
3. If youve fixed a bug or added code that should be tested, add tests!
4. Ensure the test suite passes (`yarn test`). Tip: `yarn test --watch TestName` is helpful in development.
5. Format your code with prettier (`yarn prettier`).
4. Ensure the test suite passes (`npm test`). Tip: `npm test -- --watch TestName` is helpful in development.
5. Format your code with prettier (`npm run prettier`).
## Development Setup
You will need [Node.js](http://nodejs.org) at `v8.0.0+` and [Yarn](https://yarnpkg.com/en/) at `v1.2.0+`
You will need [Node.js](http://nodejs.org) at `v12.0.0+`.
After cloning the repo, run:
```bash
$ yarn install # or npm
$ npm install # or npm
```
### Commonly used NPM scripts
``` bash
# dev-server, watch and auto reload playground
$ yarn start
$ npm start
# start playground app in production environement
$ yarn start:prod
# start playground app in production environment
$ npm run start:prod
# runt tslint
$ yarn lint
$ npm run lint
# try autofix tslint issues
$ yarn lint --fix
$ npm run lint -- --fix
# run unit tests
$ yarn unit
$ npm run unit
# run e2e tests
$ yarn e2e
$ npm run e2e
# open cypress UI to debug e2e test
$ yarn cy:open
$ npm run cy:open
# run the full test suite, include linting / unit / e2e
$ yarn test
$ npm test
# prepare bundles
$ yarn bundle
$ npm run bundle
# format the code using prettier
$ yarn prettier
$ npm run prettier
# auto-generate changelog
$ yarn changelog
$ npm run changelog
```
There are some other scripts available in the `scripts` section of the `package.json` file.
@ -84,11 +84,11 @@ There are some other scripts available in the `scripts` section of the `package.
- **`src`**: contains the source code. The codebase is written in Typescript. CSS styles are managed with [Styled components](https://www.styled-components.com/). State is managed by [MobX](https://github.com/mobxjs/mobx)
- **`src/common-elements`**: containts common Styled elements or components used in multiple places
- **`src/common-elements`**: contains common Styled elements or components used in multiple places
- **`src/components`**: contains main visual components
- **`src/services`**: contains different services used by ReDoc including MobX stores
- **`src/services/models`**: contains classes for OpenAPI entities (e.g. Response, Operations, etc)
- **`src/types`**: contains extra typescript typings including OpenAPI doc typings
- **`src/utils`**: utility functions
- **`src/styled-components.ts`**: - reexprots styled-components with proper typescript annotations using theme
- **`src/styled-components.ts`**: - reexports styled-components with proper typescript annotations using theme
- **`src/theme.ts`**: - default theme (colors, fonts, etc) used by all the components

20
.github/workflows/unit-tests.yml vendored Normal file
View File

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

2
.gitignore vendored
View File

@ -34,7 +34,7 @@ cli/index.js
/coverage
.ghpages-tmp
stats.json
/package-lock.json
yarn.lock
.idea/
*.iml

View File

@ -1,7 +1,9 @@
language: node_js
node_js:
- '8'
cache: yarn
- '10'
cache:
directories:
- "~/.cache"
env:
global:
- GH_REF: github.com/Redocly/redoc.git
@ -11,9 +13,12 @@ env:
- secure: apiavCfCQngL9Een1m7MIXMf3bqO3rY4YY59TMBl/yFKi80CEsHPHhgVUkl6hC+aM5PeBt/vgjh37rHMX31j/pcSZ4Z8SO/4Bwr36iHfhSxSEuAQog8P07qWqH7wYYWGIVmF682stgl0fYF+GN92sx/6edFVzsWVECf2G7imtICKSTbhKGm3Dhn2JwGnhD7eyfgZ33omgiaswumdu0xABoXDfqSZR+16fC4Ap5rhv3fXO9ndvRNy1STn376nT+my6e86UrQL4aS/S+HNHgIe1BUs+5cOp6Jgw6t0ie7phY0EAiECsRxy9K4e3Dctv9m6+Wma4+vy65MS0zGyrqey6oyV4l827sCOjrD1qcqc9bX6FlMSouVoNfE4ZjINNAbgigTaiLSoDSPcf5I5smkkM2ezzFOMSZwZxNdaNL2LKb97vc8m/ZUkv0sKZyT7oqVL7aJweEivsSHj5l2KR8Z7XrVB1y2eI6GvyTSa/d+CL4dSRzjh8+IRN047YBrdTKD5IkdT0upfoBu14WPUfFmLKxX+iMCslXRWb6kwojhrWNYmZvL65KRAzJ6+eIPDG/W5QUOpYyYT77bLlBQjVo6NmVvl9v3HMECq9CHH0ivKFBGPiKMOx7cJkTax3FuyznOW2WCXB9kTb5Zk9toaiNlSp9L6ll/h2Eyxa6n6sWUgmmM=
- secure: vVRg9BKGBwF2MbXQnEccFL+XW0/7RaBmge9k7jbGYScBwkP3XjnQ/Xaj0cvTz2CM2EqXsbpwfvr4Jo+enW/E3MGy5RiEzv5hUe/jIFRR0gfAFbZxSTvg5xiFhTDffqQk0fncO4jXu+wPO5lZ2CMRWzyXz3i1MZhjMcAgoDr1+TRss/EGXLNHxr2RM88tpUW0fV2prIRoyGqhCgnYZtrm7hmr41Ej+itg1MqZLml/Rjkt3KsNgI+z0O5Qn3QSAO8GtPZqeftQxAjevOmxZGcssxY8EJvqbjAujr4y51WncXpEmCRPSY2J9R5+fkgZurqwnJapbQpjwKYemok3ps7EHg2gWkAlmPdQO4LKpbffGkM/o5b+8+HdIuQZugsSWQD9hUSftTAFLcfA1isi7V2lHE1m8bX/vk9zIyDdcPSwIaFe9y+w3PexwFmTjPLq+nia/UY2kARFZMEIFAJby6gkA70DcAJ50QOM86InJu5DSzGbIssgTGAXCn0TPPyGveaurVLw8x61j3yh8LDF46gUHey3rqv6WjpCM9h/vg7X/gq5ve/5Q2KHscUKfs/sA53Mt7qPeqRZY1QCaaRjzqJO/ZraHqWWeKmPKaWhPGR0kYEnkvB+K9GZ+HNSWCltjCO4SJ1xeEl7CRqQxAwdiMATF5SKqyiC+bn5oc35mFgbRF8=
- secure: ela1tn4wkJQZ8O4iv+4pIZi5cebxeCStVF1tEUe6qa6WWgJYVXmS2tEv3QQ36NUBFrP58Y6yl10XguPnvj/2BCqcZI4FUBHh3BfiBoUtXxDCVKI5LtlniNiOFGUwfzEeYka8T51zFlcUXSCCaxHkRZbmBsIzeJ39UwTi5fy0qwLv9GgL0czhwm8I8sZ8gyWdGmqpXNFEsb9JP4ZA3mw2qpWkGpGAqQPD9XSCkU3LmX1/ltwsBMAgGYKLLo7vU8d5KV2c8L1Gnxfl6BvfmqUD/dsas/1rnk08rU2nez5ekuQa2tJRkDLOv8bqvrGRLjHSUa3yPuisC6SsDGSU7/3DcozZyYsz7WQ6WI8tYabyjqqeJTF1N8a5T3IbZaZNV1J4JHOO9Cb/y7gIg4edANg6tbe7MzZpdEPRBnw6OkdTdirpNsWQ/jnfpY1hn6mraQZz/q8yaz3W21NjbBJhVnvfh5gWLKQ3YAAziCBhmmrThFhUu0czz+G920MuFo477TBcxvlrE7CaNJ0Q6yYkDehEPOv3jvEs1QVHPwuRrlaLTbBhrlTICKZ58gdX30O8N4i0Xgp/v6qrC03bplnMQc8E/uC61wcVLJixnlZVp8FODpUvPjsxVFkpuNSOIAaiqcERmoiPXx05Epzmr78hjU5rYCx/1MmVoeB4gs9YO+4guD4=
- secure: SEqTg6WoGPPpcWzJ03ZfcSBb3nZ2Mdhug0ec2PszuzYO3libCb9usiqi+jils9z6qyXsL6ecz8HYazDGOUepnubhIpI5otLgfn9XiapjMT06Bj//AjbKpH7eu3TJSpJMzoRHZrKIE1y9ZKIBqKwl9Xs7ko+1oa+MLhrLuxXkoi0JqRB5UzkQtJRDoxVNjysnLQn+hsfnm+yuqPHZd2+Loy++q//WHuf9bwJrlkXn2ICYQIX5oQGlxNO6ui+OZklb0YknvyO5GdQeoKaHYru3MMKKCIS6I7AG9wLmPs5Ou3T0Ia0Xx4/7xazs0rH4NCVpIceSYc3v6evR37pp8MsFTC3BzjL1V3slTnmitC1KSNM8ndGRUg1nsCBkJysnR3HpX6SHuCH+UzOuMxEjwiPdSRnzJPEbTHa1HqMfTkTJMbm4zhp7W4/ozX4TtjUB0ql6NoQE2n0Z3aYgR2C78TmzaPQun8EgredWnCID1FedyexaNcw4HyZ2rXlcvG3rBzSwLHH5PePT9skyqy6KtIaL0MlAP556ilgUeyCZfCNdTmzCvPDZuqaeLRezWDdsKnRfTkxIW80QWlmZ6sW0hynJV5JN2Oghk9Tr+QzgV4ZF68FHwoU9YXCTyX4w5iTYq/GjvfTBqB3VSGPOz3PwU7r47tmaYzPj+I44zqktgxyuxDo=
- secure: Pc86j2/rgCPAEWcjzPbbVFkL2SwNt4CpeXP69zedMi9RomQblMJa9R0wbAT6H5VCnPky2cpmxkzjFWOc91N9crzceg6aOoWkPr6pTTnTv+EL70i6+XSrAFpkgRjszprxnU1Bz+GcYEjP/zR8479fx8ooSl7MwHOaP7XiQyaBQAbY1CPlmpT+b4Ut7Fm5QnD90/NgPjbKkngl0kVUfNFdFOSfJ3QWLyFCUUSQ4DlxccJOTIaOH/n8u9Nz4NTuuHE3XeRuEsuj2SJutJnFBUYwsvugrdPvKWiubkewJfylp6EwABzByENsg6XxW9SIq80lMc3Oi7ld9L2lAgpj+8/42olnbMzH0F0rw/p1ccPAdVwQVV6YFaqCzivK5A5aX5LmGKwJ6SR9k1PgcWP6sKKMIsIEObbyM88Tke3QkreEz+cLg/3jjko7Vpb0tbqh8BtbpWV+exL4rX3r2C5Mb1Es1W597hN5LSczWYFgw0ZETpfbVZg6Ri1iZks0wpsT/E+c0q2scUaBVrdTZseHxUPB7mPDlXL1l9/i4sOxPyBHZtJRAzeTT/fOXfj4vuD+ihspXzoRRLaQbizlb8FpyPA47XdmBDpXi3OBiaIFLwvybEn7qM7rqvWxdz6vvCZv0t/AN3t3Qvh2vHKCshHecaa8NoJQHWrdFMHeecYHyeoujZ8=
addons:
chrome: stable
apt:
packages:
- libgconf-2-4
before_script: npm run bundle
script: npm test && ([ "${TRAVIS_PULL_REQUEST}" = "false" ] && npm run e2e-ci || npm
run e2e)
@ -29,9 +34,6 @@ deploy:
tags: true
- provider: script
skip_cleanup: true
script: cd cli && yarn install && yarn ci-publish || true
- provider: script
skip_cleanup: true
script: yarn deploy:demo
script: npm run deploy:demo
on:
tags: true

View File

@ -1,3 +1,228 @@
# [2.0.0-rc.24](https://github.com/Redocly/redoc/compare/v2.0.0-rc.23...v2.0.0-rc.24) (2020-03-17)
### Bug Fixes
* Add debounce for 300 ms when searching ([#1089](https://github.com/Redocly/redoc/issues/1089)) ([373f018](https://github.com/Redocly/redoc/commit/373f018d0c183f83d07a4dbad4a4e2c9ab159f69))
* do not load SearchWorker if disableSearch is `true` ([#1191](https://github.com/Redocly/redoc/issues/1191)) ([af415e8](https://github.com/Redocly/redoc/commit/af415e89e8c074a3f7c84f76f24020a7bd545483)), closes [#764](https://github.com/Redocly/redoc/issues/764)
* fix major search performance due to wrong marker element ([8c053cc](https://github.com/Redocly/redoc/commit/8c053cc474e88befc3338307317c0702d212d4c3)), closes [#1109](https://github.com/Redocly/redoc/issues/1109)
### Features
* new option expandSingleSchemaField ([7608800](https://github.com/Redocly/redoc/commit/7608800d0acaa2fa0099dc840e17cd5aa90b54ca))
# [2.0.0-rc.23](https://github.com/Redocly/redoc/compare/v2.0.0-rc.22...v2.0.0-rc.23) (2020-02-09)
### Bug Fixes
* fix broken sticky sidebar in Chrome 80 ([1a2a7dd](https://github.com/Redocly/redoc/commit/1a2a7dd8331cedd6ced4c18accf0b417549b3ff3)), closes [#1167](https://github.com/Redocly/redoc/issues/1167)
# [2.0.0-rc.22](https://github.com/Redocly/redoc/compare/v2.0.0-rc.21...v2.0.0-rc.22) (2020-01-15)
### Bug Fixes
* do not process oneOf if inherited from parent with discriminator ([5248415](https://github.com/Redocly/redoc/commit/52484157912d908daea8255d0b7d684b33258d7a))
### Features
* add HTTP syntax highlighting ([#1157](https://github.com/Redocly/redoc/issues/1157)) ([27a4af7](https://github.com/Redocly/redoc/commit/27a4af707686d56280753473b4294ee4af096534))
# [2.0.0-rc.21](https://github.com/Redocly/redoc/compare/v2.0.0-rc.20...v2.0.0-rc.21) (2020-01-10)
### Bug Fixes
* empty servers behaviour per OAS spec ([ed1db0c](https://github.com/Redocly/redoc/commit/ed1db0c9027087ae0ae923e390e3e1d638a647ae)), closes [#1151](https://github.com/Redocly/redoc/issues/1151)
* fix duplicated content in tags when using md headings ([a260c84](https://github.com/Redocly/redoc/commit/a260c8414c34a259a70a20ebcd20ecbb06c3d250)), closes [#1150](https://github.com/Redocly/redoc/issues/1150) [#1152](https://github.com/Redocly/redoc/issues/1152)
* use mobile menu background color value from theme ([#1144](https://github.com/Redocly/redoc/issues/1144)) ([41a9b3c](https://github.com/Redocly/redoc/commit/41a9b3c18228d236d182d3c15c9abc35ae72a0d5))
# [2.0.0-rc.20](https://github.com/Redocly/redoc/compare/v2.0.0-rc.19...v2.0.0-rc.20) (2019-12-13)
### Bug Fixes
* fix missing parameters ([942d782](https://github.com/Redocly/redoc/commit/942d782b5a8d08767a7538741b75587cf1e67f44)), closes [#1142](https://github.com/Redocly/redoc/issues/1142)
# [2.0.0-rc.19](https://github.com/Redocly/redoc/compare/v2.0.0-rc.18...v2.0.0-rc.19) (2019-12-13)
### Bug Fixes
* change the title of "Security Scheme Type" to match "HTTP Authorization Scheme" ([#1126](https://github.com/Redocly/redoc/issues/1126)) ([289c8e6](https://github.com/Redocly/redoc/commit/289c8e6ae1ff00371f86d3f2646607c64bc30050))
* do not URI-encode parameter values for better readability ([6aeb0bf](https://github.com/Redocly/redoc/commit/6aeb0bf68df3f03f2ca1317f8b5787545bd363f1)), closes [#1138](https://github.com/Redocly/redoc/issues/1138)
* fix sortByRequired (stabilise sort) ([#1136](https://github.com/Redocly/redoc/issues/1136)) ([d92434d](https://github.com/Redocly/redoc/commit/d92434d11b08e8b0f6be5453ec69aa1d0e0df79f)), closes [#1104](https://github.com/Redocly/redoc/issues/1104) [#1121](https://github.com/Redocly/redoc/issues/1121) [#1061](https://github.com/Redocly/redoc/issues/1061)
* h2 padding on mobile ([7ed1a7e](https://github.com/Redocly/redoc/commit/7ed1a7ef0e7978a0dfb40afcc72c3362466f9624)), closes [#1118](https://github.com/Redocly/redoc/issues/1118)
* python comment stripped in headings ([4a25aae](https://github.com/Redocly/redoc/commit/4a25aaef69fad814836392ea7e41eb32c182a261)), closes [#1116](https://github.com/Redocly/redoc/issues/1116)
* remove hardcoded fontFamily for oneOf labels ([094ce91](https://github.com/Redocly/redoc/commit/094ce914e3f9cfe567b39db4ea88208014d8b686)), closes [#1120](https://github.com/Redocly/redoc/issues/1120)
* search-box use theme ([1bf490c](https://github.com/Redocly/redoc/commit/1bf490c05b343d262f8819bf1ddc433e070be1b9))
* support discriminator mapping 1-n ([6e390f9](https://github.com/Redocly/redoc/commit/6e390f9c7909da0b5d1d6fc571ab4ad92e715d6e)), closes [#1111](https://github.com/Redocly/redoc/issues/1111)
* wrap json examples in code tag ([#1064](https://github.com/Redocly/redoc/issues/1064)) ([dc5430e](https://github.com/Redocly/redoc/commit/dc5430e53def780a81612d269cc3aea3f8785eea))
### Features
* display `multipleOf` constrains ([#1065](https://github.com/Redocly/redoc/issues/1065)) ([3e90133](https://github.com/Redocly/redoc/commit/3e901336643b988ae45ae86c485005b8865e6e04))
* enable menuToggle by default ([5d81abe](https://github.com/Redocly/redoc/commit/5d81abeb28c1e4f2826e41424c10163834c37e45))
* new option hideSchemaTitles ([11cc4c4](https://github.com/Redocly/redoc/commit/11cc4c4c3e04a7e5bf3a9ebba20d10fa882a49e5))
* new option payloadSampleIdx ([eaaa99d](https://github.com/Redocly/redoc/commit/eaaa99d68e2392273e8d9c0173db3b546e035d5f))
* **cli:** Fallback on the spec's title before falling back on… ([#1073](https://github.com/Redocly/redoc/issues/1073)) ([e01eea4](https://github.com/Redocly/redoc/commit/e01eea445c93d74b66533c860d76bb3aff4d6df2))
# [2.0.0-rc.18](https://github.com/Redocly/redoc/compare/v2.0.0-rc.17...v2.0.0-rc.18) (2019-10-16)
### Bug Fixes
* add oneOf buttons vertical space when wrapped to new line ([cd9fd61](https://github.com/Redocly/redoc/commit/cd9fd61))
* improve mime-type dropdown font ([ce885f8](https://github.com/Redocly/redoc/commit/ce885f8))
# [2.0.0-rc.17](https://github.com/Redocly/redoc/compare/v2.0.0-rc.16...v2.0.0-rc.17) (2019-10-16)
### Bug Fixes
* active menu item scroll into view ([0a01e9a](https://github.com/Redocly/redoc/commit/0a01e9a))
* changed several components style font-family to monospace ([#1063](https://github.com/Redocly/redoc/issues/1063)) ([0c20e64](https://github.com/Redocly/redoc/commit/0c20e64)), closes [#909](https://github.com/Redocly/redoc/issues/909)
* no quotes for default values in header fields. ([#1059](https://github.com/Redocly/redoc/issues/1059)) ([b5af71d](https://github.com/Redocly/redoc/commit/b5af71d))
* types over-pluralization ([#1057](https://github.com/Redocly/redoc/issues/1057)) ([4494f80](https://github.com/Redocly/redoc/commit/4494f80)), closes [#1053](https://github.com/Redocly/redoc/issues/1053)
### Features
* added support for file paths as --options cli argument ([#1049](https://github.com/Redocly/redoc/issues/1049)) ([4adb927](https://github.com/Redocly/redoc/commit/4adb927))
# [2.0.0-rc.16](https://github.com/Redocly/redoc/compare/v2.0.0-rc.15...v2.0.0-rc.16) (2019-09-30)
### Bug Fixes
* fix scrollYOffset when SSR ([d09c1c1](https://github.com/Redocly/redoc/commit/d09c1c1))
# [2.0.0-rc.15](https://github.com/Redocly/redoc/compare/v2.0.0-rc.14...v2.0.0-rc.15) (2019-09-30)
### Bug Fixes
* auth section appears twice ([5aa7784](https://github.com/Redocly/redoc/commit/5aa7784)), closes [#818](https://github.com/Redocly/redoc/issues/818)
* clicking on group title breaks first tag ([4649683](https://github.com/Redocly/redoc/commit/4649683)), closes [#1034](https://github.com/Redocly/redoc/issues/1034)
* do not crash on empty scopes ([e787d9e](https://github.com/Redocly/redoc/commit/e787d9e)), closes [#1044](https://github.com/Redocly/redoc/issues/1044)
* false-positive recursive detection with allOf at the same level ([faa74d6](https://github.com/Redocly/redoc/commit/faa74d6))
* fix scrollYOffset when SSR ([21258a5](https://github.com/Redocly/redoc/commit/21258a5))
* left menu item before group is not highlighted ([67e2a8f](https://github.com/Redocly/redoc/commit/67e2a8f)), closes [#1033](https://github.com/Redocly/redoc/issues/1033)
* remove excessive whitespace between md sections on small screens ([e318fb3](https://github.com/Redocly/redoc/commit/e318fb3)), closes [#874](https://github.com/Redocly/redoc/issues/874)
* use url-template dependency ([#1008](https://github.com/Redocly/redoc/issues/1008)) ([32a464a](https://github.com/Redocly/redoc/commit/32a464a)), closes [#1007](https://github.com/Redocly/redoc/issues/1007)
### Features
* **cli:** added support for JSON string value for --options CLI argument ([#1047](https://github.com/Redocly/redoc/issues/1047)) ([2a28130](https://github.com/Redocly/redoc/commit/2a28130)), closes [#797](https://github.com/Redocly/redoc/issues/797)
* **cli:** add `disableGoogleFont` parameter to cli ([#1045](https://github.com/Redocly/redoc/issues/1045)) ([aceb343](https://github.com/Redocly/redoc/commit/aceb343))
* new option expandDefaultServerVariables ([#1014](https://github.com/Redocly/redoc/issues/1014)) ([0360dce](https://github.com/Redocly/redoc/commit/0360dce))
# [2.0.0-rc.14](https://github.com/Redocly/redoc/compare/v2.0.0-rc.13...v2.0.0-rc.14) (2019-08-07)
### Bug Fixes
* fix escaping JSON string values ([58cb20d](https://github.com/Redocly/redoc/commit/58cb20d)), closes [#999](https://github.com/Redocly/redoc/issues/999)
* revert expanding default server variables ([7849f7f](https://github.com/Redocly/redoc/commit/7849f7f))
# [2.0.0-rc.13](https://github.com/Redocly/redoc/compare/v2.0.0-rc.12...v2.0.0-rc.13) (2019-08-01)
### Bug Fixes
* enum list doesn't wrap ([bfbb0c1](https://github.com/Redocly/redoc/commit/bfbb0c1)), closes [#993](https://github.com/Redocly/redoc/issues/993)
* incorrect serialization of some parameter samples ([aba45db](https://github.com/Redocly/redoc/commit/aba45db)), closes [#992](https://github.com/Redocly/redoc/issues/992)
* support json serialization for parameter examples ([1367380](https://github.com/Redocly/redoc/commit/1367380)), closes [#934](https://github.com/Redocly/redoc/issues/934)
* unify accordion icons for responses section ([2afc2e4](https://github.com/Redocly/redoc/commit/2afc2e4)), closes [#975](https://github.com/Redocly/redoc/issues/975)
* update to core.js 3 ([9e3375d](https://github.com/Redocly/redoc/commit/9e3375d)), closes [#997](https://github.com/Redocly/redoc/issues/997)
# [2.0.0-rc.12](https://github.com/Redocly/redoc/compare/v2.0.0-rc.11...v2.0.0-rc.12) (2019-07-30)
### Bug Fixes
* rename ObjectDescription to SchemaDefinition as discussed ([4496622](https://github.com/Redocly/redoc/commit/4496622))
# [2.0.0-rc.11](https://github.com/Redocly/redoc/compare/v2.0.0-rc.10...v2.0.0-rc.11) (2019-07-30)
### Bug Fixes
* do not add extra slashes to pattern ([70d1ee9](https://github.com/Redocly/redoc/commit/70d1ee9)), closes [#983](https://github.com/Redocly/redoc/issues/983)
* dropdown fixes related to object description ([0504ad4](https://github.com/Redocly/redoc/commit/0504ad4))
* incorrect serialization of parameter sample with hyphen ([f7dd658](https://github.com/Redocly/redoc/commit/f7dd658))
* redoc-cli: Add missing content type header on compressed responses of `/` path
### Features
* menu items from tags + md extension for Schema Definition ([#681](https://github.com/Redocly/redoc/pull/681))
* new option `menuToggle` - fold active MenuItem if clicked ([#963](https://github.com/Redocly/redoc/issues/963))
* Add option for skipping quotes in enums `enumSkipQuotes` ([#968](https://github.com/Redocly/redoc/issues/968)) ([afc7e36](https://github.com/Redocly/redoc/commit/afc7e36))
* add `sampleCollapseLevel` option ([#937](https://github.com/Redocly/redoc/issues/937)) ([d3f1c16](https://github.com/Redocly/redoc/commit/d3f1c16))
# [2.0.0-rc.10](https://github.com/Redocly/redoc/compare/v2.0.0-rc.9...v2.0.0-rc.10) (2019-07-08)
### Bug Fixes
* broken headings with single quote ([51d3b9b](https://github.com/Redocly/redoc/commit/51d3b9b)), closes [#955](https://github.com/Redocly/redoc/issues/955)
* fix fields table overflow if deeply nested with long title ([12b7057](https://github.com/Redocly/redoc/commit/12b7057))
* hide empty example when it is not defined ([4bd499f](https://github.com/Redocly/redoc/commit/4bd499f))
* markdown in examples descriptions + minor ui tweaks ([f52d9e8](https://github.com/Redocly/redoc/commit/f52d9e8))
* organize response examples in dropdown and display description ([995e557](https://github.com/Redocly/redoc/commit/995e557))
# [2.0.0-rc.9](https://github.com/Redocly/redoc/compare/v2.0.0-rc.8-1...v2.0.0-rc.9) (2019-06-27)
### Bug Fixes
* fix regression double slashes added to full URL display ([f29a4fe](https://github.com/Redocly/redoc/commit/f29a4fe))
* IE11, add missing Object.assign polyfill ([888f04e](https://github.com/Redocly/redoc/commit/888f04e))
* serialize parameter example values according to the spec ([#917](https://github.com/Redocly/redoc/issues/917)) ([3939286](https://github.com/Redocly/redoc/commit/3939286))
* styled-component style error in tabs ([#946](https://github.com/Redocly/redoc/issues/946)) ([c488bbf](https://github.com/Redocly/redoc/commit/c488bbf))
### Features
* add x-additionalPropertiesName ([#622](https://github.com/Redocly/redoc/issues/622)) ([#944](https://github.com/Redocly/redoc/issues/944)) ([0eb1e66](https://github.com/Redocly/redoc/commit/0eb1e66))
# [2.0.0-rc.8-1](https://github.com/Rebilly/ReDoc/compare/v2.0.0-rc.8...v2.0.0-rc.8-1) (2019-05-13)
@ -31,7 +256,7 @@
### Bug Fixes
* broken schema talbes with long enums ([3a74b74](https://github.com/Rebilly/ReDoc/commit/3a74b74))
* broken schema tables with long enums ([3a74b74](https://github.com/Rebilly/ReDoc/commit/3a74b74))
* deep linking sometimes not working when sent over messengers ([2491d97](https://github.com/Rebilly/ReDoc/commit/2491d97))
@ -49,7 +274,7 @@
* IE11 add missing fetch and URL polyfills ([d2ce1bd](https://github.com/Rebilly/ReDoc/commit/d2ce1bd)), closes [#875](https://github.com/Rebilly/ReDoc/issues/875)
* ignore empty x-tagGroups array ([#869](https://github.com/Rebilly/ReDoc/issues/869)) ([4366a0d](https://github.com/Rebilly/ReDoc/commit/4366a0d))
* incorrect detected schema title for deeply inherited schemas ([7d7b4e3](https://github.com/Rebilly/ReDoc/commit/7d7b4e3))
* pluralize arrray of types ([fdcac30](https://github.com/Rebilly/ReDoc/commit/fdcac30))
* pluralize array of types ([fdcac30](https://github.com/Rebilly/ReDoc/commit/fdcac30))
* remove huge space after Authentication section ([548fae3](https://github.com/Rebilly/ReDoc/commit/548fae3)), closes [#872](https://github.com/Rebilly/ReDoc/issues/872)
* remove query string from server URL ([#895](https://github.com/Rebilly/ReDoc/issues/895)) ([64453ff](https://github.com/Rebilly/ReDoc/commit/64453ff))
* remove tabs top margin ([5c187f3](https://github.com/Rebilly/ReDoc/commit/5c187f3))
@ -133,7 +358,7 @@
* improve scrolling performance in Chrome with non-wrapped json examples ([a69c402](https://github.com/Rebilly/ReDoc/commit/a69c402))
* nested oneOf button spacing ([3673720](https://github.com/Rebilly/ReDoc/commit/3673720)), closes [#719](https://github.com/Rebilly/ReDoc/issues/719)
* onLoaded callback not run on spec error ([e77df0c](https://github.com/Rebilly/ReDoc/commit/e77df0c)), closes [#690](https://github.com/Rebilly/ReDoc/issues/690)
* theme improvments by [@stasiukanya](https://github.com/stasiukanya) ([e2d0cd5](https://github.com/Rebilly/ReDoc/commit/e2d0cd5))
* theme improvements by [@stasiukanya](https://github.com/stasiukanya) ([e2d0cd5](https://github.com/Rebilly/ReDoc/commit/e2d0cd5))
* **cli:** old peer dependency issue with styled-components ([#699](https://github.com/Rebilly/ReDoc/issues/699)) ([9e2853c](https://github.com/Rebilly/ReDoc/commit/9e2853c))
@ -205,7 +430,7 @@
### Bug Fixes
* addd indent to array schema internals ([865f3ce](https://github.com/Rebilly/ReDoc/commit/865f3ce))
* add indent to array schema internals ([865f3ce](https://github.com/Rebilly/ReDoc/commit/865f3ce))
* fix oneOf/anyOf titles ([39b930d](https://github.com/Rebilly/ReDoc/commit/39b930d)), closes [#618](https://github.com/Rebilly/ReDoc/issues/618) [#621](https://github.com/Rebilly/ReDoc/issues/621)
@ -251,7 +476,7 @@
### Bug Fixes
* add some spacing between operation description and parameters ([597688e](https://github.com/Rebilly/ReDoc/commit/597688e))
* description is not rendered if doesn't containt markdown headings ([90ed717](https://github.com/Rebilly/ReDoc/commit/90ed717)), closes [#591](https://github.com/Rebilly/ReDoc/issues/591)
* description is not rendered if doesn't contain markdown headings ([90ed717](https://github.com/Rebilly/ReDoc/commit/90ed717)), closes [#591](https://github.com/Rebilly/ReDoc/issues/591)
* download button downloads index.html instead of spec with CLI ([334f904](https://github.com/Rebilly/ReDoc/commit/334f904)), closes [#594](https://github.com/Rebilly/ReDoc/issues/594)
* fix Authentication section is not rendered ([2ecc8bc](https://github.com/Rebilly/ReDoc/commit/2ecc8bc)), closes [#590](https://github.com/Rebilly/ReDoc/issues/590)
* fix linebreaks in multiparagraph field descriptions ([8fb9cd6](https://github.com/Rebilly/ReDoc/commit/8fb9cd6))
@ -562,7 +787,7 @@
### Bug Fixes
* Path parameters are not correctly overriden ([c406dc5](https://github.com/Rebilly/ReDoc/commit/c406dc5)), closes [#400](https://github.com/Rebilly/ReDoc/issues/400)
* Path parameters are not correctly overridden ([c406dc5](https://github.com/Rebilly/ReDoc/commit/c406dc5)), closes [#400](https://github.com/Rebilly/ReDoc/issues/400)
* Use parentNode instead of parentElement to fix IE11 crash ([e8adb60](https://github.com/Rebilly/ReDoc/commit/e8adb60)), closes [#406](https://github.com/Rebilly/ReDoc/issues/406)
@ -724,7 +949,7 @@
* do not ignore path level parameters ([14f8408](https://github.com/Rebilly/Redoc/commit/14f8408))
* improve rendering of types ([17da7b7](https://github.com/Rebilly/Redoc/commit/17da7b7))
* move title propagation to the correct place ([0b0bc99](https://github.com/Rebilly/Redoc/commit/0b0bc99))
* owerwrite text-align to left ([bfee3ed](https://github.com/Rebilly/Redoc/commit/bfee3ed))
* overwrite text-align to left ([bfee3ed](https://github.com/Rebilly/Redoc/commit/bfee3ed))
### Features
@ -786,7 +1011,7 @@ Complete rewrite also means that this rewrite may introduce issues, but they sho
### Deprecations
- Fonts are not loaded by ReDoc so you should load them. Default fonts can be loaded as bellow:
- Fonts are not loaded by ReDoc so you should load them. Default fonts can be loaded as below:
```html
<link href="https://fonts.googleapis.com/css?family=Montserrat:300,400,700|Roboto:300,400,700" rel="stylesheet">
@ -1155,7 +1380,7 @@ closes [#321](https://github.com/Rebilly/ReDoc/issues/321)
### Bug fixes
* Update webpack to the latest beta ([#143](https://github.com/Rebilly/ReDoc/issues/143))
* Fix read-only fields appear in request samples ([#142](https://github.com/Rebilly/ReDoc/issues/142))
* A few more minor UI improvemnts
* A few more minor UI improvements
### Features/Improvements
* Major performance optimization with new option `lazy-rendering`

View File

@ -64,11 +64,11 @@ Additionally, all the 1.x releases are hosted on our GitHub Pages-based CDN **(d
| 1.17.x | 2.0 |
## Some Real-life usages
- [Rebilly](https://rebilly.github.io/RebillyAPI)
- [Rebilly](https://rebilly-api.redoc.ly/)
- [Docker Engine](https://docs.docker.com/engine/api/v1.25/)
- [Zuora](https://www.zuora.com/developer/api-reference/)
- [Shopify Draft Orders](https://help.shopify.com/api/draft-orders)
- [Discourse](http://docs.discourse.org)
- [Commbox](https://www.commbox.io/api/)
- [APIs.guru](https://apis.guru/api-doc/)
- [FastAPI](https://github.com/tiangolo/fastapi)
@ -107,14 +107,14 @@ That's all folks!
**IMPORTANT NOTE:** if you work with untrusted user spec, use `untrusted-spec` [option](#redoc-options-object) to prevent XSS security risks.
### 1. Install ReDoc (skip this step for CDN)
Install using [yarn](https://yarnpkg.com):
Install using [npm](https://docs.npmjs.com/getting-started/what-is-npm):
npm i redoc
or using [yarn](https://yarnpkg.com):
yarn add redoc
or using [npm](https://docs.npmjs.com/getting-started/what-is-npm):
npm install redoc --save
### 2. Reference redoc script in HTML
For **CDN**:
```html
@ -138,7 +138,7 @@ For npm:
Install peer dependencies required by ReDoc if you don't have them installed already:
npm i react react-dom mobx@^4.2.0 styled-components
npm i react react-dom mobx@^4.2.0 styled-components core-js
Import `RedocStandalone` component from 'redoc' module:
@ -213,7 +213,7 @@ ReDoc makes use of the following [vendor extensions](https://swagger.io/specific
* [`x-traitTag`](docs/redoc-vendor-extensions.md#x-traitTag) - useful for handling out common things like Pagination, Rate-Limits, etc
* [`x-code-samples`](docs/redoc-vendor-extensions.md#x-code-samples) - specify operation code samples
* [`x-examples`](docs/redoc-vendor-extensions.md#x-examples) - specify JSON example for requests
* [`x-nullable`](docs/redoc-vendor-extensions.md#nullable) - mark schema param as a nullable
* [`x-nullable`](docs/redoc-vendor-extensions.md#x-nullable) - mark schema param as a nullable
* [`x-displayName`](docs/redoc-vendor-extensions.md#x-displayname) - specify human-friendly names for the menu categories
* [`x-tagGroups`](docs/redoc-vendor-extensions.md#x-tagGroups) - group tags by categories in the side menu
* [`x-servers`](docs/redoc-vendor-extensions.md#x-servers) - ability to specify different servers for API (backported from OpenAPI 3.0)
@ -223,27 +223,33 @@ ReDoc makes use of the following [vendor extensions](https://swagger.io/specific
### `<redoc>` options object
You can use all of the following options with standalone version on <redoc> tag by kebab-casing them, e.g. `scrollYOffset` becomes `scroll-y-offset` and `expandResponses` becomes `expand-responses`.
* `untrustedSpec` - if set, the spec is considered untrusted and all HTML/markdown is sanitized to prevent XSS. **Disabled by default** for performance reasons. **Enable this option if you work with untrusted user data!**
* `disableSearch` - disable search indexing and search box.
* `expandDefaultServerVariables` - enable expanding default server variables, default `false`.
* `expandResponses` - specify which responses to expand by default by response codes. Values should be passed as comma-separated list without spaces e.g. `expandResponses="200,201"`. Special value `"all"` expands all responses by default. Be careful: this option can slow-down documentation rendering time.
* `hideDownloadButton` - do not show "Download" spec button. **THIS DOESN'T MAKE YOUR SPEC PRIVATE**, it just hides the button.
* `hideHostname` - if set, the protocol and hostname is not shown in the operation definition.
* `hideLoading` - do not show loading animation. Useful for small docs.
* `hideSingleRequestSampleTab` - do not show the request sample tab for requests with only one sample.
* `expandSingleSchemaField` - automatically expand single field in a schema
* `jsonSampleExpandLevel` - set the default expand level for JSON payload samples (responses and request body). Special value 'all' expands all levels. The default value is `2`.
* `lazyRendering` - _Not implemented yet_ ~~if set, enables lazy rendering mode in ReDoc. This mode is useful for APIs with big number of operations (e.g. > 50). In this mode ReDoc shows initial screen ASAP and then renders the rest operations asynchronously while showing progress bar on the top. Check out the [demo](\\redocly.github.io/redoc) for the example.~~
* `menuToggle` - if true clicking second time on expanded menu item will collapse it, default `false`.
* `nativeScrollbars` - use native scrollbar for sidemenu instead of perfect-scroll (scrolling performance optimization for big specs).
* `noAutoAuth` - do not inject Authentication section automatically.
* `onlyRequiredInSamples` - shows only required fields in request samples.
* `pathInMiddlePanel` - show path link and HTTP verb in the middle panel instead of the right one.
* `requiredPropsFirst` - show required properties first ordered in the same order as in `required` array.
* `scrollYOffset` - If set, specifies a vertical scroll-offset. This is often useful when there are fixed positioned elements at the top of the page, such as navbars, headers etc;
`scrollYOffset` can be specified in various ways:
* **number**: A fixed number of pixels to be used as offset;
* **selector**: selector of the element to be used for specifying the offset. The distance from the top of the page to the element's bottom will be used as offset;
* **function**: A getter function. Must return a number representing the offset (in pixels);
* **number**: A fixed number of pixels to be used as offset.
* **selector**: selector of the element to be used for specifying the offset. The distance from the top of the page to the element's bottom will be used as offset.
* **function**: A getter function. Must return a number representing the offset (in pixels).
* `showExtensions` - show vendor extensions ("x-" fields). Extensions used by ReDoc are ignored. Can be boolean or an array of `string` with names of extensions to display.
* `sortPropsAlphabetically` - sort properties alphabetically.
* `suppressWarnings` - if set, warnings are not rendered at the top of documentation (they still are logged to the console).
* `lazyRendering` - _Not implemented yet_ ~~if set, enables lazy rendering mode in ReDoc. This mode is useful for APIs with big number of operations (e.g. > 50). In this mode ReDoc shows initial screen ASAP and then renders the rest operations asynchronously while showing progress bar on the top. Check out the [demo](\\redocly.github.io/redoc) for the example.~~
* `hideHostname` - if set, the protocol and hostname is not shown in the operation definition.
* `expandResponses` - specify which responses to expand by default by response codes. Values should be passed as comma-separated list without spaces e.g. `expandResponses="200,201"`. Special value `"all"` expands all responses by default. Be careful: this option can slow-down documentation rendering time.
* `requiredPropsFirst` - show required properties first ordered in the same order as in `required` array.
* `sortPropsAlphabetically` - sort properties alphabetically
* `showExtensions` - show vendor extensions ("x-" fields). Extensions used by ReDoc are ignored. Can be boolean or an array of `string` with names of extensions to display
* `noAutoAuth` - do not inject Authentication section automatically
* `pathInMiddlePanel` - show path link and HTTP verb in the middle panel instead of the right one
* `hideLoading` - do not show loading animation. Useful for small docs
* `nativeScrollbars` - use native scrollbar for sidemenu instead of perfect-scroll (scrolling performance optimization for big specs)
* `hideDownloadButton` - do not show "Download" spec button. **THIS DOESN'T MAKE YOUR SPEC PRIVATE**, it just hides the button.
* `disableSearch` - disable search indexing and search box
* `onlyRequiredInSamples` - shows only required fields in request samples.
* `theme` - ReDoc theme. Not documented yet. For details check source code: [theme.ts](https://github.com/Redocly/redoc/blob/master/src/theme.ts)
* `payloadSampleIdx` - if set, payload sample will be inserted at this index or last. Indexes start from 0.
* `theme` - ReDoc theme. Not documented yet. For details check source code: [theme.ts](https://github.com/Redocly/redoc/blob/master/src/theme.ts).
* `untrustedSpec` - if set, the spec is considered untrusted and all HTML/markdown is sanitized to prevent XSS. **Disabled by default** for performance reasons. **Enable this option if you work with untrusted user data!**
## Advanced usage of standalone version
Instead of adding `spec-url` attribute to the `<redoc>` element you can initialize ReDoc via globally exposed `Redoc` object:

View File

@ -17,7 +17,7 @@ const localDistDir = './benchmark/revisions/local/bundles';
sh.rm('-rf', localDistDir);
console.log(`Building local dist: ${localDistDir}`);
sh.mkdir('-p', localDistDir);
exec(`yarn bundle:lib --output-path ${localDistDir}`);
exec(`npm run bundle:lib --output-path ${localDistDir}`);
const revisions = [];
for (const arg of args) {
@ -32,7 +32,7 @@ const configDir = './benchmark/revisions/config.js';
console.log(`Writing config "${configDir}"`);
fs.writeFileSync(configDir, configFile);
console.log('Starging benchmark server');
console.log('Starting benchmark server');
const proc = spawn('npm', ['run', 'start:benchmark']);
proc.stdout.on('data', data => {
@ -119,7 +119,7 @@ function buildRevisionDist(revision) {
const pwd = sh.pwd();
sh.cd(buildDir);
exec('yarn remove cypress puppeteer && yarn && yarn bundle:lib');
exec('npm uninstall cypress puppeteer && npm install && npm run bundle:lib');
sh.cd(pwd);
return distDir;
}

View File

@ -7,7 +7,7 @@
# To run:
# To display the command line options:
# $ docker run --rm -it redoc-cli --help
# .. will display the comand line help
# .. will display the command line help
#
# To turn `swagger.yml` file in the current directory, to html documentation 'redoc-static.html'
# $ docker run --rm -it -v $PWD:/data redoc-cli bundle swagger.yml

View File

@ -14,7 +14,14 @@ import * as zlib from 'zlib';
import { createStore, loadAndBundleSpec, Redoc } from 'redoc';
import { watch } from 'chokidar';
import { createReadStream, existsSync, readFileSync, ReadStream, writeFileSync } from 'fs';
import {
createReadStream,
existsSync,
lstatSync,
readFileSync,
ReadStream,
writeFileSync,
} from 'fs';
import * as mkdirp from 'mkdirp';
import * as YargsParser from 'yargs';
@ -25,6 +32,7 @@ interface Options {
cdn?: boolean;
output?: string;
title?: string;
disableGoogleFont?: boolean;
port?: number;
templateFileName?: string;
templateOptions?: any;
@ -68,9 +76,11 @@ YargsParser.command(
watch: argv.watch as boolean,
templateFileName: argv.template as string,
templateOptions: argv.templateOptions || {},
redocOptions: argv.options || {},
redocOptions: getObjectOrJSON(argv.options),
};
console.log(config);
try {
await serve(argv.port as number, argv.spec as string, config);
} catch (e) {
@ -96,7 +106,12 @@ YargsParser.command(
yargs.options('title', {
describe: 'Page Title',
type: 'string',
default: 'ReDoc documentation',
});
yargs.options('disableGoogleFont', {
describe: 'Disable Google Font',
type: 'boolean',
default: false,
});
yargs.option('cdn', {
@ -108,15 +123,16 @@ YargsParser.command(
yargs.demandOption('spec');
return yargs;
},
async argv => {
const config: Options = {
async (argv: any) => {
const config = {
ssr: true,
output: argv.o as string,
cdn: argv.cdn as boolean,
title: argv.title as string,
disableGoogleFont: argv.disableGoogleFont as boolean,
templateFileName: argv.template as string,
templateOptions: argv.templateOptions || {},
redocOptions: argv.options || {},
redocOptions: getObjectOrJSON(argv.options),
};
try {
@ -156,7 +172,9 @@ async function serve(port: number, pathToSpec: string, options: Options = {}) {
},
);
} else if (request.url === '/') {
respondWithGzip(pageHTML, request, response);
respondWithGzip(pageHTML, request, response, {
'Content-Type': 'text/html',
});
} else if (request.url === '/spec.json') {
const specStr = JSON.stringify(spec, null, 2);
respondWithGzip(specStr, request, response, {
@ -178,14 +196,14 @@ async function serve(port: number, pathToSpec: string, options: Options = {}) {
if (options.watch && existsSync(pathToSpec)) {
const pathToSpecDirectory = resolve(dirname(pathToSpec));
const watchOptions = {
ignored: /(^|[\/\\])\../,
ignored: [/(^|[\/\\])\../, /___jb_[a-z]+___$/],
ignoreInitial: true,
};
const watcher = watch(pathToSpecDirectory, watchOptions);
const log = console.log.bind(console);
watcher
.on('change', async path => {
log(`${path} changed, updating docs`);
const handlePath = async _path => {
try {
spec = await loadAndBundleSpec(pathToSpec);
pageHTML = await getPageHTML(spec, pathToSpec, options);
@ -193,6 +211,19 @@ async function serve(port: number, pathToSpec: string, options: Options = {}) {
} catch (e) {
console.error('Error while updating: ', e.message);
}
};
watcher
.on('change', async path => {
log(`${path} changed, updating docs`);
handlePath(path);
})
.on('add', async path => {
log(`File ${path} added, updating docs`);
handlePath(path);
})
.on('addDir', path => {
log(`↗ Directory ${path} added. Files in here will trigger reload.`);
})
.on('error', error => console.error(`Watcher error: ${error}`))
.on('ready', () => log(`👀 Watching ${pathToSpecDirectory} for changes...`));
@ -216,7 +247,15 @@ async function bundle(pathToSpec, options: Options = {}) {
async function getPageHTML(
spec: any,
pathToSpec: string,
{ ssr, cdn, title, templateFileName, templateOptions, redocOptions = {} }: Options,
{
ssr,
cdn,
title,
disableGoogleFont,
templateFileName,
templateOptions,
redocOptions = {},
}: Options,
) {
let html;
let css;
@ -258,7 +297,8 @@ async function getPageHTML(
? '<script src="https://unpkg.com/redoc@next/bundles/redoc.standalone.js"></script>'
: `<script>${redocStandaloneSrc}</script>`) + css
: '<script src="redoc.standalone.js"></script>',
title,
title: title || spec.info.title || 'ReDoc documentation',
disableGoogleFont,
templateOptions,
});
}
@ -321,3 +361,25 @@ function handleError(error: Error) {
console.error(error.stack);
process.exit(1);
}
function getObjectOrJSON(options) {
switch (typeof options) {
case 'object':
return options;
case 'string':
try {
if (existsSync(options) && lstatSync(options).isFile()) {
return JSON.parse(readFileSync(options, 'utf-8'));
} else {
return JSON.parse(options);
}
} catch (e) {
console.log(
`Encountered error:\n\n${options}\n\nis neither a file with a valid JSON object neither a stringified JSON object.`,
);
handleError(e);
}
default:
return {};
}
}

2246
cli/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

View File

@ -1,36 +1,35 @@
{
"name": "redoc-cli",
"version": "0.8.4",
"version": "0.9.7",
"description": "ReDoc's Command Line Interface",
"main": "index.js",
"bin": "index.js",
"repository": "https://github.com/Redocly/redoc",
"author": "Roman Hotsiy <gotsijroman@gmail.com>",
"license": "MIT",
"engines": {
"node": ">= 8"
},
"dependencies": {
"chokidar": "^2.0.4",
"handlebars": "^4.0.11",
"isarray": "^2.0.4",
"chokidar": "^3.0.2",
"handlebars": "^4.1.2",
"isarray": "^2.0.5",
"mkdirp": "^0.5.1",
"mobx": "^4.2.0",
"node-libs-browser": "^2.2.0",
"react": "^16.8.4",
"react-dom": "^16.8.4",
"redoc": "^2.0.0-rc.8-1",
"styled-components": "^4.1.3",
"tslib": "^1.9.3",
"yargs": "^12.0.5"
},
"scripts": {
"ci-publish": "ci-publish"
"node-libs-browser": "^2.2.1",
"react": "^16.8.6",
"react-dom": "^16.8.6",
"redoc": "2.0.0-rc.24",
"styled-components": "^4.3.2",
"tslib": "^1.10.0",
"yargs": "^13.3.0"
},
"publishConfig": {
"access": "public"
},
"devDependencies": {
"@types/chokidar": "^1.7.5",
"@types/chokidar": "^2.1.3",
"@types/handlebars": "^4.0.39",
"@types/mkdirp": "^0.5.2",
"ci-publish": "^1.3.1"
"@types/mkdirp": "^0.5.2"
}
}

View File

@ -13,7 +13,7 @@
}
</style>
{{{redocHead}}}
<link href="https://fonts.googleapis.com/css?family=Montserrat:300,400,700|Roboto:300,400,700" rel="stylesheet">
{{#unless disableGoogleFont}}<link href="https://fonts.googleapis.com/css?family=Montserrat:300,400,700|Roboto:300,400,700" rel="stylesheet">{{/unless}}
</head>
<body>

File diff suppressed because it is too large Load Diff

View File

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

View File

@ -1,6 +1,6 @@
#!/bin/bash
# DockerHub cd into Dockerfile location before buil
# DockerHub cd into Dockerfile location before build
# So we have to undo this.
cd ../..
docker build -f config/docker/Dockerfile -t $IMAGE_NAME .

View File

@ -53,7 +53,7 @@
},
{
"name": "Contacts",
"description": "Contacts are Customer's address book.\nAll contact information used in Invoices, Subscriptions, Transacions, etc is enlisted here. Hovewer, changing a Contact won't change corresponding contact information in related resources\n"
"description": "Contacts are Customer's address book.\nAll contact information used in Invoices, Subscriptions, Transacions, etc is enlisted here. However, changing a Contact won't change corresponding contact information in related resources\n"
},
{
"name": "Coupons",
@ -93,7 +93,7 @@
},
{
"name": "Files",
"description": "A File is an entity that can store a phyiscal file and some metadata. It also provides an easy access to\nits size, mime-type, user-defined tags and description thus allowing easy sorting and searching among stored\nfiles.\nThere are several methods of file uploading available: multipart/form-data encoded form, RAW POST (by sending\nfile contents as POST body), fetching from URL (by providing the file URL via 'url' param)\nAttachment is an entity that is used to link a File to one or multiple objects like Customer, Dispute, Payment,\nTransaction, Subscription, Plan, Product, Invoice, Note. That allows to quickly find and use files related to\nthose specific entities.\n"
"description": "A File is an entity that can store a physical file and some metadata. It also provides an easy access to\nits size, mime-type, user-defined tags and description thus allowing easy sorting and searching among stored\nfiles.\nThere are several methods of file uploading available: multipart/form-data encoded form, RAW POST (by sending\nfile contents as POST body), fetching from URL (by providing the file URL via 'url' param)\nAttachment is an entity that is used to link a File to one or multiple objects like Customer, Dispute, Payment,\nTransaction, Subscription, Plan, Product, Invoice, Note. That allows to quickly find and use files related to\nthose specific entities.\n"
},
{
"name": "Gateway Accounts",
@ -15623,7 +15623,7 @@
"description": "Reset user password\n",
"responses": {
"201": {
"description": "Password was reseted successfully",
"description": "Password was reset successfully",
"headers": {
"Rate-Limit-Limit": {
"description": "The number of allowed requests in the current period",
@ -23851,17 +23851,17 @@
"type": "string"
},
"totpRequired": {
"description": "The user setting of two-factor authentification",
"description": "The user setting of two-factor authentication",
"readOnly": true,
"type": "boolean"
},
"totpSecret": {
"description": "The user TOTP key for authentification app (if TOTP enabled)",
"description": "The user TOTP key for authentication app (if TOTP enabled)",
"readOnly": true,
"type": "string"
},
"totpUrl": {
"description": "The user link to QR-code for TOTP authentification app (if TOTP enabled)",
"description": "The user link to QR-code for TOTP authentication app (if TOTP enabled)",
"readOnly": true,
"type": "string",
"format": "url"
@ -24250,7 +24250,7 @@
},
"bodyHtml": {
"type": "string",
"description": "Leave empty to recieve \"text/plain\" email.\nThe template palceholders are allowed.\n"
"description": "Leave empty to receive \"text/plain\" email.\nThe template palceholders are allowed.\n"
}
},
"required": [
@ -26495,15 +26495,15 @@
}
},
"totpRequired": {
"description": "The user setting of two-factor authentification",
"description": "The user setting of two-factor authentication",
"type": "boolean"
},
"totpSecret": {
"description": "The user TOTP key for authentification app (if TOTP enabled)",
"description": "The user TOTP key for authentication app (if TOTP enabled)",
"type": "string"
},
"totpUrl": {
"description": "The user link to QR-code for TOTP authentification app (if TOTP enabled)",
"description": "The user link to QR-code for TOTP authentication app (if TOTP enabled)",
"type": "string",
"format": "url"
},
@ -26984,7 +26984,7 @@
"collectionExpand": {
"name": "expand",
"in": "query",
"description": "Expand response to get full related object intead of ID. See the expand guide for more info.",
"description": "Expand response to get full related object instead of ID. See the expand guide for more info.",
"schema": {
"type": "string"
}

View File

@ -11,7 +11,7 @@ const demos = [
value: 'https://api.apis.guru/v2/specs/googleapis.com/calendar/v3/swagger.yaml',
label: 'Google Calendar',
},
{ value: 'https://api.apis.guru/v2/specs/slack.com/1.0.6/swagger.yaml', label: 'Slack' },
{ value: 'https://api.apis.guru/v2/specs/slack.com/1.2.0/swagger.yaml', label: 'Slack' },
{ value: 'https://api.apis.guru/v2/specs/zoom.us/2.0.0/swagger.yaml', label: 'Zoom.us' },
{
value: 'https://api.apis.guru/v2/specs/graphhopper.com/1.0/swagger.yaml',

View File

@ -38,7 +38,7 @@ info:
OAuth2 - an open protocol to allow secure authorization in a simple
and standard method from web, mobile and desktop applications.
<security-definitions />
<SecurityDefinitions />
version: 1.0.0
title: Swagger Petstore
@ -63,6 +63,14 @@ tags:
description: Access to Petstore orders
- name: user
description: Operations about user
- name: pet_model
x-displayName: The Pet Model
description: |
<SchemaDefinition schemaRef="#/components/schemas/Pet" />
- name: store_model
x-displayName: The Order Model
description: |
<SchemaDefinition schemaRef="#/components/schemas/Order" exampleRef="#/components/examples/Order" showReadOnly={true} showWriteOnly={true} />
x-tagGroups:
- name: General
tags:
@ -71,9 +79,21 @@ x-tagGroups:
- name: User Management
tags:
- user
- name: Models
tags:
- pet_model
- store_model
paths:
/pet:
parameters:
- name: Accept-Language
in: header
description: "The language you prefer for messages. Supported values are en-AU, en-CA, en-GB, en-US"
example: en-US
required: false
schema:
type: string
default: en-AU
- name: cookieParam
in: cookie
description: Some cookie
@ -286,7 +306,7 @@ paths:
tags:
- pet
summary: Finds Pets by status
description: Multiple status values can be provided with comma seperated strings
description: Multiple status values can be provided with comma separated strings
operationId: findPetsByStatus
parameters:
- name: status
@ -331,7 +351,7 @@ paths:
- pet
summary: Finds Pets by tags
description: >-
Muliple tags can be provided with comma seperated strings. Use tag1,
Multiple tags can be provided with comma separated strings. Use tag1,
tag2, tag3 for testing.
operationId: findPetsByTags
deprecated: true
@ -611,7 +631,7 @@ paths:
type: integer
format: int32
X-Expires-After:
description: date in UTC when toekn expires
description: date in UTC when token expires
schema:
type: string
format: date-time
@ -666,6 +686,7 @@ components:
type: string
description: The measured skill for hunting
default: lazy
example: adventurous
enum:
- clueless
- lazy
@ -717,6 +738,7 @@ components:
type: number
description: Average amount of honey produced per day in ounces
example: 3.14
multipleOf: .01
required:
- honeyPerDay
Id:
@ -754,6 +776,11 @@ components:
description: Indicates whenever order was completed or not
type: boolean
default: false
readOnly: true
requestId:
description: Unique Request Id
type: string
writeOnly: true
xml:
name: Order
Pet:
@ -866,14 +893,13 @@ components:
as well as digits
format: password
minLength: 8
pattern: '(?=.*[a-z])(?=.*[A-Z])(?=.*[0-9])'
pattern: '/(?=.*[a-z])(?=.*[A-Z])(?=.*[0-9])/'
example: drowssaP123
phone:
description: User phone number in international format
type: string
pattern: '^\+(?:[0-9]-?){6,14}[0-9]$'
pattern: '/^\+(?:[0-9]-?){6,14}[0-9]$/'
example: +1-202-555-0192
nullable: true
userStatus:
description: User status
type: integer
@ -926,3 +952,10 @@ components:
type: apiKey
name: api_key
in: header
examples:
Order:
value:
quantity: 1,
shipDate: 2018-10-19T16:46:45Z,
status: placed,
complete: false

View File

@ -1,5 +1,6 @@
import * as React from 'react';
import { render } from 'react-dom';
// tslint:disable-next-line
import { AppContainer } from 'react-hot-loader';
// import DevTools from 'mobx-react-devtools';

View File

@ -631,7 +631,7 @@ paths:
X-Expires-After:
type: string
format: date-time
description: date in UTC when toekn expires
description: date in UTC when token expires
'400':
description: Invalid username/password supplied
/user/logout:

View File

@ -38,7 +38,6 @@ const babelLoader = mode => ({
['@babel/plugin-syntax-decorators', { legacy: true }],
'@babel/plugin-syntax-dynamic-import',
'@babel/plugin-syntax-jsx',
mode !== 'production' ? 'react-hot-loader/babel' : undefined,
[
'babel-plugin-styled-components',
{
@ -50,6 +49,13 @@ const babelLoader = mode => ({
},
});
const babelHotLoader = {
loader: 'babel-loader',
options: {
plugins: ['react-hot-loader/babel'],
},
};
export default (env: { playground?: boolean; bench?: boolean } = {}, { mode }) => ({
entry: [
root('../src/polyfills.ts'),
@ -105,7 +111,11 @@ export default (env: { playground?: boolean; bench?: boolean } = {}, { mode }) =
{ test: [/\.eot$/, /\.gif$/, /\.woff$/, /\.svg$/, /\.ttf$/], use: 'null-loader' },
{
test: /\.tsx?$/,
use: [tsLoader(env), babelLoader(mode)],
use: compact([
mode !== 'production' ? babelHotLoader : undefined,
tsLoader(env),
babelLoader(mode),
]),
exclude: [/node_modules/],
},
{

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 + 4) + (1 + 8));
.should('have.length', 6 + (2 + 8 + 1 + 4 + 2) + (1 + 8));
});
it('should sync active menu items while scroll', () => {

View File

@ -42,7 +42,7 @@ describe('Servers', () => {
// TODO add cy-data attributes
cy.get('[data-section-id="operation/addPet"]').should(
'contain',
'http://localhost:' + win.location.port + '/e2e/pet',
'http://localhost:' + win.location.port + '/pet',
);
});
});
@ -57,7 +57,7 @@ describe('Servers', () => {
// TODO add cy-data attributes
cy.get('[data-section-id="operation/addPet"]').should(
'contain',
'http://localhost:' + win.location.port + '/e2e/pet',
'http://localhost:' + win.location.port + '/pet',
);
});
});

View File

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

17990
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

View File

@ -1,6 +1,6 @@
{
"name": "otx-redoc",
"version": "2.0.0-rc.2",
"name": "redoc",
"version": "2.0.0-rc.24",
"description": "ReDoc",
"repository": {
"type": "git",
@ -28,7 +28,7 @@
"start": "webpack-dev-server --mode=development --env.playground --hot --config demo/webpack.config.ts",
"start:prod": "webpack-dev-server --env.playground --mode=production --config demo/webpack.config.ts",
"start:benchmark": "webpack-dev-server --mode=production --env.bench --config demo/webpack.config.ts",
"test": "npm run lint && npm run unit && npm run bundlesize && npm run license-check",
"test": "npm run lint && npm run unit && npm run license-check",
"unit": "jest --coverage",
"e2e": "cypress run",
"e2e-ci": "cypress run --record",
@ -40,9 +40,9 @@
"bundle": "npm run bundle:clean && npm run bundle:lib && npm run bundle:standalone",
"declarations": "tsc --emitDeclarationOnly -p tsconfig.lib.json && cp -R src/types typings/",
"stats": "webpack --env.standalone --json --profile --mode=production > stats.json",
"prettier": "prettier --write \"src/**/*.{ts,tsx}\"",
"prettier": "prettier --write \"cli/index.ts\" \"src/**/*.{ts,tsx}\"",
"changelog": "conventional-changelog -p angular -i CHANGELOG.md -s -r 1",
"lint": "tslint --project tsconfig.json",
"lint": "eslint 'src/**/*.{js,ts,tsx}'",
"benchmark": "node ./benchmark/benchmark.js",
"start:demo": "webpack-dev-server --hot --config demo/webpack.config.ts --mode=development",
"compile:cli": "tsc custom.d.ts cli/index.ts --target es6 --module commonjs --types yargs",
@ -52,80 +52,83 @@
"docker:build": "docker build -f config/docker/Dockerfile -t redoc ."
},
"devDependencies": {
"@babel/core": "7.3.4",
"@babel/plugin-syntax-decorators": "7.2.0",
"@babel/plugin-syntax-dynamic-import": "^7.2.0",
"@babel/plugin-syntax-jsx": "7.2.0",
"@babel/plugin-syntax-typescript": "7.3.3",
"@cypress/webpack-preprocessor": "4.0.3",
"@hot-loader/react-dom": "^16.8.4",
"@types/chai": "4.1.7",
"@types/dompurify": "^0.0.32",
"@types/enzyme": "^3.9.0",
"@babel/core": "7.8.7",
"@babel/plugin-syntax-decorators": "7.8.3",
"@babel/plugin-syntax-dynamic-import": "^7.8.3",
"@babel/plugin-syntax-jsx": "7.8.3",
"@babel/plugin-syntax-typescript": "7.8.3",
"@cypress/webpack-preprocessor": "4.1.3",
"@hot-loader/react-dom": "^16.12.0",
"@types/chai": "4.2.10",
"@types/dompurify": "^2.0.1",
"@types/enzyme": "^3.10.5",
"@types/enzyme-to-json": "^1.5.3",
"@types/jest": "^24.0.11",
"@types/jest": "^25.1.4",
"@types/json-pointer": "^1.0.30",
"@types/lodash": "^4.14.122",
"@types/lodash": "^4.14.149",
"@types/lunr": "^2.3.2",
"@types/mark.js": "^8.11.3",
"@types/marked": "^0.6.3",
"@types/prismjs": "^1.9.1",
"@types/prop-types": "^15.7.0",
"@types/react": "^16.8.7",
"@types/react-dom": "^16.8.2",
"@types/react-hot-loader": "^4.1.0",
"@types/mark.js": "^8.11.5",
"@types/marked": "^0.7.3",
"@types/prismjs": "^1.16.0",
"@types/prop-types": "^15.7.3",
"@types/react": "^16.9.23",
"@types/react-dom": "^16.9.5",
"@types/react-tabs": "^2.3.1",
"@types/styled-components": "^4.1.12",
"@types/tapable": "1.0.4",
"@types/webpack": "^4.4.25",
"@types/webpack-env": "^1.13.9",
"@types/yargs": "^12.0.9",
"babel-loader": "8.0.5",
"babel-plugin-styled-components": "^1.10.0",
"@types/styled-components": "^4.4.1",
"@types/tapable": "1.0.5",
"@types/webpack": "^4.41.7",
"@types/webpack-env": "^1.15.1",
"@types/yargs": "^13.0.3",
"@typescript-eslint/eslint-plugin": "^2.24.0",
"@typescript-eslint/parser": "^2.24.0",
"babel-loader": "8.0.6",
"babel-plugin-styled-components": "^1.10.7",
"beautify-benchmark": "^0.2.4",
"bundlesize": "^0.17.1",
"conventional-changelog-cli": "^2.0.12",
"copy-webpack-plugin": "^5.0.0",
"core-js": "^2.6.5",
"coveralls": "^3.0.3",
"css-loader": "^2.1.1",
"cypress": "~3.1.5",
"deploy-to-gh-pages": "^1.3.6",
"enzyme": "^3.9.0",
"enzyme-adapter-react-16": "^1.10.0",
"enzyme-to-json": "^3.3.5",
"fork-ts-checker-webpack-plugin": "1.0.0",
"bundlesize": "^0.18.0",
"conventional-changelog-cli": "^2.0.31",
"copy-webpack-plugin": "^5.1.1",
"core-js": "^3.5.0",
"coveralls": "^3.0.9",
"css-loader": "^3.4.2",
"cypress": "~3.7.0",
"deploy-to-gh-pages": "^1.3.7",
"enzyme": "^3.11.0",
"enzyme-adapter-react-16": "^1.15.2",
"enzyme-to-json": "^3.4.4",
"eslint": "^6.8.0",
"eslint-plugin-import": "^2.20.1",
"eslint-plugin-react": "^7.19.0",
"fork-ts-checker-webpack-plugin": "3.1.1",
"html-webpack-plugin": "^3.1.0",
"jest": "^24.3.1",
"jest": "^24.9.0",
"license-checker": "^25.0.1",
"lodash": "^4.17.11",
"lodash": "^4.17.15",
"mobx": "^4.3.1",
"prettier": "^1.16.4",
"prettier-eslint": "^8.8.2",
"prettier": "^1.19.1",
"raf": "^3.4.1",
"react": "^16.8.4",
"react-dom": "^16.8.4",
"rimraf": "^2.6.3",
"react": "^16.13.0",
"react-dom": "^16.13.0",
"react-hot-loader": "^4.12.19",
"rimraf": "^3.0.2",
"shelljs": "^0.8.3",
"source-map-loader": "^0.2.4",
"style-loader": "^0.23.1",
"styled-components": "^4.1.3",
"ts-jest": "24.0.0",
"ts-loader": "5.3.3",
"ts-node": "^8.0.3",
"tslint": "^5.13.1",
"tslint-react": "^3.4.0",
"typescript": "^3.3.3333",
"style-loader": "^1.1.3",
"styled-components": "^4.4.1",
"ts-jest": "24.2.0",
"ts-loader": "6.2.1",
"ts-node": "^8.6.2",
"typescript": "^3.8.3",
"unfetch": "^4.1.0",
"url-polyfill": "^1.1.5",
"webpack": "^4.29.6",
"webpack-cli": "^3.2.3",
"webpack-dev-server": "^3.2.1",
"url-polyfill": "^1.1.8",
"webpack": "^4.42.0",
"webpack-cli": "^3.3.11",
"webpack-dev-server": "^3.10.3",
"webpack-node-externals": "^1.6.0",
"workerize-loader": "^1.0.4",
"workerize-loader": "^1.1.0",
"yaml-js": "^0.2.3"
},
"peerDependencies": {
"core-js": "^3.1.4",
"mobx": "^4.2.0 || ^5.0.0",
"react": "^16.8.4",
"react-dom": "^16.8.4",
@ -134,27 +137,27 @@
"dependencies": {
"classnames": "^2.2.6",
"decko": "^1.2.0",
"dompurify": "^1.0.10",
"eventemitter3": "^3.0.0",
"dompurify": "^2.0.8",
"eventemitter3": "^4.0.0",
"json-pointer": "^0.6.0",
"json-schema-ref-parser": "^6.1.0",
"lunr": "2.3.6",
"lunr": "2.3.8",
"mark.js": "^8.11.1",
"marked": "^0.6.1",
"memoize-one": "^5.0.0",
"mobx-react": "^5.4.3",
"openapi-sampler": "1.0.0-beta.14",
"marked": "^0.7.0",
"memoize-one": "~5.1.1",
"mobx-react": "6.1.5",
"openapi-sampler": "1.0.0-beta.15",
"perfect-scrollbar": "^1.4.0",
"polished": "^3.0.3",
"prismjs": "^1.15.0",
"polished": "^3.4.4",
"prismjs": "^1.19.0",
"prop-types": "^15.7.2",
"react-dropdown": "^1.6.4",
"react-hot-loader": "^4.8.0",
"react-tabs": "^3.0.0",
"slugify": "^1.3.4",
"react-dropdown": "^1.7.0",
"react-tabs": "^3.1.0",
"slugify": "^1.4.0",
"stickyfill": "^1.1.1",
"swagger2openapi": "^5.2.3",
"tslib": "^1.9.3"
"swagger2openapi": "^5.3.4",
"tslib": "^1.11.1",
"url-template": "^2.0.8"
},
"bundlesize": [
{

View File

@ -5,11 +5,7 @@ import { ClipboardService } from '../services/ClipboardService';
export interface CopyButtonWrapperProps {
data: any;
children: (
props: {
renderCopyButton: (() => React.ReactNode);
},
) => React.ReactNode;
children: (props: { renderCopyButton: () => React.ReactNode }) => React.ReactNode;
}
export class CopyButtonWrapper extends React.PureComponent<

View File

@ -55,7 +55,7 @@ export const StyledDropdown = styled(Dropdown)`
display: block;
height: 0;
position: absolute;
right: 0.35em;
right: 0.3em;
top: 50%;
margin-top: -0.125em;
width: 0;
@ -97,7 +97,7 @@ export const StyledDropdown = styled(Dropdown)`
export const SimpleDropdown = styled(StyledDropdown)`
margin-left: 10px;
text-transform: none;
font-size: 0.929em;
font-size: 0.969em;
.Dropdown-control {
font-size: 1em;

View File

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

View File

@ -32,6 +32,7 @@ export const TypeName = styled(FieldLabel)`
export const TypeTitle = styled(FieldLabel)`
color: ${props => props.theme.schema.typeTitleColor};
word-break: break-word;
`;
export const TypeFormat = TypeName;
@ -60,14 +61,6 @@ export const PatternLabel = styled(FieldLabel)`
&::after {
font-weight: bold;
}
&::before {
content: ' /';
}
&::after {
content: '/ ';
}
`;
export const ExampleValue = styled(FieldLabel)`
@ -76,11 +69,9 @@ export const ExampleValue = styled(FieldLabel)`
background-color: ${transparentize(0.95, theme.colors.text.primary)};
color: ${transparentize(0.1, theme.colors.text.primary)};
margin: ${theme.spacing.unit}px;
padding: 0 ${theme.spacing.unit}px;
border: 1px solid ${transparentize(0.9, theme.colors.text.primary)};
font-family: ${theme.typography.code.fontFamily};
color: ${theme.typography.code.color};
}`};
& + & {
margin-left: 0;
@ -99,6 +90,7 @@ export const ConstraintItem = styled(FieldLabel)`
margin: 0 ${theme.spacing.unit}px;
padding: 0 ${theme.spacing.unit}px;
border: 1px solid ${transparentize(0.9, theme.colors.primary.main)};
font-family: ${theme.typography.code.fontFamily};
}`};
& + & {
margin-left: 0;

View File

@ -1,14 +1,16 @@
import { SECTION_ATTR } from '../services/MenuStore';
import styled, { media } from '../styled-components';
export const MiddlePanel = styled.div`
export const MiddlePanel = styled.div<{ compact?: boolean }>`
width: calc(100% - ${props => props.theme.rightPanel.width});
padding: 0 ${props => props.theme.spacing.sectionHorizontal}px;
${media.lessThan('medium', true)`
${({ compact, theme }) =>
media.lessThan('medium', true)`
width: 100%;
padding: ${props =>
`${props.theme.spacing.sectionVertical}px ${props.theme.spacing.sectionHorizontal}px`};
padding: ${`${compact ? 0 : theme.spacing.sectionVertical}px ${
theme.spacing.sectionHorizontal
}px`};
`};
`;

View File

@ -11,13 +11,14 @@ export const OneOfLabel = styled.span`
font-size: 0.9em;
margin-right: 10px;
color: ${props => props.theme.colors.primary.main};
font-family: Montserrat;
font-family: ${props => props.theme.typography.headings.fontFamily};
}
`;
export const OneOfButton = styled.li<{ active: boolean }>`
display: inline-block;
margin-right: 10px;
margin-bottom: 5px;
font-size: 0.8em;
cursor: pointer;
border: 1px solid ${props => props.theme.colors.primary.main};

View File

@ -1,16 +1,19 @@
import { observer } from 'mobx-react';
import * as React from 'react';
import { Constants } from '../../../src/services/Constants';
import { DarkRightPanel, StyledLink } from '../../../src/common-elements';
import { ContentPanel } from '../RightPanelContent/ContentPanel';
import { MiddlePanel, Row, Section } from '../../common-elements/';
import { AppStore } from '../../services/AppStore';
import { MiddlePanel, Row, Section } from '../../common-elements/';
import { ExternalDocumentation } from '../ExternalDocumentation/ExternalDocumentation';
import { Markdown } from '../Markdown/Markdown';
import { StyledMarkdownBlock } from '../Markdown/styled.elements';
import { ApiHeader, DownloadButton, InfoSpan, InfoSpanBox, InfoSpanBoxWrap, } from './styled.elements';
import {
ApiHeader,
DownloadButton,
InfoSpan,
InfoSpanBox,
InfoSpanBoxWrap,
} from './styled.elements';
export interface ApiInfoProps {
store: AppStore;
@ -25,8 +28,8 @@ export class ApiInfo extends React.Component<ApiInfoProps> {
};
render() {
const {store} = this.props;
const {info, externalDocs} = store.spec;
const { store } = this.props;
const { info, externalDocs } = store.spec;
const hideDownloadButton = store.options.hideDownloadButton;
const downloadFilename = info.downloadFileName;
@ -35,26 +38,24 @@ export class ApiInfo extends React.Component<ApiInfoProps> {
const license =
(info.license && (
<InfoSpan>
License: <StyledLink href={info.license.url}>{info.license.name}</StyledLink>
License: <a href={info.license.url}>{info.license.name}</a>
</InfoSpan>
)) ||
null;
const website =
(info.contact &&
info.contact.url && (
(info.contact && info.contact.url && (
<InfoSpan>
URL: <StyledLink href={info.contact.url}>{info.contact.url}</StyledLink>
URL: <a href={info.contact.url}>{info.contact.url}</a>
</InfoSpan>
)) ||
null;
const email =
(info.contact &&
info.contact.email && (
(info.contact && info.contact.email && (
<InfoSpan>
{info.contact.name || 'E-mail'}:{' '}
<StyledLink href={'mailto:' + info.contact.email}>{info.contact.email}</StyledLink>
<a href={'mailto:' + info.contact.email}>{info.contact.email}</a>
</InfoSpan>
)) ||
null;
@ -62,16 +63,12 @@ export class ApiInfo extends React.Component<ApiInfoProps> {
const terms =
(info.termsOfService && (
<InfoSpan>
<StyledLink href={info.termsOfService}>Terms of Service</StyledLink>
<a href={info.termsOfService}>Terms of Service</a>
</InfoSpan>
)) ||
null;
const version =
(info.version && (
<span>({info.version})</span>
)) ||
null;
const version = (info.version && <span>({info.version})</span>) || null;
return (
<Section>
@ -103,11 +100,9 @@ export class ApiInfo extends React.Component<ApiInfoProps> {
)) ||
null}
</StyledMarkdownBlock>
<Markdown source={store.spec.info.description}/>
{externalDocs && <ExternalDocumentation externalDocs={externalDocs}/>}
<Markdown source={store.spec.info.description} data-role="redoc-description" />
{externalDocs && <ExternalDocumentation externalDocs={externalDocs} />}
</MiddlePanel>
{store.options.showOtherInfoPanel &&
<DarkRightPanel><ContentPanel content={info[Constants.OTX_EXTENSION_KEY]}/></DarkRightPanel>}
</Row>
</Section>
);

View File

@ -60,7 +60,7 @@ export class ContentItem extends React.Component<ContentItemProps> {
}
}
const middlePanelWrap = component => <MiddlePanel>{component}</MiddlePanel>;
const middlePanelWrap = component => <MiddlePanel compact={true}>{component}</MiddlePanel>;
@observer
export class SectionItem extends React.Component<ContentItemProps> {
@ -71,7 +71,7 @@ export class SectionItem extends React.Component<ContentItemProps> {
return (
<>
<Row>
<MiddlePanel>
<MiddlePanel compact={false}>
<Header>
<ShareLink to={this.props.item.id} />
{name}

View File

@ -5,7 +5,7 @@ import { Markdown } from '../Markdown/Markdown';
import { OptionsContext } from '../OptionsProvider';
import { SelectOnClick } from '../SelectOnClick/SelectOnClick';
import { getBasePath } from '../../utils';
import { expandDefaultServerVariables, getBasePath } from '../../utils';
import {
EndpointInfo,
HttpVerb,
@ -60,21 +60,26 @@ export class Endpoint extends React.Component<EndpointProps, EndpointState> {
/>
</EndpointInfo>
<ServersOverlay expanded={expanded}>
{operation.servers.map(server => (
<ServerItem key={server.url}>
{operation.servers.map(server => {
const normalizedUrl = options.expandDefaultServerVariables
? expandDefaultServerVariables(server.url, server.variables)
: server.url;
return (
<ServerItem key={normalizedUrl}>
<Markdown source={server.description || ''} compact={true} />
<SelectOnClick>
<ServerUrl>
<span>
{hideHostname || options.hideHostname
? getBasePath(server.url)
: server.url}
? getBasePath(normalizedUrl)
: normalizedUrl}
</span>
{operation.path}
</ServerUrl>
</SelectOnClick>
</ServerItem>
))}
);
})}
</ServersOverlay>
</OperationEndpointWrap>
)}

View File

@ -2,6 +2,7 @@ import * as React from 'react';
import { ExampleValue, FieldLabel } from '../../common-elements/fields';
import { l } from '../../services/Labels';
import { OptionsContext } from '../OptionsProvider';
export interface EnumValuesProps {
values: string[];
@ -9,8 +10,10 @@ export interface EnumValuesProps {
}
export class EnumValues extends React.PureComponent<EnumValuesProps> {
static contextType = OptionsContext;
render() {
const { values, type } = this.props;
const { enumSkipQuotes } = this.context;
if (!values.length) {
return null;
}
@ -20,12 +23,15 @@ export class EnumValues extends React.PureComponent<EnumValuesProps> {
<FieldLabel>
{type === 'array' ? l('enumArray') : ''}{' '}
{values.length === 1 ? l('enumSingleValue') : l('enum')}:
</FieldLabel>
{values.map((value, idx) => (
</FieldLabel>{' '}
{values.map((value, idx) => {
const exampleValue = enumSkipQuotes ? value : JSON.stringify(value);
return (
<React.Fragment key={idx}>
<ExampleValue>{JSON.stringify(value)}</ExampleValue>{' '}
<ExampleValue>{exampleValue}</ExampleValue>{' '}
</React.Fragment>
))}
);
})}
</div>
);
}

View File

@ -23,6 +23,7 @@ export interface FieldProps extends SchemaOptions {
showExamples?: boolean;
field: FieldModel;
expandByDefault?: boolean;
renderDiscriminatorSwitch?: (opts: FieldProps) => JSX.Element;
}
@ -30,13 +31,19 @@ export interface FieldProps extends SchemaOptions {
@observer
export class Field extends React.Component<FieldProps> {
toggle = () => {
if (this.props.field.expanded === undefined && this.props.expandByDefault) {
this.props.field.expanded = false;
} else {
this.props.field.toggle();
}
};
render() {
const { className, field, isLast } = this.props;
const { name, expanded, deprecated, required, kind } = field;
const { className, field, isLast, expandByDefault } = this.props;
const { name, deprecated, required, kind } = field;
const withSubSchema = !field.schema.isPrimitive && !field.schema.isCircular;
const expanded = field.expanded === undefined ? expandByDefault : field.expanded;
const paramName = withSubSchema ? (
<ClickablePropertyNameCell
onClick={this.toggle}
@ -65,8 +72,7 @@ export class Field extends React.Component<FieldProps> {
<FieldDetails {...this.props} />
</PropertyDetailsCell>
</tr>
{field.expanded &&
withSubSchema && (
{expanded && withSubSchema && (
<tr key={field.name + 'inner'}>
<PropertyCellWithInner colSpan={2}>
<InnerPropertiesWrap>

View File

@ -4,6 +4,7 @@ import { ExampleValue, FieldLabel } from '../../common-elements/fields';
export interface FieldDetailProps {
value?: any;
label: string;
raw?: boolean;
}
export class FieldDetail extends React.PureComponent<FieldDetailProps> {
@ -11,12 +12,12 @@ export class FieldDetail extends React.PureComponent<FieldDetailProps> {
if (this.props.value === undefined) {
return null;
}
const value = this.props.raw ? this.props.value : JSON.stringify(this.props.value);
return (
<div>
<FieldLabel> {this.props.label} </FieldLabel>{' '}
<ExampleValue>
{JSON.stringify(this.props.value)}
</ExampleValue>
<FieldLabel> {this.props.label} </FieldLabel> <ExampleValue>{value}</ExampleValue>
</div>
);
}

View File

@ -9,6 +9,7 @@ import {
TypePrefix,
TypeTitle,
} from '../../common-elements/fields';
import { serializeParameterValue } from '../../utils/openapi';
import { ExternalDocumentation } from '../ExternalDocumentation/ExternalDocumentation';
import { Markdown } from '../Markdown/Markdown';
import { EnumValues } from './EnumValues';
@ -20,13 +21,31 @@ import { FieldDetail } from './FieldDetail';
import { Badge } from '../../common-elements/';
import { l } from '../../services/Labels';
import { OptionsContext } from '../OptionsProvider';
export class FieldDetails extends React.PureComponent<FieldProps> {
static contextType = OptionsContext;
render() {
const { showExamples, field, renderDiscriminatorSwitch } = this.props;
const { enumSkipQuotes, hideSchemaTitles } = this.context;
const { schema, description, example, deprecated } = field;
const rawDefault = !!enumSkipQuotes || field.in === 'header'; // having quotes around header field default values is confusing and inappropriate
let exampleField: JSX.Element | null = null;
if (showExamples && example !== undefined) {
const label = l('example') + ':';
if (field.in && (field.style || field.serializationMime)) {
// decode for better readability in examples: see https://github.com/Redocly/redoc/issues/1138
const serializedValue = decodeURIComponent(serializeParameterValue(field, example));
exampleField = <FieldDetail label={label} value={serializedValue} raw={true} />;
} else {
exampleField = <FieldDetail label={label} value={example} />;
}
}
return (
<div>
<div>
@ -40,10 +59,10 @@ export class FieldDetails extends React.PureComponent<FieldProps> {
&gt;{' '}
</TypeFormat>
)}
{schema.title && <TypeTitle> ({schema.title}) </TypeTitle>}
{schema.title && !hideSchemaTitles && <TypeTitle> ({schema.title}) </TypeTitle>}
<ConstraintsView constraints={schema.constraints} />
{schema.nullable && <NullableLabel> {l('nullable')} </NullableLabel>}
{schema.pattern && <PatternLabel>{schema.pattern}</PatternLabel>}
{schema.pattern && <PatternLabel> {schema.pattern} </PatternLabel>}
{schema.isCircular && <RecursiveLabel> {l('recursive')} </RecursiveLabel>}
</div>
{deprecated && (
@ -51,9 +70,9 @@ export class FieldDetails extends React.PureComponent<FieldProps> {
<Badge type="warning"> {l('deprecated')} </Badge>
</div>
)}
<FieldDetail label={l('default') + ':'} value={schema.default} />
<FieldDetail raw={rawDefault} label={l('default') + ':'} value={schema.default} />
{!renderDiscriminatorSwitch && <EnumValues type={schema.type} values={schema.enum} />}{' '}
{showExamples && <FieldDetail label={l('example') + ':'} value={example} />}
{exampleField}
{<Extensions extensions={{ ...field.extensions, ...schema.extensions }} />}
<div>
<Markdown compact={true} source={description} />

View File

@ -5,6 +5,7 @@ import { SampleControls } from '../../common-elements';
import { CopyButtonWrapper } from '../../common-elements/CopyButtonWrapper';
import { PrismDiv } from '../../common-elements/PrismDiv';
import { jsonToHTML } from '../../utils/jsonToHtml';
import { OptionsContext } from '../OptionsProvider';
import { jsonStyles } from './style';
export interface JsonProps {
@ -32,12 +33,18 @@ class Json extends React.PureComponent<JsonProps> {
<span onClick={this.expandAll}> Expand all </span>
<span onClick={this.collapseAll}> Collapse all </span>
</SampleControls>
<OptionsContext.Consumer>
{options => (
<PrismDiv
className={this.props.className}
// tslint:disable-next-line
ref={node => (this.node = node!)}
dangerouslySetInnerHTML={{ __html: jsonToHTML(this.props.data) }}
dangerouslySetInnerHTML={{
__html: jsonToHTML(this.props.data, options.jsonSampleExpandLevel),
}}
/>
)}
</OptionsContext.Consumer>
</JsonViewerWrap>
);

View File

@ -25,7 +25,7 @@ export class AdvancedMarkdown extends React.Component<AdvancedMarkdownProps> {
renderWithOptionsAndStore(options: RedocNormalizedOptions, store?: AppStore) {
const { source, htmlWrap = i => i } = this.props;
if (!store) {
throw new Error('When using componentes in markdown, store prop must be provided');
throw new Error('When using components in markdown, store prop must be provided');
}
const renderer = new MarkdownRenderer(options);

View File

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

View File

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

View File

@ -19,11 +19,13 @@ export const linksCss = css`
}
`;
export const StyledMarkdownBlock = styled(PrismDiv as StyledComponent<
export const StyledMarkdownBlock = styled(
PrismDiv as StyledComponent<
'div',
ResolvedThemeInterface,
{ compact?: boolean; inline?: boolean }
>)`
>,
)`
font-family: ${props => props.theme.typography.fontFamily};
font-weight: ${props => props.theme.typography.fontWeightRegular};

View File

@ -3,6 +3,7 @@ import * as React from 'react';
import { DropdownProps } from '../../common-elements/dropdown';
import { MediaContentModel, MediaTypeModel, SchemaModel } from '../../services/models';
import { DropdownLabel, DropdownWrapper } from '../PayloadSamples/styled.elements';
export interface MediaTypeChildProps {
schema: SchemaModel;
@ -11,6 +12,8 @@ export interface MediaTypeChildProps {
export interface MediaTypesSwitchProps {
content?: MediaContentModel;
withLabel?: boolean;
renderDropdown: (props: DropdownProps) => JSX.Element;
children: (activeMime: MediaTypeModel) => JSX.Element;
}
@ -37,13 +40,25 @@ export class MediaTypesSwitch extends React.Component<MediaTypesSwitchProps> {
};
});
const Wrapper = ({ children }) =>
this.props.withLabel ? (
<DropdownWrapper>
<DropdownLabel>Content type</DropdownLabel>
{children}
</DropdownWrapper>
) : (
children
);
return (
<>
<Wrapper>
{this.props.renderDropdown({
value: options[activeMimeIdx],
options,
onChange: this.switchMedia,
})}
</Wrapper>
{this.props.children(content.active)}
</>
);

View File

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

View File

@ -1,17 +1,33 @@
import * as React from 'react';
import { SmallTabs, Tab, TabList, TabPanel } from '../../common-elements';
import { MediaTypeModel } from '../../services/models';
import styled from '../../styled-components';
import { DropdownProps } from '../../common-elements';
import { MediaTypeModel } from '../../services/models';
import { Markdown } from '../Markdown/Markdown';
import { Example } from './Example';
import { NoSampleLabel } from './styled.elements';
import { DropdownLabel, DropdownWrapper, NoSampleLabel } from './styled.elements';
export interface PayloadSamplesProps {
mediaType: MediaTypeModel;
renderDropdown: (props: DropdownProps) => JSX.Element;
}
export class MediaTypeSamples extends React.Component<PayloadSamplesProps> {
interface MediaTypeSamplesState {
activeIdx: number;
}
export class MediaTypeSamples extends React.Component<PayloadSamplesProps, MediaTypeSamplesState> {
state = {
activeIdx: 0,
};
switchMedia = ({ value }) => {
this.setState({
activeIdx: parseInt(value, 10),
});
};
render() {
const { activeIdx } = this.state;
const examples = this.props.mediaType.examples || {};
const mimeType = this.props.mediaType.name;
@ -21,28 +37,46 @@ export class MediaTypeSamples extends React.Component<PayloadSamplesProps> {
if (examplesNames.length === 0) {
return noSample;
}
if (examplesNames.length > 1) {
const options = examplesNames.map((name, idx) => {
return {
label: examples[name].summary || name,
value: idx.toString(),
};
});
const example = examples[examplesNames[activeIdx]];
const description = example.description;
return (
<SmallTabs defaultIndex={0}>
<TabList>
{examplesNames.map(name => (
<Tab key={name}> {examples[name].summary || name} </Tab>
))}
</TabList>
{examplesNames.map(name => (
<TabPanel key={name}>
<Example example={examples[name]} mimeType={mimeType} />
</TabPanel>
))}
</SmallTabs>
<SamplesWrapper>
<DropdownWrapper>
<DropdownLabel>Example</DropdownLabel>
{this.props.renderDropdown({
value: options[activeIdx],
options,
onChange: this.switchMedia,
})}
</DropdownWrapper>
<div>
{description && <Markdown source={description} />}
<Example example={example} mimeType={mimeType} />
</div>
</SamplesWrapper>
);
} else {
const name = examplesNames[0];
const example = examples[examplesNames[0]];
return (
<div>
<Example example={examples[name]} mimeType={mimeType} />
</div>
<SamplesWrapper>
{example.description && <Markdown source={example.description} />}
<Example example={example} mimeType={mimeType} />
</SamplesWrapper>
);
}
}
}
const SamplesWrapper = styled.div`
margin-top: 15px;
`;

View File

@ -1,11 +1,14 @@
// @ts-ignore
// eslint-disable-next-line @typescript-eslint/no-unused-vars
import ReactDropdown from 'react-dropdown';
import { observer } from 'mobx-react';
import * as React from 'react';
import { MediaTypeSamples } from './MediaTypeSamples';
import { MediaTypesSwitch } from '../MediaTypeSwitch/MediaTypesSwitch';
import { MediaContentModel } from '../../services/models';
import { DropdownOrLabel } from '../DropdownOrLabel/DropdownOrLabel';
import { MediaTypesSwitch } from '../MediaTypeSwitch/MediaTypesSwitch';
import { InvertedSimpleDropdown, MimeLabel } from './styled.elements';
export interface PayloadSamplesProps {
@ -21,8 +24,14 @@ export class PayloadSamples extends React.Component<PayloadSamplesProps> {
}
return (
<MediaTypesSwitch content={mimeContent} renderDropdown={this.renderDropdown}>
{mediaType => <MediaTypeSamples key="samples" mediaType={mediaType} />}
<MediaTypesSwitch content={mimeContent} renderDropdown={this.renderDropdown} withLabel={true}>
{mediaType => (
<MediaTypeSamples
key="samples"
mediaType={mediaType}
renderDropdown={this.renderDropdown}
/>
)}
</MediaTypesSwitch>
);
}

View File

@ -13,8 +13,7 @@ export function useExternalExample(example: ExampleModel, mimeType: string) {
prevRef.current = example;
useEffect(
() => {
useEffect(() => {
const load = async () => {
setIsLoading(true);
try {
@ -26,9 +25,7 @@ export function useExternalExample(example: ExampleModel, mimeType: string) {
};
load();
},
[example, mimeType],
);
}, [example, mimeType]);
return value.current;
}

View File

@ -1,29 +1,49 @@
// @ts-ignore
import Dropdown from 'react-dropdown';
// eslint-disable-next-line @typescript-eslint/no-unused-vars
import ReactDropdown from 'react-dropdown';
import { transparentize } from 'polished';
import styled from '../../styled-components';
import { StyledDropdown } from '../../common-elements';
export const MimeLabel = styled.div`
border-bottom: 1px solid rgba(255, 255, 255, 0.9);
padding: 12px;
background-color: ${({ theme }) => transparentize(0.6, theme.rightPanel.backgroundColor)};
margin: 0 0 10px 0;
display: block;
color: rgba(255, 255, 255, 0.8);
`;
export const DropdownLabel = styled.span`
font-family: ${({ theme }) => theme.typography.headings.fontFamily};
font-size: 12px;
position: absolute;
z-index: 1;
top: -11px;
left: 12px;
font-weight: ${({ theme }) => theme.typography.fontWeightBold};
color: ${({ theme }) => transparentize(0.6, theme.rightPanel.textColor)};
`;
export const DropdownWrapper = styled.div`
position: relative;
`;
export const InvertedSimpleDropdown = styled(StyledDropdown)`
margin-left: 10px;
text-transform: none;
font-size: 0.929em;
border-bottom: 1px solid ${({ theme }) => theme.rightPanel.textColor};
margin: 0 0 10px 0;
display: block;
background-color: ${({ theme }) => transparentize(0.6, theme.rightPanel.backgroundColor)};
.Dropdown-control {
margin-top: 0;
}
.Dropdown-control,
.Dropdown-control:hover {
font-size: 1em;
border: none;
padding: 0 1.2em 0 0;
padding: 0.9em 1.6em 0.9em 0.9em;
background: transparent;
color: ${({ theme }) => theme.rightPanel.textColor};
box-shadow: none;
@ -34,6 +54,7 @@ export const InvertedSimpleDropdown = styled(StyledDropdown)`
}
.Dropdown-menu {
margin: 0;
margin-top: 2px;
}
`;

View File

@ -1,6 +1,6 @@
import { observer } from 'mobx-react';
import * as React from 'react';
import { OperationModel, RedocNormalizedOptions } from '../../services';
import { isPayloadSample, OperationModel, RedocNormalizedOptions } from '../../services';
import { PayloadSamples } from '../PayloadSamples/PayloadSamples';
import { SourceCodeWithCopy } from '../SourceCode/SourceCode';
@ -19,15 +19,10 @@ export class RequestSamples extends React.Component<RequestSamplesProps> {
render() {
const { operation } = this.props;
const requestBodyContent = operation.requestBody && operation.requestBody.content;
const hasBodySample = requestBodyContent && requestBodyContent.hasSample;
const samples = operation.codeSamples;
const hasSamples = hasBodySample || samples.length > 0;
const hideTabList =
samples.length + (hasBodySample ? 1 : 0) === 1
? this.context.hideSingleRequestSampleTab
: false;
const hasSamples = samples.length > 0;
const hideTabList = samples.length === 1 ? this.context.hideSingleRequestSampleTab : false;
return (
(hasSamples && (
<div>
@ -35,23 +30,21 @@ export class RequestSamples extends React.Component<RequestSamplesProps> {
<Tabs defaultIndex={0}>
<TabList hidden={hideTabList}>
{hasBodySample && <Tab key="payload"> Payload </Tab>}
{samples.map(sample => (
<Tab key={sample.lang + '_' + (sample.label || '')}>
{sample.label !== undefined ? sample.label : sample.lang}
</Tab>
))}
</TabList>
{hasBodySample && (
<TabPanel key="payload">
<div>
<PayloadSamples content={requestBodyContent!} />
</div>
</TabPanel>
)}
{samples.map(sample => (
<TabPanel key={sample.lang}>
<TabPanel key={sample.lang + '_' + (sample.label || '')}>
{isPayloadSample(sample) ? (
<div>
<PayloadSamples content={sample.requestBodyContent} />
</div>
) : (
<SourceCodeWithCopy lang={sample.lang} source={sample.source} />
)}
</TabPanel>
))}
</Tabs>

View File

@ -28,8 +28,7 @@ export class ResponseView extends React.Component<{ response: ResponseModel }> {
code={code}
opened={expanded}
/>
{expanded &&
!empty && (
{expanded && !empty && (
<ResponseDetailsWrap>
<ResponseDetails response={this.props.response} />
</ResponseDetailsWrap>

View File

@ -22,7 +22,7 @@ export class ResponseTitle extends React.PureComponent<ResponseTitleProps> {
<ShelfIcon
size={'1.5em'}
color={type}
direction={opened ? 'up' : 'down'}
direction={opened ? 'down' : 'right'}
float={'left'}
/>
)}

View File

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

View File

@ -44,9 +44,7 @@ export class Schema extends React.Component<Partial<SchemaProps>> {
if (discriminatorProp !== undefined) {
if (!oneOf || !oneOf.length) {
throw new Error(
`Looks like you are using discriminator wrong: you don't have any definition inherited from the ${
schema.title
}`,
`Looks like you are using discriminator wrong: you don't have any definition inherited from the ${schema.title}`,
);
}
return (
@ -66,9 +64,9 @@ export class Schema extends React.Component<Partial<SchemaProps>> {
switch (type) {
case 'object':
return <ObjectSchema {...this.props as any} />;
return <ObjectSchema {...(this.props as any)} />;
case 'array':
return <ArraySchema {...this.props as any} />;
return <ArraySchema {...(this.props as any)} />;
}
// TODO: maybe adjust FieldDetails to accept schema

View File

@ -0,0 +1,93 @@
import * as React from 'react';
import { DarkRightPanel, MiddlePanel, MimeLabel, Row, Section } from '../../common-elements';
import { MediaTypeModel, OpenAPIParser, RedocNormalizedOptions } from '../../services';
import styled from '../../styled-components';
import { OpenAPIMediaType } from '../../types';
import { DropdownOrLabel } from '../DropdownOrLabel/DropdownOrLabel';
import { MediaTypeSamples } from '../PayloadSamples/MediaTypeSamples';
import { InvertedSimpleDropdown } from '../PayloadSamples/styled.elements';
import { Schema } from '../Schema';
export interface ObjectDescriptionProps {
schemaRef: string;
exampleRef?: string;
showReadOnly?: boolean;
showWriteOnly?: boolean;
parser: OpenAPIParser;
options: RedocNormalizedOptions;
}
export class SchemaDefinition extends React.PureComponent<ObjectDescriptionProps> {
private static getMediaType(schemaRef: string, exampleRef?: string): OpenAPIMediaType {
if (!schemaRef) {
return {};
}
const info: OpenAPIMediaType = {
schema: { $ref: schemaRef },
};
if (exampleRef) {
info.examples = { example: { $ref: exampleRef } };
}
return info;
}
private _mediaModel: MediaTypeModel;
private get mediaModel() {
const { parser, schemaRef, exampleRef, options } = this.props;
if (!this._mediaModel) {
this._mediaModel = new MediaTypeModel(
parser,
'json',
false,
SchemaDefinition.getMediaType(schemaRef, exampleRef),
options,
);
}
return this._mediaModel;
}
render() {
const { showReadOnly = true, showWriteOnly = false } = this.props;
return (
<Section>
<Row>
<MiddlePanel>
<Schema
skipWriteOnly={!showWriteOnly}
skipReadOnly={!showReadOnly}
schema={this.mediaModel.schema}
/>
</MiddlePanel>
<DarkRightPanel>
<MediaSamplesWrap>
<MediaTypeSamples renderDropdown={this.renderDropdown} mediaType={this.mediaModel} />
</MediaSamplesWrap>
</DarkRightPanel>
</Row>
</Section>
);
}
private renderDropdown = props => {
return <DropdownOrLabel Label={MimeLabel} Dropdown={InvertedSimpleDropdown} {...props} />;
};
}
const MediaSamplesWrap = styled.div`
background: ${({ theme }) => theme.codeSample.backgroundColor};
& > div,
& > pre {
padding: ${props => props.theme.spacing.unit * 4}px;
margin: 0;
}
& > div > pre {
padding: 0;
}
`;

View File

@ -7,6 +7,7 @@ import { MenuItem } from '../SideMenu/MenuItem';
import { MarkerService } from '../../services/MarkerService';
import { SearchResult } from '../../services/SearchWorker.worker';
import { bind, debounce } from 'decko';
import { PerfectScrollbarWrap } from '../../common-elements/perfect-scrollbar';
import {
ClearIcon,
@ -94,11 +95,18 @@ export class SearchBox extends React.PureComponent<SearchBoxProps, SearchBoxStat
setResults(results: SearchResult[], term: string) {
this.setState({
results,
term,
});
this.props.marker.mark(term);
}
@bind
@debounce(400)
searchCallback(searchTerm: string) {
this.props.search.search(searchTerm).then(res => {
this.setResults(res, searchTerm);
});
}
search = (event: React.ChangeEvent<HTMLInputElement>) => {
const q = event.target.value;
if (q.length < 3) {
@ -106,13 +114,12 @@ export class SearchBox extends React.PureComponent<SearchBoxProps, SearchBoxStat
return;
}
this.setState({
this.setState(
{
term: q,
});
this.props.search.search(event.target.value).then(res => {
this.setResults(res, q);
});
},
() => this.searchCallback(this.state.term),
);
};
render() {

View File

@ -1,5 +1,6 @@
import { darken } from 'polished';
import * as React from 'react';
import { darken, getLuminance, lighten } from 'polished';
import styled from '../../styled-components';
import { MenuItemLabel } from '../SideMenu/styled.elements';
@ -16,9 +17,15 @@ export const SearchInput = styled.input.attrs(() => ({
padding: 5px ${props => props.theme.spacing.unit * 2}px 5px
${props => props.theme.spacing.unit * 4}px;
border: 0;
border-bottom: 1px solid ${({theme}) => darken(0.1, theme.menu.backgroundColor)};
font-family: ${({theme}) => theme.typography.fontFamily};
font-size: 1em;
border-bottom: 1px solid
${({ theme }) =>
(getLuminance(theme.menu.backgroundColor) > 0.5 ? darken : lighten)(
0.1,
theme.menu.backgroundColor,
)};
font-family: ${({ theme }) => theme.typography.fontFamily};
font-weight: bold;
font-size: 13px;
color: ${props => props.theme.menu.textColor};
background-color: transparent;
outline: none;
@ -26,23 +33,14 @@ export const SearchInput = styled.input.attrs(() => ({
export const SearchIcon = styled((props: { className?: string }) => (
<svg
version="1.1"
id="Layer_1"
xmlns="http://www.w3.org/2000/svg"
x="0px"
y="0px"
className={props.className}
viewBox="0 0 24 24"
xmlSpace="preserve"
version="1.1"
viewBox="0 0 1000 1000"
x="0px"
xmlns="http://www.w3.org/2000/svg"
y="0px"
>
<g>
<path
className="st0"
d="M22.7,21.5l-5.1-5c1.5-1.7,2.4-4,2.4-6.5c0-5.5-4.5-10-10-10S0,4.5,0,10s4.5,10,10,10c2.3,0,4.4-0.8,6.1-2.1
l5.2,5.1c0.2,0.2,0.4,0.3,0.7,0.3c0.3,0,0.5-0.1,0.7-0.3C23.1,22.5,23.1,21.9,22.7,21.5z M10,18c-4.4,0-8-3.6-8-8s3.6-8,8-8
s8,3.6,8,8S14.4,18,10,18z"
/>
</g>
<path d="M968.2,849.4L667.3,549c83.9-136.5,66.7-317.4-51.7-435.6C477.1-25,252.5-25,113.9,113.4c-138.5,138.3-138.5,362.6,0,501C219.2,730.1,413.2,743,547.6,666.5l301.9,301.4c43.6,43.6,76.9,14.9,104.2-12.4C981,928.3,1011.8,893,968.2,849.4z M524.5,522c-88.9,88.7-233,88.7-321.8,0c-88.9-88.7-88.9-232.6,0-321.3c88.9-88.7,233-88.7,321.8,0C613.4,289.4,613.4,433.3,524.5,522z" />
</svg>
)).attrs({
className: 'search-icon',
@ -59,11 +57,12 @@ export const SearchIcon = styled((props: { className?: string }) => (
export const SearchResultsBox = styled.div`
padding: ${props => props.theme.spacing.unit}px 0;
background-color: #ededed;
background-color: ${({ theme }) => darken(0.05, theme.menu.backgroundColor)}};
color: ${props => props.theme.menu.textColor};
min-height: 150px;
max-height: 250px;
border-top: 1px solid #e1e1e1;
border-bottom: 1px solid #e1e1e1;
border-top: ${({ theme }) => darken(0.1, theme.menu.backgroundColor)}};
border-bottom: ${({ theme }) => darken(0.1, theme.menu.backgroundColor)}};
margin-top: 10px;
line-height: 1.4;
font-size: 0.9em;
@ -72,17 +71,14 @@ export const SearchResultsBox = styled.div`
padding-top: 6px;
padding-bottom: 6px;
&:hover {
background-color: #e1e1e1;
&:hover,
&.active {
background-color: ${({ theme }) => darken(0.1, theme.menu.backgroundColor)};
}
> svg {
display: none;
}
&.active {
background-color: #e1e1e1;
}
}
`;

View File

@ -49,7 +49,7 @@ export class OAuthFlow extends React.PureComponent<OAuthFlowProps> {
<strong> Scopes: </strong>
</div>
<ul>
{Object.keys(flow!.scopes).map(scope => (
{Object.keys(flow!.scopes || {}).map(scope => (
<li key={scope}>
<code>{scope}</code> - <Markdown inline={true} source={flow!.scopes[scope] || ''} />
</li>
@ -80,7 +80,7 @@ export class SecurityDefs extends React.PureComponent<SecurityDefsProps> {
<table className="security-details">
<tbody>
<tr>
<th> Security scheme type: </th>
<th> Security Scheme Type </th>
<td> {AUTH_TYPES[scheme.type] || scheme.type} </td>
</tr>
{scheme.apiKey ? (
@ -97,7 +97,7 @@ export class SecurityDefs extends React.PureComponent<SecurityDefsProps> {
scheme.http.scheme === 'bearer' && scheme.http.bearerFormat && (
<tr key="bearer">
<th> Bearer format </th>
<td> "{scheme.http.bearerFormat}" </td>
<td> &quot;{scheme.http.bearerFormat}&quot; </td>
</tr>
),
]
@ -105,7 +105,11 @@ export class SecurityDefs extends React.PureComponent<SecurityDefsProps> {
<tr>
<th> Connect URL </th>
<td>
<a target="_blank" href={scheme.openId.connectUrl}>
<a
target="_blank"
rel="noopener noreferrer"
href={scheme.openId.connectUrl}
>
{scheme.openId.connectUrl}
</a>
</td>

View File

@ -1,3 +1,4 @@
// import { observe } from 'mobx';
import { observer } from 'mobx-react';
import * as React from 'react';
@ -15,7 +16,7 @@ export interface MenuItemProps {
@observer
export class MenuItem extends React.Component<MenuItemProps> {
ref: Element | null;
ref = React.createRef<HTMLLabelElement>();
activate = (evt: React.MouseEvent<HTMLElement>) => {
this.props.onActivate!(this.props.item);
@ -31,42 +32,30 @@ export class MenuItem extends React.Component<MenuItemProps> {
}
scrollIntoViewIfActive() {
if (this.props.item.active && this.ref) {
this.ref.scrollIntoViewIfNeeded();
if (this.props.item.active && this.ref.current) {
this.ref.current.scrollIntoViewIfNeeded();
}
}
saveRef = ref => {
this.ref = ref;
};
render() {
const { item, withoutChildren } = this.props;
return (
<MenuItemLi
onClick={this.activate}
depth={item.depth}
ref={this.saveRef}
data-item-id={item.id}
>
<MenuItemLi onClick={this.activate} depth={item.depth} data-item-id={item.id}>
{item.type === 'operation' ? (
<OperationMenuItemContent {...this.props} item={item as OperationModel} />
) : (
<MenuItemLabel depth={item.depth} active={item.active} type={item.type}>
<MenuItemLabel depth={item.depth} active={item.active} type={item.type} ref={this.ref}>
<MenuItemTitle title={item.name}>
{item.name}
{this.props.children}
</MenuItemTitle>
{(item.depth > 0 &&
item.items.length > 0 && (
{(item.depth > 0 && item.items.length > 0 && (
<ShelfIcon float={'right'} direction={item.expanded ? 'down' : 'right'} />
)) ||
null}
</MenuItemLabel>
)}
{!withoutChildren &&
item.items &&
item.items.length > 0 && (
{!withoutChildren && item.items && item.items.length > 0 && (
<MenuItems
expanded={item.expanded}
items={item.items}
@ -83,11 +72,24 @@ export interface OperationMenuItemContentProps {
}
@observer
class OperationMenuItemContent extends React.Component<OperationMenuItemContentProps> {
export class OperationMenuItemContent extends React.Component<OperationMenuItemContentProps> {
ref = React.createRef<HTMLLabelElement>();
componentDidUpdate() {
if (this.props.item.active && this.ref.current) {
this.ref.current.scrollIntoViewIfNeeded();
}
}
render() {
const { item } = this.props;
return (
<MenuItemLabel depth={item.depth} active={item.active} deprecated={item.deprecated}>
<MenuItemLabel
depth={item.depth}
active={item.active}
deprecated={item.deprecated}
ref={this.ref}
>
<OperationBadge type={item.httpVerb}>{shortenHTTPVerb(item.httpVerb)}</OperationBadge>
<MenuItemTitle width="calc(100% - 38px)">
{item.name}

View File

@ -1,15 +1,16 @@
import { observer } from 'mobx-react';
import * as React from 'react';
import { StyledLink } from '../../../src/common-elements';
import { PerfectScrollbarWrap } from '../../common-elements/perfect-scrollbar';
import { IMenuItem, MenuStore } from '../../services/MenuStore';
import { OptionsContext } from '../OptionsProvider';
import { MenuItems } from './MenuItems';
import { PerfectScrollbarWrap } from '../../common-elements/perfect-scrollbar';
import { RedocAttribution } from './styled.elements';
@observer
export class SideMenu extends React.Component<{ menu: MenuStore; className?: string }> {
static contextType = OptionsContext;
private _updateScroll?: () => void;
render() {
@ -22,17 +23,21 @@ export class SideMenu extends React.Component<{ menu: MenuStore; className?: str
wheelPropagation: false,
}}
>
<MenuItems items={store.items} onActivate={this.activate} root={true}/>
<MenuItems items={store.items} onActivate={this.activate} root={true} />
<RedocAttribution>
<StyledLink href="https://www.opentext.com/" target={'_blank'}>
<a target="_blank" rel="noopener noreferrer" href="https://www.opentext.com/">
© Copyright 2019 OpenText Corp
</StyledLink>
</a>
</RedocAttribution>
</PerfectScrollbarWrap>
);
}
activate = (item: IMenuItem) => {
if (item && item.active && this.context.menuToggle) {
return item.expanded ? item.collapse() : item.expand();
}
this.props.menu.activateAndScroll(item, true);
setTimeout(() => {
if (this._updateScroll) {

View File

@ -104,7 +104,7 @@ export const menuItemDepth = {
font-size: 0.929em;
text-transform: ${({ theme }) => theme.menu.level1Items.textTransform};
&:hover {
color: ${props => props.theme.colors.primary.main};
color: ${props => props.theme.menu.activeTextColor};
}
`,
2: css`
@ -126,7 +126,7 @@ export const MenuItemLabel = styled.label.attrs((props: MenuItemLabelType) => ({
}),
}))<MenuItemLabelType>`
cursor: pointer;
color: ${props => (props.active ? props.theme.colors.primary.main : props.theme.menu.textColor)};
color: ${props => (props.active ? props.theme.menu.activeTextColor : props.theme.menu.textColor)};
margin: 0;
padding: 12.5px ${props => props.theme.spacing.unit * 4}px;
${({ depth, type, theme }) =>

View File

@ -19,6 +19,10 @@ export interface StickySidebarProps {
menu: MenuStore;
}
export interface StickySidebarState {
offsetTop?: string;
}
const stickyfill = Stickyfill && Stickyfill();
const StyledStickySidebar = styled.div<{ open?: boolean }>`
@ -29,7 +33,7 @@ const StyledStickySidebar = styled.div<{ open?: boolean }>`
flex-direction: column;
backface-visibility: hidden;
contain: strict;
/* contain: strict; TODO: breaks layout since Chrome 80*/
height: 100vh;
position: sticky;
@ -40,7 +44,7 @@ const StyledStickySidebar = styled.div<{ open?: boolean }>`
position: fixed;
z-index: 20;
width: 100%;
background: #ffffff;
background: ${({ theme }) => theme.menu.backgroundColor};
display: ${props => (props.open ? 'flex' : 'none')};
`};
@ -77,13 +81,26 @@ const FloatingButton = styled.div`
`;
@observer
export class StickyResponsiveSidebar extends React.Component<StickySidebarProps> {
export class StickyResponsiveSidebar extends React.Component<
StickySidebarProps,
StickySidebarState
> {
static contextType = OptionsContext;
context!: React.ContextType<typeof OptionsContext>;
state: StickySidebarState = { offsetTop: '0px' };
stickyElement: Element;
componentDidMount() {
if (stickyfill) {
stickyfill.add(this.stickyElement);
}
// rerender when hydrating from SSR
// see https://github.com/facebook/react/issues/8017#issuecomment-256351955
this.setState({
offsetTop: this.getScrollYOffset(this.context),
});
}
componentWillUnmount() {
@ -92,7 +109,7 @@ export class StickyResponsiveSidebar extends React.Component<StickySidebarProps>
}
}
getScrollYOffset(options) {
getScrollYOffset(options: RedocNormalizedOptions) {
let top;
if (this.props.scrollYOffset !== undefined) {
top = RedocNormalizedOptions.normalizeScrollYOffset(this.props.scrollYOffset)();
@ -105,22 +122,17 @@ export class StickyResponsiveSidebar extends React.Component<StickySidebarProps>
render() {
const open = this.props.menu.sideBarOpened;
const style = options => {
const top = this.getScrollYOffset(options);
return {
top,
height: `calc(100vh - ${top})`,
};
};
const top = this.state.offsetTop;
return (
<OptionsContext.Consumer>
{options => (
<>
<StyledStickySidebar
open={open}
className={this.props.className}
style={style(options)}
style={{
top,
height: `calc(100vh - ${top})`,
}}
// tslint:disable-next-line
ref={el => {
this.stickyElement = el as any;
@ -132,16 +144,10 @@ export class StickyResponsiveSidebar extends React.Component<StickySidebarProps>
<AnimatedChevronButton open={open} />
</FloatingButton>
</>
)}
</OptionsContext.Consumer>
);
}
private toggleNavMenu = () => {
this.props.menu.toggleSidebar();
};
// private closeNavMenu = () => {
// this.setState({ open: false });
// };
}

View File

@ -1,4 +1,4 @@
import memoize from 'memoize-one';
import * as memoize from 'memoize-one/dist/memoize-one.cjs'; // fixme: https://github.com/alexreardon/memoize-one/issues/37
import { Component, createContext } from 'react';
import { AppStore } from '../services/';

View File

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

View File

@ -24,14 +24,14 @@ describe('Components', () => {
});
test('should collapse/uncollapse', () => {
expect(component.html()).not.toContain('class="hoverable"'); // all are collapesed by default
expect(component.html()).not.toContain('class="hoverable"'); // all are collapsed by default
const expandAll = component.find('div > span[children=" Expand all "]');
expandAll.simulate('click');
expect(component.html()).toContain('class="hoverable"'); // all are collapesed
expect(component.html()).toContain('class="hoverable"'); // all are collapsed
const collapseAll = component.find('div > span[children=" Collapse all "]');
collapseAll.simulate('click');
expect(component.html()).not.toContain('class="hoverable"'); // all are collapesed
expect(component.html()).not.toContain('class="hoverable"'); // all are collapsed
});
test('should collapse/uncollapse', () => {

View File

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

View File

@ -28,3 +28,5 @@ export * from './OptionsProvider';
export * from './SideMenu/';
export * from './StickySidebar/StickyResponsiveSidebar';
export * from './SearchBox/SearchBox';
export * from './SchemaDefinition/SchemaDefinition';
export * from './SourceCode/SourceCode';

View File

@ -1,11 +1,12 @@
import 'core-js/es6/promise';
import 'core-js/fn/array/find';
import 'core-js/fn/object/assign';
import 'core-js/fn/string/ends-with';
import 'core-js/fn/string/starts-with';
import 'core-js/es/promise';
import 'core-js/es6/map';
import 'core-js/es6/symbol';
import 'core-js/es/array/find';
import 'core-js/es/object/assign';
import 'core-js/es/string/ends-with';
import 'core-js/es/string/starts-with';
import 'core-js/es/map';
import 'core-js/es/symbol';
import 'unfetch/polyfill/index';
import 'url-polyfill';

View File

@ -10,8 +10,15 @@ import { RedocNormalizedOptions, RedocRawOptions } from './RedocNormalizedOption
import { ScrollService } from './ScrollService';
import { SearchStore } from './SearchStore';
import { SchemaDefinition } from '../components/SchemaDefinition/SchemaDefinition';
import { SecurityDefs } from '../components/SecuritySchemes/SecuritySchemes';
import { SECURITY_DEFINITIONS_COMPONENT_NAME } from '../utils/openapi';
import {
SCHEMA_DEFINITION_JSX_NAME,
SECURITY_DEFINITIONS_COMPONENT_NAME,
SECURITY_DEFINITIONS_JSX_NAME,
} from '../utils/openapi';
import { IS_BROWSER } from '../utils';
export interface StoreState {
menu: {
@ -96,6 +103,9 @@ export class AppStore {
dispose() {
this.scroll.dispose();
this.menu.dispose();
if (this.search) {
this.search.dispose();
}
if (this.disposer != null) {
this.disposer();
}
@ -126,16 +136,16 @@ export class AppStore {
const elements: Element[] = [];
for (let i = start; i < end; i++) {
let elem = this.menu.getElementAt(i);
const elem = this.menu.getElementAt(i);
if (!elem) {
continue;
}
if (this.menu.flatItems[i].type === 'section') {
elem = elem.parentElement!.parentElement;
}
if (elem) {
elements.push(elem);
}
if (idx === -1 && IS_BROWSER) {
const $description = document.querySelector('[data-role="redoc-description"]');
if ($description) elements.push($description);
}
this.marker.addOnly(elements);
@ -151,5 +161,18 @@ const DEFAULT_OPTIONS: RedocRawOptions = {
securitySchemes: store.spec.securitySchemes,
}),
},
[SECURITY_DEFINITIONS_JSX_NAME]: {
component: SecurityDefs,
propsSelector: (store: AppStore) => ({
securitySchemes: store.spec.securitySchemes,
}),
},
[SCHEMA_DEFINITION_JSX_NAME]: {
component: SchemaDefinition,
propsSelector: (store: AppStore) => ({
parser: store.spec.parser,
options: store.options,
}),
},
},
};

View File

@ -28,7 +28,10 @@ export class ClipboardService {
if ((document as any).selection) {
(document as any).selection.empty();
} else if (window.getSelection) {
window.getSelection().removeAllRanges();
const selection = window.getSelection();
if (selection) {
selection.removeAllRanges();
}
}
}

View File

@ -1,6 +1,6 @@
import * as marked from 'marked';
import { highlight, safeSlugify } from '../utils';
import { highlight, safeSlugify, unescapeHTMLChars } from '../utils';
import { AppStore } from './AppStore';
import { RedocNormalizedOptions } from './RedocNormalizedOptions';
@ -45,6 +45,14 @@ export class MarkdownRenderer {
return compRegexp.test(rawText);
}
static getTextBeforeHading(md: string, heading: string): string {
const headingLinePos = md.search(new RegExp(`^##?\\s+${heading}`, 'm'));
if (headingLinePos > -1) {
return md.substring(0, headingLinePos);
}
return md;
}
headings: MarkdownHeading[] = [];
currentTopHeading: MarkdownHeading;
@ -65,6 +73,7 @@ export class MarkdownRenderer {
container: MarkdownHeading[] = this.headings,
parentId?: string,
): MarkdownHeading {
name = unescapeHTMLChars(name);
const item = {
id: parentId ? `${parentId}/${safeSlugify(name)}` : `section/${safeSlugify(name)}`,
name,
@ -88,7 +97,7 @@ export class MarkdownRenderer {
}
attachHeadingsDescriptions(rawText: string) {
const buildRegexp = heading => {
const buildRegexp = (heading: MarkdownHeading) => {
return new RegExp(`##?\\s+${heading.name.replace(/[-\/\\^$*+?.()|[\]{}]/g, '\\$&')}`);
};
@ -118,7 +127,12 @@ export class MarkdownRenderer {
.trim();
}
headingRule = (text: string, level: number, raw: string, slugger: marked.Slugger) => {
headingRule = (
text: string,
level: 1 | 2 | 3 | 4 | 5 | 6,
raw: string,
slugger: marked.Slugger,
) => {
if (level === 1) {
this.currentTopHeading = this.saveHeading(text, level);
} else if (level === 2) {

View File

@ -42,7 +42,7 @@ export class MenuBuilder {
const items: ContentItemModel[] = [];
const tagsMap = MenuBuilder.getTagsWithOperations(spec);
items.push(...MenuBuilder.addMarkdownItems(spec.info.description || '', options));
items.push(...MenuBuilder.addMarkdownItems(spec.info.description || '', undefined, 1, options));
if (spec['x-tagGroups'] && spec['x-tagGroups'].length > 0) {
items.push(
...MenuBuilder.getTagGroupsItems(parser, undefined, spec['x-tagGroups'], tagsMap, options),
@ -59,14 +59,23 @@ export class MenuBuilder {
*/
static addMarkdownItems(
description: string,
parent: GroupModel | undefined,
initialDepth: number,
options: RedocNormalizedOptions,
): ContentItemModel[] {
const renderer = new MarkdownRenderer(options);
const headings = renderer.extractHeadings(description || '');
const mapHeadingsDeep = (parent, items, depth = 1) =>
if (headings.length && parent && parent.description) {
parent.description = MarkdownRenderer.getTextBeforeHading(
parent.description,
headings[0].name,
);
}
const mapHeadingsDeep = (_parent, items, depth = 1) =>
items.map(heading => {
const group = new GroupModel('section', heading, parent);
const group = new GroupModel('section', heading, _parent);
group.depth = depth;
if (heading.items) {
group.items = mapHeadingsDeep(group, heading.items, depth + 1);
@ -82,11 +91,11 @@ export class MenuBuilder {
return group;
});
return mapHeadingsDeep(undefined, headings);
return mapHeadingsDeep(parent, headings, initialDepth);
}
/**
* Returns array of OperationsGroup items for the tag groups (x-tagGroups vendor extenstion)
* Returns array of OperationsGroup items for the tag groups (x-tagGroups vendor extension)
* @param tags value of `x-tagGroups` vendor extension
*/
static getTagGroupsItems(
@ -144,15 +153,22 @@ export class MenuBuilder {
}
const item = new GroupModel('tag', tag, parent);
item.depth = GROUP_DEPTH + 1;
item.items = this.getOperationsItems(parser, item, tag, item.depth + 1, options);
// don't put empty tag into content, instead put its operations
if (tag.name === '') {
const items = this.getOperationsItems(parser, undefined, tag, item.depth + 1, options);
const items = [
...MenuBuilder.addMarkdownItems(tag.description || '', item, item.depth + 1, options),
...this.getOperationsItems(parser, undefined, tag, item.depth + 1, options),
];
res.push(...items);
continue;
}
item.items = [
...MenuBuilder.addMarkdownItems(tag.description || '', item, item.depth + 1, options),
...this.getOperationsItems(parser, item, tag, item.depth + 1, options),
];
res.push(item);
}
return res;

View File

@ -116,7 +116,7 @@ export class MenuStore {
}
if (isScrolledDown) {
const el = this.getElementAt(itemIdx + 1);
const el = this.getElementAtOrFirstChild(itemIdx + 1);
if (this.scroll.isElementBellow(el)) {
break;
}
@ -163,6 +163,18 @@ export class MenuStore {
return (item && querySelector(`[${SECTION_ATTR}="${item.id}"]`)) || null;
}
/**
* get section/operation DOM Node related to the item or if it is group item, returns first item of the group
* @param idx item absolute index
*/
getElementAtOrFirstChild(idx: number): Element | null {
let item = this.flatItems[idx];
if (item && item.type === 'group') {
item = item.items[0];
}
return (item && querySelector(`[${SECTION_ATTR}="${item.id}"]`)) || null;
}
/**
* current active item
*/
@ -178,7 +190,7 @@ export class MenuStore {
* activate menu item
* @param item item to activate
* @param updateLocation [true] whether to update location
* @param rewriteHistory [false] whether to rewrite browser history (do not create new enrty)
* @param rewriteHistory [false] whether to rewrite browser history (do not create new entry)
*/
@action
activate(
@ -189,6 +201,11 @@ export class MenuStore {
if ((this.activeItem && this.activeItem.id) === (item && item.id)) {
return;
}
if (item && item.type === 'group') {
return;
}
this.deactivate(this.activeItem);
if (!item) {
this.history.replace('', rewriteHistory);

View File

@ -4,7 +4,11 @@ import { OpenAPIRef, OpenAPISchema, OpenAPISpec, Referenced } from '../types';
import { appendToMdHeading, IS_BROWSER } from '../utils/';
import { JsonPointer } from '../utils/JsonPointer';
import { isNamedDefinition, SECURITY_DEFINITIONS_COMPONENT_NAME } from '../utils/openapi';
import {
isNamedDefinition,
SECURITY_DEFINITIONS_COMPONENT_NAME,
SECURITY_DEFINITIONS_JSX_NAME,
} from '../utils/openapi';
import { buildComponentComment, MarkdownRenderer } from './MarkdownRenderer';
import { RedocNormalizedOptions } from './RedocNormalizedOptions';
@ -40,6 +44,7 @@ class RefCounter {
export class OpenAPIParser {
specUrl?: string;
spec: OpenAPISpec;
mergeRefs: Set<string>;
private _refCounter: RefCounter = new RefCounter();
@ -53,6 +58,8 @@ export class OpenAPIParser {
this.spec = spec;
this.mergeRefs = new Set();
const href = IS_BROWSER ? window.location.href : '';
if (typeof specUrl === 'string') {
this.specUrl = urlResolve(href, specUrl);
@ -74,7 +81,10 @@ export class OpenAPIParser {
) {
// Automatically inject Authentication section with SecurityDefinitions component
const description = spec.info.description || '';
if (!MarkdownRenderer.containsComponent(description, SECURITY_DEFINITIONS_COMPONENT_NAME)) {
if (
!MarkdownRenderer.containsComponent(description, SECURITY_DEFINITIONS_COMPONENT_NAME) &&
!MarkdownRenderer.containsComponent(description, SECURITY_DEFINITIONS_JSX_NAME)
) {
const comment = buildComponentComment(SECURITY_DEFINITIONS_COMPONENT_NAME);
spec.info.description = appendToMdHeading(description, 'Authentication', comment);
}
@ -102,7 +112,7 @@ export class OpenAPIParser {
};
/**
* checks if the objectt is OpenAPI reference (containts $ref property)
* checks if the object is OpenAPI reference (contains $ref property)
*/
isRef(obj: any): obj is OpenAPIRef {
if (!obj) {
@ -112,7 +122,7 @@ export class OpenAPIParser {
}
/**
* resets visited enpoints. should be run after
* resets visited endpoints. should be run after
*/
resetVisited() {
if (process.env.NODE_ENV !== 'production') {
@ -136,9 +146,9 @@ export class OpenAPIParser {
/**
* Resolve given reference object or return as is if it is not a reference
* @param obj object to dereference
* @param forceCircular whether to dereference even if it is cirular ref
* @param forceCircular whether to dereference even if it is circular ref
*/
deref<T extends object>(obj: OpenAPIRef | T, forceCircular: boolean = false): T {
deref<T extends object>(obj: OpenAPIRef | T, forceCircular = false): T {
if (this.isRef(obj)) {
const resolved = this.byRef<T>(obj.$ref)!;
const visited = this._refCounter.visited(obj.$ref);
@ -167,16 +177,21 @@ export class OpenAPIParser {
}
/**
* Merge allOf contsraints.
* Merge allOf constraints.
* @param schema schema with allOF
* @param $ref pointer of the schema
* @param forceCircular whether to dereference children even if it is a cirular ref
* @param forceCircular whether to dereference children even if it is a circular ref
*/
mergeAllOf(
schema: OpenAPISchema,
$ref?: string,
forceCircular: boolean = false,
used$Refs = new Set<string>(),
): MergedOpenAPISchema {
if ($ref) {
used$Refs.add($ref);
}
schema = this.hoistOneOfs(schema);
if (schema.allOf === undefined) {
@ -198,16 +213,25 @@ export class OpenAPIParser {
receiver.items = { ...receiver.items };
}
const allOfSchemas = schema.allOf.map(subSchema => {
const allOfSchemas = schema.allOf
.map(subSchema => {
if (subSchema && subSchema.$ref && used$Refs.has(subSchema.$ref)) {
return undefined;
}
const resolved = this.deref(subSchema, forceCircular);
const subRef = subSchema.$ref || undefined;
const subMerged = this.mergeAllOf(resolved, subRef, forceCircular);
const subMerged = this.mergeAllOf(resolved, subRef, forceCircular, used$Refs);
receiver.parentRefs!.push(...(subMerged.parentRefs || []));
return {
$ref: subRef,
schema: subMerged,
};
});
})
.filter(child => child !== undefined) as Array<{
$ref: string | undefined;
schema: MergedOpenAPISchema;
}>;
for (const { $ref: subSchemaRef, schema: subSchema } of allOfSchemas) {
if (
@ -251,13 +275,13 @@ export class OpenAPIParser {
}
// merge rest of constraints
// TODO: do more intelegent merge
// TODO: do more intelligent merge
receiver = { ...subSchema, ...receiver };
if (subSchemaRef) {
receiver.parentRefs!.push(subSchemaRef);
if (receiver.title === undefined && isNamedDefinition(subSchemaRef)) {
// this is not so correct behaviour. comented out for now
// this is not so correct behaviour. commented out for now
// ref: https://github.com/Redocly/redoc/issues/601
// receiver.title = JsonPointer.baseName(subSchemaRef);
}
@ -272,8 +296,8 @@ export class OpenAPIParser {
* returns map of definition pointer to definition name
* @param $refs array of references to find derived from
*/
findDerived($refs: string[]): Dict<string> {
const res: Dict<string> = {};
findDerived($refs: string[]): Dict<string[] | string> {
const res: Dict<string[]> = {};
const schemas = (this.spec.components && this.spec.components.schemas) || {};
for (const defName in schemas) {
const def = this.deref(schemas[defName]);
@ -281,7 +305,7 @@ export class OpenAPIParser {
def.allOf !== undefined &&
def.allOf.find(obj => obj.$ref !== undefined && $refs.indexOf(obj.$ref) > -1)
) {
res['#/components/schemas/' + defName] = def['x-discriminator-value'] || defName;
res['#/components/schemas/' + defName] = [def['x-discriminator-value'] || defName];
}
}
return res;

View File

@ -21,22 +21,30 @@ export interface RedocRawOptions {
disableSearch?: boolean | string;
onlyRequiredInSamples?: boolean | string;
showExtensions?: boolean | string | string[];
showOtherInfoPanel?: boolean;
hideSingleRequestSampleTab?: boolean | string;
menuToggle?: boolean | string;
jsonSampleExpandLevel?: number | string | 'all';
hideSchemaTitles?: boolean | string;
payloadSampleIdx?: number;
expandSingleSchemaField?: boolean | string;
unstable_ignoreMimeParameters?: boolean;
allowedMdComponents?: Dict<MDXComponentMeta>;
labels?: LabelsConfigRaw;
enumSkipQuotes?: boolean | string;
expandDefaultServerVariables?: boolean;
}
function argValueToBoolean(val?: string | boolean): boolean {
function argValueToBoolean(val?: string | boolean, defaultValue?: boolean): boolean {
if (val === undefined) {
return false;
return defaultValue || false;
}
if (typeof val === 'string') {
return true;
return val === 'false' ? false : true;
}
return val;
}
@ -111,6 +119,28 @@ export class RedocNormalizedOptions {
return value;
}
static normalizePayloadSampleIdx(value: RedocRawOptions['payloadSampleIdx']): number {
if (typeof value === 'number') {
return Math.max(0, value); // always greater or equal than 0
}
if (typeof value === 'string') {
return isFinite(value) ? parseInt(value, 10) : 0;
}
return 0;
}
private static normalizeJsonSampleExpandLevel(level?: number | string | 'all'): number {
if (level === 'all') {
return +Infinity;
}
if (!isNaN(Number(level))) {
return Math.ceil(Number(level));
}
return 2;
}
theme: ResolvedThemeInterface;
scrollYOffset: () => number;
hideHostname: boolean;
@ -126,12 +156,19 @@ export class RedocNormalizedOptions {
onlyRequiredInSamples: boolean;
showExtensions: boolean | string[];
hideSingleRequestSampleTab: boolean;
showOtherInfoPanel: boolean;
menuToggle: boolean;
jsonSampleExpandLevel: number;
enumSkipQuotes: boolean;
hideSchemaTitles: boolean;
payloadSampleIdx: number;
expandSingleSchemaField: boolean;
/* tslint:disable-next-line */
unstable_ignoreMimeParameters: boolean;
allowedMdComponents: Dict<MDXComponentMeta>;
expandDefaultServerVariables: boolean;
constructor(raw: RedocRawOptions, defaults: RedocRawOptions = {}) {
raw = { ...defaults, ...raw };
const hook = raw.theme && raw.theme.extensionsHook;
@ -157,11 +194,21 @@ export class RedocNormalizedOptions {
this.disableSearch = argValueToBoolean(raw.disableSearch);
this.onlyRequiredInSamples = argValueToBoolean(raw.onlyRequiredInSamples);
this.showExtensions = RedocNormalizedOptions.normalizeShowExtensions(raw.showExtensions);
this.showOtherInfoPanel = argValueToBoolean(raw.showOtherInfoPanel);
this.hideSingleRequestSampleTab = argValueToBoolean(raw.hideSingleRequestSampleTab);
this.menuToggle = argValueToBoolean(raw.menuToggle, true);
this.jsonSampleExpandLevel = RedocNormalizedOptions.normalizeJsonSampleExpandLevel(
raw.jsonSampleExpandLevel,
);
this.enumSkipQuotes = argValueToBoolean(raw.enumSkipQuotes);
this.hideSchemaTitles = argValueToBoolean(raw.hideSchemaTitles);
this.payloadSampleIdx = RedocNormalizedOptions.normalizePayloadSampleIdx(raw.payloadSampleIdx);
this.expandSingleSchemaField = argValueToBoolean(raw.expandSingleSchemaField);
// eslint-disable-next-line @typescript-eslint/camelcase
this.unstable_ignoreMimeParameters = argValueToBoolean(raw.unstable_ignoreMimeParameters);
this.allowedMdComponents = raw.allowedMdComponents || {};
this.expandDefaultServerVariables = argValueToBoolean(raw.expandDefaultServerVariables);
}
}

View File

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

View File

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

View File

@ -2,7 +2,7 @@ import * as lunr from 'lunr';
try {
// tslint:disable-next-line
require('core-js/es6/promise'); // bundle into worker
require('core-js/es/promise'); // bundle into worker
} catch (_) {
// nope
}

View File

@ -6,7 +6,7 @@ import { SecuritySchemesModel } from './models/SecuritySchemes';
import { OpenAPIParser } from './OpenAPIParser';
import { RedocNormalizedOptions } from './RedocNormalizedOptions';
/**
* Store that containts all the specification related information in the form of tree
* Store that contains all the specification related information in the form of tree
*/
export class SpecStore {
parser: OpenAPIParser;

View File

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

View File

@ -10,6 +10,13 @@
"in": "path",
"name": "test_name",
"schema": { "type": "string" }
},
"serializationParam": {
"in": "query",
"name": "serialization_test_name",
"schema": { "type": "array" },
"style": "form",
"explode": true
}
},
"headers": {

View File

@ -13,7 +13,7 @@ describe('History service', () => {
expect(fn).toHaveBeenCalled();
});
test('History subscribe should return unsubsribe function', () => {
test('History subscribe should return unsubscribe function', () => {
const fn = jest.fn();
const unsubscribe = history.subscribe(fn);
history.emit();

View File

@ -6,9 +6,9 @@ const opts = new RedocNormalizedOptions({});
describe('Models', () => {
describe('FieldModel', () => {
let parser;
// eslint-disable-next-line @typescript-eslint/no-var-requires
const spec = require('../fixtures/fields.json');
parser = new OpenAPIParser(spec, undefined, opts);
const parser = new OpenAPIParser(spec, undefined, opts);
test('basic field details', () => {
const field = new FieldModel(
@ -26,6 +26,23 @@ describe('Models', () => {
expect(field.schema.type).toEqual('string');
});
test('field details relevant for parameter serialization', () => {
const field = new FieldModel(
parser,
{
$ref: '#/components/parameters/serializationParam',
},
'#/components/parameters/serializationParam',
opts,
);
expect(field.name).toEqual('serialization_test_name');
expect(field.in).toEqual('query');
expect(field.schema.type).toEqual('array');
expect(field.style).toEqual('form');
expect(field.explode).toEqual(true);
});
test('field name should populated from name even if $ref (headers)', () => {
const field = new FieldModel(
parser,

View File

@ -22,7 +22,7 @@ describe('Models', () => {
expect(resp.type).toEqual('error');
});
test('default should be sucessful by default', () => {
test('default should be successful by default', () => {
const resp = new ResponseModel(parser, 'default', false, {}, opts);
expect(resp.type).toEqual('success');
});

View File

@ -1,3 +1,4 @@
/* eslint-disable @typescript-eslint/no-var-requires */
import { SchemaModel } from '../../models/Schema';
import { OpenAPIParser } from '../../OpenAPIParser';
import { RedocNormalizedOptions } from '../../RedocNormalizedOptions';

View File

@ -17,6 +17,7 @@ export class ApiInfoModel implements OpenAPIInfo {
constructor(private parser: OpenAPIParser) {
Object.assign(this, parser.spec.info);
this.description = parser.spec.info.description || '';
const firstHeadingLinePos = this.description.search(/^##?\s+/m);
if (firstHeadingLinePos > -1) {
this.description = this.description.substring(0, firstHeadingLinePos);

View File

@ -1,18 +1,36 @@
import { action, observable } from 'mobx';
import { OpenAPIParameter, Referenced } from '../../types';
import {
OpenAPIParameter,
OpenAPIParameterLocation,
OpenAPIParameterStyle,
Referenced,
} from '../../types';
import { RedocNormalizedOptions } from '../RedocNormalizedOptions';
import { extractExtensions } from '../../utils/openapi';
import { OpenAPIParser } from '../OpenAPIParser';
import { SchemaModel } from './Schema';
function getDefaultStyleValue(parameterLocation: OpenAPIParameterLocation): OpenAPIParameterStyle {
switch (parameterLocation) {
case 'header':
return 'simple';
case 'query':
return 'form';
case 'path':
return 'simple';
default:
return 'form';
}
}
/**
* Field or Parameter model ready to be used by components
*/
export class FieldModel {
@observable
expanded: boolean = false;
expanded: boolean | undefined;
schema: SchemaModel;
name: string;
@ -20,9 +38,13 @@ export class FieldModel {
description: string;
example?: string;
deprecated: boolean;
in?: string;
in?: OpenAPIParameterLocation;
kind: string;
extensions?: Dict<any>;
explode: boolean;
style?: OpenAPIParameterStyle;
serializationMime?: string;
constructor(
parser: OpenAPIParser,
@ -35,11 +57,29 @@ export class FieldModel {
this.name = infoOrRef.name || info.name;
this.in = info.in;
this.required = !!info.required;
this.schema = new SchemaModel(parser, info.schema || {}, pointer, options);
let fieldSchema = info.schema;
let serializationMime = '';
if (!fieldSchema && info.in && info.content) {
serializationMime = Object.keys(info.content)[0];
fieldSchema = info.content[serializationMime] && info.content[serializationMime].schema;
}
this.schema = new SchemaModel(parser, fieldSchema || {}, pointer, options);
this.description =
info.description === undefined ? this.schema.description || '' : info.description;
this.example = info.example || this.schema.example;
if (serializationMime) {
this.serializationMime = serializationMime;
} else if (info.style) {
this.style = info.style;
} else if (this.in) {
this.style = getDefaultStyleValue(this.in);
}
this.explode = !!info.explode;
this.deprecated = info.deprecated === undefined ? !!this.schema.deprecated : info.deprecated;
parser.exitRef(infoOrRef);

View File

@ -2,7 +2,7 @@ import { action, observable } from 'mobx';
import { OpenAPIExternalDocumentation, OpenAPITag } from '../../types';
import { safeSlugify } from '../../utils';
import { MarkdownHeading } from '../MarkdownRenderer';
import { MarkdownHeading, MarkdownRenderer } from '../MarkdownRenderer';
import { ContentItemModel } from '../MenuBuilder';
import { IMenuItem, MenuItemGroupType } from '../MenuStore';
@ -40,7 +40,15 @@ export class GroupModel implements IMenuItem {
this.type = type;
this.name = tagOrGroup['x-displayName'] || tagOrGroup.name;
this.level = (tagOrGroup as MarkdownHeading).level || 1;
// remove sections from markdown, same as in ApiInfo
this.description = tagOrGroup.description || '';
const items = (tagOrGroup as MarkdownHeading).items;
if (items && items.length) {
this.description = MarkdownRenderer.getTextBeforeHading(this.description, items[0].name);
}
this.parent = parent;
this.externalDocs = (tagOrGroup as OpenAPITag).externalDocs;

View File

@ -21,7 +21,7 @@ export class MediaContentModel {
* @param isRequestType needed to know if skipe RO/RW fields in objects
*/
constructor(
public parser: OpenAPIParser,
parser: OpenAPIParser,
info: Dict<OpenAPIMediaType>,
public isRequestType: boolean,
options: RedocNormalizedOptions,

View File

@ -27,9 +27,23 @@ import { ContentItemModel, ExtendedOpenAPIOperation } from '../MenuBuilder';
import { OpenAPIParser } from '../OpenAPIParser';
import { RedocNormalizedOptions } from '../RedocNormalizedOptions';
import { FieldModel } from './Field';
import { MediaContentModel } from './MediaContent';
import { RequestBodyModel } from './RequestBody';
import { ResponseModel } from './Response';
interface XPayloadSample {
lang: 'payload';
label: string;
requestBodyContent: MediaContentModel;
source: string;
}
export function isPayloadSample(
sample: XPayloadSample | OpenAPIXCodeSample,
): sample is XPayloadSample {
return sample.lang === 'payload' && (sample as any).requestBodyContent;
}
/**
* Operation model ready to be used by components
*/
@ -62,7 +76,6 @@ export class OperationModel implements IMenuItem {
path: string;
servers: OpenAPIServer[];
security: SecurityRequirementModel[];
codeSamples: OpenAPIXCodeSample[];
extensions: Dict<any>;
constructor(
@ -89,7 +102,6 @@ export class OperationModel implements IMenuItem {
this.httpVerb = operationSpec.httpVerb;
this.deprecated = !!operationSpec.deprecated;
this.operationId = operationSpec.operationId;
this.codeSamples = operationSpec['x-code-samples'] || [];
this.path = operationSpec.pathName;
const pathInfo = parser.byRef<OpenAPIPath>(
@ -144,6 +156,30 @@ export class OperationModel implements IMenuItem {
);
}
@memoize
get codeSamples() {
let samples: Array<OpenAPIXCodeSample | XPayloadSample> =
this.operationSpec['x-code-samples'] || [];
const requestBodyContent = this.requestBody && this.requestBody.content;
if (requestBodyContent && requestBodyContent.hasSample) {
const insertInx = Math.min(samples.length, this.options.payloadSampleIdx);
samples = [
...samples.slice(0, insertInx),
{
lang: 'payload',
label: 'Payload',
source: '',
requestBodyContent,
},
...samples.slice(insertInx),
];
}
return samples;
}
@memoize
get parameters() {
const _parameters = mergeParams(
@ -154,11 +190,12 @@ export class OperationModel implements IMenuItem {
).map(paramOrRef => new FieldModel(this.parser, paramOrRef, this.pointer, this.options));
if (this.options.sortPropsAlphabetically) {
sortByField(_parameters, 'name');
return sortByField(_parameters, 'name');
}
if (this.options.requiredPropsFirst) {
sortByRequired(_parameters);
return sortByRequired(_parameters);
}
return _parameters;
}

View File

@ -75,6 +75,7 @@ export class SchemaModel {
this.pointer = schemaOrRef.$ref || pointer || '';
this.rawSchema = parser.deref(schemaOrRef);
this.schema = parser.mergeAllOf(this.rawSchema, this.pointer, isChild);
this.init(parser, isChild);
parser.exitRef(schemaOrRef);
@ -125,6 +126,13 @@ export class SchemaModel {
if (!isChild && getDiscriminator(schema) !== undefined) {
this.initDiscriminator(schema, parser);
return;
} else if (
isChild &&
Array.isArray(schema.oneOf) &&
schema.oneOf.find(s => s.$ref === this.pointer)
) {
// we hit allOf of the schema with the parent discriminator
delete schema.oneOf;
}
if (schema.oneOf !== undefined) {
@ -216,7 +224,10 @@ export class SchemaModel {
) {
const discriminator = getDiscriminator(schema)!;
this.discriminatorProp = discriminator.propertyName;
const derived = parser.findDerived([...(schema.parentRefs || []), this.pointer]);
const implicitInversedMapping = parser.findDerived([
...(schema.parentRefs || []),
this.pointer,
]);
if (schema.oneOf) {
for (const variant of schema.oneOf) {
@ -224,19 +235,41 @@ export class SchemaModel {
continue;
}
const name = JsonPointer.baseName(variant.$ref);
derived[variant.$ref] = name;
implicitInversedMapping[variant.$ref] = name;
}
}
const mapping = discriminator.mapping || {};
const explicitInversedMapping = {};
for (const name in mapping) {
derived[mapping[name]] = name;
const $ref = mapping[name];
if (Array.isArray(explicitInversedMapping[$ref])) {
explicitInversedMapping[$ref].push(name);
} else {
// overrides implicit mapping here
explicitInversedMapping[$ref] = [name];
}
}
const refs = Object.keys(derived);
this.oneOf = refs.map(ref => {
const innerSchema = new SchemaModel(parser, parser.byRef(ref)!, ref, this.options, true);
innerSchema.title = derived[ref];
const inversedMapping = { ...implicitInversedMapping, ...explicitInversedMapping };
const refs: Array<{ $ref; name }> = [];
for (const $ref of Object.keys(inversedMapping)) {
const names = inversedMapping[$ref];
if (Array.isArray(names)) {
for (const name of names) {
refs.push({ $ref, name });
}
} else {
refs.push({ $ref, name: names });
}
}
this.oneOf = refs.map(({ $ref, name }) => {
const innerSchema = new SchemaModel(parser, parser.byRef($ref)!, $ref, this.options, true);
innerSchema.title = name;
return innerSchema;
});
}
@ -251,7 +284,7 @@ function buildFields(
const props = schema.properties || {};
const additionalProps = schema.additionalProperties;
const defaults = schema.default || {};
const fields = Object.keys(props || []).map(fieldName => {
let fields = Object.keys(props || []).map(fieldName => {
let field = props[fieldName];
if (!field) {
@ -280,11 +313,11 @@ function buildFields(
});
if (options.sortPropsAlphabetically) {
sortByField(fields, 'name');
fields = sortByField(fields, 'name');
}
if (options.requiredPropsFirst) {
// if not sort alphabetically sort in the order from required keyword
sortByRequired(fields, !options.sortPropsAlphabetically ? schema.required : undefined);
fields = sortByRequired(fields, !options.sortPropsAlphabetically ? schema.required : undefined);
}
if (typeof additionalProps === 'object' || additionalProps === true) {

View File

@ -3,8 +3,8 @@ import { darken, desaturate, lighten, readableColor, transparentize } from 'poli
const defaultTheme: ThemeInterface = {
spacing: {
unit: 5,
sectionHorizontal: ({spacing}) => spacing.unit * 8,
sectionVertical: ({spacing}) => spacing.unit * 8,
sectionHorizontal: ({ spacing }) => spacing.unit * 8,
sectionVertical: ({ spacing }) => spacing.unit * 8,
},
breakpoints: {
small: '50rem',
@ -15,31 +15,31 @@ const defaultTheme: ThemeInterface = {
tonalOffset: 0.3,
primary: {
main: '#232E72',
light: ({colors}) => lighten(colors.tonalOffset, colors.primary.main),
dark: ({colors}) => darken(colors.tonalOffset, colors.primary.main),
contrastText: ({colors}) => readableColor(colors.primary.main),
light: ({ colors }) => lighten(colors.tonalOffset, colors.primary.main),
dark: ({ colors }) => darken(colors.tonalOffset, colors.primary.main),
contrastText: ({ colors }) => readableColor(colors.primary.main),
},
success: {
main: '#00aa13',
light: ({colors}) => lighten(colors.tonalOffset, colors.success.main),
dark: ({colors}) => darken(colors.tonalOffset, colors.success.main),
contrastText: ({colors}) => readableColor(colors.success.main),
light: ({ colors }) => lighten(colors.tonalOffset, colors.success.main),
dark: ({ colors }) => darken(colors.tonalOffset, colors.success.main),
contrastText: ({ colors }) => readableColor(colors.success.main),
},
warning: {
main: '#d4ad03',
light: ({colors}) => lighten(colors.tonalOffset, colors.warning.main),
dark: ({colors}) => darken(colors.tonalOffset, colors.warning.main),
light: ({ colors }) => lighten(colors.tonalOffset, colors.warning.main),
dark: ({ colors }) => darken(colors.tonalOffset, colors.warning.main),
contrastText: '#ffffff',
},
error: {
main: '#e53935',
light: ({colors}) => lighten(colors.tonalOffset, colors.error.main),
dark: ({colors}) => darken(colors.tonalOffset, colors.error.main),
contrastText: ({colors}) => readableColor(colors.error.main),
light: ({ colors }) => lighten(colors.tonalOffset, colors.error.main),
dark: ({ colors }) => darken(colors.tonalOffset, colors.error.main),
contrastText: ({ colors }) => readableColor(colors.error.main),
},
text: {
primary: '#333333',
secondary: ({colors}) => lighten(colors.tonalOffset, colors.text.primary),
secondary: ({ colors }) => lighten(colors.tonalOffset, colors.text.primary),
},
border: {
dark: 'rgba(0,0,0, 0.1)',
@ -47,20 +47,20 @@ const defaultTheme: ThemeInterface = {
},
responses: {
success: {
color: ({colors}) => colors.success.main,
backgroundColor: ({colors}) => transparentize(0.9, colors.success.main),
color: ({ colors }) => colors.success.main,
backgroundColor: ({ colors }) => transparentize(0.9, colors.success.main),
},
error: {
color: ({colors}) => colors.error.main,
backgroundColor: ({colors}) => transparentize(0.9, colors.error.main),
color: ({ colors }) => colors.error.main,
backgroundColor: ({ colors }) => transparentize(0.9, colors.error.main),
},
redirect: {
color: '#ffa500',
backgroundColor: ({colors}) => transparentize(0.9, colors.responses.redirect.color),
backgroundColor: ({ colors }) => transparentize(0.9, colors.responses.redirect.color),
},
info: {
color: '#87ceeb',
backgroundColor: ({colors}) => transparentize(0.9, colors.responses.info.color),
backgroundColor: ({ colors }) => transparentize(0.9, colors.responses.info.color),
},
},
http: {
@ -110,22 +110,26 @@ const defaultTheme: ThemeInterface = {
code: {
fontSize: '13px',
fontFamily: 'Courier, monospace',
lineHeight: ({typography}) => typography.lineHeight,
fontWeight: ({typography}) => typography.fontWeightRegular,
lineHeight: ({ typography }) => typography.lineHeight,
fontWeight: ({ typography }) => typography.fontWeightRegular,
color: '#e53935',
backgroundColor: 'rgba(38, 50, 56, 0.05)',
wrap: false,
},
links: {
color: ({colors}) => colors.primary.main,
visited: ({typography}) => typography.links.color,
hover: ({typography}) => lighten(0.2, typography.links.color),
color: ({ colors }) => colors.primary.main,
visited: ({ typography }) => typography.links.color,
hover: ({ typography }) => lighten(0.2, typography.links.color),
},
},
menu: {
width: '260px',
backgroundColor: '#F3F6FB',
textColor: '#232E72',
activeTextColor: theme =>
theme.menu.textColor !== defaultTheme.menu!.textColor
? theme.menu.textColor
: theme.colors.primary.main,
groupItems: {
textTransform: 'uppercase',
},
@ -138,8 +142,8 @@ const defaultTheme: ThemeInterface = {
},
},
logo: {
maxHeight: ({menu}) => menu.width,
maxWidth: ({menu}) => menu.width,
maxHeight: ({ menu }) => menu.width,
maxWidth: ({ menu }) => menu.width,
gutter: '2px',
},
rightPanel: {
@ -148,7 +152,7 @@ const defaultTheme: ThemeInterface = {
textColor: '#ffffff',
},
codeSample: {
backgroundColor: ({rightPanel}) => darken(0.1, rightPanel.backgroundColor),
backgroundColor: ({ rightPanel }) => darken(0.1, rightPanel.backgroundColor),
},
};
@ -308,6 +312,7 @@ export interface ResolvedThemeInterface {
width: string;
backgroundColor: string;
textColor: string;
activeTextColor: string;
groupItems: {
textTransform: string;
};

View File

@ -1,5 +0,0 @@
import { AppStore } from '../services/AppStore';
export interface BaseContainerProps {
store: AppStore;
}

View File

@ -8,7 +8,11 @@ describe('Utils', () => {
const fn = (...args) => args;
const actual = mapWithLast(arr, fn);
const expected = [[1, false], [2, false], [3, true]];
const expected = [
[1, false],
[2, false],
[3, true],
];
expect(actual).toEqual(expected);
});
@ -60,7 +64,7 @@ describe('Utils', () => {
test('should behave like Object.assign on the top level', () => {
const obj1 = { a: { a1: 'A1' }, c: 'C' };
const obj2 = { a: undefined, b: { b1: 'B1' } };
expect(mergeObjects({}, obj1, obj2)).toEqual(Object.assign({}, obj1, obj2));
expect(mergeObjects({}, obj1, obj2)).toEqual({ ...obj1, ...obj2 });
});
test('should not merge array values, just override', () => {
const obj1 = { a: ['A', 'B'] };

View File

@ -8,10 +8,13 @@ import {
mergeParams,
normalizeServers,
pluralizeType,
serializeParameterValue,
sortByRequired,
} from '../';
import { OpenAPIParser } from '../../services';
import { OpenAPIParameter } from '../../types';
import { FieldModel, OpenAPIParser, RedocNormalizedOptions } from '../../services';
import { OpenAPIParameter, OpenAPIParameterLocation, OpenAPIParameterStyle } from '../../types';
import { expandDefaultServerVariables } from '../openapi';
describe('Utils', () => {
describe('openapi getStatusCode', () => {
@ -247,7 +250,7 @@ describe('Utils', () => {
expect(res).toEqual([{ url: 'http://base.com/sandbox/test', description: '' }]);
});
it('should correcly resolve url with server relative path', () => {
it('should correctly resolve url with server relative path', () => {
const res = normalizeServers('http://base.com/subpath/spec.yaml', [
{
url: '/sandbox/test',
@ -256,7 +259,7 @@ describe('Utils', () => {
expect(res).toEqual([{ url: 'http://base.com/sandbox/test', description: '' }]);
});
it('should correcly resolve url with relative path', () => {
it('should correctly resolve url with relative path', () => {
const res = normalizeServers('http://base.com/subpath/spec.yaml', [
{
url: 'sandbox/test',
@ -296,11 +299,8 @@ describe('Utils', () => {
it('should expand variables', () => {
const servers = normalizeServers('', [
{
url: '{protocol}{host}{basePath}',
url: 'http://{host}{basePath}',
variables: {
protocol: {
default: 'http://',
},
host: {
default: '127.0.0.1',
},
@ -318,9 +318,15 @@ describe('Utils', () => {
},
]);
expect(servers[0].url).toEqual('http://127.0.0.1/path/to/endpoint');
expect(servers[1].url).toEqual('http://127.0.0.2:{port}');
expect(servers[2].url).toEqual('http://127.0.0.3');
expect(expandDefaultServerVariables(servers[0].url, servers[0].variables)).toEqual(
'http://127.0.0.1/path/to/endpoint',
);
expect(expandDefaultServerVariables(servers[1].url, servers[1].variables)).toEqual(
'http://127.0.0.2:{port}',
);
expect(expandDefaultServerVariables(servers[2].url, servers[2].variables)).toEqual(
'http://127.0.0.3',
);
});
});
@ -328,7 +334,8 @@ describe('Utils', () => {
const itemConstraintSchema = (
min: number | undefined = undefined,
max: number | undefined = undefined,
) => ({ type: 'array', minItems: min, maxItems: max });
multipleOf: number | undefined = undefined,
) => ({ type: 'array', minItems: min, maxItems: max, multipleOf });
it('should not have a humanized constraint without schema constraints', () => {
expect(humanizeConstraints(itemConstraintSchema())).toHaveLength(0);
@ -350,9 +357,21 @@ describe('Utils', () => {
expect(humanizeConstraints(itemConstraintSchema(7, 7))).toContain('7 items');
});
it('should have a humazined constraint when justMinItems is set, and it is equal to 1', () => {
it('should have a humanized constraint when justMinItems is set, and it is equal to 1', () => {
expect(humanizeConstraints(itemConstraintSchema(1))).toContain('non-empty');
});
it('should have a humanized constraint when multipleOf is set, and it is in format of /^0\\.0*1$/', () => {
expect(humanizeConstraints(itemConstraintSchema(undefined, undefined, 0.01))).toContain(
'decimal places <= 2',
);
});
it('should have a humanized constraint when multipleOf is set, and it is in format other than /^0\\.0*1$/', () => {
expect(humanizeConstraints(itemConstraintSchema(undefined, undefined, 0.5))).toContain(
'multiple of 0.5',
);
});
});
describe('OpenAPI pluralizeType', () => {
@ -365,16 +384,623 @@ describe('Utils', () => {
expect(pluralizeType('array')).toEqual('arrays');
});
it('should pluralize complex dislay types', () => {
it('should pluralize complex display types', () => {
expect(pluralizeType('object (Pet)')).toEqual('objects (Pet)');
expect(pluralizeType('string <email>')).toEqual('strings <email>');
});
it('should pluralize oneOf-ed dislay types', () => {
it('should pluralize oneOf-ed display types', () => {
expect(pluralizeType('object or string')).toEqual('objects or strings');
expect(pluralizeType('object (Pet) or number <int64>')).toEqual(
'objects (Pet) or numbers <int64>',
);
});
it('should not pluralize display types that are already pluralized', () => {
expect(pluralizeType('strings')).toEqual('strings');
expect(pluralizeType('objects (Pet)')).toEqual('objects (Pet)');
expect(pluralizeType('strings <email>')).toEqual('strings <email>');
expect(pluralizeType('objects or strings')).toEqual('objects or strings');
expect(pluralizeType('objects (Pet) or numbers <int64>')).toEqual(
'objects (Pet) or numbers <int64>',
);
});
});
describe('openapi serializeParameter', () => {
interface TestCase {
style: OpenAPIParameterStyle;
explode: boolean;
expected: string;
}
interface TestValueTypeGroup {
value: any;
description: string;
cases: TestCase[];
}
interface TestLocationGroup {
location: OpenAPIParameterLocation;
name: string;
description: string;
cases: TestValueTypeGroup[];
}
const testCases: TestLocationGroup[] = [
{
location: 'path',
name: 'id',
description: 'path parameters',
cases: [
{
value: 5,
description: 'primitive values',
cases: [
{ style: 'simple', explode: false, expected: '5' },
{ style: 'simple', explode: true, expected: '5' },
{ style: 'label', explode: false, expected: '.5' },
{ style: 'label', explode: true, expected: '.5' },
{ style: 'matrix', explode: false, expected: ';id=5' },
{ style: 'matrix', explode: true, expected: ';id=5' },
],
},
{
value: [3, 4, 5],
description: 'array values',
cases: [
{ style: 'simple', explode: false, expected: '3,4,5' },
{ style: 'simple', explode: true, expected: '3,4,5' },
{ style: 'label', explode: false, expected: '.3,4,5' },
{ style: 'label', explode: true, expected: '.3.4.5' },
{ style: 'matrix', explode: false, expected: ';id=3,4,5' },
{ style: 'matrix', explode: true, expected: ';id=3;id=4;id=5' },
],
},
{
value: { role: 'admin', firstName: 'Alex' },
description: 'object values',
cases: [
{ style: 'simple', explode: false, expected: 'role,admin,firstName,Alex' },
{ style: 'simple', explode: true, expected: 'role=admin,firstName=Alex' },
{ style: 'label', explode: false, expected: '.role,admin,firstName,Alex' },
{ style: 'label', explode: true, expected: '.role=admin.firstName=Alex' },
{ style: 'matrix', explode: false, expected: ';id=role,admin,firstName,Alex' },
{ style: 'matrix', explode: true, expected: ';role=admin;firstName=Alex' },
],
},
],
},
{
location: 'query',
name: 'id',
description: 'query parameters',
cases: [
{
value: 5,
description: 'primitive values',
cases: [
{ style: 'form', explode: true, expected: 'id=5' },
{ style: 'form', explode: false, expected: 'id=5' },
],
},
{
value: [3, 4, 5],
description: 'array values',
cases: [
{ style: 'form', explode: true, expected: 'id=3&id=4&id=5' },
{ style: 'form', explode: false, expected: 'id=3,4,5' },
{ style: 'spaceDelimited', explode: true, expected: 'id=3&id=4&id=5' },
{ style: 'spaceDelimited', explode: false, expected: 'id=3%204%205' },
{ style: 'pipeDelimited', explode: true, expected: 'id=3&id=4&id=5' },
{ style: 'pipeDelimited', explode: false, expected: 'id=3|4|5' },
],
},
{
value: { role: 'admin', firstName: 'Alex' },
description: 'object values',
cases: [
{ style: 'form', explode: true, expected: 'role=admin&firstName=Alex' },
{ style: 'form', explode: false, expected: 'id=role,admin,firstName,Alex' },
{ style: 'deepObject', explode: true, expected: 'id[role]=admin&id[firstName]=Alex' },
],
},
],
},
{
location: 'cookie',
name: 'id',
description: 'cookie parameters',
cases: [
{
value: 5,
description: 'primitive values',
cases: [
{ style: 'form', explode: true, expected: 'id=5' },
{ style: 'form', explode: false, expected: 'id=5' },
],
},
{
value: [3, 4, 5],
description: 'array values',
cases: [
{ style: 'form', explode: true, expected: 'id=3&id=4&id=5' },
{ style: 'form', explode: false, expected: 'id=3,4,5' },
],
},
{
value: { role: 'admin', firstName: 'Alex' },
description: 'object values',
cases: [
{ style: 'form', explode: true, expected: 'role=admin&firstName=Alex' },
{ style: 'form', explode: false, expected: 'id=role,admin,firstName,Alex' },
],
},
],
},
{
location: 'header',
name: 'x-id',
description: 'header parameters',
cases: [
{
value: 5,
description: 'primitive values',
cases: [
{ style: 'simple', explode: false, expected: '5' },
{ style: 'simple', explode: true, expected: '5' },
],
},
{
value: [3, 4, 5],
description: 'array values',
cases: [
{ style: 'simple', explode: false, expected: '3,4,5' },
{ style: 'simple', explode: true, expected: '3,4,5' },
],
},
{
value: { role: 'admin', firstName: 'Alex' },
description: 'object values',
cases: [
{ style: 'simple', explode: false, expected: 'role,admin,firstName,Alex' },
{ style: 'simple', explode: true, expected: 'role=admin,firstName=Alex' },
],
},
],
},
];
testCases.forEach(locationTestGroup => {
describe(locationTestGroup.description, () => {
locationTestGroup.cases.forEach(valueTypeTestGroup => {
describe(valueTypeTestGroup.description, () => {
valueTypeTestGroup.cases.forEach(testCase => {
it(`should serialize correctly when style is ${testCase.style} and explode is ${testCase.explode}`, () => {
const parameter: OpenAPIParameter = {
name: locationTestGroup.name,
in: locationTestGroup.location,
style: testCase.style,
explode: testCase.explode,
};
const serialized = serializeParameterValue(parameter, valueTypeTestGroup.value);
expect(serialized).toEqual(testCase.expected);
});
});
});
});
});
});
describe('advanced serialization', () => {
it('should serialize correctly query parameter with content with application/json', () => {
const parameter: OpenAPIParameter = {
name: 'id',
in: 'query',
content: {
'application/json': {
schema: {
type: 'string',
},
},
},
};
const parser = new OpenAPIParser({ openapi: '3.0' } as any);
const opts = new RedocNormalizedOptions({});
const field = new FieldModel(parser, parameter, '', opts);
expect(serializeParameterValue(field, { name: 'test', age: 23 })).toEqual(
'id={"name":"test","age":23}',
);
});
it('should serialize correctly header parameter with content with application/json', () => {
const parameter: OpenAPIParameter = {
name: 'x-header',
in: 'header',
content: {
'application/json': {
schema: {
type: 'string',
},
},
},
};
const parser = new OpenAPIParser({ openapi: '3.0' } as any);
const opts = new RedocNormalizedOptions({});
const field = new FieldModel(parser, parameter, '', opts);
expect(serializeParameterValue(field, { name: 'test', age: 23 })).toEqual(
'{"name":"test","age":23}',
);
});
});
});
describe('OpenAPI sortByRequired', () => {
it('should equal to the old data when all items have no required props', () => {
const fields = [
{
name: 'loginName',
required: false,
},
{
name: 'displayName',
required: false,
},
{
name: 'email',
required: false,
},
{
name: 'space',
required: false,
},
{
name: 'type',
required: false,
},
{
name: 'depIds',
required: false,
},
{
name: 'depNames',
required: false,
},
{
name: 'password',
required: false,
},
{
name: 'pwdControl',
required: false,
},
{
name: 'csfLevel',
required: false,
},
{
name: 'priority',
required: false,
},
{
name: 'siteId',
required: false,
},
];
expect(sortByRequired(fields as FieldModel[])).toEqual(fields);
});
it('other item should be the same order when some of items are required', () => {
const fields = [
{
name: 'loginName',
required: true,
},
{
name: 'displayName',
required: false,
},
{
name: 'email',
required: true,
},
{
name: 'space',
required: false,
},
{
name: 'type',
required: false,
},
{
name: 'depIds',
required: false,
},
{
name: 'depNames',
required: false,
},
{
name: 'password',
required: false,
},
{
name: 'pwdControl',
required: false,
},
{
name: 'csfLevel',
required: false,
},
{
name: 'priority',
required: false,
},
{
name: 'siteId',
required: false,
},
];
const sortedFields = [
{
name: 'loginName',
required: true,
},
{
name: 'email',
required: true,
},
{
name: 'displayName',
required: false,
},
{
name: 'space',
required: false,
},
{
name: 'type',
required: false,
},
{
name: 'depIds',
required: false,
},
{
name: 'depNames',
required: false,
},
{
name: 'password',
required: false,
},
{
name: 'pwdControl',
required: false,
},
{
name: 'csfLevel',
required: false,
},
{
name: 'priority',
required: false,
},
{
name: 'siteId',
required: false,
},
];
expect(sortByRequired(fields as FieldModel[])).toEqual(sortedFields);
});
it('should the order of required items is as same as the order parameter ', () => {
const fields = [
{
name: 'loginName',
required: true,
},
{
name: 'displayName',
required: true,
},
{
name: 'email',
required: true,
},
{
name: 'space',
required: false,
},
{
name: 'type',
required: false,
},
{
name: 'depIds',
required: false,
},
{
name: 'depNames',
required: false,
},
{
name: 'password',
required: false,
},
{
name: 'pwdControl',
required: false,
},
{
name: 'csfLevel',
required: false,
},
{
name: 'priority',
required: false,
},
{
name: 'siteId',
required: false,
},
];
expect(
sortByRequired(fields as FieldModel[], ['siteId', 'displayName', 'loginName', 'email']),
).toEqual([
{
name: 'displayName',
required: true,
},
{
name: 'loginName',
required: true,
},
{
name: 'email',
required: true,
},
{
name: 'space',
required: false,
},
{
name: 'type',
required: false,
},
{
name: 'depIds',
required: false,
},
{
name: 'depNames',
required: false,
},
{
name: 'password',
required: false,
},
{
name: 'pwdControl',
required: false,
},
{
name: 'csfLevel',
required: false,
},
{
name: 'priority',
required: false,
},
{
name: 'siteId',
required: false,
},
]);
expect(sortByRequired(fields as FieldModel[], ['email', 'displayName'])).toEqual([
{
name: 'email',
required: true,
},
{
name: 'displayName',
required: true,
},
{
name: 'loginName',
required: true,
},
{
name: 'space',
required: false,
},
{
name: 'type',
required: false,
},
{
name: 'depIds',
required: false,
},
{
name: 'depNames',
required: false,
},
{
name: 'password',
required: false,
},
{
name: 'pwdControl',
required: false,
},
{
name: 'csfLevel',
required: false,
},
{
name: 'priority',
required: false,
},
{
name: 'siteId',
required: false,
},
]);
expect(sortByRequired(fields as FieldModel[], ['displayName'])).toEqual([
{
name: 'displayName',
required: true,
},
{
name: 'loginName',
required: true,
},
{
name: 'email',
required: true,
},
{
name: 'space',
required: false,
},
{
name: 'type',
required: false,
},
{
name: 'depIds',
required: false,
},
{
name: 'depNames',
required: false,
},
{
name: 'password',
required: false,
},
{
name: 'pwdControl',
required: false,
},
{
name: 'csfLevel',
required: false,
},
{
name: 'priority',
required: false,
},
{
name: 'siteId',
required: false,
},
]);
});
});
});

View File

@ -16,6 +16,7 @@ function throttle(func, wait) {
const now = new Date().getTime();
const remaining = wait - (now - previous);
context = this;
// eslint-disable-next-line prefer-rest-params
args = arguments;
if (remaining <= 0 || remaining > wait) {
if (timeout) {

View File

@ -2,7 +2,7 @@ import slugify from 'slugify';
import { format, parse } from 'url';
/**
* Maps over array passing `isLast` bool to iterator as the second arguemnt
* Maps over array passing `isLast` bool to iterator as the second argument
*/
export function mapWithLast<T, P>(array: T[], iteratee: (item: T, isLast: boolean) => P) {
const res: P[] = [];
@ -83,7 +83,7 @@ export function appendToMdHeading(md: string, heading: string, content: string)
}
// credits https://stackoverflow.com/a/46973278/1749888
export const mergeObjects = <T extends object = object>(target: T, ...sources: T[]): T => {
export const mergeObjects = (target: any, ...sources: any[]): any => {
if (!sources.length) {
return target;
}
@ -118,7 +118,7 @@ const isMergebleObject = (item): boolean => {
/**
* slugify() returns empty string when failed to slugify.
* so try to return minimun slugified-string with failed one which keeps original value
* so try to return minimum slugified-string with failed one which keeps original value
* the regex codes are referenced with https://gist.github.com/mathewbyrne/1280286
*/
export function safeSlugify(value: string): string {
@ -189,8 +189,12 @@ export function removeQueryString(serverUrl: string): string {
function parseURL(url: string) {
if (typeof URL === 'undefined') {
// node
return new (require('url')).URL(url);
return new (require('url').URL)(url);
} else {
return new URL(url);
}
}
export function unescapeHTMLChars(str: string): string {
return str.replace(/&#(\d+);/g, (_m, code) => String.fromCharCode(parseInt(code, 10)));
}

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