├── .agignore ├── .commitlintrc.json ├── .dockerignore ├── .editorconfig ├── .eslintrc ├── .github ├── ISSUE_TEMPLATE │ ├── Bug_report.md │ ├── Feature_request.md │ └── Support.md ├── dependabot.yaml ├── pull_request_template.md └── workflows │ ├── codeql.yml │ ├── dependabot-merge.yml │ ├── deploy-gh-pages.yml │ ├── deploy-rancher.yml │ ├── docker-build-push.yml │ ├── docker-image-check.yml │ ├── nightly-build.yml │ ├── nodejs.yml │ └── release.yml ├── .gitignore ├── .husky ├── commit-msg └── pre-commit ├── .huskyrc ├── .lintstagedrc ├── .npmignore ├── .npmrc ├── .nvmrc ├── .prettierrc.yaml ├── Dockerfile ├── LICENSE ├── NOTICE ├── README.md ├── SECURITY.md ├── babel.config.js ├── build-tools ├── deploy-docker-hub-tag.sh └── loadersByExtension.js ├── config ├── .eslintrc └── jest │ ├── jest.artifact.config.js │ └── jest.unit.config.js ├── cypress.config.js ├── dev-helpers └── index.html ├── dist ├── favicon-16x16.png ├── favicon-32x32.png ├── oauth2-redirect.html ├── swagger-editor-bundle.js ├── swagger-editor-bundle.js.map ├── swagger-editor-es-bundle-core.js ├── swagger-editor-es-bundle-core.js.map ├── swagger-editor-es-bundle.js ├── swagger-editor-es-bundle.js.map ├── swagger-editor-standalone-preset.js ├── swagger-editor-standalone-preset.js.map ├── swagger-editor.css ├── swagger-editor.css.map ├── swagger-editor.js └── swagger-editor.js.map ├── docker-run.sh ├── docs ├── drag-and-drop.gif ├── helpers.md └── import.md ├── index.html ├── nginx.conf ├── package-lock.json ├── package.json ├── patches ├── react-ace+4.4.0.patch └── react-transition-group+1.2.1.patch ├── release ├── .release-it.json ├── check-for-breaking-changes.sh └── get-changelog.sh ├── src ├── .eslintrc ├── index.js ├── layout.jsx ├── plugins │ ├── ast │ │ ├── ast.js │ │ └── index.js │ ├── default-definitions │ │ └── index.js │ ├── editor-autosuggest-keywords │ │ ├── get-completions.js │ │ ├── get-keywords-for-path.js │ │ ├── index.js │ │ ├── keyword-map.js │ │ └── wrap-actions.js │ ├── editor-autosuggest-oas3-keywords │ │ ├── get-completions.js │ │ ├── get-keywords-for-path.js │ │ ├── index.js │ │ ├── keyword-map.js │ │ ├── oas3-objects.js │ │ └── wrap-actions.js │ ├── editor-autosuggest-refs │ │ ├── get-completions.js │ │ ├── get-refs-for-path.js │ │ ├── index.js │ │ └── wrap-actions.js │ ├── editor-autosuggest-snippets │ │ ├── get-completions.js │ │ ├── get-snippets-for-path.js │ │ ├── index.js │ │ ├── snippets.js │ │ └── wrap-actions.js │ ├── editor-autosuggest │ │ ├── actions.js │ │ ├── fn.js │ │ ├── helpers.js │ │ ├── index.js │ │ └── spec-selectors.js │ ├── editor-metadata │ │ └── index.js │ ├── editor │ │ ├── actions.js │ │ ├── components │ │ │ ├── brace-snippets-yaml.js │ │ │ ├── editor-container.jsx │ │ │ └── editor.jsx │ │ ├── editor-helpers │ │ │ └── marker-placer.js │ │ ├── editor-plugins │ │ │ ├── gutter-click.js │ │ │ ├── hook.js │ │ │ ├── json-to-yaml.js │ │ │ └── tab-handler.js │ │ ├── index.js │ │ ├── reducers.js │ │ ├── selectors.js │ │ └── spec.js │ ├── json-schema-validator │ │ ├── SCHEMA_CONSTRUCTS.md │ │ ├── index.js │ │ ├── oas3-schema.yaml │ │ ├── swagger2-schema.yaml │ │ ├── validator.worker.js │ │ └── validator │ │ │ ├── condense-errors.js │ │ │ ├── index.js │ │ │ ├── jsonSchema.js │ │ │ ├── path-translator.js │ │ │ └── shared.js │ ├── jump-to-path │ │ ├── index.js │ │ ├── jump-icon.svg │ │ ├── jump-to-path.jsx │ │ └── spec.js │ ├── local-storage │ │ └── index.js │ ├── performance │ │ └── index.js │ ├── refs-util.js │ ├── split-pane-mode │ │ ├── components │ │ │ └── split-pane-mode.jsx │ │ └── index.js │ ├── validate-base │ │ └── index.js │ └── validate-semantic │ │ ├── README.md │ │ ├── actions.js │ │ ├── helpers.js │ │ ├── index.js │ │ ├── selectors.js │ │ └── validators │ │ ├── 2and3 │ │ ├── operations.js │ │ ├── parameters.js │ │ ├── paths.js │ │ ├── refs.js │ │ ├── schemas.js │ │ ├── security.js │ │ └── tags.js │ │ ├── form-data.js │ │ ├── helpers.js │ │ ├── oas3 │ │ ├── components.js │ │ ├── operations.js │ │ ├── parameters.js │ │ ├── refs.js │ │ └── schemas.js │ │ ├── parameters.js │ │ ├── paths.js │ │ ├── schema.js │ │ └── security.js ├── standalone │ ├── README.md │ ├── index.js │ ├── standalone-layout.js │ ├── styles │ │ ├── main.less │ │ ├── topbar-insert-forms.less │ │ ├── topbar-modal.less │ │ └── topbar.less │ ├── topbar-insert │ │ ├── dropdown │ │ │ ├── Dropdown.jsx │ │ │ └── DropdownItem.jsx │ │ ├── forms │ │ │ ├── components │ │ │ │ ├── AddForm.jsx │ │ │ │ ├── FormChild.jsx │ │ │ │ ├── FormDropdown.jsx │ │ │ │ ├── FormInput.jsx │ │ │ │ ├── FormInputWrapper.jsx │ │ │ │ ├── FormMap.jsx │ │ │ │ ├── InsertForm.jsx │ │ │ │ ├── InsertFormInput.jsx │ │ │ │ └── InsertFormList.jsx │ │ │ ├── form-data-readme.md │ │ │ ├── form-objects │ │ │ │ ├── add-operation-tags.js │ │ │ │ ├── contact-object.js │ │ │ │ ├── example-value-object.js │ │ │ │ ├── external-documentation-object.js │ │ │ │ ├── info-object.js │ │ │ │ ├── license-object.js │ │ │ │ ├── operation-object.js │ │ │ │ ├── path-object.js │ │ │ │ ├── select-operation.js │ │ │ │ ├── select-response.js │ │ │ │ ├── server-variable-object.js │ │ │ │ ├── servers-object.js │ │ │ │ ├── tag-object.js │ │ │ │ └── tags-object.js │ │ │ └── helpers │ │ │ │ ├── form-data-helpers.js │ │ │ │ └── validation-helpers.js │ │ ├── index.js │ │ ├── modal │ │ │ └── Modal.jsx │ │ └── topbar-insert.jsx │ ├── topbar-menu-edit-convert │ │ ├── components │ │ │ ├── convert-definition-menu-item.jsx │ │ │ └── convert-modal.jsx │ │ └── index.js │ ├── topbar-menu-file-import_file │ │ ├── components │ │ │ └── ImportFileMenuItem.jsx │ │ └── index.js │ ├── topbar-new-editor-button │ │ ├── components │ │ │ └── NewEditorButton.jsx │ │ └── index.js │ └── topbar │ │ ├── assets │ │ └── logo_small.svg │ │ ├── components │ │ ├── AboutMenu.jsx │ │ ├── DropdownMenu.jsx │ │ └── Topbar.jsx │ │ └── index.js ├── style.css ├── styles │ ├── _dropdown-menu.less │ ├── _editor.less │ ├── _read-only-watermark.less │ └── main.less └── window.js ├── swagger-editor-dist-package ├── .npmignore ├── .npmrc ├── README.md ├── deploy.sh └── package.json ├── test ├── .eslintrc ├── build-artifacts │ ├── .eslintrc │ ├── es-bundle-core.js │ ├── es-bundle.js │ └── umd.js ├── cypress │ └── screenshots │ │ ├── app.cy.js │ │ └── App -- should render the app -- before each hook (failed).png │ │ └── plugin.editor-preview-swagger-ui.cy.js │ │ └── Editor Preview Pane OpenAPI 2.0, 3.0.x, 3.1.x -- should display OpenAPI 2.0 (failed).png ├── e2e │ ├── .eslintrc │ ├── fixtures │ │ ├── example.json │ │ ├── petstore.openapi.yaml │ │ ├── rejected.file.1 │ │ └── rejected.file.2 │ ├── plugins │ │ └── index.js │ ├── static │ │ ├── documents │ │ │ ├── petstore.openapi.yaml │ │ │ └── petstore.swagger.yaml │ │ └── index.html │ ├── support │ │ ├── commands.js │ │ └── e2e.js │ └── tests │ │ ├── bugs │ │ └── 1862.js │ │ ├── definition-loading.js │ │ ├── layout.js │ │ └── topbar.js ├── mocha │ ├── .eslintrc │ ├── mocks │ │ └── ace.js │ ├── plugins │ │ └── editor │ │ │ └── editor.js │ └── setup.js └── unit │ ├── .eslintrc │ ├── jest-shim.js │ ├── mocks │ └── ace.js │ ├── path-translator.js │ ├── plugins │ ├── ast │ │ └── ast-manager.js │ ├── editor-autosuggest │ │ └── fn.js │ ├── editor-metadata │ │ └── index.js │ ├── editor │ │ ├── editor.js │ │ └── spec.js │ ├── json-schema-validator │ │ ├── index.js │ │ └── test-documents │ │ │ ├── 1394.yaml │ │ │ ├── 1480.yaml │ │ │ ├── 1489.yaml │ │ │ ├── 1511.yaml │ │ │ ├── 1519.yaml │ │ │ ├── 1672.yaml │ │ │ ├── 1709.yaml │ │ │ ├── 1711.yaml │ │ │ ├── 1797.yaml │ │ │ ├── 1808.yaml │ │ │ ├── 1832.yaml │ │ │ ├── 1853.yaml │ │ │ ├── 2.0-schema-additional-properties.yaml │ │ │ ├── 3.0.0-schema-additional-properties.yaml │ │ │ ├── parameter-exclusive-schema-content.yaml │ │ │ ├── response-schema-restrictions.yaml │ │ │ ├── security-scheme-http-bearerFormat.yaml │ │ │ └── tag-object-uniqueness.yaml │ ├── selectors.js │ └── validate-semantic │ │ ├── 2and3 │ │ ├── operations.js │ │ ├── parameters.js │ │ ├── paths.js │ │ ├── refs.js │ │ ├── schemas.js │ │ ├── security.js │ │ └── tags.js │ │ ├── bugs │ │ └── 1817.js │ │ ├── form-data.js │ │ ├── oas3 │ │ ├── components.js │ │ ├── operations.js │ │ ├── parameters.js │ │ ├── refs.js │ │ └── schemas.js │ │ ├── parameters.js │ │ ├── paths.js │ │ ├── refs.js │ │ ├── schema.js │ │ ├── security.js │ │ └── validate-helper.js │ ├── setup.js │ └── standalone │ └── topbar-insert │ ├── form-data-helpers.js │ ├── form-objects.js │ ├── form-validation.js │ └── index.js └── webpack ├── _RemoveSourcemapsLackingMatchingAssetsPlugin.babel.js ├── _config-builder.js ├── _helpers.js ├── bundle.babel.js ├── core.babel.js ├── dev.babel.js ├── e2e.babel.js ├── es-bundle-core.babel.js ├── es-bundle.babel.js ├── standalone.babel.js ├── stylesheets.babel.js └── test_deps_size.babel.js /.agignore: -------------------------------------------------------------------------------- 1 | dist/ 2 | -------------------------------------------------------------------------------- /.commitlintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": [ 3 | "@commitlint/config-conventional" 4 | ], 5 | "rules": { 6 | "header-max-length": [ 7 | 2, 8 | "always", 9 | 69 10 | ], 11 | "scope-case": [ 12 | 2, 13 | "always", 14 | [ 15 | "camel-case", 16 | "kebab-case", 17 | "upper-case" 18 | ] 19 | ], 20 | "subject-case": [ 21 | 0, 22 | "always" 23 | ] 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /.dockerignore: -------------------------------------------------------------------------------- 1 | /.git 2 | /.github 3 | /dev-helpers 4 | /docs 5 | /src 6 | /swagger-editor-dist-package 7 | /test 8 | /node_modules -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | end_of_line = lf 5 | indent_style = space 6 | indent_size = 2 7 | trim_trailing_whitespace = true 8 | insert_final_newline = true 9 | [*.md] 10 | trim_trailing_whitespace = false 11 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "parser": "@babel/eslint-parser", 3 | 4 | "env": { 5 | "browser": true, 6 | "node": true, 7 | "es6": true, 8 | "jest/globals": true 9 | }, 10 | 11 | "parserOptions": { 12 | "ecmaFeatures": { 13 | "jsx": true 14 | } 15 | }, 16 | 17 | "extends": ["eslint:recommended", "plugin:react/recommended"], 18 | 19 | "plugins": ["react", "import", "mocha", "jest"], 20 | 21 | "settings": { 22 | "react": { 23 | "version": "17.0.2" 24 | } 25 | }, 26 | 27 | "rules": { 28 | "semi": [2, "never"], 29 | "strict": 0, 30 | "quotes": [2, "double", { "allowTemplateLiterals": true }], 31 | "no-unused-vars": 2, 32 | "no-multi-spaces": 1, 33 | "camelcase": 1, 34 | "no-use-before-define": [2, "nofunc"], 35 | "no-underscore-dangle": 0, 36 | "no-unused-expressions": 1, 37 | "comma-dangle": 0, 38 | "no-console": ["error", { "allow": ["warn", "error"] }], 39 | "react/jsx-no-bind": [1, { "allowArrowFunctions": true }], // TODO: make this an error 40 | "react/display-name": 0, 41 | "import/no-extraneous-dependencies": [2], 42 | "no-useless-escape": 1, 43 | "mocha/no-exclusive-tests": 2 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/Bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Report an issue you're experiencing 4 | 5 | --- 6 | 7 | 24 | 25 | ### Q&A (please complete the following information) 26 | - OS: [e.g. macOS] 27 | - Browser: [e.g. chrome, safari] 28 | - Version: [e.g. 22] 29 | - Method of installation: [e.g. npm, dist assets] 30 | - Swagger-Editor version: [e.g. 3.10.0] 31 | - Swagger/OpenAPI version: [e.g. Swagger 2.0, OpenAPI 3.0] 32 | 33 | ### Content & configuration 34 | 38 | 39 | Example Swagger/OpenAPI definition: 40 | ```yaml 41 | # your YAML here 42 | ``` 43 | 44 | Swagger-Editor configuration options: 45 | ```js 46 | SwaggerEditor({ 47 | // your config options here 48 | }) 49 | ``` 50 | 51 | ``` 52 | ?yourQueryStringConfig 53 | ``` 54 | 55 | ### Describe the bug you're encountering 56 | 57 | 58 | ### To reproduce... 59 | 60 | Steps to reproduce the behavior: 61 | 1. Go to '...' 62 | 2. Click on '....' 63 | 3. Scroll down to '....' 64 | 4. See error 65 | 66 | ### Expected behavior 67 | 68 | 69 | ### Screenshots 70 | 71 | 72 | ### Additional context or thoughts 73 | 74 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/Feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest a new feature or enhancement for this project 4 | 5 | --- 6 | 7 | ### Content & configuration 8 | 9 | Swagger/OpenAPI definition: 10 | ```yaml 11 | # your YAML here 12 | ``` 13 | 14 | Swagger-Editor configuration options: 15 | ```js 16 | SwaggerEditor({ 17 | // your config options here 18 | }) 19 | ``` 20 | 21 | ``` 22 | ?yourQueryStringConfig 23 | ``` 24 | 25 | 26 | ### Is your feature request related to a problem? 27 | 31 | 32 | ### Describe the solution you'd like 33 | 34 | 35 | ### Describe alternatives you've considered 36 | 40 | 41 | ### Additional context 42 | 43 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/Support.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Support 3 | about: Ask a question or request help with your implementation. 4 | 5 | --- 6 | 7 | 14 | 15 | ### Q&A (please complete the following information) 16 | - OS: [e.g. macOS] 17 | - Browser: [e.g. chrome, safari] 18 | - Version: [e.g. 22] 19 | - Method of installation: [e.g. npm, dist assets] 20 | - Swagger-Editor version: [e.g. 3.10.0] 21 | - Swagger/OpenAPI version: [e.g. Swagger 2.0, OpenAPI 3.0] 22 | 23 | ### Content & configuration 24 | 25 | 26 | Swagger/OpenAPI definition: 27 | ```yaml 28 | # your YAML here 29 | ``` 30 | 31 | Swagger-Editor configuration options: 32 | ```js 33 | SwaggerEditor({ 34 | // your config options here 35 | }) 36 | ``` 37 | 38 | ``` 39 | ?yourQueryStringConfig 40 | ``` 41 | 42 | ### Screenshots 43 | 44 | 45 | ### How can we help? 46 | 47 | -------------------------------------------------------------------------------- /.github/dependabot.yaml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: npm 4 | directory: "/" 5 | schedule: 6 | interval: daily 7 | commit-message: 8 | prefix: "chore" 9 | include: "scope" 10 | open-pull-requests-limit: 3 11 | 12 | - package-ecosystem: "docker" 13 | # Look for a `Dockerfile` in the `root` directory 14 | directory: "/" 15 | # Check for updates once a week 16 | schedule: 17 | interval: "weekly" 18 | 19 | - package-ecosystem: "github-actions" 20 | directory: "/" 21 | schedule: 22 | interval: "weekly" 23 | 24 | ###################### 25 | # SwaggerEditor@next # 26 | ###################### 27 | 28 | - package-ecosystem: npm 29 | directory: "/" 30 | schedule: 31 | interval: daily 32 | target-branch: "next" 33 | labels: 34 | - "swagger-editor@next" 35 | - "cat: dependencies" 36 | - "javascript" 37 | commit-message: 38 | prefix: "chore" 39 | include: "scope" 40 | open-pull-requests-limit: 3 41 | ignore: 42 | # do not update react-scripts as we use forked version of Create React App via git submodule 43 | - dependency-name: "create-react-app/packages/react-scripts" 44 | - dependency-name: "react-scripts" 45 | 46 | - package-ecosystem: "docker" 47 | # Look for a `Dockerfile` in the `root` directory 48 | target-branch: "next" 49 | directory: "/" 50 | # Check for updates once a week 51 | schedule: 52 | interval: "weekly" 53 | labels: 54 | - "swagger-editor@next" 55 | - "cat: dependencies" 56 | - "cat: docker" 57 | - "docker" 58 | 59 | - package-ecosystem: "github-actions" 60 | target-branch: "next" 61 | directory: "/" 62 | schedule: 63 | interval: "weekly" 64 | labels: 65 | - "swagger-editor@next" 66 | - "cat: dependencies" 67 | - "github_actions" 68 | 69 | 70 | -------------------------------------------------------------------------------- /.github/pull_request_template.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | ### Description 4 | 5 | 6 | 7 | 8 | ### Motivation and Context 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | ### How Has This Been Tested? 17 | 18 | 19 | 20 | 21 | 22 | 23 | ### Screenshots (if appropriate): 24 | 25 | 26 | 27 | ## Checklist 28 | 29 | 30 | 31 | ### My PR contains... 32 | 33 | - [ ] No code changes (`src/` is unmodified: changes to documentation, CI, metadata, etc.) 34 | - [ ] Dependency changes (any modification to dependencies in `package.json`) 35 | - [ ] Bug fixes (non-breaking change which fixes an issue) 36 | - [ ] Improvements (misc. changes to existing features) 37 | - [ ] Features (non-breaking change which adds functionality) 38 | 39 | ### My changes... 40 | - [ ] are breaking changes to a public API (config options, System API, major UI change, etc). 41 | - [ ] are breaking changes to a private API (Redux, component props, utility functions, etc.). 42 | - [ ] are breaking changes to a developer API (npm script behavior changes, new dev system dependencies, etc). 43 | - [ ] are not breaking changes. 44 | 45 | ### Documentation 46 | - [ ] My changes do not require a change to the project documentation. 47 | - [ ] My changes require a change to the project documentation. 48 | - [ ] If yes to above: I have updated the documentation accordingly. 49 | 50 | ### Automated tests 51 | - [ ] My changes can not or do not need to be tested. 52 | - [ ] My changes can and should be tested by unit and/or integration tests. 53 | - [ ] If yes to above: I have added tests to cover my changes. 54 | - [ ] If yes to above: I have taken care to cover edge cases in my tests. 55 | - [ ] All new and existing tests passed. 56 | -------------------------------------------------------------------------------- /.github/workflows/dependabot-merge.yml: -------------------------------------------------------------------------------- 1 | name: Dependabot Merge Me! 2 | 3 | on: 4 | pull_request_target: 5 | branches: [ master, next ] 6 | 7 | jobs: 8 | merge-me: 9 | name: Merge me! 10 | if: github.actor == 'dependabot[bot]' 11 | runs-on: ubuntu-latest 12 | steps: 13 | # This first step will fail if there's no metadata and so the approval 14 | # will not occur. 15 | - name: Dependabot metadata 16 | id: dependabot-metadata 17 | uses: dependabot/fetch-metadata@v2.4.0 18 | with: 19 | github-token: "${{ secrets.GITHUB_TOKEN }}" 20 | # Here the PR gets approved. 21 | - name: Approve a PR 22 | if: ${{ steps.dependabot-metadata.outputs.update-type != 'version-update:semver-major' }} 23 | run: gh pr review --approve "$PR_URL" 24 | env: 25 | PR_URL: ${{ github.event.pull_request.html_url }} 26 | GITHUB_TOKEN: ${{ secrets.SWAGGER_BOT_GITHUB_TOKEN }} 27 | # Finally, tell dependabot to merge the PR if all checks are successful 28 | - name: Instruct dependabot to squash & merge 29 | if: ${{ steps.dependabot-metadata.outputs.update-type != 'version-update:semver-major' }} 30 | uses: mshick/add-pr-comment@v2 31 | with: 32 | repo-token: ${{ secrets.SWAGGER_BOT_GITHUB_TOKEN }} 33 | allow-repeats: true 34 | message: | 35 | @dependabot squash and merge 36 | env: 37 | GITHUB_TOKEN: ${{ secrets.SWAGGER_BOT_GITHUB_TOKEN }} 38 | -------------------------------------------------------------------------------- /.github/workflows/deploy-gh-pages.yml: -------------------------------------------------------------------------------- 1 | # inspired by https://securitylab.github.com/research/github-actions-preventing-pwn-requests/ 2 | name: Deploy SwaggerEditor@next to GitHub Pages 3 | 4 | on: 5 | workflow_run: 6 | workflows: ["SwaggerEditor@next build", "SwaggerEditor@next nightly build"] 7 | types: 8 | - completed 9 | branches: [next] 10 | 11 | jobs: 12 | 13 | deploy: 14 | if: > 15 | github.event.workflow_run.event == 'push' && 16 | github.event.workflow_run.conclusion == 'success' 17 | name: Deploy SwaggerEditor@next to GitHub Pages 18 | runs-on: ubuntu-latest 19 | 20 | steps: 21 | - uses: actions/checkout@v4 22 | with: 23 | ref: next 24 | - name: 'Download build artifact' 25 | uses: actions/github-script@v7 26 | with: 27 | script: | 28 | const allArtifacts = await github.rest.actions.listWorkflowRunArtifacts({ 29 | owner: context.repo.owner, 30 | repo: context.repo.repo, 31 | run_id: context.payload.workflow_run.id, 32 | }); 33 | const matchArtifact = allArtifacts.data.artifacts.filter((artifact) => { 34 | return artifact.name == "build" 35 | })[0]; 36 | const download = await github.rest.actions.downloadArtifact({ 37 | owner: context.repo.owner, 38 | repo: context.repo.repo, 39 | artifact_id: matchArtifact.id, 40 | archive_format: 'zip', 41 | }); 42 | const fs = require('fs'); 43 | fs.writeFileSync('${{github.workspace}}/build.zip', Buffer.from(download.data)); 44 | - run: | 45 | mkdir deploy-dir 46 | unzip build.zip -d deploy-dir 47 | - name: Deploy 🚀 48 | uses: JamesIves/github-pages-deploy-action@v4.7.3 49 | with: 50 | token: ${{ secrets.GITHUB_TOKEN }} 51 | branch: gh-pages # The branch the action should deploy to. 52 | folder: deploy-dir # The folder the action should deploy. 53 | clean: true # Automatically remove deleted files from the deploy branch 54 | -------------------------------------------------------------------------------- /.github/workflows/deploy-rancher.yml: -------------------------------------------------------------------------------- 1 | # inspired by https://securitylab.github.com/research/github-actions-preventing-pwn-requests/ 2 | name: Deploy SwaggerEditor@next to Rancher🚢 3 | 4 | concurrency: editor-next.swagger.io 5 | 6 | on: 7 | workflow_run: 8 | workflows: ["Build & Push SwaggerEditor@next Docker image"] 9 | types: 10 | - completed 11 | 12 | jobs: 13 | 14 | deploy: 15 | if: github.event.workflow_run.conclusion == 'success' 16 | name: Deploy SwaggerEditor@next to Rancher 17 | runs-on: ubuntu-latest 18 | environment: editor-next.swagger.io 19 | 20 | steps: 21 | - name: Deploy Rancher🚢 22 | run: | 23 | ts="$(date +'%Y-%m-%dT%H:%M:%SZ' --utc)" 24 | curl -s -D /dev/stderr -X PATCH \ 25 | -H "Authorization: Bearer ${RANCHER_BEARER_TOKEN}" \ 26 | -H 'Content-Type: application/strategic-merge-patch+json' \ 27 | "${RANCHER_URL}/k8s/clusters/${RANCHER_CLUSTER_ID}/apis/apps/v1/namespaces/${RANCHER_NAMESPACE}/${RANCHER_K8S_OBJECT_TYPE}/${RANCHER_K8S_OBJECT_NAME}" \ 28 | -d "{\"spec\": {\"template\": {\"metadata\": {\"annotations\": {\"cattle.io/timestamp\": \"${ts}\"}}}}}" 29 | env: 30 | RANCHER_BEARER_TOKEN: ${{ secrets.RANCHER_BEARER_TOKEN }} 31 | RANCHER_CLUSTER_ID: 'c-n8zp2' 32 | RANCHER_NAMESPACE: 'swagger-oss' 33 | RANCHER_K8S_OBJECT_TYPE: 'daemonsets' 34 | RANCHER_URL: ${{ secrets.RANCHER_URL }} 35 | RANCHER_K8S_OBJECT_NAME: 'swagger-editor-next' 36 | -------------------------------------------------------------------------------- /.github/workflows/docker-image-check.yml: -------------------------------------------------------------------------------- 1 | name: Security scan for docker image 2 | 3 | on: 4 | workflow_dispatch: 5 | schedule: 6 | - cron: '30 4 * * *' 7 | 8 | permissions: 9 | contents: read 10 | 11 | jobs: 12 | build: 13 | permissions: 14 | contents: none 15 | runs-on: ubuntu-latest 16 | steps: 17 | - name: Run Trivy vulnerability scanner 18 | uses: aquasecurity/trivy-action@master 19 | with: 20 | image-ref: 'docker.swagger.io/swaggerapi/swagger-editor:unstable' 21 | format: 'table' 22 | exit-code: '1' 23 | ignore-unfixed: true 24 | vuln-type: 'os,library' 25 | severity: 'CRITICAL,HIGH' 26 | -------------------------------------------------------------------------------- /.github/workflows/nightly-build.yml: -------------------------------------------------------------------------------- 1 | # Do not modify this file. 2 | # Github Actions only recognizes `workflow_run` and `workflow_dispatch` 3 | # events that are located in the default branch 4 | 5 | name: SwaggerEditor@next nightly build 6 | 7 | on: 8 | workflow_dispatch: 9 | schedule: 10 | - cron: '30 22 * * *' 11 | 12 | jobs: 13 | nightly-build: 14 | runs-on: ubuntu-latest 15 | 16 | steps: 17 | - uses: actions/checkout@v4 18 | with: 19 | submodules: true 20 | ref: next 21 | 22 | - name: Use Node.js 22 23 | uses: actions/setup-node@v4 24 | with: 25 | node-version: 22 26 | cache: npm 27 | cache-dependency-path: package-lock.json 28 | 29 | - name: Install dependencies 30 | run: npm ci 31 | 32 | - name: Lint code 33 | run: npm run lint 34 | 35 | - name: unit tests 36 | run: npm test 37 | env: 38 | CI: true 39 | 40 | - name: Produce build artifacts 41 | run: npm run build 42 | 43 | - name: Produce npm artifact 44 | run: npm pack 45 | 46 | - name: E2E Tests 47 | run: npm run cy:ci 48 | 49 | - name: Upload build artifact 50 | uses: actions/upload-artifact@v4 51 | with: 52 | name: build 53 | path: ./build 54 | 55 | - name: Upload npm artifact 56 | uses: actions/upload-artifact@v4 57 | with: 58 | name: "swagger-editor-next.tgz" 59 | path: ./swagger-editor-*.tgz 60 | -------------------------------------------------------------------------------- /.github/workflows/nodejs.yml: -------------------------------------------------------------------------------- 1 | # This workflow will do a clean install of node dependencies, build the source code and run tests across different versions of node 2 | # For more information see: https://help.github.com/actions/language-and-framework-guides/using-nodejs-with-github-actions 3 | name: Node.js CI 4 | 5 | on: 6 | push: 7 | branches: [ master ] 8 | pull_request: 9 | branches: [ master ] 10 | 11 | env: 12 | CYPRESS_CACHE_FOLDER: cypress/cache 13 | 14 | permissions: 15 | contents: read 16 | 17 | jobs: 18 | build: 19 | runs-on: ubuntu-latest 20 | 21 | steps: 22 | - uses: actions/checkout@v4 23 | 24 | - name: Use Node.js 20.3.0 25 | uses: actions/setup-node@v4 26 | with: 27 | node-version: 20.3.0 28 | 29 | - name: Cache Node Modules and Cypress binary 30 | uses: actions/cache@v4 31 | id: cache-primes 32 | with: 33 | path: | 34 | node_modules 35 | ${{ env.CYPRESS_CACHE_FOLDER }} 36 | key: ${{ runner.os }}-node-and-cypress-${{ hashFiles('package-lock.json') }} 37 | 38 | - name: Install dependencies 39 | if: steps.cache-primes.outputs.cache-hit != 'true' 40 | run: npm ci --legacy-peer-deps 41 | 42 | - name: Lint code 43 | run: npm run lint 44 | 45 | - name: Build SwaggerEditor 46 | run: npm run build 47 | 48 | - name: Run all tests 49 | run: npm test 50 | env: 51 | CI: true 52 | 53 | - name: Test build artifacts 54 | run: npm run test:artifact 55 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .idea 3 | .deps_check 4 | .DS_Store 5 | .vscode 6 | npm-debug.log* 7 | .eslintcache 8 | dist/log* 9 | 10 | # Cypress 11 | test/e2e/screenshots 12 | test/e2e/videos 13 | 14 | -------------------------------------------------------------------------------- /.husky/commit-msg: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | . "$(dirname "$0")/_/husky.sh" 3 | 4 | npx commitlint -e 5 | -------------------------------------------------------------------------------- /.husky/pre-commit: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | . "$(dirname "$0")/_/husky.sh" 3 | 4 | npx lint-staged 5 | -------------------------------------------------------------------------------- /.huskyrc: -------------------------------------------------------------------------------- 1 | { 2 | "hooks": { 3 | "pre-commit": "lint-staged", 4 | "commit-msg": "commitlint -E HUSKY_GIT_PARAMS" 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /.lintstagedrc: -------------------------------------------------------------------------------- 1 | { 2 | "*.js": "eslint" 3 | } 4 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | * 2 | */ 3 | !README.md 4 | !NOTICE 5 | !package.json 6 | !dist/swagger-editor.js 7 | !dist/swagger-editor.js.map 8 | !dist/swagger-editor-bundle.js 9 | !dist/swagger-editor-bundle.js.map 10 | !dist/swagger-editor-es-bundle.js 11 | !dist/swagger-editor-es-bundle.js.map 12 | !dist/swagger-editor-es-bundle-core.js 13 | !dist/swagger-editor-es-bundle-core.js.map 14 | !dist/swagger-editor.css 15 | !dist/swagger-editor.css.map 16 | !dist/validation.worker.js 17 | !dist/validation.worker.js.map 18 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | save-prefix="=" 2 | -------------------------------------------------------------------------------- /.nvmrc: -------------------------------------------------------------------------------- 1 | 20.3.0 2 | -------------------------------------------------------------------------------- /.prettierrc.yaml: -------------------------------------------------------------------------------- 1 | semi: false 2 | trailingComma: es5 3 | endOfLine: lf 4 | requirePragma: true 5 | insertPragma: true 6 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM nginx:1.27.5-alpine 2 | 3 | LABEL maintainer="vladimir.gorej@smartbear.com" \ 4 | org.opencontainers.image.authors="vladimir.gorej@smartbear.com" \ 5 | org.opencontainers.image.url="https://editor.swagger.io" \ 6 | org.opencontainers.image.source="https://github.com/swagger-api/swagger-editor" 7 | 8 | ENV BASE_URL="/" \ 9 | PORT="8080" 10 | 11 | RUN apk update && apk add --no-cache "tiff>=4.4.0-r4" 12 | 13 | COPY nginx.conf /etc/nginx/templates/default.conf.template 14 | 15 | COPY ./index.html /usr/share/nginx/html/ 16 | COPY ./dist/oauth2-redirect.html /usr/share/nginx/html/ 17 | COPY ./dist/* /usr/share/nginx/html/dist/ 18 | COPY ./docker-run.sh /docker-entrypoint.d/91-docker-run.sh 19 | 20 | RUN chmod +x /docker-entrypoint.d/91-docker-run.sh 21 | 22 | EXPOSE 8080 23 | -------------------------------------------------------------------------------- /NOTICE: -------------------------------------------------------------------------------- 1 | swagger-editor 2 | Copyright 2020-2021 SmartBear Software Inc. 3 | 4 | react-dd-menu 5 | Copyright (c) 2015 Mikkel Laursen 6 | Specific Sass Cascading Style Sheet in `_dropdown-menu.less` (the file) were originally created under MIT license in https://github.com/mlaursen/react-dd-menu repository. 7 | Specific Sass Cascading Style Sheet have been copied into this project and modified. All modifications are licensed under Apache 2.0 License. 8 | -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | # Security Policy 2 | 3 | If you believe you've found an exploitable security issue in Swagger Editor, 4 | **please don't create a public issue**. 5 | 6 | 7 | ## Supported versions 8 | 9 | This is the list of versions of `swagger-editor` which are 10 | currently being supported with security updates. 11 | 12 | | Version | Supported | Notes | 13 | | -------- | ------------------ | ---------------------- | 14 | | 4.x | :white_check_mark: | | 15 | | 3.x | :x: | End-of-life as of December 2021 | 16 | | 2.x | :x: | End-of-life as of 2017 | 17 | 18 | ## Reporting a vulnerability 19 | 20 | To report a vulnerability please send an email with the details to [security@swagger.io](mailto:security@swagger.io). 21 | 22 | We'll acknowledge receipt of your report ASAP, and set expectations on how we plan to handle it. 23 | -------------------------------------------------------------------------------- /build-tools/deploy-docker-hub-tag.sh: -------------------------------------------------------------------------------- 1 | if [ $DOCKER_HUB_USERNAME ]; then 2 | docker login --username=$DOCKER_HUB_USERNAME --password=$DOCKER_HUB_PASSWORD; 3 | 4 | if [ ! -z "$TRAVIS_TAG" ]; then 5 | DOCKER_IMAGE_TAG=${TRAVIS_TAG#?}; 6 | docker build -t $DOCKER_IMAGE_NAME .; 7 | docker tag $DOCKER_IMAGE_NAME $DOCKER_IMAGE_NAME:$DOCKER_IMAGE_TAG; 8 | docker push $DOCKER_IMAGE_NAME:$DOCKER_IMAGE_TAG; 9 | docker tag $DOCKER_IMAGE_NAME $DOCKER_IMAGE_NAME:latest; 10 | docker push $DOCKER_IMAGE_NAME:latest; 11 | fi; 12 | fi; 13 | -------------------------------------------------------------------------------- /build-tools/loadersByExtension.js: -------------------------------------------------------------------------------- 1 | function extsToRegExp(exts) { 2 | return new RegExp("\\.(" + exts.map(function(ext) { 3 | return ext.replace(/\./g, "\\."); 4 | }).join("|") + ")(\\?.*)?$"); 5 | } 6 | 7 | module.exports = function loadersByExtension(obj) { 8 | var loaders = []; 9 | Object.keys(obj).forEach(function(key) { 10 | var exts = key.split("|"); 11 | var value = obj[key]; 12 | var entry = { 13 | extensions: exts, 14 | test: extsToRegExp(exts) 15 | }; 16 | if(Array.isArray(value)) { 17 | entry.loaders = value; 18 | } else if(typeof value === "string") { 19 | entry.loader = value; 20 | } else { 21 | Object.keys(value).forEach(function(valueKey) { 22 | entry[valueKey] = value[valueKey]; 23 | }); 24 | } 25 | loaders.push(entry); 26 | }); 27 | return loaders; 28 | }; 29 | -------------------------------------------------------------------------------- /config/.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "rules": { 3 | "import/no-unresolved": 0, 4 | "import/extensions": 0, 5 | "quotes": [ 6 | "error", 7 | "single" 8 | ], 9 | "semi": [ 10 | "error", 11 | "always" 12 | ] 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /config/jest/jest.artifact.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | 3 | module.exports = { 4 | rootDir: path.join(__dirname, '..', '..'), 5 | testEnvironment: 'jsdom', 6 | setupFiles: ['/test/unit/jest-shim.js'], 7 | testMatch: ['**/test/build-artifacts/**/*.js'], 8 | transformIgnorePatterns: ['/node_modules/(?!(swagger-client|react-syntax-highlighter)/)'], 9 | }; 10 | -------------------------------------------------------------------------------- /config/jest/jest.unit.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | 3 | module.exports = { 4 | rootDir: path.join(__dirname, '..', '..'), 5 | testEnvironment: 'jsdom', 6 | testMatch: ['**/test/unit/*.js', '**/test/unit/**/*.js'], 7 | setupFiles: ['/test/unit/jest-shim.js'], 8 | setupFilesAfterEnv: ['/test/unit/setup.js'], 9 | transformIgnorePatterns: ['/node_modules/(?!(swagger-client|react-syntax-highlighter)/)'], 10 | testPathIgnorePatterns: [ 11 | '/node_modules/', 12 | '/test/build-artifacts/', 13 | '/test/mocha/', 14 | '/test/unit/jest-shim.js', 15 | '/test/unit/setup.js', 16 | '/test/unit/plugins/validate-semantic/validate-helper.js', 17 | '/test/unit/mocks/ace.js' 18 | ], 19 | }; 20 | -------------------------------------------------------------------------------- /cypress.config.js: -------------------------------------------------------------------------------- 1 | "use strict" 2 | 3 | const { defineConfig } = require("cypress") 4 | const setupNodeEvents = require("./test/e2e/plugins/index.js") 5 | 6 | module.exports = defineConfig({ 7 | fileServerFolder: "test/e2e/static", 8 | fixturesFolder: "test/e2e/fixtures", 9 | screenshotsFolder: "test/e2e/screenshots", 10 | videosFolder: "test/e2e/videos", 11 | e2e: { 12 | baseUrl: "http://localhost:3001/", 13 | supportFile: "test/e2e/support/e2e.js", 14 | specPattern: "test/e2e/tests/**/*.js", 15 | setupNodeEvents, 16 | }, 17 | }) 18 | -------------------------------------------------------------------------------- /dist/favicon-16x16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/swagger-api/swagger-editor/02db77f41b24b75904c39790d28294961cb660bc/dist/favicon-16x16.png -------------------------------------------------------------------------------- /dist/favicon-32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/swagger-api/swagger-editor/02db77f41b24b75904c39790d28294961cb660bc/dist/favicon-32x32.png -------------------------------------------------------------------------------- /dist/oauth2-redirect.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | Swagger UI: OAuth2 Redirect 4 | 5 | 6 | 7 | 69 | -------------------------------------------------------------------------------- /docs/drag-and-drop.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/swagger-api/swagger-editor/02db77f41b24b75904c39790d28294961cb660bc/docs/drag-and-drop.gif -------------------------------------------------------------------------------- /docs/helpers.md: -------------------------------------------------------------------------------- 1 | ### `getEditorMetadata` 2 | 3 | `getEditorMetadata` is a method that allows you to get information about the Editor's state without reaching directly into the plugin system. 4 | 5 | Example: 6 | 7 | ```js 8 | const editor = SwaggerEditor({ /* your configuration here */ }) 9 | 10 | SwaggerEditor.getEditorMetadata() 11 | ``` 12 | 13 | Result: 14 | 15 | ```js 16 | { 17 | contentString: String, 18 | contentObject: Object, 19 | isValid: Boolean, 20 | errors: Array, 21 | } 22 | ``` -------------------------------------------------------------------------------- /docs/import.md: -------------------------------------------------------------------------------- 1 | # Importing OpenAPI Documents 2 | 3 | Swagger Editor can import your OpenAPI document, which can be formatted as JSON or YAML. 4 | 5 | ## Local File 6 | 7 | ### File → Import File 8 | 9 | Click **Choose File** and select import. The file you are importing has to be a valid JSON or YAML OpenAPI document. Swagger Editor will prompt you about validation errors, if any exist. 10 | 11 | ### Drag and Drop 12 | 13 | Simply drag and drop your OpenAPI JSON or YAML document into the Swagger Editor browser window. 14 | 15 | ![Swagger Editor drag and drop demo](./drag-and-drop.gif) 16 | 17 | ## Public URL 18 | 19 | ### File → Import URL 20 | 21 | Paste the URL to your OpenAPI document. 22 | 23 | ### GET Parameter 24 | 25 | Request editor.swagger.io to import an OpenAPI specification from publicly accessible content via the `?url=` parameter: 26 | ``` 27 | https://editor.swagger.io/?url=https://raw.githubusercontent.com/OAI/OpenAPI-Specification/main/examples/v2.0/yaml/api-with-examples.yaml 28 | ``` 29 | -------------------------------------------------------------------------------- /nginx.conf: -------------------------------------------------------------------------------- 1 | gzip on; 2 | gzip_static on; 3 | gzip_disable "msie6"; 4 | 5 | gzip_vary on; 6 | gzip_types text/plain text/css application/javascript; 7 | 8 | add_header X-Frame-Options DENY; 9 | server_tokens off; # Hide Nginx version 10 | 11 | server { 12 | listen $PORT; 13 | server_name localhost; 14 | 15 | location $BASE_URL { 16 | absolute_redirect off; 17 | alias /usr/share/nginx/html/; 18 | index index.html index.htm; 19 | 20 | location ~* \.(?:json|yml|yaml)$ { 21 | #SWAGGER_ROOT 22 | expires -1; 23 | } 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /patches/react-ace+4.4.0.patch: -------------------------------------------------------------------------------- 1 | diff --git a/node_modules/react-ace/lib/ace.js b/node_modules/react-ace/lib/ace.js 2 | index 031682a..5c51258 100644 3 | --- a/node_modules/react-ace/lib/ace.js 4 | +++ b/node_modules/react-ace/lib/ace.js 5 | @@ -142,8 +142,8 @@ var ReactAce = function (_Component) { 6 | } 7 | } 8 | }, { 9 | - key: 'componentWillReceiveProps', 10 | - value: function componentWillReceiveProps(nextProps) { 11 | + key: 'UNSAFE_componentWillReceiveProps', 12 | + value: function UNSAFE_componentWillReceiveProps(nextProps) { 13 | var oldProps = this.props; 14 | 15 | for (var i = 0; i < editorOptions.length; i++) { 16 | -------------------------------------------------------------------------------- /patches/react-transition-group+1.2.1.patch: -------------------------------------------------------------------------------- 1 | diff --git a/node_modules/react-transition-group/CSSTransitionGroupChild.js b/node_modules/react-transition-group/CSSTransitionGroupChild.js 2 | index bb1742b..ef14d74 100644 3 | --- a/node_modules/react-transition-group/CSSTransitionGroupChild.js 4 | +++ b/node_modules/react-transition-group/CSSTransitionGroupChild.js 5 | @@ -107,7 +107,7 @@ var CSSTransitionGroupChild = function (_React$Component) { 6 | }, _temp), _possibleConstructorReturn(_this, _ret); 7 | } 8 | 9 | - CSSTransitionGroupChild.prototype.componentWillMount = function componentWillMount() { 10 | + CSSTransitionGroupChild.prototype.UNSAFE_componentWillMount = function UNSAFE_componentWillMount() { 11 | this.classNameAndNodeQueue = []; 12 | this.transitionTimeouts = []; 13 | }; 14 | diff --git a/node_modules/react-transition-group/TransitionGroup.js b/node_modules/react-transition-group/TransitionGroup.js 15 | index 1950176..1160ebd 100644 16 | --- a/node_modules/react-transition-group/TransitionGroup.js 17 | +++ b/node_modules/react-transition-group/TransitionGroup.js 18 | @@ -143,7 +143,7 @@ var TransitionGroup = function (_React$Component) { 19 | return _this; 20 | } 21 | 22 | - TransitionGroup.prototype.componentWillMount = function componentWillMount() { 23 | + TransitionGroup.prototype.UNSAFE_componentWillMount = function UNSAFE_componentWillMount() { 24 | this.currentlyTransitioningKeys = {}; 25 | this.keysToEnter = []; 26 | this.keysToLeave = []; 27 | @@ -158,7 +158,7 @@ var TransitionGroup = function (_React$Component) { 28 | } 29 | }; 30 | 31 | - TransitionGroup.prototype.componentWillReceiveProps = function componentWillReceiveProps(nextProps) { 32 | + TransitionGroup.prototype.UNSAFE_componentWillReceiveProps = function UNSAFE_componentWillReceiveProps(nextProps) { 33 | var nextChildMapping = (0, _ChildMapping.getChildMapping)(nextProps.children); 34 | var prevChildMapping = this.state.children; 35 | 36 | -------------------------------------------------------------------------------- /release/.release-it.json: -------------------------------------------------------------------------------- 1 | { 2 | "hooks": { 3 | "before:bump": [ 4 | "./release/check-for-breaking-changes.sh ${latestVersion} ${version}", 5 | "npm update swagger-client --legacy-peer-deps", 6 | "npm update swagger-ui --legacy-peer-deps", 7 | "npm run lint-errors", 8 | "npm run test:unit-mocha", 9 | "npm run test:unit-jest" 10 | ], 11 | "after:bump": [ 12 | "npm run build", 13 | "npm run cy:ci" 14 | ], 15 | "after:release": "export GIT_TAG=v${version} && echo GIT_TAG=v${version} > release/.version" 16 | }, 17 | "git": { 18 | "requireUpstream": false, 19 | "changelog": "./release/get-changelog.sh", 20 | "commitMessage": "chore(release): cut the v${version} release", 21 | "tagName": "v${version}", 22 | "push": false 23 | }, 24 | "github": { 25 | "release": true, 26 | "releaseName": "Swagger Editor v${version} Released!", 27 | "draft": true 28 | }, 29 | "plugins": { 30 | "@release-it/conventional-changelog": { 31 | "preset": "angular" 32 | } 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /release/check-for-breaking-changes.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | CURRENT_VERSION=$1 3 | NEXT_VERSION=$2 4 | CURRENT_MAJOR=${CURRENT_VERSION:0:1} 5 | NEXT_MAJOR=${NEXT_VERSION:0:1} 6 | 7 | if [ "$CURRENT_MAJOR" -ne "$NEXT_MAJOR" ] ; 8 | then if [ "$BREAKING_OKAY" = "true" ]; 9 | then echo "breaking change detected but BREAKING_OKAY is set; continuing." && exit 0; 10 | else echo "breaking change detected and BREAKING_OKAY is not set; aborting." && exit 1; 11 | fi; 12 | fi; 13 | 14 | echo "next version is not a breaking change; continuing."; -------------------------------------------------------------------------------- /release/get-changelog.sh: -------------------------------------------------------------------------------- 1 | echo "_No release summary included._\n\n#### Changelog\n" 2 | 3 | PREV_RELEASE_REF=$(git log --pretty=oneline | grep ' release: ' | head -n 2 | tail -n 1 | cut -f 1 -d " ") 4 | 5 | git log --pretty=oneline $PREV_RELEASE_REF..HEAD | awk '{ $1=""; print}' | sed -e 's/^[ \t]*//' | sed 's/^feat/0,feat/' | sed 's/^improve/1,improve/' | sed 's/^fix/2,fix/'| sort | sed 's/^[0-2],//' | sed 's/^/* /' 6 | -------------------------------------------------------------------------------- /src/.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "rules": { 3 | "import/no-extraneous-dependencies": [ 4 | 2, 5 | { 6 | "devDependencies": false 7 | } 8 | ] 9 | } 10 | } -------------------------------------------------------------------------------- /src/layout.jsx: -------------------------------------------------------------------------------- 1 | import React, { useCallback } from "react" 2 | import PropTypes from "prop-types" 3 | import { useDropzone } from "react-dropzone" 4 | 5 | const Dropzone = ({ children, onDrop }) => { 6 | const handleDrop = useCallback((acceptedFiles, rejectedFiles) => { 7 | const someFilesWereRejected = rejectedFiles && rejectedFiles.length > 0 8 | const thereIsExactlyOneAcceptedFile = acceptedFiles && acceptedFiles.length === 1 9 | 10 | if (someFilesWereRejected || !thereIsExactlyOneAcceptedFile) { 11 | alert("Sorry, there was an error processing your file.\nPlease drag and drop exactly one .yaml or .json OpenAPI definition file.") 12 | } else { 13 | const file = acceptedFiles[0] 14 | const reader = new FileReader() 15 | reader.onloadend = () => { 16 | const spec = reader.result 17 | onDrop(spec, "fileDrop") 18 | } 19 | reader.readAsText(file, "utf-8") 20 | } 21 | }, []) 22 | const {getRootProps, getInputProps, isDragActive} = useDropzone({ 23 | onDrop: handleDrop, 24 | accept: ".yaml,application/json", 25 | multiple: false, 26 | noClick: true, 27 | }) 28 | 29 | return ( 30 |
31 | 32 | {isDragActive 33 | ? ( 34 |
35 | Please drop a .yaml or .json OpenAPI spec. 36 |
37 | ) 38 | : children 39 | } 40 |
41 | ) 42 | } 43 | Dropzone.propTypes = { 44 | children: PropTypes.node.isRequired, 45 | onDrop: PropTypes.func.isRequired, 46 | } 47 | 48 | const EditorLayout = ({ specActions, getComponent }) => { 49 | const UIBaseLayout = getComponent("BaseLayout", true) 50 | const EditorContainer = getComponent("EditorContainer", true) 51 | const SplitPaneMode = getComponent("SplitPaneMode", true) 52 | const Container = getComponent("Container") 53 | 54 | const handleChange = (newYaml, origin="editor") => { 55 | specActions.updateSpec(newYaml, origin) 56 | } 57 | 58 | return ( 59 |
60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 |
69 | ) 70 | } 71 | EditorLayout.propTypes = { 72 | errSelectors: PropTypes.object.isRequired, 73 | errActions: PropTypes.object.isRequired, 74 | specActions: PropTypes.object.isRequired, 75 | getComponent: PropTypes.func.isRequired, 76 | layoutSelectors: PropTypes.object.isRequired, 77 | layoutActions: PropTypes.object.isRequired 78 | } 79 | 80 | export default EditorLayout 81 | -------------------------------------------------------------------------------- /src/plugins/ast/index.js: -------------------------------------------------------------------------------- 1 | import * as AST from "./ast" 2 | 3 | export default function() { 4 | return { 5 | fn: { AST } 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /src/plugins/editor-autosuggest-keywords/get-completions.js: -------------------------------------------------------------------------------- 1 | import keywordMap from "./keyword-map" 2 | import getKeywordsForPath from "./get-keywords-for-path" 3 | 4 | export default function getCompletions(editor, session, pos, prefix, cb, ctx, system) { 5 | 6 | const { fn: { getPathForPosition }, specSelectors } = system 7 | 8 | const { isOAS3 } = specSelectors 9 | 10 | if(isOAS3 && isOAS3()) { 11 | // isOAS3 selector exists, and returns true 12 | return cb(null, null) 13 | } 14 | 15 | const { AST } = ctx 16 | var editorValue = editor.getValue() 17 | const path = getPathForPosition({ pos, prefix, editorValue, AST}) 18 | 19 | const suggestions = getKeywordsForPath({ system, path, keywordMap }) 20 | cb(null, suggestions) 21 | } 22 | -------------------------------------------------------------------------------- /src/plugins/editor-autosuggest-keywords/index.js: -------------------------------------------------------------------------------- 1 | import { addAutosuggestionCompleters } from "./wrap-actions" 2 | 3 | export default function EditorAutosuggestKeywordsPlugin() { 4 | return { 5 | statePlugins: { 6 | editor: { 7 | wrapActions: { addAutosuggestionCompleters }, 8 | } 9 | } 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /src/plugins/editor-autosuggest-keywords/wrap-actions.js: -------------------------------------------------------------------------------- 1 | import getCompletions from "./get-completions" 2 | 3 | // Add an autosuggest completer 4 | export const addAutosuggestionCompleters = (ori, system) => (context) => { 5 | return ori(context).concat([{ 6 | getCompletions(...args) { 7 | // Add `context`, then `system` as the last args 8 | return getCompletions(...args, context, system) 9 | } 10 | }]) 11 | } 12 | -------------------------------------------------------------------------------- /src/plugins/editor-autosuggest-oas3-keywords/get-completions.js: -------------------------------------------------------------------------------- 1 | import keywordMap from "./keyword-map" 2 | import getKeywordsForPath from "./get-keywords-for-path" 3 | 4 | export default function getCompletions(editor, session, pos, prefix, cb, ctx, system) { 5 | 6 | const { fn: { getPathForPosition }, specSelectors } = system 7 | 8 | const { isOAS3 } = specSelectors 9 | 10 | if(isOAS3 && !isOAS3()) { 11 | // isOAS3 selector exists, and returns false 12 | return cb(null, null) 13 | } 14 | 15 | const { AST } = ctx 16 | var editorValue = editor.getValue() 17 | const path = getPathForPosition({ pos, prefix, editorValue, AST}) 18 | 19 | const suggestions = getKeywordsForPath({ system, path, keywordMap }) 20 | cb(null, suggestions) 21 | } 22 | -------------------------------------------------------------------------------- /src/plugins/editor-autosuggest-oas3-keywords/index.js: -------------------------------------------------------------------------------- 1 | import { addAutosuggestionCompleters } from "./wrap-actions" 2 | 3 | export default function EditorAutosuggestOAS3KeywordsPlugin() { 4 | return { 5 | statePlugins: { 6 | editor: { 7 | wrapActions: { addAutosuggestionCompleters }, 8 | } 9 | } 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /src/plugins/editor-autosuggest-oas3-keywords/keyword-map.js: -------------------------------------------------------------------------------- 1 | import { 2 | ExternalDocumentation, 3 | Info, 4 | SecurityRequirement, 5 | Server, 6 | Tag, 7 | Components, 8 | Paths 9 | } from "./oas3-objects.js" 10 | 11 | export default { 12 | openapi: String, 13 | info: Info, 14 | servers: [Server], 15 | paths: Paths, 16 | components: Components, 17 | security: [SecurityRequirement], 18 | tags: [Tag], 19 | externalDocs: ExternalDocumentation 20 | } 21 | -------------------------------------------------------------------------------- /src/plugins/editor-autosuggest-oas3-keywords/wrap-actions.js: -------------------------------------------------------------------------------- 1 | import getCompletions from "./get-completions" 2 | 3 | // Add an autosuggest completer 4 | export const addAutosuggestionCompleters = (ori, system) => (context) => { 5 | return ori(context).concat([{ 6 | getCompletions(...args) { 7 | // Add `context`, then `system` as the last args 8 | return getCompletions(...args, context, system) 9 | } 10 | }]) 11 | } 12 | -------------------------------------------------------------------------------- /src/plugins/editor-autosuggest-refs/get-completions.js: -------------------------------------------------------------------------------- 1 | import getRefsForPath from "./get-refs-for-path" 2 | 3 | export default function getCompletions(editor, session, pos, prefix, cb, ctx, system) { 4 | 5 | const { fn: { getPathForPosition } } = system 6 | const { AST } = ctx 7 | var editorValue = editor.getValue() 8 | const path = getPathForPosition({ pos, prefix, editorValue, AST}) 9 | 10 | const suggestions = getRefsForPath({ system, path}) 11 | cb(null, suggestions) 12 | } 13 | -------------------------------------------------------------------------------- /src/plugins/editor-autosuggest-refs/get-refs-for-path.js: -------------------------------------------------------------------------------- 1 | import isArray from "lodash/isArray" 2 | import last from "lodash/last" 3 | 4 | export default function getRefsForPath({ system, path }) { 5 | 6 | // Note fellow ace hackers: 7 | // we have to be weary of _what_ ace will filter on, see the order ( probably should be fixed, but... ): https://github.com/ajaxorg/ace/blob/b219b5584456534fbccb5fb20470c61011fa0b0a/lib/ace/autocomplete.js#L469 8 | // Because of that, I'm matching on `caption` and using `snippet` instead of `value` for injecting 9 | if(isArray(path) && last(path) === "$ref") { 10 | const localRefs = system.specSelectors.localRefs() 11 | const refType = system.specSelectors.getRefType(path) 12 | return localRefs 13 | .filter(r => r.get("type") == refType) 14 | .toJS() 15 | .map(r => ({ 16 | score: 100, 17 | meta: "local", 18 | snippet: `'${r.$ref}'`, // wrap in quotes 19 | caption: r.name, 20 | })) 21 | } 22 | 23 | return [] 24 | } 25 | -------------------------------------------------------------------------------- /src/plugins/editor-autosuggest-refs/index.js: -------------------------------------------------------------------------------- 1 | import { addAutosuggestionCompleters } from "./wrap-actions" 2 | 3 | export default function EditorAutosuggestRefsPlugin() { 4 | return { 5 | statePlugins: { 6 | editor: { 7 | wrapActions: { addAutosuggestionCompleters }, 8 | } 9 | } 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /src/plugins/editor-autosuggest-refs/wrap-actions.js: -------------------------------------------------------------------------------- 1 | import getCompletions from "./get-completions" 2 | 3 | // Add an autosuggest completer 4 | export const addAutosuggestionCompleters = (ori, system) => (context) => { 5 | return ori(context).concat([{ 6 | getCompletions(...args) { 7 | // Add `context`, then `system` as the last args 8 | return getCompletions(...args, context, system) 9 | } 10 | }]) 11 | } 12 | -------------------------------------------------------------------------------- /src/plugins/editor-autosuggest-snippets/get-completions.js: -------------------------------------------------------------------------------- 1 | import snippets from "./snippets" 2 | import getSnippetsForPath from "./get-snippets-for-path" 3 | 4 | export default function getCompletions(editor, session, pos, prefix, cb, ctx, system) { 5 | 6 | const { fn: { getPathForPosition }, specSelectors } = system 7 | const { isOAS3 } = specSelectors 8 | 9 | if(isOAS3 && isOAS3()) { 10 | // isOAS3 selector exists, and returns true 11 | return cb(null, null) 12 | } 13 | 14 | const { AST } = ctx 15 | const editorValue = editor.getValue() 16 | const path = getPathForPosition({ pos, prefix, editorValue, AST}) 17 | 18 | const suggestions = getSnippetsForPath({ path, snippets}) 19 | 20 | return cb(null, suggestions) 21 | } 22 | -------------------------------------------------------------------------------- /src/plugins/editor-autosuggest-snippets/get-snippets-for-path.js: -------------------------------------------------------------------------------- 1 | import isArray from "lodash/isArray" 2 | 3 | export default function getSnippetsForPath({ path, snippets }) { 4 | // find all possible snippets, modify them to be compatible with Ace and 5 | // sort them based on their position. Sorting is done by assigning a score 6 | // to each snippet, not by sorting the array 7 | if (!isArray(path)) { 8 | return [] 9 | } 10 | 11 | return snippets 12 | .filter(snippet => { 13 | return snippet.path.length === path.length 14 | }) 15 | .filter(snippet => { 16 | return snippet.path.every((k, i) => { 17 | return !!(new RegExp(k)).test(path[i]) 18 | }) 19 | }) 20 | .map(snippet => { 21 | // change shape of snippets for ACE 22 | return { 23 | caption: snippet.name, 24 | snippet: snippet.content, 25 | meta: "snippet" 26 | } 27 | }) 28 | .map(snippetSorterForPos(path)) 29 | } 30 | 31 | export function snippetSorterForPos(path) { 32 | return function(snippet) { 33 | // by default score is high 34 | let score = 1000 35 | 36 | // if snippets content has the keyword it will get a lower score because 37 | // it's more likely less relevant 38 | // (FIX) is this logic work for all cases? 39 | path.forEach(function(keyword) { 40 | if (snippet.snippet.indexOf(keyword)) { 41 | score = 500 42 | } 43 | }) 44 | 45 | snippet.score = score 46 | 47 | return snippet 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/plugins/editor-autosuggest-snippets/index.js: -------------------------------------------------------------------------------- 1 | import * as wrapActions from "./wrap-actions" 2 | 3 | export default function EditorAutosuggestSnippetsPlugin() { 4 | return { 5 | statePlugins: { 6 | editor: { 7 | wrapActions, 8 | } 9 | } 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /src/plugins/editor-autosuggest-snippets/wrap-actions.js: -------------------------------------------------------------------------------- 1 | import getCompletions from "./get-completions" 2 | 3 | // Add an autosuggest completer 4 | export const addAutosuggestionCompleters = (ori, system) => (context) => { 5 | return ori(context).concat([{ 6 | getCompletions(...args) { 7 | // Add `context`, then `system` as the last args 8 | return getCompletions(...args, context, system) 9 | } 10 | }]) 11 | } 12 | -------------------------------------------------------------------------------- /src/plugins/editor-autosuggest/actions.js: -------------------------------------------------------------------------------- 1 | // Enable Ace editor autocompletions 2 | export const enableAutocompletions = ({editor}) => () => { 3 | editor.setOptions({ 4 | enableBasicAutocompletion: true, 5 | enableSnippets: true, 6 | enableLiveAutocompletion: true 7 | }) 8 | } 9 | 10 | // Add completers. Just override this method. And concat on your completer(s) 11 | // see: https://github.com/ajaxorg/ace/blob/master/lib/ace/autocomplete.js 12 | // eg: return ori(...args).concat({ getCompletions() {...}}) 13 | export const addAutosuggestionCompleters = () => () => { 14 | return [] 15 | } 16 | -------------------------------------------------------------------------------- /src/plugins/editor-autosuggest/fn.js: -------------------------------------------------------------------------------- 1 | export function getPathForPosition({ pos: originalPos, prefix, editorValue, AST }) { 2 | var pos = Object.assign({}, originalPos) 3 | var lines = editorValue.split(/\r\n|\r|\n/) 4 | var previousLine = lines[pos.row - 1] || "" 5 | var currentLine = lines[pos.row] 6 | var nextLine = lines[pos.row + 1] || "" 7 | var prepared = false 8 | 9 | // we're always at the document root when there's no indentation, 10 | // so let's save some effort 11 | if (pos.column === 1) { 12 | return [] 13 | } 14 | 15 | let prevLineIndent = getIndent(previousLine).length 16 | let currLineIndent = getIndent(currentLine).length 17 | 18 | const isCurrentLineEmpty = currentLine.replace(prefix, "").trim() === "" 19 | 20 | if( 21 | (previousLine.trim()[0] === "-" || nextLine.trim()[0] === "-") 22 | && currLineIndent >= prevLineIndent 23 | && isCurrentLineEmpty 24 | ) { 25 | // for arrays with existing items under it, on blank lines 26 | // example: 27 | // myArray: 28 | // - a: 1 29 | // | <-- user cursor 30 | currentLine += "- a: b" // fake array item 31 | // pos.column += 1 32 | prepared = true 33 | } 34 | 35 | // if current position is in at a free line with whitespace insert a fake 36 | // key value pair so the generated AST in ASTManager has current position in 37 | // editing node 38 | if ( !prepared && isCurrentLineEmpty) { 39 | currentLine += "a: b" // fake key value pair 40 | pos.column += 1 41 | prepared = true 42 | } 43 | 44 | if(currentLine[currentLine.length - 1] === ":") { 45 | // Add a space if a user doesn't put one after a colon 46 | // NOTE: this doesn't respect the "prepared" flag. 47 | currentLine += " " 48 | pos.column += 1 49 | } 50 | 51 | //if prefix is empty then add fake, empty value 52 | if( !prepared && !prefix){ 53 | // for scalar values with no values 54 | // i.e. "asdf: " 55 | currentLine += "~" 56 | } 57 | 58 | // append inserted character in currentLine for better AST results 59 | lines[originalPos.row] = currentLine 60 | editorValue = lines.join("\n") 61 | 62 | let path = AST.pathForPosition(editorValue, { 63 | line: pos.row, 64 | column: pos.column 65 | }) 66 | 67 | return path 68 | } 69 | 70 | function getIndent(str) { 71 | let match = str.match(/^ +/) 72 | return match ? match[0] : "" 73 | } 74 | -------------------------------------------------------------------------------- /src/plugins/editor-autosuggest/helpers.js: -------------------------------------------------------------------------------- 1 | export function wrapCompleters(completers, cutoff = 100) { 2 | let isLiveCompletionDisabled = false 3 | let lastSpeeds = [] 4 | let isPerformant = () => lastSpeeds.every(speed => speed < cutoff) 5 | 6 | if(cutoff === 0 || cutoff === "0") { 7 | // never disable live autocomplete 8 | return completers 9 | } 10 | 11 | return completers.map((completer, i) => { 12 | let ori = completer.getCompletions 13 | completer.getCompletions = function(editor, session, pos, prefix, callback) { 14 | let startTime = Date.now() 15 | try { 16 | ori(editor, session, pos, prefix, (...args) => { 17 | let msElapsed = Date.now() - startTime 18 | lastSpeeds[i] = msElapsed 19 | 20 | if(isLiveCompletionDisabled && isPerformant()) { 21 | console.warn("Manual autocomplete was performant - re-enabling live autocomplete") 22 | editor.setOptions({ 23 | enableLiveAutocompletion: true 24 | }) 25 | isLiveCompletionDisabled = false 26 | } 27 | 28 | if(msElapsed > cutoff && editor.getOption("enableLiveAutocompletion")) { 29 | console.warn("Live autocomplete is slow - disabling it") 30 | editor.setOptions({ 31 | enableLiveAutocompletion: false 32 | }) 33 | isLiveCompletionDisabled = true 34 | } 35 | 36 | callback(...args) 37 | }) 38 | } catch(e) { 39 | console.error("Autocompleter encountered an error") 40 | console.error(e) 41 | callback(null, []) 42 | } 43 | } 44 | return completer 45 | }) 46 | } 47 | -------------------------------------------------------------------------------- /src/plugins/editor-autosuggest/index.js: -------------------------------------------------------------------------------- 1 | import * as actions from "./actions" 2 | import * as fn from "./fn" 3 | import * as specSelectors from "./spec-selectors" 4 | import { wrapCompleters } from "./helpers" 5 | 6 | export default function EditorAutosuggestPlugin() { 7 | return { 8 | fn, 9 | statePlugins: { 10 | spec: { 11 | selectors: specSelectors, 12 | }, 13 | editor: { 14 | actions, 15 | wrapActions: { 16 | onLoad: (ori, sys) => (context) => { 17 | const { editor } = context 18 | 19 | // Any other calls for editor#onLoad 20 | ori(context) 21 | 22 | // Enable autosuggestions ( aka: autocompletions ) 23 | sys.editorActions.enableAutocompletions(context) 24 | 25 | // Add completers ( for autosuggestions ) 26 | const completers = sys.editorActions.addAutosuggestionCompleters(context) 27 | const cutoff = sys.getConfigs().liveAutocompleteCutoff 28 | const wrappedCompleters = wrapCompleters(completers || [], cutoff) 29 | editor.completers = wrappedCompleters 30 | return 31 | } 32 | } 33 | } 34 | } 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/plugins/editor-autosuggest/spec-selectors.js: -------------------------------------------------------------------------------- 1 | import { createSelector } from "reselect" 2 | import { Set, Map } from "immutable" 3 | import { escapeJsonPointerToken } from "../refs-util" 4 | 5 | const SWAGGER2_REF_MAP = { 6 | "paths": "pathitems", 7 | "definitions": "definitions", 8 | "schema": "definitions", 9 | "parameters": "parameters", 10 | "responses": "responses" 11 | } 12 | 13 | const OAS3_REF_MAP = { 14 | schemas: "components/schemas", // for Schemas within Components 15 | schema: "components/schemas", // for Schemas throughout document 16 | parameters: "components/parameters", 17 | requestBody: "components/requestBodies", 18 | callbacks: "components/callbacks", 19 | examples: "components/examples", 20 | responses: "components/responses", 21 | headers: "components/headers", 22 | links: "components/links" 23 | } 24 | 25 | const SWAGGER2_TYPES = Set(Object.values(SWAGGER2_REF_MAP)) 26 | const OAS3_TYPES = Set(Object.values(OAS3_REF_MAP)) 27 | 28 | // Return a normalized "type" for a given path [a,b,c] 29 | // eg: /definitions/bob => definition 30 | // /paths/~1pets/responses/200/schema => definition ( because of schema ) 31 | export const getRefType = (state, path) => (sys) => createSelector( 32 | () => { 33 | for( var i=path.length-1; i>-1; i-- ) { 34 | let tag = path[i] 35 | if(sys.specSelectors.isOAS3 && sys.specSelectors.isOAS3()) { 36 | if(OAS3_REF_MAP[tag]) { 37 | return OAS3_REF_MAP[tag] 38 | } 39 | } else if( SWAGGER2_REF_MAP[tag] ) { 40 | return SWAGGER2_REF_MAP[tag] 41 | } 42 | } 43 | return null 44 | })(state) 45 | 46 | export const localRefs = (state) => (sys) => createSelector( 47 | sys.specSelectors.spec, 48 | sys.specSelectors.isOAS3 || (() => false), 49 | (spec, isOAS3) => { 50 | return (isOAS3 ? OAS3_TYPES : SWAGGER2_TYPES).toList().flatMap( type => { 51 | return spec 52 | .getIn(type.split("/"), Map({})) 53 | .keySeq() 54 | .map( name => Map({ 55 | name, 56 | type, 57 | $ref: `#/${type}/${escapeJsonPointerToken(name)}`, 58 | })) 59 | }) 60 | } 61 | )(state) 62 | -------------------------------------------------------------------------------- /src/plugins/editor-metadata/index.js: -------------------------------------------------------------------------------- 1 | export default function(system) { 2 | return { 3 | rootInjects: { 4 | getEditorMetadata() { 5 | const allErrors = system.errSelectors.allErrors() 6 | return { 7 | contentString: system.specSelectors.specStr(), 8 | contentObject: system.specSelectors.specJson().toJS(), 9 | isValid: allErrors.size === 0, 10 | errors: allErrors.toJS() 11 | } 12 | } 13 | } 14 | } 15 | } -------------------------------------------------------------------------------- /src/plugins/editor/actions.js: -------------------------------------------------------------------------------- 1 | export const JUMP_TO_LINE = "jump_to_line" 2 | 3 | export function jumpToLine(line) { 4 | return { 5 | type: JUMP_TO_LINE, 6 | payload: line 7 | } 8 | 9 | } 10 | 11 | // This is a hook. Will have editor instance 12 | // It needs to be an async-function, to avoid dispatching an object to the reducer 13 | export const onLoad = () => () => {} 14 | -------------------------------------------------------------------------------- /src/plugins/editor/components/brace-snippets-yaml.js: -------------------------------------------------------------------------------- 1 | /* global ace */ 2 | ace.define("ace/snippets/yaml", 3 | ["require","exports","module"], function(e,t,n){ // eslint-disable-line no-unused-vars 4 | t.snippetText=undefined 5 | t.scope="yaml" 6 | }) 7 | -------------------------------------------------------------------------------- /src/plugins/editor/components/editor-container.jsx: -------------------------------------------------------------------------------- 1 | import React from "react" 2 | import PropTypes from "prop-types" 3 | 4 | export default class EditorContainer extends React.Component { 5 | 6 | // This is already debounced by editor.jsx 7 | onChange = (value) => { 8 | this.props.onChange(value) 9 | } 10 | 11 | render() { 12 | let { specSelectors, getComponent, errSelectors, fn, editorSelectors, configsSelectors } = this.props 13 | 14 | let Editor = getComponent("Editor") 15 | 16 | let wrapperClasses = ["editor-wrapper"] 17 | const readOnly = !!configsSelectors.get("readOnly") 18 | 19 | if(readOnly) { 20 | wrapperClasses.push("read-only") 21 | } 22 | 23 | let propsForEditor = this.props 24 | 25 | const editorOptions = { 26 | enableLiveAutocompletion: configsSelectors.get("editorLiveAutocomplete"), 27 | readOnly: readOnly, 28 | highlightActiveLine: !readOnly, 29 | highlightGutterLine: !readOnly, 30 | } 31 | 32 | return ( 33 |
34 | { readOnly ?

Read Only

: null } 35 | 46 |
47 | ) 48 | } 49 | 50 | } 51 | 52 | EditorContainer.defaultProps = { 53 | onChange: Function.prototype 54 | } 55 | 56 | EditorContainer.propTypes = { 57 | specActions: PropTypes.object.isRequired, 58 | configsSelectors: PropTypes.object.isRequired, 59 | onChange: PropTypes.func, 60 | fn: PropTypes.object, 61 | specSelectors: PropTypes.object.isRequired, 62 | errSelectors: PropTypes.object.isRequired, 63 | editorSelectors: PropTypes.object.isRequired, 64 | getComponent: PropTypes.func.isRequired, 65 | } 66 | -------------------------------------------------------------------------------- /src/plugins/editor/editor-helpers/marker-placer.js: -------------------------------------------------------------------------------- 1 | // This code is registered as a helper, not a plugin, because its lifecycle is 2 | // unique to the needs of the marker placement logic. 3 | 4 | import countBy from "lodash/countBy" 5 | import map from "lodash/map" 6 | 7 | let removers = [] 8 | 9 | function setRemovers(arr) { 10 | removers.forEach(fn => fn()) // remove existing anchors & gutters 11 | removers = arr // use parent scope to persist reference 12 | } 13 | 14 | export function placeMarkerDecorations({editor, markers, onMarkerLineUpdate}) { 15 | 16 | if(typeof editor !== "object") { 17 | return 18 | } 19 | 20 | let markerLines = countBy(Object.values(markers), "position") 21 | 22 | let removeFns = map(markerLines, (count, line) => { 23 | let className = `editor-marker-${count > 8 ? "9-plus" : count}` 24 | let s = editor.getSession() 25 | let anchor = s.getDocument().createAnchor(+line, 0) 26 | 27 | anchor.setPosition(+line, 0) // noClip = true 28 | s.addGutterDecoration(+line, className) 29 | anchor.on("change", function (e) { 30 | var oldLine = e.old.row 31 | var newLine = e.value.row 32 | 33 | s.removeGutterDecoration(oldLine, className) 34 | s.addGutterDecoration(newLine, className) 35 | onMarkerLineUpdate([oldLine, newLine, line]) 36 | }) 37 | 38 | return function () { 39 | // // Remove the anchor & decoration 40 | let currentLine = +anchor.getPosition().row 41 | editor.getSession().removeGutterDecoration(currentLine, className) 42 | anchor.detach() 43 | } 44 | }) 45 | 46 | setRemovers(removeFns) 47 | 48 | // To manually remove them 49 | return () => setRemovers([]) 50 | 51 | } 52 | -------------------------------------------------------------------------------- /src/plugins/editor/editor-plugins/gutter-click.js: -------------------------------------------------------------------------------- 1 | import isFunction from "lodash/isFunction" 2 | 3 | export default function(editor, { onGutterClick }) { 4 | editor.on("guttermousedown", (e) => { 5 | let editor = e.editor 6 | let line = e.getDocumentPosition().row 7 | let region = editor.renderer.$gutterLayer.getRegion(e) 8 | 9 | e.stop() 10 | 11 | if(isFunction(onGutterClick)) { 12 | onGutterClick({ region, line }) 13 | } 14 | 15 | }) 16 | } 17 | -------------------------------------------------------------------------------- /src/plugins/editor/editor-plugins/hook.js: -------------------------------------------------------------------------------- 1 | // TODO: Turn these into actions, that we can override 2 | import GutterClick from "./gutter-click" 3 | import JsonToYaml from "./json-to-yaml" 4 | import TabHandler from "./tab-handler" 5 | 6 | const plugins = [ 7 | {fn: GutterClick, name: "gutterClick"}, 8 | {fn: JsonToYaml, name: "jsonToYaml"}, 9 | {fn: TabHandler, name: "tabHandler"}, 10 | ] 11 | 12 | export default function (editor, props = {}, editorPluginsToRun = [], helpers = {}) { 13 | plugins 14 | .filter(plugin => ~editorPluginsToRun.indexOf(plugin.name)) 15 | .forEach( plugin => { 16 | try { 17 | plugin.fn(editor, props, helpers) 18 | } catch(e) { 19 | console.error(`${plugin.name || ""} plugin error:`, e) 20 | } 21 | }) 22 | } 23 | -------------------------------------------------------------------------------- /src/plugins/editor/editor-plugins/json-to-yaml.js: -------------------------------------------------------------------------------- 1 | import YAML from "js-yaml" 2 | 3 | export default function(editor) { 4 | editor.on("paste", e => { 5 | const originalStr = e.text 6 | if (!isJSON(originalStr)) { 7 | return 8 | } 9 | 10 | let yamlString 11 | try { 12 | yamlString = YAML.dump(YAML.load(originalStr), { 13 | lineWidth: -1 // don't generate line folds 14 | }) 15 | } catch (e) { 16 | return 17 | } 18 | 19 | if (!confirm("Would you like to convert your JSON into YAML?")) { 20 | return 21 | } 22 | 23 | // using SelectionRange instead of CursorPosition, because: 24 | // SR.start|end === CP when there's no selection 25 | // and it catches indentation edge cases when there is one 26 | const padding = makePadding(editor.getSelectionRange().start.column) 27 | 28 | // update the pasted content 29 | e.text = yamlString 30 | .split("\n") 31 | .map((line, i) => i == 0 ? line : padding + line) // don't pad first line, it's already indented 32 | .join("\n") 33 | .replace(/\t/g, " ") // tabs -> spaces, just to be sure 34 | }) 35 | } 36 | 37 | function isJSON (str){ 38 | // basic test: "does this look like JSON?" 39 | let regex = /^[ \r\n\t]*[{[]/ 40 | 41 | return regex.test(str) 42 | 43 | } 44 | 45 | function makePadding(len) { 46 | let str = "" 47 | 48 | while(str.length < len) { 49 | str += " " 50 | } 51 | 52 | return str 53 | } 54 | -------------------------------------------------------------------------------- /src/plugins/editor/editor-plugins/tab-handler.js: -------------------------------------------------------------------------------- 1 | export default function(editor) { 2 | // NOTE: react-ace has an onPaste prop.. we could refactor to that. 3 | editor.on("paste", e => { 4 | // replace all U+0009 tabs in pasted string with two spaces 5 | e.text = e.text.replace(/\t/g, " ") 6 | }) 7 | } 8 | -------------------------------------------------------------------------------- /src/plugins/editor/index.js: -------------------------------------------------------------------------------- 1 | import makeEditor from "./components/editor" 2 | import EditorContainer from "./components/editor-container" 3 | import * as actions from "./actions" 4 | import reducers from "./reducers" 5 | import * as selectors from "./selectors" 6 | import EditorSpecPlugin from "./spec" 7 | 8 | let Editor = makeEditor({ 9 | editorPluginsToRun: ["gutterClick", "jsonToYaml", "pasteHandler"] 10 | }) 11 | 12 | export default function () { 13 | return [EditorSpecPlugin, { 14 | components: { Editor, EditorContainer }, 15 | statePlugins: { 16 | editor: { 17 | reducers, 18 | actions, 19 | selectors 20 | } 21 | } 22 | }] 23 | } 24 | -------------------------------------------------------------------------------- /src/plugins/editor/reducers.js: -------------------------------------------------------------------------------- 1 | import { 2 | JUMP_TO_LINE 3 | } from "./actions" 4 | 5 | export default { 6 | [JUMP_TO_LINE]: (state, { payload } ) =>{ 7 | return state.set("gotoLine", { line: payload }) 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /src/plugins/editor/selectors.js: -------------------------------------------------------------------------------- 1 | import { createSelector } from "reselect" 2 | import Im from "immutable" 3 | 4 | const state = state => { 5 | return state || Im.Map() 6 | } 7 | 8 | export const gotoLine = createSelector( 9 | state, 10 | state => { 11 | return state.get("gotoLine") || null 12 | } 13 | ) 14 | -------------------------------------------------------------------------------- /src/plugins/editor/spec.js: -------------------------------------------------------------------------------- 1 | const SPEC_UPDATE_ORIGIN = "spec_update_spec_origin" 2 | 3 | // wraps updateSpec to include the "origin" parameter, defaulting to "not-editor" 4 | // Includes a selector to get the origin, specSelectors.specOrigin 5 | export default function EditorSpecPlugin() { 6 | return { 7 | statePlugins: { 8 | spec: { 9 | wrapActions: { 10 | updateSpec: (ori, system) => (specStr, origin) => { 11 | system.specActions.updateSpecOrigin(origin) 12 | ori(specStr) 13 | } 14 | }, 15 | reducers: { 16 | [SPEC_UPDATE_ORIGIN]: (state, action) => { 17 | return state.set("specOrigin", action.payload) 18 | } 19 | }, 20 | selectors: { 21 | specOrigin: (state) => state.get("specOrigin") || "not-editor" 22 | }, 23 | actions: { 24 | updateSpecOrigin(origin="not-editor") { 25 | return { 26 | payload: origin+"", 27 | type: SPEC_UPDATE_ORIGIN, 28 | } 29 | } 30 | } 31 | } 32 | } 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/plugins/json-schema-validator/SCHEMA_CONSTRUCTS.md: -------------------------------------------------------------------------------- 1 | A guide to custom patterns used in the JSON Schema validation documents in this folder... 2 | 3 | ### Pivot key existential switch 4 | 5 | Applies a schema to an object based on whether a key exists in an object. 6 | 7 | > "If key A exists on the object, apply schema X. Else, apply schema Y." 8 | 9 | ```yaml 10 | switch: 11 | - if: 12 | required: [a] 13 | then: 14 | description: schema X; within `then` can be any JSON Schema content 15 | - then: 16 | description: schema Y; within `then` can be any JSON Schema content 17 | ``` 18 | 19 | ### Pivot key value switch 20 | 21 | Applies a schema to an object based on the value of a specific, always-required key (the "pivot key"). 22 | 23 | > "If key A is foo, apply schema X. Else, if key A is bar, apply schema Y. Else, tell the user that key A must be foo or bar." 24 | 25 | - The pivot key must be `required` in each `if` block, otherwise the switch may generate a false positive for the entire object when the key isn't provided at all. 26 | - The default case (the last one, with `then` but no `if`) must always require the pivot key's presence and report all possible values back as an enum, otherwise a misleading error message may be shown to the user. 27 | 28 | ```yaml 29 | switch: 30 | - if: 31 | required: [a] 32 | properties: { a: { enum: [foo] } } 33 | then: 34 | description: schema X; within `then` can be any JSON Schema content 35 | - if: 36 | required: [a] 37 | properties: { a: { enum: [bar] } } 38 | then: 39 | description: schema Y; within `then` can be any JSON Schema content 40 | - then: 41 | description: fallback schema; ensures the user is told the pivot key is needed and should have one of the enumerated values 42 | required: [a] 43 | properties: { a: { enum: [foo, bar] } } 44 | ``` -------------------------------------------------------------------------------- /src/plugins/json-schema-validator/validator.worker.js: -------------------------------------------------------------------------------- 1 | import registerPromiseWorker from "promise-worker/register" 2 | import Validator from "./validator" 3 | 4 | const validator = new Validator() 5 | 6 | registerPromiseWorker(({ type, payload }) => { 7 | if (type == "add-schema") { 8 | const { schema, schemaPath } = payload 9 | validator.addSchema(schema, schemaPath) 10 | return 11 | } 12 | 13 | if (type == "validate") { 14 | const { jsSpec, specStr, schemaPath, source } = payload 15 | let validationResults = validator.validate({ 16 | jsSpec, 17 | specStr, 18 | schemaPath, 19 | source 20 | }) 21 | 22 | return { results: validationResults } 23 | } 24 | }) 25 | -------------------------------------------------------------------------------- /src/plugins/json-schema-validator/validator/path-translator.js: -------------------------------------------------------------------------------- 1 | import get from "lodash/get" 2 | 3 | export function transformPathToArray(property, jsSpec) { 4 | if (property.slice(0, 9) === "instance.") { 5 | var str = property.slice(9) 6 | } else { 7 | // eslint-disable-next-line no-redeclare 8 | var str = property 9 | } 10 | 11 | var pathArr = [] 12 | 13 | // replace '.', '["', '"]' separators with pipes 14 | str = str.replace(/\.(?![^["]*"])|(\[")|("]\.?)/g, "|") 15 | 16 | // handle single quotes as well 17 | str = str.replace(/\['/g, "|") 18 | str = str.replace(/']/g, "|") 19 | 20 | // split on our new delimiter, pipe 21 | str = str.split("|") 22 | 23 | str 24 | .map(item => { 25 | // "key[0]" becomes ["key", "0"] 26 | if (item.indexOf("[") > -1) { 27 | let index = parseInt(item.match(/\[(.*)\]/)[1]) 28 | let keyName = item.slice(0, item.indexOf("[")) 29 | return [keyName, index.toString()] 30 | } else { 31 | return item 32 | } 33 | }) 34 | .reduce(function(a, b) { 35 | // flatten! 36 | return a.concat(b) 37 | }, []) 38 | .concat([""]) // add an empty item into the array, so we don't get stuck with something in our buffer below 39 | .reduce((buffer, curr) => { 40 | let obj = pathArr.length ? get(jsSpec, pathArr) : jsSpec 41 | 42 | if (get(obj, makeAccessArray(buffer, curr))) { 43 | if (buffer.length) { 44 | pathArr.push(buffer) 45 | } 46 | if (curr.length) { 47 | pathArr.push(curr) 48 | } 49 | return "" 50 | } else { 51 | // attach key to buffer 52 | return `${buffer}${buffer.length ? "." : ""}${curr}` 53 | } 54 | }, "") 55 | 56 | if (typeof get(jsSpec, pathArr) !== "undefined") { 57 | return pathArr 58 | } else { 59 | // if our path is not correct (there is no value at the path), 60 | // return null 61 | return null 62 | } 63 | } 64 | 65 | function makeAccessArray(buffer, curr) { 66 | let arr = [] 67 | 68 | if (buffer.length) { 69 | arr.push(buffer) 70 | } 71 | 72 | if (curr.length) { 73 | arr.push(curr) 74 | } 75 | 76 | return arr 77 | } 78 | -------------------------------------------------------------------------------- /src/plugins/json-schema-validator/validator/shared.js: -------------------------------------------------------------------------------- 1 | // export * from './ast.js' 2 | // These import/exports are shared code between worker and main bundle. 3 | // Putting them here keeps the distinction clear 4 | export { getLineNumberForPath } from "../../ast/ast.js" 5 | -------------------------------------------------------------------------------- /src/plugins/jump-to-path/index.js: -------------------------------------------------------------------------------- 1 | import spec from "./spec" 2 | import JumpToPath from "./jump-to-path" 3 | 4 | export default function JumpToPathPlugin() { 5 | return [ 6 | spec, 7 | { 8 | components: { 9 | JumpToPath 10 | }, 11 | } 12 | ] 13 | } 14 | -------------------------------------------------------------------------------- /src/plugins/jump-to-path/jump-icon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/plugins/jump-to-path/jump-to-path.jsx: -------------------------------------------------------------------------------- 1 | import React from "react" 2 | import PropTypes from "prop-types" 3 | 4 | import JumpIcon from "./jump-icon.svg" 5 | 6 | class JumpToPath extends React.Component { 7 | static propTypes = { 8 | editorActions: PropTypes.object.isRequired, 9 | specSelectors: PropTypes.object.isRequired, 10 | fn: PropTypes.object.isRequired, 11 | path: PropTypes.oneOfType([ 12 | PropTypes.array, 13 | PropTypes.string 14 | ]), 15 | content: PropTypes.element, 16 | showButton: PropTypes.bool, 17 | specPath: PropTypes.array, // The location within the spec. Used as a fallback if `path` doesn't exist 18 | } 19 | 20 | static defaultProps = { 21 | path: "", 22 | } 23 | 24 | shouldComponentUpdate(nextProps) { 25 | let { shallowEqualKeys } = nextProps.fn 26 | return shallowEqualKeys(this.props, nextProps, [ 27 | "content", "showButton", "path", "specPath" 28 | ]) 29 | } 30 | 31 | jumpToPath = (e) => { 32 | e.stopPropagation() 33 | 34 | const { 35 | specPath=[], 36 | path, 37 | specSelectors, 38 | editorActions 39 | } = this.props 40 | 41 | const jumpPath = specSelectors.bestJumpPath({path, specPath}) 42 | editorActions.jumpToLine(specSelectors.getSpecLineFromPath(jumpPath)) 43 | } 44 | 45 | 46 | defaultJumpContent = 47 | 48 | render() { 49 | let { content, showButton } = this.props 50 | 51 | if (content) { 52 | // if we were given content to render, wrap it 53 | return ( 54 | 55 | { showButton ? this.defaultJumpContent : null } 56 | {content} 57 | 58 | ) 59 | } else { 60 | // just render a link 61 | return this.defaultJumpContent 62 | 63 | } 64 | } 65 | } 66 | 67 | export default JumpToPath 68 | -------------------------------------------------------------------------------- /src/plugins/local-storage/index.js: -------------------------------------------------------------------------------- 1 | import { petStoreOas3Def } from "../default-definitions" 2 | const CONTENT_KEY = "swagger-editor-content" 3 | 4 | let localStorage = window.localStorage 5 | 6 | export const updateSpec = (ori) => (...args) => { 7 | let [spec] = args 8 | ori(...args) 9 | saveContentToStorage(spec) 10 | } 11 | 12 | export default function(system) { 13 | // setTimeout runs on the next tick 14 | setTimeout(() => { 15 | if(localStorage.getItem(CONTENT_KEY)) { 16 | system.specActions.updateSpec(localStorage.getItem(CONTENT_KEY), "local-storage") 17 | } else if(localStorage.getItem("ngStorage-SwaggerEditorCache")) { 18 | // Legacy migration for swagger-editor 2.x 19 | try { 20 | let obj = JSON.parse(localStorage.getItem("ngStorage-SwaggerEditorCache")) 21 | let yaml = obj.yaml 22 | system.specActions.updateSpec(yaml) 23 | saveContentToStorage(yaml) 24 | localStorage.setItem("ngStorage-SwaggerEditorCache", null) 25 | } catch(e) { 26 | system.specActions.updateSpec(petStoreOas3Def) 27 | } 28 | } else { 29 | system.specActions.updateSpec(petStoreOas3Def) 30 | } 31 | }, 0) 32 | return { 33 | statePlugins: { 34 | spec: { 35 | wrapActions: { 36 | updateSpec 37 | } 38 | } 39 | } 40 | } 41 | } 42 | 43 | function saveContentToStorage(str) { 44 | return localStorage.setItem(CONTENT_KEY, str) 45 | } 46 | -------------------------------------------------------------------------------- /src/plugins/performance/index.js: -------------------------------------------------------------------------------- 1 | const getTimestamp = ((that) => { 2 | if(that.performance && that.performance.now) { 3 | return that.performance.now.bind(that.performance) 4 | } 5 | return Date.now.bind(Date) 6 | })(self || window) 7 | 8 | export default function PerformancePlugin() { 9 | if(!(window || {}).LOG_PERF) { 10 | return { 11 | fn: { 12 | getTimestamp, 13 | Timer: TimerStub, 14 | timeCall: (name,fn) => fn(), 15 | } 16 | } 17 | } 18 | 19 | return { 20 | fn: { 21 | getTimestamp, 22 | Timer, 23 | timeCall, 24 | } 25 | } 26 | } 27 | 28 | function timeCall(name,fn) { 29 | fn = fn || name 30 | name = typeof name === "function" ? "that" : name 31 | const a = getTimestamp() 32 | const r = fn() 33 | const b = getTimestamp() 34 | console.log(name,"took", b - a, "ms") // eslint-disable-line no-console 35 | return r 36 | } 37 | 38 | function TimerStub() { 39 | this.start = this.mark = this.print = Function.prototype 40 | } 41 | 42 | function Timer(name, _getTimestamp=getTimestamp) { 43 | this._name = name 44 | this.getTimestamp = _getTimestamp 45 | this._markers = [] 46 | this.start() 47 | } 48 | 49 | Timer.prototype.start = function() { 50 | this._start = this.getTimestamp() 51 | } 52 | 53 | Timer.prototype.mark = function(name) { 54 | this._markers = this._markers || [] 55 | this._markers.push({ 56 | time: this.getTimestamp(), 57 | name 58 | }) 59 | } 60 | 61 | Timer.prototype.print = function(name) { 62 | this.mark(name) 63 | this._markers.forEach(m => { 64 | // eslint-disable-next-line no-console 65 | console.log(this._name, m.name, m.time - this._start, "ms") 66 | }) 67 | this._markers = [] 68 | this.start() 69 | } 70 | -------------------------------------------------------------------------------- /src/plugins/refs-util.js: -------------------------------------------------------------------------------- 1 | import qs from "querystring-browser" 2 | 3 | /** 4 | * Unescapes a JSON pointer. 5 | * @api public 6 | */ 7 | export function unescapeJsonPointerToken(token) { 8 | if (typeof token !== "string") { 9 | return token 10 | } 11 | return qs.unescape(token.replace(/~1/g, "/").replace(/~0/g, "~")) 12 | } 13 | 14 | /** 15 | * Escapes a JSON pointer. 16 | * @api public 17 | */ 18 | export function escapeJsonPointerToken(token) { 19 | return qs.escape(token.replace(/~/g, "~0").replace(/\//g, "~1")) 20 | } 21 | -------------------------------------------------------------------------------- /src/plugins/split-pane-mode/components/split-pane-mode.jsx: -------------------------------------------------------------------------------- 1 | import React from "react" 2 | import PropTypes from "prop-types" 3 | import SplitPane from "react-split-pane" 4 | 5 | const MODE_KEY = ["split-pane-mode"] 6 | const MODE_LEFT = "left" 7 | const MODE_RIGHT = "right" 8 | const MODE_BOTH = "both" // or anything other than left/right 9 | 10 | export default class SplitPaneMode extends React.Component { 11 | 12 | static propTypes = { 13 | threshold: PropTypes.number, 14 | 15 | children: PropTypes.array, 16 | 17 | layoutSelectors: PropTypes.object.isRequired, 18 | layoutActions: PropTypes.object.isRequired, 19 | } 20 | 21 | static defaultProps = { 22 | threshold: 100, // in pixels 23 | children: [], 24 | } 25 | 26 | initializeComponent = (c) => { 27 | this.splitPane = c 28 | } 29 | 30 | onDragFinished = () => { 31 | let { threshold, layoutActions } = this.props 32 | let { position, draggedSize } = this.splitPane.state 33 | this.draggedSize = draggedSize 34 | 35 | let nearLeftEdge = position <= threshold 36 | let nearRightEdge = draggedSize <= threshold 37 | 38 | layoutActions 39 | .changeMode(MODE_KEY, ( 40 | nearLeftEdge 41 | ? MODE_RIGHT : nearRightEdge 42 | ? MODE_LEFT : MODE_BOTH 43 | )) 44 | } 45 | 46 | sizeFromMode = (mode, defaultSize) => { 47 | if(mode === MODE_LEFT) { 48 | this.draggedSize = null 49 | return "0px" 50 | } else if (mode === MODE_RIGHT) { 51 | this.draggedSize = null 52 | return "100%" 53 | } 54 | // mode === "both" 55 | return this.draggedSize || defaultSize 56 | } 57 | 58 | render() { 59 | let { children, layoutSelectors } = this.props 60 | 61 | const mode = layoutSelectors.whatMode(MODE_KEY) 62 | const left = mode === MODE_RIGHT ?