diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000000000000000000000000000000000000..651d0b21a05c3d8f1f8903cdcd807137aaae32c6 --- /dev/null +++ b/.gitignore @@ -0,0 +1,38 @@ +# See https://help.github.com/articles/ignoring-files for more about ignoring files. +# +# If you find yourself ignoring temporary files generated by your text editor +# or operating system, you probably want to add a global ignore instead: +# git config --global core.excludesfile '~/.gitignore_global' + +# Ignore bundler config. +/scripts/.bundle + +# Ignore all logfiles and tempfiles. +/scripts/log/* +/scripts/tmp/* + +.byebug_history +.DS_Store +.env +.idea +/scripts/public/packs-test +node_modules +.rspec_status +.vscode +/scripts/public/packs +/scripts/public/packs-test +yarn-debug.log* +.yarn-integrity +yarn-error.log +tags +.zshrc +.solargraph.yml +.generators +.rakeTasks +/scripts/coverage +/scritps/public/assets/ +!/scripts/public/assets/images +log.txt +sidekiq.log +#Dockerfile.packages +#repackage.sh diff --git a/.pairs b/.pairs new file mode 100644 index 0000000000000000000000000000000000000000..8add4786b9206de18e055a534c8c38edb6cdd2c9 --- /dev/null +++ b/.pairs @@ -0,0 +1,15 @@ +authors: + jj: Jeremy Jackson; jejacks0n + mb: Martha Berner; martha + pg: Peter Grunde; peter.a.grunde + dl: Derik Linch; derik.linch + cl: Chad Lillquist; chad.lillquist + jh: Joel Hawksley; joel + +email: + domain: galvanize.com + +email_addresses: + jj: jejacks0n@gmail.com + pg: peter.a.grunde@gmail.com + jh: joel@hawksley.org diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000000000000000000000000000000000000..3a3ea20b13dddcc8a9b4212cfd93d1ac468c93de --- /dev/null +++ b/Dockerfile @@ -0,0 +1,77 @@ +FROM ${BASE_REGISTRY}/${BASE_IMAGE}:${BASE_TAG} +USER 0 + +# Install required libs. +RUN dnf update -y && dnf install -y \ + curl \ + make \ + patch \ + git \ + zlib-devel \ + libpq-devel \ + libxml2-devel \ + libxslt-devel \ + gcc \ + gcc-c++ + +# Setup our environment +WORKDIR /app +COPY ./scripts . + +# Switch to the binary directory. +WORKDIR /app/bundles + +# Redis. +RUN tar xzf redis-cli.tar.gz +RUN mv redis-cli /usr/local/bin/redis-cli + +# Unzip the bundle directory. +RUN tar xzf bundle.tar.gz +RUN mv bundle /usr/local/ + +# Unzip the node_modules. +RUN tar xzf node_modules.tar.gz +RUN mv node_modules /app/node_modules + +# Node.js +RUN tar xzf nodejs.tar.gz +RUN mv nodejs /app/nodejs + +# Yarn +RUN tar xzf yarn.tar.gz +RUN mv yarn /app/yarn + +# Switch back to app directory. +WORKDIR /app + +# Remove the bundles directory. +RUN rm -rf bundles + +# Add write permissions. +RUN chown -R 1001 . + +# Become the ruby user +USER 1001 + +# Set the Rails and node environment variable. +ENV RAILS_ENV production +ENV NODE_HOME /app/nodejs +ENV PATH /app/node_modules/.bin:$PATH +ENV PATH /app/yarn/bin:$PATH +ENV PATH /app/nodejs/bin:$PATH + +# Precompile assets +RUN bundle exec rake assets:precompile + +# Asset Clean +RUN bundle exec rake assets:clean + +# Remove yarn and node modules after compile. +RUN rm -rf yarn +RUN rm -rf node_modules + +# Health check. +HEALTHCHECK none + +# Set the entry point. +ENTRYPOINT ["/app/entrypoint-server.sh"] diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000000000000000000000000000000000000..6b1e57834be16f9f5608e3226dc08b2ecfc20c2f --- /dev/null +++ b/LICENSE @@ -0,0 +1,27 @@ +Copyright 2021. Galvanize Inc. Galvanize Inc. proprietary and confidential. Do not distribute. + +A commercial license will be required to run an instance (or any copy or derivative thereof) of this software and the +learning management system (the "Software"). Purchase of a license will be facilitated by contacting +legal@galvanize.com. Use of the Software is prohibited without a commercial license. By way of example, but not +limitation, without a commercial license, you may not use the Software in any way or at any time, either directly or +indirectly, including but not limited to, you may not at any time, attempt, directly or indirectly, or allow any third +party to attempt to (i) copy, modify, or create derivative works of the Software, in whole or in part; (ii) use, rent, +lease, lend, sell, license, sublicense, assign, distribute, publish, transfer, or otherwise make available the Software; +(iii) reverse engineer, disassemble, decompile, decode, adapt, or otherwise attempt to derive or gain access to any +software component of the Software, in whole or in part; (iv) remove any proprietary notices from the Software; +or (v) use the Software in any manner or for any purpose that infringes, misappropriates, or otherwise violates any +intellectual property right or other right of any person, or that violates any applicable law. THE SOFTWARE IS PROVIDED +"AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL GALVANIZE, THE AUTHORS OR +COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR +OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +FURTHER, IN NO EVENT WILL GALVANIZE, THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE UNDER OR IN CONNECTION WITH THIS +AGREEMENT UNDER ANY LEGAL OR EQUITABLE THEORY, INCLUDING BREACH OF CONTRACT, TORT (INCLUDING NEGLIGENCE), STRICT +LIABILITY, AND OTHERWISE, FOR ANY: (a) CONSEQUENTIAL, INCIDENTAL, INDIRECT, EXEMPLARY, SPECIAL, ENHANCED, OR PUNITIVE +DAMAGES; (b) INCREASED COSTS, DIMINUTION IN VALUE OR LOST BUSINESS, PRODUCTION, REVENUES, OR PROFITS; (c) LOSS OF +GOODWILL OR REPUTATION; (d) USE, INABILITY TO USE, LOSS, INTERRUPTION, DELAY OR RECOVERY OF ANY DATA, OR BREACH OF +DATA OR SYSTEM SECURITY; (e) COST OF REPLACEMENT GOODS OR SERVICES; OR (f) ANY DIRECT DAMAGES, INCLUDING ANY LIABILITY +ARISING OUT OF OR RELATED TO THIS AGREEMENT UNDER ANY LEGAL OR EQUITABLE THEORY, INCLUDING BREACH OF CONTRACT, +TORT (INCLUDING NEGLIGENCE), OR STRICT LIABILITY, IN EACH CASE REGARDLESS OF WHETHER GALVANIZE, THE AUTHORS OR +COPYRIGHT HOLDERS WAS ADVISED OF THE POSSIBILITY OF SUCH LOSSES OR DAMAGES OR SUCH LOSSES OR DAMAGES WERE OTHERWISE +FORESEEABLE. \ No newline at end of file diff --git a/README.md b/README.md index 288603e47a160909b3cc3e8b75fd4395800bf7d6..9098182b134d8ff6c9856fe4a54171964da6730a 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,109 @@ -# forge +Forge +===== -forge \ No newline at end of file +## Standards +See the [software-team-standards](https://www.github.com/Galvanize-IT/software-team-standards) for git aliases. + +## Dependency Setup + +[install Homebrew](http://brew.sh/) if you don't already have it. + +Install rbenv and get the right version of ruby (the version here may change, so check the Gemfile for the correct +version. ) + +- `brew install rbenv` +- `rbenv install 2.6.6` + +Install our data stores: + +- `brew install postgresql` +- `brew install redis` +- `brew services start redis` + +To simplify your postgres installation, once you've installed it with homebrew, it's recommended that you use the +[Postgres.app](http://postgresapp.com/), which is an easy download and install. This allows you to not need to specify a +username/password in your `database.yml`. + + +## Installation + +You will need a [Github access token](https://github.com/settings/tokens). Replace the `YOUR_GENERATED_PERSONAL_ACCESS_TOKEN` with your token. + +The token will need **Full control of private repositories** + +- `git clone git@github.com:Galvanize-IT/forge.git && cd forge` +- `rbenv local 2.6.6` +- `gem install bundler` +- `bundle config --local GITHUB__COM YOUR_GENERATED_PERSONAL_ACCESS_TOKEN:x-oauth-basic` +- `bundle` +- `yarn install` + +## Data + +- `rake db:create` +- `rake db:migrate` + +## Auth + +Set up [Auth](https://github.com/Galvanize-IT/auth) before running Forge. + +## Configuration + +Ask a co-developer for their .env example. + +### Sidekiq + +As an admin, you can view Sidekiq jobs at `/admin/sidekiq`. + +## Starting the server + +`heroku local -p 3003` + +or: +`rails s -p 3003` +`sidekiq` +`bin/webpack-dev-server` + +## Deployment + +PRs are automatically deployed to their own one-off environments. To test webhook-related Auth features, be sure to update the webhooks for Forge at `auth-staging.herokuapp.com` to point to your review enviroment. + +Sign in to your review environment using your credentials for `auth-staging.herokuapp.com`, or `dev+auth@galvanize.com:password`. + +`master` is automatically deployed to `staging` once CI passes. + +Migrations are run as part of the deployment release phase. + +Note: `galvanize-forge` will need to have access to any repos used for project challenges. + +## Mailer previews + +To preview our mailers, go to `http://localhost:3003/rails/mailers`. + +## End-to-end Tests (e2e) + +The [Forge e2e suite](https://github.com/Galvanize-IT/forge-e2e) is run on every deploy to staging, and the results are posted to `#forge_devops`. + +## Code Generators + +The `ts_routes` gem generates typesafe routes for use in TypeScript. To update them: + +`rake ts:routes` + +### API Documentation + +The specs for the API use a specific DSL that allows the tests to generate our API documentation. You can then run the +integration specs and generate the documentation. The documentation can be committed if you’ve made changes to the API, +otherwise they can be disregard. + +Run `rails spec:api:docs` to generate the documentation files and then browse to `/api/docs` to see what the +documentation looks like. + +## Linters + +We use `rubocop` and `eslint`. + +You can use overcommit to automatically run our linters in a pre-commit hook: (`gem install overcommit; overcommit --install`). +Note: If you want to disable it inline (i.e. for a WIP commit), use `OVERCOMMIT_DISABLE=1 git commit -m "wip"`. + +You can also run the linters manually with auto-correction, along with the test suite, by running `bash lintspec.sh`. diff --git a/hardening_manifest.yaml b/hardening_manifest.yaml new file mode 100644 index 0000000000000000000000000000000000000000..87f83e3bcfc6ea0a330919e0cc8eeb85dc045bd5 --- /dev/null +++ b/hardening_manifest.yaml @@ -0,0 +1,44 @@ +--- +apiVersion: v1 + +# The repository name in registry1, excluding /ironbank/ +name: "galvanize/galvanize/forge" + +# List of tags to push for the repository in registry1 +# The most specific version should be the first tag and will be shown +# on ironbank.dsop.io +tags: +- "0.1.0" +- "latest" + +# Build args passed to Dockerfile ARGs +args: + BASE_IMAGE: "opensource/ruby/ruby26" + BASE_TAG: "2.6.6" + +# Docker image labels +labels: + org.opencontainers.image.title: "forge" + org.opencontainers.image.description: "Galvanize is a technology ecosystem for learners, entrepreneurs, startups, and established companies that meets the needs of the rapidly changing digital world. Our goal is to transform individuals and teams through effective education and community programs Delivering exceptional outcomes Coalescing and nurturing a community of innovators anchored by our campuses" + org.opencontainers.image.licenses: "proprietary" + org.opencontainers.image.url: "https://www.galvanize.com" + org.opencontainers.image.vendor: "Galvanize" + org.opencontainers.image.version: "0.1.0" + mil.dso.ironbank.image.keywords: "learn,lms,galvanize,online,remote,school,learning" + mil.dso.ironbank.image.type: "commercial" + mil.dso.ironbank.product.name: "Learn" + +# List of project maintainers +maintainers: +- email: "dchong@revacomm.com" + name: "Derrin Chong" + username: "dchong" +- email: "muranaka@revacomm.com" + name: "Michael Uranaka" + username: "muranaka" +- email: "csakamaki@revacomm.com" + name: "Charlie Sakamaki" + username: "csakamaki" +- name: "Joshua Eason" + username: "jeason" + cht_member: true diff --git a/scripts/.browserslistrc b/scripts/.browserslistrc new file mode 100644 index 0000000000000000000000000000000000000000..e94f8140cc002ea77aeda3dae01314d9c3d491a9 --- /dev/null +++ b/scripts/.browserslistrc @@ -0,0 +1 @@ +defaults diff --git a/scripts/.env.example b/scripts/.env.example new file mode 100644 index 0000000000000000000000000000000000000000..775936082f46344572250e00545f1eb5aee4a0c2 --- /dev/null +++ b/scripts/.env.example @@ -0,0 +1,17 @@ +AUTH_CLIENT_ID=forge +AUTH_CLIENT_SECRET= +AUTH_WEBHOOK_TOKEN= +GITHUB_TOKEN= +ASSESSMENTS_API_KEY= +ASSESSMENTS_CALLBACK_TOKEN= +LEARN_FIND_COHORT_URL="http://localhost:3002/cohorts/find" +DS_PREP_UID="01t0a000005He03AAC" +STANDARD_EVENT_UIDS="40f3bb48-1d23-4105-a22a-7f07b90bd1e6,cc22ef7e-d9c9-4e3d-ae9f-39f3477c5861" +S3_REGION=us-gov-west-1 +accesskey= +secretkey= +appS3BucketUUID= +S3_BUCKET= +host= +port= +protocol= \ No newline at end of file diff --git a/scripts/.eslintrc.json b/scripts/.eslintrc.json new file mode 100644 index 0000000000000000000000000000000000000000..53cbea6afd905890de42f693688f76b8e4a4ea16 --- /dev/null +++ b/scripts/.eslintrc.json @@ -0,0 +1,201 @@ +{ + "parserOptions": { + "ecmaVersion": 8, + "ecmaFeatures": { + "experimentalObjectRestSpread": true, + "jsx": true + }, + "sourceType": "module" + }, + + "env": { + "es6": true, + "node": false + }, + + "plugins": [ + "import", + "promise", + "react", + "jsx-a11y", + "standard" + ], + + "globals": { + "document": false, + "navigator": false, + "window": false + }, + + "rules": { + "array-bracket-spacing": ["error", "never"], + "curly": ["error", "multi-line"], + "block-spacing": ["error", "always"], + "brace-style": ["error", "1tbs", { "allowSingleLine": true }], + "semi": ["error", "always"], + "quotes": ["error", "single", { "avoidEscape": true, "allowTemplateLiterals": true }], + "comma-spacing": ["error", { "before": false, "after": true }], + "no-debugger": "error", + "no-extra-parens": ["error", "functions"], + "no-floating-decimal": "error", + "no-implicit-coercion": "error", + "no-irregular-whitespace": "error", + "no-multi-spaces": "error", + "no-regex-spaces": "error", + "no-undef-init": "error", + "no-unsafe-negation": "error", + "no-unused-labels": "error", + "no-useless-return": "error", + "wrap-iife": ["error", "any", { "functionPrototypeMethods": true }], + "yoda": ["error", "never"], + "eol-last": "error", + "func-call-spacing": ["error", "never"], + "indent": ["error", 2], + "key-spacing": ["error", { "beforeColon": false, "afterColon": true }], + "keyword-spacing": ["error", { "before": true, "after": true }], + "new-parens": "error", + "newline-after-var": ["error", "always"], + "newline-before-return": "error", + "no-lonely-if": "error", + "no-multiple-empty-lines": ["error", { "max": 1 }], + "no-trailing-spaces": "error", + "no-unneeded-ternary": ["error", { "defaultAssignment": false }], + "no-whitespace-before-property": "error", + "nonblock-statement-body-position": "error", + "object-curly-spacing": ["error", "always"], + "operator-assignment": "error", + "space-before-blocks": ["error", "always"], + "space-before-function-paren": ["error", "never"], + "space-in-parens": ["error", "never"], + "space-infix-ops": "error", +// "spaced-comment": ["error", "always", { +// "line": { "markers": ["*package", "!", "//", ","] }, +// "block": { "balanced": true, "markers": ["*package", "!", ",", ":", "::", "flow-include"], "exceptions": ["*", "//"] } +// }], + "wrap-regex": "error" +// "accessor-pairs": "error", +// "arrow-spacing": ["error", { "before": true, "after": true }], +// "camelcase": ["error", { "properties": "never" }], +// "comma-dangle": ["error", { +// "arrays": "never", +// "objects": "never", +// "imports": "never", +// "exports": "never", +// "functions": "never" +// }], +// "comma-style": ["error", "last"], +// "constructor-super": "error", +// "dot-location": ["error", "property"], +// "eqeqeq": ["error", "always", { "null": "ignore" }], +// "generator-star-spacing": ["error", { "before": true, "after": true }], +// "handle-callback-err": ["error", "^(err|error)$" ], +// "new-cap": ["error", { "newIsCap": true, "capIsNew": false }], +// "no-array-constructor": "error", +// "no-caller": "error", +// "no-class-assign": "error", +// "no-compare-neg-zero": "error", +// "no-cond-assign": "error", +// "no-const-assign": "error", +// "no-constant-condition": ["error", { "checkLoops": false }], +// "no-control-regex": "error", +// "no-delete-var": "error", +// "no-dupe-args": "error", +// "no-dupe-class-members": "error", +// "no-dupe-keys": "error", +// "no-duplicate-case": "error", +// "no-empty-character-class": "error", +// "no-empty-pattern": "error", +// "no-eval": "error", +// "no-ex-assign": "error", +// "no-extend-native": "error", +// "no-extra-bind": "error", +// "no-extra-boolean-cast": "error", +// "no-fallthrough": "error", +// "no-func-assign": "error", +// "no-global-assign": "error", +// "no-implied-eval": "error", +// "no-inner-declarations": ["error", "functions"], +// "no-invalid-regexp": "error", +// "no-iterator": "error", +// "no-label-var": "error", +// "no-labels": ["error", { "allowLoop": false, "allowSwitch": false }], +// "no-lone-blocks": "error", +// "no-mixed-operators": ["error", { +// "groups": [ +// ["==", "!=", "===", "!==", ">", ">=", "<", "<="], +// ["&&", "||"], +// ["in", "instanceof"] +// ], +// "allowSamePrecedence": true +// }], +// "no-mixed-spaces-and-tabs": "error", +// "no-multi-str": "error", +// "no-negated-in-lhs": "error", +// "no-new": "error", +// "no-new-func": "error", +// "no-new-object": "error", +// "no-new-require": "error", +// "no-new-symbol": "error", +// "no-new-wrappers": "error", +// "no-obj-calls": "error", +// "no-octal": "error", +// "no-octal-escape": "error", +// "no-path-concat": "error", +// "no-proto": "error", +// "no-redeclare": "error", +// "no-return-assign": ["error", "except-parens"], +// "no-return-await": "error", +// "no-self-assign": "error", +// "no-self-compare": "error", +// "no-sequences": "error", +// "no-shadow-restricted-names": "error", +// "no-sparse-arrays": "error", +// "no-tabs": "error", +// "no-template-curly-in-string": "error", +// "no-this-before-super": "error", +// "no-throw-literal": "error", +// "no-undef": "error", +// "no-unexpected-multiline": "error", +// "no-unmodified-loop-condition": "error", +// "no-unreachable": "error", +// "no-unsafe-finally": "error", +// "no-unused-expressions": ["error", { "allowShortCircuit": true, "allowTernary": true, "allowTaggedTemplates": true }], +// "no-unused-vars": ["error", { "vars": "all", "args": "none", "ignoreRestSiblings": true }], +// "no-use-before-define": ["error", { "functions": false, "classes": false, "variables": false }], +// "no-useless-call": "error", +// "no-useless-computed-key": "error", +// "no-useless-constructor": "error", +// "no-useless-escape": "error", +// "no-useless-rename": "error", +// "no-with": "error", +// "object-property-newline": ["error", { "allowMultiplePropertiesPerLine": true }], +// "one-var": ["error", { "initialized": "never" }], +// "operator-linebreak": ["error", "after", { "overrides": { "?": "before", ":": "before" } }], +// "padded-blocks": ["error", { "blocks": "never", "switches": "never", "classes": "never" }], +// "prefer-promise-reject-errors": "error", +// "rest-spread-spacing": ["error", "never"], +// "semi-spacing": ["error", { "before": false, "after": true }], +// "space-unary-ops": ["error", { "words": true, "nonwords": false }], +// "symbol-description": "error", +// "template-curly-spacing": ["error", "never"], +// "template-tag-spacing": ["error", "never"], +// "unicode-bom": ["error", "never"], +// "use-isnan": "error", +// "valid-typeof": ["error", { "requireStringLiterals": true }], +// "yield-star-spacing": ["error", "both"], +// "import/export": "error", +// "import/first": "error", +// "import/no-duplicates": "error", +// "import/no-webpack-loader-syntax": "error", +// +// "node/no-deprecated-api": "error", +// "node/process-exit-as-throw": "error", +// +// "promise/param-names": "error", +// +// "standard/array-bracket-even-spacing": ["error", "either"], +// "standard/computed-property-even-spacing": ["error", "even"], +// "standard/no-callback-literal": "error", +// "standard/object-curly-even-spacing": ["error", "either"] + } +} diff --git a/scripts/.gitlab-ci.yml b/scripts/.gitlab-ci.yml new file mode 100644 index 0000000000000000000000000000000000000000..dcfae110a74c6112af532c2e92d3cda1f9a22fcf --- /dev/null +++ b/scripts/.gitlab-ci.yml @@ -0,0 +1,4 @@ +include: + - project: 'platform-one/devops/pipeline-products' + ref: 'learn-ci' + file: 'products/tron/galvanize/learn/forge-ci.yml' diff --git a/scripts/.haml-lint.yml b/scripts/.haml-lint.yml new file mode 100644 index 0000000000000000000000000000000000000000..c82c789ce0fa135976b4b098b96d088bc3bf3dc3 --- /dev/null +++ b/scripts/.haml-lint.yml @@ -0,0 +1,6 @@ +linters: + LineLength: + max: 200 + + InlineStyles: + enabled: false diff --git a/scripts/.nvmrc b/scripts/.nvmrc new file mode 100644 index 0000000000000000000000000000000000000000..47266f80f219088593360abdc1498afb6ca1cd32 --- /dev/null +++ b/scripts/.nvmrc @@ -0,0 +1 @@ +14.15.4 \ No newline at end of file diff --git a/scripts/.overcommit.yml b/scripts/.overcommit.yml new file mode 100644 index 0000000000000000000000000000000000000000..13aa9ce293ba9037f74dde322688a9c33e3d7f91 --- /dev/null +++ b/scripts/.overcommit.yml @@ -0,0 +1,963 @@ +# Default configuration that all Overcommit configurations inherit from. +# +# This is an opinionated list of which hooks are valuable to run and what their +# out-of-the-box settings should be. +#------------------------------------------------------------------------------- + +# Loads Bundler context from a Gemfile. If false, does nothing (default). +# +# Specifying a Gemfile for Bundler to load allows you to control which gems are +# available in the load path (i.e. loadable via `require`) within your hook +# runs. Note that having a Gemfile requires you to include `overcommit` itself +# in your Gemfile (otherwise Overcommit can't load itself!). +# +# This is useful if you want to: +# +# - Enforce a specific version of Overcommit to use for all hook runs +# (or to use a version from the master branch that has not been released yet) +# - Enforce a specific version or unreleased branch is used for a gem you want +# to use in your git hooks +# +# WARNING: This makes your hook runs slower, but you can work around this! +# +# Loading a Bundler context necessarily adds a startup delay to your hook runs +# as Bundler parses the Gemfile and checks that the dependencies are satisfied. +# Thus for projects with many gems this can introduce a noticeable delay. +# +# The recommended workaround is to create a separate Gemfile in the root of your +# repository (call it `.overcommit_gems.rb`), and include only the gems that +# your Overcommit hooks need in order to run. This significantly reduces the +# startup delay in your hook runs. Make sure to commit both +# `.overcommit_gems.rb` and the resulting `.overcommit_gems.rb.lock` file to +# your repository, and then set the `gemfile` option below to the name you gave +# the file. +# (Generate lock file by running `bundle install --gemfile=.overcommit_gems.rb`) +gemfile: false + +# Where to store hook plugins specific to a repository. These are loaded in +# addition to the default hooks Overcommit comes with. The location is relative +# to the root of the repository. +plugin_directory: '.git-hooks' + +# Whether to hide hook output by default. This results in completely silent hook +# runs except in the case of warning or failure. +quiet: false + +# Number of hooks that can be run concurrently. Typically this won't need to be +# adjusted, but if you know that some of your hooks themselves use multiple +# processors you can lower this value accordingly. You can define +# single-operator mathematical expressions, e.g. '%{processors} * 2', or +# '%{processors} / 2'. +concurrency: '%{processors}' + +# Whether to check if a hook plugin has changed since Overcommit last ran it. +# This is a defense mechanism when working with repositories which can contain +# untrusted code (e.g. when you fetch a pull request from a third party). +# See https://github.com/brigade/overcommit#security for more information. +verify_signatures: false + +# Hooks that are run against every commit message after a user has written it. +# These hooks are useful for enforcing policies on commit messages written for a +# project. +CommitMsg: + ALL: + requires_files: false + quiet: false + + CapitalizedSubject: + enabled: false + description: 'Check subject capitalization' + + EmptyMessage: + enabled: false + description: 'Check for empty commit message' + quiet: true + + GerritChangeId: + enabled: false + description: 'Ensure Gerrit Change-Id is present' + required: true + + HardTabs: + enabled: false + description: 'Check for hard tabs' + + MessageFormat: + enabled: false + description: 'Check commit message matches expected pattern' + pattern: '(.+)[|](.+)[|](.+)' + expected_pattern_message: ' | | ' + sample_message: 'DEFECT-1234 | Refactored Onboarding flow | John Doe' + + RussianNovel: + enabled: false + description: 'Check length of commit message' + quiet: true + + SingleLineSubject: + enabled: false + description: 'Check subject line' + + SpellCheck: + enabled: false + description: 'Check for misspelled words' + required_executable: 'hunspell' + flags: ['-a'] + + TextWidth: + enabled: false + description: 'Check text width' + max_subject_width: 60 + max_body_width: 72 + + TrailingPeriod: + enabled: false + description: 'Check for trailing periods in subject' + +# Hooks that are run after `git commit` is executed, before the commit message +# editor is displayed. These hooks are ideal for syntax checkers, linters, and +# other checks that you want to run before you allow a commit object to be +# created. +PreCommit: + ALL: + problem_on_unmodified_line: report + requires_files: true + required: false + quiet: false + + AuthorEmail: + enabled: false + description: 'Check author email' + requires_files: false + required: true + quiet: true + pattern: '^[^@]+@.*$' + + AuthorName: + enabled: false + description: 'Check for author name' + requires_files: false + required: true + quiet: true + + BerksfileCheck: + enabled: false + description: 'Check Berksfile lock' + required_executable: 'berks' + flags: ['list', '--quiet'] + install_command: 'gem install berks' + include: + - 'Berksfile' + - 'Berksfile.lock' + + BrokenSymlinks: + enabled: false + description: 'Check for broken symlinks' + quiet: true + + BundleAudit: + enabled: false + description: 'Check for vulnerable versions of gems' + required_executable: 'bundle-audit' + install_command: 'gem install bundler-audit' + + BundleCheck: + enabled: false + description: 'Check Gemfile dependencies' + required_executable: 'bundle' + flags: ['check'] + install_command: 'gem install bundler' + include: + - 'Gemfile' + - 'Gemfile.lock' + - '*.gemspec' + + BundleOutdated: + enabled: false + description: 'List installed gems with newer versions available' + required_executable: 'bundle' + flags: ['outdated', '--strict', '--parseable'] + install_command: 'gem install bundler' + + CaseConflicts: + enabled: false + description: 'Check for case-insensitivity conflicts' + quiet: true + + ChamberSecurity: + enabled: false + description: 'Check that settings have been secured with Chamber' + required_executable: 'chamber' + flags: ['secure', '--files'] + install_command: 'gem install chamber' + include: + - 'config/settings.yml' + - 'config/settings/**/*.yml' + + CoffeeLint: + enabled: false + description: 'Analyze with coffeelint' + required_executable: 'coffeelint' + flags: ['--reporter=csv'] + install_command: 'npm install -g coffeelint' + include: '**/*.coffee' + + Credo: + enabled: false + description: 'Analyze with credo' + required_executable: 'mix' + flags: ['credo', '--all', '--strict', '--format', 'flycheck'] + include: + - '**/*.ex' + - '**/*.exs' + + CssLint: + enabled: false + description: 'Analyze with csslint' + required_executable: 'csslint' + flags: ['--quiet', '--format=compact'] + install_command: 'npm install -g csslint' + include: '**/*.css' + + Dogma: + enabled: false + description: 'Analyze with dogma' + required_executable: 'mix' + flags: ['dogma'] + include: + - '**/*.ex' + - '**/*.exs' + + EsLint: + enabled: true + on_warn: fail + required_executable: './node_modules/.bin/eslint' + include: + - 'app/**/*.js' + - 'app/**/*.jsx' + + ExecutePermissions: + enabled: false + description: 'Check for file execute permissions' + quiet: true + + Fasterer: + enabled: false + description: 'Analyzing for potential speed improvements' + required_executable: 'fasterer' + install_command: 'gem install fasterer' + include: '**/*.rb' + + FixMe: + enabled: false + description: 'Check for "token" strings' + required_executable: 'grep' + flags: ['-IEHnw'] + keywords: ['BROKEN', 'BUG', 'ERROR', 'FIXME', 'HACK', 'NOTE', 'OPTIMIZE', 'REVIEW', 'TODO', 'WTF', 'XXX'] + + Foodcritic: + enabled: false + description: 'Analyze with Foodcritic' + required_executable: 'foodcritic' + flags: ['--epic-fail=any'] + install_command: 'gem install foodcritic' + + ForbiddenBranches: + enabled: false + description: 'Check for commit to forbidden branch' + quiet: true + branch_patterns: ['master'] + + GoLint: + enabled: false + description: 'Analyze with golint' + required_executable: 'golint' + install_command: 'go get github.com/golang/lint/golint' + include: '**/*.go' + + GoVet: + enabled: false + description: 'Analyze with go vet' + required_executable: 'go' + flags: ['tool', 'vet'] + install_command: 'go get golang.org/x/tools/cmd/vet' + include: '**/*.go' + + HamlLint: + enabled: false + description: 'Analyze with haml-lint' + required_executable: 'haml-lint' + install_command: 'gem install haml-lint' + include: '**/*.haml' + on_warn: 'fail' + + HardTabs: + enabled: false + description: 'Check for hard tabs' + quiet: true + required_executable: 'grep' + flags: ['-IHn', "\t"] + + Hlint: + enabled: false + description: 'Analyze with hlint' + required_executable: 'hlint' + install_command: 'cabal install hlint' + include: '**/*.hs' + + HtmlHint: + enabled: false + description: 'Analyze with HTMLHint' + required_executable: 'htmlhint' + install_command: 'npm install -g htmlhint' + include: '**/*.html' + + HtmlTidy: + enabled: false + description: 'Analyze HTML with tidy' + required_executable: 'tidy' + flags: ['-errors', '-quiet', '-utf8'] + include: '**/*.html' + + ImageOptim: + enabled: false + description: 'Check for optimizable images' + required_executable: 'image_optim' + install_command: 'gem install image_optim' + include: + - '**/*.gif' + - '**/*.jpeg' + - '**/*.jpg' + - '**/*.png' + - '**/*.svg' + + JavaCheckstyle: + enabled: false + description: 'Analyze with checkstyle' + required_executable: 'checkstyle' + flags: ['-c', '/sun_checks.xml'] + include: '**/*.java' + + Jscs: + enabled: false + description: 'Analyze with JSCS' + required_executable: 'jscs' + flags: ['--reporter=inline'] + install_command: 'npm install -g jscs' + include: '**/*.js' + + JsHint: + enabled: false + description: 'Analyze with JSHint' + required_executable: 'jshint' + flags: ['--verbose'] + install_command: 'npm install -g jshint' + include: '**/*.js' + + JsLint: + enabled: false + description: 'Analyze with JSLint' + required_executable: 'jslint' + flags: ['--terse'] + install_command: 'npm install -g jslint' + include: '**/*.js' + + Jsl: + enabled: false + description: 'Analyze with JSL' + required_executable: 'jsl' + flags: ['-nologo', '-nofilelisting', '-nocontext', '-nosummary'] + include: '**/*.js' + + JsonSyntax: + enabled: false + description: 'Validate JSON syntax' + required_library: 'json' + install_command: 'gem install json' + include: '**/*.json' + + LicenseHeader: + enabled: false + license_file: 'LICENSE.txt' + description: 'Check source files for license headers' + + LocalPathsInGemfile: + enabled: false + description: 'Check for local paths in Gemfile' + required_executable: 'grep' + flags: ['-IHnE', "^[^#]*((\\bpath:)|(:path[ \t]*=>))"] + include: '**/Gemfile' + + Mdl: + enabled: false + description: 'Analyze with mdl' + required_executable: 'mdl' + install_command: 'gem install mdl' + include: '**/*.md' + + MergeConflicts: + enabled: false + description: 'Check for merge conflicts' + quiet: true + required_executable: 'grep' + flags: ['-IHn', "^<<<<<<<[ \t]"] + + NginxTest: + enabled: false + description: 'Test nginx configs' + required_executable: 'nginx' + flags: ['-t'] + include: '**/nginx.conf' + + Pep257: + enabled: false + description: 'Analyze docstrings with pep257' + required_executable: 'pep257' + install_command: 'pip install pep257' + include: '**/*.py' + + Pep8: + enabled: false + description: 'Analyze with pep8' + required_executable: 'pep8' + install_command: 'pip install pep8' + include: '**/*.py' + + PuppetLint: + enabled: false + description: 'Analyze with puppet-lint' + required_executable: 'puppet-lint' + install_command: 'gem install puppet-lint' + flags: + - '--log-format="%{fullpath}:%{line}:%{column}:%{KIND}: %{message} (%{check})"' + - '--fail-on-warnings' + - '--error-level=all' + include: '**/*.pp' + + Pyflakes: + enabled: false + description: 'Analyze with pyflakes' + required_executable: 'pyflakes' + install_command: 'pip install pyflakes' + include: '**/*.py' + + Pylint: + enabled: false + description: 'Analyze with Pylint' + required_executable: 'pylint' + install_command: 'pip install pylint' + flags: + - '--msg-template="{path}:{line}:{C}: {msg} ({symbol})"' + - '--reports=n' + - '--persistent=n' + include: '**/*.py' + + PythonFlake8: + enabled: false + description: 'Analyze with flake8' + required_executable: 'flake8' + install_command: 'pip install flake8' + include: '**/*.py' + + RakeTarget: + enabled: false + description: 'Run rake targets' + # targets: + # - 'lint' + # - 'validate' + # - '...' + required_executable: 'rake' + install_command: 'gem install rake' + + RailsBestPractices: + enabled: false + description: 'Analyze with RailsBestPractices' + required_executable: 'rails_best_practices' + flags: ['--without-color'] + install_command: 'gem install rails_best_practices' + + RailsSchemaUpToDate: + enabled: false + description: 'Check if database schema is up to date' + include: + - 'db/migrate/*.rb' + - 'db/schema.rb' + - 'db/structure.sql' + + Reek: + enabled: false + description: 'Analyze with Reek' + required_executable: 'reek' + flags: ['--single-line', '--no-color'] + install_command: 'gem install reek' + include: + - '**/*.gemspec' + - '**/*.rake' + - '**/*.rb' + - '**/Gemfile' + - '**/Rakefile' + + RuboCop: + enabled: true + description: 'Analyze with RuboCop' + required_executable: 'rubocop' + flags: ['--format=emacs', '--force-exclusion', '--display-cop-names'] + install_command: 'gem install rubocop' + on_warn: 'fail' + include: + - '**/*.gemspec' + - '**/*.rake' + - '**/*.rb' + - '**/*.ru' + - '**/Gemfile' + - '**/Rakefile' + + RubyLint: + enabled: false + description: 'Analyze with ruby-lint' + required_executable: 'ruby-lint' + flags: ['--presenter=syntastic', '--levels=error,warning'] + install_command: 'gem install ruby-lint' + include: + - '**/*.gemspec' + - '**/*.rb' + + Scalariform: + enabled: false + description: 'Check formatting with Scalariform' + required_executable: 'scalariform' + flags: ['--test'] + include: '**/*.scala' + + Scalastyle: + enabled: false + description: 'Analyze with Scalastyle' + required_executable: 'scalastyle' + include: '**/*.scala' + + StyleLint: + enabled: false + description: 'Analyze with style-lint' + required_library: 'json' + required_executable: 'stylelint' + flags: ['app/**/*.css', 'app/**/*.scss'] + install_command: 'npm install -g stylelint' + include: + - '**/*.scss' + - '**/*.css' + + SemiStandard: + enabled: false + description: 'Analyze with semistandard' + required_executable: 'semistandard' + flags: ['--verbose'] + install_command: 'npm install -g semistandard' + include: '**/*.js' + + ShellCheck: + enabled: false + description: 'Analyze with ShellCheck' + required_executable: 'shellcheck' + flags: ['--format=gcc'] + include: '**/*.sh' + + SlimLint: + enabled: false + description: 'Analyze with slim-lint' + required_executable: 'slim-lint' + install_command: 'gem install slim_lint' + include: '**/*.slim' + + Sqlint: + enabled: false + description: 'Analyze with sqlint' + required_executable: 'sqlint' + install_command: 'gem install sqlint' + include: '**/*.sql' + + Standard: + enabled: false + description: 'Analyze with standard' + required_executable: 'standard' + flags: ['--verbose'] + install_command: 'npm install -g standard' + include: '**/*.js' + + TsLint: + enabled: false + description: 'Analyze with TSLint' + required_executable: 'tslint' + install_command: 'npm install -g tslint typescript' + flags: ['--t=prose'] + include: '**/*.ts' + + TrailingWhitespace: + enabled: false + description: 'Check for trailing whitespace' + required_executable: 'grep' + flags: ['-IHn', "[ \t]$"] + + TravisLint: + enabled: false + description: 'Check Travis CI configuration' + required_executable: 'travis' + flags: ['lint'] + install_command: 'gem install travis' + include: '.travis.yml' + + Vint: + enabled: false + description: 'Analyze with Vint' + required_executable: 'vint' + install_command: 'pip install vim-vint' + include: + - '**/*.vim' + - '**/*.vimrc' + + W3cCss: + enabled: false + description: 'Analyze with W3C CSS validation service' + required_library: 'w3c_validators' + install_command: 'gem install w3c_validators' + validator_uri: 'http://jigsaw.w3.org/css-validator/validator' + language: 'en' + profile: 'css3' + warn_level: 2 + include: + - '**/*.css' + + W3cHtml: + enabled: false + description: 'Analyze with W3C HTML validation service' + required_library: 'w3c_validators' + install_command: 'gem install w3c_validators' + validator_uri: 'http://validator.w3.org/check' + charset: 'utf-8' + doctype: 'HTML5' + include: + - '**/*.html' + + LineEndings: + description: 'Check line endings' + enabled: false + eol: "\n" # or "\r\n" for Windows-style newlines + + XmlLint: + enabled: false + description: 'Analyze with xmllint' + required_executable: 'xmllint' + flags: ['--noout'] + include: + - '**/*.xml' + - '**/*.svg' + + XmlSyntax: + enabled: false + description: 'Check XML syntax' + required_library: 'rexml/document' + include: + - '**/*.xml' + - '**/*.svg' + + YamlLint: + enabled: false + description: 'Analyze with YAMLlint' + required_executable: 'yamllint' + flags: ['--format=parsable'] + install_command: 'pip install yamllint' + include: + - '**/*.yaml' + - '**/*.yml' + + YamlSyntax: + enabled: false + description: 'Check YAML syntax' + required_library: 'yaml' + include: + - '**/*.yaml' + - '**/*.yml' + +# Hooks that run after HEAD changes or a file is explicitly checked out. +PostCheckout: + ALL: + required: false + quiet: false + skip_file_checkout: true + + BowerInstall: + enabled: false + description: 'Install bower dependencies' + requires_files: true + required_executable: 'bower' + install_command: 'npm install -g bower' + flags: ['install'] + include: 'bower.json' + + BundleInstall: + enabled: false + description: 'Install Bundler dependencies' + requires_files: true + required_executable: 'bundle' + install_command: 'gem install bundler' + flags: ['install'] + include: + - 'Gemfile' + - 'Gemfile.lock' + - '*.gemspec' + + IndexTags: + enabled: false + description: 'Generate tags file from source' + quiet: true + required_executable: 'ctags' + + NpmInstall: + enabled: false + description: 'Install NPM dependencies' + requires_files: true + required_executable: 'npm' + flags: ['install'] + include: + - 'package.json' + - 'npm-shrinkwrap.json' + + SubmoduleStatus: + enabled: false + description: 'Check submodule status' + quiet: true + recursive: false + +# Hooks that run after a commit is created. +PostCommit: + ALL: + requires_files: false + required: false + quiet: false + + BowerInstall: + enabled: false + description: 'Install bower dependencies' + requires_files: true + required_executable: 'bower' + install_command: 'npm install -g bower' + flags: ['install'] + include: 'bower.json' + + BundleInstall: + enabled: false + description: 'Install Bundler dependencies' + requires_files: true + required_executable: 'bundle' + install_command: 'gem install bundler' + flags: ['install'] + include: + - 'Gemfile' + - 'Gemfile.lock' + - '*.gemspec' + + Commitplease: + enabled: false + description: 'Analyze with Commitplease' + required_executable: './node_modules/.bin/commitplease' + install_command: 'npm install --save-dev commitplease' + flags: ['-1'] + + GitGuilt: + enabled: false + description: 'Calculate changes in blame since last commit' + requires_files: true + required_executable: 'git-guilt' + flags: ['HEAD~', 'HEAD'] + install_command: 'npm install -g git-guilt' + + IndexTags: + enabled: false + description: 'Generate tags file from source' + quiet: true + required_executable: 'ctags' + + NpmInstall: + enabled: false + description: 'Install NPM dependencies' + requires_files: true + required_executable: 'npm' + flags: ['install'] + include: + - 'package.json' + - 'npm-shrinkwrap.json' + + SubmoduleStatus: + enabled: false + description: 'Check submodule status' + quiet: true + recursive: false + +# Hooks that run after `git merge` executes successfully (no merge conflicts). +PostMerge: + ALL: + requires_files: false + quiet: false + + BowerInstall: + enabled: false + description: 'Install bower dependencies' + requires_files: true + required_executable: 'bower' + install_command: 'npm install -g bower' + flags: ['install'] + include: 'bower.json' + + BundleInstall: + enabled: false + description: 'Install Bundler dependencies' + requires_files: true + required_executable: 'bundle' + install_command: 'gem install bundler' + flags: ['install'] + include: + - 'Gemfile' + - 'Gemfile.lock' + - '*.gemspec' + + IndexTags: + enabled: false + description: 'Generate tags file from source' + quiet: true + required_executable: 'ctags' + + NpmInstall: + enabled: false + description: 'Install NPM dependencies' + requires_files: true + required_executable: 'npm' + flags: ['install'] + include: + - 'package.json' + - 'npm-shrinkwrap.json' + + SubmoduleStatus: + enabled: false + description: 'Check submodule status' + quiet: true + recursive: false + +# Hooks that run after a commit is modified by an amend or rebase. +PostRewrite: + ALL: + requires_files: false + quiet: false + + BowerInstall: + enabled: false + description: 'Install bower dependencies' + requires_files: true + required_executable: 'bower' + install_command: 'npm install -g bower' + flags: ['install'] + include: 'bower.json' + + BundleInstall: + enabled: false + description: 'Install Bundler dependencies' + requires_files: true + required_executable: 'bundle' + install_command: 'gem install bundler' + flags: ['install'] + include: + - 'Gemfile' + - 'Gemfile.lock' + - '*.gemspec' + + IndexTags: + enabled: false + description: 'Generate tags file from source' + quiet: true + required_executable: 'ctags' + + NpmInstall: + enabled: false + description: 'Install NPM dependencies' + requires_files: true + required_executable: 'npm' + flags: ['install'] + include: + - 'package.json' + - 'npm-shrinkwrap.json' + + SubmoduleStatus: + enabled: false + description: 'Check submodule status' + quiet: true + recursive: false + +# Hooks that run during `git push`, after remote refs have been updated but +# before any objects have been transferred. +PrePush: + ALL: + requires_files: false + required: false + quiet: false + + ProtectedBranches: + enabled: false + description: 'Check for illegal pushes to protected branches' + destructive_only: true + branches: ['master'] + + Pytest: + enabled: false + description: 'Run pytest test suite' + required_executable: 'pytest' + install_command: 'pip install -U pytest' + + PythonNose: + enabled: false + description: 'Run nose test suite' + required_executable: 'nosetests' + install_command: 'pip install -U nose' + + RSpec: + enabled: false + description: 'Run RSpec test suite' + required_executable: 'rspec' + + RakeTarget: + enabled: false + description: 'Run rake targets' + # targets: + # - 'lint' + # - 'validate' + # - '...' + required_executable: 'rake' + install_command: 'gem install rake' + + Minitest: + enabled: false + description: 'Run Minitest test suite' + command: ['ruby', '-Ilib:test', '-rminitest', "-e 'exit! Minitest.run'"] + include: 'test/**/*_test.rb' + + TestUnit: + enabled: false + description: 'Run Test::Unit test suite' + command: ['ruby', '-Ilib:test', '-rtest/unit', "-e 'exit! Test::Unit::AutoRunner.run'"] + + Brakeman: + enabled: false + description: 'Check for security vulnerabilities' + required_executable: 'brakeman' + flags: ['--exit-on-warn', '--quiet', '--summary'] + install_command: 'gem install brakeman' + +# Hooks that run during `git rebase`, before any commits are rebased. +# If a hook fails, the rebase is aborted. +PreRebase: + ALL: + requires_files: false + required: false + quiet: false + + MergedCommits: + enabled: false + description: 'Check for commits that have already been merged' + branches: ['master'] diff --git a/scripts/.postcssrc.yml b/scripts/.postcssrc.yml new file mode 100644 index 0000000000000000000000000000000000000000..6574125de3909f730bc1f866a6cbce45b1b8d00b --- /dev/null +++ b/scripts/.postcssrc.yml @@ -0,0 +1,3 @@ +#plugins: +# postcss-import: {} +# postcss-cssnext: {} diff --git a/scripts/.rubocop.yml b/scripts/.rubocop.yml new file mode 100644 index 0000000000000000000000000000000000000000..267ce070c0993093a82c9570ca97957ad03e3eb6 --- /dev/null +++ b/scripts/.rubocop.yml @@ -0,0 +1,212 @@ +AllCops: + Exclude: + - 'bin/**/*' + - 'vendor/**/*' + - 'db/schema.rb' + - 'node_modules/**/*' + - 'spec/**/*' + - 'db/seeds.rb' + +Bundler/OrderedGems: + Enabled: false + +Style/Semicolon: + Enabled: true + +Lint/AssignmentInCondition: + Enabled: true + +Style/FloatDivision: + Enabled: false + +Naming/MemoizedInstanceVariableName: + Enabled: false + +Layout/EndAlignment: + Enabled: true + +Lint/HandleExceptions: + Enabled: true + +Lint/IneffectiveAccessModifier: + Enabled: true + +Lint/UselessAccessModifier: + Enabled: true + +Lint/RescueException: + Enabled: true + +Lint/ParenthesesAsGroupedExpression: + Enabled: false + +Lint/NonLocalExitFromIterator: + Enabled: false + +Lint/UriEscapeUnescape: + Enabled: false + +Style/DoubleNegation: + Enabled: false + +Style/IfUnlessModifier: + Enabled: false + +Style/MultilineBlockChain: + Enabled: false + +Naming/PredicateName: + Enabled: true + Exclude: + - app/controllers/application_controller.rb + +Naming/VariableNumber: + Enabled: true + Exclude: + - spec/**/* + +Metrics/AbcSize: + Enabled: false + +Metrics/BlockLength: + Enabled: true + Max: 1200 + +Metrics/BlockNesting: + Enabled: true + Max: 6 + +Metrics/ClassLength: + Enabled: true + Max: 500 + +Metrics/CyclomaticComplexity: + Enabled: true + Max: 30 + +Metrics/LineLength: + Enabled: true + Max: 140 + Exclude: + - app/controllers/cohorts_controller.rb + - app/jobs/create_release_job.rb + - app/services/standard_submissions_service.rb + - app/services/sql_challenge_db_service.rb + - spec/**/* + +Metrics/MethodLength: + Enabled: true + Max: 200 + +Metrics/ModuleLength: + Enabled: true + Max: 500 + +Metrics/ParameterLists: + Enabled: true + Max: 8 + +Metrics/PerceivedComplexity: + Enabled: false + +Layout/DotPosition: + EnforcedStyle: trailing + +Layout/EmptyLinesAroundArguments: + Enabled: false + +Layout/SpaceInsideBlockBraces: + Enabled: false + +Layout/MultilineMethodCallIndentation: + Enabled: false + +Style/SpecialGlobalVars: + EnforcedStyle: use_perl_names + +Style/StringLiterals: + EnforcedStyle: double_quotes + +Style/TrivialAccessors: + Enabled: true + +Security/YAMLLoad: + Enabled: false + +Style/AndOr: + Enabled: false + +Layout/CaseIndentation: + Enabled: false + +Style/Documentation: + Enabled: false + +Style/EmptyMethod: + Enabled: false + +Style/FrozenStringLiteralComment: + Enabled: false + +Style/MutableConstant: + Enabled: false + +Style/NumericLiteralPrefix: + Enabled: false + +Style/NumericLiterals: + Enabled: false + +Style/NumericPredicate: + Enabled: false + +Style/RedundantParentheses: + Enabled: false + +Style/RegexpLiteral: + Enabled: false + +Style/UnneededPercentQ: + Enabled: false + +Naming/VariableName: + Enabled: true + +Style/WordArray: + Enabled: false + +Style/ClassAndModuleChildren: + Enabled: false + +Style/ClassVars: + Enabled: false + +Style/DateTime: + Enabled: false + +Style/Next: + Enabled: false + +Style/GuardClause: + Enabled: false + +Style/SignalException: + Enabled: false + +Style/SymbolProc: + Enabled: true + Exclude: + - app/controllers/cohorts_controller.rb + +Style/FormatStringToken: + Enabled: false + +Layout/AlignHash: + Enabled: false + +Layout/AlignParameters: + Enabled: false + EnforcedStyle: with_fixed_indentation + +Layout/EmptyLinesAroundBlockBody: + Enabled: true diff --git a/scripts/.ruby-version b/scripts/.ruby-version new file mode 100644 index 0000000000000000000000000000000000000000..338a5b5d8fec491b97978114dc35e36348fa56a7 --- /dev/null +++ b/scripts/.ruby-version @@ -0,0 +1 @@ +2.6.6 diff --git a/scripts/.stylelintrc b/scripts/.stylelintrc new file mode 100644 index 0000000000000000000000000000000000000000..2c85a9901461531a5f355d128f31e08094679090 --- /dev/null +++ b/scripts/.stylelintrc @@ -0,0 +1,161 @@ +{ + "rules": { + # "at-rule-empty-line-before": "always", + # "at-rule-blacklist": string|[], + # "at-rule-name-case": "lower"|"upper", + # "at-rule-name-newline-after": "always"|"always-multi-line", + # "at-rule-name-space-after": "always"|"always-single-line", + # "at-rule-no-unknown": true, + # "at-rule-no-vendor-prefix": true, + # "at-rule-semicolon-newline-after": "always", + # "at-rule-semicolon-space-before": "always"|"never", + # "at-rule-whitelist": string|[], + # "block-closing-brace-empty-line-before": "always-multi-line"|"never", + "block-closing-brace-newline-after": "always", + "block-closing-brace-newline-before": "always", + # "block-closing-brace-space-after": "always"|"always-single-line"|"never-single-line"|"always-multi-line"|"never-multi-line", + # "block-closing-brace-space-before": "always"|"never"|"always-single-line"|"never-single-line"|"always-multi-line"|"never-multi-line", + # "block-no-empty": true, + # "color-hex-case": "lower"|"upper", + # "color-hex-length": "short"|"long", + # "color-named": "always-where-possible"|"never", + # "color-no-hex": true, + # "color-no-invalid-hex": true, + # "comment-empty-line-before": "always"|"never", + # "comment-no-empty": true, + # "comment-whitespace-inside": "always"|"never", + # "comment-word-blacklist": string|[], + # "custom-media-pattern": string, + # "custom-property-empty-line-before": "always"|"never", + # "custom-property-pattern": string, + # "declaration-bang-space-after": "always"|"never", + # "declaration-bang-space-before": "always"|"never", + # "declaration-block-no-duplicate-properties": true, + # "declaration-block-no-redundant-longhand-properties": true, + # "declaration-block-no-shorthand-property-overrides": true, + # "declaration-block-semicolon-newline-after": "always"|"always-multi-line"|"never-multi-line", + # "declaration-block-semicolon-newline-before": "always"|"always-multi-line"|"never-multi-line", + # "declaration-block-semicolon-space-after": "always"|"never"|"always-single-line"|"never-single-line", + # "declaration-block-semicolon-space-before": "always"|"never"|"always-single-line"|"never-single-line", + # "declaration-block-single-line-max-declarations": int, + # "declaration-block-trailing-semicolon": "always"|"never", + # "declaration-colon-newline-after": "always"|"always-multi-line", + "declaration-colon-space-after": "always", + "declaration-colon-space-before": "never", + "declaration-empty-line-before": "never", + # "declaration-no-important": true, + # "declaration-property-unit-blacklist": {}, + # "declaration-property-unit-whitelist": {}, + # "declaration-property-value-blacklist": {}, + # "declaration-property-value-whitelist": {}, + # "font-family-name-quotes": "always-where-required"|"always-where-recommended"|"always-unless-keyword", + # "font-family-no-duplicate-names": true, + # "font-weight-notation": "numeric"|"named", + # "function-blacklist": string|[], + # "function-calc-no-unspaced-operator": true, + # "function-comma-newline-after": "always"|"always-multi-line"|"never-multi-line", + # "function-comma-newline-before": "always"|"always-multi-line"|"never-multi-line", + # "function-comma-space-after": "always"|"never"|"always-single-line"|"never-single-line", + # "function-comma-space-before": "always"|"never"|"always-single-line"|"never-single-line", + # "function-linear-gradient-no-nonstandard-direction": true, + # "function-max-empty-lines": int, + # "function-name-case": "lower"|"upper", + # "function-parentheses-newline-inside": "always"|"always-multi-line"|"never-multi-line", + # "function-parentheses-space-inside": "always"|"never"|"always-single-line"|"never-single-line", + # "function-url-data-uris": "always"|"never", + # "function-url-no-scheme-relative": true, + # "function-url-quotes": "always"|"never", + # "function-url-scheme-whitelist": string|[], + # "function-whitelist": string|[], + # "function-whitespace-after": "always"|"never", + "indentation": 2, + # "keyframe-declaration-no-important": true, + # "length-zero-no-unit": true, + # "max-empty-lines": int, + # "max-line-length": int, + # "max-nesting-depth": int, + # "media-feature-colon-space-after": "always"|"never", + # "media-feature-colon-space-before": "always"|"never", + # "media-feature-name-blacklist": string|[], + # "media-feature-name-case": "lower"|"upper", + # "media-feature-name-no-unknown": true, + # "media-feature-name-no-vendor-prefix": true, + # "media-feature-name-whitelist": string|[], + # "media-feature-parentheses-space-inside": "always"|"never", + # "media-feature-range-operator-space-after": "always"|"never", + # "media-feature-range-operator-space-before": "always"|"never", + # "media-query-list-comma-newline-after": "always"|"always-multi-line"|"never-multi-line", + # "media-query-list-comma-newline-before": "always"|"always-multi-line"|"never-multi-line", + # "media-query-list-comma-space-after": "always"|"never"|"always-single-line"|"never-single-line", + # "media-query-list-comma-space-before": "always"|"never"|"always-single-line"|"never-single-line", + # "no-descending-specificity": true, + # "no-duplicate-selectors": true, + # "no-empty-source": true, + # "no-eol-whitespace": true, + # "no-extra-semicolons": true, + # "no-invalid-double-slash-comments": true, + # "no-missing-end-of-source-newline": true, + # "no-unknown-animations": true, + "number-leading-zero": "always", + # "number-max-precision": int, + "number-no-trailing-zeros": true, + # "property-blacklist": string|[], + # "property-case": "lower"|"upper", + # "property-no-unknown": true, + # "property-no-vendor-prefix": true, + # "property-whitelist": string|[], + # "rule-empty-line-before": "always"|"never"|"always-multi-line"|"never-multi-line", + # "selector-attribute-brackets-space-inside": "always"|"never", + # "selector-attribute-operator-blacklist": string|[], + # "selector-attribute-operator-space-after": "always"|"never", + # "selector-attribute-operator-space-before": "always"|"never", + # "selector-attribute-operator-whitelist": string|[], + # "selector-attribute-quotes": "always"|"never", + # "selector-class-pattern": string, + "selector-combinator-space-after": "always", + "selector-combinator-space-before": "always", + # "selector-descendant-combinator-no-non-space": true, + # "selector-id-pattern": string, + # "selector-list-comma-newline-after": "always"|"always-multi-line"|"never-multi-line", + # "selector-list-comma-newline-before": "always"|"always-multi-line"|"never-multi-line", + # "selector-list-comma-space-after": "always"|"never"|"always-single-line"|"never-single-line", + "selector-list-comma-space-before": "never", + # "selector-max-empty-lines": int, + # "selector-max-class": int, + # "selector-max-compound-selectors": int, + # "selector-max-specificity": string, + # "selector-nested-pattern": string, + # "selector-no-attribute": true, + # "selector-no-combinator": true, + # "selector-no-id": true, + # "selector-no-qualifying-type": true, + # "selector-no-type": true, + # "selector-no-universal": true, + # "selector-no-vendor-prefix": true, + # "selector-pseudo-class-blacklist": string|[], + # "selector-pseudo-class-case": "lower"|"upper", + # "selector-pseudo-class-no-unknown": true, + # "selector-pseudo-class-parentheses-space-inside": "always"|"never", + # "selector-pseudo-class-whitelist": string|[], + # "selector-pseudo-element-case": "lower"|"upper", + # "selector-pseudo-element-colon-notation": "single"|"double", + # "selector-pseudo-element-no-unknown": true, + # "selector-type-case": "lower"|"upper", + # "selector-type-no-unknown": true, + "shorthand-property-no-redundant-values": true, + # "string-no-newline": true, + "string-quotes": "double", + # "time-min-milliseconds": int, + # "unit-blacklist": string|[], + # "unit-case": "lower"|"upper", + # "unit-no-unknown": true, + # "unit-whitelist": string|[], + # "value-keyword-case": "lower"|"upper", + # "value-list-comma-newline-after": "always"|"always-multi-line"|"never-multi-line", + # "value-list-comma-newline-before": "always"|"always-multi-line"|"never-multi-line", + # "value-list-comma-space-after": "always"|"never"|"always-single-line"|"never-single-line", + # "value-list-comma-space-before": "always"|"never"|"always-single-line"|"never-single-line", + # "value-list-max-empty-lines": int, + # "value-no-vendor-prefix": true + } +} diff --git a/scripts/Dockerfile.packages b/scripts/Dockerfile.packages new file mode 100644 index 0000000000000000000000000000000000000000..68f70931fb9a4b233c7e0e878f0a8fb40c542a77 --- /dev/null +++ b/scripts/Dockerfile.packages @@ -0,0 +1,84 @@ +ARG BASE_REGISTRY +ARG BASE_IMAGE +ARG BASE_TAG + +FROM ${BASE_REGISTRY}/${BASE_IMAGE}:${BASE_TAG} +USER 0 + +ARG NODE_VERSION +ARG YARN_VERSION + +# Install required libs. +RUN dnf update -y && dnf install -y \ + curl \ + make \ + patch \ + git \ + zlib-devel \ + libpq-devel \ + libxml2-devel \ + libxslt-devel \ + gcc \ + gcc-c++ + +# Setup our environment +WORKDIR /app +ADD . . + +# Switch to the binary directory. +WORKDIR /app/bundles + +# Node.js +RUN curl -L https://nodejs.org/dist/v$NODE_VERSION/node-v$NODE_VERSION-linux-x64.tar.gz -o node-v$NODE_VERSION-linux-x64.tar.gz +RUN tar xzf node-v$NODE_VERSION-linux-x64.tar.gz +RUN mv node-v$NODE_VERSION-linux-x64 /app/nodejs + +# Yarn +RUN curl -L https://yarnpkg.com/downloads/$YARN_VERSION/yarn-v$YARN_VERSION.tar.gz -o yarn-v$YARN_VERSION.tar.gz +RUN ls -la +RUN tar xzf yarn-v$YARN_VERSION.tar.gz +RUN mv yarn-v$YARN_VERSION /app/yarn + +# Switch back to app dir. +WORKDIR /app + +# Add write permissions. +RUN chown -R 1001 . + +# Become the ruby user +USER 1001 + +# Set Environment Variables +ENV NODE_HOME /app/nodejs +ENV PATH /app/node_modules/.bin:$PATH +ENV PATH /app/yarn/bin:$PATH +ENV PATH /app/nodejs/bin:$PATH +RUN ln -s /app/nodejs/bin/node /app/nodejs/bin/nodejs + +# Upgrade npm and repackage node. +RUN npm i -g npm +RUN tar czf nodejs.tar.gz nodejs + +# Repackage yarn. +RUN tar czf yarn.tar.gz yarn + +# Precompile assets +RUN yarn install +RUN tar czf node_modules.tar.gz node_modules + +# Run bundle install. +RUN gem install bundler +RUN bundle install + +# Go to bundle dir. +WORKDIR /usr/local +USER 0 +RUN tar czf bundle.tar.gz bundle +RUN cp bundle.tar.gz /app/ +USER 1001 + +# Change back to app dir. +WORKDIR /app + +# Set the entry point. +CMD tail -f /dev/null diff --git a/scripts/Gemfile b/scripts/Gemfile new file mode 100644 index 0000000000000000000000000000000000000000..1a86e246f56548fdb35692ec5210080f24e0d982 --- /dev/null +++ b/scripts/Gemfile @@ -0,0 +1,97 @@ +source "https://rubygems.org" +ruby "2.6.6" + +git_source(:github) do |repo_name| + repo_name = "#{repo_name}/#{repo_name}" unless repo_name.include?("/") + "https://github.com/#{repo_name}.git" +end + +# architecture +gem "aws-sdk" +gem "pg" +gem "redis" +gem "puma" +gem "pundit" +gem "httparty" +gem "pagy" +gem "rails" +gem "sidekiq" +gem "dotenv-rails" +gem "rubyzip" +gem "zip-zip" +gem "tzinfo-data", platforms: %i[mingw mswin x64_mingw jruby] +gem "rack-attack" + +# assets +gem "ts_routes" +gem "webpacker" +gem "bootstrap" +gem "font-awesome-rails" +gem "sass-rails" +gem "uglifier" + +# views +gem "haml" +gem "jquery-rails" +gem "mathjax-rails" +gem "react-rails" +gem "underscore-rails" +gem "js-routes" +gem "browser" +gem "apitome" + +# services +gem 'jwt' +gem "honeybadger" +gem "github_url" +gem "gitlab" +gem "analytics-ruby", require: "segment/analytics" +gem "octokit" +gem "barnes" +gem "scout_apm" +gem "solid_use_case" + +# file processing +### We need to pass git credentials into the docker build command so we can clone the block-parser +# gem "block_parser", :git => "https://code.il2.dso.mil/tron/products/learn-lms/ruby-gems/block-parser.git" +gem "block_parser", path: "./gems/block-parser" + +# Reduces boot times through caching; required in config/boot.rb +gem "bootsnap" + +# performance +gem "rack-mini-profiler" +gem "memory_profiler" +gem "flamegraph" +gem "stackprof" + +group :development, :test do + gem "json_spec" + gem "byebug" + gem "factory_bot_rails" + gem "rspec-rails" +end + +group :development do + gem "foreman" + gem "overcommit" + gem "rubocop" + gem "better_errors" + gem "binding_of_caller" + gem "letter_opener" + gem "listen" +end + +group :test do + gem "capybara" + gem "database_cleaner" + gem "rspec_junit_formatter" + gem "selenium-webdriver" + gem "shoulda-matchers" + gem "timecop" + gem "vcr" + gem "webmock" + gem "rails-controller-testing" + gem "rspec_api_documentation" + gem 'simplecov', require: false, group: :test +end diff --git a/scripts/Gemfile.lock b/scripts/Gemfile.lock new file mode 100644 index 0000000000000000000000000000000000000000..1fc26f0e7a7763609158eb3a3fead272eed8fa38 --- /dev/null +++ b/scripts/Gemfile.lock @@ -0,0 +1,1527 @@ +PATH + remote: gems/block-parser + specs: + block_parser (0.1.0) + activemodel + commonmarker + github-markup + github_url + gitlab + nokogiri + octokit + psych + redcarpet + rspec_junit_formatter + +GEM + remote: https://rubygems.org/ + specs: + actioncable (6.1.1) + actionpack (= 6.1.1) + activesupport (= 6.1.1) + nio4r (~> 2.0) + websocket-driver (>= 0.6.1) + actionmailbox (6.1.1) + actionpack (= 6.1.1) + activejob (= 6.1.1) + activerecord (= 6.1.1) + activestorage (= 6.1.1) + activesupport (= 6.1.1) + mail (>= 2.7.1) + actionmailer (6.1.1) + actionpack (= 6.1.1) + actionview (= 6.1.1) + activejob (= 6.1.1) + activesupport (= 6.1.1) + mail (~> 2.5, >= 2.5.4) + rails-dom-testing (~> 2.0) + actionpack (6.1.1) + actionview (= 6.1.1) + activesupport (= 6.1.1) + rack (~> 2.0, >= 2.0.9) + rack-test (>= 0.6.3) + rails-dom-testing (~> 2.0) + rails-html-sanitizer (~> 1.0, >= 1.2.0) + actiontext (6.1.1) + actionpack (= 6.1.1) + activerecord (= 6.1.1) + activestorage (= 6.1.1) + activesupport (= 6.1.1) + nokogiri (>= 1.8.5) + actionview (6.1.1) + activesupport (= 6.1.1) + builder (~> 3.1) + erubi (~> 1.4) + rails-dom-testing (~> 2.0) + rails-html-sanitizer (~> 1.1, >= 1.2.0) + activejob (6.1.1) + activesupport (= 6.1.1) + globalid (>= 0.3.6) + activemodel (6.1.1) + activesupport (= 6.1.1) + activerecord (6.1.1) + activemodel (= 6.1.1) + activesupport (= 6.1.1) + activestorage (6.1.1) + actionpack (= 6.1.1) + activejob (= 6.1.1) + activerecord (= 6.1.1) + activesupport (= 6.1.1) + marcel (~> 0.3.1) + mimemagic (~> 0.3.2) + activesupport (6.1.1) + concurrent-ruby (~> 1.0, >= 1.0.2) + i18n (>= 1.6, < 2) + minitest (>= 5.1) + tzinfo (~> 2.0) + zeitwerk (~> 2.3) + addressable (2.7.0) + public_suffix (>= 2.0.2, < 5.0) + analytics-ruby (2.2.8) + apitome (0.3.0) + kramdown + railties + ast (2.4.1) + autoprefixer-rails (10.2.0.0) + execjs + aws-eventstream (1.1.0) + aws-partitions (1.418.0) + aws-sdk (3.0.1) + aws-sdk-resources (~> 3) + aws-sdk-accessanalyzer (1.14.0) + aws-sdk-core (~> 3, >= 3.109.0) + aws-sigv4 (~> 1.1) + aws-sdk-acm (1.38.0) + aws-sdk-core (~> 3, >= 3.109.0) + aws-sigv4 (~> 1.1) + aws-sdk-acmpca (1.32.0) + aws-sdk-core (~> 3, >= 3.109.0) + aws-sigv4 (~> 1.1) + aws-sdk-alexaforbusiness (1.43.0) + aws-sdk-core (~> 3, >= 3.109.0) + aws-sigv4 (~> 1.1) + aws-sdk-amplify (1.27.0) + aws-sdk-core (~> 3, >= 3.109.0) + aws-sigv4 (~> 1.1) + aws-sdk-amplifybackend (1.1.0) + aws-sdk-core (~> 3, >= 3.109.0) + aws-sigv4 (~> 1.1) + aws-sdk-apigateway (1.58.0) + aws-sdk-core (~> 3, >= 3.109.0) + aws-sigv4 (~> 1.1) + aws-sdk-apigatewaymanagementapi (1.19.0) + aws-sdk-core (~> 3, >= 3.109.0) + aws-sigv4 (~> 1.1) + aws-sdk-apigatewayv2 (1.30.0) + aws-sdk-core (~> 3, >= 3.109.0) + aws-sigv4 (~> 1.1) + aws-sdk-appconfig (1.12.0) + aws-sdk-core (~> 3, >= 3.109.0) + aws-sigv4 (~> 1.1) + aws-sdk-appflow (1.4.0) + aws-sdk-core (~> 3, >= 3.109.0) + aws-sigv4 (~> 1.1) + aws-sdk-appintegrationsservice (1.0.0) + aws-sdk-core (~> 3, >= 3.109.0) + aws-sigv4 (~> 1.1) + aws-sdk-applicationautoscaling (1.49.0) + aws-sdk-core (~> 3, >= 3.109.0) + aws-sigv4 (~> 1.1) + aws-sdk-applicationdiscoveryservice (1.33.0) + aws-sdk-core (~> 3, >= 3.109.0) + aws-sigv4 (~> 1.1) + aws-sdk-applicationinsights (1.16.0) + aws-sdk-core (~> 3, >= 3.109.0) + aws-sigv4 (~> 1.1) + aws-sdk-appmesh (1.33.0) + aws-sdk-core (~> 3, >= 3.109.0) + aws-sigv4 (~> 1.1) + aws-sdk-appregistry (1.3.0) + aws-sdk-core (~> 3, >= 3.109.0) + aws-sigv4 (~> 1.1) + aws-sdk-appstream (1.49.0) + aws-sdk-core (~> 3, >= 3.109.0) + aws-sigv4 (~> 1.1) + aws-sdk-appsync (1.37.0) + aws-sdk-core (~> 3, >= 3.109.0) + aws-sigv4 (~> 1.1) + aws-sdk-athena (1.33.0) + aws-sdk-core (~> 3, >= 3.109.0) + aws-sigv4 (~> 1.1) + aws-sdk-auditmanager (1.1.0) + aws-sdk-core (~> 3, >= 3.109.0) + aws-sigv4 (~> 1.1) + aws-sdk-augmentedairuntime (1.10.0) + aws-sdk-core (~> 3, >= 3.109.0) + aws-sigv4 (~> 1.1) + aws-sdk-autoscaling (1.53.0) + aws-sdk-core (~> 3, >= 3.109.0) + aws-sigv4 (~> 1.1) + aws-sdk-autoscalingplans (1.29.0) + aws-sdk-core (~> 3, >= 3.109.0) + aws-sigv4 (~> 1.1) + aws-sdk-backup (1.25.0) + aws-sdk-core (~> 3, >= 3.109.0) + aws-sigv4 (~> 1.1) + aws-sdk-batch (1.43.0) + aws-sdk-core (~> 3, >= 3.109.0) + aws-sigv4 (~> 1.1) + aws-sdk-braket (1.5.0) + aws-sdk-core (~> 3, >= 3.109.0) + aws-sigv4 (~> 1.1) + aws-sdk-budgets (1.36.0) + aws-sdk-core (~> 3, >= 3.109.0) + aws-sigv4 (~> 1.1) + aws-sdk-chime (1.40.0) + aws-sdk-core (~> 3, >= 3.109.0) + aws-sigv4 (~> 1.1) + aws-sdk-cloud9 (1.29.0) + aws-sdk-core (~> 3, >= 3.109.0) + aws-sigv4 (~> 1.1) + aws-sdk-clouddirectory (1.29.0) + aws-sdk-core (~> 3, >= 3.109.0) + aws-sigv4 (~> 1.1) + aws-sdk-cloudformation (1.46.0) + aws-sdk-core (~> 3, >= 3.109.0) + aws-sigv4 (~> 1.1) + aws-sdk-cloudfront (1.47.0) + aws-sdk-core (~> 3, >= 3.109.0) + aws-sigv4 (~> 1.1) + aws-sdk-cloudhsm (1.27.0) + aws-sdk-core (~> 3, >= 3.109.0) + aws-sigv4 (~> 1.1) + aws-sdk-cloudhsmv2 (1.31.0) + aws-sdk-core (~> 3, >= 3.109.0) + aws-sigv4 (~> 1.1) + aws-sdk-cloudsearch (1.27.0) + aws-sdk-core (~> 3, >= 3.109.0) + aws-sigv4 (~> 1.1) + aws-sdk-cloudsearchdomain (1.22.0) + aws-sdk-core (~> 3, >= 3.109.0) + aws-sigv4 (~> 1.1) + aws-sdk-cloudtrail (1.31.0) + aws-sdk-core (~> 3, >= 3.109.0) + aws-sigv4 (~> 1.1) + aws-sdk-cloudwatch (1.47.0) + aws-sdk-core (~> 3, >= 3.109.0) + aws-sigv4 (~> 1.1) + aws-sdk-cloudwatchevents (1.40.0) + aws-sdk-core (~> 3, >= 3.109.0) + aws-sigv4 (~> 1.1) + aws-sdk-cloudwatchlogs (1.38.0) + aws-sdk-core (~> 3, >= 3.109.0) + aws-sigv4 (~> 1.1) + aws-sdk-codeartifact (1.6.0) + aws-sdk-core (~> 3, >= 3.109.0) + aws-sigv4 (~> 1.1) + aws-sdk-codebuild (1.65.0) + aws-sdk-core (~> 3, >= 3.109.0) + aws-sigv4 (~> 1.1) + aws-sdk-codecommit (1.40.0) + aws-sdk-core (~> 3, >= 3.109.0) + aws-sigv4 (~> 1.1) + aws-sdk-codedeploy (1.37.0) + aws-sdk-core (~> 3, >= 3.109.0) + aws-sigv4 (~> 1.1) + aws-sdk-codeguruprofiler (1.12.0) + aws-sdk-core (~> 3, >= 3.109.0) + aws-sigv4 (~> 1.1) + aws-sdk-codegurureviewer (1.14.0) + aws-sdk-core (~> 3, >= 3.109.0) + aws-sigv4 (~> 1.1) + aws-sdk-codepipeline (1.39.0) + aws-sdk-core (~> 3, >= 3.109.0) + aws-sigv4 (~> 1.1) + aws-sdk-codestar (1.27.0) + aws-sdk-core (~> 3, >= 3.109.0) + aws-sigv4 (~> 1.1) + aws-sdk-codestarconnections (1.12.0) + aws-sdk-core (~> 3, >= 3.109.0) + aws-sigv4 (~> 1.1) + aws-sdk-codestarnotifications (1.8.0) + aws-sdk-core (~> 3, >= 3.109.0) + aws-sigv4 (~> 1.1) + aws-sdk-cognitoidentity (1.29.0) + aws-sdk-core (~> 3, >= 3.109.0) + aws-sigv4 (~> 1.1) + aws-sdk-cognitoidentityprovider (1.48.0) + aws-sdk-core (~> 3, >= 3.109.0) + aws-sigv4 (~> 1.1) + aws-sdk-cognitosync (1.24.0) + aws-sdk-core (~> 3, >= 3.109.0) + aws-sigv4 (~> 1.1) + aws-sdk-comprehend (1.42.0) + aws-sdk-core (~> 3, >= 3.109.0) + aws-sigv4 (~> 1.1) + aws-sdk-comprehendmedical (1.23.0) + aws-sdk-core (~> 3, >= 3.109.0) + aws-sigv4 (~> 1.1) + aws-sdk-computeoptimizer (1.11.0) + aws-sdk-core (~> 3, >= 3.109.0) + aws-sigv4 (~> 1.1) + aws-sdk-configservice (1.55.0) + aws-sdk-core (~> 3, >= 3.109.0) + aws-sigv4 (~> 1.1) + aws-sdk-connect (1.38.0) + aws-sdk-core (~> 3, >= 3.109.0) + aws-sigv4 (~> 1.1) + aws-sdk-connectcontactlens (1.0.0) + aws-sdk-core (~> 3, >= 3.109.0) + aws-sigv4 (~> 1.1) + aws-sdk-connectparticipant (1.9.0) + aws-sdk-core (~> 3, >= 3.109.0) + aws-sigv4 (~> 1.1) + aws-sdk-core (3.111.2) + aws-eventstream (~> 1, >= 1.0.2) + aws-partitions (~> 1, >= 1.239.0) + aws-sigv4 (~> 1.1) + jmespath (~> 1.0) + aws-sdk-costandusagereportservice (1.28.0) + aws-sdk-core (~> 3, >= 3.109.0) + aws-sigv4 (~> 1.1) + aws-sdk-costexplorer (1.56.0) + aws-sdk-core (~> 3, >= 3.109.0) + aws-sigv4 (~> 1.1) + aws-sdk-customerprofiles (1.1.0) + aws-sdk-core (~> 3, >= 3.109.0) + aws-sigv4 (~> 1.1) + aws-sdk-databasemigrationservice (1.50.0) + aws-sdk-core (~> 3, >= 3.109.0) + aws-sigv4 (~> 1.1) + aws-sdk-dataexchange (1.10.0) + aws-sdk-core (~> 3, >= 3.109.0) + aws-sigv4 (~> 1.1) + aws-sdk-datapipeline (1.24.0) + aws-sdk-core (~> 3, >= 3.109.0) + aws-sigv4 (~> 1.1) + aws-sdk-datasync (1.28.0) + aws-sdk-core (~> 3, >= 3.109.0) + aws-sigv4 (~> 1.1) + aws-sdk-dax (1.27.0) + aws-sdk-core (~> 3, >= 3.109.0) + aws-sigv4 (~> 1.1) + aws-sdk-detective (1.11.0) + aws-sdk-core (~> 3, >= 3.109.0) + aws-sigv4 (~> 1.1) + aws-sdk-devicefarm (1.39.0) + aws-sdk-core (~> 3, >= 3.109.0) + aws-sigv4 (~> 1.1) + aws-sdk-devopsguru (1.2.0) + aws-sdk-core (~> 3, >= 3.109.0) + aws-sigv4 (~> 1.1) + aws-sdk-directconnect (1.37.0) + aws-sdk-core (~> 3, >= 3.109.0) + aws-sigv4 (~> 1.1) + aws-sdk-directoryservice (1.37.0) + aws-sdk-core (~> 3, >= 3.109.0) + aws-sigv4 (~> 1.1) + aws-sdk-dlm (1.37.0) + aws-sdk-core (~> 3, >= 3.109.0) + aws-sigv4 (~> 1.1) + aws-sdk-docdb (1.27.0) + aws-sdk-core (~> 3, >= 3.109.0) + aws-sigv4 (~> 1.1) + aws-sdk-dynamodb (1.58.0) + aws-sdk-core (~> 3, >= 3.109.0) + aws-sigv4 (~> 1.1) + aws-sdk-dynamodbstreams (1.27.0) + aws-sdk-core (~> 3, >= 3.109.0) + aws-sigv4 (~> 1.1) + aws-sdk-ebs (1.11.0) + aws-sdk-core (~> 3, >= 3.109.0) + aws-sigv4 (~> 1.1) + aws-sdk-ec2 (1.221.0) + aws-sdk-core (~> 3, >= 3.109.0) + aws-sigv4 (~> 1.1) + aws-sdk-ec2instanceconnect (1.11.0) + aws-sdk-core (~> 3, >= 3.109.0) + aws-sigv4 (~> 1.1) + aws-sdk-ecr (1.40.0) + aws-sdk-core (~> 3, >= 3.109.0) + aws-sigv4 (~> 1.1) + aws-sdk-ecrpublic (1.0.0) + aws-sdk-core (~> 3, >= 3.109.0) + aws-sigv4 (~> 1.1) + aws-sdk-ecs (1.73.0) + aws-sdk-core (~> 3, >= 3.109.0) + aws-sigv4 (~> 1.1) + aws-sdk-efs (1.36.0) + aws-sdk-core (~> 3, >= 3.109.0) + aws-sigv4 (~> 1.1) + aws-sdk-eks (1.46.0) + aws-sdk-core (~> 3, >= 3.109.0) + aws-sigv4 (~> 1.1) + aws-sdk-elasticache (1.50.0) + aws-sdk-core (~> 3, >= 3.109.0) + aws-sigv4 (~> 1.1) + aws-sdk-elasticbeanstalk (1.40.0) + aws-sdk-core (~> 3, >= 3.109.0) + aws-sigv4 (~> 1.1) + aws-sdk-elasticinference (1.10.0) + aws-sdk-core (~> 3, >= 3.109.0) + aws-sigv4 (~> 1.1) + aws-sdk-elasticloadbalancing (1.29.0) + aws-sdk-core (~> 3, >= 3.109.0) + aws-sigv4 (~> 1.1) + aws-sdk-elasticloadbalancingv2 (1.56.0) + aws-sdk-core (~> 3, >= 3.109.0) + aws-sigv4 (~> 1.1) + aws-sdk-elasticsearchservice (1.46.0) + aws-sdk-core (~> 3, >= 3.109.0) + aws-sigv4 (~> 1.1) + aws-sdk-elastictranscoder (1.27.0) + aws-sdk-core (~> 3, >= 3.109.0) + aws-sigv4 (~> 1.1) + aws-sdk-emr (1.40.0) + aws-sdk-core (~> 3, >= 3.109.0) + aws-sigv4 (~> 1.1) + aws-sdk-emrcontainers (1.0.0) + aws-sdk-core (~> 3, >= 3.109.0) + aws-sigv4 (~> 1.1) + aws-sdk-eventbridge (1.18.0) + aws-sdk-core (~> 3, >= 3.109.0) + aws-sigv4 (~> 1.1) + aws-sdk-firehose (1.35.0) + aws-sdk-core (~> 3, >= 3.109.0) + aws-sigv4 (~> 1.1) + aws-sdk-fms (1.33.0) + aws-sdk-core (~> 3, >= 3.109.0) + aws-sigv4 (~> 1.1) + aws-sdk-forecastqueryservice (1.10.0) + aws-sdk-core (~> 3, >= 3.109.0) + aws-sigv4 (~> 1.1) + aws-sdk-forecastservice (1.14.0) + aws-sdk-core (~> 3, >= 3.109.0) + aws-sigv4 (~> 1.1) + aws-sdk-frauddetector (1.15.0) + aws-sdk-core (~> 3, >= 3.109.0) + aws-sigv4 (~> 1.1) + aws-sdk-fsx (1.33.0) + aws-sdk-core (~> 3, >= 3.109.0) + aws-sigv4 (~> 1.1) + aws-sdk-gamelift (1.39.0) + aws-sdk-core (~> 3, >= 3.109.0) + aws-sigv4 (~> 1.1) + aws-sdk-glacier (1.35.0) + aws-sdk-core (~> 3, >= 3.109.0) + aws-sigv4 (~> 1.1) + aws-sdk-globalaccelerator (1.27.0) + aws-sdk-core (~> 3, >= 3.109.0) + aws-sigv4 (~> 1.1) + aws-sdk-glue (1.82.0) + aws-sdk-core (~> 3, >= 3.109.0) + aws-sigv4 (~> 1.1) + aws-sdk-gluedatabrew (1.0.0) + aws-sdk-core (~> 3, >= 3.109.0) + aws-sigv4 (~> 1.1) + aws-sdk-greengrass (1.37.0) + aws-sdk-core (~> 3, >= 3.109.0) + aws-sigv4 (~> 1.1) + aws-sdk-greengrassv2 (1.1.0) + aws-sdk-core (~> 3, >= 3.109.0) + aws-sigv4 (~> 1.1) + aws-sdk-groundstation (1.15.0) + aws-sdk-core (~> 3, >= 3.109.0) + aws-sigv4 (~> 1.1) + aws-sdk-guardduty (1.43.0) + aws-sdk-core (~> 3, >= 3.109.0) + aws-sigv4 (~> 1.1) + aws-sdk-health (1.31.0) + aws-sdk-core (~> 3, >= 3.109.0) + aws-sigv4 (~> 1.1) + aws-sdk-healthlake (1.1.0) + aws-sdk-core (~> 3, >= 3.109.0) + aws-sigv4 (~> 1.1) + aws-sdk-honeycode (1.4.0) + aws-sdk-core (~> 3, >= 3.109.0) + aws-sigv4 (~> 1.1) + aws-sdk-iam (1.46.0) + aws-sdk-core (~> 3, >= 3.109.0) + aws-sigv4 (~> 1.1) + aws-sdk-identitystore (1.3.0) + aws-sdk-core (~> 3, >= 3.109.0) + aws-sigv4 (~> 1.1) + aws-sdk-imagebuilder (1.17.0) + aws-sdk-core (~> 3, >= 3.109.0) + aws-sigv4 (~> 1.1) + aws-sdk-importexport (1.24.0) + aws-sdk-core (~> 3, >= 3.109.0) + aws-sigv2 (~> 1.0) + aws-sdk-inspector (1.32.0) + aws-sdk-core (~> 3, >= 3.109.0) + aws-sigv4 (~> 1.1) + aws-sdk-iot (1.64.0) + aws-sdk-core (~> 3, >= 3.109.0) + aws-sigv4 (~> 1.1) + aws-sdk-iot1clickdevicesservice (1.26.0) + aws-sdk-core (~> 3, >= 3.109.0) + aws-sigv4 (~> 1.1) + aws-sdk-iot1clickprojects (1.26.0) + aws-sdk-core (~> 3, >= 3.109.0) + aws-sigv4 (~> 1.1) + aws-sdk-iotanalytics (1.36.0) + aws-sdk-core (~> 3, >= 3.109.0) + aws-sigv4 (~> 1.1) + aws-sdk-iotdataplane (1.26.0) + aws-sdk-core (~> 3, >= 3.109.0) + aws-sigv4 (~> 1.1) + aws-sdk-iotdeviceadvisor (1.0.0) + aws-sdk-core (~> 3, >= 3.109.0) + aws-sigv4 (~> 1.1) + aws-sdk-iotevents (1.20.0) + aws-sdk-core (~> 3, >= 3.109.0) + aws-sigv4 (~> 1.1) + aws-sdk-ioteventsdata (1.13.0) + aws-sdk-core (~> 3, >= 3.109.0) + aws-sigv4 (~> 1.1) + aws-sdk-iotfleethub (1.0.0) + aws-sdk-core (~> 3, >= 3.109.0) + aws-sigv4 (~> 1.1) + aws-sdk-iotjobsdataplane (1.25.0) + aws-sdk-core (~> 3, >= 3.109.0) + aws-sigv4 (~> 1.1) + aws-sdk-iotsecuretunneling (1.9.0) + aws-sdk-core (~> 3, >= 3.109.0) + aws-sigv4 (~> 1.1) + aws-sdk-iotsitewise (1.16.0) + aws-sdk-core (~> 3, >= 3.109.0) + aws-sigv4 (~> 1.1) + aws-sdk-iotthingsgraph (1.12.0) + aws-sdk-core (~> 3, >= 3.109.0) + aws-sigv4 (~> 1.1) + aws-sdk-iotwireless (1.1.0) + aws-sdk-core (~> 3, >= 3.109.0) + aws-sigv4 (~> 1.1) + aws-sdk-ivs (1.5.0) + aws-sdk-core (~> 3, >= 3.109.0) + aws-sigv4 (~> 1.1) + aws-sdk-kafka (1.33.0) + aws-sdk-core (~> 3, >= 3.109.0) + aws-sigv4 (~> 1.1) + aws-sdk-kendra (1.20.0) + aws-sdk-core (~> 3, >= 3.109.0) + aws-sigv4 (~> 1.1) + aws-sdk-kinesis (1.30.0) + aws-sdk-core (~> 3, >= 3.109.0) + aws-sigv4 (~> 1.1) + aws-sdk-kinesisanalytics (1.29.0) + aws-sdk-core (~> 3, >= 3.109.0) + aws-sigv4 (~> 1.1) + aws-sdk-kinesisanalyticsv2 (1.24.0) + aws-sdk-core (~> 3, >= 3.109.0) + aws-sigv4 (~> 1.1) + aws-sdk-kinesisvideo (1.30.0) + aws-sdk-core (~> 3, >= 3.109.0) + aws-sigv4 (~> 1.1) + aws-sdk-kinesisvideoarchivedmedia (1.29.0) + aws-sdk-core (~> 3, >= 3.109.0) + aws-sigv4 (~> 1.1) + aws-sdk-kinesisvideomedia (1.26.0) + aws-sdk-core (~> 3, >= 3.109.0) + aws-sigv4 (~> 1.1) + aws-sdk-kinesisvideosignalingchannels (1.8.0) + aws-sdk-core (~> 3, >= 3.109.0) + aws-sigv4 (~> 1.1) + aws-sdk-kms (1.41.0) + aws-sdk-core (~> 3, >= 3.109.0) + aws-sigv4 (~> 1.1) + aws-sdk-lakeformation (1.11.0) + aws-sdk-core (~> 3, >= 3.109.0) + aws-sigv4 (~> 1.1) + aws-sdk-lambda (1.57.0) + aws-sdk-core (~> 3, >= 3.109.0) + aws-sigv4 (~> 1.1) + aws-sdk-lambdapreview (1.24.0) + aws-sdk-core (~> 3, >= 3.109.0) + aws-sigv4 (~> 1.1) + aws-sdk-lex (1.33.0) + aws-sdk-core (~> 3, >= 3.109.0) + aws-sigv4 (~> 1.1) + aws-sdk-lexmodelbuildingservice (1.42.0) + aws-sdk-core (~> 3, >= 3.109.0) + aws-sigv4 (~> 1.1) + aws-sdk-lexmodelsv2 (1.0.0) + aws-sdk-core (~> 3, >= 3.109.0) + aws-sigv4 (~> 1.1) + aws-sdk-lexruntimev2 (1.0.0) + aws-sdk-core (~> 3, >= 3.109.0) + aws-sigv4 (~> 1.1) + aws-sdk-licensemanager (1.23.0) + aws-sdk-core (~> 3, >= 3.109.0) + aws-sigv4 (~> 1.1) + aws-sdk-lightsail (1.41.0) + aws-sdk-core (~> 3, >= 3.109.0) + aws-sigv4 (~> 1.1) + aws-sdk-locationservice (1.0.0) + aws-sdk-core (~> 3, >= 3.109.0) + aws-sigv4 (~> 1.1) + aws-sdk-lookoutforvision (1.0.0) + aws-sdk-core (~> 3, >= 3.109.0) + aws-sigv4 (~> 1.1) + aws-sdk-machinelearning (1.25.0) + aws-sdk-core (~> 3, >= 3.109.0) + aws-sigv4 (~> 1.1) + aws-sdk-macie (1.25.0) + aws-sdk-core (~> 3, >= 3.109.0) + aws-sigv4 (~> 1.1) + aws-sdk-macie2 (1.19.0) + aws-sdk-core (~> 3, >= 3.109.0) + aws-sigv4 (~> 1.1) + aws-sdk-managedblockchain (1.18.0) + aws-sdk-core (~> 3, >= 3.109.0) + aws-sigv4 (~> 1.1) + aws-sdk-marketplacecatalog (1.9.0) + aws-sdk-core (~> 3, >= 3.109.0) + aws-sigv4 (~> 1.1) + aws-sdk-marketplacecommerceanalytics (1.30.0) + aws-sdk-core (~> 3, >= 3.109.0) + aws-sigv4 (~> 1.1) + aws-sdk-marketplaceentitlementservice (1.24.0) + aws-sdk-core (~> 3, >= 3.109.0) + aws-sigv4 (~> 1.1) + aws-sdk-marketplacemetering (1.32.0) + aws-sdk-core (~> 3, >= 3.109.0) + aws-sigv4 (~> 1.1) + aws-sdk-mediaconnect (1.28.0) + aws-sdk-core (~> 3, >= 3.109.0) + aws-sigv4 (~> 1.1) + aws-sdk-mediaconvert (1.61.0) + aws-sdk-core (~> 3, >= 3.109.0) + aws-sigv4 (~> 1.1) + aws-sdk-medialive (1.61.0) + aws-sdk-core (~> 3, >= 3.109.0) + aws-sigv4 (~> 1.1) + aws-sdk-mediapackage (1.36.0) + aws-sdk-core (~> 3, >= 3.109.0) + aws-sigv4 (~> 1.1) + aws-sdk-mediapackagevod (1.19.0) + aws-sdk-core (~> 3, >= 3.109.0) + aws-sigv4 (~> 1.1) + aws-sdk-mediastore (1.30.0) + aws-sdk-core (~> 3, >= 3.109.0) + aws-sigv4 (~> 1.1) + aws-sdk-mediastoredata (1.27.0) + aws-sdk-core (~> 3, >= 3.109.0) + aws-sigv4 (~> 1.1) + aws-sdk-mediatailor (1.33.0) + aws-sdk-core (~> 3, >= 3.109.0) + aws-sigv4 (~> 1.1) + aws-sdk-migrationhub (1.29.0) + aws-sdk-core (~> 3, >= 3.109.0) + aws-sigv4 (~> 1.1) + aws-sdk-migrationhubconfig (1.9.0) + aws-sdk-core (~> 3, >= 3.109.0) + aws-sigv4 (~> 1.1) + aws-sdk-mobile (1.24.0) + aws-sdk-core (~> 3, >= 3.109.0) + aws-sigv4 (~> 1.1) + aws-sdk-mq (1.34.0) + aws-sdk-core (~> 3, >= 3.109.0) + aws-sigv4 (~> 1.1) + aws-sdk-mturk (1.27.0) + aws-sdk-core (~> 3, >= 3.109.0) + aws-sigv4 (~> 1.1) + aws-sdk-mwaa (1.0.0) + aws-sdk-core (~> 3, >= 3.109.0) + aws-sigv4 (~> 1.1) + aws-sdk-neptune (1.32.0) + aws-sdk-core (~> 3, >= 3.109.0) + aws-sigv4 (~> 1.1) + aws-sdk-networkfirewall (1.0.0) + aws-sdk-core (~> 3, >= 3.109.0) + aws-sigv4 (~> 1.1) + aws-sdk-networkmanager (1.9.0) + aws-sdk-core (~> 3, >= 3.109.0) + aws-sigv4 (~> 1.1) + aws-sdk-opsworks (1.30.0) + aws-sdk-core (~> 3, >= 3.109.0) + aws-sigv4 (~> 1.1) + aws-sdk-opsworkscm (1.40.0) + aws-sdk-core (~> 3, >= 3.109.0) + aws-sigv4 (~> 1.1) + aws-sdk-organizations (1.55.0) + aws-sdk-core (~> 3, >= 3.109.0) + aws-sigv4 (~> 1.1) + aws-sdk-outposts (1.13.0) + aws-sdk-core (~> 3, >= 3.109.0) + aws-sigv4 (~> 1.1) + aws-sdk-personalize (1.20.0) + aws-sdk-core (~> 3, >= 3.109.0) + aws-sigv4 (~> 1.1) + aws-sdk-personalizeevents (1.14.0) + aws-sdk-core (~> 3, >= 3.109.0) + aws-sigv4 (~> 1.1) + aws-sdk-personalizeruntime (1.20.0) + aws-sdk-core (~> 3, >= 3.109.0) + aws-sigv4 (~> 1.1) + aws-sdk-pi (1.25.0) + aws-sdk-core (~> 3, >= 3.109.0) + aws-sigv4 (~> 1.1) + aws-sdk-pinpoint (1.48.0) + aws-sdk-core (~> 3, >= 3.109.0) + aws-sigv4 (~> 1.1) + aws-sdk-pinpointemail (1.24.0) + aws-sdk-core (~> 3, >= 3.109.0) + aws-sigv4 (~> 1.1) + aws-sdk-pinpointsmsvoice (1.21.0) + aws-sdk-core (~> 3, >= 3.109.0) + aws-sigv4 (~> 1.1) + aws-sdk-polly (1.38.0) + aws-sdk-core (~> 3, >= 3.109.0) + aws-sigv4 (~> 1.1) + aws-sdk-pricing (1.24.0) + aws-sdk-core (~> 3, >= 3.109.0) + aws-sigv4 (~> 1.1) + aws-sdk-prometheusservice (1.1.0) + aws-sdk-core (~> 3, >= 3.109.0) + aws-sigv4 (~> 1.1) + aws-sdk-qldb (1.11.0) + aws-sdk-core (~> 3, >= 3.109.0) + aws-sigv4 (~> 1.1) + aws-sdk-qldbsession (1.10.0) + aws-sdk-core (~> 3, >= 3.109.0) + aws-sigv4 (~> 1.1) + aws-sdk-quicksight (1.39.0) + aws-sdk-core (~> 3, >= 3.109.0) + aws-sigv4 (~> 1.1) + aws-sdk-ram (1.22.0) + aws-sdk-core (~> 3, >= 3.109.0) + aws-sigv4 (~> 1.1) + aws-sdk-rds (1.111.0) + aws-sdk-core (~> 3, >= 3.109.0) + aws-sigv4 (~> 1.1) + aws-sdk-rdsdataservice (1.23.0) + aws-sdk-core (~> 3, >= 3.109.0) + aws-sigv4 (~> 1.1) + aws-sdk-redshift (1.53.0) + aws-sdk-core (~> 3, >= 3.109.0) + aws-sigv4 (~> 1.1) + aws-sdk-redshiftdataapiservice (1.2.0) + aws-sdk-core (~> 3, >= 3.109.0) + aws-sigv4 (~> 1.1) + aws-sdk-rekognition (1.47.0) + aws-sdk-core (~> 3, >= 3.109.0) + aws-sigv4 (~> 1.1) + aws-sdk-resourcegroups (1.33.0) + aws-sdk-core (~> 3, >= 3.109.0) + aws-sigv4 (~> 1.1) + aws-sdk-resourcegroupstaggingapi (1.35.0) + aws-sdk-core (~> 3, >= 3.109.0) + aws-sigv4 (~> 1.1) + aws-sdk-resources (3.94.0) + aws-sdk-accessanalyzer (~> 1) + aws-sdk-acm (~> 1) + aws-sdk-acmpca (~> 1) + aws-sdk-alexaforbusiness (~> 1) + aws-sdk-amplify (~> 1) + aws-sdk-amplifybackend (~> 1) + aws-sdk-apigateway (~> 1) + aws-sdk-apigatewaymanagementapi (~> 1) + aws-sdk-apigatewayv2 (~> 1) + aws-sdk-appconfig (~> 1) + aws-sdk-appflow (~> 1) + aws-sdk-appintegrationsservice (~> 1) + aws-sdk-applicationautoscaling (~> 1) + aws-sdk-applicationdiscoveryservice (~> 1) + aws-sdk-applicationinsights (~> 1) + aws-sdk-appmesh (~> 1) + aws-sdk-appregistry (~> 1) + aws-sdk-appstream (~> 1) + aws-sdk-appsync (~> 1) + aws-sdk-athena (~> 1) + aws-sdk-auditmanager (~> 1) + aws-sdk-augmentedairuntime (~> 1) + aws-sdk-autoscaling (~> 1) + aws-sdk-autoscalingplans (~> 1) + aws-sdk-backup (~> 1) + aws-sdk-batch (~> 1) + aws-sdk-braket (~> 1) + aws-sdk-budgets (~> 1) + aws-sdk-chime (~> 1) + aws-sdk-cloud9 (~> 1) + aws-sdk-clouddirectory (~> 1) + aws-sdk-cloudformation (~> 1) + aws-sdk-cloudfront (~> 1) + aws-sdk-cloudhsm (~> 1) + aws-sdk-cloudhsmv2 (~> 1) + aws-sdk-cloudsearch (~> 1) + aws-sdk-cloudsearchdomain (~> 1) + aws-sdk-cloudtrail (~> 1) + aws-sdk-cloudwatch (~> 1) + aws-sdk-cloudwatchevents (~> 1) + aws-sdk-cloudwatchlogs (~> 1) + aws-sdk-codeartifact (~> 1) + aws-sdk-codebuild (~> 1) + aws-sdk-codecommit (~> 1) + aws-sdk-codedeploy (~> 1) + aws-sdk-codeguruprofiler (~> 1) + aws-sdk-codegurureviewer (~> 1) + aws-sdk-codepipeline (~> 1) + aws-sdk-codestar (~> 1) + aws-sdk-codestarconnections (~> 1) + aws-sdk-codestarnotifications (~> 1) + aws-sdk-cognitoidentity (~> 1) + aws-sdk-cognitoidentityprovider (~> 1) + aws-sdk-cognitosync (~> 1) + aws-sdk-comprehend (~> 1) + aws-sdk-comprehendmedical (~> 1) + aws-sdk-computeoptimizer (~> 1) + aws-sdk-configservice (~> 1) + aws-sdk-connect (~> 1) + aws-sdk-connectcontactlens (~> 1) + aws-sdk-connectparticipant (~> 1) + aws-sdk-costandusagereportservice (~> 1) + aws-sdk-costexplorer (~> 1) + aws-sdk-customerprofiles (~> 1) + aws-sdk-databasemigrationservice (~> 1) + aws-sdk-dataexchange (~> 1) + aws-sdk-datapipeline (~> 1) + aws-sdk-datasync (~> 1) + aws-sdk-dax (~> 1) + aws-sdk-detective (~> 1) + aws-sdk-devicefarm (~> 1) + aws-sdk-devopsguru (~> 1) + aws-sdk-directconnect (~> 1) + aws-sdk-directoryservice (~> 1) + aws-sdk-dlm (~> 1) + aws-sdk-docdb (~> 1) + aws-sdk-dynamodb (~> 1) + aws-sdk-dynamodbstreams (~> 1) + aws-sdk-ebs (~> 1) + aws-sdk-ec2 (~> 1) + aws-sdk-ec2instanceconnect (~> 1) + aws-sdk-ecr (~> 1) + aws-sdk-ecrpublic (~> 1) + aws-sdk-ecs (~> 1) + aws-sdk-efs (~> 1) + aws-sdk-eks (~> 1) + aws-sdk-elasticache (~> 1) + aws-sdk-elasticbeanstalk (~> 1) + aws-sdk-elasticinference (~> 1) + aws-sdk-elasticloadbalancing (~> 1) + aws-sdk-elasticloadbalancingv2 (~> 1) + aws-sdk-elasticsearchservice (~> 1) + aws-sdk-elastictranscoder (~> 1) + aws-sdk-emr (~> 1) + aws-sdk-emrcontainers (~> 1) + aws-sdk-eventbridge (~> 1) + aws-sdk-firehose (~> 1) + aws-sdk-fms (~> 1) + aws-sdk-forecastqueryservice (~> 1) + aws-sdk-forecastservice (~> 1) + aws-sdk-frauddetector (~> 1) + aws-sdk-fsx (~> 1) + aws-sdk-gamelift (~> 1) + aws-sdk-glacier (~> 1) + aws-sdk-globalaccelerator (~> 1) + aws-sdk-glue (~> 1) + aws-sdk-gluedatabrew (~> 1) + aws-sdk-greengrass (~> 1) + aws-sdk-greengrassv2 (~> 1) + aws-sdk-groundstation (~> 1) + aws-sdk-guardduty (~> 1) + aws-sdk-health (~> 1) + aws-sdk-healthlake (~> 1) + aws-sdk-honeycode (~> 1) + aws-sdk-iam (~> 1) + aws-sdk-identitystore (~> 1) + aws-sdk-imagebuilder (~> 1) + aws-sdk-importexport (~> 1) + aws-sdk-inspector (~> 1) + aws-sdk-iot (~> 1) + aws-sdk-iot1clickdevicesservice (~> 1) + aws-sdk-iot1clickprojects (~> 1) + aws-sdk-iotanalytics (~> 1) + aws-sdk-iotdataplane (~> 1) + aws-sdk-iotdeviceadvisor (~> 1) + aws-sdk-iotevents (~> 1) + aws-sdk-ioteventsdata (~> 1) + aws-sdk-iotfleethub (~> 1) + aws-sdk-iotjobsdataplane (~> 1) + aws-sdk-iotsecuretunneling (~> 1) + aws-sdk-iotsitewise (~> 1) + aws-sdk-iotthingsgraph (~> 1) + aws-sdk-iotwireless (~> 1) + aws-sdk-ivs (~> 1) + aws-sdk-kafka (~> 1) + aws-sdk-kendra (~> 1) + aws-sdk-kinesis (~> 1) + aws-sdk-kinesisanalytics (~> 1) + aws-sdk-kinesisanalyticsv2 (~> 1) + aws-sdk-kinesisvideo (~> 1) + aws-sdk-kinesisvideoarchivedmedia (~> 1) + aws-sdk-kinesisvideomedia (~> 1) + aws-sdk-kinesisvideosignalingchannels (~> 1) + aws-sdk-kms (~> 1) + aws-sdk-lakeformation (~> 1) + aws-sdk-lambda (~> 1) + aws-sdk-lambdapreview (~> 1) + aws-sdk-lex (~> 1) + aws-sdk-lexmodelbuildingservice (~> 1) + aws-sdk-lexmodelsv2 (~> 1) + aws-sdk-lexruntimev2 (~> 1) + aws-sdk-licensemanager (~> 1) + aws-sdk-lightsail (~> 1) + aws-sdk-locationservice (~> 1) + aws-sdk-lookoutforvision (~> 1) + aws-sdk-machinelearning (~> 1) + aws-sdk-macie (~> 1) + aws-sdk-macie2 (~> 1) + aws-sdk-managedblockchain (~> 1) + aws-sdk-marketplacecatalog (~> 1) + aws-sdk-marketplacecommerceanalytics (~> 1) + aws-sdk-marketplaceentitlementservice (~> 1) + aws-sdk-marketplacemetering (~> 1) + aws-sdk-mediaconnect (~> 1) + aws-sdk-mediaconvert (~> 1) + aws-sdk-medialive (~> 1) + aws-sdk-mediapackage (~> 1) + aws-sdk-mediapackagevod (~> 1) + aws-sdk-mediastore (~> 1) + aws-sdk-mediastoredata (~> 1) + aws-sdk-mediatailor (~> 1) + aws-sdk-migrationhub (~> 1) + aws-sdk-migrationhubconfig (~> 1) + aws-sdk-mobile (~> 1) + aws-sdk-mq (~> 1) + aws-sdk-mturk (~> 1) + aws-sdk-mwaa (~> 1) + aws-sdk-neptune (~> 1) + aws-sdk-networkfirewall (~> 1) + aws-sdk-networkmanager (~> 1) + aws-sdk-opsworks (~> 1) + aws-sdk-opsworkscm (~> 1) + aws-sdk-organizations (~> 1) + aws-sdk-outposts (~> 1) + aws-sdk-personalize (~> 1) + aws-sdk-personalizeevents (~> 1) + aws-sdk-personalizeruntime (~> 1) + aws-sdk-pi (~> 1) + aws-sdk-pinpoint (~> 1) + aws-sdk-pinpointemail (~> 1) + aws-sdk-pinpointsmsvoice (~> 1) + aws-sdk-polly (~> 1) + aws-sdk-pricing (~> 1) + aws-sdk-prometheusservice (~> 1) + aws-sdk-qldb (~> 1) + aws-sdk-qldbsession (~> 1) + aws-sdk-quicksight (~> 1) + aws-sdk-ram (~> 1) + aws-sdk-rds (~> 1) + aws-sdk-rdsdataservice (~> 1) + aws-sdk-redshift (~> 1) + aws-sdk-redshiftdataapiservice (~> 1) + aws-sdk-rekognition (~> 1) + aws-sdk-resourcegroups (~> 1) + aws-sdk-resourcegroupstaggingapi (~> 1) + aws-sdk-robomaker (~> 1) + aws-sdk-route53 (~> 1) + aws-sdk-route53domains (~> 1) + aws-sdk-route53resolver (~> 1) + aws-sdk-s3 (~> 1) + aws-sdk-s3control (~> 1) + aws-sdk-s3outposts (~> 1) + aws-sdk-sagemaker (~> 1) + aws-sdk-sagemakeredgemanager (~> 1) + aws-sdk-sagemakerfeaturestoreruntime (~> 1) + aws-sdk-sagemakerruntime (~> 1) + aws-sdk-savingsplans (~> 1) + aws-sdk-schemas (~> 1) + aws-sdk-secretsmanager (~> 1) + aws-sdk-securityhub (~> 1) + aws-sdk-serverlessapplicationrepository (~> 1) + aws-sdk-servicecatalog (~> 1) + aws-sdk-servicediscovery (~> 1) + aws-sdk-servicequotas (~> 1) + aws-sdk-ses (~> 1) + aws-sdk-sesv2 (~> 1) + aws-sdk-shield (~> 1) + aws-sdk-signer (~> 1) + aws-sdk-simpledb (~> 1) + aws-sdk-sms (~> 1) + aws-sdk-snowball (~> 1) + aws-sdk-sns (~> 1) + aws-sdk-sqs (~> 1) + aws-sdk-ssm (~> 1) + aws-sdk-ssoadmin (~> 1) + aws-sdk-ssooidc (~> 1) + aws-sdk-states (~> 1) + aws-sdk-storagegateway (~> 1) + aws-sdk-support (~> 1) + aws-sdk-swf (~> 1) + aws-sdk-synthetics (~> 1) + aws-sdk-textract (~> 1) + aws-sdk-timestreamquery (~> 1) + aws-sdk-timestreamwrite (~> 1) + aws-sdk-transcribeservice (~> 1) + aws-sdk-transcribestreamingservice (~> 1) + aws-sdk-transfer (~> 1) + aws-sdk-translate (~> 1) + aws-sdk-waf (~> 1) + aws-sdk-wafregional (~> 1) + aws-sdk-wafv2 (~> 1) + aws-sdk-wellarchitected (~> 1) + aws-sdk-workdocs (~> 1) + aws-sdk-worklink (~> 1) + aws-sdk-workmail (~> 1) + aws-sdk-workmailmessageflow (~> 1) + aws-sdk-workspaces (~> 1) + aws-sdk-xray (~> 1) + aws-sdk-robomaker (1.31.0) + aws-sdk-core (~> 3, >= 3.109.0) + aws-sigv4 (~> 1.1) + aws-sdk-route53 (1.45.0) + aws-sdk-core (~> 3, >= 3.109.0) + aws-sigv4 (~> 1.1) + aws-sdk-route53domains (1.28.0) + aws-sdk-core (~> 3, >= 3.109.0) + aws-sigv4 (~> 1.1) + aws-sdk-route53resolver (1.22.0) + aws-sdk-core (~> 3, >= 3.109.0) + aws-sigv4 (~> 1.1) + aws-sdk-s3 (1.87.0) + aws-sdk-core (~> 3, >= 3.109.0) + aws-sdk-kms (~> 1) + aws-sigv4 (~> 1.1) + aws-sdk-s3control (1.26.0) + aws-sdk-core (~> 3, >= 3.109.0) + aws-sigv4 (~> 1.1) + aws-sdk-s3outposts (1.0.0) + aws-sdk-core (~> 3, >= 3.109.0) + aws-sigv4 (~> 1.1) + aws-sdk-sagemaker (1.75.0) + aws-sdk-core (~> 3, >= 3.109.0) + aws-sigv4 (~> 1.1) + aws-sdk-sagemakeredgemanager (1.0.0) + aws-sdk-core (~> 3, >= 3.109.0) + aws-sigv4 (~> 1.1) + aws-sdk-sagemakerfeaturestoreruntime (1.0.0) + aws-sdk-core (~> 3, >= 3.109.0) + aws-sigv4 (~> 1.1) + aws-sdk-sagemakerruntime (1.28.0) + aws-sdk-core (~> 3, >= 3.109.0) + aws-sigv4 (~> 1.1) + aws-sdk-savingsplans (1.12.0) + aws-sdk-core (~> 3, >= 3.109.0) + aws-sigv4 (~> 1.1) + aws-sdk-schemas (1.10.0) + aws-sdk-core (~> 3, >= 3.109.0) + aws-sigv4 (~> 1.1) + aws-sdk-secretsmanager (1.43.0) + aws-sdk-core (~> 3, >= 3.109.0) + aws-sigv4 (~> 1.1) + aws-sdk-securityhub (1.38.0) + aws-sdk-core (~> 3, >= 3.109.0) + aws-sigv4 (~> 1.1) + aws-sdk-serverlessapplicationrepository (1.32.0) + aws-sdk-core (~> 3, >= 3.109.0) + aws-sigv4 (~> 1.1) + aws-sdk-servicecatalog (1.57.0) + aws-sdk-core (~> 3, >= 3.109.0) + aws-sigv4 (~> 1.1) + aws-sdk-servicediscovery (1.31.0) + aws-sdk-core (~> 3, >= 3.109.0) + aws-sigv4 (~> 1.1) + aws-sdk-servicequotas (1.12.0) + aws-sdk-core (~> 3, >= 3.109.0) + aws-sigv4 (~> 1.1) + aws-sdk-ses (1.36.0) + aws-sdk-core (~> 3, >= 3.109.0) + aws-sigv4 (~> 1.1) + aws-sdk-sesv2 (1.14.0) + aws-sdk-core (~> 3, >= 3.109.0) + aws-sigv4 (~> 1.1) + aws-sdk-shield (1.33.0) + aws-sdk-core (~> 3, >= 3.109.0) + aws-sigv4 (~> 1.1) + aws-sdk-signer (1.27.0) + aws-sdk-core (~> 3, >= 3.109.0) + aws-sigv4 (~> 1.1) + aws-sdk-simpledb (1.24.0) + aws-sdk-core (~> 3, >= 3.109.0) + aws-sigv2 (~> 1.0) + aws-sdk-sms (1.27.0) + aws-sdk-core (~> 3, >= 3.109.0) + aws-sigv4 (~> 1.1) + aws-sdk-snowball (1.35.0) + aws-sdk-core (~> 3, >= 3.109.0) + aws-sigv4 (~> 1.1) + aws-sdk-sns (1.37.0) + aws-sdk-core (~> 3, >= 3.109.0) + aws-sigv4 (~> 1.1) + aws-sdk-sqs (1.35.0) + aws-sdk-core (~> 3, >= 3.109.0) + aws-sigv4 (~> 1.1) + aws-sdk-ssm (1.103.0) + aws-sdk-core (~> 3, >= 3.109.0) + aws-sigv4 (~> 1.1) + aws-sdk-ssoadmin (1.4.0) + aws-sdk-core (~> 3, >= 3.109.0) + aws-sigv4 (~> 1.1) + aws-sdk-ssooidc (1.8.0) + aws-sdk-core (~> 3, >= 3.109.0) + aws-sigv4 (~> 1.1) + aws-sdk-states (1.37.0) + aws-sdk-core (~> 3, >= 3.109.0) + aws-sigv4 (~> 1.1) + aws-sdk-storagegateway (1.52.0) + aws-sdk-core (~> 3, >= 3.109.0) + aws-sigv4 (~> 1.1) + aws-sdk-support (1.28.0) + aws-sdk-core (~> 3, >= 3.109.0) + aws-sigv4 (~> 1.1) + aws-sdk-swf (1.25.0) + aws-sdk-core (~> 3, >= 3.109.0) + aws-sigv4 (~> 1.1) + aws-sdk-synthetics (1.10.0) + aws-sdk-core (~> 3, >= 3.109.0) + aws-sigv4 (~> 1.1) + aws-sdk-textract (1.22.0) + aws-sdk-core (~> 3, >= 3.109.0) + aws-sigv4 (~> 1.1) + aws-sdk-timestreamquery (1.2.0) + aws-sdk-core (~> 3, >= 3.109.0) + aws-sigv4 (~> 1.1) + aws-sdk-timestreamwrite (1.2.0) + aws-sdk-core (~> 3, >= 3.109.0) + aws-sigv4 (~> 1.1) + aws-sdk-transcribeservice (1.50.0) + aws-sdk-core (~> 3, >= 3.109.0) + aws-sigv4 (~> 1.1) + aws-sdk-transcribestreamingservice (1.24.0) + aws-sdk-core (~> 3, >= 3.109.0) + aws-sigv4 (~> 1.1) + aws-sdk-transfer (1.29.0) + aws-sdk-core (~> 3, >= 3.109.0) + aws-sigv4 (~> 1.1) + aws-sdk-translate (1.29.0) + aws-sdk-core (~> 3, >= 3.109.0) + aws-sigv4 (~> 1.1) + aws-sdk-waf (1.36.0) + aws-sdk-core (~> 3, >= 3.109.0) + aws-sigv4 (~> 1.1) + aws-sdk-wafregional (1.37.0) + aws-sdk-core (~> 3, >= 3.109.0) + aws-sigv4 (~> 1.1) + aws-sdk-wafv2 (1.14.0) + aws-sdk-core (~> 3, >= 3.109.0) + aws-sigv4 (~> 1.1) + aws-sdk-wellarchitected (1.0.0) + aws-sdk-core (~> 3, >= 3.109.0) + aws-sigv4 (~> 1.1) + aws-sdk-workdocs (1.28.0) + aws-sdk-core (~> 3, >= 3.109.0) + aws-sigv4 (~> 1.1) + aws-sdk-worklink (1.21.0) + aws-sdk-core (~> 3, >= 3.109.0) + aws-sigv4 (~> 1.1) + aws-sdk-workmail (1.33.0) + aws-sdk-core (~> 3, >= 3.109.0) + aws-sigv4 (~> 1.1) + aws-sdk-workmailmessageflow (1.9.0) + aws-sdk-core (~> 3, >= 3.109.0) + aws-sigv4 (~> 1.1) + aws-sdk-workspaces (1.49.0) + aws-sdk-core (~> 3, >= 3.109.0) + aws-sigv4 (~> 1.1) + aws-sdk-xray (1.35.0) + aws-sdk-core (~> 3, >= 3.109.0) + aws-sigv4 (~> 1.1) + aws-sigv2 (1.0.1) + aws-sigv4 (1.2.2) + aws-eventstream (~> 1, >= 1.0.2) + babel-source (5.8.35) + babel-transpiler (0.7.0) + babel-source (>= 4.0, < 6) + execjs (~> 2.0) + barnes (0.0.8) + multi_json (~> 1) + statsd-ruby (~> 1.1) + better_errors (2.9.1) + coderay (>= 1.0.0) + erubi (>= 1.0.0) + rack (>= 0.9.0) + binding_of_caller (1.0.0) + debug_inspector (>= 0.0.1) + bootsnap (1.5.1) + msgpack (~> 1.0) + bootstrap (4.5.3) + autoprefixer-rails (>= 9.1.0) + popper_js (>= 1.14.3, < 2) + sassc-rails (>= 2.0.0) + browser (5.3.0) + builder (3.2.4) + byebug (11.1.3) + capybara (3.34.0) + addressable + mini_mime (>= 0.1.3) + nokogiri (~> 1.8) + rack (>= 1.6.0) + rack-test (>= 0.6.3) + regexp_parser (~> 1.5) + xpath (~> 3.2) + childprocess (3.0.0) + coderay (1.1.3) + commonmarker (0.21.1) + ruby-enum (~> 0.5) + concurrent-ruby (1.1.8) + connection_pool (2.2.3) + crack (0.4.5) + rexml + crass (1.0.6) + database_cleaner (1.8.5) + debug_inspector (1.0.0) + deterministic (0.6.0) + diff-lcs (1.4.4) + docile (1.3.5) + dotenv (2.7.6) + dotenv-rails (2.7.6) + dotenv (= 2.7.6) + railties (>= 3.2) + erubi (1.10.0) + execjs (2.7.0) + factory_bot (6.1.0) + activesupport (>= 5.0.0) + factory_bot_rails (6.1.0) + factory_bot (~> 6.1.0) + railties (>= 5.0.0) + faraday (1.3.0) + faraday-net_http (~> 1.0) + multipart-post (>= 1.2, < 3) + ruby2_keywords + faraday-net_http (1.0.1) + ffi (1.14.2) + flamegraph (0.9.5) + font-awesome-rails (4.7.0.6) + railties (>= 3.2, < 6.2) + foreman (0.87.2) + github-markup (3.0.5) + github_url (0.2.1) + gitlab (4.17.0) + httparty (~> 0.18) + terminal-table (~> 1.5, >= 1.5.1) + globalid (0.4.2) + activesupport (>= 4.2.0) + haml (5.2.1) + temple (>= 0.8.0) + tilt + hashdiff (1.0.1) + honeybadger (4.7.2) + httparty (0.18.1) + mime-types (~> 3.0) + multi_xml (>= 0.5.2) + i18n (1.8.7) + concurrent-ruby (~> 1.0) + iniparse (1.5.0) + jmespath (1.4.0) + jquery-rails (4.4.0) + rails-dom-testing (>= 1, < 3) + railties (>= 4.2.0) + thor (>= 0.14, < 2.0) + js-routes (1.4.14) + railties (>= 4) + json_spec (1.1.5) + multi_json (~> 1.0) + rspec (>= 2.0, < 4.0) + jwt (2.2.2) + kramdown (2.3.0) + rexml + launchy (2.5.0) + addressable (~> 2.7) + letter_opener (1.7.0) + launchy (~> 2.2) + listen (3.4.1) + rb-fsevent (~> 0.10, >= 0.10.3) + rb-inotify (~> 0.9, >= 0.9.10) + loofah (2.9.0) + crass (~> 1.0.2) + nokogiri (>= 1.5.9) + mail (2.7.1) + mini_mime (>= 0.1.1) + marcel (0.3.3) + mimemagic (~> 0.3.2) + mathjax-rails (2.6.1) + railties (>= 3.0) + memory_profiler (1.0.0) + method_source (1.0.0) + mime-types (3.3.1) + mime-types-data (~> 3.2015) + mime-types-data (3.2020.1104) + mimemagic (0.3.5) + mini_mime (1.0.2) + mini_portile2 (2.5.0) + minitest (5.14.3) + msgpack (1.3.3) + multi_json (1.15.0) + multi_xml (0.6.0) + multipart-post (2.1.1) + mustache (1.1.1) + nio4r (2.5.4) + nokogiri (1.11.1) + mini_portile2 (~> 2.5.0) + racc (~> 1.4) + octokit (4.20.0) + faraday (>= 0.9) + sawyer (~> 0.8.0, >= 0.5.3) + overcommit (0.57.0) + childprocess (>= 0.6.3, < 5) + iniparse (~> 1.4) + pagy (3.10.0) + parallel (1.20.1) + parser (3.0.0.0) + ast (~> 2.4.1) + pg (1.2.3) + popper_js (1.16.0) + psych (3.3.0) + public_suffix (4.0.6) + puma (5.1.1) + nio4r (~> 2.0) + pundit (2.1.0) + activesupport (>= 3.0.0) + racc (1.5.2) + rack (2.2.3) + rack-attack (6.3.1) + rack (>= 1.0, < 3) + rack-mini-profiler (2.3.0) + rack (>= 1.2.0) + rack-proxy (0.6.5) + rack + rack-test (1.1.0) + rack (>= 1.0, < 3) + rails (6.1.1) + actioncable (= 6.1.1) + actionmailbox (= 6.1.1) + actionmailer (= 6.1.1) + actionpack (= 6.1.1) + actiontext (= 6.1.1) + actionview (= 6.1.1) + activejob (= 6.1.1) + activemodel (= 6.1.1) + activerecord (= 6.1.1) + activestorage (= 6.1.1) + activesupport (= 6.1.1) + bundler (>= 1.15.0) + railties (= 6.1.1) + sprockets-rails (>= 2.0.0) + rails-controller-testing (1.0.5) + actionpack (>= 5.0.1.rc1) + actionview (>= 5.0.1.rc1) + activesupport (>= 5.0.1.rc1) + rails-dom-testing (2.0.3) + activesupport (>= 4.2.0) + nokogiri (>= 1.6) + rails-html-sanitizer (1.3.0) + loofah (~> 2.3) + railties (6.1.1) + actionpack (= 6.1.1) + activesupport (= 6.1.1) + method_source + rake (>= 0.8.7) + thor (~> 1.0) + rainbow (3.0.0) + rake (13.0.3) + rb-fsevent (0.10.4) + rb-inotify (0.10.1) + ffi (~> 1.0) + react-rails (2.6.1) + babel-transpiler (>= 0.7.0) + connection_pool + execjs + railties (>= 3.2) + tilt + redcarpet (3.5.1) + redis (4.2.5) + regexp_parser (1.8.2) + rexml (3.2.4) + rspec (3.10.0) + rspec-core (~> 3.10.0) + rspec-expectations (~> 3.10.0) + rspec-mocks (~> 3.10.0) + rspec-core (3.10.1) + rspec-support (~> 3.10.0) + rspec-expectations (3.10.1) + diff-lcs (>= 1.2.0, < 2.0) + rspec-support (~> 3.10.0) + rspec-mocks (3.10.1) + diff-lcs (>= 1.2.0, < 2.0) + rspec-support (~> 3.10.0) + rspec-rails (4.0.2) + actionpack (>= 4.2) + activesupport (>= 4.2) + railties (>= 4.2) + rspec-core (~> 3.10) + rspec-expectations (~> 3.10) + rspec-mocks (~> 3.10) + rspec-support (~> 3.10) + rspec-support (3.10.1) + rspec_api_documentation (6.1.0) + activesupport (>= 3.0.0) + mustache (~> 1.0, >= 0.99.4) + rspec (~> 3.0) + rspec_junit_formatter (0.4.1) + rspec-core (>= 2, < 4, != 2.12.0) + rubocop (1.8.1) + parallel (~> 1.10) + parser (>= 3.0.0.0) + rainbow (>= 2.2.2, < 4.0) + regexp_parser (>= 1.8, < 3.0) + rexml + rubocop-ast (>= 1.2.0, < 2.0) + ruby-progressbar (~> 1.7) + unicode-display_width (>= 1.4.0, < 3.0) + rubocop-ast (1.4.0) + parser (>= 2.7.1.5) + ruby-enum (0.8.0) + i18n + ruby-progressbar (1.11.0) + ruby2_keywords (0.0.4) + rubyzip (2.3.0) + sass-rails (6.0.0) + sassc-rails (~> 2.1, >= 2.1.1) + sassc (2.4.0) + ffi (~> 1.9) + sassc-rails (2.1.2) + railties (>= 4.0.0) + sassc (>= 2.0) + sprockets (> 3.0) + sprockets-rails + tilt + sawyer (0.8.2) + addressable (>= 2.3.5) + faraday (> 0.8, < 2.0) + scout_apm (4.0.3) + parser + selenium-webdriver (3.142.7) + childprocess (>= 0.5, < 4.0) + rubyzip (>= 1.2.2) + semantic_range (2.3.1) + shoulda-matchers (4.5.1) + activesupport (>= 4.2.0) + sidekiq (6.1.3) + connection_pool (>= 2.2.2) + rack (~> 2.0) + redis (>= 4.2.0) + simplecov (0.21.2) + docile (~> 1.1) + simplecov-html (~> 0.11) + simplecov_json_formatter (~> 0.1) + simplecov-html (0.12.3) + simplecov_json_formatter (0.1.2) + solid_use_case (2.2.0) + deterministic (~> 0.6.0) + sprockets (4.0.2) + concurrent-ruby (~> 1.0) + rack (> 1, < 3) + sprockets-rails (3.2.2) + actionpack (>= 4.0) + activesupport (>= 4.0) + sprockets (>= 3.0.0) + stackprof (0.2.16) + statsd-ruby (1.5.0) + temple (0.8.2) + terminal-table (1.6.0) + thor (1.1.0) + tilt (2.0.10) + timecop (0.9.2) + ts_routes (1.0.3) + railties (>= 4.0) + tzinfo (2.0.4) + concurrent-ruby (~> 1.0) + uglifier (4.2.0) + execjs (>= 0.3.0, < 3) + underscore-rails (1.8.3) + unicode-display_width (2.0.0) + vcr (6.0.0) + webmock (3.11.1) + addressable (>= 2.3.6) + crack (>= 0.3.2) + hashdiff (>= 0.4.0, < 2.0.0) + webpacker (5.2.1) + activesupport (>= 5.2) + rack-proxy (>= 0.6.1) + railties (>= 5.2) + semantic_range (>= 2.3.0) + websocket-driver (0.7.3) + websocket-extensions (>= 0.1.0) + websocket-extensions (0.1.5) + xpath (3.2.0) + nokogiri (~> 1.8) + zeitwerk (2.4.2) + zip-zip (0.3) + rubyzip (>= 1.0.0) + +PLATFORMS + ruby + +DEPENDENCIES + analytics-ruby + apitome + aws-sdk + barnes + better_errors + binding_of_caller + block_parser! + bootsnap + bootstrap + browser + byebug + capybara + database_cleaner + dotenv-rails + factory_bot_rails + flamegraph + font-awesome-rails + foreman + github_url + gitlab + haml + honeybadger + httparty + jquery-rails + js-routes + json_spec + jwt + letter_opener + listen + mathjax-rails + memory_profiler + octokit + overcommit + pagy + pg + puma + pundit + rack-attack + rack-mini-profiler + rails + rails-controller-testing + react-rails + redis + rspec-rails + rspec_api_documentation + rspec_junit_formatter + rubocop + rubyzip + sass-rails + scout_apm + selenium-webdriver + shoulda-matchers + sidekiq + simplecov + solid_use_case + stackprof + timecop + ts_routes + tzinfo-data + uglifier + underscore-rails + vcr + webmock + webpacker + zip-zip + +RUBY VERSION + ruby 2.6.6p146 + +BUNDLED WITH + 2.1.4 diff --git a/scripts/Procfile b/scripts/Procfile new file mode 100644 index 0000000000000000000000000000000000000000..ed57e30b51d0c94c8f74dc8a843f761b732b906f --- /dev/null +++ b/scripts/Procfile @@ -0,0 +1,3 @@ +web: bundle exec rails server -p $PORT +worker: sidekiq +release: rake db:migrate diff --git a/scripts/Procfile.local b/scripts/Procfile.local new file mode 100644 index 0000000000000000000000000000000000000000..0e0c11fabc5f4043e046abdd82ae83599b835a7c --- /dev/null +++ b/scripts/Procfile.local @@ -0,0 +1,3 @@ +web: bundle exec rails server -p 3003 +worker: bundle exec sidekiq +webpack: ./bin/webpack-dev-server diff --git a/scripts/README.md b/scripts/README.md new file mode 100644 index 0000000000000000000000000000000000000000..b7fc7d59b341292f873136524b76c0a2d24e5f24 --- /dev/null +++ b/scripts/README.md @@ -0,0 +1,109 @@ +Forge +===== + +## Standards +See the [software-team-standards](https://www.github.com/Galvanize-IT/software-team-standards) for git aliases. + +## Dependency Setup + +[install Homebrew](http://brew.sh/) if you don't already have it. + +Install rbenv and get the right version of ruby (the version here may change, so check the Gemfile for the correct +version. ) + +- `brew install rbenv` +- `rbenv install 2.6.6` + +Install our data stores: + +- `brew install postgresql` +- `brew install redis` +- `brew services start redis` + +To simplify your postgres installation, once you've installed it with homebrew, it's recommended that you use the +[Postgres.app](http://postgresapp.com/), which is an easy download and install. This allows you to not need to specify a +username/password in your `database.yml`. + + +## Installation + +You will need a [Github access token](https://github.com/settings/tokens). Replace the `YOUR_GENERATED_PERSONAL_ACCESS_TOKEN` with your token. + +The token will need **Full control of private repositories** + +- `git clone git@github.com:Galvanize-IT/forge.git && cd forge` +- `rbenv local 2.6.6` +- `gem install bundler` +- `bundle config --local GITHUB__COM YOUR_GENERATED_PERSONAL_ACCESS_TOKEN:x-oauth-basic` +- `bundle` +- `yarn install` + +## Data + +- `rake db:create` +- `rake db:migrate` + +## Auth + +Set up [Auth](https://github.com/Galvanize-IT/auth) before running Forge. + +## Configuration + +Ask a co-developer for their .env example. + +### Sidekiq + +As an admin, you can view Sidekiq jobs at `/admin/sidekiq`. + +## Starting the server + +`heroku local -p 3003` + +or: +`rails s -p 3003` +`sidekiq` +`bin/webpack-dev-server` + +## Deployment + +PRs are automatically deployed to their own one-off environments. To test webhook-related Auth features, be sure to update the webhooks for Forge at `auth-staging.herokuapp.com` to point to your review enviroment. + +Sign in to your review environment using your credentials for `auth-staging.herokuapp.com`, or `dev+auth@galvanize.com:password`. + +`master` is automatically deployed to `staging` once CI passes. + +Migrations are run as part of the deployment release phase. + +Note: `galvanize-forge` will need to have access to any repos used for project challenges. + +## Mailer previews + +To preview our mailers, go to `http://localhost:3003/rails/mailers`. + +## End-to-end Tests (e2e) + +The [Forge e2e suite](https://github.com/Galvanize-IT/forge-e2e) is run on every deploy to staging, and the results are posted to `#forge_devops`. + +## Code Generators + +The `ts_routes` gem generates typesafe routes for use in TypeScript. To update them: + +`rake ts:routes` + +### API Documentation + +The specs for the API use a specific DSL that allows the tests to generate our API documentation. You can then run the +integration specs and generate the documentation. The documentation can be committed if you’ve made changes to the API, +otherwise they can be disregard. + +Run `rails spec:api:docs` to generate the documentation files and then browse to `/api/docs` to see what the +documentation looks like. + +## Linters + +We use `rubocop` and `eslint`. + +You can use overcommit to automatically run our linters in a pre-commit hook: (`gem install overcommit; overcommit --install`). +Note: If you want to disable it inline (i.e. for a WIP commit), use `OVERCOMMIT_DISABLE=1 git commit -m "wip"`. + +You can also run the linters manually with auto-correction, along with the test suite, by running `bash lintspec.sh`. diff --git a/scripts/Rakefile b/scripts/Rakefile new file mode 100644 index 0000000000000000000000000000000000000000..9a5ea7383aa83eec12490380a7391d1bb93eeb96 --- /dev/null +++ b/scripts/Rakefile @@ -0,0 +1,6 @@ +# Add your own tasks in files placed in lib/tasks ending in .rake, +# for example lib/tasks/capistrano.rake, and they will automatically be available to Rake. + +require_relative "config/application" + +Rails.application.load_tasks diff --git a/scripts/app.json b/scripts/app.json new file mode 100644 index 0000000000000000000000000000000000000000..d82526749ed48751611e6c410cee7c43138e245f --- /dev/null +++ b/scripts/app.json @@ -0,0 +1,168 @@ +{ + "name": "forge", + "scripts": { + "postdeploy": "bundle exec rake db:seed" + }, + "env": { + "ACTIONMAILER_HOST": { + "required": true + }, + "ASSESSMENTS_API_KEY": { + "required": true + }, + "ASSESSMENTS_CALLBACK_TOKEN": { + "required": true + }, + "ASSESSMENTS_SERVICE_DOMAIN": { + "required": true + }, + "AUTH_CLIENT_ID": { + "required": true + }, + "AUTH_CLIENT_SECRET": { + "required": true + }, + "AUTH_URL": { + "required": true + }, + "AUTH_WEBHOOK_TOKEN": { + "required": true + }, + "AWS_ACCESS_KEY_ID": { + "required": true + }, + "AWS_SECRET_ACCESS_KEY": { + "required": true + }, + "BUNDLE_GITHUB__COM": { + "required": true + }, + "LEARN_FIND_COHORT_URL": { + "required": true + }, + "GITHUB_TOKEN": { + "required": true + }, + "GITHUB_COM_TOKEN": { + "required": true + }, + "GITLAB_COM_TOKEN": { + "required": true + }, + "GLEARN_ACCESS_KEY_ID": { + "required": true + }, + "GLEARN_BUCKET_NAME": { + "required": true + }, + "GLEARN_KEY_PREFIX": { + "required": true + }, + "GLEARN_SECRET_ACCESS_KEY": { + "required": true + }, + "HEROKU_APP_NAME": { + "required": true + }, + "HONEYBADGER_API_KEY": { + "required": true + }, + "HONEYBADGER_ENV": { + "value": "review" + }, + "INTERCOM_APP_ID": { + "required": true + }, + "LANG": { + "required": true + }, + "MIXPANEL_PROJECT_TOKEN": { + "required": true + }, + "FORGE_PROTOCOL": { + "required": true + }, + "RACK_ENV": { + "required": true + }, + "RAILS_ENV": { + "required": true + }, + "RAILS_LOG_TO_STDOUT": { + "required": true + }, + "RAILS_SERVE_STATIC_FILES": { + "required": true + }, + "REDIS_PROVIDER": { + "required": true + }, + "REDIS_URL": { + "required": true + }, + "SECRET_KEY_BASE": { + "required": true + }, + "SENDGRID_PASSWORD": { + "required": true + }, + "SENDGRID_USERNAME": { + "required": true + }, + "S3_BUCKET_NAME": { + "required": true + }, + "DS_PREP_UID": { + "required": true + }, + "SE_PREP_UID": { + "required": true + }, + "STANDARD_EVENT_UIDS": { + "required": true + }, + "SEGMENT_WRITE_KEY_SERVER": { + "required": true + }, + "SEGMENT_WRITE_KEY_CLIENT": { + "required": true + }, + "AWS_SQLSNIPPETS_PASSWORD": { + "required": true + }, + "AWS_SQLSNIPPETS_USER": { + "required": true + }, + "AWS_SQLSNIPPETS_HOST": { + "required": true + }, + "GITLAB_DSOP_IL2_TOKEN": { + "required": true + } + }, + "formation": { + "web": { + "quantity": 1, + "size": "hobby" + }, + "worker": { + "quantity": 1, + "size": "hobby" + } + }, + "addons": [ + "heroku-postgresql", + "heroku-redis" + ], + "buildpacks": [ + { + "url": "heroku/python" + }, + { + "url": "heroku/nodejs" + }, + { + "url": "heroku/ruby" + } + ] +} diff --git a/scripts/app/assets/config/manifest.js b/scripts/app/assets/config/manifest.js new file mode 100644 index 0000000000000000000000000000000000000000..1983ee4e54b48de4f02821c03ff9e5b207c365e6 --- /dev/null +++ b/scripts/app/assets/config/manifest.js @@ -0,0 +1,3 @@ +//= link_tree ../images +//= link application.css +//= link application.js diff --git a/scripts/app/assets/images/favicon.ico b/scripts/app/assets/images/favicon.ico new file mode 100644 index 0000000000000000000000000000000000000000..d44fabf799ec51b278a628f5185ac4ed1bce7b90 Binary files /dev/null and b/scripts/app/assets/images/favicon.ico differ diff --git a/scripts/app/assets/images/loader-black.svg b/scripts/app/assets/images/loader-black.svg new file mode 100644 index 0000000000000000000000000000000000000000..a33da184c0d1b938ead1f8aab00709c4372b5cdd --- /dev/null +++ b/scripts/app/assets/images/loader-black.svg @@ -0,0 +1,12 @@ + + + + + diff --git a/scripts/app/assets/images/loader-cyan.svg b/scripts/app/assets/images/loader-cyan.svg new file mode 100644 index 0000000000000000000000000000000000000000..50f34ddbf6beb39745b396fda11d42f4639ee1f0 --- /dev/null +++ b/scripts/app/assets/images/loader-cyan.svg @@ -0,0 +1,12 @@ + + + + + diff --git a/scripts/app/assets/images/loader-white.svg b/scripts/app/assets/images/loader-white.svg new file mode 100644 index 0000000000000000000000000000000000000000..0e73c3cd300319500e065ac9d8f870894d6a46fd --- /dev/null +++ b/scripts/app/assets/images/loader-white.svg @@ -0,0 +1,12 @@ + + + + + diff --git a/scripts/app/assets/images/lost.jpg b/scripts/app/assets/images/lost.jpg new file mode 100644 index 0000000000000000000000000000000000000000..a6c5f225f7816ed95b5a4ecf5f7675adddda6a43 Binary files /dev/null and b/scripts/app/assets/images/lost.jpg differ diff --git a/scripts/app/assets/images/mobile-logo.png b/scripts/app/assets/images/mobile-logo.png new file mode 100644 index 0000000000000000000000000000000000000000..af1f4aaeee4eff038288cda1873cc51995b3ca41 Binary files /dev/null and b/scripts/app/assets/images/mobile-logo.png differ diff --git a/scripts/app/assets/images/p1-learn-lockup.png b/scripts/app/assets/images/p1-learn-lockup.png new file mode 100644 index 0000000000000000000000000000000000000000..b31deb79644aab9ec93c019e83068c0380e2cd23 Binary files /dev/null and b/scripts/app/assets/images/p1-learn-lockup.png differ diff --git a/scripts/app/assets/images/spinner.gif b/scripts/app/assets/images/spinner.gif new file mode 100644 index 0000000000000000000000000000000000000000..f83606ab0f82a916187f525f8e998eca7b11f899 Binary files /dev/null and b/scripts/app/assets/images/spinner.gif differ diff --git a/scripts/app/assets/images/svg/baseline-notes-24px.svg b/scripts/app/assets/images/svg/baseline-notes-24px.svg new file mode 100644 index 0000000000000000000000000000000000000000..23c12501fc4388fe5da570fb066e04b2fb91f4b5 --- /dev/null +++ b/scripts/app/assets/images/svg/baseline-notes-24px.svg @@ -0,0 +1,4 @@ + + + + diff --git a/scripts/app/assets/images/svg/g-learn-lockup.svg b/scripts/app/assets/images/svg/g-learn-lockup.svg new file mode 100644 index 0000000000000000000000000000000000000000..745a72694b09096512dc691943513658e0750b02 --- /dev/null +++ b/scripts/app/assets/images/svg/g-learn-lockup.svg @@ -0,0 +1,37 @@ + + + + g-learn-lockup + Created with Sketch. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/scripts/app/assets/images/svg/galvanize-logo.svg b/scripts/app/assets/images/svg/galvanize-logo.svg new file mode 100644 index 0000000000000000000000000000000000000000..30d6af45afbb32bc7f72fa8e56077d884e95516f --- /dev/null +++ b/scripts/app/assets/images/svg/galvanize-logo.svg @@ -0,0 +1,48 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/scripts/app/assets/images/svg/github-icon.svg b/scripts/app/assets/images/svg/github-icon.svg new file mode 100644 index 0000000000000000000000000000000000000000..1c6c9ece618ac54ea18c3e5f6626adc3dcfa12f6 --- /dev/null +++ b/scripts/app/assets/images/svg/github-icon.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/scripts/app/assets/images/svg/mobile-logo.svg b/scripts/app/assets/images/svg/mobile-logo.svg new file mode 100644 index 0000000000000000000000000000000000000000..d51b17ad826d0540c89be8381561405069311d35 --- /dev/null +++ b/scripts/app/assets/images/svg/mobile-logo.svg @@ -0,0 +1,24 @@ + + + + + + + + + + + + + + + + diff --git a/scripts/app/assets/images/svg/octicon-git-branch.svg b/scripts/app/assets/images/svg/octicon-git-branch.svg new file mode 100644 index 0000000000000000000000000000000000000000..4a418920c236f33df8716d970873ea0fefd89fd4 --- /dev/null +++ b/scripts/app/assets/images/svg/octicon-git-branch.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/scripts/app/assets/images/svg/svg-sprite-action-symbol.svg b/scripts/app/assets/images/svg/svg-sprite-action-symbol.svg new file mode 100755 index 0000000000000000000000000000000000000000..c8abe240a98e871ae2efe5396109f31623643076 --- /dev/null +++ b/scripts/app/assets/images/svg/svg-sprite-action-symbol.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/scripts/app/assets/images/svg/svg-sprite-av-symbol.svg b/scripts/app/assets/images/svg/svg-sprite-av-symbol.svg new file mode 100644 index 0000000000000000000000000000000000000000..84aaa88c43070774db5f9b78817f1ebb3b33a1a9 --- /dev/null +++ b/scripts/app/assets/images/svg/svg-sprite-av-symbol.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/scripts/app/assets/images/svg/svg-sprite-device-symbol.svg b/scripts/app/assets/images/svg/svg-sprite-device-symbol.svg new file mode 100644 index 0000000000000000000000000000000000000000..ad4bc20102851af3e2b51826db364a783e719e77 --- /dev/null +++ b/scripts/app/assets/images/svg/svg-sprite-device-symbol.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/scripts/app/assets/images/svg/svg-sprite-image-symbol.svg b/scripts/app/assets/images/svg/svg-sprite-image-symbol.svg new file mode 100644 index 0000000000000000000000000000000000000000..54313587dd4b652b9e7d2c0bcc0b331436e566ef --- /dev/null +++ b/scripts/app/assets/images/svg/svg-sprite-image-symbol.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/scripts/app/assets/images/svg/svg-sprite-navigation-symbol.svg b/scripts/app/assets/images/svg/svg-sprite-navigation-symbol.svg new file mode 100755 index 0000000000000000000000000000000000000000..af094493030ef398c2a0161613bef9170a9384ee --- /dev/null +++ b/scripts/app/assets/images/svg/svg-sprite-navigation-symbol.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/scripts/app/assets/javascripts/application.js b/scripts/app/assets/javascripts/application.js new file mode 100644 index 0000000000000000000000000000000000000000..fb0297b1f82fc35231123e26c8ced8094e03c3bf --- /dev/null +++ b/scripts/app/assets/javascripts/application.js @@ -0,0 +1,6 @@ +//= require jquery +//= require rails-ujs +//= require popper +//= require bootstrap-sprockets +//= require bootstrap-datepicker +//= require hopscotch diff --git a/scripts/app/assets/javascripts/mobile.js b/scripts/app/assets/javascripts/mobile.js new file mode 100644 index 0000000000000000000000000000000000000000..b3b4c3d75c08147891390ba1bde31fc10cfebd8d --- /dev/null +++ b/scripts/app/assets/javascripts/mobile.js @@ -0,0 +1,10 @@ +window.onDocumentReady(function() { + $('body').on('touchmove', function() { + // + // What does the :hover selector do?? + // + if ($('.navigation-dropdown:hover').is(':visible')) { + $('.navigation-dropdown').unbind('hover'); + } + }); +}); diff --git a/scripts/app/assets/stylesheets/application.scss b/scripts/app/assets/stylesheets/application.scss new file mode 100644 index 0000000000000000000000000000000000000000..5cf08783b8bcf385bc880691917706d81e2811f3 --- /dev/null +++ b/scripts/app/assets/stylesheets/application.scss @@ -0,0 +1,103 @@ +/* + * This is a manifest file that'll be compiled into application.css, which will include all the files + * listed below. + * + * Any CSS and SCSS file within this directory, lib/assets/stylesheets, or any plugin's + * vendor/assets/stylesheets directory can be referenced here using a relative path. + * + * You're free to add application-wide styles to this file and they'll appear at the bottom of the + * compiled file so the styles you add here take precedence over styles defined in any other CSS/SCSS + * files in this directory. Styles in this file should be added after the last require_* statement. + * It is generally better to create a new file per style scope. + *= require font-awesome + */ + +@import "font-awesome"; +@import "bootstrap-custom"; +@import "bootstrap-datepicker"; +@import "hopscotch"; + +@import "variables"; +@import "mixins"; +@import "base"; +@import "typography"; +@import "hopscotch-overrides"; + +@import "rc-slider/assets/index"; +@import "rc-tooltip/assets/bootstrap"; + +@import "components/activity-feed"; +@import "components/auth-style-search"; +@import "components/action-menu"; +@import "components/badge"; +@import "components/blocks-index"; +@import "components/blocks-stats"; +@import "components/callouts"; +@import "components/challenge-block"; +@import "components/challenge-detail-comments"; +@import "components/challenge-detail-view"; +@import "components/challenge-timeline"; +@import "components/checkpoint-landing"; +@import "components/checkpoint-submission"; +@import "components/checkpoint-submissions-columns"; +@import "components/checkpoint-submissions-columns-student"; +@import "components/checkpoint-submissions-index"; +@import "components/checkpoint_submisstion_state"; +@import "components/checkpoint_student_scores"; +@import "components/checkpoint_toolbar"; +@import "components/code-block"; +@import "components/cohorts"; +@import "cohorts"; +@import "components/content-file-show"; +@import "components/content-file-sidebar"; +@import "components/curriculum"; +@import "components/curriculum-progress"; +@import "components/curriculum-checkpoint-summary"; +@import "components/dropdown-component"; +@import "components/footer"; +@import "components/flash-message"; +@import "components/activity-dashboard"; +@import "components/external-link"; +@import "components/grade-buttons"; +@import "components/lp-style-button"; +@import "components/mastery-table"; +@import "components/navigation-dropdown"; +@import "components/notifications"; +@import "components/new-comment-form"; +@import "components/primary-header"; +@import "components/primary-navigation"; +@import "components/progress"; +@import "components/thresholds"; +@import "components/progress-table"; +@import "components/search-bar"; +@import "components/secondary-navigation"; +@import "components/sort-dropdown-component"; +@import "components/standard-card"; +@import "components/standards-mastery-beans"; +@import "components/student-mastery-table"; +@import "components/student-name-bar"; +@import "components/submissions-dashboard-table"; +@import "components/cohort-submissions-table"; +@import "components/svg-icon"; +@import "components/user-avatar"; +@import "components/universal-list"; +@import "components/universal-row"; +@import "components/universal-table"; +@import "components/404-container"; +@import "components/video-player.scss"; +@import "components/cohort_releases"; +@import "components/checkbox-input"; +@import "components/partnerup"; +@import "components/integer_picker"; +@import "components/challenge_status"; +@import "components/status_picker"; +@import "components/slideshow"; +@import "components/progress_thresholds_slider"; +@import "components/progress_thresholds_key"; +@import "components/progress_thresholds_modal"; +@import "components/modal"; +@import "components/pill"; +@import "components/api-interactions"; +@import "components/pagination"; +@import "components/api_token"; +@import "components/button"; diff --git a/scripts/app/assets/stylesheets/base.scss b/scripts/app/assets/stylesheets/base.scss new file mode 100644 index 0000000000000000000000000000000000000000..1e9d53dc803990e367faff0c1ec4c5396234d39a --- /dev/null +++ b/scripts/app/assets/stylesheets/base.scss @@ -0,0 +1,66 @@ +html, +body { + overflow-x: hidden; + overflow-y: auto; +} + +body { + margin: 0; + width: 100%; + max-width: 100%; + background-color: $color-black-98-2; +} + +main { + min-height: calc(100vh - 6.5rem); + > .with-margins { + margin: 0 auto; + padding: 1rem ; + } +} + +body > main > .container-404 { + background-color: #e9eaec; + text-align: center; + font-family: sans-serif; + background: url("#{image_path("lost.jpg")}") no-repeat center center fixed; + background-size: cover; + -webkit-font-smoothing: antialiased; + margin: 0; + vertical-align: middle; + width: 100%; + + .logo { + padding-top: 2rem; + } + + h1, h2, h3 { + color: #fff; + } + + h1 { + font-size: 4rem; + font-weight: 200; + margin: 3rem 0 3rem; + } + + h2 { + font-size: 2rem; + font-weight: 200; + margin-bottom: 3rem; + padding-left: 20%; + padding-right: 20%; + } + + h3 { + font-size: 1.5rem; + text-transform: uppercase; + letter-spacing: .1rem; + font-weight: 600; + } + + a { + color: rgb(247, 144, 30); + text-decoration: none; + } +} diff --git a/scripts/app/assets/stylesheets/bootstrap-custom.scss b/scripts/app/assets/stylesheets/bootstrap-custom.scss new file mode 100644 index 0000000000000000000000000000000000000000..7859210c89165affe1b06aae8600dcac767bf2de --- /dev/null +++ b/scripts/app/assets/stylesheets/bootstrap-custom.scss @@ -0,0 +1,13 @@ +@import "bootstrap"; + +.alert { + margin: 1.5rem 2rem 0; +} + +.table p:last-child { + margin-bottom: 0; +} + +.card .table { + margin-bottom: 0; +} diff --git a/scripts/app/assets/stylesheets/cohorts.scss b/scripts/app/assets/stylesheets/cohorts.scss new file mode 100644 index 0000000000000000000000000000000000000000..18fbca1bc7cbaec9d3e8914446549b7aff99c636 --- /dev/null +++ b/scripts/app/assets/stylesheets/cohorts.scss @@ -0,0 +1,14 @@ +.center { + text-align: center; +} + +.alert-danger { + a { + color: $color-lightened-black; + } +} + +.form-group.required .control-label:after { + content:"*"; + color:red; +} \ No newline at end of file diff --git a/scripts/app/assets/stylesheets/components/_404-container.scss b/scripts/app/assets/stylesheets/components/_404-container.scss new file mode 100644 index 0000000000000000000000000000000000000000..8638f2bd0c82c26e4da5f45fd2daf3be80b37baf --- /dev/null +++ b/scripts/app/assets/stylesheets/components/_404-container.scss @@ -0,0 +1,43 @@ +.container-404 { + position: absolute; + width: 100%; + height: 100%; + text-align: center; + background: url("#{image_path("lost.jpg")}") no-repeat center center fixed; + background-size: cover; + margin: 0; + + .logo { + padding-top: 2rem; + } + + h1, h2, h3 { + color: #fff; + } + + h1 { + font-size: 4rem; + font-weight: 200; + margin: 3rem 0 3rem; + } + + h2 { + font-size: 2rem; + font-weight: 200; + margin-bottom: 3rem; + padding-left: 20%; + padding-right: 20%; + } + + h3 { + font-size: 1.5rem; + text-transform: uppercase; + letter-spacing: .1rem; + font-weight: 600; + } + + a { + color: rgb(247, 144, 30); + text-decoration: none; + } +} \ No newline at end of file diff --git a/scripts/app/assets/stylesheets/components/_action-menu.scss b/scripts/app/assets/stylesheets/components/_action-menu.scss new file mode 100644 index 0000000000000000000000000000000000000000..87ebbd71fc88f5e41ed9cd7b3746c7c5a267eb70 --- /dev/null +++ b/scripts/app/assets/stylesheets/components/_action-menu.scss @@ -0,0 +1,53 @@ +.action-menu-list, #action-menu-list { + position: absolute; + margin: 0; + right: 0; + top: 40px; + z-index: 10; + white-space: nowrap; + + &#action-menu { + margin: 48px 48px 0 0; + top: unset; + } + + &.lesson { + margin: -6px 156px 0 0 + } + + ul { + background: white; + border: 1px solid $color-black-77; + list-style-type: none; + margin: 0; + padding: 0; + border-radius: 8px; + box-shadow: 0 1px 10px 0 rgba(0,0,0,0.05); + border-color: rgb(238,238,238); + + li { + margin: 0; + + a, p { + color: $color-tundora; + text-decoration: none; + font-size: 16px; + font-weight: 400; + cursor: pointer; + line-height: 48px; + padding-left: 16px; + padding-right: 16px; + display: flex; + &:hover { + background: rgb(223,243,245); + } + + &.remove { + color: $color-valencia; + } + } + } + } + + +} diff --git a/scripts/app/assets/stylesheets/components/_activity-dashboard.scss b/scripts/app/assets/stylesheets/components/_activity-dashboard.scss new file mode 100644 index 0000000000000000000000000000000000000000..85de563c59345d456fb29a52d97dd8e29b5b1cae --- /dev/null +++ b/scripts/app/assets/stylesheets/components/_activity-dashboard.scss @@ -0,0 +1,253 @@ +.activity-dashboard { + & { + background-color: $color-white; + overflow-x: scroll; + overflow-y: hidden; + white-space: nowrap; + } + + > .headerrow { + height: 67px; + position: relative; + z-index: 6; + + &.-is-sticky { + position: fixed; + top: 0; + } + } + + > .headerspacer { + height: 67px; + border-bottom: 1px solid $color-black-83-1; + position: relative; + z-index: 9; + left: 0; + box-shadow: 1px 0px 0px 0px $color-black-83-1; + + &.-is-sticky { + position: fixed; + top: 0; + } + } + + > .headerspacer, + > .headerrow > .headermonths { + background-color: $color-black-97-3; + display: inline-block; + height: 67px; + vertical-align: top; + } + + > .headerrow > .headermonths { + position: relative; + z-index: 6; + } + + > .headerspacer, + > .studentrow > .studentnameplate { + width: 225px; + left: 0; + position: absolute; + } + + > .headerrow > .headermonths, > .studentrow > .studentdata { + margin-left: 225px; + } + + > .headerrow > .headermonths > .headermonth { + display: inline-block; + + &:last-child { + border-right: 1px solid $color-black-76-5; + } + } + + > .headerrow > .headermonths > .headermonth > .monthname { + border-left: 1px solid $color-black-76-5; + border-bottom: 1px solid $color-black-83-1; + font-size: 12px; + font-weight: bold; + padding: 4px 12px; + overflow: hidden; + text-overflow: ellipsis; + } + + > .headerrow > .headermonths > .headermonth > .dates > .dateitem { + border-bottom: 1px solid $color-black-83-1; + border-left: 1px solid $color-black-83-1; + width: 40px; + height: 40px; + display: inline-block; + font-size: 12px; + text-align: center; + position: relative; + + &.-is-first-of-month { + border-left-color: $color-black-76-5; + } + + &.-is-highlighted { + background-color: rgba(0,165,181,0.1); + border-left-color: $color-cyan; + box-shadow: 1px 0px 0px 0px $color-cyan, 0px -1px 0px 0px $color-cyan; + position: relative; + z-index: 7; + border-bottom: 1px solid #00A5B5; + + &::after { + width: 0; + height: 0; + border-left: 5px solid transparent; + border-right: 5px solid transparent; + content: " "; + border-top: 5px solid $color-cyan; + position: absolute; + left: 14px; + bottom: -5px; + } + } + } + + > .headerrow > .headermonths > .headermonth > .dates > .dateitem > .dayofmonth { + font-weight: bold; + padding-top: 6px; + line-height: 1; + } + + > .headerrow > .headermonths > .headermonth > .dates > .dateitem > .dayofweek { + color: $color-black-66-7; + text-transform: uppercase; + padding-top: 3px; + font-size: 10px; + } + + > .studentrow { + height: 40px; + } + + > .studentrow > .studentnameplate { + display: flex; + flex-flow: row nowrap; + align-items: center; + height: 40px; + padding: 0 1rem; + border-bottom: 1px solid $color-black-83-1; + background: $color-white; + box-shadow: 1px 0px 0px 0px #d4d4d4; + overflow: hidden; + z-index: 8; + } + + > .studentrow > .studentnameplate > .avatarfullname { + font-weight: 500; + overflow: hidden; + white-space: nowrap; + text-overflow: ellipsis; + } + + > .studentrow > .studentnameplate > .user-avatar { + flex: 0 0 auto; + margin-right: .5rem; + } + + > .studentrow > .studentdata { + position: relative; + margin-left: 225px; + height: 40px; + } + + > .studentrow > .studentdata > .chart { + height: 40px; + position: absolute; + top: 0; + left: 0; + z-index: 1; + } + + > .studentrow > .studentdata > .studentdataitems { + position: absolute; + z-index: 2; + border-right: 1px solid $color-black-76-5; + } + + > .studentrow > .studentdata > .studentdataitems > .studentdataitem { + height: 40px; + width: 40px; + border-bottom: 1px solid $color-black-83-1; + border-left: 1px solid $color-black-83-1; + line-height: 40px; + text-align: center; + display: inline-block; + overflow: hidden; + + &.-is-weekend { + background-color: rgba(0,0,0,0.02); + } + + &.-is-first-of-month { + border-left-color: $color-black-76-5; + } + + &.-is-highlighted { + background-color: rgba(0,165,181,0.1); + border-left-color: $color-cyan; + box-shadow: 1px 0px 0px 0px $color-cyan; + position: relative; + z-index: 99; + } + } +} + +.activity-loadmore { + margin: 0; + margin-left: 1rem; + align-items: center; + display: flex; + + a { + margin-left: 0.5rem; + } +} + +.activity-dashboard-legendrow { + & { + position: relative; + display: flex; + flex-flow: row; + justify-content: flex-end; + background: $color-black-97-3; + border-bottom: 1px solid $color-black-90-2; + } + + > .svg { + opacity: .6; + margin: .5rem; + cursor: help; + + &:hover + .infomodal { + display: block; + } + } + + > .infomodal { + font-size: 14px; + max-width: 360px; + margin: .5rem; + padding: 1rem 1.5rem; + border-radius: .5rem; + color: $color-white; + background-color: $color-black-10-2; + box-shadow: 0 2px 8px 0 rgba(0,0,0,0.3); + cursor: help; + display: none; + position: absolute; + top: 0; + right: 0; + z-index: 10; + + &:hover { + display: block; + } + } +} diff --git a/scripts/app/assets/stylesheets/components/_activity-feed.scss b/scripts/app/assets/stylesheets/components/_activity-feed.scss new file mode 100644 index 0000000000000000000000000000000000000000..c32a86802706a8b2cc30f7b72f6ee1baa73b4d8c --- /dev/null +++ b/scripts/app/assets/stylesheets/components/_activity-feed.scss @@ -0,0 +1,69 @@ +.activityfeed { + > .table > tbody > .activityitem > .message * { + vertical-align: middle; + } + + > .table > tbody > .activityitem > .message > .user-avatar { + display: inline-block; + margin-right: 8px; + } +} + +.activitylegend { + display: inline-block; + list-style-type: none; +} + +.activitylegend > .activitylegenditem { + display: inline-block; + vertical-align: middle; + margin-left: 16px; +} + +.activitylegend > .activitylegenditem * { + vertical-align: middle; +} + +.activitylegend > .activitylegenditem > .activitylabel { + margin-right: 4px; +} + +.activitylabel { + display: inline-block; + width: 8px; + height: 8px; + border-radius: 100%; + margin-right: 8px; + + &.-primary { + background-color: $color-cyan; + } + + &.-warning { + background-color: $color-banana-yellow; + } + + &.-default { + background-color: $color-black-80; + } + + &.-danger { + background-color: $color-valencia; + } + + &.-success { + background-color: $color-med-green; + } + + &.-score-1 { + background-color: $color-tangerine-orange; + } + + &.-score-2 { + background-color: $color-banana-yellow; + } + + &.-score-3 { + background-color: $color-lime-green; + } +} diff --git a/scripts/app/assets/stylesheets/components/_api-interactions.scss b/scripts/app/assets/stylesheets/components/_api-interactions.scss new file mode 100644 index 0000000000000000000000000000000000000000..7603cf19a4869c7211dd5e11c9a6f0b9f475229a --- /dev/null +++ b/scripts/app/assets/stylesheets/components/_api-interactions.scss @@ -0,0 +1,62 @@ +.api-interactions-wrapper { + margin: 60px 100px 30px 100px; + + h3 { + margin-bottom: 15px; + } + + .api-interactions-display { + box-shadow: 0px 0px 10px 1px rgba(0, 0, 0, 0.2); + border-radius: 5px; + + .api-interactions-table { + text-align: center; + font-family: "Trebuchet MS", Arial, Helvetica, sans-serif; + border-collapse: collapse; + width: 100%; + + th { + font-size: 13px; + } + + .td-metadata { + display: flex; + justify-content: flex-end; + padding-right: 45px; + } + } + + .api-interactions-table td, .api-interactions-table th { + padding: 14px; + text-align: left; + } + + .tr { + cursor: pointer; + border-bottom: 0.5px solid #e6e6e6; + background-color: white; + } + + .tr:hover { + background-color: #e6e6e6; + } + + .api-interactions-table th { + padding-top: 12px; + padding-bottom: 12px; + background-color: $color-galvanize-orange; + color: white; + } + } + + .metadata-wrapper { + p { + color: white; + margin-bottom: 0px; + } + } + + .pagination-wrapper { + margin-bottom: 100px; + } +} \ No newline at end of file diff --git a/scripts/app/assets/stylesheets/components/_api_token.scss b/scripts/app/assets/stylesheets/components/_api_token.scss new file mode 100644 index 0000000000000000000000000000000000000000..67554ab50e63eaa4492f4c1e94c82c365e3e109c --- /dev/null +++ b/scripts/app/assets/stylesheets/components/_api_token.scss @@ -0,0 +1,37 @@ +.api_token-wrapper { + + h3 { + @include font-h3; + } + + p { + @include font-paragraph; + margin-bottom: 0; + } + + .one-column { + @include one-column; + } + + .container-large { + @include container-large; + } + + .caption { + @include font-caption; + margin-bottom: 0; + margin-top: $font-default-line-height; + } + + .token-row { + margin: 20px -$input-spacing 0 -$input-spacing; + } + + input { + @include input; + + &.disabled { + @include input-disabled; + } + } +} diff --git a/scripts/app/assets/stylesheets/components/_auth-style-search.scss b/scripts/app/assets/stylesheets/components/_auth-style-search.scss new file mode 100644 index 0000000000000000000000000000000000000000..a246cb37c8d9810f13a936dd34da45859c0982cd --- /dev/null +++ b/scripts/app/assets/stylesheets/components/_auth-style-search.scss @@ -0,0 +1,26 @@ +.auth-style-search { + width: 100%; + box-shadow: rgba(0, 0, 0, 0.13) 1px 1px 2px 0px; + border-bottom-color: rgba(0, 0, 0, 0.15); + border-radius: 40px 40px 40px 40px; + border-bottom-style: solid; + border-bottom-width: 1px; + border-image-outset: 0px; + border-image-repeat: stretch; + border-image-slice: 100%; + border-image-source: none; + border-image-width: 1; + border-left-color: rgba(0, 0, 0, 0.15); + border-left-style: solid; + border-left-width: 1px; + border-right-color: rgba(0, 0, 0, 0.15); + border-right-style: solid; + border-right-width: 1px; + border-top-color: rgba(0, 0, 0, 0.15); + border-top-style: solid; + border-top-width: 1px; + padding-bottom: 8px; + padding-left: 28px; + padding-right: 12px; + padding-top: 8px; +} \ No newline at end of file diff --git a/scripts/app/assets/stylesheets/components/_blocks-index.scss b/scripts/app/assets/stylesheets/components/_blocks-index.scss new file mode 100644 index 0000000000000000000000000000000000000000..ba7ff61e09fce11deb8081e653bab39c45d66d7f --- /dev/null +++ b/scripts/app/assets/stylesheets/components/_blocks-index.scss @@ -0,0 +1,133 @@ +.blocks-table{ + overflow: scroll; + max-height: 616px; + width: 100%; + margin: 0 auto; + z-index: 1000000; + background-color: $color-black-98-2; + + .createdcolumn { + padding-left: 24px; + } + + thead { + background-color: #FFFFFF; + border-top: 1px solid $color-black-77; + border-bottom: 1px solid $color-black-77; + height: 40px; + + tr { + font-size: 14px; + line-height: 16px; + text-transform: uppercase; + color: #999999; + } + + tr > th { + font-weight: 500; + padding: 12px; + } + } + + tbody { + background-color: #FFFFFF; + + .releaserow { + position: relative; + border-bottom: 1px solid #e7e7e7; + + .readme-text { + overflow: hidden; + text-overflow: ellipsis; + display: -webkit-box; + -webkit-line-clamp: 3; + -webkit-box-orient: vertical; + } + + &.current-release { + background-color: rgba(0,165,181,0.05); + } + } + + .releaserow > td { + position: relative; + padding: 16px; + font-size: 16px; + vertical-align: top; + + &:nth-child(2) { + max-width: 360px; + } + + &.createdcolumn { + padding-left: 24px; + } + } + + .releaserow > td > h5 { + font-weight: bold; + line-height: 16px; + font-size: 16px; + } + + .releaserow > td > p { + line-height: 16px; + margin-bottom: 8px; + } + + .releaserow > td > a.gitlink { + line-height: 16px; + font-weight: 300; + margin-right: 24px; + position: relative; + } + + .releaserow > td > a.gitlink > svg { + width: 16px; + position: absolute; + margin-left: 3px; + + } + + .releaserow > td > span.currentlabel { + line-height: 16px; + color: $color-cyan; + text-transform: uppercase; + font-size: 12px; + font-weight: bold; + letter-spacing: 1px; + text-align: center; + padding: 0 24px; + } + + .releaserow > td .cohorts-tooltip { + position: absolute; + right: calc(100% - 2rem); + top: 1rem; + display: flex; + flex-direction: column; + width: 360px; + border-radius: .5rem; + background-color: $color-black-10-2; + color: $color-white; + overflow: hidden; + box-shadow: 0 2px 8px 0 rgba(0,0,0,0.3); + z-index: 9; + } + + .releaserow > td .cohorts-tooltip > .body > div { + padding: 24px; + } + + .releaserow > td .cohorts-tooltip > .body > div > p > a { + text-decoration: none; + color: white; + + } + + .releaserow > td .cohorts-tooltip > .body > div > p > a > svg { + height: 16px; + margin-left: 8px; + } + } +} \ No newline at end of file diff --git a/scripts/app/assets/stylesheets/components/_blocks-stats.scss b/scripts/app/assets/stylesheets/components/_blocks-stats.scss new file mode 100644 index 0000000000000000000000000000000000000000..46c8a0b111e7076991ff408227546caacb0ebd5e --- /dev/null +++ b/scripts/app/assets/stylesheets/components/_blocks-stats.scss @@ -0,0 +1,70 @@ +.block-stats { + display: flex; + flex-direction: column; + flex-basis: 380px; + flex-shrink: 0; + + .block-stats__user-row { + display: flex; + justify-content: flex-start; + align-items: center; + margin-bottom: 15px; + flex-wrap: wrap; + @include font-paragraph; + font-size: 15px; + line-height: 20px; + + .block-stats__user-name { + margin-left: 8px; + margin-right: 8px; + } + + .block-stats__latest_version { + color: $color-silver-chalice; + font-weight: 500; + } + } + + .block-stats__stats-row { + display: flex; + justify-content: flex-start; + font-size: 14px; + color: $color-tundora; + font-weight: 400; + + a { + width: fit-content; + } + + &.vertical { + flex-direction: column; + + a { + margin-bottom: 10px; + font-weight: 500; + + &:last-child { + margin-bottom: 0; + } + } + } + } + + .block-stats__stats-icon { + margin-right: 15px; + display: flex; + align-items: center; + } + + .stats--verbose { + margin-left: 5px;; + } + + &.--vertical { + flex-basis: auto; + } + + svg { + margin-right: 3px; + } +} diff --git a/scripts/app/assets/stylesheets/components/_button.scss b/scripts/app/assets/stylesheets/components/_button.scss new file mode 100644 index 0000000000000000000000000000000000000000..f6c709f69fd530d4aa613b70c74dd8f5de384fe7 --- /dev/null +++ b/scripts/app/assets/stylesheets/components/_button.scss @@ -0,0 +1,57 @@ +.button-wrapper { + @include button; + + &.button-solid-primary { + @include button-theme($color-primary-teal, primary); + + &:hover { + background-color: lighten( $color-primary-teal, $lighten ); + .submit-with-avatars .user-avatar { + border: 2px solid lighten( $color-primary-teal, $lighten ); + } + } + + &:active { + background-color: darken( $color-primary-teal, $darken ); + .submit-with-avatars .user-avatar { + border: 2px solid darken( $color-primary-teal, $darken ); + } + } + } + + &.button-outline-primary { + @include button-theme($color-primary-teal, outline); + + &:hover { + background-color: adjust-color($color-primary-teal, $alpha: -0.9); + } + + &:active { + background-color: adjust-color($color-primary-teal, $alpha: -0.7); + } + } + + &.button-solid-red { + @include button-theme($color-red, primary); + + &:hover { + background-color: lighten( $color-red, $lighten ); + } + + &:active { + background-color: darken( $color-red, $darken ); + } + } + + &.button-outline-red { + @include button-theme($color-red, outline); + + &:hover { + background-color: adjust-color($color-red, $alpha: -0.9); + } + + &:active { + background-color: adjust-color($color-red, $alpha: -0.7); + } + } +} diff --git a/scripts/app/assets/stylesheets/components/_callouts.scss b/scripts/app/assets/stylesheets/components/_callouts.scss new file mode 100644 index 0000000000000000000000000000000000000000..71efbbad1fcfd0d8fe1a0ba38323ac89d8e56211 --- /dev/null +++ b/scripts/app/assets/stylesheets/components/_callouts.scss @@ -0,0 +1,54 @@ +.callout { + border-left: 4px solid rgb(77, 169, 179); + border-radius: 4px; + padding: 25px 30px; + margin: 25px auto; + background: rgb(245, 245, 245); + max-width: 700px; + + h1,h2,h3,h4,h5,h6 { + font-size: 20px!important; + font-weight: 500!important; + letter-spacing: -0.75px!important; + margin-bottom: 0.5rem!important; + margin-top: 0!important; + color: rgb(77, 169, 179)!important; + } + + svg + h1,svg + h2,svg + h3,svg + h4,svg + h5,svg + h6 { + display: inline-block; + position: relative; + top: -5px; + padding-left: 10px; + } + + p { + margin: 0px!important; + } + + &.callout-info, &.callout-attention, &.callout-note, &.callout-tip, &.callout-hint, &.callout-primary { + border-left: 4px solid rgb(77, 169, 179); + h1,h2,h3,h4,h5,h6 { color: rgb(77, 169, 179)!important; } + svg { fill: rgb(77, 169, 179)!important; } + } + &.callout-warning, &.callout-caution { + border-left: 4px solid rgb(214, 146, 28); + h1,h2,h3,h4,h5,h6 { color: rgb(214, 146, 28)!important; } + svg { fill: rgb(214, 146, 28)!important; } + } + &.callout-danger, &.callout-error { + border-left: 4px solid rgb(199, 76, 76); + h1,h2,h3,h4,h5,h6 { color: rgb(199, 76, 76)!important; } + svg { fill: rgb(199, 76, 76)!important; } + } + &.callout-success, &.callout-important { + border-left: 4px solid rgb(109, 179, 104); + h1,h2,h3,h4,h5,h6 { color: rgb(109, 179, 104)!important; } + svg { fill: rgb(109, 179, 104)!important; } + } + &.callout-secondary, &.callout-light, &.callout-dark, &.callout-admonition { + border-left: 4px solid rgb(128, 128, 128); + h1,h2,h3,h4,h5,h6 { color: rgb(128, 128, 128)!important; } + svg { fill: rgb(128, 128, 128)!important; } + } +} \ No newline at end of file diff --git a/scripts/app/assets/stylesheets/components/_challenge-block.scss b/scripts/app/assets/stylesheets/components/_challenge-block.scss new file mode 100644 index 0000000000000000000000000000000000000000..1307045490934fc0dab6f27c4ce2fa19803406ec --- /dev/null +++ b/scripts/app/assets/stylesheets/components/_challenge-block.scss @@ -0,0 +1,698 @@ +form.challenge-block { + border-width: 0; + + .topics { + background-color: $color-black-97-3; + display: flex; + justify-content: flex-start; + padding-bottom: 30px; + flex-direction: row; + flex-wrap: wrap; + } +} + +.challenge-legendrow { + margin-left: 14px !important; +} + +form[data-challenge-type="code-snippet"] .challenge-legendrow, +form[data-challenge-type="paragraph"] .challenge-legendrow, +form[data-challenge-type="multiple-choice"] .challenge-legendrow, +form[data-challenge-type="checkbox"] .challenge-legendrow, +form[data-challenge-type="short-answer"] .challenge-legendrow, +form[data-challenge-type="custom-snippet"] .challenge-legendrow, +form[data-challenge-type="testable-project"] .challenge-legendrow { + display: inline-block !important; + top: 5px; +} + +form[data-challenge-type="code-snippet"] .actions.checkpoint, +form[data-challenge-type="custom-snippet"] .actions.checkpoint, +form[data-challenge-type="testable-project"] .actions.checkpoint { + padding: 5px 16px 24px; +} + +.body-container .mdown-container .challenge-block, .challenge-block { + $em: 8; + + & { + position: relative; + background: $color-white; + box-shadow: 0 2px 8px 0 rgba(0, 0, 0, 0.1); + margin: ($em * 3 + px) 0; + } + + > .challenge-header { + position: relative; + background: $color-white; + border-bottom: 1px solid $color-black-90-2; + padding: ($em * 2 + px); + display: flex; + flex-flow: row nowrap; + align-items: center; + min-height: ($em * 9 + px); + } + + > .challenge-header .vertical-dots { + position: relative; + height: ($em * 3 + px); + width: ($em * 3 + px); + display: flex; + flex: 0 0 auto; + margin-left: auto; + margin-right: 0; + color: $color-black-60; + + &:hover { + cursor: pointer; + color: $color-cyan; + } + } + + > .challenge-header > .textcontainer { + flex: 1 1 auto; + margin-left: ($em * 2 + px); + margin-right: ($em * 2 + px); + display: flex; + flex-flow: column; + justify-content: center; + } + + > .challenge-header > .textcontainer > .title { + height: ($em * 3 + px); + color: $color-darker-gray-blue; + font-size: ($em * 2 + px); + font-weight: bold; + line-height: ($em * 3 + px); + } + + > .challenge-header > .textcontainer > .info { + height: ($em * 2 + px); + color: $color-black-60; + font-size: ($em * 1.5 + px); + line-height: ($em * 2 + px); + } + + > .challenge-header > .icon-container { + position: absolute; + top: 0; + bottom: 0; + left: -($em * 2 + px); + right: auto; + margin: auto; + background: $color-darkest-blue-gray; + height: ($em * 4 + px); + width: ($em * 4 + px); + overflow: hidden; + border-radius: 50%; + display: flex; + align-items: center; + justify-content: center; + color: white; + font-size: 0.8rem; + font-weight: 400; + } + + > .challenge-header > .icon-container > svg { + height: 100%; + width: 100%; + } + + > .problem { + background: $color-white; + border-bottom: 1px solid $color-black-90-2; + padding: ($em * 3 + px); + + p { + color: $color-black-46-7; + } + + blockquote { + border-left: 3px solid $color-black-46-7; + font-style: italic; + padding-left: 20px; + } + + p, ul, ol, li { + margin-top: 12px; + margin-bottom: 0px; + &:first-child, &.first-child > img { + margin-top: 0px; + } + } + + h1 + p, h2 + p, h3 + p, h4 + p, h5 + p, h6 + p { + margin-top: 8px; + } + + h1, h1:first-of-type, h2, h3, h4, h5, h6 { + margin-top: 24px; + margin-bottom: 0px; + &:first-child { + margin-top: 0px; + } + } + + table, pre, blockquote { + margin-top: 24px; + margin-bottom: 24px; + &:first-child { + margin-top: 0px; + } + } + + p > img { + margin-top: 24px; + margin-bottom: 24px; + } + + img { + max-width: 100%; + } + + &.-truncate { + height: 200px; + overflow: hidden; + } + } + + .show-all { + background: white; + box-shadow: 0px -20px 20px 0px rgba(255,255,255,1); + color: $color-cyan; + text-align: center; + position: relative; + padding: 8px; + cursor: pointer; + } + + > .answer { + background: $color-black-97-3; + border-bottom: 1px solid $color-black-90-2; + padding: ($em * 3 + px); + + &.-codeeditor { + position: relative; + min-height: 400px; + width: 100%; + padding: 0; + overflow: scroll; + } + } + + > .answer > .checkbox-challenge-label > .renderedmdown > ul, + > .answer > .checkbox-challenge-label > .renderedmdown > h3 { + margin-top: 12px; + margin-bottom: 12px; + } + + > .answer .checkbox-challenge-info { + font-size: 14px; + color: $color-black-60; + font-weight: 400; + text-transform: uppercase; + } + + .input-lang { + background: #25282c; + text-align: right; + font-size: 12px; + text-transform: uppercase; + padding: 2px 4px; + color: #C5C8C6; + } + + > .answer > input { + height: ($em * 5 + px); + width: 100%; + padding: 0 ($em * 2 + px); + border: 1px solid $color-black-83-1; + border-radius: 4px; + background-color: $color-white; + box-shadow: inset 0 1px 2px 0 rgba(0, 0, 0, 0.1); + } + + > .answer > input::placeholder, > .answer > textarea::placeholder { + color: $color-black-83-1; + font-style: italic; + } + + > .answer > textarea { + width: 100%; + padding: ($em * 2 + px); + border: 1px solid $color-black-83-1; + border-radius: 4px; + background-color: $color-white; + box-shadow: inset 0 1px 2px 0 rgba(0, 0, 0, 0.1); + resize: vertical; + } + + > .actions { + min-height: ($em * 9 + px); + padding: ($em * 2 + px) ($em * 3 + px); + background-color: $color-black-97-3; + display: flex; + flex-flow: column-reverse nowrap; + align-items: center; + + .draft-timestamp { + font-size: 14px; + color: $color-black-60; + font-weight: 400; + } + + @media screen and (min-width: $tablet-breakpoint) { + flex-flow: row nowrap; + } + } + + > .actions .feedback { + display: flex; + flex-flow: row; + align-items: center; + font-size: ($em * 2.25 + px); + line-height: ($em * 3 + px); + margin-right: 10px; + } + + > .actions .feedback > .icon-container { + flex: 0 0 auto; + height: ($em * 3 + px); + width: ($em * 3 + px); + display: flex; + align-items: center; + justify-content: center; + margin-right: ($em * 1 + px); + } + + > .actions > .buttons { + display: flex; + flex-direction: row; + justify-content: center; + align-items: center; + margin-bottom: 1rem; + + @media screen and (min-width: $tablet-breakpoint) { + margin: 0 0 0 auto; + } + } + + > .actions > .buttons > .resetbutton { + background-color: inherit; + color: $color-cyan; + width: 135px; + } + + .multi-and-checkbox { + & { + display: flex; + flex-flow: row nowrap; + align-items: center; + margin: ($em * 1 + px) 0; + } + + &:hover { + background: rgba($color-cyan, 0.05); + border-radius: 8px; + user-select: none; + cursor: pointer; + } + + > .renderedmdown { + flex: 1; + padding: 0.5rem 1rem; + border: 1px solid rgba(0,0,0, 0.1); + border-radius: 0.5rem; + overflow: hidden; + pointer-events: none; + } + + > .renderedmdown > pre, + > .renderedmdown > pre > code { + background: none; + margin: 0; + white-space: pre-wrap; + } + } + + .checkbox-challenge-label { + @extend .multi-and-checkbox; + + > input[type="checkbox"]:checked + .renderedmdown { + border-color: $color-cyan; + background: rgba($color-cyan, 0.05); + } + + > input[type="checkbox"] { + box-sizing: border-box; + padding: 0; + display: inline-block; + vertical-align: middle; + -webkit-appearance: none; + -moz-appearance: none; + appearance: none; + cursor: pointer; + height: ($em * 3 + px); + width: ($em * 3 + px); + background: none center no-repeat; + background-size: cover; + margin: ($em * 1 + px); + margin-right: ($em * 2 + px); + flex: 0 0 auto; + } + + > input[type="checkbox"] { + background-image: url("data:image/svg+xml;charset=utf-8, %3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20width%3D%2224%22%20height%3D%2224%22%20viewBox%3D%220%200%2024%2024%22%20style%3D%22fill%3A%23cccccc%3B%22%3E%3Cpath%20d%3D%22M19%205v14H5V5h14m0-2H5c-1.1%200-2%20.9-2%202v14c0%201.1.9%202%202%202h14c1.1%200%202-.9%202-2V5c0-1.1-.9-2-2-2z%22%2F%3E%3C%2Fsvg%3E"); + } + + > input[type="checkbox"]:checked { + background-image: url("data:image/svg+xml;charset=utf-8, %3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20width%3D%2224%22%20height%3D%2224%22%20viewBox%3D%220%200%2024%2024%22%20style%3D%22fill%3A%2369A2D7%3B%22%3E%3Cpath%20d%3D%22M19%203H5c-1.11%200-2%20.9-2%202v14c0%201.1.89%202%202%202h14c1.11%200%202-.9%202-2V5c0-1.1-.89-2-2-2zm-9%2014l-5-5%201.41-1.41L10%2014.17l7.59-7.59L19%208l-9%209z%22%2F%3E%3C%2Fsvg%3E"); + } + + > .renderedmdown > p { + margin-top: 12px !important; + margin-bottom: 12px !important; + font-size: 18px !important; + } + } + + .multi-choice-challenge-label { + @extend .multi-and-checkbox; + + > input[type="radio"]:checked + .renderedmdown { + border-color: $color-cyan; + background: rgba($color-cyan, 0.05); + } + + > input[type="radio"] { + box-sizing: border-box; + padding: 0; + display: inline-block; + vertical-align: middle; + -webkit-appearance: none; + -moz-appearance: none; + appearance: none; + cursor: pointer; + height: ($em * 3 + px); + width: ($em * 3 + px); + background: none center no-repeat; + background-size: cover; + margin: ($em * 1 + px); + margin-right: ($em * 2 + px); + flex: 0 0 auto; + } + + > input[type="radio"] { + background-image: url("data:image/svg+xml, %3Csvg%20width%3D%2220%22%20height%3D%2220%22%20viewBox%3D%220%200%2020%2020%22%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%3E%3Cg%20fill%3D%22none%22%20fill-rule%3D%22evenodd%22%3E%3Cpath%20d%3D%22M10%200C4.48%200%200%204.48%200%2010s4.48%2010%2010%2010%2010-4.48%2010-10S15.52%200%2010%200z%22%20fill%3D%22%23FFF%22%20fill-rule%3D%22nonzero%22%2F%3E%3Cpath%20d%3D%22M10%200C4.48%200%200%204.48%200%2010s4.48%2010%2010%2010%2010-4.48%2010-10S15.52%200%2010%200zm0%2018c-4.42%200-8-3.58-8-8s3.58-8%208-8%208%203.58%208%208-3.58%208-8%208z%22%20fill%3D%22%23D4D4D4%22%20fill-rule%3D%22nonzero%22%2F%3E%3Cpath%20d%3D%22M-2-2h24v24H-2z%22%2F%3E%3C%2Fg%3E%3C%2Fsvg%3E"); + } + + > input[type="radio"]:checked { + background-image: url("data:image/svg+xml, %3Csvg%20width%3D%2220%22%20height%3D%2220%22%20viewBox%3D%220%200%2020%2020%22%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%3E%3Cg%20fill%3D%22none%22%20fill-rule%3D%22evenodd%22%3E%3Cpath%20d%3D%22M10%205c-2.76%200-5%202.24-5%205s2.24%205%205%205%205-2.24%205-5-2.24-5-5-5zm0-5C4.48%200%200%204.48%200%2010s4.48%2010%2010%2010%2010-4.48%2010-10S15.52%200%2010%200zm0%2018c-4.42%200-8-3.58-8-8s3.58-8%208-8%208%203.58%208%208-3.58%208-8%208z%22%20fill%3D%22%2300A5B5%22%20fill-rule%3D%22nonzero%22%2F%3E%3Cpath%20d%3D%22M-2-2h24v24H-2z%22%2F%3E%3C%2Fg%3E%3C%2Fsvg%3E"); + } + + > .renderedmdown > p { + margin-top: 12px !important; + margin-bottom: 12px !important; + font-size: 18px !important; + } + } + + + .challenge-grading-buttons { + & { + display: flex; + flex-flow: row nowrap; + align-items: center; + justify-content: space-between; + min-height: 72px; + background-color: $color-black-97-3; + padding: 0 24px; + + border: solid $color-black-90-2; + border-width: 1px 0; + } + + .grading-details { + & { + display: flex; + flex-flow: row nowrap; + align-items: center; + justify-content: flex-end; + margin-left: auto; + } + .timestamp { + flex: 0 1 auto; + font-size: 16px; + color: $color-black-60-8; + font-weight: 500; + margin-right: 18px; + } + + .buttons { + display: flex; + flex-flow: row nowrap; + margin-left: 16px; + } + + .points-ratio { + font-size: 24px; + } + + .points-section { + display: flex; + align-items: center; + position: relative; + + .points { + margin-left: 5px; + font-weight: 500; + color: $color-black-60-8; + font-weight: 500; + margin-left: 15px; + } + + .correct-check { + color: $color-cyan; + width: 32px; + height: 32px; + } + + .incorrect-check { + color: $color-valencia; + width: 32px; + height: 32px; + } + } + } + } + + .topics { + padding: 10px 16px 16px; + } + +} + +//challenge-accordion +.challenge-accordion { + $em: 8; + + & { + position: relative; + background: $color-white; + } + + &.-is-expanded > .heading > .icon-container { + transform: rotate(0deg); + } + + &.-is-expanded > .body > .hint-container { + margin-left: 1em; + margin-right: 0.5em; + + p { + margin-bottom: 1.5em; + } + } + + &.-is-expanded > .body > .text-center > p.get-hint-text { + color: $color-cyan; + cursor: pointer; + } + + > .heading { + user-select: none; + height: ($em * 5 + px); + padding: 0 ($em * 2 + px); + border: solid $color-black-90-2; + border-width: 1px 0; + display: flex; + flex-flow: row nowrap; + align-items: center; + cursor: pointer; + } + + > .heading > .headingtitle { + flex: 1 1 100%; + height: ($em * 1.5 + px); + color: $color-cyan; + font-size: ($em * 1.5 + px); + font-weight: bold; + letter-spacing: 1px; + line-height: ($em * 1.5 + px); + margin: 0 ($em * 1 + px); + text-transform: uppercase; + } + + > .heading > .icon-container { + flex: 0 0 auto; + height: ($em * 3 + px); + width: ($em * 3 + px); + display: flex; + align-items: center; + justify-content: center; + transform: rotate(-90deg); + } + + > .body { + padding: ($em * 3 + px); + overflow-x: scroll; + + pre { + font-family: Menlo, Monaco, Consolas, "Courier New", monospace; + margin: 0 0 10px; + padding: 9.5px; + background-color: $color-black-97-3; + font-size: 13px; + word-wrap: normal; + margin-top: 0px !important; + } + + p, h1, h2, h3, h4, h5, h6, table { + margin-top: 0px !important; + } + + .problem pre { + word-break: none; + word-wrap: normal; + color: $color-black-20; + background-color: $color-black-97-3; + border: none; + border-radius: 0px; + } + } +} + +.challenge-legendrow { + & { + position: relative; + display: flex; + flex-flow: row; + background: $color-black-97-3; + } + + > .svg { + opacity: .6; + margin: .5rem; + cursor: help; + margin-right: 10px; + margin-left: auto; + + &:hover + .infomodal { + display: block; + } + } + + > .infomodal { + font-size: 14px; + width: 260px; + margin: .5rem; + padding: 1rem 1.5rem; + border-radius: .5rem; + color: $color-white; + background-color: $color-black-10-2; + box-shadow: 0 2px 8px 0 rgba(0,0,0,0.3); + cursor: help; + display: none; + position: absolute; + top: 0; + right: 0; + z-index: 10; + + &:hover { + display: block; + } + } +} + +// menu.js.jsx +.menu-default { + $em: 8; + + & { + position: absolute; + top: 0; + right: 0; + background: $color-white; + border-radius: 4px; + border: 1px solid $color-black-83-1; + box-shadow: 0 2px 8px 0 rgba(0, 0, 0, 0.1); + overflow: hidden; + width: 100%; + min-width: 200px; + } + + > ul { + margin: 0; + padding: 0; + list-style: none; + } + + > ul > li { + margin: 0; + padding: 0; + height: ($em * 5 + px); + line-height: ($em * 5 + px); + background-color: $color-white; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + cursor: pointer; + user-select: none; + font-size: ($em * 2 + px); + color: black; + + &:hover { + color: $color-cyan; + background-color: rgba($color-cyan, 0.02); + } + } + + a { + display: block; + height: inherit; + line-height: inherit; + font-style: inherit; + color: inherit; + text-decoration: none; + padding: 0 ($em * 2 + px); + } +} + +// icon.js.jsx +.svg { + &.primary-color { + color: $color-cyan; + } + + &.error-color { + color: #E36875; + } +} + +.ace_editor { + display: block; + position: relative; + min-height: 400px; + min-width: 500px; + width: 100%; +} diff --git a/scripts/app/assets/stylesheets/components/_challenge_status.scss b/scripts/app/assets/stylesheets/components/_challenge_status.scss new file mode 100644 index 0000000000000000000000000000000000000000..05fae013b8bef66fbe6a0047beca15c7f521e093 --- /dev/null +++ b/scripts/app/assets/stylesheets/components/_challenge_status.scss @@ -0,0 +1,18 @@ +.status-section { + display: flex; + align-items: center; + position: relative; + margin-left: 25px; + + .incorrect-check { + color: $color-valencia; + width: 40px; + height: 45px; + } + + .correct-check { + color: $color-cyan; + width: 40px; + height: 45px; + } +} diff --git a/scripts/app/assets/stylesheets/components/_checkbox-input.scss b/scripts/app/assets/stylesheets/components/_checkbox-input.scss new file mode 100644 index 0000000000000000000000000000000000000000..47f92e904883e954aa34a49d19ace018559db538 --- /dev/null +++ b/scripts/app/assets/stylesheets/components/_checkbox-input.scss @@ -0,0 +1,15 @@ +input[type="checkbox"].checkbox-input { + display: inline-block; + vertical-align: middle; + appearance: none; + cursor: pointer; + height: 24px; + width: 24px; + background: none center no-repeat; + background-size: cover; + background-image: url("data:image/svg+xml;charset=utf-8, %3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20width%3D%2224%22%20height%3D%2224%22%20viewBox%3D%220%200%2024%2024%22%20style%3D%22fill%3A%23cccccc%3B%22%3E%3Cpath%20d%3D%22M19%205v14H5V5h14m0-2H5c-1.1%200-2%20.9-2%202v14c0%201.1.9%202%202%202h14c1.1%200%202-.9%202-2V5c0-1.1-.9-2-2-2z%22%2F%3E%3C%2Fsvg%3E"); + + &:checked { + background-image: url("data:image/svg+xml;charset=utf-8, %3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20width%3D%2224%22%20height%3D%2224%22%20viewBox%3D%220%200%2024%2024%22%20style%3D%22fill%3A%2369A2D7%3B%22%3E%3Cpath%20d%3D%22M19%203H5c-1.11%200-2%20.9-2%202v14c0%201.1.89%202%202%202h14c1.11%200%202-.9%202-2V5c0-1.1-.89-2-2-2zm-9%2014l-5-5%201.41-1.41L10%2014.17l7.59-7.59L19%208l-9%209z%22%2F%3E%3C%2Fsvg%3E"); + } +} \ No newline at end of file diff --git a/scripts/app/assets/stylesheets/components/_checkpoint-landing.scss b/scripts/app/assets/stylesheets/components/_checkpoint-landing.scss new file mode 100644 index 0000000000000000000000000000000000000000..c13e74006533bae3d65aa5db76574db912d7673a --- /dev/null +++ b/scripts/app/assets/stylesheets/components/_checkpoint-landing.scss @@ -0,0 +1,170 @@ +.instructor-actions { + color: $color-dove-gray; + display: flex; + flex-flow: row nowrap; + width: 170px; + justify-content: flex-end; + position: relative; + + a { + text-decoration: none; + } + div { + display: flex; + flex-direction: row; + margin-left: 2rem; + } + span { + font-size: 16px; + } +} + +.checkpoint-landing { + @include font-base; + margin-bottom: 50px; +} + +.checkpoint-attributes, .checkpoint-attributes .checkpoint-attribute { + display: flex; + flex-flow: row nowrap; + background-color: white; +} + +.checkpoint-landing-title { + @include font-h1; + margin: 66px 0 60px 0; +} + +.checkpoint-attributes { + margin-bottom: 1rem; +} + +.checkpoint-attributes .checkpoint-attribute:not(:last-child) { + margin-right: 5rem; +} + +.checkpoint-attribute svg { + margin-right: 10px; + position: relative; + top: -2px; +} + +.checkpoint-points-earned svg, .checkpoint-scoring svg { + margin-right: 5px; + position: relative; + top: 4px; + height: 21px; +} + +.checkpoint-attribute .quantity-attribute { + @include font-base; + line-height: 20px; + color: $color-mine-shaft; + font-weight: 500; +} + +.checkpoint-attribute .quantity-attribute { + b { + @include font-base; + font-weight: 500; + color: $color-mine-shaft; + + text-align: right; + line-height: normal; + } + + .subtext { + @include font-small; + color: $color-gray; + margin-left: 8px; + line-height: normal; + } +} + +.checkpoint-attribute .text-attribute, .checkpoint-scored-at { + @include font-small; + color: $color-gray; +} + +.checkpoint-scoring { + @include font-small; +} + +.checkpoint-scored-at { + line-height: 1.7; +} + +.checkpoint-scoring > .view-checkpoint { + line-height: 1.4; +} + +.checkpoint-scoring .actioncircle { + --actioncircle-size: 24px; + line-height: 0; +} + +.checkpoint-landing-summary { + margin-bottom: 30px; +} + +.checkpoint-centered { + margin: 0 auto 1rem; +} + +.checkpoint-points-earned { + @include font-base; + line-height: 1; + color: $color-mine-shaft; + display: inline; +} + +.checkpoint-started-at { + @include font-small; + font-weight: 400; + margin: 0 auto 1rem; +} + +.checkpoint-end-message { + text-align: center; + font-size: 18px; + color: $color-mine-shaft; + margin: 2rem 0 4rem; +} + +.checkpoint-details { + display: flex; + flex-flow: column nowrap; + padding: 2rem; + background-color: #FBFBFB; + border-radius: 5px; + border: 1px solid #E1E1E1; + margin-top: 2rem; + + .checkpoint-scoring { + margin-bottom: 32px; + } + + .button-wrapper.checkpoint-landing-button { + margin: 0 auto; + max-width: 275px; + position: relative; + } + + &.timed-checkpoint { + background-color: #FFFCF0; + + .checkpoint-started-at { + @include font-paragraph; + } + + .time-remaining svg{ + position: relative; + top: 6px; + margin-right: 8px; + } + } +} + +.primary-teal { + color: $color-primary-teal; +} \ No newline at end of file diff --git a/scripts/app/assets/stylesheets/components/_checkpoint-submission.scss b/scripts/app/assets/stylesheets/components/_checkpoint-submission.scss new file mode 100644 index 0000000000000000000000000000000000000000..f7b309afcc8319df4f8dfa0b7e02551030dc5e84 --- /dev/null +++ b/scripts/app/assets/stylesheets/components/_checkpoint-submission.scss @@ -0,0 +1,152 @@ +.checkpoint-submission { + .header { + background: $color-darkest-blue-gray; + height: 112px; + padding: 32px; + display: flex; + justify-content: space-between; + } + + .header > div > .label { + color: $color-black-60-8; + font-size: 12px; + letter-spacing: 2px; + margin-bottom: 8px; + font-weight: 300; + padding-left: 0; + } + + .header > div > .title { + color: $color-white; + font-size: 24px; + } + + .header > .header-right .lp-badge { + margin-bottom: 8px; + } + + .header > .header-right > .title { + text-align: center; + } + + .challenge-block > .problem { + h3 { + font-size: 24px; + font-weight: 500; + line-height: 1.1; + color: $color-black-20; + margin: 20px 0 10px; + } + + p { + margin: 0 0 10px; + } + + pre, + pre > code, + pre > code > span { + font-family: Menlo, Monaco, Consolas, "Courier New", monospace; + } + } +} + +.checkpoint-submission .body-container { + & { + display: flex; + flex-flow: row wrap; + height: auto; + + @media screen and (min-width: $tablet-breakpoint) { + min-height: 100vh; + } + } + + > .challenges { + flex: 1 1 auto; + width: 60%; + padding: 0 1.5rem; + overflow: hidden; + background: $color-white; + } + + > .standards-wrapper { + flex: 0 0 auto; + width: 480px; + max-width: 100%; + } + + > .standards-wrapper > .standards { + background: $color-black-97-3; + padding: 1.5rem 1rem 0; + + &.-is-sticky { + flex: 0 0 auto; + width: 480px; + overflow: scroll; + position: fixed; + z-index: 9999; + height: calc(100% - 4rem); + top: 4rem; + right: 0; + } + &.-is-student { + height: 100%; + top: 0; + } + } + + > .standards-wrapper > .standards .standard-score-info { + flex: 1 1 100%; + } + + > .standards-wrapper > .standards .standard-score-info > .label { + height: 20px; + font-size: 12px; + font-weight: bold; + letter-spacing: 1px; + line-height: 12px; + text-transform: uppercase; + padding-left: 0; + color: $color-dark-blue-gray; + + &.gray-label { + color: $color-black-60-8; + } + + &.cyan-label { + color: $color-cyan; + } + + > .svg { + flex: 0 0 auto; + position: absolute; + left: auto; + margin: auto; + pointer-events: none; + right: 60px; + } + } + + .standards .standard-score-info > p { + margin: 0; + height: 16px; + font-size: 12px; + font-weight: 400; + line-height: 16px; + color: $color-black-60-8; + } + + .standard-topics-rollups { + display: flex; + flex-flow: row wrap; + margin-top: 10px; + + > .pill-wrapper.small { + margin: 0 10px 10px 0; + } + + &.hide-rollups { + display: none; + } + } +} diff --git a/scripts/app/assets/stylesheets/components/_checkpoint-submissions-columns-student.scss b/scripts/app/assets/stylesheets/components/_checkpoint-submissions-columns-student.scss new file mode 100644 index 0000000000000000000000000000000000000000..4ab3c531f32b0ddc6e9c6d31b408c4ffd66b27b7 --- /dev/null +++ b/scripts/app/assets/stylesheets/components/_checkpoint-submissions-columns-student.scss @@ -0,0 +1,72 @@ +.checkpoint-submissions-columns-student { + & { + position: relative; + display: flex; + flex-flow: row nowrap; + background: white; + height: 56px; + border: 1px solid transparent; + border-bottom-color: $color-black-83-1; + padding: 8px 16px; + } + + &.-is-not-submitted { + align-items: center; + background: none; + + &:hover { + border-color: transparent; + border-bottom: 1px solid $color-black-83-1; + box-shadow: none; + cursor: auto; + } + + > .usercel > .usertimestamp { + display: none; + } + + > .lp-badge { + display: none; + } + } + + &:hover { + border-color: $color-cyan; + box-shadow: 0 2px 8px 0 rgba(0,0,0,0.2); + cursor: pointer; + } + + > .user-avatar { + padding: 0; + flex: 0 0 auto; + } + + > .usercel { + margin: 0 8px; + flex: 1 1 auto; + min-width: 0; + } + + > .usercel > .username { + color: $color-darker-gray-blue; + font-size: 18px; + line-height: 24px; + overflow: hidden; + white-space: nowrap; + text-overflow: ellipsis; + } + + > .usercel > .usertimestamp { + color: $color-black-60-8; + font-size: 12px; + line-height: 16px; + overflow: hidden; + white-space: nowrap; + text-overflow: ellipsis; + } + + > .lp-badge { + flex: 0 0 auto; + margin: 2px 0; + } +} diff --git a/scripts/app/assets/stylesheets/components/_checkpoint-submissions-columns.scss b/scripts/app/assets/stylesheets/components/_checkpoint-submissions-columns.scss new file mode 100644 index 0000000000000000000000000000000000000000..40135126ff666d88daacd6b42bf391d775b9fe1a --- /dev/null +++ b/scripts/app/assets/stylesheets/components/_checkpoint-submissions-columns.scss @@ -0,0 +1,36 @@ +.checkpoint-submissions-columns { + & { + display: flex; + flex-flow: row nowrap; + } + + > .column { + flex: 1 1 auto; + width: calc(100%/3 - 64px); + + &.-no-shadow > .colbody { + box-shadow: none; + } + } + > .column:not(:last-of-type) { + margin-right: 32px; + } + + > .column > .colbody { + box-shadow: 0 2px 8px 0 rgba(0,0,0,0.1); + } + + > .column > .colbody > a { + text-decoration: none; + } + + > .column > .colheading { + height: 56px; + border-bottom: 2px solid $color-black-83-1; + color: $color-black; + font-size: 20px; + display: flex; + align-items: center; + justify-content: center; + } +} diff --git a/scripts/app/assets/stylesheets/components/_checkpoint-submissions-index.scss b/scripts/app/assets/stylesheets/components/_checkpoint-submissions-index.scss new file mode 100644 index 0000000000000000000000000000000000000000..5d102a643deb4014365481e4d1a6c31770e1ebd4 --- /dev/null +++ b/scripts/app/assets/stylesheets/components/_checkpoint-submissions-index.scss @@ -0,0 +1,5 @@ +.checkpoint-submissions-index { + background: $color-black-97-3; + overflow: hidden; + padding: 0 32px 40px; +} diff --git a/scripts/app/assets/stylesheets/components/_checkpoint_student_scores.scss b/scripts/app/assets/stylesheets/components/_checkpoint_student_scores.scss new file mode 100644 index 0000000000000000000000000000000000000000..c31efc4442e6a77f2e4528a1d45986cd9390c40b --- /dev/null +++ b/scripts/app/assets/stylesheets/components/_checkpoint_student_scores.scss @@ -0,0 +1,184 @@ +.lesson-layout .body-container .checkpoint-student-scores { + background: $color-alabaster; + + .mdown-container .checkpoint-results { + h1:first-of-type { + margin-top: 45px; + margin-bottom: 40px; + } + h2:first-of-type { + margin-top: 45px; + margin-bottom: 40px; + } + } + + table.checkpoints-scores { + font-size: 16px; + width: 100%; + margin-top: 24px; + + tbody { + color: $color-tundora; + border: 1px solid $color-mercury; + box-shadow: 0 1px 10px 0 rgba(0,0,0,0.06); + background: white; + + tr { + border-bottom: 1px solid $color-black-93-3; + height: 56px; + + &:hover, &.never-signed-in:hover { + border-bottom: 1px solid #d0d0d0; + box-shadow: 0 0 10px 0 rgba(0,0,0,0.08); + } + + &.never-signed-in { + background: $color-alabaster; + span, div { + color: $color-black-60; + } + } + + &.row-selected td { + background: rgba(77, 169, 179, 0.1); + } + + &:last-of-type { + td { + border-bottom: none; + } + } + } + } + + th, td { + white-space: nowrap; + + &.checkbox-col { + width: 5%; + text-align: center; + } + + &.name-col > span, &.name-col > a { + padding: 0 20px 0 40px; + } + + &.score-col { + > a, > span { + padding: 0 20px; + } + } + + &.status-col { + width: 170px; + + > a, > span { + padding: 0 40px 0 20px; + } + } + + > input[type="checkbox"] { + box-sizing: border-box; + padding: 0; + display: inline-block; + vertical-align: middle; + -webkit-appearance: none; + -moz-appearance: none; + appearance: none; + cursor: pointer; + height: (8 * 3 + px); + width: (8 * 3 + px); + background: none center no-repeat; + background-size: cover; + margin: (8 * 1 + px); + margin-right: (8 * 2 + px); + flex: 0 0 auto; + } + + > input[type="checkbox"] { + background-image: url("data:image/svg+xml;charset=utf-8, %3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20width%3D%2224%22%20height%3D%2224%22%20viewBox%3D%220%200%2024%2024%22%20style%3D%22fill%3A%23cccccc%3B%22%3E%3Cpath%20d%3D%22M19%205v14H5V5h14m0-2H5c-1.1%200-2%20.9-2%202v14c0%201.1.9%202%202%202h14c1.1%200%202-.9%202-2V5c0-1.1-.9-2-2-2z%22%2F%3E%3C%2Fsvg%3E"); + } + + } + + th { + height: 36px; + > span { + color: $color-dove-gray; + font-weight: 400; + } + + > input[type="checkbox"]:checked { + background-image: url("data:image/svg+xml;charset=utf-8, %3Csvg%20xmlns=%22http://www.w3.org/2000/svg%22%20width=%2224%22%20height=%2224%22%20viewBox=%220%200%2024%2024%22%3E%3Cpath%20stroke=%22666%22%20strok-width=%221%22%20d=%22M19%206.41L17.59%205%2012%2010.59%206.41%205%205%206.41%2010.59%2012%205%2017.59%206.41%2019%2012%2013.41%2017.59%2019%2019%2017.59%2013.41%2012z%22/%3E%3C/svg%3E"); + } + } + + td { + height: inherit; + + > input[type="checkbox"]:checked { + border-color: $color-cyan; + background: rgba($color-cyan, 0.05); + background-image: url("data:image/svg+xml;charset=utf-8, %3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20width%3D%2224%22%20height%3D%2224%22%20viewBox%3D%220%200%2024%2024%22%20style%3D%22fill%3A%234DA9B3%3B%22%3E%3Cpath%20d%3D%22M19%203H5c-1.11%200-2%20.9-2%202v14c0%201.1.89%202%202%202h14c1.11%200%202-.9%202-2V5c0-1.1-.89-2-2-2zm-9%2014l-5-5%201.41-1.41L10%2014.17l7.59-7.59L19%208l-9%209z%22%2F%3E%3C%2Fsvg%3E"); + } + + > a, > span { + color: $color-tundora; + text-decoration: none; + height: 100%; + display: flex; + align-items: center; + } + + > a:hover { + color: $color-link-blue; + } + + &.name-col { + width: 100px; + height: inherit; + + .user-avatar { + margin-right: 10px; + } + + div { + max-width: 170px; + overflow: hidden; + text-overflow: ellipsis; + } + } + + &.score-col { + .checkpoint-score-text { + width: 18%; + } + + .checkpoint-score-bar { + width: 82%; + + div { + border-radius: 4px; + height: 12px; + } + } + } + + &.status-col { + svg { + margin-left: -2px; + margin-right: 5px; + } + + div.pending-submission { + border-radius: 100%; + height: 6px; + width: 6px; + background-color: #DC3D48; + margin-left: 2px; + margin-right: 8px; + } + } + } + } +} diff --git a/scripts/app/assets/stylesheets/components/_checkpoint_submisstion_state.scss b/scripts/app/assets/stylesheets/components/_checkpoint_submisstion_state.scss new file mode 100644 index 0000000000000000000000000000000000000000..8edd6dcba2d067d52a296b6efb7710d1467b3527 --- /dev/null +++ b/scripts/app/assets/stylesheets/components/_checkpoint_submisstion_state.scss @@ -0,0 +1,29 @@ +.checkpoint-submission-state-wrapper { + & { + text-align: right; + } + + >.badgerow { + display: flex; + flex-flow: row nowrap; + align-items: center; + justify-content: flex-end; + height: auto; + } + + > .badgerow > .lp-badge { + flex: 0 0 auto; + } + + > .badgerow > a { + color: $color-cyan; + text-transform: uppercase; + margin-left: 8px; + } + + > .timestamp { + color: $color-darkest-blue-gray; + font-size: 14px; + line-height: 24px; + } +} diff --git a/scripts/app/assets/stylesheets/components/_checkpoint_toolbar.scss b/scripts/app/assets/stylesheets/components/_checkpoint_toolbar.scss new file mode 100644 index 0000000000000000000000000000000000000000..84d7a6dcbff000214ec6f1ec1226ea7c8f35782b --- /dev/null +++ b/scripts/app/assets/stylesheets/components/_checkpoint_toolbar.scss @@ -0,0 +1,207 @@ +$sidebar-width: 280px; +$max-content-width: 1280px; + +.checkpoint-submit { + & { + width: 100%; + height: auto; + text-align: center; + z-index: 9; + + .lesson-layout.-is-collapsed & { + padding-left: 0; + } + } + + > .checkpoint-toolbar { + position: relative; + display: flex; + flex-flow: column; + justify-content: center; + width: 100%; + max-width: $max-content-width; + margin: 0 auto; + padding: 0 1rem; + color: $color-white; + height: auto; + min-height: 4.5rem; + @media screen and (min-width: $tablet-breakpoint) { + padding: 0 3rem; + } + } + + .bodyrow { + flex: 0 0 auto; + margin-top: 40px; + max-width: 78%; + color: $color-white; + font-size: 20px; + font-weight: 500; + line-height: 24px; + display: none; + @media screen and (min-width: $tablet-breakpoint) { + display: block; + } + } + + .actionrow { + flex: 0 0 auto; + display: flex; + flex-flow: column; + align-items: center; + justify-content: center; + padding: 1rem 0; + } + + .actionrow > .-submitbttn { + margin: 1rem; + } + + .actionrow > .-savebttn { + margin: 1rem; + } + + .closebttn { + position: absolute; + top: 0; + right: 80px; + display: none; + align-items: center; + justify-content: center; + padding: 20px 8px; + cursor: pointer; + + &:hover { + color: $color-white; + } + @media screen and (min-width: $tablet-breakpoint) { + display: flex; + } + + } + + .afterSubmitModal { + position: fixed; + top: 0; + left: 0; + z-index: 1000000; + background: rgba(0, 0, 0, 0.4); + width: 100%; + height: 100%; + display: flex; + justify-content: center; + align-items: center; + + .error-wrapper { + display: flex; + flex-direction: row; + justify-content: center; + align-items: center; + } + + .error-svg { + height: 24px; + width: 24px; + + svg { + fill: red; + } + } + + .error-header { + font-size: 22px; + font-weight: 600; + margin: 0 !important; + text-transform: uppercase; + color: #616161; + } + + .error-header-wrapper { + display: flex; + align-items: center; + height: 47px; + margin-left: 5px; + } + + .error-subheader { + margin: 16px 0 25px 0 !important; + text-align: left; + font-weight: 500; + color: #616161 !important; + } + + .error-message-wrapper { + margin-bottom: 20px; + } + + .error-challenge-number { + text-align: left; + text-transform: uppercase; + color: #b7b7b7; + font-size: 17px; + margin: 0 !important; + } + + .error-message { + text-align: left; + margin: 0 !important; + font-weight: 500; + color: #616161 !important; + } + } + + .afterSubmitModal .theModal .results-link { + color: $color-cyan; + font-size: 12px; + font-weight: bold; + text-decoration: none; + } + + .afterSubmitModal .theModal { + width: 500px; + border: 1px solid $color-black-60; + background: $color-white; + border-radius: 3px; + padding: 3rem; + position: relative; + } + + .afterSubmitModal .theModal h3 { + color: $color-dark-gray; + text-transform: uppercase; + } + + .afterSubmitModal .theModal h3 svg { + position: relative; + top: 6px; + margin-right: 0.5rem; + color: $color-black-60; + height: 30px; + width: 30px; + } + + .afterSubmitModal .theModal h3 img { + position: relative; + top: -3px; + margin-right: 0.5rem + } + + .afterSubmitModal .theModal p { + color: $color-black-60; + margin: 2rem 0; + } + + .afterSubmitModal .theModal p span { + font-size: 2rem; + + span { + color: $color-cyan; + font-weight: bold; + } + } + + .afterSubmitModal .theModal .closebttn { + right: 15px; + color: $color-black-60; + } +} diff --git a/scripts/app/assets/stylesheets/components/_code-block.scss b/scripts/app/assets/stylesheets/components/_code-block.scss new file mode 100644 index 0000000000000000000000000000000000000000..37624685941fc9940b51373b81e3066a74f53b8e --- /dev/null +++ b/scripts/app/assets/stylesheets/components/_code-block.scss @@ -0,0 +1,21 @@ +@import "highlightjs/styles/atom-one-light"; + +.hljs { + background: #f6f8fa; + padding: 16px; +} + +pre { + background: $color-white; + border-color: $color-black-83-1; +} + +pre.text { + background: $color-black-97-3; + padding: 0.5em; +} + +code { + color: black; + background-color: #f8f8f8; +} diff --git a/scripts/app/assets/stylesheets/components/_cohort-submissions-table.scss b/scripts/app/assets/stylesheets/components/_cohort-submissions-table.scss new file mode 100644 index 0000000000000000000000000000000000000000..da27f8113e225b6ee469d35603980d08ea08a4f5 --- /dev/null +++ b/scripts/app/assets/stylesheets/components/_cohort-submissions-table.scss @@ -0,0 +1,798 @@ +$submissions-cell-height: 56px; +.cohort-submissions { + %student-column { + & { + flex: 0 0 auto; + width: 40px; + } + + > .block-title-spacer { + background: $color-black-70-2; + height: 56px; + } + + > .student { + background: $color-white; + display: flex; + flex-flow: column; + align-items: center; + justify-content: center; + height: $submissions-cell-height; + padding: 8px; + cursor: pointer; + + &:hover { + background: $color-black-97-3; + } + } + + > .standard > .standarditem { + position: relative; + width: 100%; + border-right: 1px solid $color-black-94-3; + border-bottom: 1px solid $color-black-94-3; + + &:not(:last-of-type) { + border-bottom: 2px solid $color-black-83-1; + } + + &.checkpoint { + border-bottom: 21px solid $color-black-84-7; + } + } + + > .standard > .standarditem > .itemheading { + position: relative; + display: flex; + flex-flow: column; + align-items: center; + justify-content: center; + height: $submissions-cell-height; + background: $color-black-97-3; + padding: 8px; + } + + > .standard > .standarditem > .itemheading.performance { + background: $color-white; + } + + > .standard > .standarditem > .itemheading .tinybang { + position: absolute; + bottom: 26px; + right: 28px; + height: 12px; + } + + > .standard > .standarditem > .itemheading .actioncircle.tinyscore { + position: absolute; + bottom: 38px; + right: 24px; + height: 12px; + width: 12px; + } + + > .standard > .standarditem > .itemheading .actioncircle.pending-submission { + position: absolute; + bottom: 40px; + left: 27px; + height: 8px; + width: 8px; + background-color: #DC3D48; + } + + > .standard > .standarditem > .itemheading a { + text-decoration: none; + } + + > .standard > .standarditem > .itemheading .emdash { + &::before { + content: "—" + } + color: $color-dark-gray; + } + + > .standard > .standarditem > .itemheading .percent-correctness { + font-size: 13px; + font-weight: 300; + color: $color-dark-gray; + } + + > .standard > .standarditem > .itemheading .actioncircle { + &{ + border-radius: 100%; + color: $color-white; + display: flex; + align-items: center; + justify-content: center; + height: 23px; + width: 23px; + font-size: 10px; + line-height: 15px; + font-weight: 200; + } + + &.-score-3 { + background-color: $color-lime-green; + } + + &.-score-2 { + background-color: $color-banana-yellow; + } + + &.-score-1 { + background-color: $color-tangerine-orange; + } + + &.-ungraded { + border: 2px solid $color-black-20; + } + } + + > .standard > .standarditem > .itembody { + display: flex; + flex-flow: column; + align-items: center; + justify-content: center; + position: relative; + overflow: hidden; + background: $color-black-99-3; + height: $submissions-cell-height; + border-top: 1px solid $color-black-94-3; + } + + > .standard > .standarditem > .itembody > a { + flex: 1 1 100%; + display: inherit; + align-items: inherit; + justify-content: inherit; + padding: 8px; + + &:hover { + background: $color-black-92-2; + } + } + + > .standard > .emptyrow { + height: 56px; + background: $color-black-92-2; + } + + > .standard > .standarditem > .itemheading > svg.donut, + > .standard > .standarditem > .itembody > svg.donut { + height: 100%; + width: 100%; + } + } + + %standard-item { + & { + } + + > .standardtitle { + display: flex; + flex-flow: row nowrap; + align-items: center; + background: $color-white; + color: $color-dark-blue-gray; + font-size: 12px; + font-weight: bold; + letter-spacing: 2px; + height: 56px; + line-height: 56px; + text-transform: uppercase; + user-select: none; + cursor: pointer; + } + + > .standardtitle .action-kebab { + position: relative; + top: 8px; + cursor: pointer; + color: $color-black-46-7; + } + + > .standardtitle .action-menu, .standarditem .action-menu { + position: absolute; + top: -1px; + left: 8px; + width: 220px; + z-index: 10; + + ul { + background: white; + border: 1px solid $color-black-60-8; + border-radius: 8px; + list-style-type: none; + margin: 0; + padding: 0; + line-height: 56px; + + li { + padding: 0px 16px; + box-shadow: 0 1px 0 0 rgba(0, 0, 0, 0.05); + + a { + font-size: 16px; + font-weight: 500; + line-height: 24px; + letter-spacing: 0px; + color: $color-black; + text-decoration: none; + text-transform: none; + } + } + + li:last-of-type { + border-bottom: 0px solid transparent; + } + } + } + .standarditem .action-menu { + top: -9px; + } + + > .standardtitle > .iconwrapper.kebab { + position: relative; + } + + > .standardtitle > .iconwrapper.arrow { + transform: rotate(-90deg); + color: inherit; + cursor: pointer; + } + + .standardtitle.-open > .iconwrapper.arrow { + transform: rotate(0); + color: $color-cyan; + } + + > .standardtitle > .title { + flex: 1; + height: 100%; + padding: 0 16px; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + } + + > .standarditem { + position: relative; + + &:not(:last-of-type) { + border-bottom: 2px solid $color-black-83-1; + } + + &.-open > .itemheading > .iconwrapper.arrow { + transform: rotate(0); + color: $color-cyan; + } + + &.checkpoint { + border-bottom: 20px solid $color-black-84-7; + } + } + + > .standarditem .kebab { + position: absolute; + right: 0; + top: 15px; + cursor: pointer; + color: $color-black-46-7; + } + + > .standarditem > .itemheading { + color: $color-dark-blue-gray; + background: $color-black-97-3; + position: relative; + display: flex; + flex-flow: column; + justify-content: center; + height: $submissions-cell-height; + + &:hover { + cursor: pointer; + color: $color-cyan; + } + } + + > .standarditem > .itemheading > .iconwrapper { + position: absolute; + top: 0; + bottom: 0; + left: 0; + right: auto; + margin: auto; + margin-left: 8px; + height: 24px; + width: 24px; + } + + > .standarditem > .itemheading > .iconwrapper { + &.arrow { + transform: rotate(-90deg); + color: inherit; + } + &.star { + color: $color-galvanize-orange + } + } + + > .standarditem > .itemheading > .challenge-table-label { + text-transform: uppercase; + color: $color-black-60-8; + font-size: 10px; + letter-spacing: 2px; + line-height: 16px; + margin-left: 40px; + } + + > .standarditem > .itemheading > .title { + //color: $dark-blue-gray; + color: inherit; + font-size: 16px; + line-height: 24px; + margin-left: 40px; + padding-right: 16px; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + } + + > .standarditem > .itemheading > .title > a { + text-decoration: none; + color: inherit; + } + + > .standarditem > .itembody { + position: relative; + height: $submissions-cell-height; + background: $color-black-99-3; + border-left: 4px solid $color-cyan; + padding-left: 32px; + } + + > .standarditem > .itembody > .short-border-box { + display: flex; + flex-flow: column; + justify-content: center; + border-top: 1px solid $color-black-94-3; + height: 100%; + } + + > .standarditem > .itembody > .short-border-box > .challenge-table-label { + color: $color-black-60-8; + font-size: 10px; + letter-spacing: 2px; + line-height: 16px; + text-transform: uppercase; + margin-left: 16px; + } + + > .standarditem > .itembody > .short-border-box > .title { + color: $color-dark-blue-gray; + font-size: 16px; + line-height: 24px; + margin-left: 16px; + margin-right: 8px; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + } + } + + .submissions-dashboard-table-wrapper { + .flex-row { + > .nameplate { + margin-bottom: 1rem; + } + + > .nameplate > .avatar-wrapper { + width: 40px; + display: inline-block; + line-height: 56px; + margin-right: 10px; + vertical-align: middle; + } + + > .nameplate > span { + display: inline-block; + line-height: 56px; + font-size: 1.1rem; + vertical-align: middle; + color: $color-darker-gray-blue; + } + } + } + + .flex-row { + .sort-dropdown-component { + margin-bottom: 15px; + } + + .expand-collapse { + font-size: 10px; + color: $color-sky-blue; + align-self: center; + } + } + + .manual-score-performance { + width: 188px; + height: 40px; + background-color: white; + border: 1px solid $color-black-90-2; + position: absolute; + z-index: 10; + top: 8px; + left: 107%; + box-shadow: 0px 0px 4px #ccc; + + .scores { + display: flex; + flex-flow: row; + align-items: center; + height: 100%; + } + + &.show-success-criteria { + width: 270px; + height: auto; + + .scores .toggle-success-criteria { + margin-right: 12px; + margin-left: auto; + } + + .scores { + height: 38px; + } + + .success-criteria h1 { + color: #272e39; + font-size: 16px; + font-weight: bold; + letter-spacing: 2px; + margin-bottom: 12px; + } + + .success-criteria h4 { + color: #a8a8a8; + text-transform: uppercase; + font-size: 12px; + font-weight: 600; + margin-bottom: 10px; + } + + .success-criteria ul { + padding-left: 1.2rem; + } + } + + .scores > .actioncircle { + border-radius: 100%; + color: $color-white; + display: flex; + align-items: center; + justify-content: center; + height: 23px; + width: 23px; + font-size: 10px; + line-height: 15px; + font-weight: 200; + background: $color-black-84-7; + margin-left: 12px; + cursor: pointer; + + &.score1 { + &.active { + background-color: $color-tangerine-orange; + } + &:hover { + background-color: $color-tangerine-orange; + } + } + &.score2 { + &.active { + background-color: $color-banana-yellow; + } + &:hover { + background-color: $color-banana-yellow; + } + } + &.score3 { + &.active { + background-color: $color-lime-green; + } + &:hover { + background-color: $color-lime-green; + } + } + } + + .scores > .triangle-thing { + width: 15px; + height: 15px; + z-index: 9; + position: absolute; + background: $color-white; + border-left: 1px solid $color-black-90-2; + border-bottom: 1px solid $color-black-90-2; + -webkit-transform: rotate(45deg); + transform: rotate(45deg); + left: -8px; + } + + .scores > .toggle-success-criteria { + color: $color-black-84-7; + height: 23px; + width: 23px; + display: flex; + align-items: center; + justify-content: center; + margin-left: 12px; + cursor: pointer; + } + + > .success-criteria { + padding: 12px; + width: 100%; + border-top: 1px solid $color-black-90-2; + } + } + + .cohort-submission-legend { + & { + position: relative; + display: flex; + flex-flow: row; + justify-content: flex-end; + } + + > .svg { + opacity: .6; + margin-bottom: .5rem; + cursor: help; + + &:hover + .infomodal { + display: block; + } + } + + > .infomodal { + font-size: 14px; + max-width: 360px; + padding: 1rem 1.5rem; + border-radius: .5rem; + color: $color-white; + background-color: $color-black-10-2; + box-shadow: 0 2px 8px 0 rgba(0,0,0,0.3); + cursor: help; + display: none; + position: absolute; + top: 0; + right: 0; + z-index: 10; + + &:hover { + display: block; + } + } + } + + .challengeindex-table { + & { + position: relative; + display: flex; + flex-flow: row nowrap; + overflow: hidden; + background-color: $color-white; + box-shadow: 0 2px 8px 0 rgba(0, 0, 0, 0.1); + } + + &:after { + content: ""; + position: absolute; + left: 100%; + top: 0; + bottom: 0; + box-shadow: -4px 0 6px 1px rgba(black, 0.1); + background: white; + width: 10px; + } + + &.single-student { + &:after { + box-shadow: none; + } + } + + > .studentspanel { + position: relative; + z-index: 0; + flex: 1 1 100%; + overflow: hidden; + background: #e8e8e8; + } + + > .studentspanel .studentcolumn { + @extend %student-column; + } + + > .curriculumpanel { + position: relative; + z-index: 1; + flex: 0 0 auto; + width: 320px; + padding-top: $submissions-cell-height; + box-shadow: 4px 0 6px 1px rgba(black, 0.1); + } + + > .curriculumpanel > .block-title { + background: $color-black-70-2; + height: 56px; + padding: 0 1rem; + color: $color-dark-blue-gray; + font-size: 12px; + font-weight: bold; + letter-spacing: 2px; + line-height: 56px; + text-transform: uppercase; + user-select: none; + cursor: default; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + } + + > .curriculumpanel > .standard { + @extend %standard-item; + border-bottom: 1px solid $color-black-83-1; + } + + > .curriculumpanel > .standard > .firstcontentfile { + border-top: 2px solid $color-black-83-1; + } + + > .curriculumpanel { + &.single-student { + padding-top: 0; + box-shadow: none; + + > .standard > .standarditem { + border-right: 2px solid $color-black-83-1; + } + } + } + + > .studentspanel { + &.single-student { + overflow-x: auto; + + .studentcolumn { + flex: 1 0 auto; + } + + > .studentcolumn > .standard > .standarditem > .itemheading { + flex-flow: row; + justify-content: flex-end; + } + + > .studentcolumn > .standard > .standarditem > .itembody { + flex-flow: row; + justify-content: flex-end; + } + + > .studentcolumn > .standard > .standarditem > .itemheading svg.donut { + width: 24px; + } + } + } + } + + .columnwrapper { + width: 100%; + overflow: auto; + display: flex; + flex-flow: row nowrap; + } + + .studentnamebar { + position: fixed; + top: 0; + z-index: 999; + width: calc(100% - 32px - 320px); + overflow: auto; + + &.-is-hidden { + visibility: hidden; + pointer-events: none; + } + + > .studentavatars { + display: flex; + flex-flow: row nowrap; + } + + > .studentavatars > .student { + background: $color-white; + flex: 0 0 auto; + display: flex; + flex-flow: column; + align-items: center; + justify-content: center; + height: $submissions-cell-height; + width: 40px; + padding: 8px; + border-bottom: 1px solid $color-black-83-1; + + &:hover { + background: $color-black-97-3; + } + } + } + + .dropdown-component > select { + background-color: $color-white; + } + + .icon-legend { + & { + height: 48px; + background: none; + border: 1px solid $color-black-83-1; + border-radius: 4px; + display: inline-flex; + flex-flow: row nowrap; + vertical-align: bottom; + align-items: center; + margin-bottom: 32px; + user-select: none; + overflow: hidden; + position: relative; + } + + > .item { + flex: 1 0 auto; + display: flex; + flex-flow: row nowrap; + align-items: center; + margin: 0 16px; + } + + > .item > .challenge-table-label { + color: $color-darker-gray-blue; + font-size: 16px; + line-height: 22px; + } + + > .item > .svg { + margin-right: 8px; + } + } + + .flex-row { + & { + position: relative; + display: flex; + flex-flow: row; + } + &.-align-middle { + align-items: center; + } + + > .-flex-alignright { + margin-right: 0; + margin-left: auto; + } + + > .-margin-right { + margin-right: 8px; + } + } + + // other flex-row styles + .flex-row > .pullright { + margin-right: 0; + margin-left: auto; + } +} diff --git a/scripts/app/assets/stylesheets/components/_cohort_releases.scss b/scripts/app/assets/stylesheets/components/_cohort_releases.scss new file mode 100644 index 0000000000000000000000000000000000000000..e95a7f41d6877b7591850f40b96bb72bc5635151 --- /dev/null +++ b/scripts/app/assets/stylesheets/components/_cohort_releases.scss @@ -0,0 +1,205 @@ +.available-blocks { + h2 { + font-weight: bold; + } + + table.block-table { + color: $color-black-46-7; + + tr:nth-of-type(odd) { + background: rgba(0, 0, 0, 0.05); + + td:first-of-type { + border-top-left-radius: 5px; + border-bottom-left-radius: 5px; + } + td:last-of-type { + border-top-right-radius: 5px; + border-bottom-right-radius: 5px; + } + } + + th { + background: white; + } + + th, td { + padding: .5rem; + border-bottom: none; + border-top: none; + } + + .center { + text-align: center; + } + } + + .action-cell { + position: relative; + + .action-menu { + display: none; + position: absolute; + top: 0; + right: 0; + width: 420px; + z-index: 10; + + ul { + background: white; + border: 1px solid $color-black-46-7; + list-style-type: none; + margin: 0; + padding: 0; + + li { + font-size: 14px; + padding: .5rem 1rem; + + a { + svg.svg { + height: 14px; + width: 14px; + margin-left: 4px; + vertical-align: middle; + } + + div { + display: inline; + } + } + } + + li:last-of-type { + border-bottom: 0px solid transparent; + } + } + } + + &:hover { + .action-menu { + display: block; + } + } + } +} + +.cohort-releases-show, .releases-show { + h2 { + padding: 16px 32px; + } + + .pick-version-table { + width: 100%; + } + + .pick-version-table .createdcolumn { + padding-left: 24px; + } + + .pick-version-table > thead { + background-color: #FFFFFF; + border-top: 1px solid $color-black-77; + border-bottom: 1px solid $color-black-77; + height: 40px; + + tr { + font-size: 14px; + line-height: 16px; + text-transform: uppercase; + color: #999999; + } + + tr > th { + font-weight: 500; + padding: 12px; + } + } + + .pick-version-table > tbody { + background-color: #FFFFFF; + + .releaserow { + border-bottom: 1px solid #e7e7e7; + + &.current-release { + background-color: rgba(0,165,181,0.05); + } + } + + .releaserow > td { + padding: 16px; + font-size: 16px; + vertical-align: top; + position: relative; + + &.createdcolumn { + padding-left: 24px; + } + } + + .releaserow > td > h5 { + font-weight: bold; + line-height: 16px; + font-size: 16px; + } + + .releaserow > td > p { + line-height: 16px; + margin-bottom: 8px; + } + + .releaserow > td > a.gitlink { + line-height: 16px; + font-weight: 300; + margin-right: 24px; + } + + .releaserow > td > span.currentlabel { + line-height: 16px; + color: $color-cyan; + text-transform: uppercase; + font-size: 12px; + font-weight: bold; + letter-spacing: 1px; + text-align: center; + padding: 0 24px; + } + } +} + +.cohorts-tooltip { + position: absolute; + top: 14%; + right: 80%; + display: flex; + flex-direction: column; + width: 220px; + border-radius: .5rem; + background-color: $color-black-10-2; + color: $color-white; + overflow: hidden; + box-shadow: 0 2px 8px 0 rgba(0,0,0,0.3); + margin-top: -22px; + margin-left: -10px; +} + +.cohorts-tooltip > .body > div { + padding: 16px; +} + +.cohorts-tooltip > .body > div > p { + margin: 0; +} + +.cohorts-tooltip > .body > div > p > a { + text-decoration: none; + color: white; +} + +.cohorts-tooltip > .body > div > p > a > svg { + height: 14px; + margin-left: 5px; + position: absolute; + margin-top: 5px; +} diff --git a/scripts/app/assets/stylesheets/components/_cohorts.scss b/scripts/app/assets/stylesheets/components/_cohorts.scss new file mode 100644 index 0000000000000000000000000000000000000000..5694369242a119579fc53331dab5962534cb7728 --- /dev/null +++ b/scripts/app/assets/stylesheets/components/_cohorts.scss @@ -0,0 +1,957 @@ +.cohorts { + padding: 36px 0; + + .cohortstable { + margin: 20px 0; + } + + .cohortsetuptable .cohorts-list .cohortsetuprow > td, th { + line-height: 1.5rem; + padding: 12px; + vertical-align: middle; + font-weight: normal; + } + + .cohortsetuptable .cohortsetupheader .sortdirection { + position: absolute; + display: inline-flex; + align-items: center; + justify-content: center; + } + + .filters { + margin-bottom: 20px; + } + + a { + font-size: 14px; + margin-top: 7px; + text-decoration: none; + color: $color-sky-blue; + } + + .filterGroup { + margin-bottom: 14px; + } + + .filter-types { + display: -webkit-flex; /* Safari */ + -webkit-flex-wrap: wrap; /* Safari 6.1+ */ + display: flex; + flex-wrap: wrap; + + label { + color: black; + background: linear-gradient(to bottom, #fff 0%, #eceeef 100%); + border-radius: 4px; + margin: 2px 5px 2px 0; + padding: 3px 5px 3px 5px; + border: 1px solid #e6e6e6; + font-size: 12px; + font-family: Arial, sans-serif; + font-weight: lighter; + line-height: 16px; + overflow: hidden; + white-space: nowrap; + text-overflow: ellipsis; + } + + i { + padding-right: 5px; + font-size: 10px; + color: black; + cursor: pointer; + font-weight: bold; + } + } + + .new-cohort-btn { + float: right; + + .help-text { + display: none; + } + &.disabled { + a.disabled { + background-color: grey; + border-color: grey; + } + &:hover { + .help-text { + display: block; + font-size: .6rem; + margin-top: 1px; + padding-right: 5px; + } + } + } + } +} + +.cohortinfo { + background-color: $color-black-94-9; + height: 87px; + padding: 0px 36px 0px 36px; + display: flex; + + p { + font-weight: 400; + font-size: 14px; + } + + h6 { + font-weight: bold; + margin-bottom: 4px; + } + + .cohorttitle{ + flex: 1 0 auto; + justify-content: flex-start; + margin-top: 24px; + padding-left: 7px; + } + .cohortdetails{ + display: flex; + justify-content: flex-end; + flex: 0 0 auto; + margin-top: 24px; + } + .cohortdetails > a { + color: $color-cyan; + font-size: 16px; + line-height: 20px; + display: flex; + justify-content: space-between; + } + .cohortdetails > .cohortdetail { + margin-right: 32px; + position: relative; + } +} + +.settingscontainer { + margin: 0px 36px 0px 36px; + + .settingsrow { + display: flex; + border-bottom: 1px solid $color-black-84-7; + } + + .settingsrow > .selectedsettingstab { + flex: 1 0 auto; + justify-content: flex-start; + display: flex; + } + + .settingsrow > .selectedsettingstab > div > p { + font-size: 18px; + line-height: 16px; + font-weight: 500; + padding: 16px 24px 16px 8px; + margin-bottom: 0; + } + .settingsrow > .selectedsettingstab > div > p > a { + text-decoration: none; + color: inherit; + } + + .settingsrow > .selectedsettingstab > .selected { + color: $color-cyan; + border-bottom: 4px solid $color-cyan; + } + + .settingsrow > .managebutton { + flex: 0 0 auto; + align-self: center; + } + + .settingsrow > .managebutton > a { + display: flex; + flex-flow: row nowrap; + justify-content: space-between; + font-weight: 200; + color: white; + } + + .resync-ui { + padding: 1rem 0; + .submit { margin-left: 1rem; } + #resync-url { text-overflow: ellipsis; } + .github-icon { + height: 1.4rem; + position: relative; top: -1px; + } + .non-success-label { padding-right: 0.5rem; } + } + .resync-success-icon { + display: inline-block; + height: 24px; + vertical-align: bottom; + width: 24px; + background: url("data:image/svg+xml;charset=utf-8, %3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20width%3D%2224%22%20height%3D%2224%22%20viewBox%3D%220%200%2024%2024%22%20style%3D%22fill%3A%2369A2D7%3B%22%3E%3Cpath%20d%3D%22M19%203H5c-1.11%200-2%20.9-2%202v14c0%201.1.89%202%202%202h14c1.11%200%202-.9%202-2V5c0-1.1-.89-2-2-2zm-9%2014l-5-5%201.41-1.41L10%2014.17l7.59-7.59L19%208l-9%209z%22%2F%3E%3C%2Fsvg%3E"); + } + .successful-resync { + margin-top: 8px; + font-size: 80%; + } + + .import-work { + margin-right: 2rem; + display: flex; + height: 100%; + align-items: center; + + span { + margin: 0 10px; + color: $color-black-60; + } + + svg.success { + color: $color-cyan; + } + + svg.failure { + color: $color-valencia + } + + a { + color: $color-cyan; + text-decoration: none; + } + } +} + +.branch-modal { + position: fixed; + top: 0; + right: 0; + left: 0; + margin: 0 auto; + z-index: 1000000; + box-sizing: border-box; + width: 482px; + height: 202px; + background-color: #FFFFFF; + box-shadow: 0 2px 8px 0 rgba(0,0,0,0.15); + + .modal-header { + padding: 0px; + padding-top: 10px; + padding-left: 24px; + padding-right: 24px; + background-color: #FAFAFA; + } + + .modal-header > h1 { + font-size: 16px; + font-weight: bold; + line-height: 24px; + } + + .modal-header > div > svg { + color: $color-black-60; + cursor: pointer; + } + + .modal-body { + ::-webkit-input-placeholder { /* Chrome/Opera/Safari */ + color: gray; + opacity: .5; + font-weight: italic; + font-style: italic; + padding: 1rem; + border-top: 1px solid #D4D4D4; + } + } + + .modal-body > div > label { + padding-left: 10px; + padding-right: 24px; + } + + .modal-footer { + padding: 6px 0 0 0px; + text-align: right; + width: 100%; + + } + + .modal-footer > .lp-style-button { + margin-right: 22px; + } + + .modal-body > div > label { + font-size: 16px; + font-weight: bold; + line-height: 16px; + } + + .modal-body > div { + margin-top: 8px; + } + + .modal-body > div > span { + margin-left: 67px; + } + + .branch-name { + width: 300px; + height: 32px; + border: 2px solid #D4D4D4; + border-radius: 8px; + background-color: #FFFFFF; + margin-left: 8px; + padding: 8px; + } + +} + +.pick-version-modal .modal-header { + padding: 24px 24px 0 24px; +} + +.settingscontainer > .blocksmodal .modal-header { + padding: 24px 24px 12px 24px; +} + +.pick-version-modal, +.settingscontainer > .blocksmodal, +.cohortinfo > .switch-cohort-mode-modal { + max-width: 1200px; + width: 100%; + position: fixed; + top: 0; + right: 0; + left: 0; + margin: 0 auto; + z-index: 9999999; + box-shadow: 0 2px 8px 0 rgba(0,0,0,0.2); + background-color: $color-black-98-2; + + .actionbar { + left: 0; + bottom: 0; + width: 100%; + height: 80px; + padding: 16px; + background-color: white; + text-align: right; + border-top: 1px solid $color-black-90-2; + } + + .actionbar .lp-style-button { + margin-left: 16px; + } + + .actionbar label { + margin-left: 12px; + } + + .modal-header { + background-color: $color-black-98-2; + flex-wrap: wrap; + } + + .modal-header > h1 { + font-size: 24px; + font-weight: bold; + line-height: 24px; + width: 90%; + } + + .modal-header > .manual-warning { + width: 100%; + padding: 0.5rem 1rem; + margin: 0 0 1rem; + } + + .modal-header > div > svg { + color: $color-black-60; + cursor: pointer; + } + + .loading-spinner { + text-align: center; + padding: 2rem; + } + + .tablewrapper { + overflow-y: auto; + } + + .tablewrapper > .pick-version-table { + width: 100%; + } + + .tablewrapper > .pick-version-table .createdcolumn { + padding-left: 24px; + } + + .tablewrapper > .pick-version-table .checkboxcell { + width: 45px; + } + + .tablewrapper > .pick-version-table > thead { + background-color: #FFFFFF; + border-top: 1px solid $color-black-77; + border-bottom: 1px solid $color-black-77; + height: 40px; + + tr { + font-size: 14px; + line-height: 16px; + text-transform: uppercase; + color: #999999; + } + + tr > th { + font-weight: 500; + padding: 12px; + } + } + + .tablewrapper > .pick-version-table > tbody { + background-color: #FFFFFF; + + .releaserow { + border-bottom: 1px solid #e7e7e7; + + &.current-release { + background-color: rgba(0,165,181,0.05); + } + } + + .releaserow > td { + padding: 16px; + font-size: 16px; + vertical-align: top; + position: relative; + + &.createdcolumn { + padding-left: 24px; + } + } + + .releaserow > td > h5 { + font-weight: bold; + line-height: 16px; + font-size: 16px; + } + + .releaserow > td > p { + line-height: 16px; + margin-bottom: 8px; + } + + .releaserow > td > a.gitlink { + line-height: 16px; + font-weight: 300; + margin-right: 24px; + } + + .releaserow > td > a.gitlink > svg { + width: 16px; + vertical-align: bottom; + margin-left: 0.5rem; + } + + .releaserow > td > span.currentlabel { + line-height: 16px; + color: $color-cyan; + text-transform: uppercase; + font-size: 12px; + font-weight: bold; + letter-spacing: 1px; + text-align: center; + padding: 0 24px; + } + + .releaserow > td .cohorts-tooltip { + z-index: 99999999; + position: absolute; + top: 15%; + right: 80%; + display: flex; + flex-direction: column; + width: 220px; + border-radius: .5rem; + background-color: $color-black-10-2; + color: $color-white; + overflow: hidden; + box-shadow: 0 2px 8px 0 rgba(0,0,0,0.3); + } + + .releaserow > td .cohorts-tooltip > .body > div { + padding: 16px; + } + + .releaserow > td .cohorts-tooltip > .body > div > p { + margin: 0; + } + + .releaserow > td .cohorts-tooltip > .body > div > p > a { + text-decoration: none; + color: white; + } + + .releaserow > td .cohorts-tooltip > .body > div > p > a > svg { + height: 14px; + margin-left: 5px; + position: absolute; + margin-top: 5px; + } + } + + .loadingcontainer { + width: 100%; + height: 15rem; + display: flex; + align-items: center; + justify-content:center; + } + + .loadingcontainer:first-child { + position: relative; + right: 1rem; + } +} + +.course-scoring-info{ + & { + position: absolute; + top: -11px; + left: -38px; + display: flex; + } + + > .svg { + opacity: .6; + margin: .5rem; + cursor: help; + + &:hover + .infomodal { + display: block; + } + } + + > .infomodal { + font-size: 14px; + width: 310px; + margin: .5rem; + padding: 1rem 1.5rem; + border-radius: .5rem; + color: $color-white; + background-color: $color-black-10-2; + box-shadow: 0 2px 8px 0 rgba(0,0,0,0.3); + cursor: help; + display: none; + position: absolute; + top: 0; + right: 0; + z-index: 10; + + &:hover { + display: block; + } + } +} + +.cohortinfo > .switch-cohort-mode-modal { + position: fixed; + top: 0; + right: 0; + left: 0; + margin: 0 auto; + z-index: 1000000; + box-sizing: border-box; + width: 482px; + height: 165px; + background-color: #FFFFFF; + box-shadow: 0 2px 8px 0 rgba(0,0,0,0.15); + + .modal-header { + padding: 0px; + padding-top: 10px; + padding-left: 24px; + padding-right: 24px; + background-color: #FAFAFA; + } + + .modal-header > h1 { + font-size: 16px; + font-weight: bold; + line-height: 24px; + } + + .modal-header > div > svg { + color: $color-black-60; + cursor: pointer; + } + + .modal-body > div > label { + padding-left: 10px; + padding-right: 24px; + } + + .modal-footer { + padding: 6px 0 0 0px; + text-align: right; + width: 100%; + + } + + .modal-footer > .lp-style-button { + margin-right: 22px; + } + + .modal-body > div > label { + font-size: 16px; + font-weight: bold; + line-height: 16px; + } + + .modal-body > div { + margin-top: 8px; + } + + .modal-body > div > span { + margin-left: 67px; + } +} + +.api-documentation { + margin: 2rem auto; + max-width: 800px; +} + +.curriclum-visibility { + + .settingscontainer { + display: flex; + flex-flow: row wrap; + + .settingsrow { + margin-bottom: 2rem; + width: 100%; + } + } + + .visibility { + flex: 0 0 auto; + width: 40%; + margin-right: 1rem; + + .section { + border-bottom: 2px solid $color-black-92-2; + } + + .section-header { + padding: 1rem 0 1rem 2rem; + font-weight: bold; + background: $color-black-94-9; + border-top: 2px solid $color-black-92-2; + } + + .standard { + padding: 1rem 1rem 1rem 1.6rem; + font-weight: 400; + background: white; + border-top: 2px solid $color-black-92-2; + + > .header { + display: flex; + align-items: center; + &.hidden { color: #bfbfbf; } + } + + .chevron { + cursor: pointer; + flex: 1; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + + svg { + color: #ccc; + top: 7px; + position: relative; + margin-right: 0.2rem; + } + } + + .content-file-row { + cursor: pointer; + padding: 0.5rem 0 0.5rem 3rem; + display: flex; + justify-content: space-between; + &.hidden { color: #bfbfbf; } + &.previewed-contentfile { + background: $color-cyan; + color: white; + + svg { + color: white!important; + } + } + } + .content-file-row > p { + font-weight: 400; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + max-width: 450px; + margin-bottom: 0px; + + &.previewed-contentfile { + background: $color-cyan; + color: white; + + svg { + color: white; + } + } + } + .content-file-row > svg { + position: relative; + cursor: pointer; + } + } + } + + .lesson-preview { + padding: 2rem; + flex: 1 1 auto; + width: 55%; + background: $color-black-88-2; + margin-left: 1rem; + color: #ccc; + + &.has-content { + background: white; + overflow: scroll; + color: #333; + padding: 0; + + div[id^="challenge"] { + height: 200px; + margin-bottom: 2rem; + background: #ccc; + display: flex; + justify-content: center; + align-items: center; + } + } + } + + .lesson-preview h3.placeholder { + margin: 2rem 0; + width: 100%; + text-align: center; + } + + .status-header { + padding: 6px 3rem 0; + display: flex; + background: $color-black-88-2; + color: $color-black-60; + + svg { + color: $color-black-60; + fill: $color-black-60; + } + + .content-file-visibility { + text-transform: uppercase; + } + + .content-file-links { + margin-right: 0; + margin-left: auto; + + .github-icon { + height: 19px; + width: 19px; + position: relative; + top: -9px; + margin-left: 1rem; + } + } + } +} + +.cohortsetuptable { + font-size: 14px; + width: 100%; + + > .cohortsetupheader { + border-bottom: 2px solid $color-black-77; + color: $color-black-60; + text-transform: uppercase; + line-height: 40px; + } + + > .cohortsetupheader > tr > th { + font-weight: 500; + } + + > tbody > .cohortsetuprow > td { + line-height: 48px; + background-color: #FFFFFF; + border-bottom: 1px solid #E7E7E7; + font-size: 16px; + font-weight: 500; + } + + > tbody > .sectiontitleseparator > td { + line-height: 48px; + background-color: $color-black-94-9; + border-bottom: 1px solid #E7E7E7; + font-size: 16px; + font-weight: 600; + padding-left: 16px; + border-top: 2px solid $color-black-77; + } + + td.cohortusername, th.cohortusername { + padding-left: 20px; + } + + > tbody > .cohortsetuprow > td > select { + border: 1px solid #fff; + background-color: transparent; + margin-left: -8px; + } + + .sortcolumn { + padding-left: 24px; + width: 8em; + } + + .sortcolumn > a > svg { + margin-left: -4px; + margin-top: 16px; + cursor: pointer; + } + + > tbody > .cohortsetuprow > td > div > a > .versionlabel { + height: 24px; + background-color: rgba(74,144,226,0.1); + color: $color-sky-blue; + border-radius: 4px; + padding: 4px 10px; + cursor: pointer; + } + + > tbody > .cohortsetuprow > td > div > a > .versionlabel { + > .arrow-svg > svg { + vertical-align: text-bottom; + margin: 0px 8px; + } + } + + > tbody > .cohortsetuprow > td.action-cell { + position: relative; + } + + > tbody > .cohortsetuprow > td.action-cell > div { + display: inline-block; + } + + > tbody > .cohortsetuprow > td.action-cell .action-kebab { + float: right; + color: $color-black-46-7; + + &.active { + color: $color-cyan; + } + + &.disabled { + opacity: 0.25; + + svg { + cursor: not-allowed; + } + } + } + + > tbody > .cohortsetuprow > td.action-cell .user-action { + float: right; + } + > tbody > .cohortsetuprow > td.action-cell .user-action .action-kebab { + padding-top: 7px; + height: 40px; + } + > tbody > .cohortsetuprow > td.action-cell .user-action .action-menu { + margin: 18px 48px 0 0; + } + + > tbody > .cohortsetuprow > td.action-cell > a > svg { + margin: 12px 8px 0 0; + cursor: pointer; + } + + > tbody > .cohortsetuprow > td.updatecolumn > div > svg { + margin-top: 12px; + cursor: pointer; + } + + > tbody > .cohortsetuprow .error-sync { + color: $color-valencia; + } + + > tbody > .cohortsetuprow .error-sync > .errorsyncicon { + cursor:pointer; + } + + > tbody > .cohortsetuprow .error-sync > .errorsyncicon > svg { + vertical-align: middle; + margin-left: 8px; + } + + > tbody > .cohortsetuprow .error-sync > .errors-tooltip { + position: absolute; + display: flex; + flex-direction: column; + width: 400px; + border-radius: 0.5rem; + background-color: #FFFFFF; + color: $color-valencia; + overflow: hidden; + box-shadow: 0 2px 8px 0 rgba(0,0,0,0.3); + padding: 1rem; + margin-top: -8px; + + li { + line-height: 1.5rem; + } + } + + > tbody > .cohortsetuprow .error-sync > a { + //vertical-align: super; + margin-left: 8px; + text-decoration: underline; + color: $color-valencia; + } + + .blockcolumn { + padding-left: 1rem; + } + + > tbody > .cohortsetuprow > td.blockcolumn > svg { + vertical-align: text-bottom; + margin-right: 1rem; + } + + > tbody > .cohortsetuprow > td.blockcolumn > span.managereleases { + font-size: 10px; + background-color: #4e92df; + padding: 1px 8px; + border: none; + border-radius: 12px; + margin-left: 10px; + line-height: 16px; + text-transform: uppercase; + cursor: default; + height: 18px; + } +} diff --git a/scripts/app/assets/stylesheets/components/_content-file-show.scss b/scripts/app/assets/stylesheets/components/_content-file-show.scss new file mode 100644 index 0000000000000000000000000000000000000000..29a161e1c2f08807928bf54f81bb62769e240598 --- /dev/null +++ b/scripts/app/assets/stylesheets/components/_content-file-show.scss @@ -0,0 +1,342 @@ + +$sidebar-width: 260px; +$max-content-width: 1280px; +$max-text-content-width: 700px; + +.lesson-layout, #content-file-html { + & { + position: relative; + display: flex; + width: 100%; + height: auto; + min-height: calc(100vh - 104px); + } + + &.-collapsed { + .left-container { + width: 55px; + } + + .content-file-sidebar { + overflow-x: hidden; + width: 55px; + + .toggleaction { + transform: rotate(-90deg); + margin-right: 4px; + } + + .standardtitle, .lessoncel, .blockslink { + display: none; + } + + .standardscore { + margin-left: 0; + } + + .itemrow > svg { + margin-right: 0px; + } + } + } + + .left-container { + position: relative; + flex: 0 0 auto; + width: 0; + background: $color-white; + margin-left: 0; + + @media screen and (min-width: $tablet-breakpoint) { + width: $sidebar-width; + } + } + + .body-container { + flex: 1 1 auto; + display: flex; + flex-flow: column; + justify-content: space-between; + width: 100%; + background: white; + overflow-x: auto; + } + + .body-container .mdown-container { + width: 100%; + margin: 0 auto; + padding: 1rem 50px; + color: $color-tundora; + font-size: 19px; + font-weight: 300; + + @media screen and (min-width: $tablet-breakpoint) { + padding: 2rem 3rem; + } + + pre { + font-size: 14px; + } + + h1 { + @include font-h1; + } + + .checkpoint-info h1 { + margin-bottom: 50px; + margin-top: 30px; + } + + h2 { font-size: 28px; } + h3 { font-size: 22px; } + h4 { font-size: 20px; } + h5 { font-size: 18px; } + h6 { font-size: 18px; } + + h2, h3, h4, h5, h6 { + font-weight: 500; + letter-spacing: -0.3px; + line-height: 1.1; + // margin-bottom: -28px; + } + + h1, h2 { + margin-top: 66px; + } + + h3, h4, h5, h6 { + margin-top: 48px; + } + + h1:first-of-type { + margin-bottom: 60px; + } + + iframe { + display: block; + margin-left: auto; + margin-right: auto; + border: 1px solid #ddd; + margin-top: 42px; + margin-bottom: 6px; + } + + .fluidvids > iframe { + margin-top: 0px; + margin-bottom: 0px; + } + + .fluidvids { + margin-top: 42px; + margin-bottom: 6px; + } + + img { + margin-top: 6px; + margin-bottom: 6px; + } + + pre, table, blockquote, .challenge-block { + margin-top: 42px; + margin-bottom: 6px; + } + + p { + line-height: 1.7; + margin-top: 36px; + margin-bottom: 0px; + } + + blockquote { + font-style: italic; + border-left: 3px solid $color-tundora; + padding-left: 20px; + } + + b, strong { + font-weight: 500; + } + + ul, ol, li { + margin-top: 18px; + margin-bottom: 0px; + } + + h1 + p, h2 + p, h3 + p, h4 + p, h5 + p, h6 + p { + margin-top: 8px; + } + + table, + pre, code, + p, ul, ol, + blockquote, + h1,h2,h3,h4,h5,h6, + .challenge-block, + .checkbox-container, .checkpoint-landing, + .checkpoint-results { + max-width: $max-text-content-width; + margin-left: auto; + margin-right: auto; + } + } + + .body-container .pdf-viewer { + background: $color-black-98-2; + + .react-pdf__Page__canvas { + margin-bottom: 20px; + border: 1px solid #E3E3E3; + } + } + + .body-container .mdown-container img { + max-width: 100%; + } + + .body-container .checkpoint-spacer { + margin-bottom: 72px; + } + + .body-container .content-arrow-container { + display: flex; + flex-direction: column; + border-top: 1px solid $color-black-94-9; + } + + .body-container .content-arrow-container > .content-arrows { + display: flex; + flex-direction: row; + justify-content: space-between; + height: 4rem; + } + + .body-container .content-arrow-container > .content-arrows > .standard-arrow { + flex: 0 1 auto; + max-width: 50%; + padding: 0 1.5rem; + font-weight: 500; + text-decoration: none; + text-transform: uppercase; + + @media screen and (min-width: $tablet-breakpoint) { + padding: 0 3rem; + } + + &.-prev { + margin-right: auto; + } + + &.-next { + margin-left: auto; + } + + &.-prev span { + margin-left: 1rem; + } + + &.-next span { + margin-right: 1rem; + } + } + + .body-container .content-arrow-container > .content-arrows > .standard-arrow > div { + display: flex; + align-items: center; + justify-content: center; + height: 100%; + width: 100%; + color: $color-black-60; + + white-space: nowrap; + } + + .body-container .content-arrow-container > .content-arrows > .standard-arrow > div > span { + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + color: $color-cyan; + display: none; + @media screen and (min-width: $tablet-breakpoint) { + display: inline-block; + } + } + + .body-container .content-arrow-container > .content-arrows > .standard-arrow > div > svg { + flex: 0 0 24px; + } + +} + +.react-pdf__Page__canvas { + margin: 0 auto; +} + +.checkpoint-info { + $border-width: 10px; + + .checkpoint-info--header { + background: #666; + color: white; + font-size: 18px; + line-height: 36px; + margin: 0 auto; + max-width: $max-text-content-width; + padding: 2rem 4rem; + + border-top: none; + border-right: none; + border-bottom: none; + border-left: $border-width solid transparent; + + &.-state-complete-all-correct { border-left-color: $color-cyan; } + &.-state-complete-some-incorrect { border-left-color: $color-tangerine-orange; } + + &.-state-incomplete { border-left-color: $color-banana-yellow; } + &.-state-rejected { border-left-color: #E36875; } + &.-state-pending { border-left-color: #AAA; } + + &.-state-score-1 { border-left-color: $color-tangerine-orange; } + &.-state-score-2 { border-left-color: $color-banana-yellow; } + &.-state-score-3 { border-left-color: $color-lime-green; } + + > p:last-child { + margin-bottom: 0; + } + + > ul { + list-style-position: inside; + margin-bottom: 0; + padding-left: 4px; + } + + > .view-results { + background: white; + border-radius: 999px; + border: none; + color: #666666; + display: inline-block; + font-size: 90%; + font-weight: 500; + padding: 6px 35px 4px (35px - $border-width); + text-transform: uppercase; + } + } +} + +ul.task-list { + padding-left: 2em; + + ul { + margin-bottom: 0; + margin-top: 0 + } + + li.task-list-item { + list-style-type: none; + + input.md-task { + margin: 0 0.2em 0.25em -1.2em; + vertical-align: middle; + } + } +} diff --git a/scripts/app/assets/stylesheets/components/_content-file-sidebar.scss b/scripts/app/assets/stylesheets/components/_content-file-sidebar.scss new file mode 100644 index 0000000000000000000000000000000000000000..430833245dd39e464718f1b0e27c2e47dcdfe27b --- /dev/null +++ b/scripts/app/assets/stylesheets/components/_content-file-sidebar.scss @@ -0,0 +1,308 @@ +$sidebar-width: 260px; + +.navigation-move-right { + margin-left: auto; + margin-right: 0; +} + +.content-file-sidebar { + & { + position: fixed; + top: 0; + left: 0; + z-index: 99; + height: 100vh; + width: $sidebar-width; + max-width: calc(100vw - 40px); + transform: translateX(-100%); + transition: transform 200ms ease-out; + flex-shrink: 0; + background: #3D424A; + overflow-x: hidden; + transform: translateX(0); + + &.-is-sticky { + position: fixed; + } + } + + .lesson-layout { + @media screen and (min-width: $tablet-breakpoint) { + transform: translateX(0); + transform: translateX(-100%); + } + } + + .lesson-layout.-is-collapsed & { + transform: translateX(0); + @media screen and (min-width: $tablet-breakpoint) { + transform: translateX(-100%); + } + } + + > .actionsrow { + flex: 0 0 auto; + background: #343940; + padding: 1rem 0.5rem; + display: flex; + flex-flow: row nowrap; + align-items: center; + height: 60px; + border-bottom: 1px solid rgba(255, 255, 255, 0.1); + + .blockslink { + margin-left: 5px; + font-size: 0px; + + svg { + fill: rgba(255, 255, 255, 0.6); + } + } + + .toggleaction { + margin-left: auto; + margin-right: 15px; + transform: rotate(90deg); + cursor: pointer; + + @media screen and (max-width: $tablet-breakpoint) { + display: none; + } + + svg { + fill: rgba(255, 255, 255, 0.6); + } + } + } + + > .standardrow { + flex: 0 0 auto; + background: #343940; + padding: 1rem 1.25rem 1rem 1rem; + display: flex; + flex-flow: row nowrap; + align-items: center; + + &.-status-in-progress.-mode-percentage { + padding-bottom: 0.3rem + } + + > .standardtitle { + font-size: 14px; + font-weight: bold; + color: rgba(255,255,255,0.55); + line-height: 24px; + flex: 1 1 auto; + max-width: 208px; + } + + > .actioncircle { + --actioncircle-size: 24px; + } + > .actioncircle > .svg { + color: $color-cyan; + line-height: 1; + } + > .actioncircle.-awaiting-grade > .svg { + color: #A3A7AE; + line-height: 1; + } + } + + .standardprogressbar { + background: #343940; + padding: 0 1.25rem 1rem 1rem; + > .progressbar { --progressbar-height: 3px } + } + + > .bodyrow { + flex: 1 1 auto; + padding-bottom: 7.25rem; + background: #3D424A; + overflow-y: auto; // Always show scrollbar to avoid alignment issues + scrollbar-width: none; + + &::-webkit-scrollbar { + width: 4px; + } + /* Track */ + &::-webkit-scrollbar-track { + background: $color-medium-blue-gray; + } + /* Handle */ + &::-webkit-scrollbar-thumb { + background: #3D424A; + } + /* Handle on hover */ + &::-webkit-scrollbar-thumb:hover { + background: #3D424A; + } + } + + > .bodyrow > a { + display: block; + color: #3A4551; + text-decoration: none; + } + + > .bodyrow > a.checkpoint svg { + + } + + > .bodyrow > a > .itemrow.-is-active, + > .bodyrow > a:visited > .itemrow.-is-active { + background-color: #3A6371; + + > .iconcel { + color: $color-cyan; + opacity: 1; + } + + > .lessoncel > .lessontitle { + color: rgba(255,255,255,1); + } + } + + .itemrow { + display: flex; + flex-flow: row nowrap; + align-items: center; + height: 60px; + width: 100%; + color: #A3A7AE; + background: #3D424A; + + &:not(.-is-active) { + border-bottom: 1px solid #3b3b3b; + } + } + + .itemrow > .iconcel { + visibility: hidden; + flex: 0 0 auto; + height: 18px; + width: 0; + margin-left: 16px; + margin-right: 0; + opacity: 0.5; + color: $color-black; + } + + .itemrow > .iconcel .svg { + height: 18px; + width: 18px; + color: rgba(255,255,255,0.9); + } + + .itemrow > .lessoncel { + flex: 1 1 auto; + margin-right: 16px; + overflow: hidden; + white-space: nowrap; + } + + .itemrow > .lessoncel > .lessontitle { + color: rgba(255,255,255,0.6); + text-overflow: ellipsis; + overflow: hidden; + font-size: 16px; + line-height: 16px; + } + + .itemrow > svg, .itemrow img { + margin-right: 16px; + flex-shrink: 0; + } + + .itemrow .cant-complete-icon { + position: relative; + + .infomodal { + font-size: 14px; + max-width: 360px; + margin: .5rem; + padding: .5rem 1rem; + border-radius: .5rem; + color: $color-white; + background-color: $color-black-10-2; + box-shadow: 0 2px 8px 0 rgba(0, 0, 0, 0.3); + cursor: help; + display: none; + position: absolute; + top: -13px; + right: 100%; + z-index: 10; + width: 174px; + } + + &:hover { + .infomodal { + display: block; + } + } + } + + .sidebar-checkpoint-percentage { + margin: 0; + font-weight: 500; + padding-right: 5%; + color: white; + } + + .checkpoint-complete-sidebar-title { + height: 60px; + display: flex; + justify-content: space-between; + align-items: center; + color: rgba(255, 255, 255, 0.6); + font-weight: 500; + padding-left: 16px; + padding-right: 16px; + background-color: #343940; + + p { + margin-bottom: 0; + } + } +} + + +.showhidecircle { + position: fixed; + z-index: 999999; + bottom: 3.5rem; + left: 0; + transform: translateX(-.5rem); + height: 3rem; + width: 3rem; + background: $color-white; + border-radius: 100%; + box-shadow: 0 2px 6px 0 rgba(0, 0, 0, 0.5); + user-select: none; + cursor: pointer; + + @media screen and (min-width: $tablet-breakpoint) { + height: 2.5rem; + width: 2.5rem; + position: absolute; + top: 1.5rem; + bottom: auto; + transform: translateX( calc(280px - 50%) ); + + .-is-sticky + & { + position: fixed; + transform: translateX(calc(280px - 50%)); + } + .-is-collapsed & { + transform: translateX( 1rem ); + } + } + + > div { + display: flex; + justify-content: center; + align-items: center; + height: inherit; + width: inherit; + } +} diff --git a/scripts/app/assets/stylesheets/components/_curriculum-checkpoint-summary.scss b/scripts/app/assets/stylesheets/components/_curriculum-checkpoint-summary.scss new file mode 100644 index 0000000000000000000000000000000000000000..aab88a183200209f9e8a365ba504e1da7b3534f3 --- /dev/null +++ b/scripts/app/assets/stylesheets/components/_curriculum-checkpoint-summary.scss @@ -0,0 +1,46 @@ +.checkpoint-summary-wrapper { + display: flex; + flex-flow: column; + align-items: center; + justify-content: space-between; + margin-right: 1.5rem; + height: 100%; +} + +.checkpoint-summary { + display: flex; + flex-flow: row nowrap; + align-items: center; + line-height: 1rem; + + .avg { + color: $color-cyan; + font-size: 2.75rem; + position: relative; + line-height: 32px; + } + + .indicators { + display: flex; + flex-flow: column nowrap; + margin-left: 1rem; + height: 41px; + + .indicator { + width: 25px; + height: 5px; + margin-top: 3px; + border-radius: 8px; + flex: 0 0 auto; + background-color: $color-black-84-7; + + &:first-of-type { + margin-top: 0px; + } + + &.fill { + background-color: $color-cyan; + } + } + } +} diff --git a/scripts/app/assets/stylesheets/components/_curriculum-progress.scss b/scripts/app/assets/stylesheets/components/_curriculum-progress.scss new file mode 100644 index 0000000000000000000000000000000000000000..6bf94b5cbc37207d244be0808087fde5d422d935 --- /dev/null +++ b/scripts/app/assets/stylesheets/components/_curriculum-progress.scss @@ -0,0 +1,239 @@ +.curriculumprogress { + & { + display: flex; + flex-flow: column; + align-items: center; + justify-content: space-between; + margin: 64px auto 0px auto; + height: auto; + + @media screen and (min-width: $tablet-breakpoint) { + flex-flow: row nowrap; + padding: 0 4rem; + height: 48px; + } + } + + .progress-indicators { + display: flex; + flex-flow: row nowrap; + align-items: center; + justify-content: space-between; + } + + .curriculumprogressheader { + line-height: 2.5rem; + font-size: 40px; + font-weight: 300; + margin-bottom: 1rem; + letter-spacing: -1px; + + @media screen and (min-width: $tablet-breakpoint) { + font-size: 40px; + margin-bottom: 0; + } + } + + .masterywrapper { + display: flex; + flex-flow: row nowrap; + align-items: center; + + &.-percentage { + flex-flow: column nowrap; + justify-content: space-between; + height: 100%; + + .progressdonut { + width: auto; + height: auto; + } + } + } + + .masterywrapper .masteryaverage { + flex: 0 0 auto; + text-align: center; + } + + .masterywrapper .masteryaverage .masteryaveragevalue { + height: 24px; + color: $black; + font-size: 24px; + font-weight: 500; + line-height: 24px; + } + + .masterywrapper .masteryaverage .masteryaveragelabel { + height: 24px; + color: $color-black-50-2; + font-size: 16px; + font-weight: 500; + } + + .masterywrapper .progressdonut { + position: relative; + flex: 0 0 auto; + height: 96px; + width: 96px; + } + + .masterywrapper .progressdonut.-percentage { + display: flex; + flex-flow: row nowrap; + align-items: center; + justify-content: space-between; + + .donut { + width: 60px; + } + + .donutscore { + position: relative; + color: $color-cyan; + font-size: 2.75rem; + } + } + + .masterywrapper .progressdonut .donutscore { + position: absolute; + width: 100%; + height: 100%; + display: flex; + align-items: center; + justify-content: center; + color: $color-lime-green; + font-size: 32px; + line-height: 32px; + + &.-percentage { + color: $color-cyan; + } + } + + .masterywrapper .progressdonut .donutscore .percent { + font-size: 18px; + font-weight: bold; + line-height: 16px; + margin-top: -8px; + } + + .masterywrapper .progressdonut .donut { + width: 100%; + height: 100%; + transform: rotate(90deg); + } + + .masterywrapper .progressdonut .donut .donutring { + stroke: $color-black-84-7; + } + + .masterywrapper .progressdonut .donut .donutsegment { + stroke-linecap: round; + &.-score-3 { + stroke: $color-lime-green; + } + + &.-score-2 { + stroke: $color-banana-yellow; + } + + &.-score-1 { + stroke: $color-tangerine-orange; + } + + &.-completed { + stroke: $color-cyan; + } + + &.-pending { + stroke: $color-black-50-2; + } + } + + .masterywrapper .progresspercentages { + flex: 0 0 auto; + flex-flow: row nowrap; + } + + .masterywrapper .progresspercentages .progresspercentage { + display: flex; + flex-flow: row nowrap; + align-items: center; + flex: 0 0 auto; + } + + .masterywrapper .progresspercentages .progresspercentage .standardbean { + margin-right: 0.75rem; + } + + .masterywrapper .progresspercentages .progresspercentage { + height: 16px; + color: $color-black-20; + font-size: 16px; + font-weight: 500; + line-height: 16px; + margin-bottom: 8px; + } + .masterywrapper .progresspercentages .progresspercentage.percentunscored { + margin-bottom: 0; + } + + .progress-indicators { + position: relative; + height: 60px; + + .infomodal { + font-size: 14px; + padding: .5rem 1rem; + border-radius: .5rem; + color: $color-white; + background-color: $color-black-10-2; + box-shadow: 0 2px 8px 0 rgba(0,0,0,0.3); + cursor: help; + display: none; + position: absolute; + margin-top: -5px; + margin-right: 1rem; + right: 100%; + z-index: 10; + width: 360px; + } + + &:hover { + .infomodal { + display: block; + } + } + } +} + +.curriculum-last-viewed { + width: 100%; + display: flex; + flex-flow: row; + align-items: center; + justify-content: space-between; + padding: 0 4rem; + margin-top: 12px; + + .last-viewed-content { + font-weight: 400; + color: $color-dove-gray; + font-size: 16px; + + a { + color: $color-primary-teal; + } + } +} + +.curriculum-subset-notice.flash-message { + display: block; + position: fixed; top: 0; right: 0; left: 0; + text-align: center; + z-index: $z-primary-navigation + 1; +} +body.-with-curriculum-subset-notice { + padding-top: 50px; +} diff --git a/scripts/app/assets/stylesheets/components/_curriculum.scss b/scripts/app/assets/stylesheets/components/_curriculum.scss new file mode 100644 index 0000000000000000000000000000000000000000..b43e20b277ed1aca26388a3dab27aaa7bade860b --- /dev/null +++ b/scripts/app/assets/stylesheets/components/_curriculum.scss @@ -0,0 +1,277 @@ +.cohort-curriculum { + > .no-curriculum { + padding: 2.5rem; + } + + > .block-container { + padding: 0; + border-bottom: 6px solid #EAEAEA; + + &:last-of-type { + border-bottom: 0px solid transparent; + } + @media screen and (min-width: 800px) { + padding: 0 2.5rem 0; + } + + &.block-hidden { + .block-info { + margin-bottom: 20px; + } + + .standards-container { + display: none; + } + } + } + + > .block-container > .standards-container { + display: flex; + flex-flow: row wrap; + padding: 16px; + margin-bottom: 26px; + } +} + +.slideshow { + display: flex; + justify-content: center; + padding-top: 25px; +} + +.standardsprogress { + display: flex; + flex-flow: row nowrap; + align-items: center; + margin: 0 0 0 12px; +} + +.standardsprogress > .standardscount { + margin-left: 8px; + + opacity: 0.5; + color: #000000; + + font-size: 14px; + font-weight: 500; + line-height: 16px; + + margin-bottom: -2px; +} + +.block-info { + & { + display: flex; + flex-flow: column; + justify-content: center; + margin: 80px 24px 20px; + } + + > .blocktitle { + line-height: 24px; + font-weight: 500; + font-size: 24px; + display: flex; + align-items: center; + + > span { + margin-left: -5px; + margin-right: 12px; + } + } + + > .blocktitle .svg { + position: relative; + top: 2px; + color: #757575; + cursor: pointer; + } +} + +.non-sidebar-standard-complete { + position: relative; + margin-left: 220px; + margin-bottom: 256px; + + .standard-complete { + width: 21px; + } +} + +.standard-card, a.standard-card{ + .checkpoint-data { + position: absolute; + display: flex; + margin-top: 7.5%; + + .donut { + height: 20px; + width: 20px; + margin-top: 2px; + } + + .checkpoint-complete { + margin-top: 2px; + height: 20px; + } + + .grade-percent { + color: $color-cyan; + font-weight: 600; + padding-left: 5px; + } + } + + & { + position: relative; + display: flex; + flex-flow: column; + flex: 1 1 100%; + height: 180px; + max-width: 274px; + min-width: 274px; + margin: 1rem 0.5rem; + background-color: $color-white; + box-shadow: 0 1px 10px 0 rgba(0,0,0,0.06); + border: 1px solid #E3E3E3; + text-decoration: none; + color: inherit; + border-radius: 4px; + } + + &.-placeholder { + h5, p { + background-color: #333333; + border-radius: 3px; + opacity: 0.1; + } + h5 { width: 50%; } + .action p { + background-color: transparent; + opacity: 1; + } + } + + @media screen and (min-width: 800px) { + flex: 0 0 calc((100% / 4) - 1rem); + max-width: 288px; + } + + &.-completed-standard { + background-color: #f4f4f4; + border: 1px solid #E3E3E3; + box-shadow: none; + + h5, p { + color: #808080; + } + } + + &:hover { + &.-score-3 { + box-shadow: 0 1px 4px 0 rgba(0, 0, 0, 0.2), 0 0 0 1px $color-lime-green; + } + + &.-score-2 { + box-shadow: 0 1px 4px 0 rgba(0, 0, 0, 0.2), 0 0 0 1px $color-banana-yellow; + } + + &.-score-1 { + box-shadow: 0 1px 4px 0 rgba(0, 0, 0, 0.2), 0 0 0 1px $color-tangerine-orange; + } + } + + &.-score-3 > .action .percentage-badge-container > .percentage-badge.actioncircle { + background-color: $color-lime-green; + } + + &.-score-2 > .action .percentage-badge-container > .percentage-badge.actioncircle { + background-color: $color-banana-yellow; + } + + &.-score-1 > .action .percentage-badge-container > .percentage-badge.actioncircle { + background-color: $color-tangerine-orange; + } + + > h5 { + font-size: 18px; + font-weight: 500; + line-height: 26px; + margin: 24px 24px 4px; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + color: $color-black-20; + } + + > p { + font-size: 14px; + line-height: 19px; + margin: 0px 24px 32px; + overflow: hidden; + display: -webkit-box; + -webkit-line-clamp: 3; + -webkit-box-orient: vertical; + color: $color-black-20; + } + + > .action { + font-size: 12px; + font-weight: 600; + line-height: 16px; + height: 40px; + padding: 0 24px; + display: flex; + position: relative; + flex-flow: row nowrap; + align-items: center; + margin-top: auto; + + &.-is-visited { + color: $color-black-20; + } + + .actioncircle { + position: absolute; right: 16px; bottom: 22px; + } + + p { + font-size: 12px; + font-weight: 400; + color: $color-black-50-2; + text-align: right; + margin-top: 4px; + } + + .percentage-badge-container { + p.pending-score { + margin-left: 45px; + text-align: left; + margin-top: -20px; + } + + .percentage-badge.actioncircle { + right: auto; + left: 19px; + background-color: $color-cyan; + + &.-awaiting-grade { + background-color: transparent; + color: $color-black-50-2; + } + } + } + + .full-width { + width: 100%; + } + } + + > span svg { + height: 16px; + width: 16px; + display: inline-block; + margin-right: 6px; + } + +} diff --git a/scripts/app/assets/stylesheets/components/_dropdown-component.scss b/scripts/app/assets/stylesheets/components/_dropdown-component.scss new file mode 100644 index 0000000000000000000000000000000000000000..863abf559c9608523dc55dbd767740f9246d3a77 --- /dev/null +++ b/scripts/app/assets/stylesheets/components/_dropdown-component.scss @@ -0,0 +1,79 @@ +%dropdown-select { + -webkit-appearance: none; + -moz-appearance: none; + appearance: none; + height: 100%; + min-width: 100%; + margin: 0; + padding: 0 40px 0 16px; + border: 0; + font-size: inherit; + color: inherit; + cursor: inherit; + display: flex; + flex-flow: row; + align-items: center; +} + +.dropdown-component { + & { + display: inline-block; + position: relative; + height: 48px; + border: 1px solid $color-black-92-2; + border-radius: 4px; + background-color: $color-white; + box-shadow: 0 1px 2px 0 rgba(155, 155, 155, 0.2); + color: rgba($color-darkest-blue-gray, 0.8); + cursor: pointer; + user-select: none; + width: auto; + min-width: 200px; + max-width: 400px; + margin-bottom: 32px; + font-size: 18px; + margin-right: 1rem; + } + + &:hover { + color: $color-darkest-blue-gray; + + > .svg { + fill: $color-cyan; + } + } + + > select { + @extend %dropdown-select; + } + + > .svg { + pointer-events: none; + position: absolute; + top: 0; + bottom: 0; + left: auto; + right: 8px; + margin: auto; + fill: $color-black-83-1; + } +} + +.faux-dropdown { + @extend .dropdown-component; + background-color: white; + + > .fauxselect { + @extend %dropdown-select; + } + + &.-is-pressed { + color: $color-dark-blue-gray; + background: $color-black-94-9; + box-shadow: none; + } + + &:hover > .svg { + fill: $color-black-83-1; + } +} diff --git a/scripts/app/assets/stylesheets/components/_external-link.scss b/scripts/app/assets/stylesheets/components/_external-link.scss new file mode 100644 index 0000000000000000000000000000000000000000..cc24d52b1782804eb1558819fdfaf14dc5770964 --- /dev/null +++ b/scripts/app/assets/stylesheets/components/_external-link.scss @@ -0,0 +1,13 @@ +a[target="_blank"].external-link { + > svg.svg { + display: inline; + height: 12px; + width: 12px; + margin-left: 4px; + vertical-align: middle; + } + + &.view-in-github { + font-size: 11px; + } +} diff --git a/scripts/app/assets/stylesheets/components/_flash-message.scss b/scripts/app/assets/stylesheets/components/_flash-message.scss new file mode 100644 index 0000000000000000000000000000000000000000..b63f831a187ae8b072921a65685ab0fd0f837db8 --- /dev/null +++ b/scripts/app/assets/stylesheets/components/_flash-message.scss @@ -0,0 +1,21 @@ +.flash-message { + color: rgba(0, 0, 0, 0.6); + display: flex; + justify-content: space-between; + margin: 0; + border-radius: 0; + line-height: 24px; + font-size: 16px; + font-weight: 400; + strong { + font-weight: 500; + } + + .flash-closebtn { + height: 24px; + } + .flash-closebtn > svg { + color: rgb(153, 153, 153); + cursor: pointer; + } +} \ No newline at end of file diff --git a/scripts/app/assets/stylesheets/components/_footer.scss b/scripts/app/assets/stylesheets/components/_footer.scss new file mode 100644 index 0000000000000000000000000000000000000000..6ca24843638eee63a4e7294fc0bbbde50cf366ba --- /dev/null +++ b/scripts/app/assets/stylesheets/components/_footer.scss @@ -0,0 +1,66 @@ +body > footer { + & { + display: flex; + flex-flow: column-reverse; + width: 100%; + min-height: 3.5rem; + padding: 0.5rem 2rem; + border-top: 1px solid $color-black-90-2; + font-size: 12px; + text-align: center; + + @media screen and (min-width: $tablet-breakpoint) { + flex-direction: row; + align-items: center; + padding: 0 2rem; + } + } + + > ul { + list-style: none; + padding: 0; + margin: 0 0 0 auto; + text-transform: uppercase; + + @media screen and (min-width: $tablet-breakpoint) { + display: flex; + flex-flow: row nowrap; + } + } + + > ul > li:not(:last-child) { + @media screen and (min-width: $tablet-breakpoint) { + margin-right: 1rem; + } + } + + > ul > li > a { + font-weight: 400; + line-height: 1.5rem; + color: $color-cyan; + text-decoration: none; + + &:hover { + color: $color-navy-blue; + } + } + + > span { + font-weight: 400; + line-height: 1.5rem; + } + + > .logo { + background: none left center no-repeat; + background-image: image-url("svg/mobile-logo.svg"); + background-size: contain; + height: 34px; + margin: 0 1rem 0 0; + min-width: 40px; + @media screen and (min-width: $tablet-breakpoint) { + background-image: image-url("svg/g-learn-lockup.svg"); + margin: 0 1.5rem 0 0; + min-width: 150px; + } + } +} diff --git a/scripts/app/assets/stylesheets/components/_grade-buttons.scss b/scripts/app/assets/stylesheets/components/_grade-buttons.scss new file mode 100644 index 0000000000000000000000000000000000000000..12c3318ec99f7ec64c4b1db68730cf676fe7818b --- /dev/null +++ b/scripts/app/assets/stylesheets/components/_grade-buttons.scss @@ -0,0 +1,12 @@ +.grade-buttons { + & { + right: 0; + position: absolute; + margin-right: 32px; + } + + form { + display: inline-block; + margin-left: 8px; + } +} diff --git a/scripts/app/assets/stylesheets/components/_integer_picker.scss b/scripts/app/assets/stylesheets/components/_integer_picker.scss new file mode 100644 index 0000000000000000000000000000000000000000..6af8843abc1bd3780909da14c1f8fd6d9625ac37 --- /dev/null +++ b/scripts/app/assets/stylesheets/components/_integer_picker.scss @@ -0,0 +1,57 @@ +.integer-container { + display: flex; + border-radius: 25px; + border: 2px solid $color-black-93-3; + background-color: $color-white; + height: 45px; + justify-content: center; + align-items: center; + padding: 0 16px 0 16px; + + &.vertical { + flex-direction: column; + height: auto; + position: absolute; + top: 33px; + right: 23px; + border-radius: 10px; + z-index: 1; + width: 70px; + padding: 0; + box-shadow: 0 0 8px 0 rgba(0, 0, 0, 0.1); + + .integer { + padding: 5px 0 5px 0; + width: 100%; + } + + .integer:first-child { + border-radius: 8px 8px 0 0; + } + + .integer:last-child { + border-radius: 0 0 8px 8px; + } + + .integer:not(:last-child) { + border-bottom: 1px solid $color-black-93-3; + } + } + + .integer { + color: $color-black-60-8; + text-align: center; + width: 25px; + font-size: 1.2rem; + font-weight: 500; + cursor: pointer; + height: 100%; + display: flex; + justify-content: center; + align-items: center; + + &:hover { + background-color: $color-cyan-very-light; + } + } +} diff --git a/scripts/app/assets/stylesheets/components/_lp-style-button.scss b/scripts/app/assets/stylesheets/components/_lp-style-button.scss new file mode 100644 index 0000000000000000000000000000000000000000..ae02900f909ee1b8720541e03b74c911158f24c7 --- /dev/null +++ b/scripts/app/assets/stylesheets/components/_lp-style-button.scss @@ -0,0 +1,127 @@ +%submit-button { + $color-em: 8; + $color-height: ($color-em * 5 + px); + + & { + display: inline-block; + height: $color-height; + width: auto; + min-width: 120px; + padding: 0 ($color-em * 3 + px); + border: none; + border-radius: 100px; + background-color: $color-cyan; + cursor: pointer; + line-height: $color-height; + color: white; + font-size: ($color-em * 1.5 + px); + font-weight: bold; + letter-spacing: 1px; + text-align: center; + text-transform: uppercase; + vertical-align: middle; + + } + + &:focus { + outline: 0; + } + + &:hover { + color: white; + text-decoration: none; + } + + &.-cancel { + background: none; + border: 1px solid rgba(220, 53, 73, 1); + color: rgba(220, 53, 73, 1); + } + + &.-cancel:hover { + background: rgba(220, 53, 73, 1); + border: 1px solid rgba(220, 53, 73, 1); + color: white; + } + + &.-disabled { + opacity: 0.3; + cursor: not-allowed; + } + + &.-outline { + background: none; + border: 1px solid $color-cyan; + color: $color-cyan; + } + + &.-outline:hover { + background: $color-cyan; + border: 1px solid $color-cyan; + color: white; + } + + &.-reject { + background: $color-valencia; + border: 1px solid $color-valencia; + color: $color-white; + } + + &.-white { + background: $color-white; + color: $color-cyan; + + &:hover { + background-color: rgba(255, 255, 255, 0.7); + } + } + + &.-white-grey-border { + background: $color-white; + color: $color-cyan; + font-weight: bold; + border: 1px solid $color-black-90-2; + height: auto; + padding: 2px 18px; + min-width: 80px; + line-height: 24px; + } + + &.-white-alt { + background: $color-white; + color: $color-darkest-blue-gray; + } + + &.-white-outline { + background: none; + border: 1px solid white; + color: white; + } + + &.-red-outline { + background: none; + border: 1px solid red; + color: red; + } + + &.-nowrap { + white-space: nowrap; + } + + &.-small { + font-weight: 200; + height: auto; + padding: 2px; + min-width: 80px; + line-height: 24px; + color: white; + } + + &.max-attempts { + line-height: 1; + } +} + +.lp-style-button { + @extend %submit-button; +} diff --git a/scripts/app/assets/stylesheets/components/_mastery-table.scss b/scripts/app/assets/stylesheets/components/_mastery-table.scss new file mode 100644 index 0000000000000000000000000000000000000000..d90fcde4752745da8d7a56dd5fba9260436f146b --- /dev/null +++ b/scripts/app/assets/stylesheets/components/_mastery-table.scss @@ -0,0 +1,382 @@ +.masterytable { + overflow-x: scroll; +} + +.table-performance-grid { + & { + width: 100%; + margin: 0 auto; + background: $color-white; + margin-top: 50px; + } + + th.student-label { + width: 30px; + + &:last-child { + padding-right: 15px; + } + } + + th.vertical-text { + height: 100px; + white-space: nowrap; + font-weight: normal; + } + + th.vertical-text > div { + width: 30px; + transform: translate(19px, 35px) rotate(315deg); + } + + th.vertical-text > div > span { + border-bottom: 1px solid $color-black-80; + } + + th.vertical-text > div > span.selected { + font-weight: bold; + background: yellow; + } + + .tr-summary { + background: $color-light-gray-blue; + } + + .tr-standard > th { + font-weight: normal; + } + + tbody th { + padding: 0 3px; + } + + tbody th, + tbody td { + border: 1px solid $color-black-80; + } + + tbody td.score { + text-align: center; + } + + tbody td.score-0 { + background: white; + } + + tbody td.score-1 { + background: $color-soft-red; + } + + tbody td.score-2 { + background: $color-soft-orange; + } + + tbody td.score-3 { + background: $color-sage; + } + + tbody td.selected { + box-shadow: 0 0 0 2px black inset; + font-weight: bold; + } + + .clear-filters { + position: relative; + } + + .standard-legend { + position: absolute; + bottom: 3rem; + border: 1px solid $color-gray-light; + color: gray; + border-radius: 0.25rem; + padding: 0.5rem 2rem; + margin-right: 0.5rem; + margin-top: 0.5rem; + + &.elective { + left: 80px; + background-color: $color-pale-yellow; + } + } +} + +// print overwrites? +@media print { + table.table-performance-grid { + td { + &.score-selected { + box-shadow: 0 0 0 2px black inset; + } + + &.score-1, + &.score-2, + &.score-3 { + color: $color-black-80 !important; + } + + &.score-0 { + color: white !important; + } + } + } +} + +// used in: +// app/views/instructor/performances/index.html.haml +// app/views/instructor/performances/active_performances.html.haml +.workbook-performance-table { + .student-header { + position: absolute; + width: 100%; + left: 0; + height: 200px; + background: -moz-linear-gradient(top, rgba(0, 0, 0, 0.65) 0%, rgba(0, 0, 0, 0) 100%); + background: -webkit-linear-gradient(top, rgba(0, 0, 0, 0.65) 0%, rgba(0, 0, 0, 0) 100%); + background: linear-gradient(to bottom, rgba(0, 0, 0, 0.65) 0%, rgba(0, 0, 0, 0) 100%); + filter: progid:DXImageTransform.Microsoft.gradient(startColorstr="#a6000000", endColorstr="#00000000", GradientType=0); + } + + .student-selected-view { + &.card { + background: rgb(125, 125, 125); + vertical-align: top; + overflow: hidden; + position: fixed; + height: 120px; + width: 300px; + z-index: 2; + + .prop { + text-shadow: 1px 1px 0.5px rgba(65, 65, 65, 0.3); + padding-left: 15px; + display: block; + width: 175px; + float: left; + color: $color-white; + + &.avatar { + height: 119px; + width: 123px; + object-position: center; + object-fit: cover; + } + + &.first-name { + margin: 15px 0 0; + font-weight: 600; + font-size: 20px; + } + + &.last-name { + margin: 2px 0 0; + font-weight: 100; + font-size: 16px; + } + + &.student-performance-gauge { + margin: 20px 0 0; + font-size: 15px; + color: white; + bottom: 10px; + } + } + } + } + + .student-map-wrapper { + background: rgb(255, 255, 255); + position: fixed; + overflow: auto; + height: 120px; + right: 15px; + left: 315px; + z-index: 1; + + .card { + font-size: 0; + padding: 0; + margin: 0; + + li { + box-shadow: inset 0 0 1px rgba(119, 119, 119, 0.75); + transition: ease-in 0.33s; + display: inline-block; + background: white; + width: 20%; + margin: 0; + + .prop { + width: calc(100% - 45px); + padding-left: 5px; + float: left; + + &.avatar { + padding-left: 0; + height: 40px; + width: 40px; + object-position: center; + object-fit: cover; + } + + &.first-name { + padding-top: 4px; + font-size: 16px; + font-weight: 300; + } + + &.last-name { + font-size: 11px; + font-weight: 600; + } + } + + &.active { + box-shadow: inset 0 0 1px $black; + background: $color-galvanize-orange; + } + } + } + } + + .performance-spinner { + position: relative; + text-align: center; + margin: 33% auto; + width: 100%; + } + + .table-performance-grid { + position: relative; + width: 100%; + top: 135px; + + .clear-filters { + vertical-align: bottom; + } + + th.current-name { + vertical-align: bottom; + padding-bottom: 1em; + } + + th.vertical-text { + height: 180px; + white-space: nowrap; + font-weight: normal; + } + + th.vertical-text > div { + transform: translate(19px, 75px) rotate(315deg); + width: 30px; + // hide + display: none; + } + + th.vertical-text > div > span { + border-bottom: 1px solid $color-black-80; + } + + .selected { + background: yellow; + font-weight: bold; + } + + tbody { + tr.tr-summary { + th, + td { + background: $color-black-80; + padding: 10px 0; + + a { + color: $color-black-20; + font-size: 15px; + } + } + } + + th { + padding: 0 3px; + } + + th, + td { + border: 1px solid $color-black-80; + } + + td { + text-align: center; + padding: 8px; + + &.score-0 { + background: white; + } + + &.score-1 { + background: $color-soft-red; + } + + &.score-2 { + background: $color-soft-orange; + } + + &.score-3 { + background: $color-sage; + } + + &.selected { + box-shadow: 0 0 0 2px black inset; + font-weight: bold; + border-bottom: 1px; + border-right: 1px; + } + } + + .tr-success-criterion > th { + font-weight: normal; + max-width: 300px; + background: white; + } + } + } + + table.table { + tr.tr-summary, + tr.tr-standard { + th, + td { + background: $color-light-gray-blue; + } + } + + td { + &.score-selected { + box-shadow: 0 0 0 2px black inset; + } + + &.score-0 { + text-align: center; + vertical-align: middle; + } + + &.score-1 { + background: $color-soft-red; + text-align: center; + vertical-align: middle; + } + + &.score-2 { + background: $color-soft-orange; + text-align: center; + vertical-align: middle; + } + + &.score-3 { + background: $color-sage; + text-align: center; + vertical-align: middle; + } + } + } +} diff --git a/scripts/app/assets/stylesheets/components/_modal.scss b/scripts/app/assets/stylesheets/components/_modal.scss new file mode 100644 index 0000000000000000000000000000000000000000..91a7560a582f55bb2e25883fca2f2d595126b4bc --- /dev/null +++ b/scripts/app/assets/stylesheets/components/_modal.scss @@ -0,0 +1,195 @@ +.modal-container { + position: fixed; + top: 0; + left: 0; + width: 100%; + height: 100%; + background-color: rgba(0,0,0,0.6); + z-index: 1000000; +} + +.modal { + position: fixed; + top: 0; + left: 0; +} + +.modal-main, .mdown-container .modal-container .modal-main { + color: $color-darker-gray-blue; + position: relative; + background: white; + height: auto; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + width: 730px; + border-radius: 5px; + box-shadow: 0 2px 20px 0 rgba(0,0,0,0.07); + overflow: hidden; + + .close-button-wrapper { + display: flex; + justify-content: flex-end; + width: 100%; + padding-top: 5px; + padding-right: 5px; + + svg { + fill: lightgrey; + cursor: pointer; + width: 100%; + height: 100%; + } + + .close-button { + height: 40px; + width: 40px; + + &:hover { + svg { + fill: #AFADAD; + } + } + } + } + + .modal-contents { + padding: 40px 35px 15px; + max-height: 450px; + overflow-y: auto; + + // h1, h3, h4 { + // @include font-h1-settings; + // font-size: 28px; + // font-weight: 400; + // margin-bottom: 25px; + // color: $color-emperor; + // margin-top: 0; + // } + + h4, h1, h3 { + @include font-h1-settings; + display: flex; + font-size: 28px; + font-weight: 400; + margin-bottom: 25px; + color: $color-emperor; + margin-top: 0; + + img { + margin-right: 10px; + } + } + + p, div { + @include font-paragraph; + } + + label { + @include font-paragraph; + margin: 0; + color: $color-silver-chalice; + } + + input[type="checkbox"] { + box-sizing: border-box; + padding: 0; + display: inline-block; + vertical-align: middle; + -webkit-appearance: none; + -moz-appearance: none; + appearance: none; + cursor: pointer; + height: 24px; + width: 24px; + background: none center no-repeat; + background-size: cover; + margin: 8px; + flex: 0 0 auto; + } + + input[type="checkbox"] { + background-image: url("data:image/svg+xml;charset=utf-8, %3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20width%3D%2224%22%20height%3D%2224%22%20viewBox%3D%220%200%2024%2024%22%20style%3D%22fill%3A%23cccccc%3B%22%3E%3Cpath%20d%3D%22M19%205v14H5V5h14m0-2H5c-1.1%200-2%20.9-2%202v14c0%201.1.9%202%202%202h14c1.1%200%202-.9%202-2V5c0-1.1-.9-2-2-2z%22%2F%3E%3C%2Fsvg%3E"); + } + + input[type="checkbox"]:checked { + border-color: $color-cyan; + background: rgba($color-cyan, 0.05); + background-image: url("data:image/svg+xml;charset=utf-8, %3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20width%3D%2224%22%20height%3D%2224%22%20viewBox%3D%220%200%2024%2024%22%20style%3D%22fill%3A%234DA9B3%3B%22%3E%3Cpath%20d%3D%22M19%203H5c-1.11%200-2%20.9-2%202v14c0%201.1.89%202%202%202h14c1.11%200%202-.9%202-2V5c0-1.1-.89-2-2-2zm-9%2014l-5-5%201.41-1.41L10%2014.17l7.59-7.59L19%208l-9%209z%22%2F%3E%3C%2Fsvg%3E"); + } + + input[type="text"] { + @include modal-text-input; + } + + textarea { + resize: none; + @extend input, [type="text"]; + padding: 8px 16px; + + &.modal__input--large { + height: 180px; + } + } + + .modal__input-wrapper { + margin-bottom: 30px; + } + + .modal__input--error, .new-block__input--error { + border: 1px solid $color-red !important; + } + + .modal__input--error-msg { + color: $color-red; + } + } + + .modal-help-text { + margin-right: 22px; + } + + .modal-actions { + padding: 0 22px 0 0; + border-top: 1px solid #E0E0E0; + width: 100%; + height: 90px; + background-color: #FCFCFC; + display: flex; + align-items: center; + justify-content: flex-end; + } + + .helper-text { + display: flex; + height: 100%; + } + + .helper-text > svg { + margin-right: 10px; + } + + .helper-text > p { + margin: 0 0 2px 0; + } + + // removed this temporarily because it was overriding button hover styles. Leaving it short-term in case it turns out it was needed for something else + // .modal-action { + // &:hover { + // background-color: #F5F3F3; + // } + + // h3 { + // text-transform: uppercase; + // margin-bottom: 0; + // } + // } +} + +.display-block { + display: block; +} + +.display-none { + display: none; +} diff --git a/scripts/app/assets/stylesheets/components/_navigation-dropdown.scss b/scripts/app/assets/stylesheets/components/_navigation-dropdown.scss new file mode 100644 index 0000000000000000000000000000000000000000..a0ddceed7319af16f3055d28940b5fb6380f3213 --- /dev/null +++ b/scripts/app/assets/stylesheets/components/_navigation-dropdown.scss @@ -0,0 +1,103 @@ +.navigation-dropdown { + & { + position: relative; + display: flex; + flex-flow: row; + align-items: center; + height: inherit; + user-select: none; + cursor: pointer; + } + + .cohort-label { + max-width: 320px; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + } + + &:hover { + background-color: $color-black-97-3; + box-shadow: 0 2px 4px 0 rgba(0,0,0,0.1); + } + + &:hover > .list { + display: block; + } + + &:hover > .label > .sprite-icon { + color: $color-black; + } + + > .label { + display: flex; + flex-flow: row nowrap; + flex: 1 0 auto; + align-items: center; + padding: 0 1rem; + } + + > .label > .user-avatar { + margin-right: 0.5rem; + } + + > .label > .sprite-icon { + color: $color-black-83-1; + margin: 0 0 0 auto; + } + + > .list { + display: none; + position: fixed; + left: 0; + right: 0; + top: 3.5rem; + z-index: 100; + min-width: 100%; + margin: 0; + padding: 0; + background-color: $color-black-97-3; + box-shadow: 0 2px 4px 0 rgba(0,0,0,0.1); + list-style: none; + + @media screen and (min-width: $tablet-breakpoint) { + position: absolute; + top: 100%; + left: auto; + } + } + + > .list > .item { + color: inherit; + border: 0 solid rgba(0,0,0,0.1); + + &:not(:first-child) { + border-top-width: 1px; + } + + &:hover { + background-color: $color-white; + color: $color-black; + } + } + + > .list > .item > a { + color: $color-black-60; + display: block; + line-height: 2.5rem; + padding: 0 1rem; + text-decoration: none; + text-transform: uppercase; + white-space: nowrap; + max-width: 400px; + overflow: hidden; + text-overflow: ellipsis; + } + + .account-email { + padding: 0 1rem; + font-size: 14px; + position: relative; + top: -8px; + } +} diff --git a/scripts/app/assets/stylesheets/components/_notifications.scss b/scripts/app/assets/stylesheets/components/_notifications.scss new file mode 100644 index 0000000000000000000000000000000000000000..049764523340ad0809e9517ebf9656c17d8103ca --- /dev/null +++ b/scripts/app/assets/stylesheets/components/_notifications.scss @@ -0,0 +1,202 @@ +.notifications { + & { + position: relative; + font-size: 12px; + width: 40px; + user-select: none; + } + + > .notificationsicon { + position: relative; + display: flex; + align-items: center; + justify-content: center; + height: 40px; + width: 40px; + cursor: pointer; + } + + > .notificationsicon > svg { + fill: $color-black-80; + } + + &.open > .notificationsicon svg { + fill: $black; + } + + > .notificationsicon > .notificationscount { + top: 8px; + right: 22px; + color: $white; + background-color: $color-valencia; + height: 14px; + min-width: 14px; + font-size: 12px; + font-weight: 500; + position: absolute; + border-radius: 14px; + padding: 0 6px; + display: flex; + align-items: center; + justify-content: center; + } + + > .notificationspanel { + position: fixed; + z-index: 500; + left: 0.5rem; + right: 0.5rem; + top: 3.5rem; + display: flex; + flex-flow: column; + max-height: 448px; + width: auto; + margin: auto; + border: 1px solid $color-black-97-3; + background-color: $white; + box-shadow: 0 2px 16px 0 rgba(0,0,0,0.25); + + @media screen and (min-width: $tablet-breakpoint) { + position: absolute; + top: 100%; + left: -140px; + width: 320px; + } + + &:after { + content: ""; + display: none; + position: absolute; + left: 0; + right: 0; + bottom: 100%; + width: 0; + height: 0; + margin: auto; + border-left: 8px solid transparent; + border-right: 8px solid transparent; + border-bottom: 8px solid $white; + background-color: transparent; + @media screen and (min-width: $tablet-breakpoint) { + display: inherit; + } + } + } + + > .notificationspanel > .headerrow { + background-color: $white; + border-bottom: 1px solid $color-black-90-2; + color: $color-black-50-2; + display: flex; + flex-flow: row nowrap; + font-size: 12px; + font-weight: bold; + align-items: center; + height: 32px; + padding: 0 16px; + flex: 0 0 auto; + text-transform: uppercase; + } + + > .notificationspanel > .bodyrow { + background-color: $color-black-96-1; + display: flex; + flex-flow: column; + flex: 0 1 auto; + overflow-y: auto; + } + + > .notificationspanel > .bodyrow > .notificationitem { + background-color: $white; + border-bottom: 1px solid $color-black-90-2; + text-decoration: none; + padding: 8px 16px 16px 32px; + position: relative; + + &:hover { + > .notificationtitle { + text-decoration: underline; + } + } + + &.unread { + background-color: $color-cyan-opacity-4; + + > .notificationtitle { + font-weight: bold; + } + + &:after { + content: ""; + height: 8px; + width: 8px; + background-color: $color-cyan; + border-radius: 100%; + position: absolute; + top: 12px; + left: 12px; + } + } + } + + > .notificationspanel > .bodyrow > .notificationitem > .notificationheader { + height: 16px; + display: flex; + flex-flow: row nowrap; + justify-content: space-between; + } + + > .notificationspanel > .bodyrow > .notificationitem > .notificationheader > .notificationtagline { + font-size: 12px; + font-weight: bold; + color: $black; + line-height: 16px; + flex: 0 1 auto; + } + + > .notificationspanel > .bodyrow > .notificationitem > .notificationheader > .notificationtimestamp { + font-size: 12px; + font-weight: 500; + color: $color-black-50-2; + line-height: 16px; + text-align: right; + flex: 0 0 auto; + } + + > .notificationspanel > .bodyrow > .notificationitem > .notificationtitle { + font-size: 14px; + color: $color-cyan; + line-height: 16px; + margin-top: 8px; + } + + > .notificationspanel > .bodyrow > .notificationitem > .notificationdescription { + font-size: 14px; + color: $color-black-20; + line-height: 16px; + margin-top: 8px; + } + + > .notificationspanel > .footerrow { + background-color: $white; + border-top: 1px solid $color-black-90-2; + color: $color-cyan; + display: flex; + flex-flow: row nowrap; + font-size: 12px; + font-weight: bold; + align-items: center; + height: 40px; + padding: 0 16px; + flex: 0 0 auto; + text-transform: uppercase; + } + + > .notificationspanel > .footerrow > .markread { + cursor: pointer; + } + + > .notificationspanel > .notificationitem { + margin: 10px 0; + } +} diff --git a/scripts/app/assets/stylesheets/components/_pagination.scss b/scripts/app/assets/stylesheets/components/_pagination.scss new file mode 100644 index 0000000000000000000000000000000000000000..4e8ea67d8e8c2cfbc34737b80068cfb5bab0b205 --- /dev/null +++ b/scripts/app/assets/stylesheets/components/_pagination.scss @@ -0,0 +1,46 @@ +.react-paginate-container { + margin: 0px 0px 30px 0px; + display: block; + float: left; + list-style: none; + width: 100%; + display: flex; + justify-content: center; + align-items: center; + padding: 0; +} + +.react-paginate-page { + background-color: blue; + display: block; + float: left; + width: 40px; + padding: 5px; + cursor: pointer; + border: 1px solid grey; + background-color: white; + border-radius: 1px; + text-align: center; + padding: 4px; + + &:hover { + background-color: #ffd4a8; + text-decoration: none; + } +} + +.react-paginate-link { + display: block; + width: 100%; + height: 100%; + text-decoration: none !important; + color: black; +} + +.react-paginate-break { + display: none; +} + +.react-paginate-active { + background-color: #ffd4a8; +} \ No newline at end of file diff --git a/scripts/app/assets/stylesheets/components/_pill.scss b/scripts/app/assets/stylesheets/components/_pill.scss new file mode 100644 index 0000000000000000000000000000000000000000..e2e88e456138a57612a9b7ff3209aed39d413ffc --- /dev/null +++ b/scripts/app/assets/stylesheets/components/_pill.scss @@ -0,0 +1,78 @@ +.pill-wrapper { + border-radius: 6px; + display: inline-block; + text-transform: uppercase; + color: #333; + font-weight: 300; + + p { + margin-top: 0px !important; + margin-bottom: 0px !important; + font-family: $font-family-proxima-nova; + } + + &.small { + padding: 2px 10px 2px 10px; + height: 18px; + + .p-text { + font-size: 10px; + line-height: 17px; + + } + + .a-text { + font-size: 10px; + line-height: 17px; + } + } + + &.medium { + padding: 2px 10px 2px 10px; + height: 24px; + + .p-text { + font-size: 12px; + line-height: 20px; + } + + .a-text { + font-size: 12px; + line-height: 20px; + } + } + + &.large { + padding: 5px 20px 5px 20px; + height: 30px; + + .p-text { + font-size: 14px; + line-height: 23px; + } + + .a-text { + font-size: 14px; + line-height: 23px; + } + } + + .p-text { + margin: 0; + text-transform: inherit; + color: inherit; + letter-spacing: 0.1rem; + margin-right: -0.1rem !important; + font-weight: inherit; + } + + .a-text { + margin: 0; + text-transform: inherit; + color: inherit; + letter-spacing: 0.1rem; + margin-right: -0.1rem !important; + font-weight: inherit; + vertical-align: top; + } +} diff --git a/scripts/app/assets/stylesheets/components/_point_grade_buttons.scss b/scripts/app/assets/stylesheets/components/_point_grade_buttons.scss new file mode 100644 index 0000000000000000000000000000000000000000..499c4f81ca6f9d7b2b1d347558ee33527435affb --- /dev/null +++ b/scripts/app/assets/stylesheets/components/_point_grade_buttons.scss @@ -0,0 +1,19 @@ +.points-container { + display: flex; + border-radius: 25px; + border: 2px solid $color-black-93-3; + background-color: $color-white; + height: 45px; + justify-content: center; + align-items: center; + padding: 0 10px 0 10px; + + .point { + color: $color-black-60-8; + text-align: center; + width: 25px; + font-size: 1.2rem; + font-weight: 500; + cursor: pointer; + } +} \ No newline at end of file diff --git a/scripts/app/assets/stylesheets/components/_primary-header.scss b/scripts/app/assets/stylesheets/components/_primary-header.scss new file mode 100644 index 0000000000000000000000000000000000000000..7ec922df7d19a74e205a5a68d9abd2dd8012690b --- /dev/null +++ b/scripts/app/assets/stylesheets/components/_primary-header.scss @@ -0,0 +1,100 @@ +.primary-header { + & { + display: flex; + align-items: center; + justify-content: space-between; + padding: 2rem 1.5rem; + } + + &.-blue { + background: $color-darkest-blue-gray; + } + + &.-blue > div > .label { + color: $color-black-60-8; + font-weight: 300; + } + + &.-blue > div > .title { + color: $color-white; + font-size: 1.5rem; + line-height: 1.5rem; + } + + &.-white { + border-bottom: 0.125rem solid $color-black-83-1; + padding: 1.5rem 0; + } + + &.-white > div > .label { + color: $color-black-60; + font-weight: bold; + } + + > .header-left { + max-width: 50%; + } + + > .header-left > .label, + > .header-left > .title { + height: auto; + overflow: hidden; + text-overflow: ellipsis; + } + + > .header-left > .title > span > a { + padding-left: 7px; + color: white; + text-decoration: none; + } + + > .header-left > .title > span > a.disabled { + color: $color-black-50-2; + } + + > .header-left > .title > span > a:hover { + color: $color-black-50-2; + } + + > .header-left > .title > span > a > div { + display: inline; + } + + .header-left > .label > a { + color: $color-black-60-8; + text-decoration: underline; + } + + .header-left > .label > a:hover { + color: $color-black-50-2; + text-decoration: none; + } + + > div > .label { + font-size: 0.75rem; + margin-bottom: 0.5rem; + text-transform: uppercase; + line-height: 1rem; + } + + > div > .title { + line-height: 1.5rem; + font-size: 1.5rem; + } + + > div > .title > .lp-badge { + vertical-align: middle; + } + + > .header-right .lp-badge { + margin-bottom: 0.5rem; + } + + > .header-right a { + text-decoration: none; + } + + > .header-right > .title { + text-align: center; + } +} diff --git a/scripts/app/assets/stylesheets/components/_primary-navigation.scss b/scripts/app/assets/stylesheets/components/_primary-navigation.scss new file mode 100644 index 0000000000000000000000000000000000000000..42fc38296a102f8169124cc51a87654f88c9eb88 --- /dev/null +++ b/scripts/app/assets/stylesheets/components/_primary-navigation.scss @@ -0,0 +1,132 @@ +.primary-navigation { + & { + position: relative; + z-index: $z-primary-navigation; + display: flex; + flex-flow: row nowrap; + align-items: center; + height: 3.5rem; + width: 100%; + border-bottom: 1px solid #f2f2f2; + background-color: $color-white; + @media screen and (min-width: $tablet-breakpoint) { + padding-right: 1rem; + } + } + + &.navshadow { + box-shadow: 0 1px 1px 0 rgba(0,0,0, 0.2); + border-bottom: 0; + } + &.checkpoint-navshadow { + box-shadow: 0 2px 20px 0 rgba(0,0,0, 0.07); + border-bottom: 1px solid $color-black-96-1; + } + + > .logo { + background: none left center no-repeat; + background-image: image-url("svg/mobile-logo.svg"); + background-size: contain; + height: 34px; + margin: 0 1rem; + min-width: 40px; + @media screen and (min-width: $tablet-breakpoint) { + background-image: image-url("svg/g-learn-lockup.svg"); + margin: 0 1.5rem; + min-width: 236px; + } + } + + > a .custom-logo { + height: 3rem; + padding: 0.25rem 0.5rem; + } + + > .item { + flex: 0 0 auto; + height: inherit; + margin: 0; + + &:first-of-type { + margin-left: auto; + } + } + + &.checkpoint-navbar { + position: fixed; + justify-content: inherit; + + .checkpoint-navbar-details { + display: flex; + justify-content: right; + white-space: nowrap; + + .checkpoint-timer { + @include font-base; + display: flex; + align-items: center; + margin-right: 4rem; + font-weight: 500; + color: $color-mine-shaft; + min-width: 92px; + + .final-countdown { + } + + svg { + margin-right: 10px; + + &.under-minute { + color: $color-valencia; + } + } + } + .final-countdown > b { + @include font-base; + font-weight: 500; + color: $color-mine-shaft; + display: inline-block; + } + + .final-countdown > .subtext { + margin-right: 10px; + } + + div.its-the.final-countdown > .subtext, div.its-the.final-countdown > b { + color: $color-valencia; + } + + .subtext { + @include font-small; + color: $color-gray; + margin-left: 8px; + } + + .checkpoint-challenges-attempted { + @include font-base; + font-weight: 500; + color: $color-mine-shaft; + display: flex; + align-items: baseline; + margin: auto 4rem auto 0; + white-space: nowrap; + } + + .checkpoint-pair-menu { + margin: auto 0 auto 10px; + display: flex; + position: relative; + + svg { + color: $color-gray; + } + + .pair-menu { + position: absolute; + z-index: 10; + bottom: 0; + } + } + } + } +} diff --git a/scripts/app/assets/stylesheets/components/_progress.scss b/scripts/app/assets/stylesheets/components/_progress.scss new file mode 100644 index 0000000000000000000000000000000000000000..7fbb166171b1b8af52d89da42b5651abc6a58108 --- /dev/null +++ b/scripts/app/assets/stylesheets/components/_progress.scss @@ -0,0 +1,59 @@ +// Named this way to avoid conflict with bootstrap's .progress-bar styling +.progressbar { + --progressbar-height: 4px; + width: 100%; + height: var(--progressbar-height); + position: relative; + + &.-theme-light { + background: $color-black-92-2; + } + &.-theme-dark { + background: #4B5157; + } + + .progress-percentage { + height: var(--progressbar-height); + position: absolute; + z-index: 15; + background: $color-cyan; + } + + .progress-pending { + height: var(--progressbar-height); + position: absolute; + z-index: 10; + background: $color-black-50-2; + } +} + +.actioncircle { + --actioncircle-size: 32px; + border-radius: 100%; + color: $color-white; + display: flex; + align-items: center; + justify-content: center; + height: var(--actioncircle-size); + width: var(--actioncircle-size); + font-size: 14px; + line-height: 15px; + font-weight: 200; + flex-shrink: 0; + + svg { + height: var(--actioncircle-size); + width: var(--actioncircle-size); + } + + &.-score-3 { + background-color: $color-lime-green; + } + &.-score-2 { + background-color: $color-banana-yellow; + } + &.-score-1 { + background-color: $color-tangerine-orange; + } +} + diff --git a/scripts/app/assets/stylesheets/components/_progress_thresholds_key.scss b/scripts/app/assets/stylesheets/components/_progress_thresholds_key.scss new file mode 100644 index 0000000000000000000000000000000000000000..770e1c3bbbd1c344566aa168bb91ed4c7e7dc87e --- /dev/null +++ b/scripts/app/assets/stylesheets/components/_progress_thresholds_key.scss @@ -0,0 +1,35 @@ +.thresholds-key-wrapper { + display: flex; + align-items: center; + + .thresholds-key-square { + width: 15px; + height: 15px; + margin-right: 8px; + border-radius: 2px; + } + + .thresholds-key-content { + margin-top: 1px; + margin-right: 30px; + margin-bottom: 0; + } + + .unscored-key-wrapper { + display: flex; + align-items: center; + + .unscored-key { + height: 10px; + width: 10px; + border-radius: 50%; + background-color: #D23C44; + } + + p { + margin-top: 0; + margin-left: 8px; + margin-right: 30px; + } + } +} \ No newline at end of file diff --git a/scripts/app/assets/stylesheets/components/_progress_thresholds_modal.scss b/scripts/app/assets/stylesheets/components/_progress_thresholds_modal.scss new file mode 100644 index 0000000000000000000000000000000000000000..27c768798717530dbcb37f2ad7cced9d71bf7d46 --- /dev/null +++ b/scripts/app/assets/stylesheets/components/_progress_thresholds_modal.scss @@ -0,0 +1,20 @@ +.progress-thresholds-modal { + .title { + color: #999999; + text-align: center; + margin-bottom: 50px; + } + + .slider-wrapper { + width: 500px; + margin: 0 auto; + } + + .description { + color: #999999; + width: 80%; + margin: 0 auto; + text-align: center; + margin-top: 63px; + } +} \ No newline at end of file diff --git a/scripts/app/assets/stylesheets/components/_progress_thresholds_slider.scss b/scripts/app/assets/stylesheets/components/_progress_thresholds_slider.scss new file mode 100644 index 0000000000000000000000000000000000000000..ef1cdc99a4d7ab9bd1f29fee57d8841a52feba55 --- /dev/null +++ b/scripts/app/assets/stylesheets/components/_progress_thresholds_slider.scss @@ -0,0 +1,8 @@ +.ranger-rick { + width: 100%; + position: relative; +} + +.rc-slider-tooltip { + z-index: 1000001; +} \ No newline at end of file diff --git a/scripts/app/assets/stylesheets/components/_search-bar.scss b/scripts/app/assets/stylesheets/components/_search-bar.scss new file mode 100644 index 0000000000000000000000000000000000000000..bcd4176df05fb6c1f8bd139c0c80619f5bcd8f33 --- /dev/null +++ b/scripts/app/assets/stylesheets/components/_search-bar.scss @@ -0,0 +1,44 @@ +.search-bar__wrapper { + display: flex; + width: 100%; + + .search-bar { + display: flex; + padding-left: 5px; + align-items: center; + height: 40px; + transition: flex-grow 0.1s, background-color 0.1s; + + &.search-bar--expanded { + @include input; + border-radius: 40px; + padding-left: 4px; + padding-right: 3px; + background-color: $color-white; + border: 1px solid $color-mercury; + margin: 0; + margin-right: 8px; + flex-grow: 1; + max-width: 400px; + } + + .search-bar__icon { + height: 32px; + padding: 4px 8px; + border-radius: 24px; + &:hover { + background-color: #E8E8E8; + } + } + + input { + border: none; + flex-grow: inherit; + margin-left: 4px; + + &:focus { /* WARNING: accessibility depends on focus outline */ + outline: none; + } + } + } +} diff --git a/scripts/app/assets/stylesheets/components/_secondary-navigation.scss b/scripts/app/assets/stylesheets/components/_secondary-navigation.scss new file mode 100644 index 0000000000000000000000000000000000000000..983231b71a560147d561d3c18099a11d1888fd42 --- /dev/null +++ b/scripts/app/assets/stylesheets/components/_secondary-navigation.scss @@ -0,0 +1,58 @@ +.secondary-navigation { + & { + position: relative; + z-index: 9999; + display: flex; + flex-flow: row nowrap; + height: 3rem; + width: 100%; + background-color: $color-white; + box-shadow: 0 1px 1px 0 rgba(0,0,0, 0.2); + overflow-x: scroll; + @media screen and (min-width: $tablet-breakpoint) { + overflow-x: auto; + } + } + + > .item { + flex: 1; + margin: 0; + height: inherit; + border: 0 solid transparent; + border-width: 0 0 2px 0; + color: $color-black-60; + @media screen and (min-width: $tablet-breakpoint) { + flex: 0 0 auto; + } + } + + > .item.-is-active { + color: black; + border-color: $color-cyan; + } + + > .item:hover { + color: black; + background-color: $color-black-97-3; + border-color: $color-black-83-1; + } + + > .item:active { + border-color: $color-dark-cyan; + } + + > .item > a { + position: relative; + display: flex; + flex-flow: row; + align-items: center; + justify-content: center; + height: inherit; + padding: 0 1rem; + text-transform: uppercase; + color: inherit; + text-decoration: none; + user-select: none; + cursor: pointer; + } +} diff --git a/scripts/app/assets/stylesheets/components/_slideshow.scss b/scripts/app/assets/stylesheets/components/_slideshow.scss new file mode 100644 index 0000000000000000000000000000000000000000..52e1bcd9ba38261873d7c49d0319237590aecab6 --- /dev/null +++ b/scripts/app/assets/stylesheets/components/_slideshow.scss @@ -0,0 +1,264 @@ +.slideshow-container { + position: absolute; + height: 100%; + width: 100%; + box-shadow: 0 1px 10px 0 rgba(0, 0, 0, 0.2); +} + +.slides { + position: relative; + height: 100%; + padding: 0px; + margin: 0px; + list-style-type: none; + overflow: hidden; +} + +.slide { + position: absolute; + left: 0; + top: 0; + width: 100%; + height: 100%; + opacity: 0; + z-index: 1; + padding: 0; + margin: 0; + background-repeat: no-repeat; + background-size: cover; + background-position: center; +} + +.showing-none { + opacity: 1; + z-index: 2; +} + +.showing-fade { + opacity: 1; + z-index: 2; + animation-duration: 1s; + animation-name: slide-fade; +} + +@keyframes slide-fade { + from { + opacity: 0; + } + + to { + opacity: 1; + } +} + +.showing-right { + opacity: 1; + animation-duration: 1s; + animation-name: slide-right; +} + +@keyframes slide-right { + from { + left: -50%; + } + + to { + left: 0; + } +} + +.showing-left { + opacity: 1; + animation-duration: 1s; + animation-name: slide-left; +} + +@keyframes slide-left { + from { + left: 100%; + } + + to { + left: 0; + } +} + +.showing-top { + opacity: 1; + animation-duration: 1s; + animation-name: slide-top; +} + +@keyframes slide-top { + from { + top: 100%; + } + + to { + top: 0; + } +} + +.showing-bottom { + opacity: 1; + animation-duration: 1s; + animation-name: slide-bottom; +} + +@keyframes slide-bottom { + from { + top: -100%; + } + + to { + top: 0; + } +} + +.showing-bounce-left { + opacity: 1; + animation-duration: 1s; + animation-name: slide-bounce-left; +} + +@keyframes slide-bounce-left { + 0% { + left: 100%; + } + 50% { + left: -20%; + } + 100% { + left: 0; + } +} + +.showing-bounce-right { + opacity: 1; + animation-duration: 1s; + animation-name: slide-bounce-right; +} + +@keyframes slide-bounce-right { + 0% { + left: -100%; + } + 50% { + left: 20%; + } + 100% { + left: 0; + } +} + +.show-index.is-text { + color: black; + position: absolute; + bottom: 0; + z-index: 100; + left: 50%; + font-size: 28px; + font-family: 'Slabo 27px', serif; +} + +.show-index.is-dot { + color: black; + position: absolute; + bottom: 0; + z-index: 100; + left: 50%; + font-size: 28px; + font-family: 'Slabo 27px', serif; + display: flex; + transform: translate(-50%); +} + +.show-index.is-dot .dot { + width: 10px; + height: 10px; + margin: 0 3px 3rem 3px; + background-color: black; + border-radius: 50%; + opacity: 0.5; +} + +.show-index.is-dot .dot.is-active { + opacity: 1; +} + +.slideshow-container button:focus { + outline: none; +} + +.btn-arrow { + display: block; + font-size: 5px; + line-height: 0; + -moz-transition: all 0.3s; + -o-transition: all 0.3s; + -webkit-transition: all 0.3s; + transition: all 0.3s; + position: absolute; + top: 50%; + z-index: 10; + cursor: pointer; + height: 50px; + width: 50px; + border-radius: 50%; + padding: 5px; + z-index: 10000; +} + +.btn-arrow:hover, +.btn-arrow:focus { + background: $color-black-92-2; +} + +.btn-arrow:hover::before, +.btn-arrow:hover::after, +.btn-arrow:focus::before, +.btn-arrow:focus::after { + background: rgba(256, 256, 256, 0.5); +} + +.btn-arrow::before { + content: ''; + display: block; + background: rgba(256, 256, 256, 0.6); + position: absolute; + top: 0; + left: 0; + width: 6em; + height: 2em; +} + +.btn-arrow::after { + content: ''; + display: block; + background: rgba(256, 256, 256, 0.6); + position: absolute; + top: 0; + left: 0; + width: 2em; + height: 6em; +} + +.btn-arrow.btn-arrow-left { + left: -75px; +} + +.btn-arrow.btn-arrow-left::before, +.btn-arrow.btn-arrow-left::after { + top: 18px; + left: 18px; +} + +.btn-arrow.btn-arrow-right { + right: -75px; +} + +.btn-arrow.btn-arrow-right::before, +.btn-arrow.btn-arrow-right::after { + top: 18px; + left: 18px; +} \ No newline at end of file diff --git a/scripts/app/assets/stylesheets/components/_sort-dropdown-component.scss b/scripts/app/assets/stylesheets/components/_sort-dropdown-component.scss new file mode 100644 index 0000000000000000000000000000000000000000..1912fbd04816f63d70b7c1f5700f932e5cf9a5c3 --- /dev/null +++ b/scripts/app/assets/stylesheets/components/_sort-dropdown-component.scss @@ -0,0 +1,52 @@ +.sort-dropdown-component { + & { + position: relative; + display: flex; + flex-flow: row nowrap; + align-items: center; + margin-bottom: 32px; + user-select: none; + } + + > .label { + flex: 1 0 auto; + color: $color-black-60-8; + font-size: 18px; + line-height: 24px; + margin-right: 8px; + } + + > .svg { + flex: 0 0 auto; + fill: $color-black-83-1; + position: absolute; + left: auto; + right: 0; + top: 0; + bottom: 0; + margin: auto; + pointer-events: none; + } + + > select { + -webkit-appearance: none; + -moz-appearance: none; + appearance: none; + flex: 0 0 auto; + border: none; + background: none; + margin: 0; + padding: 0; + width: auto; + color: $color-darkest-blue-gray; + font-size: 18px; + font-weight: bold; + line-height: 24px; + cursor: pointer; + padding-right: (24 + 4 + px); + + &:hover + .svg { + fill: $color-cyan; + } + } +} diff --git a/scripts/app/assets/stylesheets/components/_standard-card.scss b/scripts/app/assets/stylesheets/components/_standard-card.scss new file mode 100644 index 0000000000000000000000000000000000000000..08364f4d367ca53542a6776c5ddf70308f786b64 --- /dev/null +++ b/scripts/app/assets/stylesheets/components/_standard-card.scss @@ -0,0 +1,234 @@ +.standardcard { + background-color: $color-white; + margin-bottom: 1.5rem; + border-radius: 5px; + border: 1px solid $color-black-90-2; + display: flex; + flex-flow: row wrap; + align-items: flex-start; + justify-content: space-between; + padding: 20px; + + &.Percentage { + .standard-scoring-block { + border-bottom: 0px solid transparent; + height: auto; + padding: 0; + flex-flow: column; + flex: 0; + } + + .student-details { + width: 50%; + flex: 1; + font-size: 18px; + + span { + min-width: 250px; + } + } + + .info { + color: $color-black-60; + font-size: 14px; + margin-top: 10px; + } + + button { + margin: 25px auto 0; + width: 100%; + } + } + + > .standard-current-score { + background-color: $color-black-92-2; + width: 100%; + padding: 0.3125rem 1.5rem; + font-size: 0.75rem; + font-weight: bold; + letter-spacing: 0.0625rem; + text-transform: uppercase; + } + + > .standard-current-score > .label { + color: $color-black; + opacity: 0.5; + } + + > .standard-description { + padding-top: 1rem; + padding-bottom: 1rem; + width: 100%; + border-top: 1px solid #e6e6e6; + margin-left: 20px; + margin-right: 20px; + } + + > .standard-description > h4 { + margin: 0 0 1.25rem; + font-size: 1.25rem; + font-weight: normal; + line-height: 1.75rem; + color: $color-black; + } + + > .standard-description > .success-criteria { + color: $color-black; + font-size: 1rem; + line-height: 1.5rem; + } + + > .standard-description > .success-criteria > ul { + padding: 0 1rem 0 1.25rem; + margin: 1rem 0 0; + } + + > .standard-description > .success-criteria > ul > li { + padding-left: 0.5rem; + } + + > .standard-description > .toggle-success-criteria { + cursor: pointer; + position: relative; + } + + > .standard-description > .toggle-success-criteria > .label.gray-label { + color: $color-black-60-8; + text-transform: uppercase; + font-size: 0.75rem; + font-weight: bold; + letter-spacing: 0.0625rem; + } + + > .standard-description > .toggle-success-criteria > .svg { + position: absolute; + left: auto; + margin: auto; + pointer-events: none; + right: 0; + color: $color-black-60-8; + top: 0; + bottom: 0; + font-size: 0.875rem; + } + + > .standard-description > .null-success-criteria { + font-style: italic; + color: $color-black-60-8; + } + + > .standard-scoring-block { + display: flex; + height: 4.5rem; + align-items: center; + justify-content: space-between; + padding: 0.3125rem 0; + margin-left: 20px; + margin-right: 20px; + } + + > .standard-scoring-block > .label { + width: 50%; + color: $color-black; + } + + > .standard-scoring-block > .standard-scoring-buttons { + display: flex; + } + + > .standard-scoring-block > .standard-scoring-buttons > .standard-scoring-button { + height: 2.5rem; + width: 2.5rem; + background-color: $color-black-90-2; + color: $color-white; + cursor: pointer; + line-height: 2.5rem; + text-align: center; + margin-left: 0.5rem; + border-radius: 100%; + user-select: none; + font-weight: bolder; + font-size: 1rem; + + &.score-1:hover, + &.score-1.-is-selected { + background-color: $color-valencia; + cursor: default; + } + + &.score-2:hover, + &.score-2.-is-selected { + background-color: $color-banana-yellow; + cursor: default; + } + + &.score-3:hover, + &.score-3.-is-selected { + background-color: $color-lime-green; + cursor: default; + } + + &.-is-suggested:hover, + &.-is-suggested { + border: 0.25rem solid $color-darkest-blue-gray; + background-color: $color-white; + color: $color-darkest-blue-gray; + line-height: 2rem; + } + } + + > .standard-scoring-block > .standard-scoring-buttons > .standard-scoring-button > .suggested-score-tooltip { + background: $color-darkest-blue-gray !important; + padding-top: 0.5rem !important; + padding-bottom: 0.5rem !important; + margin-top: 0.25rem; + + &:after { + display: inline-block !important; + border-left-color: $color-darkest-blue-gray; + height: 0.75rem; + margin-top: -0.375rem; + } + } + + > .standard-scoring-block.-percentage-mode > .percentage-score { + display: flex; + align-items: center; + // padding-right: 20px; + flex-flow: column nowrap; + width: 100px; + + > .overall { + color: #333; + font-size: 24px; + display: flex; + flex-flow: column; + align-items: center; + + .scoring { + padding-top: 5px; + + p { + margin: 0; + padding: 0; + text-align: center; + line-height: 23px; + + &:first-of-type { + font-size: 30px; + font-weight: 400; + } + &:last-of-type { + font-size: 14px; + color: rgb(54,54,54) + } + + } + } + } + .p-text { + color: #DD3D48; + font-weight: 400; + } + } +} diff --git a/scripts/app/assets/stylesheets/components/_standard-cards.scss b/scripts/app/assets/stylesheets/components/_standard-cards.scss new file mode 100644 index 0000000000000000000000000000000000000000..5ca56811fdf0a874b43c2bcfb57dfd43f8da8742 --- /dev/null +++ b/scripts/app/assets/stylesheets/components/_standard-cards.scss @@ -0,0 +1,159 @@ +.standards-cards { + .standardcard { + & { + box-shadow: 0 1px 3px rgba(0, 0, 0, 0.12), 0 1px 2px rgba(0, 0, 0, 0.24); + background-color: $color-white; + margin: 24px; + } + + > .standard-current-score { + background-color: $color-black-92-2; + width: 100%; + padding: 5px 24px; + font-size: 12px; + font-weight: bold; + letter-spacing: 1px; + text-transform: uppercase; + + > .label { + color: $color-black; + opacity: 0.5; + } + } + + > .standard-description { + padding: 16px 24px 24px; + + > h4 { + margin: 0 0 20px; + font-size: 24px; + font-weight: normal; + line-height: 28px; + color: $color-black; + } + + > .success-criteria { + color: $color-black; + font-size: 16px; + line-height: 24px; + + > ul { + padding: 0px 16px 0px 20px; + margin: 16px 0px 0px; + + > li { + padding-left: 8px; + } + } + } + + > .toggle-success-criteria { + cursor: pointer; + position: relative; + } + + > .toggle-success-criteria > .label.gray-label { + color: $color-black-60-8; + text-transform: uppercase; + font-size: 12px; + font-weight: bold; + letter-spacing: 1px; + } + + > .toggle-success-criteria > .svg { + position: absolute; + left: auto; + margin: auto; + pointer-events: none; + right: 0; + color: $color-black-60-8; + top: 0px; + bottom: 0px; + font-size: 14px; + } + + > .null-success-criteria { + font-style: italic; + color: $color-black-60-8; + } + } + + > .standard-scoring-block { + display: flex; + height: 72px; + align-items: center; + justify-content: space-between; + border-bottom: 1px solid $color-black-90-2; + padding: 5px 24px; + + > .label { + width: 50%; + color: $color-black; + } + + > .standard-scoring-buttons { + display: flex; + + > .standard-scoring-button { + height: 40px; + width: 40px; + background-color: $color-black-90-2; + color: $color-white; + cursor: pointer; + line-height: 40px; + text-align: center; + margin-left: 8px; + border-radius: 100%; + user-select: none; + font-weight: bolder; + font-size: 16px; + + &.score-1:hover, + &.score-1.-is-selected { + background-color: $color-valencia; + } + + &.score-2:hover, + &.score-2.-is-selected { + background-color: $color-navy-blue; + } + + &.score-3:hover, + &.score-3.-is-selected { + background-color: $color-lime-green; + } + + &.-is-suggested:hover, + &.-is-suggested { + border: 4px solid $color-darkest-blue-gray; + background-color: $color-white; + color: $color-darkest-blue-gray; + line-height: 32px; + } + } + + > .standard-scoring-button > .suggested-score-tooltip { + background: $color-darkest-blue-gray !important; + padding-top: 8px !important; + padding-bottom: 8px !important; + margin-top: 4px; + + &:after { + display: inline-block !important; + border-left-color: $color-darkest-blue-gray; + height: 12px; + margin-top: -6px; + } + } + } + } + } + + .nullstandardcard { + & { + font-size: 16px; + font-style: italic; + padding: 24px; + } + } +} diff --git a/scripts/app/assets/stylesheets/components/_standards-mastery-beans.scss b/scripts/app/assets/stylesheets/components/_standards-mastery-beans.scss new file mode 100644 index 0000000000000000000000000000000000000000..5452867c590d68a4f6e4a8c87bf072a4e42ea27c --- /dev/null +++ b/scripts/app/assets/stylesheets/components/_standards-mastery-beans.scss @@ -0,0 +1,35 @@ +.standardbeans { + display: flex; + flex-flow: row nowrap; + align-items: center; + margin-right: 16px; +} + +.standardbean { + width: 8px; + height: 8px; + margin-right: 6px; + border-radius: 8px; + flex: 0 0 auto; + + + &.-unscored { + background-color: $color-black-84-7; + } + + &.-is-completed { + background-color: $color-cyan; + } + + &.-score-3 { + background-color: $color-lime-green; + } + + &.-score-2 { + background-color: $color-banana-yellow; + } + + &.-score-1 { + background-color: $color-tangerine-orange; + } +} diff --git a/scripts/app/assets/stylesheets/components/_status_picker.scss b/scripts/app/assets/stylesheets/components/_status_picker.scss new file mode 100644 index 0000000000000000000000000000000000000000..c26f1544e2c3e7647e6a1d7f539e0b01c22eaa51 --- /dev/null +++ b/scripts/app/assets/stylesheets/components/_status_picker.scss @@ -0,0 +1,132 @@ +.status-container { + display: flex; + border-radius: 25px; + border: 2px solid $color-black-93-3; + background-color: $color-white; + height: 45px; + justify-content: space-between; + align-items: center; + padding: 0; + width: 120px; + background: linear-gradient( + to right, + transparent 0%, + transparent calc(50% - 0.81px), + $color-black-93-3 calc(50% - 0.8px), + $color-black-93-3 calc(50% + 0.8px), + transparent calc(50% + 0.81px), + transparent 100% + ); + background-color: white; + cursor: pointer; + + .incorrect-status { + height: 100%; + width: 60px; + display: flex; + justify-content: center; + border-radius: 25px 0 0 25px; + + .svg-div { + color: $color-black-60-8; + text-align: center; + font-size: 1.2rem; + font-weight: 500; + height: 100%; + display: flex; + justify-content: center; + align-items: center; + width: 35px; + } + + .incorrect-check { + color: $color-valencia; + width: 40px; + height: 40px; + } + + &:hover { + background-color: $color-cyan-very-light; + } + } + + .correct-status { + height: 100%; + width: 60px; + display: flex; + justify-content: center; + border-radius: 0 25px 25px 0; + + .svg-div { + color: $color-black-60-8; + text-align: center; + font-size: 1.2rem; + font-weight: 500; + height: 100%; + display: flex; + justify-content: center; + align-items: center; + width: 35px; + } + + .correct-check { + color: $color-cyan; + width: 40px; + height: 40px; + } + + &:hover { + background-color: $color-cyan-very-light; + } + } + + &.vertical { + flex-direction: column; + justify-content: center; + height: auto; + position: absolute; + top: 43px; + right: -18px; + border-radius: 10px; + z-index: 1; + width: 80px; + padding: 0; + box-shadow: 0 0 8px 0 rgba(0, 0, 0, 0.1); + background: white; + + .status { + width: 100%; + } + + .incorrect-status { + width: 100%; + border-bottom: 1px solid $color-black-93-3; + border-radius: 8px 8px 0 0; + + &:hover { + background-color: $color-cyan-very-light; + } + } + + .correct-status { + width: 100%; + border-radius: 0 0 8px 8px; + + &:hover { + background-color: $color-cyan-very-light; + } + } + + .correct-check { + margin: 0; + } + + .incorrect-check { + margin: 0; + } + + .svg-div { + width: 100%; + } + } +} diff --git a/scripts/app/assets/stylesheets/components/_student-mastery-table.scss b/scripts/app/assets/stylesheets/components/_student-mastery-table.scss new file mode 100644 index 0000000000000000000000000000000000000000..c51659296a6f37e6e6d4d2f3a412a9ddd2f0c4e0 --- /dev/null +++ b/scripts/app/assets/stylesheets/components/_student-mastery-table.scss @@ -0,0 +1,156 @@ +.student-performances-table { + & { + background: $color-black-97-3 + } + > .page-header > .container { + > .performance-breadcrumb { + margin: 32px; + padding: 0; + list-style: none; + color: $color-too-gray; + font-size: 16px; + > .item { + color: inherit; + display: inline-block; + } + > .item:not(:first-of-type):before { + content: ""; + display: inline-block; + margin: 0 3px; + height: 18px; + width: 24px; + background: none center no-repeat; + background-image: url("data:image/svg+xml;charset=utf-8, %3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20width%3D%2224%22%20height%3D%2224%22%20viewBox%3D%220%200%2024%2024%22%3E%0A%20%20%3Cpath%20fill%3D%22grey%22%20d%3D%22M10%206L8.59%207.41%2013.17%2012l-4.58%204.59L10%2018l6-6z%22%2F%3E%0A%3C%2Fsvg%3E"); + background-size: contain; + transform: translateY(4px); + } + > .item > a { + font-weight: 300; + text-decoration: none; + outline: 0; + transition: all 300ms ease; + color: inherit; + opacity: 0.5; + } + > .item a:hover { + opacity: 1; + } + } + + > .performance-heading { + margin: 2rem 0 0; + font-size: 48px; + } + } + + .scores > .score > .score-label { + font-size: 12px; + color: $color-white; + position: absolute; + bottom: 1rem; + left: 0.25rem; + right: 0; + text-align: center; + } + + .scores > .score > .value { + font-size: 64px; + font-weight: normal; + color: white; + line-height: 144px; + } + + .scores > .score { + display: inline-block; + position: relative; + background: $color-blue-dark; + border: 1px solid $color-gray-light; + text-align: center; + width: 160px; + height: 160px; + flex: 1 1 auto; + } + + .scores { + position: relative; + padding: 2rem 0; + width: 100%; + display: flex; + margin: 0 auto; + text-align: right; + } + + .performances-container { + & { + padding: 16px; + background: $color-white; + } + + > .legend-div { + display: flex; + margin-bottom: 18px; + align-items: center; + } + + > .legend-div > .standard-legend { + border: 1px solid $color-gray-light; + bottom: 3rem; + border-radius: 0.25rem; + padding: 0.5rem 2rem; + margin-right: 0.5rem; + margin-top: 0.5rem; + } + + > .legend-div > .standard-legend.elective { + background-color: $color-pale-yellow; + } + + > .legend-div > a { + flex-grow: 1; + text-align: right; + } + + .standard-elective { + background-color: $color-pale-yellow; + } + } +} + +table.table { + tr.tr-summary, + tr.tr-standard { + th, + td { + background: $color-light-gray-blue; + } + } + + td { + &.score-selected { + box-shadow: 0 0 0 2px black inset; + } + + &.score-0 { + text-align: center; + vertical-align: middle; + } + + &.score-1 { + background: $color-soft-red; + text-align: center; + vertical-align: middle; + } + + &.score-2 { + background: $color-soft-orange; + text-align: center; + vertical-align: middle; + } + + &.score-3 { + background: $color-sage; + text-align: center; + vertical-align: middle; + } + } +} diff --git a/scripts/app/assets/stylesheets/components/_student-name-bar.scss b/scripts/app/assets/stylesheets/components/_student-name-bar.scss new file mode 100644 index 0000000000000000000000000000000000000000..a857e6a8c4bfcd439da12bd34a4c759b73abecf0 --- /dev/null +++ b/scripts/app/assets/stylesheets/components/_student-name-bar.scss @@ -0,0 +1,92 @@ +.student-name-bar { + & { + height: 4rem; + background-color: $color-darkester-blue-gray; + position: relative; + width: 100%; + display: flex; + justify-content: space-between; + align-items: center; + } + + &.-is-sticky { + position: fixed; + z-index: 9999; + top: 0; + left: 0; + } + + > .avatarname { + color: $color-white; + display: flex; + align-items: center; + font-size: 1.125rem; + } + + > .avatarname > div > .user-avatar, + > .avatarname > .user-avatar { + padding: 0; + margin-right: 10px; + height: 32px; + width: 32px; + } + + > .avatarname > .submit-with-avatars > .user-avatar { + background-color: $color-black-60; + background-size: contain; + border-radius: 100%; + position: relative; + color: $color-white; + margin-right: auto; + border: 2px solid #262A2E; + } + + > .avatarname > .submit-with-avatars.showing-one > .user-avatar { + margin-right: 10px; + } + + > .avatarname > .submit-with-avatars.others .user-avatar { + margin-right: 10px; + } + + > .backtochallengeslink { + margin-left: 2rem; + color: $color-cyan; + font-size: 0.75rem; + cursor: pointer; + text-transform: uppercase; + text-decoration: none; + font-weight: 400; + } + + > .nextsubmissionlink { + margin-right: 2rem; + color: $color-cyan; + font-size: 0.75em; + cursor: pointer; + text-transform: uppercase; + text-decoration: none; + font-weight: 400; + } + + > .allgraded { + display: flex; + align-items: center; + margin-right: 17px; + &:hover { + text-decoration: none; + } + } + + > .avatarname > a { + color: $color-white; + } + + > .avatarname > a.-disabled { + opacity: 0.3; + } + + > .avatarname > a svg { + margin: 1.5625rem 1rem 1rem; + } +} diff --git a/scripts/app/assets/stylesheets/components/_submissions-dashboard-table.scss b/scripts/app/assets/stylesheets/components/_submissions-dashboard-table.scss new file mode 100644 index 0000000000000000000000000000000000000000..4af63c34b0c975e287b95318081b4e3e99bd496c --- /dev/null +++ b/scripts/app/assets/stylesheets/components/_submissions-dashboard-table.scss @@ -0,0 +1,506 @@ +$cell-height: 56px; + +%student-column { + & { + flex: 0 0 auto; + width: 40px; + } + + > .block-title-spacer { + background: $color-black-70-2; + height: 40px; + } + + > .student { + background: $color-white; + display: flex; + flex-flow: column; + align-items: center; + justify-content: center; + height: $cell-height; + padding: 8px; + cursor: pointer; + + &:hover { + background: $color-black-97-3; + } + } + + > .standard > .standarditem { + position: relative; + width: 100%; + border-right: 1px solid $color-black-93-3; + + &:not(:last-of-type) { + border-bottom: 2px solid $color-black-83-1; + } + } + + > .standard > .standarditem > .itemheading { + position: relative; + display: flex; + flex-flow: column; + align-items: center; + justify-content: center; + height: $cell-height; + background: white; + padding: 8px; + } + + > .standard > .standarditem > .itemheading > a { + text-decoration: none; + } + + > .standard > .standarditem > .itemheading > a > .actioncircle { + &{ + border-radius: 100%; + color: $color-white; + display: flex; + align-items: center; + justify-content: center; + height: 23px; + width: 23px; + font-size: 10px; + line-height: 15px; + font-weight: 200; + } + + &.-score-3 { + background-color: $color-lime-green; + } + + &.-score-2 { + background-color: $color-banana-yellow; + } + + &.-score-1 { + background-color: $color-tangerine-orange; + } + } + + > .standard > .standarditem > .itembody { + display: flex; + flex-flow: column; + align-items: center; + justify-content: center; + position: relative; + overflow: hidden; + background: $color-black-97-3; + height: $cell-height; + border-top: 1px solid $color-black-93-3; + } + + > .standard > .standarditem > .itembody > a { + flex: 1 1 100%; + display: inherit; + align-items: inherit; + justify-content: inherit; + padding: 8px; + + &:hover { + background: $color-black-92-2; + } + } + + > .standard > .emptyrow { + height: 40px; + background: $color-black-92-2; + } + + > .standard > .standarditem > .itemheading > svg.donut, + > .standard > .standarditem > .itembody > svg.donut { + height: 100%; + width: 100%; + } +} + +%standard-item { + & { + } + + .standarditem.hidden .itemheading .title { + opacity: 0.4; + } + + > .standardtitle { + display: flex; + flex-flow: row nowrap; + align-items: center; + background: $color-black-92-2; + color: $color-dark-blue-gray; + font-size: 12px; + font-weight: bold; + letter-spacing: 2px; + height: 40px; + line-height: 40px; + text-transform: uppercase; + user-select: none; + cursor: default; + } + + > .standardtitle > .challenge-table-label { + flex: 0 0 auto; + height: 100%; + padding: 0 16px; + background: $color-black-83-1; + } + + > .standardtitle > .title { + flex: 1; + height: 100%; + padding: 0 16px; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + } + + > .standarditem { + position: relative; + background: white; + + &:not(:last-of-type) { + border-bottom: 2px solid $color-black-83-1; + } + + &.-is-expanded > .itemheading > .iconwrapper.arrow { + transform: rotate(0); + color: $color-cyan; + } + } + + > .standarditem > .itemheading { + color: $color-dark-blue-gray; + background: white; + position: relative; + display: flex; + flex-flow: column; + justify-content: center; + height: $cell-height; + + &:hover { + cursor: pointer; + color: $color-cyan; + } + } + + > .standarditem > .itemheading > .iconwrapper { + position: absolute; + top: 0; + bottom: 0; + left: 0; + right: auto; + margin: auto; + margin-left: 8px; + height: 24px; + width: 24px; + } + + > .standarditem > .itemheading > .iconwrapper { + &.arrow { + transform: rotate(-90deg); + color: inherit; + } + &.star { + color: $color-galvanize-orange + } + } + + > .standarditem > .itemheading > .challenge-table-label { + text-transform: uppercase; + color: $color-black-60-8; + font-size: 10px; + letter-spacing: 2px; + line-height: 16px; + margin-left: 40px; + } + + > .standarditem > .itemheading > .title { + //color: $dark-blue-gray; + color: inherit; + font-size: 16px; + line-height: 24px; + margin-left: 40px; + padding-right: 16px; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + } + + > .standarditem > .itemheading > .title > a { + text-decoration: none; + color: inherit; + } + + > .standarditem > .itembody { + position: relative; + height: $cell-height; + background: $color-black-97-3; + border-left: 4px solid $color-cyan; + padding-left: 32px; + } + + > .standarditem > .itembody > .short-border-box { + display: flex; + flex-flow: column; + justify-content: center; + border-top: 1px solid $color-black-93-3; + height: 100%; + } + + > .standarditem > .itembody > .short-border-box > .challenge-table-label { + color: $color-black-60-8; + font-size: 10px; + letter-spacing: 2px; + line-height: 16px; + text-transform: uppercase; + margin-left: 16px; + } + + > .standarditem > .itembody > .short-border-box > .title { + color: $color-dark-blue-gray; + font-size: 16px; + line-height: 24px; + margin-left: 16px; + margin-right: 8px; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + } +} + +.submissions-dashboard-table-wrapper { + + .flex-row { + > .nameplate { + margin-bottom: 1rem; + } + + > .nameplate > .avatar-wrapper { + width: 40px; + display: inline-block; + line-height: 40px; + margin-right: 10px; + vertical-align: middle; + } + + > .nameplate > span { + display: inline-block; + line-height: 40px; + font-size: 1.1rem; + vertical-align: middle; + color: $color-darker-gray-blue; + } + } +} + +.challengeindex-table { + & { + position: relative; + display: flex; + flex-flow: row nowrap; + overflow: hidden; + background-color: $color-white; + box-shadow: 0 2px 8px 0 rgba(0, 0, 0, 0.1); + } + + &:after { + content: ""; + position: absolute; + left: 100%; + top: 0; + bottom: 0; + box-shadow: -4px 0 6px 1px rgba(black, 0.1); + background: white; + width: 10px; + } + + &.single-student { + &:after { + box-shadow: none; + } + } + + > .studentspanel { + position: relative; + z-index: 0; + flex: 1 1 100%; + overflow: hidden; + background: $color-black-83-1; + } + + > .studentspanel .studentcolumn { + @extend %student-column; + } + + > .curriculumpanel { + position: relative; + z-index: 1; + flex: 0 0 auto; + width: 320px; + padding-top: $cell-height; + box-shadow: 4px 0 6px 1px rgba(black, 0.1); + } + + > .curriculumpanel > .block-title { + background: $color-black-70-2; + height: 40px; + padding: 0 1rem; + color: $color-dark-blue-gray; + font-size: 12px; + font-weight: bold; + letter-spacing: 2px; + line-height: 40px; + text-transform: uppercase; + user-select: none; + cursor: default; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + } + + > .curriculumpanel > .standard { + @extend %standard-item; + } + + > .curriculumpanel { + &.single-student { + padding-top: 0; + box-shadow: none; + + > .standard > .standarditem { + border-right: 2px solid $color-black-83-1; + } + } + } + + > .studentspanel { + &.single-student { + overflow-x: auto; + + .studentcolumn { + flex: 1 0 auto; + } + + > .studentcolumn > .standard > .standarditem > .itemheading { + flex-flow: row; + justify-content: flex-end; + } + + > .studentcolumn > .standard > .standarditem > .itembody { + flex-flow: row; + justify-content: flex-end; + } + + > .studentcolumn > .standard > .standarditem > .itemheading svg.donut { + width: 24px; + } + } + } +} + +.columnwrapper { + width: 100%; + overflow: auto; + display: flex; + flex-flow: row nowrap; +} + +.studentnamebar { + position: fixed; + top: 0; + z-index: 999; + width: calc(100% - 32px - 320px); + overflow: auto; + + &.-is-hidden { + visibility: hidden; + pointer-events: none; + } + + > .studentavatars { + display: flex; + flex-flow: row nowrap; + } + + > .studentavatars > .student { + background: $color-white; + flex: 0 0 auto; + display: flex; + flex-flow: column; + align-items: center; + justify-content: center; + height: $cell-height; + width: 40px; + padding: 8px; + border-bottom: 1px solid $color-black-83-1; + + &:hover { + background: $color-black-97-3; + } + } +} + +.dropdown-component > select { + background-color: $color-white; +} + +.icon-legend { + & { + height: 48px; + background: none; + border: 1px solid $color-black-83-1; + border-radius: 4px; + display: inline-flex; + flex-flow: row nowrap; + vertical-align: bottom; + align-items: center; + margin-bottom: 32px; + user-select: none; + overflow: hidden; + position: relative; + } + + > .item { + flex: 1 0 auto; + display: flex; + flex-flow: row nowrap; + align-items: center; + margin: 0 16px; + } + + > .item > .challenge-table-label { + color: $color-darker-gray-blue; + font-size: 16px; + line-height: 22px; + } + + > .item > .svg { + margin-right: 8px; + } +} + +.flex-row { + & { + position: relative; + display: flex; + flex-flow: row; + } + &.-align-middle { + align-items: center; + } + + > .-flex-alignright { + margin-right: 0; + margin-left: auto; + } + + > .-margin-right { + margin-right: 8px; + } +} + +// other flex-row styles +.flex-row > .pullright { + margin-right: 0; + margin-left: auto; +} diff --git a/scripts/app/assets/stylesheets/components/_svg-icon.scss b/scripts/app/assets/stylesheets/components/_svg-icon.scss new file mode 100644 index 0000000000000000000000000000000000000000..203a037e9e64618e39b157b44f20bd3c3315f65f --- /dev/null +++ b/scripts/app/assets/stylesheets/components/_svg-icon.scss @@ -0,0 +1,33 @@ +svg.icon { + fill: currentColor; + display: inline-block; + height: 24px; + width: 24px; +} + +svg, +.svg { + fill: currentColor; + width: 24px; + height: 24px; +} + +.svg-24px { + width: 24px; + height: 24px; +} + +.svg-21px { + width: 21px; + height: 21px; +} + +.svg-18px { + width: 18px; + height: 18px; +} + +.svg-15px { + width: 15px; + height: 15px; +} diff --git a/scripts/app/assets/stylesheets/components/_thresholds.scss b/scripts/app/assets/stylesheets/components/_thresholds.scss new file mode 100644 index 0000000000000000000000000000000000000000..9ada910d6a5ff8de9dc6bf9247374469b620f53f --- /dev/null +++ b/scripts/app/assets/stylesheets/components/_thresholds.scss @@ -0,0 +1,33 @@ +.thresholds-key-wrapper-div { + display: flex; + padding-top: 10px; + + .settings-cog { + display: flex; + align-items: center; + cursor: pointer; + height: 25px; + width: 25px; + position: relative; + + svg { + fill: $color-cyan; + } + + &:hover { + svg { + fill: #2D909A; + } + } + } +} + +.lesson-layout .body-container .mdown-container .settings-cog { + top: 4px; +} + +.lesson-layout .body-container .mdown-container p.thresholds-key-content { + margin-top: 1px; + margin-right: 30px; + margin-bottom: 0; +} \ No newline at end of file diff --git a/scripts/app/assets/stylesheets/components/_universal-list.scss b/scripts/app/assets/stylesheets/components/_universal-list.scss new file mode 100644 index 0000000000000000000000000000000000000000..11d456825f0a3cc51842c22e10d2eea289c44b49 --- /dev/null +++ b/scripts/app/assets/stylesheets/components/_universal-list.scss @@ -0,0 +1,46 @@ +.universal-list-wrapper { + + background-color: $color-alabaster; + + .one-column { + @include one-column; + } + + .list__header { + display: flex; + justify-content: flex-start; + margin-bottom: 20px; + align-items: center; + + h1 { + @include font-h1-settings; + } + + } + + .list__header-bar { + display: flex; + justify-content: flex-end; + align-items: center; + margin-bottom: 12px; + + button { + @include button-theme($color-primary-teal, 'material'); + } + } + + .list-container { + @include list-container; + + a:hover { + text-decoration: none; + } + } + + .back-btn { + height: 24px; + margin-right: 8px; + margin-bottom: 40px; + } + +} diff --git a/scripts/app/assets/stylesheets/components/_universal-row.scss b/scripts/app/assets/stylesheets/components/_universal-row.scss new file mode 100644 index 0000000000000000000000000000000000000000..4074033ae6f8e5b11baae781517b66ed5e538fff --- /dev/null +++ b/scripts/app/assets/stylesheets/components/_universal-row.scss @@ -0,0 +1,68 @@ +.universal-row { + display: flex; + justify-content: space-between; + padding: 30px 40px; + border-bottom: 2px solid $color-black-94-9; + + h2 { + @include font-h2-settings; + } + + a { + @include link-theme; + } + + p { + @include font-paragraph; + font-size: 15px; + line-height: 20px; + margin-bottom: 15px; + } + + img { + margin-right: 6px; + margin-bottom: 1px !important; + fill: #000; + } + + .gray-text { + color: $color-silver-chalice; + font-weight: 500; + } + + .row__footer { + display: flex; + flex-wrap: wrap; + } + + .pill-wrapper { + margin-right: 5px; + margin-top: 5px; + } + + .row__title-col :last-child { + margin-bottom: 0; + } + + .row__title-col { + flex-grow: 1; + + .row__cat-header { + @include font-caption; + margin-left: 3px; + margin-bottom: 15px; + } + } + + &.hidden { + display: none; + } +} + +.universal-row-click-wrapper { + cursor: pointer; + + &:hover { + box-shadow: $hover-box-shadow; + } +} diff --git a/scripts/app/assets/stylesheets/components/_universal-table.scss b/scripts/app/assets/stylesheets/components/_universal-table.scss new file mode 100644 index 0000000000000000000000000000000000000000..d9998c7234476aa25f86dd26a2583143eddf7abd --- /dev/null +++ b/scripts/app/assets/stylesheets/components/_universal-table.scss @@ -0,0 +1,121 @@ +.universal-table__wrapper { + + .universal-table__table { + font-size: 16px; + width: 100%; + table-layout: fixed; + + tbody { + color: $color-tundora; + border: 1px solid $color-mercury; + font-weight: 500; + background: white; + + &.--box-shadow { + box-shadow: 0 1px 10px 0 rgba(0,0,0,0.06); + } + + tr { + border-bottom: 1px solid $color-black-93-3; + height: 56px; + + &.--highlight:hover { + border-bottom: 1px solid #d0d0d0; + box-shadow: 0 0 10px 0 rgba(0,0,0,0.08); + } + + &.--disabled { + background: $color-alabaster; + color: $color-black-60; + + a, span, div { + color: $color-black-60; + } + } + + &.--selected td { + background: rgba(77, 169, 179, 0.1); + } + + &:last-of-type { + td { + border-bottom: none; + } + } + } + } + + th, td { + white-space: nowrap; + padding: 0 20px; + overflow: hidden; + text-overflow: ellipsis; + + &.checkbox-col { + width: 5%; + text-align: center; + } + + &:first-child { + padding-left: 30px; + } + + &:last-child { + padding-right: 30px; + } + } + + th { + height: 36px; + color: $color-dove-gray; + font-weight: 500; + } + + td { + height: inherit; + + a, span { + color: $color-tundora; + text-decoration: none; + height: 100%; + display: flex; + align-items: center; + display: inline-block; + height: fit-content; + vertical-align: middle; + } + + a:hover { + color: $color-link-blue; + } + + .user-avatar { + margin-right: 10px; + } + + div { + max-width: 170px; + overflow: hidden; + text-overflow: ellipsis; + display: inline-block; + vertical-align: middle; + } + } + + .--empty { + text-align: center; + } + + .universal-table__col { + overflow: visible !important; + } + } + + .universal-table__view-all-btn { + @include font-paragraph; + font-size: 15px; + color: $color-primary-teal; + cursor: pointer; + width: fit-content; + } +} diff --git a/scripts/app/assets/stylesheets/components/_user-avatar.scss b/scripts/app/assets/stylesheets/components/_user-avatar.scss new file mode 100644 index 0000000000000000000000000000000000000000..c8d3f383d961ec2fb7e68736262121d9bd32e50e --- /dev/null +++ b/scripts/app/assets/stylesheets/components/_user-avatar.scss @@ -0,0 +1,65 @@ +.user-avatar { + & { + background-color: $color-black-60; + background-size: contain; + border-radius: 100%; + height: 24px; + position: relative; + min-width: 24px; + width: 24px; + color: $color-white; + } + + > svg { + height: 100%; + width: 100%; + position: absolute; + top: 0; + left: 0; + } +} + +.student-details { + display: flex; + flex-flow: row nowrap; + align-items: center; + justify-content: flex-start; + + span { + margin-left: 10px; + } +} + +.submit-with-avatars .user-avatar { + border: 2px solid $color-primary-teal; + background: $color-white; + color: $color-primary-teal; + height: 28px; + width: 28px; + svg text { + font-weight: 500; + } +} + +.submit-with-avatars { + display: flex; + align-items: center; + + &.showing-many { + padding-left: 5px; + + .user-avatar:nth-of-type(1) { + margin-left: 5px; + z-index: 3; + } + .user-avatar:nth-of-type(2) { + z-index: 2; + left: -5px; + } + .user-avatar:nth-of-type(3) { + z-index: 1; + left: -10px; + } + } + +} diff --git a/scripts/app/assets/stylesheets/components/_video-player.scss b/scripts/app/assets/stylesheets/components/_video-player.scss new file mode 100644 index 0000000000000000000000000000000000000000..5ffe3003369699fdbda4d60ad4aab9cc3ffc48bd --- /dev/null +++ b/scripts/app/assets/stylesheets/components/_video-player.scss @@ -0,0 +1,10 @@ +// just additional styles +.fluidvids-item { + box-shadow: 0 0 0 1px $color-black-96-1; +} + +// width overwrite +.p3sdk-container > .p3sdk-interactive-transcript { + min-width: 100%; + max-width: 100%; +} diff --git a/scripts/app/assets/stylesheets/components/badge.scss b/scripts/app/assets/stylesheets/components/badge.scss new file mode 100644 index 0000000000000000000000000000000000000000..2a57f35adba22dd8f8b5df9b1b62b574affef791 --- /dev/null +++ b/scripts/app/assets/stylesheets/components/badge.scss @@ -0,0 +1,26 @@ +.lp-badge { + height: 20px; + min-width: 80px; + display: inline-flex; + align-items: center; + justify-content: center; + border-radius: 4px; + color: $color-white; + font-size: 10px; + font-weight: bold; + letter-spacing: 1px; + padding: 2px 8px; + text-transform: uppercase; + + &.-default { + background-color: $color-black-60-8; + } + + &.-info { + background-color: $color-cyan; + } + + &.-error { + background-color: $color-valencia; + } +} \ No newline at end of file diff --git a/scripts/app/assets/stylesheets/components/challenge-detail-comments.scss b/scripts/app/assets/stylesheets/components/challenge-detail-comments.scss new file mode 100644 index 0000000000000000000000000000000000000000..8cc5af601198df9452d075579fe76b1ce034db88 --- /dev/null +++ b/scripts/app/assets/stylesheets/components/challenge-detail-comments.scss @@ -0,0 +1,94 @@ +.challenge-detail-comments-wrapper { + & { + border-top: 1px solid $color-black-94-9; + padding: 24px; + } + + .label.gray-label { + padding-left: 0px; + } + + .challenge-detail-comments { + & { + padding-top: 15px; + margin-top: 8px; + } + + > div > .avatar-name-date { + display: flex; + flex-direction: row; + align-items: center; + } + + > div > .avatar-name-date > .avatar-wrapper { + height: 32px; + width: 32px; + margin-right: 8px; + } + + .comment-content { + background-color: $color-white; + border-radius: 10px; + margin: 10px 0px 16px 40px; + padding: 5px 10px; + } + + .comment-name { + font-weight: bold; + } + + .date { + font-size: 12px; + margin-left: auto; + color: $color-black-60-8; + } + + .item-wrapper { + position: relative; + } + + .item-wrapper:before, + .item-wrapper:after { + content: ""; + position: absolute; + z-index: -1; + top: 0; + left: 15px; + right: auto; + margin: auto 0; + width: 0; + height: calc(100% + 2rem); + border: 1px solid rgb(191, 191, 191); + } + + .item-wrapper:before { + bottom: 0; + } + + .item-wrapper:last-child:before { + top: -1rem; + bottom: auto; + height: calc(44px / 2 + 1rem); + } + + .item-wrapper:last-child:after { + display: block; + top: calc(44px / 2); + bottom: auto; + height: calc(100% + 1rem); + border-color: $color-black-97-3; + } + + .item-wrapper:first-child:before, + .item-wrapper:only-child:before { + top: auto; + bottom: -1rem; + height: 100%; + } + } + + .null-comments-block { + font-size: 16px; + font-style: italic; + } +} diff --git a/scripts/app/assets/stylesheets/components/challenge-detail-view.scss b/scripts/app/assets/stylesheets/components/challenge-detail-view.scss new file mode 100644 index 0000000000000000000000000000000000000000..8ae98b3faac5938e936dabfe39f7ade4801aa1db --- /dev/null +++ b/scripts/app/assets/stylesheets/components/challenge-detail-view.scss @@ -0,0 +1,38 @@ +.user-challenge-show { + .challenge { + padding-left: 28px; + } +} + +.comments-standards { + background-color: $color-black-97-3; + z-index: 0; + + .standard-card { + z-index: 2; + } + + .label { + flex: 1 1 100%; + height: 12px; + font-size: 12px; + font-weight: bold; + letter-spacing: 1px; + line-height: 12px; + text-transform: uppercase; + padding-left: 0; + + &.gray-label { + color: $color-black-60-8; + } + + &.cyan-label { + color: $color-cyan; + } + } +} + +[class*="col-"].no-gutter { + padding-right: 0; + padding-left: 0; +} diff --git a/scripts/app/assets/stylesheets/components/challenge-timeline.scss b/scripts/app/assets/stylesheets/components/challenge-timeline.scss new file mode 100644 index 0000000000000000000000000000000000000000..d7c3610edcb3af7868b73df33553c0a188408c36 --- /dev/null +++ b/scripts/app/assets/stylesheets/components/challenge-timeline.scss @@ -0,0 +1,69 @@ +.challenge-timeline { + background-color: $color-dark-cyan; + color: $color-white; + display: flex; + justify-content: center; + align-items: center; + + > .timestamp { + padding: 19px; + background-color: rgba(0,0,0,0.3); + font-size: 12px; + white-space: nowrap; + } + + > ul { + display: flex; + list-style: none; + flex-grow: 1; + height: 100%; + align-items: center; + margin: 0; + padding: 16px 0; + margin-right: 8px; + overflow-x: auto; + } + > ul > div > li { + height: 4px; + width: 4px; + background-color: white; + border-radius: 50%; + } + > ul > div { + display: flex; + justify-content: center; + align-items: center; + border-radius: 50%; + min-height: 14px; + min-width: 14px; + margin: 0 8px; + cursor: pointer; + cursor: hand; + } + > ul > div.highlight { + background-color: rgba(255,255,255,0.3); + } + + > .buttons { + height: 100%; + display: flex; + align-items: center; + padding: 16px; + border-left: 1px solid $color-white; + } + > .buttons > div { + align-items: center; + display: flex; + cursor: pointer; + cursor: hand; + } + > .buttons > div:first-of-type { + margin-right: 12px; + } + > .buttons > div > .svg.primary-color { + color: $color-white; + } + > .buttons > div.disabled > .svg.primary-color { + color: $color-cyan; + } +} diff --git a/scripts/app/assets/stylesheets/components/new-comment-form.scss b/scripts/app/assets/stylesheets/components/new-comment-form.scss new file mode 100644 index 0000000000000000000000000000000000000000..2c10acae4ed6e544700f98752e2f933b12559851 --- /dev/null +++ b/scripts/app/assets/stylesheets/components/new-comment-form.scss @@ -0,0 +1,94 @@ +.new-comment-form { + & { + border-top: 1px solid $color-black-94-9; + padding: 24px; + } + + textarea { + resize: none; + width: 100%; + border: 2px solid $color-black-92-2; + border-radius: 4px; + background-color: $color-white; + outline: none; + padding: 8px; + } + + textarea:focus { + box-shadow: 0 0 3px 0 $color-cyan; + border-color: $color-cyan; + } + + .actions { + & { + display: flex; + } + + a { + flex: 1 1 auto; + color: $color-black-51; + + &:hover { + color: $color-cyan; + text-decoration: none; + } + + > svg { + margin-top: 6px; + } + + > p { + font-size: 10px; + margin-left: 32px; + margin-top: -24px; + } + } + + span { + height: 16px; + width: 187px; + color: $color-black-60-8; + font-size: 12px; + line-height: 16px; + } + + .comment-preview-link { + display: inline-block; + flex: 0 0 auto; + padding: 12px 16px; + border: none; + font-size: 12px; + font-weight: bold; + letter-spacing: 1px; + text-transform: uppercase; + text-decoration: none; + color: $color-cyan; + cursor: pointer; + } + + .lp-style-button { + min-width: 80px; + } + } + + .comment-preview { + background-color: $color-white; + box-shadow: 0 2px 8px 0 rgba(0, 0, 0, 0.1); + padding: 16px; + margin-bottom: 8px; + + p { + margin: 0; + } + } +} + +.comment-and-reject { + background-color: $color-white; + color: $color-black-20; + box-shadow: 0 2px 8px 0 rgba(0, 0, 0, 0.3); + position: absolute; + min-width: 448px; + right: 6px; + z-index: 10; +} diff --git a/scripts/app/assets/stylesheets/components/partnerup.scss b/scripts/app/assets/stylesheets/components/partnerup.scss new file mode 100644 index 0000000000000000000000000000000000000000..1bc5f7f9d64ee4c4fb195095bf377da57cc44dae --- /dev/null +++ b/scripts/app/assets/stylesheets/components/partnerup.scss @@ -0,0 +1,249 @@ +.partnerup { + .empty-pairs { + margin-top: 1rem; + } + + max-width: 1200px; + display: flex; + justify-content: center; + flex-direction: column; + align-items: center; + margin: 0 auto; + + .actions { + width: 60%; + display: flex; + justify-content: space-around; + text-align: center; + border-bottom: 1px solid #ccc; + } + + .actions > .action-item{ + font-size: 18px; + line-height: 16px; + font-weight: 500; + padding: 16px 0; + width: 50%; + border-bottom: 4px solid transparent; + cursor: pointer; + } + + .actions > .action-item.active { + color: $color-cyan; + border-bottom: 4px solid $color-cyan; + } + + .past-pairings { + &.hide { display: none; } + + .pairing { + &:first-child { + margin-top: 30px; + } + + &:not(:first-child) { + margin-top: 70px; + } + + .pairing-header { + display: flex; + justify-content: space-between; + align-items: center; + width: 447px; + margin: 0 auto; + + .pairing-title { + overflow: hidden; + white-space: nowrap; + text-overflow: ellipsis; + } + } + + .pairing-groups { + display: flex; + flex-flow: column wrap; + + .pairing-edit { + cursor: pointer; + color: $color-cyan; + font-size: 0.9rem; + font-weight: 300; + } + } + + .group { + display: flex; + flex-wrap: wrap; + flex-direction: row; + border-bottom: 1px solid $color-black-83-1; + } + } + + .toggle-history { + display: flex; + justify-content:center; + align-items: center; + margin: 1rem 0; + } + } + + .student { + width: 12rem; + height: 3.5rem; + padding-right: 0.5rem; + display: flex; + flex-grow: 1; + flex-flow: row nowrap; + align-items: center; + + .user-avatar { + margin: 0 0.5rem; + } + + .name { + color: $color-darker-gray-blue; + font-size: 18px; + line-height: 24px; + overflow: hidden; + white-space: nowrap; + text-overflow: ellipsis; + } + + .student-actions { + margin-right: 0; + margin-left: auto; + + .fa { + cursor: pointer; + } + } + } + + .pairingform { + margin-top: 30px; + width: 100%; + + &.hide { display: none; } + + h3 { + text-align: center; + } + + input, select { + border: 2px solid #D4D4D4; + border-radius: 5px; + background-color: #FFFFFF; + padding: 8px; + height: 32px; + } + + .form { + display: flex; + flex-flow: column; + width: 600px; + margin: 0 auto; + + .form-inputs { + display: flex; + justify-content: space-around; + flex-flow: row; + } + + .form-group { + margin: 0 1rem; + } + } + + .form-buttons { + display: flex; + justify-content: space-around; + align-items: center; + margin: 1rem 0; + + button { + width: 5rem; + margin: 0 1rem; + + &:disabled { + cursor: not-allowed; + } + } + } + } + + .copy-to-clipboard-container { + display: flex; + align-items: center; + padding-left: 17px; + + .transition-group { + padding-bottom: 20px; + padding-left: 10px; + font-weight: 600; + } + + .copy-text { + position: absolute; + color: $color-galvanize-orange; + } + + .opacity-fade-enter { + opacity: 0.01; + } + + .opacity-fade-enter.opacity-fade-enter-active { + opacity: 1; + transition: opacity 400ms ease-in; + } + + .opacity-fade-leave { + opacity: 1; + } + + .opacity-fade-leave.opacity-fade-leave-active { + opacity: 0.01; + transition: opacity 400ms ease-in; + } + } + + .drag-area { + padding: 0.5rem; + background: $color-black-88-2; + + .student-list-wrapper { + display: flex; + flex-direction: row; + flex-wrap: wrap; + + .inner-student-list-wrapper { + width: 280px; + margin-right: 1rem; + padding: 0.5rem; + + .student { + box-shadow: 0 1px 4px 0 rgba(0, 0, 0, 0.2); + margin: 1rem 0; + background-color: #ffffff; + width: 100%; + + .fa-exclamation-triangle { + margin-left: 0.5rem; + color: $color-banana-yellow; + cursor: default; + } + } + } + } + } + + .lp-style-button.-outline.-small { + border: 1px solid transparent; + padding: 2px 10px; + color: $color-cyan; + + &:hover { + color: $color-white; + } + } +} + diff --git a/scripts/app/assets/stylesheets/components/progress-table.scss b/scripts/app/assets/stylesheets/components/progress-table.scss new file mode 100644 index 0000000000000000000000000000000000000000..015d8e5bf9a545b400336777686359e1f268baed --- /dev/null +++ b/scripts/app/assets/stylesheets/components/progress-table.scss @@ -0,0 +1,355 @@ +.progress-table { + & { + position: relative; + margin: 10px 32px; + } + + .column-header { + max-width: 78px; + overflow: hidden; + text-overflow: ellipsis + } + + .bold-font { + font-weight: bold; + } + + .export-container { + display: flex; + flex-flow: row nowrap; + position: relative; + padding-top: 10px; + } + + > .export-container > .lp-style-button { + position: relative; + color: white; + } + + > .export-container > .lp-style-button > img { + width: 20px; + height: 20px; + margin-right: 5px; + } + + > .heading { + border-bottom: 2px solid #d4d4d4; + position: relative; + } + + + > .heading > .progress-row { + display: flex; + flex-flow: row nowrap; + overflow: hidden; + margin-top: 25px; + + .cell.threshold.percentage:not(:first-child) { + margin: 0 5px 0 5px; + } + } + + // Disable scroll bar when horizontally scrolling + .heading .progress-row::-webkit-scrollbar { height: 0 !important } + .heading .progress-row { + -ms-overflow-style: none; + overflow: -moz-scrollbars-none; + } + + .progress-body .progress-row:not(:last-child)::-webkit-scrollbar { height: 0 !important } + .progress-body .progress-row:not(:last-child) { + -ms-overflow-style: none; + overflow: -moz-scrollbars-none; + } + + > .heading > .progress-row > .cell { + flex: 0 0 auto; + display: flex; + flex-flow: row nowrap; + align-items: center; + padding: 0 16px; + } + + > .heading > .progress-row > .cell.avg { + justify-content: center; + } + + > .heading > .progress-row > .cell > .label { + color: #9b9b9b; + font-size: 12px; + letter-spacing: 2px; + height: 24px; + line-height: 24px; + text-transform: uppercase; + display: flex; + flex-flow: row nowrap; + align-items: center; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + user-select: none; + + &.-is-sortable { + display: flex !important; + justify-content: center !important; + align-items: center !important; + + cursor: pointer; + + &:hover { + color: #000; + } + } + } + + > .heading > .progress-row > .cell.threshold { + display: flex; + flex-flow: row nowrap; + } + + > .heading > .progress-row > .cell.threshold > .label { + flex: 1 1 auto; + width: calc(100% / 3); + display: flex; + flex-flow: column; + align-items: center; + justify-content: flex-end; + height: 60px; + white-space: normal; + line-height: 1rem; + } + + > .heading > .progress-row > .cell.percentage > .label { + flex-flow: row; + justify-content: center; + align-items: flex-end; + } + + > .heading > .progress-row > .cell.threshold > .label > .score-circle { + margin-bottom: 8px; + } + + > .progress-body { + background: #e7e7e7; + box-shadow: 0 2px 8px 0 rgba(0, 0, 0, 0.1); + } + + > .progress-body > .progress-row { + display: flex; + flex-flow: row nowrap; + overflow: hidden; + height: 60px; + background: white; + border-bottom: 1px solid #e7e7e7; + } + + > .progress-body > .progress-row > .cell { + flex: 0 0 auto; + display: flex; + flex-flow: row nowrap; + align-items: center; + padding: 0 16px; + } + + .progress-body .progress-row .cell:not(:first-child) { + margin: 0 5px 0 5px; + } + + > .progress-body > .progress-row > .cell.avg { + flex: 0 0 auto; + } + + > .progress-body > .progress-row > .cell.mastery-progress, .percentage-progress { + flex: 1 1 auto; + display: flex; + flex-flow: column; + align-items: flex-start; + justify-content: center; + font-weight: 500; + color: #898989; + font-size: 20px; + } + + > .progress-body > .progress-row > .cell.mastery-progress > .label { + height: 16px; + color: #9b9b9b; + font-size: 12px; + line-height: 16px; + margin-top: 8px; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + } + + > .progress-body > .progress-row > .cell.threshold { + display: flex; + flex-flow: row nowrap; + } + + > .progress-body > .progress-row > .cell.threshold > .percent-box { + flex: 1 1 100%; + } + + > .progress-body > .progress-row > .cell.student { + display: flex; + flex-flow: row nowrap; + align-items: center; + font-size: 18px; + } + + .heading .progress-row .cell.student:not(:first-child) { + margin: 0 5px 0 5px; + } + + > .progress-body > .progress-row > .cell.student > .user-avatar { + flex: 0 0 auto; + padding-top: 0; + height: 32px; + width: 32px; + } + + a { + color: rgba(0, 165, 181, 1); + margin-left: 16px; + + &:hover { + color: rgba(1, 123, 134, 1); + } + } + + .avg-box { + color: white; + background: $color-black-20; + display: flex; + align-items: center; + justify-content: center; + height: 40px; + width: 100%; + min-width: 40px; + padding: 8px 16px; + font-size: 20px; + font-weight: bolder; + -webkit-user-select: none; + -moz-user-select: none; + -ms-user-select: none; + user-select: none; + } + + .percent-box { + $one-color: #dc3549; + $two-color: #4b5259; + $three-color: #9cca68; + color: #898989; + border: 1px solid; + background: white; + display: flex; + align-items: center; + justify-content: center; + height: 45px; + min-width: 56px; + padding: 8px; + font-size: 20px; + -webkit-user-select: none; + -moz-user-select: none; + -ms-user-select: none; + user-select: none; + border-radius: 5%/15%; + font-weight: 500; + + &:not(:last-of-type) { + margin-right: 8px; + } + + &.-is-one { + color: black; + border-color: rgba($one-color, 0.5); + background-color: white; + } + + &.-is-one.-is-filled { + color: white; + background-color: $one-color; + } + + &.-is-two { + color: black; + border-color: rgba($two-color, 0.5); + background-color: white; + } + + &.-is-two.-is-filled { + color: white; + background-color: $two-color; + } + + &.-is-three { + color: black; + border-color: rgba($three-color, 0.5); + background-color: white; + } + + &.-is-three.-is-filled { + color: white; + background-color: $three-color; + } + } + + .percentage-progressbar { + margin-left: 10px; + .progress-complete { + height: 10px; + background-color: #9A9A9A; + border-radius: 8px; + } + } + + .mastery-progressbar, .percentage-progressbar { + $one-color: #dc3549; + $two-color: #4b5259; + $three-color: #9cca68; + $bg-color: #efefef; + + & { + display: flex; + height: 10px; + width: 100%; + min-width: 100px; + border-radius: 8px; + overflow: hidden; + } + + > .one, + .two, + .three { + height: inherit; + border-right: 1px solid $bg-color; + } + + > .one { + background: $one-color; + } + + > .two { + background: $two-color; + } + + > .three { + background: $three-color; + } + } + + &.Percentage { + a { + color: $color-black-20; + } + + .percent-box { + min-width: 110px; + + &.percentage-mode { + border: 0px; + color: $color-black-30-2; + } + } + } +} diff --git a/scripts/app/assets/stylesheets/hopscotch-overrides.scss b/scripts/app/assets/stylesheets/hopscotch-overrides.scss new file mode 100644 index 0000000000000000000000000000000000000000..d9bc5c3d0e9d5d8d33f0599487e515fa98b24ce2 --- /dev/null +++ b/scripts/app/assets/stylesheets/hopscotch-overrides.scss @@ -0,0 +1,100 @@ +.hopscotch-bubble-number { + content: ""; + background-image: url(/assets/svg/mobile-logo-d01e8af51b97d9f642d52a826ffe07193b8290e4d1767f6f2b2475bac759e9bc.svg)!important; + background-size: 130%!important; + height: 80px!important; + margin-top: -11px!important; +} + +.hopscotch-bubble { + border: 0px!important; + border-radius: 6px!important; + background-color: #333!important; + box-shadow: 2px 2px 8px rgba(0,0,0,0.4)!important; +} + +.hopscotch-bubble-arrow-border { + visibility: hidden!important; +} + +.hopscotch-bubble-arrow-container.left .hopscotch-bubble-arrow { + border-right: 17px solid #333!important; +} + +.hopscotch-bubble-arrow-container.right .hopscotch-bubble-arrow { + border-left: 17px solid #333!important; +} + +.hopscotch-bubble-arrow-container.up .hopscotch-bubble-arrow { + border-bottom: 17px solid #333!important; +} + +.hopscotch-bubble-arrow-container.down .hopscotch-bubble-arrow { + border-top: 17px solid #333!important; +} + +.hopscotch-bubble-container { + width: 360px!important; + padding: 40px!important; +} + +.hopscotch-title { + font-size: 28px!important; + color: #eee!important; + font-family: "proxima-nova"!important; + line-height: 1!important; + margin-bottom: 20px!important; + margin-left: 15px!important; + letter-spacing: -0.75px!important; +} + +.hopscotch-content { + font-size: 18px!important; + color: #ccc!important; + font-family: "proxima-nova"!important; + font-weight: 300!important; + line-height: 1.3!important; + margin-left: 15px!important; +} + +.hopscotch-actions { + margin-top: 35px!important; + margin-bottom: 10px!important; + text-align: center!important; +} + +.hopscotch-nav-button { + $color-em: 8; + $color-height: ($color-em * 5 + px); + + & { + display: inline-block !important; + height: $color-height !important; + width: auto !important; + min-width: 120px !important; + padding: 0 ($color-em * 3 + px) !important; + border: none !important; + border-radius: 100px !important; + background-color: $color-cyan !important; + background-image: none !important; + cursor: pointer !important; + line-height: $color-height !important; + color: white !important; + font-size: ($color-em * 1.5 + px) !important; + font-weight: bold !important; + letter-spacing: 1px !important; + text-align: center !important; + text-transform: uppercase !important; + vertical-align: middle !important; + } + + &:focus { + outline: 0 !important; + } + + &:hover { + color: white !important; + background-color: #2396a0 !important; + text-decoration: none !important; + } +} diff --git a/scripts/app/assets/stylesheets/mixins.scss b/scripts/app/assets/stylesheets/mixins.scss new file mode 100644 index 0000000000000000000000000000000000000000..d06598ca0311edbc204536bc005d34ecab859097 --- /dev/null +++ b/scripts/app/assets/stylesheets/mixins.scss @@ -0,0 +1,197 @@ +// Fonts +// ===== + +@mixin font-base () { + color: $color-tundora; + font-size: 19px; + font-weight: 300; + line-height: 1.7; + text-align: left; + font-style: normal; +} + +@mixin font-small () { + @include font-base; + font-size: 15px; +} + +@mixin font-h1 () { + font-size: 48px; + font-weight: 300; + letter-spacing: -1px; + line-height: 1.1; +} + +@mixin font-h3 () { + font-size: 32px; + line-height: $font-default-line-height; + text-align: left; + color: $color-emperor; + font-style: normal; + font-weight: 300; + font-family: $font-famiy-system; +} + +@mixin font-h1-settings () { + font-size: 36px; + line-height: $font-default-line-height; + margin-bottom: 40px; + text-align: left; + color: $color-emperor; + font-style: normal; + font-weight: 300; + font-family: $font-famiy-system; +} + +@mixin font-h2-settings () { + font-size: 21px; + font-weight: 500; + color: $color-tundora; + margin-bottom: 15px; + font-family: $font-famiy-system; +} + +@mixin font-paragraph () { + font-size: 16px; + line-height: $font-default-line-height; + text-align: left; + color: $color-tundora; + font-style: normal; + font-weight: 400; + font-family: $font-famiy-system; +} + +@mixin font-caption () { + font-size: 14px; + line-height: $font-default-line-height; + text-align: left; + color: $color-gray; + font-style: normal; + font-weight: 500; + font-family: $font-famiy-system; + letter-spacing: 1.3px; + text-transform: uppercase; +} + +// Components +// =========== + +@mixin input () { + height: $input-height; + border-radius: $round-corners-1; + padding-left: $input-spacing; + padding-right: $input-spacing; + margin: $input-spacing; + background-color: $color-iron; + border: 1px solid $color-athens-gray; + @include font-paragraph; +} + +@mixin input-disabled () { + color: $color-boulder; +} + +@mixin modal-text-input () { + display: block; + width: 100%; + border: 1px solid #E0E0E0; + margin: 3px 0; + padding: 0 16px; + height: 45px; + border-radius: $round-corners-1; + box-shadow: inset 0px 1px 2px $color-inner-shadow; +} + +@mixin button { + display: inline-block; + border-radius: $round-corners-1; + padding: 5px 17px; + font-size: 17px; + font-weight: 400; + margin: 8px; + cursor: pointer; + white-space: nowrap; +}; + +@mixin button-theme ($color, $type: primary) { + @if $type == primary { + background-color: $color; + border: none; + color: $color-white; + } @else if $type == outline { + background-color: unset; + color: $color; + border: 1px solid $color; + } @else if $type == material { + background-color: inherit; + color: $color-primary-teal; + margin: 0; + font-weight: 500; + + &:hover { + background-color: rgba($color-primary-teal, 0.15); + } + + &:active { + background-color: rgba($color-primary-teal, 0.30); + } + } @else { + @error "Unknown button type #{$type}. Must be 'primary' or 'outline' or 'material'." + } +}; + +@mixin link-theme { + color: $color-tundora; + + &:hover { + color: $color-link-blue; + text-decoration: none; + } +} + +@mixin h5-material { + font-size: 20px; + font-weight: 500; + color: $color-tundora; + margin-bottom: 15px; + font-family: $font-famiy-system; +} + +@mixin h6-material { + @include h5-material; + font-size: 14px; + font-weight: 400; + font-family: $font-famiy-system; +} + + + +// Layouts +// ======= + +@mixin one-column () { + max-width: 1084px; + margin: 70px auto 0 auto; + padding: 0 60px; +} + +@mixin container-large () { + background-color: $color-white; + margin: 20px 0; + padding: 5px 30px 40px 30px; + border-radius: $round-corners-1; + box-shadow: $box-shadow-offset-x $box-shadow-offset-y $box-shadow-blur-radius $box-shadow-spread $color-box-shadow; +} + +@mixin list-container { + @include container-large; + padding: 0; + margin: 0; +} + +// styles + +@mixin row-hover () { + border-bottom: 1px solid #d0d0d0; + box-shadow: 0 0 10px 0 rgba(0,0,0,0.08); +} diff --git a/scripts/app/assets/stylesheets/mobile.scss b/scripts/app/assets/stylesheets/mobile.scss new file mode 100644 index 0000000000000000000000000000000000000000..5c336bc6a66b89bf6013253c3c41a5ae6590341c --- /dev/null +++ b/scripts/app/assets/stylesheets/mobile.scss @@ -0,0 +1 @@ +@import "mobile/*"; \ No newline at end of file diff --git a/scripts/app/assets/stylesheets/mobile/_submissions-dashboard-table.scss b/scripts/app/assets/stylesheets/mobile/_submissions-dashboard-table.scss new file mode 100644 index 0000000000000000000000000000000000000000..c93e515124677948a5ed74a5c35e61185cf7ff0d --- /dev/null +++ b/scripts/app/assets/stylesheets/mobile/_submissions-dashboard-table.scss @@ -0,0 +1,51 @@ +@media (max-width: 820px) { + .challengeindex-table > .curriculumpanel { + width: 83%; + } + + .challengeindex-table > .studentspanel > .studentcolumn > .standard > .emptyrow { + height: 80px; + background: linear-gradient(to bottom, #d4d4d4 50%,#d4d4d4 50%,#ebebeb 50%) + } + + .challengeindex-table > .curriculumpanel > .standard > .standardtitle { + flex-flow: column nowrap; + height: 80px; + align-items: left; + } + + .challengeindex-table > .curriculumpanel > .standard > .standardtitle > .challenge-table-label, + .challengeindex-table > .curriculumpanel > .standard > .standardtitle > .title { + width: 100%; + height: auto; + padding: 0 2rem; + } + + .challengeindex-table > .studentspanel.single-student > .studentcolumn > .standard > .standarditem > .itemheading, + .challengeindex-table > .studentspanel.single-student > .studentcolumn > .standard > .standarditem > .itembody { + justify-content: center; + } + + .submissions-dashboard-table-wrapper .flex-row { + flex-flow: column !important; // override bootstrap class... + } + + .dropdown-component { + width: 100%; + margin-bottom: 1rem; + } + + .icon-legend { + flex-wrap: wrap; + width: 100%; + margin-bottom: 1rem; + align-items: left; + height: auto; + + .item { + width: 50%; + margin: 0; + padding: .5rem 1rem; + } + } +} diff --git a/scripts/app/assets/stylesheets/typography.scss b/scripts/app/assets/stylesheets/typography.scss new file mode 100644 index 0000000000000000000000000000000000000000..5e69baa7fc0a69ebad72bd0b375e671447c4fab7 --- /dev/null +++ b/scripts/app/assets/stylesheets/typography.scss @@ -0,0 +1,5 @@ +body { + font-family: "proxima-nova", sans-serif; + font-size: 16px; + font-weight: 300; +} diff --git a/scripts/app/assets/stylesheets/variables.scss b/scripts/app/assets/stylesheets/variables.scss new file mode 100644 index 0000000000000000000000000000000000000000..6cfcbbbf09923c76797ca68393987a3d70aed7cc --- /dev/null +++ b/scripts/app/assets/stylesheets/variables.scss @@ -0,0 +1,136 @@ +// Colors (most names from http://chir.ag/projects/name-that-color) +// ================================================================ + +$lighten: 10%; +$darken: 10%; +$more-dark: 20%; +$more-light: 20%; + + +// Brand colors +$color-primary-teal: #4DA9B3; // was #34A8B3 +$color-primary-teal-lighter: lighten( $color-primary-teal, $lighten ); +$color-primary-teal-darker: darken( $color-primary-teal, $darken ); +$color-galvanize-orange-2: #ea8832; +$color-link-blue: #007BFF; + +// Functional colors +$color-white: #ffffff; +$color-black: #000000; +$color-red: #EB574C; +$color-red-lighter: lighten( $color-red, $lighten ); +$color-red-darker: darken( $color-red, $darken ); + +// grays +$color-mine-shaft: #333333; +$color-tundora: #444444; +$color-emperor: #505050; +$color-dove-gray: #666666; +$color-boulder: #777777; +$color-gray: #888888; +$color-silver-chalice: #AAAAAA; +$color-mercury: #E3E3E3; +$color-iron: #E3E4E6; +$color-athens-gray: #F2F3F5; +$color-alabaster: #FAFAFA; +$color-lightened-black: rgba($color-black, 0.6); +$color-light-outline: rgba($color-black, 0.07); +$color-box-shadow: rgba($color-black, 0.04); +$color-inner-shadow: rgba($color-black, 0.1); +$hover-box-shadow: 0 0 10px 0 rgba(0,0,0,0.08); + +// pill colors +$color-pill-light-purple: #E3E3FC; + + +// Fonts +// ===== + +$font-famiy-system: -apple-system, BlinkMacSystemFont, "Segoe UI", + "Roboto", "Oxygen", "Ubuntu", "Helvetica Neue", Arial, sans-serif; +$font-default-line-height: 28px; +$font-family-proxima-nova: "proxima-nova", sans-serif; + +// Layouts +// ======= + +$box-shadow-offset-x: 0; +$box-shadow-offset-y: 4px; +$box-shadow-blur-radius: 20px; +$box-shadow-spread: 0; +$round-corners-1: 4px; +$input-height: 40px; +$input-spacing: 8px; + +// Styles for pages that have not been updated yet +// =============================================== + +$color-dark-gray: #666666; + + +$color-black-10-2: #1A1A1A; +$color-black-14-9: #262626; +$color-black-20: #333333; +$color-black-30-2: #606060; +$color-black-46-7: #777777; +$color-black-50-2: #808080; +$color-black-51: #828282; +$color-black-60-8: #9b9b9b; +$color-black-60-9: #9f9f9f; +$color-black-60: #999999; +$color-black-66-7: #aaaaaa; +$color-black-70-2: #b3b3b3; +$color-black-76-5: #c3c3c3; +$color-black-77: #c6c6c6; +$color-black-80: #cccccc; +$color-black-83-1: #d4d4d4; +$color-black-84-7: #d8d8d8; +$color-black-88-2: #e1e1e1; +$color-black-90-2: #e6e6e6; +$color-black-92-2: #ebebeb; +$color-black-93-3: #eeeeee; // this is gallery "color" from the link above +$color-black-94-2: #efefef; +$color-black-94-3: #e5e5e5; +$color-black-94-9: #f2f2f2; +$color-black-96-1: #f5f5f5; +$color-black-97-3: #f8f8f8; +$color-black-98-2: #fafafa; +$color-black-99-3: #f0f0f0; + +$color-galvanize-orange: #ff901e; +$color-valencia: #dc3549; +$color-med-green: #9cca68; + +$color-sky-blue: #4A90E2; +$color-cyan: #34A8B3; +$color-cyan-opacity-4: #F2FBFB; +$color-dark-cyan: #00737E; +$color-navy-blue: #0C5773; +$color-cyan-very-light: #d2e9ec; + +$color-medium-blue-gray: #3a4352; +$color-dark-blue-gray: #272e39; +$color-darker-gray-blue: #4b5259; +$color-darkest-blue-gray: #3B4248; +$color-darkester-blue-gray: #262A2E; + +// new forge colors +$color-lime-green: #7DCA60; +$color-banana-yellow: #FEC449; +$color-tangerine-orange: #FC7336; + +// old mastery table colors +$color-light-gray-blue: #c6d7f4; +$color-pale-yellow: #f4efd4; +$color-soft-orange: #fce8b2; +$color-soft-red: #f4c7c3; +$color-sage: #b7e1cd; +$color-blue-dark: #33485f; +$color-too-gray: #b6b8ba; +$color-gray-light: #e9eaec; + +// breakpoints +$tablet-breakpoint: 1024px; + +// z-index +$z-primary-navigation: 999999; diff --git a/scripts/app/component_props/activity_feed_item_component_props.rb b/scripts/app/component_props/activity_feed_item_component_props.rb new file mode 100644 index 0000000000000000000000000000000000000000..596ab3bfe07ac2f87848e236f9c6d030a70921b9 --- /dev/null +++ b/scripts/app/component_props/activity_feed_item_component_props.rb @@ -0,0 +1,120 @@ +class ActivityFeedItemComponentProps + def self.execute(activity) + { + id: activity.id, + creator: actor_from_activity(activity).full_name, + message: message_from_activity(activity), + created_at: activity.created_at, + label_class: label_class_from_activity(activity), + user_photo: actor_from_activity(activity).profile_image, + user_initials: actor_from_activity(activity).initials + } + end + + private_class_method def self.actor_from_activity(activity) + activity.creator.presence || activity.subject.user + end + + private_class_method def self.label_class_from_activity(activity) + case activity.name + when Activity::NAMES[:checkpoint_submission_evaluated] + performance = Performance.find_by(checkpoint_submission_id: activity.subject_id) + + case performance.score + when 1 + "-score-1" + when 2 + "-score-2" + when 3 + "-score-3" + end + when Activity::NAMES[:checkpoint_submission_created], + Activity::NAMES[:checkpoint_submission_resubmitted] + if activity.subject.submitted_challenge_answers.first.challenge.content_file.autoscore? + "-primary" + else + "-warning" + end + when Activity::NAMES[:comment_created] + "-primary" + when Activity::NAMES[:content_file_viewed] + "-default" + when Activity::NAMES[:submitted_challenge_answer_created], + Activity::NAMES[:submitted_challenge_answer_created_project], + Activity::NAMES[:submitted_challenge_answer_created_code_snippet], + Activity::NAMES[:submitted_challenge_answer_evaluated] + if activity.subject.correct? + "-success" + elsif activity.subject.incorrect? + "-danger" + elsif activity.subject.processing? + "-primary" + else + "-warning" + end + when Activity::NAMES[:video_started] + "-primary" + when Activity::NAMES[:video_ended] + "-success" + end + end + + private_class_method def self.message_from_activity(activity) + case activity.name + when Activity::NAMES[:checkpoint_submission_evaluated] + performance = Performance.find_by(checkpoint_submission_id: activity.subject_id) + content_file = activity.subject.submitted_challenge_answers.first.challenge.content_file + + "scored a #{performance.score} on #{content_file_label(content_file)}." + when Activity::NAMES[:checkpoint_submission_created], + Activity::NAMES[:checkpoint_submission_resubmitted] + + action = activity.name == Activity::NAMES[:checkpoint_submission_resubmitted] ? "resubmitted" : "submitted" + content_file = activity.subject.submitted_challenge_answers.first.challenge.content_file + + suffix = + if content_file.autoscore? + "awaiting automatic evaluation" + else + "awaiting manual scoring" + end + + "#{action} #{content_file_label(content_file)}, #{suffix}." + when Activity::NAMES[:comment_created] + "left a comment." + when Activity::NAMES[:content_file_viewed] + "viewed #{content_file_label(activity.subject)}." + when Activity::NAMES[:submitted_challenge_answer_created], + Activity::NAMES[:submitted_challenge_answer_created_project], + Activity::NAMES[:submitted_challenge_answer_created_code_snippet], + Activity::NAMES[:submitted_challenge_answer_evaluated] + + content_file = activity.subject.challenge.content_file + title = activity.subject.challenge.title + + "answered #{title} - #{content_file_label(content_file)}#{submitted_challenge_answer_suffix(activity.subject)}." + when Activity::NAMES[:video_started] + "started a video." + when Activity::NAMES[:video_ended] + "finished a video." + end + end + + private_class_method def self.content_file_label(content_file) + "#{content_file.standard.release.block.title} - #{content_file.standard.title} - #{content_file.title}" + end + + private_class_method def self.submitted_challenge_answer_suffix(submitted_challenge_answer) + if submitted_challenge_answer.correct? + " correctly" + elsif submitted_challenge_answer.incorrect? + " incorrectly" + elsif submitted_challenge_answer.ungraded? + ", awaiting manual scoring" + elsif submitted_challenge_answer.processing? + ", awaiting automatic evaluation" + else + " but ran into an error: #{submitted_challenge_answer.status}" + end + end +end diff --git a/scripts/app/component_props/notifications_component_props.rb b/scripts/app/component_props/notifications_component_props.rb new file mode 100644 index 0000000000000000000000000000000000000000..21dd11c9a88f99fd3f20bd6a87ffb5274e23bb53 --- /dev/null +++ b/scripts/app/component_props/notifications_component_props.rb @@ -0,0 +1,22 @@ +class NotificationsComponentProps + def self.execute(user_id) + notifications = NotificationPolicy::Scope.new(User.find(user_id), Notification).resolve + unread_count = notifications.unread.count + + { + unreadCount: unread_count, + prettyUnreadCount: unread_count > 99 ? "99+" : unread_count, + notifications: notifications.order(read_at: :desc, created_at: :desc).map do |notification| + { + id: notification.id, + tagline: notification.tagline, + title: notification.title, + url: Rails.application.routes.url_helpers.notification_path(notification), + description: notification.description, + read_at: notification.read_at, + created_at: notification.created_at + } + end + } + end +end diff --git a/scripts/app/component_props/standard_card_component_props.rb b/scripts/app/component_props/standard_card_component_props.rb new file mode 100644 index 0000000000000000000000000000000000000000..4ab4c68e2c35a4da9d320c3ce08c00b4ceb7697e --- /dev/null +++ b/scripts/app/component_props/standard_card_component_props.rb @@ -0,0 +1,9 @@ +class StandardCardComponentProps + attr_reader :standard, + :checkpoint_performance + + def initialize(standard:, latest_performance: nil, checkpoint_performance: nil) + @standard = StandardPresenter::ForCheckpointSubmission.new(standard: standard, latest_performance: latest_performance) + @checkpoint_performance = checkpoint_performance ? PerformancePresenter.new(checkpoint_performance) : nil + end +end diff --git a/scripts/app/controllers/api/application_controller.rb b/scripts/app/controllers/api/application_controller.rb new file mode 100644 index 0000000000000000000000000000000000000000..8c6e56e38e8b0e347abbe59cd2949a531ec107d6 --- /dev/null +++ b/scripts/app/controllers/api/application_controller.rb @@ -0,0 +1,114 @@ +class Api::ApplicationController < ActionController::API + include Pundit + include Api::ExceptionHandler + include Api::Response + + before_action :ensure_json_request + before_action :require_authorized_user + + around_action :monitor_api + + rescue_from ActionController::ParameterMissing do |exception| + raise exception if Rails.env.development? + + json_response( + { + "errors": { + "status": "400", + "title": "A parameter is missing to fulfill your request", + "code": "bad_request" + } + }, 400 + ) + end + + def require_authorized_user + unless current_user + json_response( + { + "errors": { + "status": "401", + "title": "You are not authenticated for this request", + "code": "unauthorized" + } + }, :unauthorized + ) + end + end + + def current_user + @current_user ||= if request.headers["X-LEARN-API-TOKEN"] + User.find_by(api_token: bearer_token) + else + User.find_by(uid: session[:user_uid]) + end + end + + def current_cohort + @current_cohort ||= if params[:cohort_id].present? + Cohort.find(params[:cohort_id]) + end + end + + def preview_block + @preview_block ||= Block.find_by(title: "preview") + end + + def bearer_token + pattern = /^Bearer / + header = request.headers["X-LEARN-API-TOKEN"] + header.gsub(pattern, "") if header&.match(pattern) + end + + def ensure_json_request + return if request.format == :json + + render body: nil, status: 406 + end + + def not_found_or_invalid_version + # does this path exist then 405 + # does this path not exist then 404 + match = request.path.match(/^\/api\/(v(?[1])?)/i) # look for a valid version + error = match && match[:version] ? :not_found : :gone + + json_response( + { + "errors": { + "status": error == :not_found ? 404 : 410, + "title": error == :not_found ? "The requested resource could not be found" : "The requested API version no longer exists", + "code": error == :not_found ? "not_found" : "gone" + } + }, + error + ) + end + + # monitor_api wraps all calls to our Api::ApplicationController and records + # data. Set @api_interaction_metadata anywhere in an api controller action + # to populate additional custom metadata for an api_interaction + def monitor_api + start = Time.now + + yield + + finish = Time.now + duration = duration_in_millseconds(start, finish) + + CreateApiInteractionJob.perform_later( + user_id: current_user&.id, + ip: request.remote_ip, + path: request.path, + method: request.method, + action_name: action_name, + duration: duration, + controller_name: controller_name, + response_code: response.code.to_i, + metadata: @api_interaction_metadata + ) + end + + def duration_in_millseconds(start, finish) + (finish - start) * 1000.0 + end +end diff --git a/scripts/app/controllers/api/v1/blocks/releases_controller.rb b/scripts/app/controllers/api/v1/blocks/releases_controller.rb new file mode 100644 index 0000000000000000000000000000000000000000..d3d8171fc27a10f054ebfc03a2454d94636d9084 --- /dev/null +++ b/scripts/app/controllers/api/v1/blocks/releases_controller.rb @@ -0,0 +1,28 @@ +class Api::V1::Blocks::ReleasesController < Api::ApplicationController + def create + render_method_not_allowed_response and return unless request.post? + authorize(Release) + + pending_release = Release.create(block_id: current_block.id, + notes: release_notes || "", + github_sha: "pending", + user_id: current_user.id, + state: Release::STATES[:pending]) + + CreateReleaseJob.perform_later(pending_release_id: pending_release.id) if pending_release.id + + json_response({ release_id: pending_release.id }, pending_release.id ? :ok : :bad_request) + end + + private + + def current_block + @current_block ||= Block.find(params[:block_id]) + end + + def release_notes + if params[:release] + params[:release][:notes] + end + end +end diff --git a/scripts/app/controllers/api/v1/blocks_controller.rb b/scripts/app/controllers/api/v1/blocks_controller.rb new file mode 100644 index 0000000000000000000000000000000000000000..d35e50724eb085b3a226f762f91dc9ee87dfc923 --- /dev/null +++ b/scripts/app/controllers/api/v1/blocks_controller.rb @@ -0,0 +1,52 @@ +class Api::V1::BlocksController < Api::ApplicationController + def index + render_method_not_allowed_response and return unless request.get? + authorize(Block) + if params[:repo_name].present? + block = Block.includes(releases: :cohort_releases).where("repo_name ilike ?", params[:repo_name]) + if params[:org].present? && params[:origin].present? + block = block.where("org ilike ? AND origin ilike ?", params[:org], params[:origin]) + end + render json: { blocks: block.empty? ? [] : [block_details(block.first)] } + else + blocks = Block.includes(releases: :cohort_releases).order(title: :asc) + render json: { blocks: blocks.map {|b| block_details(b) } } + end + end + + def create + render_method_not_allowed_response and return unless request.post? + authorize(Block) + + block = Block.new(block_params) + + block.title = block.repo_name if block.title.blank? + + if block.save + json_response({ blocks: [block] }, :ok) + else + json_response( + { errors: { status: 400, title: "errors: #{block.errors.full_messages.join(', ')}" } }, + :bad_request + ) + end + end + + private + + def block_params + params.require(:block).permit(:title, :repo_name, :org, :origin) + end + + def block_details(block) + { + id: block.id, + origin: block.origin, + org: block.org, + repo_name: block.repo_name, + sync_errors: block.sync_errors, + title: block.title, + cohorts_using: block.releases.reduce([]) { |m, r| m + r.cohort_releases.map(&:cohort_id) }.compact + } + end +end diff --git a/scripts/app/controllers/api/v1/cohorts/blocks/content_files_controller.rb b/scripts/app/controllers/api/v1/cohorts/blocks/content_files_controller.rb new file mode 100644 index 0000000000000000000000000000000000000000..33d9791e9b3d9826a294268807051552b7677628 --- /dev/null +++ b/scripts/app/controllers/api/v1/cohorts/blocks/content_files_controller.rb @@ -0,0 +1,218 @@ +class Api::V1::Cohorts::Blocks::ContentFilesController < Api::ApplicationController + include Api::ContentVisibilityCrud + + def update + render_method_not_allowed_response and return unless request.patch? + + authorize(Cohort.find(params[:cohort_id]), :toggle_content_visibility?) + + result = handle_param_logic("ContentFile", visibility_type_params) + + json_response({ status: result.to_s }, result) + end + + def take_assessment + render_method_not_allowed_response and return unless request.post? + authorize(current_cohort, :take_assessment?) + + unless current_content_file.checkpoint? + json_response({ error: "only checkpoints can be assessed" }, 403) and return + end + + submission = CheckpointSubmissionFinder.latest_for_checkpoint_content_file_for_user_in_cohort( + cohort_id: current_cohort.id, + content_file: current_content_file, + user_id: current_user.id, + with_started: true + ) + + if submission.nil? || submission.state != CheckpointSubmission::STATES[:started] + # if the state was started, we don't need to check for limit reach + if user_reached_attempt_limit? + json_response(limit_reached_response, :ok) and return + end + + submission = CheckpointSubmission.create!( + cohort_id: current_cohort.id, + content_file_uid: current_content_file.uid, + content_file_block_id: params[:block_id], + user_id: current_user.id, + state: CheckpointSubmission::STATES[:started] + ) + if current_content_file.time_limit.present? + GradeTimedCheckpointJob.set(wait: current_content_file.time_limit.minutes).perform_later(current_content_file.id, submission.id) + end + end + + json_response({ + content_file_html: newer_current_content_file.html, + submission_id: submission&.id, + challenges_with_answers: challenges_with_answers, + created_at: submission&.created_at, + limit_reached: false, + time_limit: newer_current_content_file.time_limit + }, :ok) + end + + def student_scores + render_method_not_allowed_response and return unless request.get? + authorize(current_cohort, :student_scores?) + if current_cohort.students.count < 1000 + students = current_cohort.students + + checkpoint_submissions_for_students = CheckpointSubmissionFinder.lastest_by_state_for_students_in_cohort( + cohort_id: current_cohort.id, + block_id: params[:block_id], + content_file_uid: current_content_file.uid, + student_ids: students.map(&:id) + ).includes(submitted_challenge_answers: :challenge).group_by(&:user_id) + + resp = formatted_student_scores(students, checkpoint_submissions_for_students) + + json_response(resp, :ok) + else + json_response({ student_limit_reached: true }, :ok) + end + end + + private + + def formatted_student_scores(students, submissions) + students.map do |student| + { + id: student.id, + auth_edit_url: student.auth_edit_url, + email: student.email, + initials: student.initials, + first_name: student.first_name, + last_name: student.last_name, + profile_image: student.profile_image, + has_signed_in: student.signed_in?, + submissions: submissions[student.id].nil? ? {} : format_submissions(submissions[student.id]) + } + end + end + + def format_submissions(submissions) + started = submissions.find { |s| s.state == "started"} + needs_review = submissions.find { |s| s.state == "needs_review"} + done = submissions.find { |s| s.state == "done"} + retry_sub = submissions.find { |s| s.state == "retry"} + + ultimate_done = [done, retry_sub].compact.sort {|a, b| b.created_at <=> a.created_at}&.first + submissions = [ultimate_done, needs_review].compact.sort {|a, b| b.created_at <=> a.created_at} + + # Original implementation when 'started' was present in set + # May revisit this when add time feature is built + # [] => nil, nil + # [done] => done score, done state + # [needs_review] => needs review suggest?, needs review state + # [started, needs_review] => needs review suggest?, started state + # [needs_review, done] => done score, scored-pending state + # [done, needs_review] => done score, done state + # [started, done] => done score, started state + # [started, needs_review, done] => done score, started state + # + # Changed to: + # [] => nil, nil + # [done] => done score, done state + # [needs_review] => needs review suggest?, needs review state + # [done, needs_review] => done score, done state + # [needs_review, done] => done score, needs review and done state + + score = nil + if ultimate_done + score = ultimate_done.percent_score + elsif needs_review # no done or retry, see if score is suggested + # if any submitted challenge answer has nil points, we cannot suggest a score + all_points = needs_review.submitted_challenge_answers.map(&:points) + unless all_points.include?(nil) + suggested_points = all_points.reduce(:+) + total_points = needs_review.submitted_challenge_answers.map(&:challenge).map(&:points).reduce(:+) + score = suggested_points && total_points ? ((suggested_points.to_f / total_points.to_f) * 100).to_i : 0 + end + end + + { + needs_review_id: needs_review&.id, + done_id: ultimate_done&.id, + started_id: started&.id, + score: score, + state: submssion_state(started, submissions), + created_at: (submissions[0] || started)&.created_at + } + end + + def submssion_state(started, submissions) + if submissions.length == 2 && submissions[0].state == "needs_review" && submissions[1].state == "done" + "needs_review_and_done" + elsif submissions.empty? && started + "started" + else + submissions[0]&.state + end + end + + def user_reached_attempt_limit? + return false if current_content_file.max_checkpoint_submissions.nil? + + user_attempts = current_user.checkpoint_submissions_count( + current_cohort.id, + current_content_file.standard.release.block_id, + current_content_file.uid + ) + + if user_attempts >= current_content_file.max_checkpoint_submissions + return true + end + + false + end + + def limit_reached_response + { + content_file_html: current_content_file.html, + submission_id: nil, + challenges_with_answers: challenges_with_answers, + limit_reached: true + } + end + + def current_content_file + @current_content_file ||= ContentFile.find(params[:id]) + end + + def newer_current_content_file + @newer_current_content_file ||= ContentFileFinder.from_cohort_block_content_file_path(cohort_id: current_cohort.id, + block_id: params[:block_id], + content_file_path: current_content_file.path) + end + + def challenges_with_answers + challenges = Challenge.where(content_file: current_content_file).order(position: :desc) + all_submitted_challenge_answers = SubmittedChallengeAnswerFinder.latest_for_user_id_content_file_for_cohort( + current_user.id, + current_content_file, + current_cohort.id + ) + challenges.map do |challenge| + submitted_challenge_answers = all_submitted_challenge_answers.select do |sca| + sca.challenge.uid == challenge.uid && sca.answered? + end.flatten.compact + ChallengeWithSubmittedChallengeAnswersPresenter.new( + current_cohort, + challenge, + submitted_challenge_answers, + current_user.instructor_or_admin?(current_cohort.id), + user_context: current_user, + is_checkpoint: true, + can_view_status: false, + can_view_explanation: false + ) + end + end + + def visibility_type_params + params.permit(:visible, :optional) + end +end diff --git a/scripts/app/controllers/api/v1/cohorts/blocks/units_controller.rb b/scripts/app/controllers/api/v1/cohorts/blocks/units_controller.rb new file mode 100644 index 0000000000000000000000000000000000000000..68c45a0d32b97d66c4da0c62538fe2dce2deea4a --- /dev/null +++ b/scripts/app/controllers/api/v1/cohorts/blocks/units_controller.rb @@ -0,0 +1,19 @@ +class Api::V1::Cohorts::Blocks::UnitsController < Api::ApplicationController + include Api::ContentVisibilityCrud + + def update + render_method_not_allowed_response and return unless request.patch? + + authorize(Cohort.find(params[:cohort_id]), :toggle_content_visibility?) + + result = handle_param_logic("Standard", visibility_type_params) + + json_response({ status: result.to_s }, result) + end + + private + + def visibility_type_params + params.permit(:visible, :optional) + end +end diff --git a/scripts/app/controllers/api/v1/cohorts/cohorts_controller.rb b/scripts/app/controllers/api/v1/cohorts/cohorts_controller.rb new file mode 100644 index 0000000000000000000000000000000000000000..93285bb60bd63ce14c44b1ec5c35088182d45233 --- /dev/null +++ b/scripts/app/controllers/api/v1/cohorts/cohorts_controller.rb @@ -0,0 +1,119 @@ +class Api::V1::Cohorts::CohortsController < Api::ApplicationController + def create + render_method_not_allowed_response and return unless request.post? + authorize(current_user, :onboarder?) + + if param_errors.any? + json_response({ error: "Validation Error: #{param_errors.join(', ')}" }, 400) and return + end + + # TODO: AuthApi fix + # client = AuthApi::Client.new("product.write") + # + # resp = client.create_product(formatted_cohort) + # AuthResolverService.resolve(resp) if resp&.uid + + json_response({ status: "ok", uid: 'todo' }, :ok) and return + rescue StandardError => e + json_response({ error: JSON.parse(e.message) }, 400) and return + end + + def curriculum + render_method_not_allowed_response and return unless request.get? + + authorize(current_cohort, :api_documentation?) + + @cohort_releases_by_sections, @visibilities = current_cohort.current_curriculum + + json_response({ + cohort_title: current_cohort.name, + cohort_id: current_cohort.id, + course_yaml_url: ResyncJobResult.find_latest(current_cohort.id)&.data&.dig("course_config_url"), + sections: build_sections + }, :ok) + end + + private + + def formatted_cohort + { + product: { + uid: SecureRandom.hex[0...18], + name: params[:name], + product_type: params[:product_type], + label: params[:label], + campus_name: params[:campus_name], + opt_out_for_marketing: params[:opt_out_for_marketing].nil? ? false : params[:opt_out_for_marketing], + starts_on: params[:starts_on], + ends_on: params[:ends_on] + } + } + end + + def param_errors + validation_response = [] + + if params[:name].blank? + validation_response.push "name can't be blank" + end + if params[:starts_on].blank? + validation_response.push "starts_on can't be blank" + elsif /[0-9]{4}-[0-9]{2}-[0-9]{2}/.match(params[:starts_on]).nil? + validation_response.push "starts_on must be in format yyyy-mm-dd" + end + if params[:product_type].blank? + validation_response.push "product_type can't be blank" + end + if params[:opt_out_for_marketing] && !([true, false].include? params[:opt_out_for_marketing]) + validation_response.push "opt_out_for_marketing must be true or false" + end + validation_response + end + + def build_sections + @cohort_releases_by_sections.map do |section, _v| + { + title: section.title, + units: build_units(section) + } + end + end + + def build_units(section) + @cohort_releases_by_sections[section].map do |r| + StandardFinder.for_cohort_release(cohort_id: current_cohort.id, release_id: r.release_id) + end.flatten.map do |standard| + { + uid: standard.uid, + title: standard.title, + description: standard.description, + visible: @visibilities["Standard"][standard.uid].nil? ? true : !@visibilities["Standard"][standard.uid].hidden?, + block_id: standard.release.block_id, + api_endpoint: api_v1_cohort_block_units_url(current_cohort.id, standard.release.block_id, standard.uid), + content_files: standard.content_files.unscoped.where(standard_id: standard.id).in_default_order.map do |cf| + { + uid: cf.uid, + title: cf.title, + path: content_file_path(current_cohort.id, standard.release.block_id, cf.path), + visible: @visibilities["ContentFile"][cf.uid].nil? ? true : !@visibilities["ContentFile"][cf.uid].hidden?, + git_url: git_url(cf), + type: cf.content_file_type, + api_endpoint: api_v1_cohort_block_content_files_url(current_cohort.id, standard.release.block_id, cf.uid) + } + end + } + end + end + + def git_url(content_file) + release = content_file.standard.release + block = release.block + base_url = block.git_url + branch = release.branch_name + "#{base_url}/blob/#{branch}/#{content_file.path}" + end + + def current_cohort + @current_cohort ||= Cohort.find(params[:cohort_id]) + end +end diff --git a/scripts/app/controllers/api/v1/cohorts/users_controller.rb b/scripts/app/controllers/api/v1/cohorts/users_controller.rb new file mode 100644 index 0000000000000000000000000000000000000000..9dc65df441992f32cbacab934f0ef3636af17d85 --- /dev/null +++ b/scripts/app/controllers/api/v1/cohorts/users_controller.rb @@ -0,0 +1,95 @@ +class Api::V1::Cohorts::UsersController < Api::ApplicationController + def index + render_method_not_allowed_response and return unless request.get? + authorize(current_cohort, :api_documentation?) + + json_response(formatted_cohort_users, :ok) + end + + def create + render_method_not_allowed_response and return unless request.post? + authorize(current_cohort, :enroll?) + + if param_errors.any? + json_response({ error: "Validation Error: #{param_errors.join(', ')}" }, 400) and return + end + + user = find_or_create_user(params[:email]) + + if user.errors.full_messages.any? + json_response({ error: "Validation Error: #{user.errors.full_messages.join(', ')}" }, 400) and return + end + + if add_user_to_cohort(user, current_cohort) + json_response({ status: "ok" }, :ok) and return + else + json_response({ error: "User is already in the cohort." }, 400) and return + end + + rescue StandardError => e + json_response({ error: e.message }, 400) and return + end + + private + + def current_cohort + @current_cohort ||= Cohort.find(params[:cohort_id]) + end + + def target_user + @target_user ||= User.find(params[:id]) + end + + def user_from_email + @user_from_email ||= User.find_by(email: params[:email]) + end + + def user_enrolled_in_cohort? + user_from_email.student_or_instructor_of?(current_cohort.id) + end + + def formatted_cohort_users + User.select("users.id, users.uid, users.first_name, users.last_name, users.email, cohort_users.roles"). + joins(:cohort_users). + where("cohort_users.cohort_id = ?", current_cohort.id). + order("cohort_users.created_at DESC").map do |user| + { + id: user.id, + uid: user.uid, + first_name: user.first_name, + last_name: user.last_name, + email: user.email, + roles: user.roles + } + end + end + + def param_errors + validation_response = [] + + if params[:email].blank? + validation_response.push "email can't be blank" + end + validation_response + end + + def find_or_create_user(email) + user = User.find_by(email: email) + + if user + user + else + user = User.create(email: email, + roles: []) + + user + end + end + + def add_user_to_cohort(user, cohort) + cohort_user = CohortUser.find_by(cohort_id: cohort.id, user_id: user.id) + if cohort_user.nil? + CohortUser.create(user: user, cohort:cohort) + end + end +end diff --git a/scripts/app/controllers/api/v1/content_files_controller.rb b/scripts/app/controllers/api/v1/content_files_controller.rb new file mode 100644 index 0000000000000000000000000000000000000000..7fe82d58b6efad876d9626b6b38840996d6a8e35 --- /dev/null +++ b/scripts/app/controllers/api/v1/content_files_controller.rb @@ -0,0 +1,34 @@ +class Api::V1::ContentFilesController < Api::ApplicationController + skip_around_action :monitor_api, only: [:show] + + def show + authorize(current_user, :content_file_api?) + + content_file = ContentFile.unscoped.find(params[:id]) + + render json: content_file + end + + def create + render_method_not_allowed_response and return unless request.post? + + authorize(current_user, :content_file_api?) + + outcome = PreviewContentFileService.new(sandbox_id: current_user.sandbox.id, + s3_key: create_params[:s3_key]).execute + + if outcome[:content_file] + render json: { preview_url: content_file_url(current_user.sandbox.id, + preview_block.id, + outcome[:content_file].path) }, status: :ok + elsif outcome[:errors] + render json: { errors: outcome[:errors].join(", ") }, status: :unprocessable_entity + end + end + + private + + def create_params + params.permit(:s3_key) + end +end diff --git a/scripts/app/controllers/api/v1/pineapple_controller.rb b/scripts/app/controllers/api/v1/pineapple_controller.rb new file mode 100644 index 0000000000000000000000000000000000000000..4ae3969764c4906053c57e7dc941d499597edf20 --- /dev/null +++ b/scripts/app/controllers/api/v1/pineapple_controller.rb @@ -0,0 +1,6 @@ +class Api::V1::PineappleController < Api::ApplicationController + def pineapple + render_method_not_allowed_response and return unless request.get? + json_response(this_is_for_you: "\u{1f34d}") + end +end diff --git a/scripts/app/controllers/api/v1/releases_controller.rb b/scripts/app/controllers/api/v1/releases_controller.rb new file mode 100644 index 0000000000000000000000000000000000000000..2e62d2b399c862d1f1a5d3cb6b156dc781dc7398 --- /dev/null +++ b/scripts/app/controllers/api/v1/releases_controller.rb @@ -0,0 +1,70 @@ +class Api::V1::ReleasesController < Api::ApplicationController + def create + authorize(current_user, :regenerate_api_token?) + + release = Release.new( + s3_key: release_params[:s3_key], + github_sha: "preview", + branch_name: "preview", + block_id: Block.find_or_create_by(title: "preview").id, + preview_cohort_id: current_user.sandbox.id, + state: Release::STATES[:pending] + ) + + if release.save + render json: { release_id: release.id }, status: :ok + else + render json: { errors: release.errors.full_messages.join(", ") }, status: :internal_server_error + end + end + + def release_polling + authorize(current_user, :release_polling?) + + release = Release.find_by(id: release_params[:release_id]) + + if release.nil? + render json: { errors: "Could not find the release requested", sync_warnings: "" }, status: :unprocessable_entity + elsif release.state == Release::STATES[:success] + preview_url = nil + + if release.preview_cohort_id.present? + cf_count = ContentFileFinder.all_from_cohort_block(cohort_id: current_user.sandbox.id, + block_id: Block.find_by(title: "preview").id) + + preview_url = if cf_count.size == 1 + release.update(sync_warnings: release.sync_warnings.reject { |warning| warning.include?("link") }.compact) + cf_no_link_warnings = cf_count.first.dup + cf_no_link_warnings.warnings = cf_no_link_warnings.warnings.reject { |warning| warning.include?("link") }.compact + cf_no_link_warnings.uid = cf_no_link_warnings.uid.reverse + + if cf_no_link_warnings.save + new_challenges = cf_count.first.challenges.map(&:dup) + new_challenges.each { |c| c.content_file_id = cf_no_link_warnings.id } + cf_count.first.challenges.map(&:delete) + new_challenges.map(&:save) + cf_count.first.delete + end + + content_file_url(current_user.sandbox.id, + preview_block.id, + cf_no_link_warnings.path) + else + Rails.application.routes.url_helpers.cohort_url(current_user.sandbox.id) + end + end + + render json: { status: release.state, preview_url: preview_url, sync_warnings: release.sync_warnings }, status: :ok + elsif release.state == Release::STATES[:pending] || release.state == Release::STATES[:processing] + render json: { status: release.state, sync_warnings: release.sync_warnings }, status: :ok + else + render json: { errors: release.sync_errors.join(", "), sync_warnings: release.sync_warnings }, status: :unprocessable_entity + end + end + + private + + def release_params + params.permit(:release_id, :s3_key) + end +end diff --git a/scripts/app/controllers/api/v1/uploads_controller.rb b/scripts/app/controllers/api/v1/uploads_controller.rb new file mode 100644 index 0000000000000000000000000000000000000000..10d31c714b30599e7df97233211f6c681253b171 --- /dev/null +++ b/scripts/app/controllers/api/v1/uploads_controller.rb @@ -0,0 +1,23 @@ +class Api::V1::UploadsController < Api::ApplicationController + def create + + render_method_not_allowed_response and return unless request.post? + + authorize(current_user, :regenerate_api_token?) + + @full_path = params[:full_path] + + if (@full_path.nil?) + json_response(error: "Parameter full_path not set") + return + end + + # Upload to s3 bucket + file_content = request.body.read + aws_image_object = Rails.application.config.aws.object(@full_path) + aws_image_object.put(body: file_content) + + json_response(controller: "Uploaded file #{@full_path}") + end + +end diff --git a/scripts/app/controllers/api/v1/users_controller.rb b/scripts/app/controllers/api/v1/users_controller.rb new file mode 100644 index 0000000000000000000000000000000000000000..4cc51b90a279a0377cba3bfcb859e70485204898 --- /dev/null +++ b/scripts/app/controllers/api/v1/users_controller.rb @@ -0,0 +1,57 @@ +class Api::V1::UsersController < Api::ApplicationController + def regenerate_token + authorize(current_user, :regenerate_api_token?) + + current_user.regenerate_api_token + current_user.sandbox + + render json: { token: current_user.api_token } + end + + def learn_cli_credentials + render_method_not_allowed_response and return unless request.get? + + authorize(current_user, :learn_cli_credentials?) + + Block.find_or_create_by(title: "preview", repo_name: "preview") + + render json: { + user_id: current_user.id.to_s, + user_email: current_user.email, + s3: { + access_key_id: Rails.application.secrets.glearn_access_key_id, + secret_access_key: Rails.application.secrets.glearn_secret_access_key, + key_prefix: Rails.application.secrets.glearn_key_prefix, + bucket_name: Rails.application.secrets.glearn_bucket_name + }, + slack: { + dev_notify_url: Rails.application.secrets.dev_notify_slack_url + } + } + end + + def learn_cli_metadata + render_method_not_allowed_response and return unless request.post? + + authorize(current_user, :learn_cli_metadata?) + + @api_interaction_metadata = { + cli_benchmark: user_params.as_json + } + + render json: { message: "Success" }, status: :ok + end + + private + + def user_params + params.require(:cli_benchmark).permit( + :time_to_compress, + :time_to_build_on_learn, + :time_to_upload_to_s3, + :master_release_and_build, + :total_cmd_time, + :command_name + ) + end +end diff --git a/scripts/app/controllers/application_controller.rb b/scripts/app/controllers/application_controller.rb new file mode 100644 index 0000000000000000000000000000000000000000..6b36460f3be4c806cfb7ae245e1f715215d8b50e --- /dev/null +++ b/scripts/app/controllers/application_controller.rb @@ -0,0 +1,155 @@ +class ApplicationController < ActionController::Base + include Pundit + include JsonHelper + include Pagy::Backend + + helper_method :current_user + + protect_from_forgery with: :exception + before_action :require_signed_in_user + before_action :is_mobile? + before_action :miniprofiler + after_action :set_last_viewed_cohort_id + after_action :verify_authorized + before_action :can_access_api? + before_action :warn_browser_compatibility + + rescue_from StandardError do |exception| + raise exception if Rails.env.development? + + if exception.is_a?(Pundit::NotAuthorizedError) + if request.format.json? + render_error_response(status: 403, text: exception.message, backtrace: exception.backtrace) + else + redirect_to root_path, alert: "You do not have permission to access that page." + end + elsif exception.is_a?(ActiveRecord::RecordNotFound) + error_404 + elsif request.format.json? + Honeybadger.notify(exception, context: { referer: request.referer }) + render_error_response(status: 500, text: exception.message, backtrace: exception.backtrace) + else + Honeybadger.notify(exception, context: { referer: request.referer }) + error_500 + end + end + + def current_user + @current_user ||= User.find_by(uid: session[:user_uid]) unless session[:user_uid].nil? + end + + def require_signed_in_user + if current_user.blank? + session[:requested_path] = request.fullpath unless request.fullpath == "/" + # TODO: AuthApi testing + # redirect_to auth_api.new_session_path + redirect_to '/sign_in' + end + end + + def error_500 + skip_authorization + render template: "error_500", status: :service_unavailable, format: :html + end + + def error_404 + skip_authorization + if request.format.json? + render_error_response(status: 404, text: "not found", backtrace: nil) + else + render template: "error_404", status: :not_found, format: :html + end + end + + def api_token + authorize(current_user, :regenerate_api_token?) + render template: "api_token", format: :html + end + + def api_interactions + authorize(current_user, :view_api_interactions?) + + begin + @pagy, @records = pagy( + ApiInteraction.order(created_at: :desc), + page: params[:page] || 1 + ) + # Main reason for this rescue is for if someone types page=not_a_valid_page_number + # into the URL bar so nothing breaks we send to page 1 + rescue StandardError + @pagy, @records = pagy(ApiInteraction.order(created_at: :desc), page: 1) + + render template: "api_interactions", locals: { + api_interactions: @records, + page: @pagy.vars[:page], + per_page: @pagy.vars[:items], + total_count: @pagy.vars[:count] + }, format: :html and return + end + + render template: "api_interactions", locals: { + api_interactions: @records, + page: @pagy.vars[:page], + per_page: @pagy.vars[:items], + total_count: @pagy.vars[:count] + }, format: :html + end + + private + + def warn_browser_compatibility + return if Rails.env.test? + + @unsupported_browser = false + uagent = request.env["HTTP_USER_AGENT"] + if uagent =~ /Edge/ || uagent =~ /MSIE/ || uagent =~ /Opera/ || uagent =~ /OPR/ || uagent =~ /Trident/ + @unsupported_browser = true + end + end + + def can_access_api? + @authorized_api_access ||= current_user.admin? || CohortUser.instructor.where(user_id: current_user.id).any? + end + + def render_error_response(status:, text:, backtrace:) + json_response = { errors: [message: "#{status} #{text}"] } + json_response[:backtrace] = backtrace unless Rails.env.production? + render json: json_response, status: status + end + + def set_last_viewed_cohort_id + return unless current_user + return unless @current_cohort + + current_user.update(last_viewed_cohort_id: @current_cohort.id) + end + + def is_mobile? + @is_mobile_device = browser.mobile? + end + + def segment_identify + return if Rails.env.test? + return unless current_user + + @current_user_uid = current_user.uid + @identify_properties = { + firstName: current_user.first_name, + lastName: current_user.last_name, + email: current_user.email, + githubUsername: current_user.github_username, + createdAt: current_user.created_at, + learnId: current_user.id, + roleName: current_user.admin? ? "admin" : "student" + } + Analytics.identify( + user_id: @current_user_uid, + traits: @identify_properties + ) + end + + def miniprofiler + emails = ["chad.lillquist@galvanize.com", "brad.lamson@galvanize.com", "grunde@galvanize.com"] + Rack::MiniProfiler.authorize_request if emails.include?(current_user&.email) + end +end diff --git a/scripts/app/controllers/blocks/releases_controller.rb b/scripts/app/controllers/blocks/releases_controller.rb new file mode 100644 index 0000000000000000000000000000000000000000..75e3079ce23bf50e3ed2201207c48019f8a04d97 --- /dev/null +++ b/scripts/app/controllers/blocks/releases_controller.rb @@ -0,0 +1,69 @@ +module Blocks + class ReleasesController < ApplicationController + before_action :set_html_title + + def index + authorize(current_cohort, :create_cohort_release?) + + releases = ReleaseFinder.for_block(params[:block_id]) + release_presenters = BlockPresenter::ForCohortReleases.releases_for_cohort( + releases: releases, + cohort: current_cohort, + block_id: params[:block_id] + ) + + render json: { releases: release_presenters } + end + + def new + authorize(Release) + render(locals: { block: current_block, release: Release.new }) + end + + def create + authorize(Release) + + pending_release = Release.create(block_id: current_block.id, + notes: release_params[:notes], + github_sha: "pending", + user_id: current_user.id, + state: Release::STATES[:pending]) + + CreateReleaseJob.perform_later(pending_release_id: pending_release.id) + + flash[:notice] = "Release creation queued." + redirect_to block_url(current_block) + end + + private + + def set_html_title + @html_title = "Blocks" + end + + def current_cohort + @current_cohort ||= Cohort.find(params[:cohort_id]) + end + + def current_block + @current_block ||= Block.find(params[:block_id]) + end + + def release_params + params.require(:release).permit(:notes) + end + + def set_segment_page_properties + @track_properties = { + productId: nil, + pageType: @html_title, + cohortId: nil, + cohortName: nil, + blockId: current_block&.id, + blockName: current_block&.title, + unitId: nil, + unitName: nil + }.to_json + end + end +end diff --git a/scripts/app/controllers/blocks_controller.rb b/scripts/app/controllers/blocks_controller.rb new file mode 100644 index 0000000000000000000000000000000000000000..18c8d394b3c40a1d0da0fa5bb7eb03be13433a2c --- /dev/null +++ b/scripts/app/controllers/blocks_controller.rb @@ -0,0 +1,77 @@ +class BlocksController < ApplicationController + before_action :set_html_title + + def index + authorize(Block) + relations = { releases: { cohort_releases: { cohort: :cohort_users } } } + blocks = Block.all.active.without_preview.includes(relations).order(title: :asc) + render( + locals: { + blocks: blocks.map { |b| BlockPresenter::ForBlock.new(block: b) } + } + ) + end + + def show + authorize(current_block) + releases = Release.where(block_id: current_block.id, branch_name: "master").order(id: :desc).map do |r| + BlockPresenter::ForReleases.new(release: r) + end + SetBlockCachesJob.perform_later(block_id: current_block.id) + render(locals: { block: current_block, url: current_block.repo_url, releases: releases }) + end + + def blockpagev2 + authorize(current_block) + releases = Release.where(block_id: current_block.id, branch_name: "master").order(id: :desc).map do |r| + BlockPresenter::ForReleases.new(release: r) + end + render( + locals: { + block: BlockPresenter::ForBlock.new(block: current_block), + github_url: current_block.git_url, + releases: releases + } + ) + end + + def release_polling + authorize(current_block) + releases = Release.where(block_id: current_block.id, branch_name: "master").order(id: :desc).map do |r| + BlockPresenter::ForReleases.new(release: r) + end + render json: { block: current_block, releases: releases } + end + + def new + authorize(Block) + render(locals: { block: Block.new }) + end + + private + + def set_html_title + @html_title = "Blocks" + end + + def current_block + @current_block ||= Block.find(params[:id]) + end + + def block_params + params.require(:block).permit(:title, :repo_name, :org, :origin) + end + + def set_segment_page_properties + @track_properties = { + productId: nil, + pageType: @html_title, + cohortId: nil, + cohortName: nil, + blockId: params[:id].present? ? current_block&.id : nil, + blockName: params[:id].present? ? current_block&.title : nil, + unitId: nil, + unitName: nil + }.to_json + end +end diff --git a/scripts/app/controllers/cohorts/blocks/content_files_controller.rb b/scripts/app/controllers/cohorts/blocks/content_files_controller.rb new file mode 100644 index 0000000000000000000000000000000000000000..cc30315448218852c3fd0ae4e284c6f0b1828276 --- /dev/null +++ b/scripts/app/controllers/cohorts/blocks/content_files_controller.rb @@ -0,0 +1,306 @@ +module Cohorts + module Blocks + class ContentFilesController < ApplicationController + def show + skip_authorization and render template: "error_404" and return if current_content_file.nil? + authorize(current_content_file, :show?, visible_for_cohort: current_cohort) + @html_title = current_content_file.title + @page_type = current_content_file.content_file_type.titleize + @current_category = "curriculum" + @force_navigation_hide = current_content_file.content_file_type == ContentFile::TYPES[:resource] + + run_content_file_visit + + render( + locals: current_content_file.checkpoint? ? checkpoint_locals : lesson_locals + ) + end + + def lesson_locals + curriculum_warning_count = current_user.instructor_or_admin?(current_cohort.id) ? current_content_file.warnings.length : 0 + { + repo_details: current_block.repo_details, + visited_lesson_uids: lesson_visits.map(&:content_file_uid) | [current_content_file.uid], # SideBar + standard: standard_presenter, # SideBar + cohort_path: cohort_path(current_cohort), # SideBar, SubmissionRenderer + checkpoint_info: checkpoint_info, # SideBar, SubmissionRenderer + content_files_submissions: single_standard_submissions, # Lesson, SideBar + user_id: current_user.id, # ChallengeBlock, SideBar + content_file: content_file_locals, # ChallengeBlock, SubmissionRenderer + cohort_mode: current_cohort.mode, # ChallengeBlock, SubmissionRenderer + content_file_presenter: content_file_presenter, # ChallengeBlock, SubmissionRenderer + is_instructor_or_admin: instructor_or_admin, # ChallengeBlock, SubmissionRenderer + content_file_submission_path: content_file_submission_path, # ChallengeBlock + is_ipynb_file: current_content_file.path.include?(".ipynb"), # SubmissionRenderer, html + content_file_hidden: content_visisbility.include?(current_content_file.uid), # SubmissionRenderer + standard_hidden: content_visisbility.include?(current_standard.uid), # SubmissionRenderer + visibility_path: instructor_or_admin ? content_cohort_path(current_cohort) : nil, # SubmissionRenderer + cohort_id: current_cohort.id, # SubmissionRenderer + is_sandbox_cohort: current_cohort.sandbox, # SubmissionRenderer + curriculum_warning_count: curriculum_warning_count, # SubmissionRenderer + checkpoint_is_autoscored: current_content_file.autoscore, # SubmissionRenderer + is_enrolled_student: current_user.student_of?(current_cohort.id), # SubmissionRenderer + content_file_footer_presenter: ContentFilePresenter::ForFooter.new( + cohort: current_cohort, + content_file: current_content_file + ) # Lesson, SubmissionRenderer + } + end + + # sidebar attributes are used for both checkpoint and lesson, should be extracted + # pull out links from any locals, refactor to use routes.tsx + # checkpoint landing page locals should be from a separate method, will cleanup lesson locals + def checkpoint_locals + checkpoint_attempts_remaining = nil + checkpoint_submissions_count = current_user.checkpoint_submissions_count( + current_cohort.id, + current_block.id, + current_content_file.uid + ) + if current_content_file.max_checkpoint_submissions.present? + checkpoint_attempts_remaining = current_content_file.max_checkpoint_submissions - checkpoint_submissions_count + checkpoint_attempts_remaining = checkpoint_attempts_remaining < 0 ? 0 : checkpoint_attempts_remaining + end + + { + load_student_scores: current_cohort.students.size < 1000, + repo_details: current_block.repo_details, + is_ipynb_file: current_content_file.path.include?(".ipynb"), + checkpoint_title: current_content_file.title, # SideBar + visited_lesson_uids: lesson_visits.map(&:content_file_uid) | [current_content_file.uid], # SideBar + standard: standard_presenter, # SideBar + cohort_path: cohort_path(current_cohort), # SideBar, SubmissionRenderer + cohort_id: current_cohort.id, + checkpoint_info: checkpoint_info, # SideBar, SubmissionRenderer + content_files_submissions: single_standard_submissions, # Lesson, SideBar + user_id: current_user.id, # ChallengeBlock, SideBar + cohort_mode: current_cohort.mode, # ChallengeBlock, SubmissionRenderer + checkpoint_attempts_remaining: checkpoint_attempts_remaining, + time_limit: current_content_file.time_limit, + challenge_types: current_content_file.challenges.map(&:challenge_type).uniq, + progress_thresholds: current_cohort.settings["progress_thresholds"], + content_file: content_file_locals, # ChallengeBlock, SubmissionRenderer + is_instructor_or_admin: instructor_or_admin, # Checkpoint and children + is_preview_sandbox: current_cohort.sandbox, + student_scores_endpoint: api_v1_cohort_block_student_scores_path(current_cohort.id, + current_block.id, + current_content_file.id), + github_url: content_file_github_url, + take_assessment_path: api_v1_cohort_block_take_assessment_path(current_cohort, + current_block, + current_content_file), + storage_key: "viewed_new_assessment", + is_enrolled_student: current_user.student_of?(current_cohort.id), + cohort_allow_paired_submissions: current_cohort.allow_paired_submissions + } + end + + def content_file_locals + { + id: current_content_file.id, + github_url: content_file_github_url, + html: current_content_file.html.html_safe, + is_checkpoint: current_content_file.checkpoint?, + is_resource: current_content_file.resource?, + title: current_content_file.title, + path: current_content_file.path, + permalink: permalink_url(CGI.escape(current_block.repo_name), current_content_file.path), + type: current_content_file.content_file_type, + students_in_cohort: current_cohort.students.map do |s| + { id: s.id, full_name: s.full_name, initials: s.initials, profile_image: s.profile_image } + end + } + end + + def track_activity_event + render nothing: true, status: 404 and return if current_cohort.nil? || current_content_file.nil? + + authorize(current_cohort, :show?) + + Activity.create( + cohort: current_cohort, + creator: current_user, + name: params[:event], + subject: current_content_file, + content: params[:content] + ) + + respond_to do |format| + format.json do + render json: { ack: :ok } + end + end + end + + private + + def current_cohort + @current_cohort ||= Cohort.find(params[:cohort_id]) + end + + def current_content_file + @content_file ||= ContentFileFinder.from_cohort_block_content_file_path( + cohort_id: current_cohort.id, + block_id: params[:block_id], + content_file_path: params[:path] + ) + end + + def content_file_github_url + release = current_content_file.standard.release + block = release.block + base_url = block.git_url + branch = release.branch_name + "#{base_url}/blob/#{branch}/#{current_content_file.path}" + end + + def current_standard + @current_standard ||= current_content_file.standard + end + + def current_block + @current_block ||= current_standard.release.block + end + + def content_visisbility + @content_visisbility ||= ContentVisibility.where( + cohort_id: current_cohort.id, + content_uid: [current_standard.uid, current_content_file.uid] + ).map(&:content_uid) + end + + def all_checkpoints + @all_checkpoints ||= CheckpointSubmission.where( + content_file_uid: current_content_file.uid, + cohort_id: current_cohort.id, + content_file_block_id: current_block.id, + user_id: current_user.id + ).order(created_at: :desc) + end + + def checkpoints_by_state + @checkpoints_by_state ||= { + done: all_checkpoints.find { |chs| chs.state == CheckpointSubmission::STATES[:done] }, + retry: all_checkpoints.find { |chs| chs.state == CheckpointSubmission::STATES[:retry] }, + started: all_checkpoints.find { |chs| chs.state == CheckpointSubmission::STATES[:started] }, + needs_review: all_checkpoints.find { |chs| chs.state == CheckpointSubmission::STATES[:needs_review] } + } + end + + def single_standard_submissions + @single_standard_submissions ||= StandardSubmissionsService.new( + standard: current_standard, + cohort: current_cohort, + student_ids: [current_user.id], + all_checkpoints: all_checkpoints, + instructor_or_admin: current_user.instructor_or_admin?(current_cohort.id) + ).single + end + + def lesson_visits + @lesson_visits ||= LessonVisit.where( + user_id: current_user.id, + standard_uid: current_standard.uid, + cohort_id: current_cohort.id, + block_id: current_block.id + ) + end + + def content_file_presenter + @content_file_presenter ||= ContentFilePresenter::ForShow.new( + current_content_file, + current_cohort, + current_user + ) + end + + def standard_presenter + @standard_presenter ||= StandardPresenter::ForStandardCard.new( + standard: current_standard, + cohort: current_cohort, + current_user_id: current_user.id, + user_performances: + if current_cohort.mode == Cohort::MODES[:mastery] + PerformanceFinder.latest_for_standards_in_cohort( + standard_uids: [current_standard.uid], + user_ids: [current_user.id], + cohort_id: current_cohort.id + ).to_a + else + [] + end, + cohort_standard_progress_data: single_standard_submissions, + instructor_or_admin: instructor_or_admin, + lesson_visits: lesson_visits + ) + end + + def mastery_score + @mastery_score ||= Performance.where( + user_id: current_user.id, standard_uid: current_standard.uid, block_id: current_block.id + ).last&.score || nil + end + + def instructor_or_admin + @instructor_or_admin ||= current_user.instructor_or_admin?(current_cohort.id) + end + + def checkpoint_info + @checkpoint_info ||= { + cohortId: current_cohort.id, + cohortMode: current_cohort.mode, + checkpointsByState: checkpoints_by_state, + challengeTotal: content_file_presenter.challenges_with_answers.length, + contentFilesSubmission: + single_standard_submissions.find do |cfs| + cfs[:content_file_type] == ContentFile::TYPES[:checkpoint] + end. + try(:dig, :checkpoint_submissions, current_user.id) + + }.merge( + if current_cohort.mode == Cohort::MODES[:mastery] + { + masteryScore: mastery_score + } + else + { + challengeCorrect: content_file_presenter.challenges_with_answers.count do |ch| + ch.submitted_challenge_answer_presenters[0]&.status == "correct" + end + } + end + ) + end + + def run_content_file_visit + return if current_content_file.content_file_type == ContentFile::TYPES[:resource] || + current_content_file.content_file_type == ContentFile::TYPES[:instructor] + + ContentFileVisitJob.perform_later( + current_user.id, + current_block.id, + current_cohort.id, + current_content_file.id, + current_standard.uid + ) + end + + def content_file_submission_path + cohort_content_file_submitted_challenge_answers_path(current_cohort, current_content_file) + end + + def set_segment_page_properties + @track_properties = { + productId: current_cohort.uid, + pageType: @page_type, + cohortId: current_cohort.id, + cohortName: current_cohort.name, + blockId: current_block.id, + blockName: current_block.title, + unitId: current_content_file.standard.id, + unitName: current_content_file.standard.title + }.to_json + end + end + end +end diff --git a/scripts/app/controllers/cohorts/checkpoint_submissions/activities_controller.rb b/scripts/app/controllers/cohorts/checkpoint_submissions/activities_controller.rb new file mode 100644 index 0000000000000000000000000000000000000000..cc83f6784b6230e19eec2de28214021784623997 --- /dev/null +++ b/scripts/app/controllers/cohorts/checkpoint_submissions/activities_controller.rb @@ -0,0 +1,80 @@ +module Cohorts + module CheckpointSubmissions + class ActivitiesController < ApplicationController + before_action :current_cohort + + def create + authorize(checkpoint_submission, :comment?) + + activity = Activity.new( + content: activity_params[:content], + subject: checkpoint_submission, + creator: current_user, + name: Activity::NAMES[:comment_created], + cohort: current_cohort + ) + + if activity.save + first_sca = checkpoint_submission.submitted_challenge_answers.first + content_file = first_sca.challenge.content_file + checkpoint_submission_url = cohort_checkpoint_submission_url(current_cohort, checkpoint_submission) + + if activity_params[:notify] == "true" + notification = { + tagline: "Comment Left", + title: + "#{current_user.full_name} commented on #{content_file.standard.release.block.title}" \ + " - #{content_file.standard.title} - Checkpoint", + url: checkpoint_submission_url, + description: "\"#{activity_params[:content]}\"" + } + + if checkpoint_submission.user_id == current_user.id + current_cohort.instructors.each { |instructor| instructor.notify(notification) } + + slack_users(CohortUser.instructor.where(cohort: current_cohort)&.map(&:user), checkpoint_submission_url, content_file.title) + else + checkpoint_submission.user.notify(notification) + + slack_users([first_sca.user], checkpoint_submission_url, content_file.title) + end + end + + render json: ActivityPresenter.new(activity), status: 200 + else + render json: { error: activity.errors.full_messages }, status: 400 + end + end + + private + + def activity_params + params.require(:activity).permit(:content, :notify) + end + + def current_cohort + @cohort ||= CohortPolicy::Scope.new(current_user, Cohort).resolve.find_by!(id: params[:cohort_id]) + end + + def checkpoint_submission + @checkpoint_submission ||= CheckpointSubmission.find(params[:checkpoint_submission_id]) + end + + def slack_users(users, checkpoint_submission_url, content_file_title) + users.each do |user| + user.send_slack_message( + slack_message_content( + commentor: current_user, + url: checkpoint_submission_url, + content_file_title: content_file_title + ) + ) + end + end + + def slack_message_content(commentor:, content_file_title:, url:) + "#{commentor.full_name} left a comment on #{content_file_title}. Read it here: #{url}." + end + end + end +end diff --git a/scripts/app/controllers/cohorts/cohort_releases_controller.rb b/scripts/app/controllers/cohorts/cohort_releases_controller.rb new file mode 100644 index 0000000000000000000000000000000000000000..76bf5c813db47c487824a363a03108aafd7a7502 --- /dev/null +++ b/scripts/app/controllers/cohorts/cohort_releases_controller.rb @@ -0,0 +1,156 @@ +module Cohorts + class CohortReleasesController < ApplicationController + def index + authorize(current_cohort, :create_cohort_release?) + + @current_category = "setup" + render json: { cohort_releases: cohort_release_presenters } + end + + def resync + authorize(current_cohort, :create_cohort_release?) + + ResyncCourseJob.perform_later( + cohort_id: current_cohort.id, + course_url: params[:course_config_url] + ) + + render json: {} + end + + def resync_status + authorize(current_cohort, :create_cohort_release?) + latest = ResyncJobResult.find_latest(current_cohort.id) + latest.timeout_latest! if latest&.older_than_one_minute? + render json: latest + end + + def archive_resync_error + authorize(current_cohort, :create_cohort_release?) + if params[:cancel] + ResyncJobResult.cancel_latest!(current_cohort.id) + else + ResyncJobResult.archive_latest_failure!(current_cohort.id) + end + render json: ResyncJobResult.find_latest(current_cohort.id) + end + + def create + authorize(current_cohort, :create_cohort_release?) + + CohortRelease.create!(cohort: current_cohort, release_id: cohort_release_params[:release_id]) + + redirect_to(setup_cohort_path(current_cohort), flash: { success: "Block attached." }) + end + + def update + authorize(current_cohort_release) + + if cohort_release_params[:use_latest_release] == true + latest_release_id = Release.where(block_id: current_cohort_release.release.block_id, branch_name: "master").order(:id).last.id + cohort_release_params[:release_id] = latest_release_id + elsif cohort_release_params[:release_id].present? && cohort_release_params[:use_latest_release].blank? + cohort_release_params[:use_latest_release] = false + end + + current_cohort_release.update!(cohort_release_params.merge(pending_release_id: nil)) + + respond_to do |format| + format.html do |_f| + redirect_to(setup_cohort_path(current_cohort), flash: { success: "Block release updated." }) + end + format.json do |_f| + render json: { release: CohortReleasePresenter::ForCohortSetup.new(cohort_release: current_cohort_release) } + end + end + end + + def switch_to_branch + authorize(current_cohort, :create_cohort_release?) + block = Block.find(params[:block_id]) + repo_name = block.repo_name + github_sha = branch_sha(params[:block_id], params[:branch_name]) + flash_info = if !github_sha + { type: "danger", + msg: "Error. The provided branch '#{params[:branch_name]}' does not exist on repo: #{block.repo_url}." } + else + resolve_branch_release(github_sha) + end + + render json: { + cohort_release: CohortReleasePresenter::ForCohortSetup.new(cohort_release: current_cohort_release), + flash_info: flash_info.merge(title: repo_name.titleize) + } + end + + def branch_polling + authorize(current_cohort, :create_cohort_release?) + render json: { + cohort_release: CohortReleasePresenter::ForCohortSetup.new(cohort_release: current_cohort_release), + cohort_releases: cohort_release_presenters + } + end + + private + + def resolve_branch_release(github_sha) + release_at_sha = Release.find_by(github_sha: github_sha, block_id: params[:block_id], branch_name: params[:branch_name]) + if release_at_sha.present? && release_at_sha.successfully_synced? + msg = if current_cohort_release.release_id == release_at_sha.id + "Nothing to publish, block is already up-to-date with the latest commit on '#{params[:branch_name]}'." + else + "Attached existing and previously synced branch '#{params[:branch_name]}'" + end + current_cohort_release.update(release_id: release_at_sha.id, pending_release_id: nil, use_latest_release: false) + flash_info = { type: "success", msg: msg } + elsif release_at_sha.present? && !release_at_sha.successfully_synced? + release_at_sha.update( + state: Release::STATES[:pending], + sync_errors: [] + ) + flash_info = queue_job_with_publish_flash(release_at_sha) + else + new_release = Release.create( + block_id: params[:block_id], + github_sha: branch_sha(params[:block_id], params[:branch_name]), + state: Release::STATES[:pending], + branch_name: params[:branch_name] + ) + flash_info = queue_job_with_publish_flash(new_release) + end + flash_info + end + + def branch_sha(block_id, branch_name) + return @github_sha if @github_sha + + git_type = Block.find(block_id).git_type + git_klass = git_type == "github" ? DownloadGithubRepositoryService : DownloadGitlabRepositoryService + @github_sha = git_klass.new(release: nil, tmp_path: "").branch_sha(block_id, branch_name) + end + + def queue_job_with_publish_flash(release) + current_cohort_release.update(pending_release_id: release.id, use_latest_release: false) + SwitchToBranchJob.perform_later(pending_release_id: release.id, cohort_release_id: current_cohort_release.id) + { type: "warning", msg: "Publishing branch '#{params[:branch_name]}'" } + end + + def current_cohort + @current_cohort ||= Cohort.find(params[:cohort_id]) + end + + def current_cohort_release + @current_cohort_release ||= CohortRelease.includes(:release).find(params[:id]) + end + + def cohort_release_params + @cohort_release_params ||= params.require(:cohort_release).permit(:release_id, :use_latest_release) + end + + def cohort_release_presenters + CohortRelease.attached_to_cohort(current_cohort.id).map do |cohort_release| + CohortReleasePresenter::ForCohortSetup.new(cohort_release: cohort_release) + end + end + end +end diff --git a/scripts/app/controllers/cohorts/content_files/checkpoint_submissions_controller.rb b/scripts/app/controllers/cohorts/content_files/checkpoint_submissions_controller.rb new file mode 100644 index 0000000000000000000000000000000000000000..131ec5a672402413ad550d87d9ea8bd88c485a29 --- /dev/null +++ b/scripts/app/controllers/cohorts/content_files/checkpoint_submissions_controller.rb @@ -0,0 +1,338 @@ +module Cohorts + module ContentFiles + class CheckpointSubmissionsController < ApplicationController + def show + authorize(current_checkpoint_submission, :show?) + + if current_checkpoint_submission.state == CheckpointSubmission::STATES[:started] + redirect_back fallback_location: cohorts_path(current_cohort), + flash: { alert: "Cannot view a Checkpoint that has not yet been submitted." } and return + end + + @html_title = "#{loaded_student.full_name}: #{current_content_file.title}" + activities = + Activity.where( + subject_type: CheckpointSubmission.to_s, + subject_id: CheckpointSubmissionFinder.all_for_user_id_content_file_in_cohort(loaded_student.id, + current_content_file, + current_cohort.id)&.map(&:id), + name: Activity::NAMES[:comment_created] + ).order(created_at: :asc) + is_instructor_or_admin = current_user.instructor_or_admin?(current_cohort.id) + + render locals: { + current_checkpoint_submission: current_checkpoint_submission_presenter, + loaded_student: loaded_student, + loaded_pairs: loaded_pairs, + pair_submissions: pair_submissions, + challenges: challenge_presenters, + standard_card_props: standard_card_props, + cohort_id: current_cohort.id, + cohort_mode: current_cohort.mode, + has_scored: standard_card_props.checkpoint_performance.present?, + current_user_id: current_user.id, + is_instructor_or_admin: is_instructor_or_admin, + resubmit_url: content_file_path(current_cohort, current_content_file.standard.release.block, current_content_file.path), + next_ungraded_submission_url: next_ungraded_submission_url(current_checkpoint_submission), + performance_path: cohort_user_performances_path( + current_cohort, + current_checkpoint_submission.submitted_challenge_answers.first.user + ), + previous_challenge_path: previous_challenge(current_checkpoint_submission), + next_challenge_path: next_challenge(current_checkpoint_submission), + progress_thresholds: current_cohort.settings["progress_thresholds"], + activities: activities.order(created_at: :asc).map { |activity| ActivityPresenter.new(activity) }, + track_event_path: track_activity_event_path( + current_cohort, + current_content_file.standard.release.block_id, + current_content_file.path + ) + } + end + + def update + authorize(current_checkpoint_submission) + + current_answers = current_checkpoint_submission.submitted_challenge_answers.includes(:challenge) + + current_checkpoint_submission.update!(checkpoint_submission_params.merge(grader_id: current_user.id)) + + if checkpoint_submission_params[:state] == CheckpointSubmission::STATES[:done] + total_points = current_answers.map(&:challenge).map(&:points).compact.reduce(:+) + correct_points = current_answers.map(&:points).compact.reduce(:+) + current_checkpoint_submission.update!(total_points: total_points, correct_points: correct_points) + end + + if current_checkpoint_submission.cohort.mode == Cohort::MODES[:percentage] && + checkpoint_submission_params[:state] != CheckpointSubmission::STATES[:retry] + Performance.create( + user_id: loaded_student.id, + updator: current_user, + score: 0, + standard_id: current_standard.id, + cohort_id: current_checkpoint_submission.cohort.id, + checkpoint_submission_id: current_checkpoint_submission.id + ) + end + + student = current_answers.first.user + student.send_slack_message(slack_message_content) + + NotificationService.checkpoint_submission_graded( + user: student, + grader: current_user, + cohort: current_checkpoint_submission.cohort, + content_file: current_content_file, + checkpoint_submission: current_checkpoint_submission.reload + ) + render json: { + current_checkpoint_submission: current_checkpoint_submission_presenter + } + end + + private + + def checkpoint_submission_params + params.require(:checkpoint_submission).permit(:state) + end + + def current_cohort + @current_cohort ||= CohortPolicy::Scope.new(current_user, Cohort).resolve.find(params[:cohort_id]) + end + + def current_content_file + @current_content_file ||= current_checkpoint_submission.submitted_challenge_answers.first.challenge.content_file + end + + def current_block + @current_block ||= current_standard.release.block + end + + def current_standard + @current_standard ||= current_content_file.standard + end + + def current_cohort_release + @current_cohort_release ||= CohortRelease.joins(release: :standards). + where("cohort_releases.cohort_id = ? AND standards.uid = ?", current_cohort.id, current_standard.uid). + first + end + + def current_checkpoint_submission + CheckpointSubmission.find(params[:id]) + end + + def current_checkpoint_submission_presenter + CheckpointSubmissionPresenter.new( + checkpoint_submission: current_checkpoint_submission, + content_file: current_content_file, + url_cohort_id: current_cohort.id + ) + end + + def student + @student ||= current_checkpoint_submission.user + end + + def loaded_student + return @loaded_student if @loaded_student.present? + + @loaded_student ||= UserPresenter::ForAvatar.new(student) + end + + def loaded_pairs + return @loaded_pairs if @loaded_pairs.present? + return [loaded_student] if current_checkpoint_submission.pair_submission_ids.empty? || + current_cohort.mode == Cohort::MODES[:mastery] || + current_user.instructor_or_admin?(current_cohort) == false + + students = User.select(:first_name, :last_name, :id, :profile_image).where(id: pair_submissions.map(&:user_id)).order(id: :desc) + @loaded_pairs ||= students.map { |student| UserPresenter::ForAvatar.new(student) }.unshift(loaded_student) + end + + def pair_submissions + return [] if current_cohort.mode == Cohort::MODES[:mastery] || current_user.student_of?(current_cohort) + return @pair_submissions if @pair_submissions.present? + + @pair_submissions ||= CheckpointSubmission.where(id: current_checkpoint_submission.pair_submission_ids).filter do |cs| + CheckpointSubmission.where( + user_id: cs.user_id, + cohort_id: cs.cohort_id, + content_file_block_id: cs.content_file_block_id, + content_file_uid: cs.content_file_uid + ).where.not(state: CheckpointSubmission::STATES[:started]). + where("created_at > ?", cs.created_at).empty? + end.map do |sub| + CheckpointSubmissionPresenter.new( + checkpoint_submission: sub, + content_file: current_content_file, + url_cohort_id: current_cohort.id + ) + end + end + + def next_ungraded_submission_url(checkpoint_submission) + user_ids = CohortUser.where(cohort_id: checkpoint_submission.cohort_id).student.map(&:user_id) + not_in_ids = [checkpoint_submission.id, checkpoint_submission.pair_submission_ids].compact.flatten + pair_user_ids = CheckpointSubmission.select(:user_id).where(id: not_in_ids).map(&:user_id) + + previous_submission_ids_by_users = CheckpointSubmission.select(:id). + where("created_at < ?", checkpoint_submission.created_at). + where( + user_id: pair_user_ids, + content_file_block_id: checkpoint_submission.content_file_block_id, + content_file_uid: checkpoint_submission.content_file_uid + ).map(&:id) + + oldest_ungraded_submission = CheckpointSubmission. + where(content_file_uid: current_content_file.uid, + content_file_block_id: current_content_file.standard.release.block_id, + user_id: user_ids, + cohort_id: checkpoint_submission.cohort_id). + where.not(id: not_in_ids + previous_submission_ids_by_users).where.not(state: CheckpointSubmission::STATES[:started]). + select("DISTINCT ON (checkpoint_submissions.user_id) checkpoint_submissions.id, checkpoint_submissions.state"). + order("checkpoint_submissions.user_id, created_at DESC").to_a.select {|cs| cs.state == "needs_review"}.last + if oldest_ungraded_submission + Rails.application.routes.url_helpers.cohort_checkpoint_submission_path( + current_cohort.id, oldest_ungraded_submission.id + ) + else + Rails.application.routes.url_helpers.unit_progress_cohort_path(current_cohort.id) + end + end + + def challenge_presenters + is_instructor_or_admin = current_user.instructor_or_admin?(current_cohort.id) + challenge_ids = current_checkpoint_submission.submitted_challenge_answers.pluck(:challenge_id).uniq + challenges = Challenge.where(id: challenge_ids).order(position: :asc) + + all_submissions = SubmittedChallengeAnswer.where( + user_id: loaded_pairs.map(&:id), + challenge_uid: challenges.map(&:uid).uniq, + block_id: current_checkpoint_submission.content_file_block_id # select only for submissions in block + ).where( + "status <> 'draft'" + ).order(created_at: :desc) + + challenges.map do |challenge| + ChallengeWithSubmittedChallengeAnswersPresenter.new( + current_cohort, + challenge, + all_submissions.select {|sca| sca.challenge_uid == challenge.uid }, + is_instructor_or_admin, + user_context: loaded_pairs, + is_checkpoint: true, + can_view_status: is_instructor_or_admin || current_checkpoint_submission.done?, + can_view_explanation: current_checkpoint_submission.done? + ) + end + end + + def standard_card_props + standard = current_content_file.standard + @standard_card_props ||= StandardCardComponentProps.new( + standard: standard, + latest_performance: Performance.order(created_at: :desc). + find_by(user_id: loaded_student.id, standard_id: standard.id), + checkpoint_performance: Performance.order(created_at: :desc). + find_by(user_id: loaded_student.id, standard_id: standard.id, checkpoint_submission_id: params[:id]) + ) + end + + def next_checkpoint_submission_url(all_needs_review_checkpoint_submissions, current_checkpoint_submission) + sorted_oldest_first_submissions = all_needs_review_checkpoint_submissions.sort_by(&:updated_at) + + next_index = sorted_oldest_first_submissions.map(&:id).index(current_checkpoint_submission.id) + # next_index = next_index.nil? ? 0 : next_index + 1 + + if next_index.nil? + next_needs_review_checkpoint_submission = sorted_oldest_first_submissions.first + else + next_needs_review_checkpoint_submission = sorted_oldest_first_submissions[next_index + 1] + if next_needs_review_checkpoint_submission.nil? && sorted_oldest_first_submissions.length > 1 + next_needs_review_checkpoint_submission = sorted_oldest_first_submissions.first + end + end + + if next_needs_review_checkpoint_submission + cohort_checkpoint_submission_url( + params[:cohort_id], + next_needs_review_checkpoint_submission.id + ) + else + unit_progress_cohort_path(params[:cohort_id]) + end + end + + def slack_message_content + action = case current_checkpoint_submission.reload.state + when CheckpointSubmission::STATES[:done] + "finished scoring" + when CheckpointSubmission::STATES[:rejected] + "rejected" + end + standard_title = current_content_file.standard.title + checkpoint_name = current_content_file.title + url = cohort_checkpoint_submission_url( + params[:cohort_id], + current_checkpoint_submission.id + ) + + "#{current_user.full_name} #{action} your submission to #{standard_title} Checkpoint: #{checkpoint_name}. Review it here: #{url}." + end + + NO_ORPHANS = "NOT (content_files.content_file_type = 'checkpoint' AND submitted_challenge_answers.checkpoint_submission_id IS NULL)" + + def previous_challenge(checkpoint_submission) + # check for previous within block, not necessarily in standard + previous_sca_in_block = SubmittedChallengeAnswer.joins(challenge: [content_file: [standard: :release]]). + where("submitted_challenge_answers.user_id = ?", loaded_student.id). + where("submitted_challenge_answers.cohort_id = ?", current_cohort.id). + where("standards.position <= ?", current_standard.position). + where("releases.block_id = ?", checkpoint_submission.content_file_block_id). + where("challenges.id NOT IN (?)", current_content_file.challenges.map(&:id)). + where(NO_ORPHANS). + order("standards.position DESC, content_files.position DESC, challenges.position DESC").first + return previous_sca_in_block.review_path(cohort_id: current_cohort.id, user_id: loaded_student.id) if previous_sca_in_block.present? + + # check within previous blocks + previous_sca_blocks = SubmittedChallengeAnswer.joins(challenge: [content_file: [standard: [release: :cohort_releases]]]). + where("submitted_challenge_answers.user_id = ?", loaded_student.id). + where("submitted_challenge_answers.cohort_id = ?", current_cohort.id). + where("cohort_releases.position < ?", current_cohort_release.position). + where(NO_ORPHANS). + order("cohort_releases.position DESC"). + order("standards.position DESC"). + order("content_files.position DESC"). + order("challenges.position DESC").first + + return previous_sca_blocks.review_path(cohort_id: current_cohort.id, user_id: loaded_student.id) if previous_sca_blocks.present? + end + + def next_challenge(checkpoint_submission) + # check within block with standard position greater than submission's standard position + next_sca = SubmittedChallengeAnswer.joins(challenge: [content_file: [standard: :release]]). + where("submitted_challenge_answers.user_id = ?", loaded_student.id). + where("submitted_challenge_answers.cohort_id = ?", current_cohort.id). + where("standards.position > ?", current_standard.position). + where("releases.block_id = ?", checkpoint_submission.content_file_block_id). + where("challenges.id NOT IN (?)", current_content_file.challenges.map(&:id)). + where(NO_ORPHANS). + order("standards.position ASC, content_files.position ASC, challenges.position ASC").first + return next_sca.review_path(cohort_id: current_cohort.id, user_id: loaded_student.id) if next_sca.present? + + # check in blocks later in the cohort + next_sca_blocks = SubmittedChallengeAnswer.joins(challenge: [content_file: [standard: [release: :cohort_releases]]]). + where("submitted_challenge_answers.user_id = ?", loaded_student.id). + where("submitted_challenge_answers.cohort_id = ?", current_cohort.id). + where("cohort_releases.position > ?", current_cohort_release.position). + where(NO_ORPHANS). + order("cohort_releases.position DESC"). + order("standards.position DESC"). + order("content_files.position DESC"). + order("challenges.position DESC").first + return next_sca_blocks.review_path(cohort_id: current_cohort.id, user_id: loaded_student.id) if next_sca_blocks.present? + end + end + end +end diff --git a/scripts/app/controllers/cohorts/content_files/submitted_challenge_answers_controller.rb b/scripts/app/controllers/cohorts/content_files/submitted_challenge_answers_controller.rb new file mode 100644 index 0000000000000000000000000000000000000000..5dec944c9846f6c585c85035e6f32d04a96c11ff --- /dev/null +++ b/scripts/app/controllers/cohorts/content_files/submitted_challenge_answers_controller.rb @@ -0,0 +1,346 @@ +module Cohorts + module ContentFiles + class SubmittedChallengeAnswersController < ApplicationController + include CreateSubmittedChallengeAnswerService + + before_action :current_cohort # no route should function unless the cohort exists + + def update_local + sca = SubmittedChallengeAnswer.includes(:challenge).find(params[:id]) + + authorize(sca, :update_local_challenge_answer?) + + if params["m"] == true # See ChallengeBlock.tsx + sca.update(status: SubmittedChallengeAnswer::STATUSES[:correct]) + else + sca.update(status: SubmittedChallengeAnswer::STATUSES[:incorrect]) + end + + render json: { + submittedChallengeAnswer: SubmittedChallengeAnswerPresenter.new( + sca, + instructor_or_admin + ) + } + end + + def create + start_time = DateTime.current + authorize(current_cohort, :submit_challenge_answer?) + + challenge = Challenge.find(params[:challenge_id]) + submitted_challenge_answer = if params[:draft] + create_submitted_challenge_answer_draft(challenge, params[:answer]) + else + create_submitted_challenge_answer( + current_cohort, + current_user, + challenge, + params[:answer], + params[:checkpoint_submission_id] + ) + end + + end_time = DateTime.current + if ["checkbox", "local-snippet", "multiple-choice", "number", "poll", "short-answer"].include?(challenge.challenge_type) + # Challenges that dont need callouts to assessment-service should not take longer than 2 secs, alert us + length_of_scoring = end_time.to_i - start_time.to_i + if length_of_scoring > 10 + SlackChallengePerformanceJob.perform_later(submitted_challenge_answer.id, request.referer, length_of_scoring) + end + end + + render json: { + submittedChallengeAnswer: SubmittedChallengeAnswerPresenter.new( + submitted_challenge_answer, + instructor_or_admin + ) + } + end + + def batch_create + authorize(current_cohort, :submit_challenge_answer?) + + unless current_content_file.max_checkpoint_submissions.nil? + user_attempts = current_user.checkpoint_submissions_count( + current_cohort.id, + current_content_file.standard.release.block_id, + current_content_file.uid + ) + + if user_attempts >= current_content_file.max_checkpoint_submissions + render json: { + message: "Sorry, you have already submitted this checkpoint the max amount of times." + } and return + end + end + + checkpoint_submission = params[:draft] == true ? nil : find_or_create_checkpoint_submission(current_user.id) + + challenge_ids = params[:submitted_challenge_answers].map {|s| s[:challenge_id]} + challenges = Challenge.where(id: challenge_ids.compact).to_a + params[:submitted_challenge_answers]&.each do |submission| + challenge = challenges.find { |c| c.id == submission[:challenge_id].to_i } + if checkpoint_submission + create_submitted_challenge_answer( + current_cohort, + current_user, + challenge, + submission[:answer], + checkpoint_submission&.id + ) + else + create_submitted_challenge_answer_draft(challenge, submission[:answer]) + end + end + + if checkpoint_submission + checkpoint_submission.update(submitted_at: DateTime.current) + name = if previous_checkpoint_submission?(checkpoint_submission.id) + Activity::NAMES[:checkpoint_submission_resubmitted] + else + Activity::NAMES[:checkpoint_submission_created] + end + + Activity.create( + cohort: current_cohort, + creator: current_user, + name: name, + subject: checkpoint_submission + ) + end + + checkpoint_submission_presenter = if checkpoint_submission + CheckpointSubmissionPresenter.new( + checkpoint_submission: checkpoint_submission, + content_file: current_content_file + ) + end + + submitted_date = checkpoint_submission&.created_at + + if checkpoint_submission + CheckpointSubmissionService.new( + checkpoint_submission, + current_content_file, + current_cohort, + current_user + ).attempt_autoscore + + if params[:pairs].present? + create_pair_submissions(checkpoint_submission) + CheckpointPairedSubmissionJob.perform_later(checkpoint_submission.id, current_content_file.autoscore) + end + end + + render json: { + submittedDate: submitted_date, + checkpointSubmission: checkpoint_submission_presenter + } + end + + def checkpoint_submission_results + authorize(current_cohort, :submit_challenge_answer?) + + results = {} + + latest_checkpoint_submission = CheckpointSubmissionFinder.latest_for_checkpoint_content_file_for_user_in_cohort( + cohort_id: current_cohort.id, + content_file: current_content_file, + user_id: current_user.id + ) + + if current_content_file.autoscore + scas = SubmittedChallengeAnswer.includes(:challenge).where( + checkpoint_submission_id: latest_checkpoint_submission.id, + user: current_user, + cohort: current_cohort + ) + + failed_scas = scas.where(status: SubmittedChallengeAnswer::FAILURE_STATUSES.values) + + if failed_scas.any? + results["errors"] = failed_scas.map do |sca| + { + challenge_num: sca.challenge.position, + challenge_err: sca.test_results.empty? ? sca.status.gsub("_", " ").titleize : sca.test_results + } + end + + scas.update_all(checkpoint_submission_id: nil) + Performance.where(checkpoint_submission_id: latest_checkpoint_submission.id).delete_all + latest_checkpoint_submission.destroy + + render json: results and return + end + + if scas.where(status: SubmittedChallengeAnswer::UNGRADED_STATUSES[:processing]).empty? + results = assign_checkpoint_results(latest_checkpoint_submission, scas) + end + else + results["url"] = cohort_checkpoint_submission_path(current_cohort, latest_checkpoint_submission) + end + + render json: results + end + + def update + authorize(current_submitted_challenge_answer) + + new_attributes = submitted_challenge_answer_params + new_attributes[:manual_grader_id] = current_user.id if SubmittedChallengeAnswer.new(new_attributes).graded? + + if current_submitted_challenge_answer.update(new_attributes) && current_submitted_challenge_answer.checkpoint_submission + current_submitted_challenge_answer.checkpoint_submission.update_points + if current_submitted_challenge_answer.checkpoint_submission.done? + NotificationService.checkpoint_challenge_graded( + user: current_submitted_challenge_answer.user, + cohort: current_submitted_challenge_answer.cohort, + content_file: current_content_file, + checkpoint_submission: current_submitted_challenge_answer.checkpoint_submission, + challenge: current_submitted_challenge_answer.challenge + ) + end + end + + render json: { + submitted_challenge_answer_presenter: SubmittedChallengeAnswerPresenter.new( + current_submitted_challenge_answer, + true + ) + }, status: 200 + end + + def cancel + authorize(current_submitted_challenge_answer) + + if current_submitted_challenge_answer.processing? + current_submitted_challenge_answer.update( + status: SubmittedChallengeAnswer::STATUSES[:canceled], + test_results: "Tests canceled by student." + ) + end + + render json: { submittedChallengeAnswer: + SubmittedChallengeAnswerPresenter.new(current_submitted_challenge_answer, instructor_or_admin) } + end + + def show + authorize(current_submitted_challenge_answer) + + render json: SubmittedChallengeAnswerPresenter.new(current_submitted_challenge_answer, instructor_or_admin) + end + + private + + def assign_checkpoint_results(latest_checkpoint_submission, scas) + results = { + "url": cohort_checkpoint_submission_path(current_cohort, latest_checkpoint_submission), + "created_at": latest_checkpoint_submission.created_at, + "state": latest_checkpoint_submission.state, + "cohort_mode": current_cohort.mode, + "correct_challenges": scas.select {|sca| sca.status == SubmittedChallengeAnswer::GRADED_STATUSES[:correct] }.count, + "total_challenges": scas.count, + "total_points": scas.map { |sa| sa.challenge.points }.flatten.reduce(:+), + "correct_points": scas.find_all do |sa| + sa.points if sa.status == SubmittedChallengeAnswer::STATUSES[:correct] || sa.challenge.can_assign_partial_credit + end.map(&:points).reduce(:+) || 0 + } + + perf = latest_checkpoint_submission.performances.order("created_at DESC").last + results["mastery_score"] = perf.score if perf.present? + + results + end + + def previous_checkpoint_submission?(submission_id) + challenge_ids = params[:submitted_challenge_answers]&.map { |sca| sca[:challenge_id] } + return false if challenge_ids.blank? + + SubmittedChallengeAnswer.where(user: current_user, challenge_id: challenge_ids). + where("checkpoint_submission_id != ?", submission_id). + map(&:checkpoint_submission_id).compact.any? + end + + def create_submitted_challenge_answer_draft(challenge, answer) + submitted_challenge_answer = SubmittedChallengeAnswer.new( + cohort: current_cohort, + answer: answer.nil? ? "" : answer, + challenge_id: challenge.id, + checkpoint_submission_id: nil, + user: current_user, + status: SubmittedChallengeAnswer::UNGRADED_STATUSES[:draft], + hints_shown: 0, + test_results: "" + ) + submitted_challenge_answer.save! + submitted_challenge_answer + end + + def instructor_or_admin + current_user.instructor_or_admin?(current_cohort.id) + end + + def current_cohort + @cohort ||= CohortPolicy::Scope.new(current_user, Cohort).resolve.find_by!(id: params[:cohort_id]) + end + + def current_content_file + @current_content_file ||= ContentFile.unscoped.find(params[:content_file_id]) + end + + def current_submitted_challenge_answer + @submitted_challenge_answer ||= SubmittedChallengeAnswer.find(params[:id]) + end + + def find_or_create_checkpoint_submission(user_id) + checkpoint_submission = CheckpointSubmission.find_by( + cohort_id: params[:cohort_id], + user_id: user_id, + content_file_block_id: current_content_file.standard.release.block_id, + content_file_uid: current_content_file.uid, + state: CheckpointSubmission::STATES[:started] + ) + if checkpoint_submission.nil? + checkpoint_submission = CheckpointSubmission.create!( + cohort_id: params[:cohort_id], + user_id: user_id, + content_file_block_id: current_content_file.standard.release.block_id, + content_file_uid: current_content_file.uid, + state: CheckpointSubmission::STATES[:needs_review] + ) + else + # TODO: save on a db transaction by waiting until later? + checkpoint_submission.update(state: CheckpointSubmission::STATES[:needs_review]) + end + checkpoint_submission + end + + def create_pair_submissions(checkpoint_submission) + student_pair_ids = params[:pairs].keys.map(&:to_i).delete_if { |id| id == current_user.id } + submissions = student_pair_ids.map do |user_id| + unless current_content_file.max_checkpoint_submissions.nil? + user = User.find(user_id) + user_attempts = user.checkpoint_submissions_count( + current_cohort.id, + current_content_file.standard.release.block_id, + current_content_file.uid + ) + next if user_attempts >= current_content_file.max_checkpoint_submissions + end + + find_or_create_checkpoint_submission(user_id) + end.compact + checkpoint_submission.update(pair_submission_ids: submissions.map(&:id)) unless submissions.empty? + submissions.each do |sub| + ids = [checkpoint_submission.id, submissions.map(&:id).reject { |id| id == sub.id}].flatten + sub.update(pair_submission_ids: ids) + end + end + + def submitted_challenge_answer_params + params.require(:submitted_challenge_answer).permit(:status, :points) + end + end + end +end diff --git a/scripts/app/controllers/cohorts/content_files_controller.rb b/scripts/app/controllers/cohorts/content_files_controller.rb new file mode 100644 index 0000000000000000000000000000000000000000..8302e25e584cdc729d3e0870b74db8a772c02bb9 --- /dev/null +++ b/scripts/app/controllers/cohorts/content_files_controller.rb @@ -0,0 +1,260 @@ +module Cohorts + class ContentFilesController < ApplicationController + def visibility + authorize(current_cohort, :submissions_dashboard?) + query = ContentVisibility.where( + cohort_id: current_cohort.id, + content_type: "ContentFile", + content_uid: current_content_file.uid, + visibility_type: params[:visibility_type] + ) + if request.method == "PUT" + query.first_or_create! + elsif request.method == "DELETE" + query.delete_all + end + render json: { success: true } + end + + def student_summary_csv + authorize(current_cohort, :submissions_dashboard?) + data = ActiveRecord::Base.connection.exec_query( + student_summary_sql, + "SQL", + [[nil, current_cohort.id], [nil, current_content_file.uid], [nil, current_block_id]] + ) + head :no_content and return if data.empty? + data = add_topics(data) + respond_to do |format| + format.html { redirect_to cohort_path(current_cohort) } + format.csv do + csv_headers("student_summary") + self.response_body = build_csv_enumerator(data[0].map {|k, _| k }, data) + end + end + end + + def challenge_data_csv + authorize(current_cohort, :submissions_dashboard?) + sql_query_params = [[nil, current_cohort.id], [nil, current_content_file.uid], [nil, current_block_id]] + data = ActiveRecord::Base.connection.exec_query(challenge_data_sql, + "SQL", + sql_query_params) + head :no_content and return if data.empty? + respond_to do |format| + format.html { redirect_to cohort_path(current_cohort) } + format.csv do + csv_headers("challenge_data") + self.response_body = build_csv_enumerator(data[0].map {|k, _| k }, data) + end + end + end + + private + + def add_topics(data) + group_checkpoint_submissions = CheckpointSubmission.includes(submitted_challenge_answers: :challenge). + where(id: data.map { |r| r["checkpoint_submission_id"] }). + group_by(&:id) + + data.map do |r| + if r["checkpoint_submission_id"] + submission = group_checkpoint_submissions[r["checkpoint_submission_id"]][0] + ungraded = submission.state == CheckpointSubmission::STATES[:needs_review] && + submission.submitted_challenge_answers.map(&:points).include?(nil) + + topic_hash = parse_checkpoint_topics(submission, ungraded) + if ungraded + topic_hash.merge!("points_earned" => nil, "score_percentage" => nil) + end + r.to_hash.merge(topic_hash) + else + r.to_hash.merge(parse_checkpoint_topics(nil, false)) + end + end + end + + def parse_checkpoint_topics(submission, ungraded) + topic_hash = content_file_topics.each_with_object({}) do |topic, hash| + pts_earned = checkpoint_submission_pts_earned(submission, topic, ungraded) + pts_avail = content_file_pts_avail[topic] + + if submission && pts_avail && pts_earned + percent = (((pts_earned).to_f / (pts_avail).to_f) * 100).to_i + end + hash["topic:#{topic}_pts_earned"] = pts_earned + hash["topic:#{topic}_pts_avail"] = pts_avail + hash["topic:#{topic}_percent"] = percent + end + topic_hash + end + + def content_file_pts_avail + @content_file_pts_avail ||= content_file_topics.each_with_object({}) do |topic, hash| + hash[topic] = current_content_file.challenges.select {|c| c.topics&.include?(topic)}.map(&:points).compact.reduce(:+) + end + end + + def checkpoint_submission_pts_earned(submission, topic, ungraded) + return nil if submission.nil? || ungraded + + matching_scas = submission.submitted_challenge_answers.select { |sca| sca.challenge.topics&.include?(topic)} + matching_scas.map(&:points).compact.reduce(:+) + end + + def content_file_topics + @content_file_topics ||= current_content_file.challenges.map(&:topics).flatten.compact.uniq + @content_file_topics + end + + def current_content_file + @current_content_file ||= ContentFile.unscoped.find(params[:id]) + end + + def current_cohort + @current_cohort ||= CohortPolicy::Scope.new(current_user, Cohort).resolve.find(params[:cohort_id]) + end + + def current_block_id + @current_block ||= current_content_file.standard.release.block_id + end + + def csv_filename(type) + "#{type}-#{current_content_file.title}-cohort#{current_cohort.id}-#{Time.zone.now.to_date.to_s(:default)}.csv" + end + + def csv_headers(filename) + headers["X-Accel-Buffering"] = "no" + headers["Cache-Control"] = "no-cache" + headers["Content-Type"] = "text/csv; charset=utf-8" + headers["Content-Disposition"] = + %(attachment; filename="#{csv_filename(filename)}") + headers["Last-Modified"] = Time.zone.now.ctime.to_s + end + + def build_csv_enumerator(header, data) + Enumerator.new do |y| + CsvBuilder.new(header, data, y).build + end + end + + class CsvBuilder + attr_accessor :output, :header, :data + + def initialize(header, data, output = "") + @output = output + @header = header + @data = data + end + + def build + output << CSV.generate_line(header) + data.each do |row| + output << CSV.generate_line(row.map {|_, v| v}) + end + output + end + end + + def challenge_data_sql + <<~SQL + WITH checkpoint_attempts AS ( + SELECT DISTINCT id, user_id FROM checkpoint_submissions AS cs + WHERE cs.cohort_id = $1 AND cs.content_file_uid = $2 AND cs.content_file_block_id = $3 + ), + checkpoint_user_attempts AS ( + SELECT id, ROW_NUMBER () OVER (PARTITION BY user_id ORDER BY id) from checkpoint_attempts + ) + SELECT u.first_name, + u.last_name, + u.email, + c.challenge_type, + c.position, + c.title, + c.topics, + c.question, + c.answer AS correct_answer, + sca.answer AS student_answer, + c.points AS possible_points, + sca.points AS student_points, + sca.created_at AS submitted_at, + cs.content_file_uid, + cs.state AS checkpoint_state, + cs.id AS checkpoint_id, + c.uid AS challenge_uid, + cua.row_number AS attempt_number + FROM submitted_challenge_answers AS sca + JOIN users AS u + ON sca.user_id = u.id + JOIN cohort_users AS cu + ON cu.user_id = u.id + JOIN checkpoint_submissions AS cs + ON cs.id = sca.checkpoint_submission_id + JOIN challenges AS c + ON c.id = sca.challenge_id + JOIN checkpoint_user_attempts AS cua + ON cua.id = cs.id + WHERE sca.cohort_id = $1 + AND cu.roles = '{}' + AND cu.cohort_id = $1 + AND sca.checkpoint_submission_id IS NOT NULL + AND cs.content_file_uid = $2 + AND cs.content_file_block_id = $3 + AND sca.status <> 'draft' + AND cs.state <> 'started' + ORDER BY u.last_name, + c.position, + cs.created_at + SQL + end + + def student_summary_sql + <<~SQL + SELECT u.first_name, + u.last_name, + u.email, + CASE + WHEN u.last_viewed_cohort_id IS NOT NULL THEN 'y' + ELSE 'n' + END AS has_logged_in, + CASE + WHEN cs.id IS NULL THEN NULL + ELSE Row_number() + OVER( + partition BY u.id + ORDER BY cs.id ) + END AS attempt_number, + cs.id AS checkpoint_submission_id, + cs.state AS submission_state, + cs.created_at AS attempt_started, + cs.submitted_at AS attempt_submitted, + Sum(sca.points) AS points_earned, + Sum(c.points) AS total_points, + 100 * Sum(sca.points) / Sum(c.points) AS score_percentage + FROM users AS u + JOIN cohort_users AS cu + ON cu.user_id = u.id + LEFT JOIN checkpoint_submissions AS cs + ON cs.user_id = u.id + AND cs.cohort_id = cu.cohort_id + AND (cs.content_file_uid = $2 OR cs.content_file_uid IS NULL) + AND cs.content_file_block_id = $3 + LEFT JOIN submitted_challenge_answers AS sca + ON sca.checkpoint_submission_id = cs.id + LEFT JOIN challenges AS c + ON c.id = sca.challenge_id + WHERE cu.cohort_id = $1 + AND cu.roles = '{}' + GROUP BY u.id, + u.first_name, + u.last_name, + u.email, + u.last_viewed_cohort_id, + cs.id + ORDER BY has_logged_in DESC, + u.last_name, + attempt_number + SQL + end + end +end diff --git a/scripts/app/controllers/cohorts/pairings_controller.rb b/scripts/app/controllers/cohorts/pairings_controller.rb new file mode 100644 index 0000000000000000000000000000000000000000..c4f872b472e4d2e9dc6ec34d5c8750c38acdb685 --- /dev/null +++ b/scripts/app/controllers/cohorts/pairings_controller.rb @@ -0,0 +1,49 @@ +module Cohorts + class PairingsController < ApplicationController + def index + authorize(current_cohort, :partnerup?) + + pairings = Pairing.where(cohort_id: current_cohort.id).order(created_at: :desc) + + render json: pairings + end + + def create + authorize(current_cohort, :partnerup?) + + pairing = Pairing.create!(pairing_params.merge(cohort_id: current_cohort.id)) + + render json: pairing + end + + def update + authorize(current_cohort, :partnerup?) + + loaded_pairing.update!(pairing_params) + + render json: loaded_pairing + end + + def destroy + authorize(current_cohort, :partnerup?) + + loaded_pairing.destroy! + + render json: { ack: "ok" } + end + + protected + + def pairing_params + @pairing_params ||= params.require(:pairing).permit(:size, :title, :groups) + end + + def loaded_pairing + @loaded_pairing ||= Pairing.find(params[:id]) + end + + def current_cohort + @current_cohort ||= CohortPolicy::Scope.new(current_user, Cohort).resolve.find(params[:cohort_id]) + end + end +end diff --git a/scripts/app/controllers/cohorts/standards_controller.rb b/scripts/app/controllers/cohorts/standards_controller.rb new file mode 100644 index 0000000000000000000000000000000000000000..4a3ce64f644092ecb40dc327ef659164d254c626 --- /dev/null +++ b/scripts/app/controllers/cohorts/standards_controller.rb @@ -0,0 +1,43 @@ +module Cohorts + class StandardsController < ApplicationController + def show + authorize(current_cohort, :submissions_dashboard?) + all_checkpoints = CheckpointSubmission.where(content_file_block_id: current_cohort.block_ids, + cohort_id: current_cohort.id). + where.not(state: CheckpointSubmission::STATES[:started]). + order(created_at: :desc) + result = StandardSubmissionsService. + new(standard: current_standard, cohort: current_cohort, + all_checkpoints: all_checkpoints, + instructor_or_admin: false).single(show_hidden: true) + + render json: result + end + + def visibility + authorize(current_cohort, :submissions_dashboard?) + query = ContentVisibility.where( + cohort_id: current_cohort.id, + content_type: "Standard", + content_uid: current_standard.uid, + visibility_type: params["visibility_type"] + ) + if request.method == "PUT" + query.first_or_create! + elsif request.method == "DELETE" + query.delete_all + end + render json: { success: true } + end + + private + + def current_standard + @current_standard ||= Standard.find(params[:id]) + end + + def current_cohort + @current_cohort ||= CohortPolicy::Scope.new(current_user, Cohort).resolve.find(params[:cohort_id]) + end + end +end diff --git a/scripts/app/controllers/cohorts/submitted_challenge_answers/activities_controller.rb b/scripts/app/controllers/cohorts/submitted_challenge_answers/activities_controller.rb new file mode 100644 index 0000000000000000000000000000000000000000..0df2ebd1d05733f75e8fce84f402c5ce98809830 --- /dev/null +++ b/scripts/app/controllers/cohorts/submitted_challenge_answers/activities_controller.rb @@ -0,0 +1,84 @@ +module Cohorts + module SubmittedChallengeAnswers + class ActivitiesController < ApplicationController + before_action :current_cohort + + def create + activity = Activity.new( + content: activity_content[:content], + subject: current_submitted_challenge_answer, + creator: current_user, + name: Activity::NAMES[:comment_created], + cohort: current_cohort + ) + + authorize(activity, :create_for_submitted_challenge_answer?) + + if activity.save + challenge = current_submitted_challenge_answer.challenge + content_file = challenge.content_file + standard = content_file.standard + block = standard.release.block + + url = cohort_user_challenge_url( + current_cohort, + current_submitted_challenge_answer.user, + current_submitted_challenge_answer.challenge + ) + if current_submitted_challenge_answer.user_id == current_user.id + current_cohort.instructors.each do |instructor| + instructor.send_slack_message( + slack_message_content( + commentor: current_user, + challenge: current_submitted_challenge_answer.challenge, + url: url + ) + ) + + instructor.notify( + tagline: "Comment Left", + title: + "#{current_user.full_name} commented on #{block.title} - #{standard.title} - #{content_file.title} - #{challenge.title}", + url: cohort_user_challenge_url(current_cohort, current_submitted_challenge_answer.user, challenge), + description: "\"#{activity_content[:content]}\"" + ) + end + else + current_submitted_challenge_answer.user.send_slack_message( + slack_message_content(commentor: current_user, challenge: current_submitted_challenge_answer.challenge, url: url) + ) + + current_submitted_challenge_answer.user.notify( + tagline: "Comment Left", + title: + "#{current_user.full_name} commented on #{block.title} - #{standard.title} - #{content_file.title} - #{challenge.title}", + url: cohort_user_challenge_url(current_cohort, current_submitted_challenge_answer.user, challenge), + description: "\"#{activity_content[:content]}\"" + ) + end + render json: ActivityPresenter.new(activity), status: 200 + else + render json: { error: activity.errors.full_messages }, status: 400 + end + end + + private + + def activity_content + params.require(:activity).permit(:content) + end + + def current_cohort + @current_cohort ||= CohortPolicy::Scope.new(current_user, Cohort).resolve.find_by!(id: params[:cohort_id]) + end + + def current_submitted_challenge_answer + @submitted_challenge_answer ||= SubmittedChallengeAnswer.find(params[:submitted_challenge_answer_id]) + end + + def slack_message_content(commentor:, challenge:, url:) + "#{commentor.full_name} left a comment on #{challenge.title}. Read it here: #{url}." + end + end + end +end diff --git a/scripts/app/controllers/cohorts/users/challenges_controller.rb b/scripts/app/controllers/cohorts/users/challenges_controller.rb new file mode 100644 index 0000000000000000000000000000000000000000..ca13285e6444fb8fcabb2bc929c5be75ed6856bb --- /dev/null +++ b/scripts/app/controllers/cohorts/users/challenges_controller.rb @@ -0,0 +1,182 @@ +module Cohorts + module Users + class ChallengesController < ApplicationController + def show + current_user.admin? ? skip_authorization : authorize(loaded_cohort_user, :view_user_challenges?) + + @html_title = "#{loaded_user.full_name}: #{current_challenge.title}" + + is_instructor_or_admin = current_user.instructor_or_admin?(current_cohort.id) + submitted_challenge_answers = SubmittedChallengeAnswerFinder.all_for_user_id_challenge_in_cohort(loaded_user.id, + current_challenge, + current_cohort.id). + where.not(status: SubmittedChallengeAnswer::UNGRADED_STATUSES[:draft]) + + activities = Activity.where( + subject_type: SubmittedChallengeAnswer.to_s, + name: Activity::NAMES[:comment_created], + subject_id: submitted_challenge_answers&.map(&:id) + ).order(created_at: :asc) + + render( + locals: { + challenge: ChallengeWithSubmittedChallengeAnswersPresenter.new( + current_cohort, + current_challenge, + submitted_challenge_answers, + is_instructor_or_admin, + user_context: loaded_user, + is_checkpoint: false + ), + block_id: current_release.block_id, + content_file_path: current_challenge.content_file.path, + content_file_position: current_challenge.content_file.position, + content_file_type: current_challenge.content_file.content_file_type, + activities: activities.order(created_at: :asc).map { |activity| ActivityPresenter.new(activity) }, + standard: StandardCardComponentProps.new(standard: current_challenge.standard), + loaded_user: loaded_user_details, + is_instructor_or_admin: is_instructor_or_admin, + cohort_id: current_cohort.id, + cohort_mode: current_cohort.mode, + previous_challenge_path: previous_challenge, + next_challenge_path: next_challenge + } + ) + end + + private + + def loaded_user_details + { id: loaded_user.id, profile_image: loaded_user.profile_image, full_name: loaded_user.full_name, initials: loaded_user.initials } + end + + def loaded_cohort_user + @loaded_cohort_user ||= CohortUser.find_by!(user_id: user_params[:user_id], cohort_id: cohort_params[:cohort_id]) + end + + def loaded_user + @loaded_user ||= UserPolicy::Scope.new(current_user, User).resolve.find(user_params[:user_id]) + end + + def current_cohort + @current_cohort ||= CohortPolicy::Scope.new(current_user, Cohort).resolve.find(cohort_params[:cohort_id]) + end + + def current_challenge + @current_challenge ||= Challenge.find(challenge_params[:id]) + end + + def current_standard + @current_standard ||= current_challenge.content_file.standard + end + + def current_release + @current_release ||= current_standard.release + end + + def current_cohort_release + @current_cohort_release ||= CohortRelease.joins(release: :standards). + where("cohort_releases.cohort_id = ? AND standards.uid = ?", current_cohort.id, current_standard.uid). + first + end + + def user_params + params.permit(:user_id) + end + + def cohort_params + params.permit(:cohort_id) + end + + def challenge_params + params.permit(:id) + end + + def previous_challenge + # first check in content file + previous_in_cf = SubmittedChallengeAnswerFinder.latest_for_user_id_content_file_for_cohort( + loaded_user.id, current_challenge.content_file, current_cohort.id + ).joins(:challenge). + where("challenges.position < ?", current_challenge.position). + order("challenges.position DESC").first + return previous_in_cf.review_path(cohort_id: current_cohort.id, user_id: loaded_user.id) if previous_in_cf.present? + + # check in previous content files in standard + previous_in_standard = SubmittedChallengeAnswer.joins(challenge: [content_file: [standard: :release]]). + where("submitted_challenge_answers.user_id = ?", loaded_user.id). + where("content_files.position < ?", current_challenge.content_file.position). + where("standards.uid = ?", current_standard.uid). + where("releases.block_id = ?", current_release.block_id). + where("submitted_challenge_answers.cohort_id = ?", current_cohort.id). + no_orphans. + order("content_files.position DESC, challenges.position DESC").first + return previous_in_standard.review_path(cohort_id: current_cohort.id, user_id: loaded_user.id) if previous_in_standard.present? + + # check in previous standards in block + previous_in_block = SubmittedChallengeAnswer.joins(challenge: [content_file: [standard: :release]]). + where("submitted_challenge_answers.user_id = ?", loaded_user.id). + where("standards.position < ?", current_standard.position). + where("releases.block_id = ?", current_release.block_id). + where("submitted_challenge_answers.cohort_id = ?", current_cohort.id). + no_orphans. + order("standards.position DESC, content_files.position DESC, challenges.position DESC").first + return previous_in_block.review_path(cohort_id: current_cohort.id, user_id: loaded_user.id) if previous_in_block.present? + + # check in previous blocks + previous_out_block = SubmittedChallengeAnswer.joins(challenge: [content_file: [standard: [release: :cohort_releases]]]). + where("submitted_challenge_answers.user_id = ?", loaded_user.id). + where("cohort_releases.position < ?", current_cohort_release.position). + where("submitted_challenge_answers.cohort_id = ?", current_cohort.id). + no_orphans. + order("cohort_releases.position DESC"). + order("standards.position DESC"). + order("content_files.position DESC"). + order("challenges.position DESC").first + return previous_out_block.review_path(cohort_id: current_cohort.id, user_id: loaded_user.id) if previous_out_block.present? + end + + def next_challenge + # first check in content file + next_in_cf = SubmittedChallengeAnswerFinder.latest_for_user_id_content_file_for_cohort( + loaded_user.id, current_challenge.content_file, current_cohort.id + ).joins(:challenge). + where("challenges.position > ?", current_challenge.position). + order("challenges.position ASC").first + return next_in_cf.review_path(cohort_id: current_cohort.id, user_id: loaded_user.id) if next_in_cf.present? + + # check in next content files in standard + next_in_standard = SubmittedChallengeAnswer.joins(challenge: [content_file: [standard: :release]]). + where("submitted_challenge_answers.user_id = ?", loaded_user.id). + where("content_files.position > ?", current_challenge.content_file.position). + where("standards.uid = ?", current_standard.uid). + where("releases.block_id = ?", current_release.block_id). + where("submitted_challenge_answers.cohort_id = ?", current_cohort.id). + no_orphans. + order("content_files.position ASC, challenges.position ASC").first + return next_in_standard.review_path(cohort_id: current_cohort.id, user_id: loaded_user.id) if next_in_standard.present? + + # check in next standards in block + next_in_block = SubmittedChallengeAnswer.joins(challenge: [content_file: [standard: :release]]). + where("submitted_challenge_answers.user_id = ?", loaded_user.id). + where("standards.position > ?", current_standard.position). + where("releases.block_id = ?", current_release.block_id). + where("submitted_challenge_answers.cohort_id = ?", current_cohort.id). + no_orphans. + order("standards.position ASC, content_files.position ASC, challenges.position ASC").first + return next_in_block.review_path(cohort_id: current_cohort.id, user_id: loaded_user.id) if next_in_block.present? + + # check in next blocks + next_out_block = SubmittedChallengeAnswer.joins(challenge: [content_file: [standard: [release: :cohort_releases]]]). + where("submitted_challenge_answers.user_id = ?", loaded_user.id). + where("cohort_releases.position > ?", current_cohort_release.position). + where("submitted_challenge_answers.cohort_id = ?", current_cohort.id). + no_orphans. + order("cohort_releases.position ASC"). + order("standards.position ASC"). + order("content_files.position ASC"). + order("challenges.position ASC").first + return next_out_block.review_path(cohort_id: current_cohort.id, user_id: loaded_user.id) if next_out_block.present? + end + end + end +end diff --git a/scripts/app/controllers/cohorts/users/performances_controller.rb b/scripts/app/controllers/cohorts/users/performances_controller.rb new file mode 100644 index 0000000000000000000000000000000000000000..e5e34dfda6e6922ff335038e6dd6841bb032f229 --- /dev/null +++ b/scripts/app/controllers/cohorts/users/performances_controller.rb @@ -0,0 +1,41 @@ +module Cohorts + module Users + class PerformancesController < ApplicationController + def create + performances = [] + + performance_params["standard_ids"].each do |standard_id| + performance = Performance.new( + user: loaded_user, + updator: current_user, + score: performance_params["score"], + standard_id: standard_id, + cohort_id: current_cohort.id, + checkpoint_submission_id: performance_params["checkpoint_submission_id"] + ) + + authorize(performance) + + next unless performance.save + + performances << PerformancePresenter.new(performance) + end + render json: performances + end + + private + + def performance_params + params.require(:performance).permit(:checkpoint_submission_id, :score, standard_ids: []) + end + + def current_cohort + @current_cohort ||= Cohort.find(params[:cohort_id]) + end + + def loaded_user + @loaded_user ||= User.find(params[:user_id]) + end + end + end +end diff --git a/scripts/app/controllers/cohorts/users_controller.rb b/scripts/app/controllers/cohorts/users_controller.rb new file mode 100644 index 0000000000000000000000000000000000000000..034a6762a1535d80c0725859454546800056c118 --- /dev/null +++ b/scripts/app/controllers/cohorts/users_controller.rb @@ -0,0 +1,154 @@ +module Cohorts + class UsersController < ApplicationController + def submissions_dashboard + authorize(loaded_cohort_user, :view_user_challenges?) + + @current_category = "submissions" + students = [{ + id: loaded_user.id, + avatar: loaded_user.profile_image, + name: loaded_user.full_name, + initials: loaded_user.initials, + challenges_path: submissions_dashboard_cohort_user_path(current_cohort, loaded_user.id) + }] + + submission_data = SubmissionsDashboardPresenter.submission_data( + cohort: current_cohort, + cohort_mode: current_cohort.mode, + student_ids: [loaded_user.id] + ) + + render( + locals: { + cohort_id: current_cohort.id, + students: students, + submission_data: submission_data, + progress_thresholds: current_cohort.settings["progress_thresholds"] + } + ) + end + + def curriculum_progress + authorize(current_cohort) + + cohort_releases = ReleaseFinder.for_cohort(current_cohort.id).includes(standards: [{ content_files: :challenges }]).to_a + + hidden_uids = ContentVisibility.where(cohort_id: current_cohort.id, content_type: "Standard").pluck(:content_uid) + all_standards = cohort_releases.flat_map(&:standards).reject {|s| hidden_uids.include?(s.uid) }.sort_by(&:position) + + all_checkpoints = CheckpointSubmission.where( + content_file_block_id: cohort_releases.map(&:block_id), + user_id: current_user.id, + cohort_id: current_cohort.id + ).where.not(state: CheckpointSubmission::STATES[:started]). + order(created_at: :desc).group_by(&:content_file_block_id) + + completion_data = StandardSubmissionsService.new(standard: nil, + cohort: current_cohort, + cohort_releases: cohort_releases, + standard_uids: all_standards.map(&:uid), + student_ids: [current_user.id], + all_checkpoints: all_checkpoints.values.flatten, + instructor_or_admin: current_user.instructor_or_admin?(current_cohort.id)).all_standards + + progress = {} + user_performances = PerformanceFinder.latest_for_standards_in_cohort(standard_uids: all_standards.map(&:uid), + user_ids: [current_user.id], + cohort_id: current_cohort.id). + includes(:checkpoint_submission). + to_a + + if current_cohort.mode == Cohort::MODES[:mastery] + progress = calc_progress(Cohort::MODES[:mastery], cohort_releases, user_performances, nil, current_user.id, nil, all_standards) + progress[:mastery_average] = MasteryAverageService.execute(user_performances&.select { |p| p.score > 0 }&.map(&:score)) + elsif current_cohort.mode == Cohort::MODES[:percentage] + lesson_visits = LessonVisit.visible_for(current_cohort.id). + where(user_id: current_user.id, block_id: cohort_releases.map(&:block_id)). + order(updated_at: :desc) + progress = calc_progress(Cohort::MODES[:percentage], + cohort_releases, nil, + completion_data, + current_user.id, + lesson_visits, + all_standards) + end + + cohort_has_checkpoints, user_has_checkpoints, user_has_scored_submission = checkpoint_presence(completion_data) + render json: { + checkpoint_average: user_has_scored_submission ? progress[:checkpoint_average] : nil, + user_has_checkpoints: user_has_checkpoints, + cohort_uses_checkpoints: cohort_has_checkpoints, + progress: progress.to_json + } + end + + private + + # checkpoint_presence returns three booleans + # the first is the presence of a checkpoint + # the second is the presence of a user's checkpoint + # the third is the presence of a done checkpoint + def checkpoint_presence(completion_data) + cohort_has_checkpoint = false + user_has_checkpoint_submission = false + user_has_scored_submission = false + + completion_data.each_key do |release_id| + completion_data[release_id].each_key do |standard_id| + checkpoint = completion_data[release_id][standard_id].find {|cf| cf[:content_file_type] == ContentFile::TYPES[:checkpoint] } + + if checkpoint + cohort_has_checkpoint = true + user_submission = checkpoint[:checkpoint_submissions][current_user.id] + + user_has_checkpoint_submission = true if user_submission.present? + user_has_scored_submission = true if user_submission && user_submission[:state] == CheckpointSubmission::STATES[:done] + end + end + end + [cohort_has_checkpoint, user_has_checkpoint_submission, user_has_scored_submission] + end + + def calc_progress(cohort_mode, _cohort_releases, performances, completion_data, user_id, lesson_visits, standards) + if cohort_mode == Cohort::MODES[:mastery] + return nil unless current_user.student_of?(current_cohort) && !performances.nil? + + CurriculumProgressService.new(performances: performances, user_id: user_id, standards: standards).execute_mastery + elsif cohort_mode == Cohort::MODES[:percentage] + return nil unless current_user.student_of?(current_cohort) && !completion_data.nil? + + CurriculumProgressService.new( + completion_data: completion_data, + user_id: user_id, + lesson_visits: lesson_visits, + standards: standards + ).execute_completion + end + end + + def set_segment_page_properties + @track_properties = { + productId: current_cohort.uid, + pageType: "Submissions", + cohortId: current_cohort.id, + cohortName: current_cohort.name, + blockId: nil, + blockName: nil, + unitId: nil, + unitName: nil + }.to_json + end + + def loaded_cohort_user + @loaded_cohort_user ||= CohortUser.find_by!(user_id: params[:id], cohort_id: params[:cohort_id]) + end + + def loaded_user + @loaded_user ||= UserPolicy::Scope.new(current_user, User).resolve.find(params[:id]) + end + + def current_cohort + @current_cohort ||= CohortPolicy::Scope.new(current_user, Cohort).resolve.find(params[:cohort_id]) + end + end +end diff --git a/scripts/app/controllers/cohorts_controller.rb b/scripts/app/controllers/cohorts_controller.rb new file mode 100644 index 0000000000000000000000000000000000000000..5aa9eff3c8a9ebcf3b2ebb811e1aacd8251149e3 --- /dev/null +++ b/scripts/app/controllers/cohorts_controller.rb @@ -0,0 +1,512 @@ +class CohortsController < ApplicationController + before_action :handle_last_setup_visit, only: %i[setup users content partnerup] + + def new + authorize(Cohort) + + @cohort = Cohort.new() + @controller_action = 'create' + @button_text = 'Create Cohort' + end + + def create + authorize(Cohort) + + cohort = Cohort.new(cohort_params) + cohort.uid = SecureRandom.hex[0...18] + + if cohort.save + redirect_to setup_cohort_path(cohort) + else + flash[:error] = cohort.errors.full_messages.join(", ") + render :new + end + + end + + def edit + authorize(current_cohort) + + @cohort = current_cohort + @controller_action = 'update_cohort' + @button_text = 'Update Cohort' + end + + def update_cohort + authorize(current_cohort) + + @cohort = current_cohort + + if @cohort.update(cohort_params) + redirect_to setup_cohort_path(@cohort) + else + flash[:error] = @cohort.errors.full_messages.join(", ") + render :edit + end + end + + def index + authorize(Cohort) + @html_title = "Cohorts" + @page_type = "Cohorts" + + own_cohorts = current_user.cohorts.without_sandboxes.order(name: :asc) + all_other_cohorts = Cohort.all.without_sandboxes.where.not(id: own_cohorts.pluck(:id)).order(name: :asc) + cohort_types = own_cohorts.map(&:product_type).concat(all_other_cohorts.map(&:product_type)).uniq.compact.reject(&:blank?) + cohort_campuses = own_cohorts.map(&:campus_name).concat(all_other_cohorts.map(&:campus_name)).uniq.compact.reject(&:blank?) + cohort_id_counts = Hash[CohortUser.all.pluck(:cohort_id).group_by { |x| x }.map { |k, v| [k, v.count] }] + + render( + locals: { + all_other_cohorts: all_other_cohorts, + own_cohorts: own_cohorts, + cohort_id_counts: cohort_id_counts, + learn_base_url: "", + cohort_types: cohort_types, + cohort_campuses: cohort_campuses, + new_auth_product_url: "" + } + ) + end + + def update + authorize(current_cohort) + + current_cohort.mode = update_cohort_params[:mode] if update_cohort_params[:mode] + if update_cohort_params.dig("settings", "progress_thresholds") + current_cohort.settings["progress_thresholds"] = update_cohort_params[:settings]["progress_thresholds"] + end + + flash = if current_cohort.save + { type: "success", msg: "successfully updated #{current_cohort.name}." } + else + { type: "error", msg: "could not update #{current_cohort.name}." } + end + + respond_to do |format| + format.json do |_f| + render json: { cohort: current_cohort, flash: flash } + end + end + end + + def users + authorize(current_cohort) + redirect_to content_cohort_path(current_cohort) and return if current_cohort.sandbox + @html_title = "Setup" + @page_type = "Setup" + @current_category = "setup" + release_count = CohortRelease.attached_to_cohort(current_cohort.id).count + + render( + locals: { + cohort: current_cohort, + release_count: release_count, + auth_product_url: "" + } + ) + end + + def content + authorize(current_cohort) + @html_title = "Setup" + @page_type = "Setup" + @current_category = "setup" + + cohort_releases_by_sections, visibilities = current_cohort.current_curriculum + + render( + locals: { + cohort: current_cohort, + release_count: cohort_releases_by_sections.values.flatten.count, + sections: cohort_releases_by_sections.map do |k, releases| + CohortSetup::Visibility.new( + section: k, + releases_in_sections: releases, + cohort_id: current_cohort.id, + visibilities: visibilities + ) + end, + auth_product_url: "" + } + ) + end + + def show + authorize(current_cohort) + @html_title = current_cohort.name + @page_type = "Curriculum Overview" + @current_category = "curriculum" + @titles_filter = params[:'standard-names']&.split("|") + @body_class = "-with-curriculum-subset-notice" if @titles_filter + + cohort_cohort_releases = CohortRelease.attached_to_cohort(current_cohort.id) + cohort_releases = Release.where(id: cohort_cohort_releases.map(&:release_id)). + includes(standards: [{ content_files: :challenges }]).to_a + + all_standards = cohort_releases.map do |cohort_release| + cohort_release.standards.visible_for(current_cohort.id) + end.flatten.sort_by(&:position) + + visible_content_files = ContentFile.unscoped.where(standard_id: all_standards.map(&:id)) + hidden_content_files = ContentVisibility.where( + cohort: current_cohort, + content_type: "ContentFile", + content_uid: visible_content_files.pluck(:uid) + ) + hidden_content_file_count = hidden_content_files.count + content_file_count = visible_content_files.count + + sections = cohort_cohort_releases. + group_by {|cr| cr.section}. + reject {|section, _crs| section.nil?}. # Legacy data might not have a section + to_a. + sort_by {|section, _crs| section.position}. + map do |section, crs| + release_ids = crs.sort_by(&:position).map(&:release_id) + standards_in_section = all_standards.select { |s| release_ids.include?(s.release_id) } + standards = standards_in_section.reject do |standard| + cfs = visible_content_files.select { |cf| cf.standard_id == standard.id } + cf_count = (cfs.map(&:uid) - hidden_content_files.map(&:content_uid)).count + cf_count == 0 + end + section.as_json.merge( + release_ids: release_ids, + standard_count: standards.count, + standardsForSectionPath: cohort_standards_for_section_path(current_cohort, section.id) + ) + end + + warning_message = no_curriculum_warning( + sections.empty?, + all_standards.empty?, + hidden_content_file_count, + content_file_count + ) + + welcome_to_learn = welcome_to_learn( + sections.empty?, + all_standards.empty?, + hidden_content_file_count, + content_file_count + ) + + last_viewed_presenter = nil + last_viewed_lesson_visit = LessonVisit.visible_for(current_cohort.id). + where(user_id: current_user.id, block_id: cohort_releases.map(&:block_id)). + order(updated_at: :desc).first + unless all_standards.empty? || warning_message != "" || welcome_to_learn + last_viewed_presenter = ContentFilePresenter::ForCurriculumLastViewed.new( + cohort: current_cohort, + standards: all_standards, + last_viewed_lesson_visit: last_viewed_lesson_visit + ) + end + + render( + locals: { + curriculum_progress_path: curriculum_progress_cohort_user_path(current_cohort, current_user), + current_cohort: current_cohort, + content_view: last_viewed_presenter, + sections: sections, + warning_message: warning_message, + warning_count: current_user.instructor_or_admin?(current_cohort.id) ? cohort_releases.flat_map(&:sync_warnings).length : 0, + welcome_to_learn: welcome_to_learn + } + ) + end + + def error + authorize(current_cohort) + + cohort_cohort_releases = CohortRelease.attached_to_cohort(current_cohort.id) + cohort_releases = Release.where(id: cohort_cohort_releases.map(&:release_id)).to_a + + render( + locals: { + warnings: cohort_releases.flat_map(&:sync_warnings).map do |warning| + { warning: warning, anchor: warning.split(":").first } + end + } + ) + end + + def standards_for_section + authorize(current_cohort) + + cohort_cohort_releases = CohortRelease. + attached_to_cohort(current_cohort.id). + where(section_id: params[:section_id]). + includes(release: [standards: [{ content_files: :challenges }]]). + to_a + + cohort_releases = cohort_cohort_releases.map(&:release) + hidden_uids = ContentVisibility.where(cohort_id: current_cohort.id, content_type: "Standard").pluck(:content_uid) + all_standards = cohort_releases.flat_map do |cohort_release| + cohort_release.standards + end.reject {|s| hidden_uids.include?(s.uid) }.sort_by(&:position) + + user_performances = nil + if current_cohort.mode == Cohort::MODES[:mastery] + user_performances = PerformanceFinder.latest_for_standards_in_cohort(standard_uids: all_standards.map(&:uid), + user_ids: [current_user.id], + cohort_id: current_cohort.id).to_a + end + lesson_visits = LessonVisit.visible_for(current_cohort.id). + where(user_id: current_user.id, block_id: cohort_releases.map(&:block_id)). + order(updated_at: :desc) + + all_checkpoints = CheckpointSubmission.includes(submitted_challenge_answers: :challenge).where( + content_file_block_id: cohort_releases.map(&:block_id), + user_id: current_user.id, + cohort_id: current_cohort.id + ).where.not(state: CheckpointSubmission::STATES[:started]). + order(created_at: :desc) + + instructor_or_admin = current_user.instructor_or_admin?(current_cohort.id) + + progress_data = StandardSubmissionsService.new(standard: nil, + cohort: current_cohort, + cohort_releases: cohort_releases, + standard_uids: all_standards.map(&:uid), + student_ids: [current_user.id], + all_checkpoints: all_checkpoints, + instructor_or_admin: instructor_or_admin).all_standards + + cohort_releases = cohort_releases.map do |release| + standards = all_standards.select { |s| s.release_id == release.id } + standards.map do |standard| + cohort_standard_progress_data = progress_data[release.id][standard.id] if progress_data + StandardPresenter::ForStandardCard.new( + standard: standard, + cohort: current_cohort, + user_performances: user_performances, + cohort_standard_progress_data: cohort_standard_progress_data || [], + current_user_id: current_user.id, + instructor_or_admin: instructor_or_admin, + lesson_visits: lesson_visits, + checkpoint_submissions: all_checkpoints + ) + end + end + render json: cohort_releases.to_json + end + + def setup + authorize(current_cohort) + redirect_to content_cohort_path(current_cohort) and return if current_cohort.sandbox + @html_title = "Setup" + @page_type = "Setup" + @current_category = "setup" + + available_blocks = BlockFinder.available_blocks_for(current_cohort.id). + includes(releases: { cohort_releases: :cohort }). + uniq + + render( + locals: { + resync_job: ResyncJobResult.find_latest(current_cohort.id), + successful_resync_job: ResyncJobResult.find_latest_successful(current_cohort.id), + all_available_blocks: available_blocks.map { |block| BlockPresenter::ForBlock.new(block: block) }, + cohort: current_cohort, + cohort_releases: + CohortRelease.attached_to_cohort(current_cohort.id).map do |cohort_release| + CohortReleasePresenter::ForCohortSetup.new(cohort_release: cohort_release) + end, + unattached_block_release_options: + ReleaseFinder.latest_for_all_blocks_not_attached_to_cohort(current_cohort.id), + auth_product_url: "" + } + ) + end + + def partnerup + authorize(current_cohort) + redirect_to content_cohort_path(current_cohort) and return if current_cohort.sandbox + if current_cohort.students.count > 500 + flash[:error] = "Partnerup disabled for cohorts over 500." + redirect_to content_cohort_path(current_cohort) and return + end + + @html_title = "Setup" + @page_type = "Setup" + @current_category = "setup" + release_count = CohortRelease.attached_to_cohort(current_cohort.id).count + + render( + locals: { + cohort: current_cohort, + students: current_cohort.cohort_users.student.map { |s| { uid: s.user.uid, name: s.user.full_name, profile_image: s.user.profile_image }}, + release_count: release_count, + pairings: Pairing.where(cohort_id: current_cohort.id).order(created_at: :desc), + auth_product_url: "" + } + ) + end + + def unit_progress + authorize(current_cohort, :submissions_dashboard?) + redirect_to cohort_path(current_cohort) and return if current_cohort.sandbox + @html_title = "Unit Progress" + @page_type = "Unit Progress" + @current_category = "submissions" + + students = nil + submission_data = nil + if current_cohort.students.length < 500 + student_ids = [] + students = current_cohort.students.order(:first_name).map do |student| + student_ids.push(student.id) + { + id: student.id, + avatar: student.profile_image, + name: student.full_name, + initials: student.initials, + cohort_reg_date: student.cohort_users.where(cohort_id: current_cohort.id).first&.created_at, + challenges_path: submissions_dashboard_cohort_user_path(current_cohort, student.id) + } + end + + submission_data = SubmissionsDashboardPresenter.submission_data( + cohort: current_cohort, + cohort_mode: current_cohort.mode, + student_ids: student_ids + ) + end + + render( + locals: { + cohort_mode: current_cohort.mode, + cohort_id: current_cohort.id, + students: students, + submission_data: submission_data, + progress_thresholds: current_cohort.settings["progress_thresholds"] + } + ) + end + + def activity_dashboard + authorize(current_cohort) + redirect_to cohort_path(current_cohort) and return if current_cohort.sandbox + @html_title = "Activity Dashboard" + @page_type = "Activity" + @current_category = "activity" + + respond_to do |format| + format.html do + render locals: { + url: activity_dashboard_cohort_path(current_cohort, format: :json), + student_count: current_cohort.students.count + } + end + format.json do + render json: ActivityAggregatorService.new(current_cohort, params[:offset], current_user.timezone).execute + end + end + end + + def course_stats + authorize(current_cohort, :progress?) + redirect_to cohort_path(current_cohort) and return if current_cohort.sandbox + @html_title = "Course Stats" + @page_type = "Course Stats" + @current_category = "progress" + + students = nil + if current_cohort.id == 1974 || current_cohort.students.length < 100 + students = CohortStudentProgressService.new(current_cohort).students + end + + render( + locals: { + students: students, + cohort_mode: current_cohort.mode, + progress_thresholds: current_cohort.settings["progress_thresholds"], + cohort_id: current_cohort.id + } + ) + end + + def course_stats_csv + authorize(current_cohort, :progress?) + if current_cohort.id == 1974 + ProgressCsvJob.perform_later(current_cohort.id, current_user.id, false) + else + ProgressCsvJob.perform_later(current_cohort.id, current_user.id, current_cohort.students.length > 100) + end + render json: { ack: "ok" } + end + + def import_users_work + authorize(current_cohort) + ImportStudentWorkJob.perform_later(current_cohort.id, current_user.email) + render json: { ack: "ok" } + end + + def import_users_work_status + authorize(current_cohort) + render json: { student_import_state: current_cohort.student_import_state, + student_import_completed_at: current_cohort.student_import_completed_at } + end + + def feed + authorize(current_cohort) + + render( + locals: { + activities: Activity. + where(cohort: current_cohort). + order(created_at: :desc). + first(50). + map { |activity| ActivityFeedItemComponentProps.execute(activity) }, + current_cohort: current_cohort + } + ) + end + + private + + def current_cohort + @current_cohort ||= params[:cohort_id].present? ? Cohort.find(params[:cohort_id]) : Cohort.find(params[:id]) + end + + # Cohort Create and Update Admin Views. + def cohort_params + params.require(:cohort).permit(:name, :product_type, :campus_name, :starts_on, :ends_on) + end + + def update_cohort_params + params.require(:cohort).permit(:mode, settings: [progress_thresholds: []]) + end + + def handle_last_setup_visit + cohort_user = CohortUser.where(cohort_id: current_cohort.id, user_id: current_user.id).first + return unless cohort_user + + unless cohort_user.last_setup_visit == request.original_fullpath + cohort_user.update(last_setup_visit: request.original_fullpath) + end + end + + def no_curriculum_warning(no_sections, no_standards, hidden_content_file_count, content_file_count) + if current_user.instructor_or_admin?(current_cohort.id) + if no_sections + "It looks like you don't have any curriculum yet. To fix this you can add your Curriculum #{view_context.link_to 'here', setup_cohort_path(current_cohort)}." + elsif no_standards || hidden_content_file_count == content_file_count + "It looks the curriculum is not visible yet. To fix this you can #{view_context.link_to 'change visibility here', content_cohort_path(current_cohort)}." + else + "" + end + else + "" + end + end + + def welcome_to_learn(no_sections, no_standards, hidden_content_file_count, content_file_count) + no_content = no_sections || no_standards || hidden_content_file_count == content_file_count + + return true if !current_user.instructor_or_admin?(current_cohort.id) && no_content + + false + end +end diff --git a/scripts/app/controllers/concerns/api/content_visibility_crud.rb b/scripts/app/controllers/concerns/api/content_visibility_crud.rb new file mode 100644 index 0000000000000000000000000000000000000000..ec247f8b7333e96617239d32e7bd717a0ca7b1e1 --- /dev/null +++ b/scripts/app/controllers/concerns/api/content_visibility_crud.rb @@ -0,0 +1,58 @@ +module Api::ContentVisibilityCrud + extend ActiveSupport::Concern + + included do + def handle_param_logic(content_type, the_params) + result = :error + + return :not_found if content_type.constantize.where(uid: params[:id]).empty? + return :not_found unless exists_in_requested_cohort?(content_type) + + if the_params.key?(:visible) + result = create_content_visibility(content_type, "hidden") if the_params[:visible].to_s == "false" + result = destroy_content_visibility(content_type, "hidden") if the_params[:visible].to_s == "true" + end + + if the_params.key?(:optional) + result = create_content_visibility(content_type, "optional") if the_params[:optional].to_s == "false" + result = destroy_content_visibility(content_type, "optional") if the_params[:optional].to_s == "true" + end + + result + end + + def exists_in_requested_cohort?(content_type) + if content_type == "Standard" + StandardFinder.all_for_cohort(cohort_id: params[:cohort_id]).map(&:uid) + elsif content_type == "ContentFile" + ContentFileFinder.all_from_cohort_block(cohort_id: params[:cohort_id], + block_id: params[:block_id]).map(&:uid) + end.include?(params[:id]) + end + + def create_content_visibility(content_type, visibility_type) + cv = ContentVisibility.new(cohort_id: params[:cohort_id], + content_uid: params[:id], + content_type: content_type, + visibility_type: visibility_type) + if cv.save + :ok + else + :error + end + rescue ActiveRecord::RecordNotUnique + :ok + end + + def destroy_content_visibility(content_type, visibility_type) + cv = ContentVisibility.find_by(cohort_id: params[:cohort_id], + content_uid: params[:id], + content_type: content_type, + visibility_type: visibility_type) + + cv&.destroy + + :ok + end + end +end diff --git a/scripts/app/controllers/concerns/api/exception_handler.rb b/scripts/app/controllers/concerns/api/exception_handler.rb new file mode 100644 index 0000000000000000000000000000000000000000..0cfa71b32d534e7618c6055664e8c1d1e2be90d5 --- /dev/null +++ b/scripts/app/controllers/concerns/api/exception_handler.rb @@ -0,0 +1,75 @@ +module Api::ExceptionHandler + extend ActiveSupport::Concern + + CODES = { + # routing layer + not_allowed: { status: 405, title: "Method not allowed" }, + invalid_version: { status: 410, title: "The requested API version no longer exists" }, + + # controller layer + unauthorized: { status: 401, title: "You do not have access to the requested resource" }, + unauthenticated: { status: 403, title: "You are not authenticated for this request" }, + not_found: { status: 404, title: "The requested resource could not be found" }, + malformed_params: { status: 422, title: "The params provided are malformed" }, + missing_param: { status: 422, title: "A required param was not provided" }, + invalid_resource: { status: 422, title: "The current resource was invalid", messages: [], attrs: {} }, + server_error: { status: 500, title: "An unknown error has occurred" } + } + + included do + rescue_from ActiveRecord::RecordNotFound, with: :not_found_response + rescue_from Aws::S3::Errors::NoSuchKey, with: :not_found_response + + rescue_from BlockParser::ConvertMdToJson::MdParseError, with: :unprocessable_entity_response + rescue_from BlockParser::BuildHtmlFromMdJson::InvalidMdError, with: :unprocessable_entity_response + rescue_from ActiveRecord::RecordInvalid, with: :unprocessable_entity_response + + rescue_from Pundit::NotAuthorizedError do |_e| + json_response( + { + "errors": { + "status": "401", + "title": "You are not authenticated for this request", + "code": "unauthorized" + } + }, :unauthorized + ) + end + + def not_found_response + json_response({ message: "The requested resource could not be found" }, :not_found) + end + + def unprocessable_entity_response + json_response({ message: "The current resource was invalid" }, :unprocessable_entity) + end + + def render_method_not_allowed_response + json_response( + { + "errors": { + "status": "405", + "title": "Method not allowed", + "code": "not_allowed" + } + }, :method_not_allowed + ) + end + end + + class_methods do + def rack_response(code, options = {}) + status, json = error_options(code, options) + [status, { "Content-Type" => "application/json" }, [json.to_json]] + end + + def error_options(code, options = {}) + hash = (CODES[code] || CODES[:server_error]).merge(options) + hash[:code] ||= code + hash[:status] ||= 500 + status = hash[:status] + hash[:status] = hash[:status].to_s + [status, { errors: hash }] + end + end +end diff --git a/scripts/app/controllers/concerns/api/response.rb b/scripts/app/controllers/concerns/api/response.rb new file mode 100644 index 0000000000000000000000000000000000000000..acac2d21f10bec7bdf2f59ad4173a7ce9b45976a --- /dev/null +++ b/scripts/app/controllers/concerns/api/response.rb @@ -0,0 +1,5 @@ +module Api::Response + def json_response(object, status = :ok) + render json: object, status: status + end +end diff --git a/scripts/app/controllers/home_controller.rb b/scripts/app/controllers/home_controller.rb new file mode 100644 index 0000000000000000000000000000000000000000..8bcebc65095ae0c8444fca8d0c0560a3efa9161c --- /dev/null +++ b/scripts/app/controllers/home_controller.rb @@ -0,0 +1,28 @@ +class HomeController < ApplicationController + skip_before_action :require_signed_in_user, only: :hello + + def hello + skip_authorization + render plain: "Hello!" + end + + def index + authorize(current_user, :show?) + last_viewed_cohort_id = current_user.last_viewed_cohort_id + + path = if session[:requested_path].present? + requested_path = session[:requested_path] + session[:requested_path] = nil + requested_path + elsif last_viewed_cohort_id && current_user.student_instructor_or_admin_of?(last_viewed_cohort_id) + current_user.update(last_viewed_cohort_id: nil) + cohort_path(last_viewed_cohort_id) + elsif current_user.cohorts.any? + cohort_path(CohortUser.where(user: current_user).order(:created_at).last.cohort_id) + elsif current_user.admin? + cohorts_path + end + + redirect_to path if path.present? + end +end diff --git a/scripts/app/controllers/notifications_controller.rb b/scripts/app/controllers/notifications_controller.rb new file mode 100644 index 0000000000000000000000000000000000000000..37896e75d6d66783904fc2832fb3e2df80283470 --- /dev/null +++ b/scripts/app/controllers/notifications_controller.rb @@ -0,0 +1,32 @@ +class NotificationsController < ApplicationController + def index + authorize(Notification) + + render_notifications_json + end + + def show + authorize(current_notification) + + current_notification.update(read_at: DateTime.current) unless current_notification.read_at? + + redirect_to(current_notification.url) + end + + def mark_all_read + authorize(Notification) + policy_scope(Notification).unread.update_all(read_at: DateTime.current) + + render_notifications_json + end + + private + + def render_notifications_json + render json: NotificationsComponentProps.execute(current_user.id) + end + + def current_notification + @current_notification ||= policy_scope(Notification).find(params[:id]) + end +end diff --git a/scripts/app/controllers/permalinks_controller.rb b/scripts/app/controllers/permalinks_controller.rb new file mode 100644 index 0000000000000000000000000000000000000000..6001e7e9669b2a08c03077079d8bd22a7b93d933 --- /dev/null +++ b/scripts/app/controllers/permalinks_controller.rb @@ -0,0 +1,85 @@ +class PermalinksController < ApplicationController + def permalink + authorize(current_user, :show?) + + block = Block.find_by(repo_name: params[:repo_name]) + error_404 and return if block.nil? + + if referer_cohort && referer_cohort_contains_content? + redirect_to content_file_path(referer_cohort, block.id, params[:path]) and return + end + + if all_content_file_paths.count == 1 && !referer_cohort + redirect_to all_content_file_paths.first[:path] and return + end + return error_404 if all_content_file_paths.count == 0 + + cohort_count = all_content_file_paths.length + render(locals: { + fullpath: "#{request.protocol}#{request.host_with_port}#{request.fullpath}", + header: cohort_count == 1 ? "Switch cohorts" : "Choose a Cohort", + paragraph: if cohort_count == 1 + " a different cohort. Click the link below to go where it is located." + + else + " multiple cohorts. Select the cohort where you'd like to open it." + end, + all_content_file_paths: all_content_file_paths + }) + end + + private + + def referer_cohort_contains_content? + @referer_cohort_contains_content ||= !content_file_paths_for_cohorts([referer_cohort.id]).empty? + end + + def referer_cohort + @referer_cohort ||= if request.referer && referer_is_from_learn + path = request.referer[root_url.length..-1] + path = path[1..path.length - 1] if path[0] == "/" + path.start_with?("cohorts/") ? Cohort.find(path.split("/")[1].to_i) : nil + end + end + + def referer_is_from_learn + return false if request.referer.nil? + + request_url = request.referer[0..root_url.length - 1] + request_url == root_url + end + + def block + @block ||= Block.find_by(repo_name: params[:repo_name]) + end + + def cohorts_using_block + @cohorts_using_block ||= Cohort.joins(cohort_releases: :release).where("releases.block_id = ?", block.id) + end + + def users_cohorts_using_block + @users_cohorts_that_have_block ||= cohorts_using_block.where(id: current_user.cohort_users.map(&:cohort_id)) + end + + def cohorts + @cohorts ||= current_user.admin? ? cohorts_using_block : users_cohorts_using_block + end + + def all_content_file_paths + return @all_content_files if @all_content_files + + all_content_files = content_file_paths_for_cohorts(cohorts.map(&:id)) + + @all_content_files = all_content_files.map do |cf| + { name: cohorts.find { |c| c.id == cf.cohort_id }.name, path: content_file_path(cf.cohort_id, block.id, cf.path) } + end.uniq.sort { |a, b| a[:name] <=> b[:name] } + end + + def content_file_paths_for_cohorts(cohort_ids) + ContentFile.unscoped.select("cohort_releases.cohort_id AS cohort_id, path"). + joins(standard: { release: :cohort_releases }). + where("releases.block_id = ?", block.id). + where("cohort_id IN (?)", cohort_ids). + where("path = ?", params[:path]) + end +end diff --git a/scripts/app/controllers/sessions_controller.rb b/scripts/app/controllers/sessions_controller.rb new file mode 100644 index 0000000000000000000000000000000000000000..e2f99ce96a0c7ee4dff98b0fdd24964b54e0b7d5 --- /dev/null +++ b/scripts/app/controllers/sessions_controller.rb @@ -0,0 +1,41 @@ +class SessionsController < ActionController::Base + def new + if !current_user + @user = PlatformOneAuthResolverService.new(token).find_or_create_user + session[:user_uid] = @user.uid + end + + redirect_to '/' + end + + def failure? + redirect_to '/' + end + + def destroy + session[:user_uid] = nil + redirect_to '/' + end + + protected + + def current_user + @current_user ||= User.find_by(uid: session[:user_uid]) unless session[:user_uid].nil? + end + + def token + if Rails.env == 'production' + pattern = /^Bearer / + header = request.headers["Authorization"] + header.gsub(pattern, '') if header&.match(pattern) + else + if !request.headers["Authorization"].nil? + pattern = /^Bearer / + header = request.headers["Authorization"] + header.gsub(pattern, '') if header&.match(pattern) + elsif !request.query_parameters["token"].nil? + request.query_parameters["token"] + end + end + end +end diff --git a/scripts/app/controllers/users_controller.rb b/scripts/app/controllers/users_controller.rb new file mode 100644 index 0000000000000000000000000000000000000000..659d1d042825a77522f72d5e3c5336652774e010 --- /dev/null +++ b/scripts/app/controllers/users_controller.rb @@ -0,0 +1,67 @@ +class UsersController < ApplicationController + def index + authorize(current_user) + @users = User.order('LOWER(email)') + render :index + end + + def edit_user + authorize(current_user) + user + render :edit_user + end + + def update_user + authorize(current_user) + + unless user.update(roles:[params[:forge_admin], params[:forge_blocks_manager], params[:auth_product_admin]].compact) + flash[:error] = user.errors.full_messages.join(", ") + render :edit + return + end + + redirect_to users_path + end + + def new + authorize(Cohort) + @cohort_id = params[:cohort_id] + end + + def edit + authorize(current_cohort) + current_cohort_user + user + end + + def update + authorize(current_cohort) + unless current_cohort_user.update(roles: [params[:instructor]].compact) + flash[:error] = current_cohort_user.errors.full_messages.join(", ") + render :edit + return + end + + redirect_to users_cohort_path(current_cohort) + end + + def destroy + authorize(current_cohort) + current_cohort_user.destroy + redirect_to users_cohort_path(current_cohort) + end + + private + + def current_cohort + @current_cohort ||= Cohort.find(params[:cohort_id]) + end + + def current_cohort_user + @cohort_user ||= CohortUser.find_by(cohort_id: params[:cohort_id], user_id: params[:id]) + end + + def user + @user ||= User.find_by(id: params[:id]) + end +end diff --git a/scripts/app/controllers/webhooks/assessments_service/submitted_challenge_answers_controller.rb b/scripts/app/controllers/webhooks/assessments_service/submitted_challenge_answers_controller.rb new file mode 100644 index 0000000000000000000000000000000000000000..751a64b1473c00587cc9fef4a90ccbd08eb59454 --- /dev/null +++ b/scripts/app/controllers/webhooks/assessments_service/submitted_challenge_answers_controller.rb @@ -0,0 +1,120 @@ +module Webhooks + module AssessmentsService + class SubmittedChallengeAnswersController < ActionController::Base + class InvalidChallengeTypeEvaluation < StandardError + end + + def update + if params[:token] == Rails.application.secrets.assessments_callback_token + submitted_challenge_answer = SubmittedChallengeAnswer.find(params[:id]) + + if params[:status_only] + submitted_challenge_answer.update(status: params[:status]) + head :ok, content_type: "application/json" and return + end + + if submitted_challenge_answer.challenge.code_snippet? || + submitted_challenge_answer.challenge.testable_project? || + submitted_challenge_answer.challenge.custom_snippet? + unless submitted_challenge_answer.status == "canceled" + results = params[:results] + status = params[:status] + correct = status == SubmittedChallengeAnswer::STATUSES[:correct] + points = correct ? submitted_challenge_answer.challenge.points : 0 + submitted_challenge_answer.update( + status: status, + test_results: results, + points: points, + assessment_ran_at: params[:assessment_ran_at] + ) + cohort = Cohort.find_by(id: params[:cohort_id]) + + checkpoint_submission = submitted_challenge_answer.checkpoint_submission + if checkpoint_submission.present? + CheckpointSubmissionService.new(checkpoint_submission, + submitted_challenge_answer.challenge.content_file, + cohort, + submitted_challenge_answer.user).attempt_autoscore + else + # Alert when challenge takes a while + slack_performance_delays(submitted_challenge_answer) + + Activity.create( + cohort_id: params[:cohort_id], + subject: submitted_challenge_answer, + name: Activity::NAMES[:submitted_challenge_answer_evaluated] + ) + end + end + else + Honeybadger.notify( + InvalidChallengeTypeEvaluation.new("An invalid challenge type was submitted to the assessments service for evaluation.") + ) + end + + head :ok, content_type: "application/json" + else + head :unauthorized, content_type: "application/json" + end + rescue ActiveRecord::RecordNotFound + head :not_found, content_type: "application/json" + end + + private + + def slack_performance_delays(submitted_challenge_answer) + length_of_scoring = submitted_challenge_answer.updated_at.to_i - submitted_challenge_answer.created_at.to_i + content_file_url = content_file_url(submitted_challenge_answer.cohort_id, + submitted_challenge_answer.block_id, + submitted_challenge_answer.challenge.content_file.path) + + if submitted_challenge_answer.challenge.code_snippet? + if length_of_scoring > 10 && submitted_challenge_answer.challenge.language != "java" + SlackChallengePerformanceJob.perform_later(submitted_challenge_answer.id, content_file_url, length_of_scoring) + elsif length_of_scoring > 20 && submitted_challenge_answer.challenge.language == "java" + SlackChallengePerformanceJob.perform_later(submitted_challenge_answer.id, content_file_url, length_of_scoring) + end + + elsif submitted_challenge_answer.challenge.testable_project? + if length_of_scoring > 80 + SlackChallengePerformanceJob.perform_later(submitted_challenge_answer.id, content_file_url, length_of_scoring) + end + + elsif submitted_challenge_answer.challenge.custom_snippet? + if length_of_scoring > 60 + SlackChallengePerformanceJob.perform_later(submitted_challenge_answer.id, content_file_url, length_of_scoring) + end + end + end + + def segment_track_challenge_submitted(sca, cohort) + return if Rails.env.test? + + standard = sca.challenge.content_file.standard + block = standard.release.block + Analytics.track( + user_id: sca.user.uid, + event: "Challenge Submitted", + properties: { + pageType: sca.challenge.content_file.content_file_type.titleize, + path: Rails.application.routes.url_helpers.content_file_path(cohort, block, sca.challenge.content_file.path), + title: sca.challenge.content_file.title, + productId: cohort.uid, + cohortId: cohort.id, + cohortName: cohort.name, + blockId: block.id, + blockName: block.title, + unitId: standard.id, + unitUId: standard.uid, + unitName: standard.title, + challengeID: sca.challenge.id, + challengeUID: sca.challenge.uid, + challengeName: sca.challenge.title, + challengeType: sca.challenge.challenge_type, + challengeStatus: sca.status + } + ) + end + end + end +end diff --git a/scripts/app/exporters/performance_exporter.rb b/scripts/app/exporters/performance_exporter.rb new file mode 100644 index 0000000000000000000000000000000000000000..548dc807e3f7d1cc2cc53e7eefa2ddd5fa3fffb1 --- /dev/null +++ b/scripts/app/exporters/performance_exporter.rb @@ -0,0 +1,55 @@ +require "csv" + +class PerformanceExporter + def initialize(cohort) + @cohort = cohort + end + + def to_csv + headers = [ + "Cohort Title", + "Cohort Label", + "Block Title", + "Standard" + ] + + users = User.where(id: [@cohort.student_ids]).sort_by(&:full_name) + headers += users.map(&:full_name) + + standards = [] + performances = {} + + ReleaseFinder.for_cohort(@cohort.id).includes(%i[block standards]).each do |release| + standards.concat(release.standards) + + release_performances = PerformanceFinder.latest_for_cohort_release(cohort_id: @cohort.id, release: release) + + release.standards.each do |standard| + release_performances.where("standards.uid = ?", standard.uid).each do |performance| + performances[standard.id] ||= {} + performances[standard.id][performance.user_id] = performance.score + end + end + end + + CSV.generate do |csv| + csv << headers + standards.each do |standard| + block = standard.release.block + + row = [ + @cohort.name, + @cohort.label, + block.title, + standard.description + ] + + row += users.map do |user| + performances.fetch(standard.id) { {} }.fetch(user.id) { 0 } + end + + csv << row + end + end + end +end diff --git a/scripts/app/exporters/progress_exporter.rb b/scripts/app/exporters/progress_exporter.rb new file mode 100644 index 0000000000000000000000000000000000000000..54c92e43cfd91d1802b2048884d2ad24e2d0461d --- /dev/null +++ b/scripts/app/exporters/progress_exporter.rb @@ -0,0 +1,107 @@ +require "csv" + +# ProgressExporter is coupled to the data format form StudentProgressPresenter +class ProgressExporter + def initialize(students_data, cohort_mode, cohort_id) + @students_data = students_data + @cohort_mode = cohort_mode + @cohort_id = cohort_id + end + + def topics + @topics ||= @students_data.map(&:progress_percentages).map { |pp| pp[:topic_averages] }.map(&:keys).flatten.uniq + end + + def topic_headers + topics.map { |t| ["topic:#{t}_pts_earned", "topic:#{t}_pts_avail", "topic:#{t}_percent"]}.flatten + end + + def student_topics(student) + student_topics = topics.map do |t| + topic_avg = student.progress_percentages[:topic_averages][t] + if topic_avg + [topic_avg[:earned], topic_avg[:possible], topic_avg[:average]] + else + [nil, nil, nil] + end + end.flatten + student_totals = student.progress_percentages[:totals] + [student_totals[:total_earned], student_totals[:total_possible], student_totals[:percent]] + student_topics + end + + def headers + if @cohort_mode == Cohort::MODES[:percentage] + [ + "student", + "email", + "link to student submission page in Learn", + "link to student contact in sfdc", + "preferred campus", + "timestamp of last activity", + "sign up date", + "checkpoint avg", + "% complete", + "course_points_earned", + "course_points_avail", + "course_points_percent" + ] + topic_headers + elsif @cohort_mode == Cohort::MODES[:mastery] + [ + "student", + "email", + "link to student submission page in Learn", + "link to student contact in sfdc", + "preferred campus", + "timestamp of last activity", + "sign up date", + "% 3s", + "% 2s", + "% 1s", + "% complete", + "avg" + ] + end + end + + def to_csv + sfdc_url = if Rails.env != "production" + "https://test.salesforce.com/" + else + "https://login.salesforce.com/" + end + CSV.generate do |csv| + csv << headers + @students_data.each do |s| + row = if @cohort_mode == Cohort::MODES[:percentage] + [ + s.full_name, + s.email, + "https://learn-2.galvanize.com#{s.performances_url}", + "#{sfdc_url}#{s.uid}", + s.preferred_campus.to_s, + s.latest_activity, + s.registration_date, + "#{s.progress_percentages[:checkpoint_average]} %", + "#{s.progress_percentages[:percent_completed]} %" + ] + student_topics(s) + elsif @cohort_mode == Cohort::MODES[:mastery] + [ + s.full_name, + s.email, + "https://learn-2.galvanize.com#{s.performances_url}", + "#{sfdc_url}#{s.uid}", + s.preferred_campus.to_s, + s.latest_activity, + s.registration_date, + "#{s.score_percentages[:three]} %", + "#{s.score_percentages[:two]} %", + "#{s.score_percentages[:one]} %", + "#{s.progress_percentages[:three]} %", + s.average + ] + end + csv << row + end + end + end +end diff --git a/scripts/app/finders/block_finder.rb b/scripts/app/finders/block_finder.rb new file mode 100644 index 0000000000000000000000000000000000000000..31ead2a7d1459f9aa20c331de1c6717b8d826380 --- /dev/null +++ b/scripts/app/finders/block_finder.rb @@ -0,0 +1,7 @@ +class BlockFinder + def self.available_blocks_for(cohort_id) + attached_release_ids = CohortRelease.where(cohort_id: cohort_id).pluck(:release_id) + block_ids = Release.where.not(id: attached_release_ids).pluck(:block_id).uniq + Block.where(id: block_ids).order(:title) + end +end diff --git a/scripts/app/finders/checkpoint_submission_finder.rb b/scripts/app/finders/checkpoint_submission_finder.rb new file mode 100644 index 0000000000000000000000000000000000000000..e4ab94b97a57e74914864c87a3dae0f230345d12 --- /dev/null +++ b/scripts/app/finders/checkpoint_submission_finder.rb @@ -0,0 +1,74 @@ +class CheckpointSubmissionFinder + # rubocop:disable Metrics/LineLength + def self.latest_two_by_state_by_standards(cohort_id:, student_ids:, standard_uids:, block_ids:) + firsts = CheckpointSubmission.includes(submitted_challenge_answers: :challenge). + select("DISTINCT ON (checkpoint_submissions.user_id, checkpoint_submissions.state, standards.uid) checkpoint_submissions.*, standards.uid"). + joins(submitted_challenge_answers: { challenge: { content_file: :standard } }). + where("checkpoint_submissions.cohort_id = ? AND content_file_block_id IN (?) AND standards.uid IN (?) AND checkpoint_submissions.user_id IN (?)", + cohort_id, + block_ids, + standard_uids, + student_ids). + where.not(state: "started"). + order("checkpoint_submissions.user_id, checkpoint_submissions.state, standards.uid, created_at DESC") + seconds = CheckpointSubmission.includes(submitted_challenge_answers: :challenge). + select("DISTINCT ON (checkpoint_submissions.user_id, checkpoint_submissions.state, standards.uid) checkpoint_submissions.*, standards.uid"). + joins(submitted_challenge_answers: { challenge: { content_file: :standard } }). + where("checkpoint_submissions.cohort_id = ? AND content_file_block_id IN (?) AND standards.uid IN (?) AND checkpoint_submissions.user_id IN (?) AND checkpoint_submissions.id NOT IN (?)", + cohort_id, + block_ids, + standard_uids, + student_ids, + firsts.map(&:id)). + where.not(state: "started"). + order("checkpoint_submissions.user_id, checkpoint_submissions.state, standards.uid, created_at DESC") + firsts + seconds + end + # rubocop:enable Metrics/LineLength + + def self.lastest_by_state_for_students_in_cohort(cohort_id:, block_id:, content_file_uid:, student_ids:) + CheckpointSubmission. + select("DISTINCT ON (user_id, state) checkpoint_submissions.*"). + where("cohort_id = ? AND content_file_block_id = ? AND content_file_uid = ? AND user_id IN (?)", + cohort_id, + block_id, + content_file_uid, + student_ids). + order("user_id, state, created_at DESC") + end + + def self.latest_for_checkpoint_content_file_for_user_in_cohort(cohort_id:, content_file:, user_id:, with_started: false) + submissions = CheckpointSubmission.where( + content_file_uid: content_file.uid, + content_file_block_id: content_file.standard.release.block_id, + user_id: user_id, + cohort_id: cohort_id + ) + + unless with_started + submissions = submissions.where.not(state: CheckpointSubmission::STATES[:started]) + end + + submissions.order("created_at DESC").first + end + + def self.all_for_user_id_content_file_in_cohort(user_id, content_file, cohort_id) + CheckpointSubmission. + where(user_id: user_id, + cohort_id: cohort_id, + content_file_uid: content_file.uid, + content_file_block_id: content_file.standard.release.block_id). + where("state != 'retry'"). + order(created_at: :desc) + end + + # all_for_user_standards yields the checkpoints a user has submitted for a set of standards + # it includes the uid of the standard for that checkpoint submission + def self.all_for_user_standards(user_id, standard_uids) + CheckpointSubmission.joins(submitted_challenge_answers: [challenge: [content_file: :standard]]). + select("DISTINCT ON (checkpoint_submissions.id) checkpoint_submissions.*, standards.uid as standard_uid"). + where("checkpoint_submissions.user_id = ? AND standards.uid IN (?)", user_id, standard_uids). + group("checkpoint_submissions.id, standards.uid"). + order("checkpoint_submissions.id") + end +end diff --git a/scripts/app/finders/cohort_user_finder.rb b/scripts/app/finders/cohort_user_finder.rb new file mode 100644 index 0000000000000000000000000000000000000000..2c5f8067627c6433edcdf0282369dce78df3f73b --- /dev/null +++ b/scripts/app/finders/cohort_user_finder.rb @@ -0,0 +1,9 @@ +class CohortUserFinder + def self.instructors_for_cohorts_using_different_version_of_release_block(release) + CohortUser. + instructor. + joins(cohort: [cohort_releases: :release]). + where("releases.block_id = ? AND cohort_releases.release_id != ?", release.block_id, release.id). + order(:id) + end +end diff --git a/scripts/app/finders/content_file_finder.rb b/scripts/app/finders/content_file_finder.rb new file mode 100644 index 0000000000000000000000000000000000000000..96a2eaee48b6c2b0cbd72dc6322da8c0c7ea1255 --- /dev/null +++ b/scripts/app/finders/content_file_finder.rb @@ -0,0 +1,20 @@ +class ContentFileFinder + def self.from_cohort_block_content_file_path(cohort_id:, block_id:, content_file_path:) + ContentFile.unscoped. + joins(standard: :release). + joins("INNER JOIN cohort_releases ON releases.id = cohort_releases.release_id"). + where("releases.block_id = ? AND cohort_releases.cohort_id = ? AND path = ?", block_id, cohort_id, content_file_path). + first + end + + def self.all_from_cohort_block(cohort_id:, block_id:) + ContentFile.unscoped. + joins(standard: :release). + joins("INNER JOIN cohort_releases ON releases.id = cohort_releases.release_id"). + where("releases.block_id = ? AND cohort_releases.cohort_id = ?", block_id, cohort_id) + end + + def self.first_for_standard(standard_id) + ContentFile.where(standard_id: standard_id).order(position: :asc).first + end +end diff --git a/scripts/app/finders/performance_finder.rb b/scripts/app/finders/performance_finder.rb new file mode 100644 index 0000000000000000000000000000000000000000..b18c98ee9e352f72678d74dc4a04bb39161ed799 --- /dev/null +++ b/scripts/app/finders/performance_finder.rb @@ -0,0 +1,71 @@ +class PerformanceFinder + def self.latest_for_cohort_release(cohort_id:, release:) + Performance. + joins(standard: :release). + where("releases.block_id = ? AND performances.cohort_id = ?", release.block_id, cohort_id). + joins("INNER JOIN cohort_users ON cohort_users.user_id = performances.user_id"). + where("cohort_users.cohort_id = ?", cohort_id). + merge(CohortUser.student). + where("standards.uid in (SELECT uid FROM standards WHERE standards.release_id=?)", release.id). + select("DISTINCT ON (performances.user_id, standards.uid) performances.*"). + order("performances.user_id, standards.uid, performances.created_at DESC") + end + + def self.latest_for_standards_in_cohort(standard_uids:, user_ids:, cohort_id:) + Performance. + joins(:standard). + where("cohort_id = ?", cohort_id). + where("standards.uid in (?)", standard_uids). + where("performances.user_id in (?)", user_ids). + where("performances.cohort_id = ?", cohort_id). + select("DISTINCT ON (performances.user_id, standards.uid, performances.block_id) performances.*"). + order("performances.user_id, standards.uid, performances.block_id, performances.created_at DESC") + end + + def self.latest_for_cohort_student_release(cohort_id:, release:, user_id:) + latest_for_cohort_release(cohort_id: cohort_id, release: release).where("cohort_users.user_id = ?", user_id) + end + + def self.latest_for_cohort_user(cohort_id:, user_id:) + Performance. + where("performances.cohort_id = ? AND performances.user_id = ?", cohort_id, user_id). + select("DISTINCT ON (performances.user_id, performances.standard_uid) performances.*"). + order("performances.user_id, performances.standard_uid, performances.created_at DESC") + end + + def self.latest_standard_scores_for_cohort(user_ids: [], content_file_uids: nil, cohort_id:) + return {} if user_ids.empty? + + performances = Performance.joins( + "LEFT OUTER JOIN checkpoint_submissions ON checkpoint_submissions.id = performances.checkpoint_submission_id" + ).select( + "performances.user_id, performances.score, performances.standard_uid, performances.block_id, " \ + "performances.checkpoint_submission_id, checkpoint_submissions.state, checkpoint_submissions.created_at AS checkpoint_created_at, " \ + "checkpoint_submissions.total_points AS checkpoint_total_points, checkpoint_submissions.correct_points AS checkpoint_correct_points" + ).where( + "performances.cohort_id = ? AND performances.user_id IN (?)", cohort_id, user_ids + ).order("performances.created_at DESC") + + if content_file_uids + performances = performances.where("checkpoint_submissions.content_file_uid in (?)", content_file_uids) + end + + grouped_performances = performances.to_a.group_by(&:user_id) + result = Hash[user_ids.map { |key, _value| [key, {}] }] + grouped_performances.each do |key, value| + result[key] = value.each_with_object({}) do |perf, hash| + hash[perf.block_id] ||= {} + hash[perf.block_id][perf.standard_uid] ||= { + score: perf.score, + checkpoint_submission_id: perf.checkpoint_submission_id, + checkpoint_created_at: perf.checkpoint_created_at, + standard_uid: perf.standard_uid, + checkpoint_state: perf.state, + total_points: perf.checkpoint_total_points, + correct_points: perf.checkpoint_correct_points + } + end || {} + end + result + end +end diff --git a/scripts/app/finders/release_finder.rb b/scripts/app/finders/release_finder.rb new file mode 100644 index 0000000000000000000000000000000000000000..4dad71fa5606fa523c75adc5fa1b60761e93a922 --- /dev/null +++ b/scripts/app/finders/release_finder.rb @@ -0,0 +1,24 @@ +class ReleaseFinder + def self.latest_for_all_blocks_not_attached_to_cohort(cohort_id) + Release. + where( + "releases.block_id NOT IN (SELECT releases.block_id FROM cohort_releases INNER " \ + " JOIN releases ON cohort_releases.release_id = releases.id WHERE cohort_releases.cohort_id = ?)", + cohort_id + ). + select("DISTINCT ON (releases.block_id) releases.*"). + order("releases.block_id, releases.created_at DESC") + end + + def self.for_cohort(cohort_id) + Release.joins(:cohort_releases).where("cohort_releases.cohort_id = ?", cohort_id).order("cohort_releases.position ASC") + end + + def self.for_block(block_id) + Release.where("block_id = ? AND branch_name = 'master'", block_id).order("releases.created_at DESC") + end + + def self.available_update_count_for(release) + Release.where("id > ? AND block_id = ? AND branch_name = 'master'", release.id, release.block_id).count + end +end diff --git a/scripts/app/finders/standard_finder.rb b/scripts/app/finders/standard_finder.rb new file mode 100644 index 0000000000000000000000000000000000000000..ddd5d448d1541098488c43a76da72343c6fd9422 --- /dev/null +++ b/scripts/app/finders/standard_finder.rb @@ -0,0 +1,10 @@ +class StandardFinder + def self.all_for_cohort(cohort_id:) + Standard.joins(release: :cohort_releases).where("cohort_releases.cohort_id = ?", cohort_id) + end + + def self.for_cohort_release(cohort_id:, release_id:) + Standard.joins(release: :cohort_releases).where(release_id: release_id). + where("cohort_releases.cohort_id = ?", cohort_id).in_default_order + end +end diff --git a/scripts/app/finders/submitted_challenge_answer_finder.rb b/scripts/app/finders/submitted_challenge_answer_finder.rb new file mode 100644 index 0000000000000000000000000000000000000000..05f740379b2582612f92b7c29a44eb89b5e60d4b --- /dev/null +++ b/scripts/app/finders/submitted_challenge_answer_finder.rb @@ -0,0 +1,28 @@ +class SubmittedChallengeAnswerFinder + def self.latest_for_user_id_content_file_for_cohort(user_id, content_file, cohort_id) + challenge_uids = Challenge.where(content_file_id: content_file.id).pluck(:uid) + + SubmittedChallengeAnswer. + where(user_id: user_id, block_id: content_file.standard.release.block_id, + challenge_uid: challenge_uids, cohort_id: cohort_id). + latest_for_user_id_challenge_uid + end + + def self.all_for_user_id_challenge_in_cohort(user_id, challenge, cohort_id) + content_file = ContentFile.unscoped.find(challenge.content_file_id) + SubmittedChallengeAnswer. + where(user_id: user_id, block_id: content_file.standard.release.block_id, + challenge_uid: challenge.uid, cohort_id: cohort_id). + order(created_at: :desc) + end + + # only used in ChallengeWithSubmittedChallengeAnswersPresenter + def self.all_for_challenge_in_cohort(cohort, challenge, users_id = nil) + content_file = ContentFile.unscoped.find(challenge.content_file_id) + SubmittedChallengeAnswer.includes(:user). + where(user_id: users_id || cohort.students.map(&:id), block_id: content_file.standard.release.block_id, challenge_uid: challenge.uid). + select("DISTINCT ON (submitted_challenge_answers.user_id) submitted_challenge_answers.*"). + order("submitted_challenge_answers.user_id, created_at DESC"). + sort { |a, b| a.user.first_name <=> b.user.first_name } + end +end diff --git a/scripts/app/finders/user_finder.rb b/scripts/app/finders/user_finder.rb new file mode 100644 index 0000000000000000000000000000000000000000..ac5d5b1b8039e0bbca01b719b5535d00188ad508 --- /dev/null +++ b/scripts/app/finders/user_finder.rb @@ -0,0 +1,8 @@ +class UserFinder + def self.students_joined_cohort_yesterday(cohort_id) + User.joins(:cohort_users). + where("cohort_users.cohort_id = ?", cohort_id). + where("cohort_users.created_at > ?", Time.current - 1.day). + merge(CohortUser.student) + end +end diff --git a/scripts/app/helpers/application_helper.rb b/scripts/app/helpers/application_helper.rb new file mode 100644 index 0000000000000000000000000000000000000000..d9a15857c0a81a38ad9c51e4a835e037f6733c27 --- /dev/null +++ b/scripts/app/helpers/application_helper.rb @@ -0,0 +1,34 @@ +module ApplicationHelper + def sprite_navigation(name) + content_tag( + :svg, + content_tag( + :use, + "", + "xlink:href" => "/assets/images/svg/svg-sprite-navigation-symbol.svg#{name}" + ), + class: "icon" + ) + end + + def flash_class(level) + case level.to_sym + when :notice then "alert alert-info" + when :success then "alert alert-success" + when :error then "alert alert-danger" + when :alert then "alert alert-danger" + end + end + + def errors_for?(model, attribute) + model.errors[attribute].any? + end + + def errors_for(model, attribute) + return unless errors_for?(model, attribute) + + content_tag :small, class: "text-danger" do + model.errors[attribute].map(&:humanize).join(", ") + end + end +end diff --git a/scripts/app/helpers/json_helper.rb b/scripts/app/helpers/json_helper.rb new file mode 100644 index 0000000000000000000000000000000000000000..45516ad9a7df8f756216b59f2664467d83a91ad1 --- /dev/null +++ b/scripts/app/helpers/json_helper.rb @@ -0,0 +1,9 @@ +module JsonHelper + def true?(obj) + obj.to_s == "true" + end + + def false?(obj) + obj.to_s == "false" + end +end diff --git a/scripts/app/helpers/secondary_navigation_helper.rb b/scripts/app/helpers/secondary_navigation_helper.rb new file mode 100644 index 0000000000000000000000000000000000000000..fe799929f8a1b55ae27401f427b6f076554b0663 --- /dev/null +++ b/scripts/app/helpers/secondary_navigation_helper.rb @@ -0,0 +1,10 @@ +module SecondaryNavigationHelper + def setup_href(cohort) + return content_cohort_path(cohort.id) if cohort.sandbox + + cohort_user = current_user.cohort_users.where(cohort_id: cohort.id).first + return content_cohort_path(cohort.id) if cohort_user.nil? || cohort_user.last_setup_visit.nil? + + cohort_user.last_setup_visit + end +end diff --git a/scripts/app/helpers/standard_navigation_helper.rb b/scripts/app/helpers/standard_navigation_helper.rb new file mode 100644 index 0000000000000000000000000000000000000000..6019a28329f86e585add214a2dbd40863df3ed60 --- /dev/null +++ b/scripts/app/helpers/standard_navigation_helper.rb @@ -0,0 +1,165 @@ +module StandardNavigationHelper + delegate :url_helpers, to: "Rails.application.routes" + + def get_previous_standard_link(current_standard:, cohort:, content_file_id:) + @cohort = cohort + @current_content_file_id = content_file_id + @current_standard = current_standard + + if current_content_file_position.zero? # current file is first standard file + if previous_standard&.content_files&.any? + standard_link(@previous_standard, @previous_standard.content_files.order(position: :asc).last) + end + elsif current_content_file_position > 0 # current file is not first standard file + previous_content_file = current_content_files.where("position < ?", current_content_file_position).last + previous_content_file ? standard_link(@current_standard, previous_content_file) : nil + end + end + + def get_next_standard_link(current_standard:, cohort:, content_file_id:) + @cohort = cohort + @current_content_file_id = content_file_id + @current_standard = current_standard + + if current_content_file_position == current_content_files.length - 1 # current file is last standard file + return nil unless next_standard + + standard_link(next_standard, ContentFileFinder.first_for_standard(next_standard.id)) + else + next_content_file = current_content_files.where("position > ?", current_content_file_position).first + standard_link(@current_standard, next_content_file) + end + end + + def get_previous_content_file_link(current_standard:, cohort:, content_file_id:) + @cohort = cohort + @current_content_file_id = content_file_id + @current_standard = current_standard + + previous_content_file_id = current_content_files. + where("position < ?", current_content_file_position). + last&.id + + if previous_content_file_id.nil? + if previous_standard.present? && !previous_standard_visible_files.empty? + content_file_link( + previous_standard, + previous_standard_visible_files.order(position: :asc).last.id + ) + else + return nil unless current_standard_cohort_release_position + + previous_release = current_cohorts_releases. + where("position = ?", current_standard_cohort_release_position - 1). + last&.release + + return nil unless previous_release + + previous_release_standard = previous_release&.standards&.last + previous_content_file_id = previous_release_standard&.content_files&.last&.id + + content_file_link(previous_release_standard, previous_content_file_id) + end + else + content_file_link(@current_standard, previous_content_file_id) + end + end + + def get_next_content_file_link(current_standard:, cohort:, content_file_id:) + @cohort = cohort + @current_content_file_id = content_file_id + @current_standard = current_standard + + next_content_file_id = current_content_files. + where("position > ?", current_content_file_position). + first&. + id + + if next_content_file_id.nil? + next_standards_content_files = + next_standard.present? ? next_standard.content_files.order(position: :asc).visible_for(@cohort.id) : [] + + if !next_standards_content_files.empty? + content_file_link( + next_standard, + next_standards_content_files.first.id + ) + else + return nil unless current_standard_cohort_release_position + + next_release = current_cohorts_releases. + where("position = ?", current_standard_cohort_release_position + 1). + first&. + release + + return nil unless next_release + + next_release_standard = next_release&.standards&.visible_for(@cohort.id)&.first + next_content_file_id = next_release_standard&.content_files&.visible_for(@cohort.id)&.first&.id + + content_file_link(next_release_standard, next_content_file_id) + end + else + content_file_link(@current_standard, next_content_file_id) + end + end + + private + + def current_standard_position + @current_standard_position ||= @current_standard.position + end + + def current_content_file_position + @current_content_file_position ||= current_content_files.unscoped.find(@current_content_file_id).position + end + + def current_content_files + @current_content_files ||= ContentFile.where(standard: @current_standard). + visible_for(@cohort.id). + order(position: :asc) + end + + def current_standard_cohort_release_position + @current_standard_cohort_release_position ||= current_cohorts_releases. + select { |cohort_release| cohort_release.release == @current_standard.release }. + first&.position + end + + def current_cohorts_releases + @current_cohorts_releases ||= CohortRelease.where(cohort: @cohort).in_default_order + end + + def standard_link(standard, content_file) + return nil if content_file.blank? || standard.blank? + + { title: content_file.title, url: url_helpers.content_file_path(@cohort, standard.release.block, content_file.path) } + end + + def content_file_link(standard, content_file_id) + return nil if content_file_id.blank? + + content_file = standard.content_files.find_by(id: content_file_id) + block = standard.release.block + + { title: content_file.title, url: url_helpers.content_file_path(@cohort, block, content_file.path) } + end + + def cohort_standards + @cohort_standards ||= StandardFinder. + for_cohort_release(cohort_id: @cohort.id, release_id: @current_standard.release_id). + visible_for(@cohort.id) + end + + def previous_standard + @previous_standard ||= cohort_standards.where("standards.position < ?", current_standard_position).last + end + + def previous_standard_visible_files + @previous_standard_visible_files ||= previous_standard.content_files.visible_for(@cohort.id) + end + + def next_standard + @next_standard ||= cohort_standards.where("standards.position > ?", current_standard_position).first + end +end diff --git a/scripts/app/javascript/api.d.ts b/scripts/app/javascript/api.d.ts new file mode 100644 index 0000000000000000000000000000000000000000..74b5d40beae6b63e31a6e6130bc4d2a9bd20eb90 --- /dev/null +++ b/scripts/app/javascript/api.d.ts @@ -0,0 +1,558 @@ +declare namespace Api { + type rails_path = string + type user_id = number + + type Score = null | 1 | 2 | 3 + + type Activity = { + id: number + // formatted_created_at: number + name: string + content: string + created_at: string + user_photo: string + user_initials: string + } + + type Standard = { + id: number + success_criteria: string + current_score: Api.Score + description: string + } + + type Student = { + id: number, + full_name: string, + initials: string, + profile_image: string + } + + type User = { + id: number, + first_name: string, + last_name: string, + full_name: string, + email: string + } + + type ContentFile = { + id: number + uid: string + title: string + hidden: boolean + content_file_path: string + content_file_type: 'lesson' | 'checkpoint' | 'instructor' | 'resource' + challenges: ChallengeWithSubmissions[] + checkpoint_submissions?: Record + type_relative_position: number, + max_checkpoint_submissions: number + } + + type CheckpointStudentScore = { + email: string + first_name: string + has_signed_in: boolean + id: number + auth_edit_url: string + initials: string + last_name: string + profile_image: string + submissions: CheckpointStudentScoreSubmission + } + + type CheckpointStudentScoreSubmission = { + created_at: string + done_id: number + needs_review_id: number + score: number + started_id: number + state: string + } + + type CheckpointSubmission = { + id: number + cohort_id: number + state: CheckpointSubmissionState + created_at: string + updated_at: string + last_performance_percent: number + total_possible_points: number + points_earned: number + challenge_total: number + path: string + } + + type CheckpointSubmissionState = 'needs_review' | 'rejected' | 'done' | 'retry' | 'started' + + type CheckpointDataBase = { + cohortId: number + challengeTotal: number + contentFilesSubmission: Api.CheckpointSubmission | null + checkpointsByState: { + done: Api.CheckpointSubmission | null + retry: Api.CheckpointSubmission | null + needs_review: Api.CheckpointSubmission | null + started: Api.CheckpointSubmission | null + } + } + + type CheckpointInfo + = CheckpointDataBase & { + cohortMode: Api.CohortModeMastery | Api.CohortModePercentage + masteryScore: number + challengeCorrect: number + } + + type ContentFileType = 'checkpoint' | 'lesson' | 'resource' | 'instructor' | undefined + + type ChallengeWithSubmissions = { + id: number + title: string + student_submissions: Record> + } + + type Notification = { + id: number + created_at: Date + description: null | string + read_at: null | Date + tagline: string + title: string + updated_at: Date + url: string + user_id: number + } + + type Challenge = { + id: number + uid: string + answer: null | string + content_file_id: number + created_at: Date + docker_directory_path: null | string + explanation: null | string + hints: string[] + language: null | string + position: number + max_attemps: number + raw_json: any + setup: string + tests: null | string + title: string + updated_at: Date + upstream_repo_path: null | string + validate_fork: boolean + } + + type ChallengeWithSubmittedChallengeAnswersPresenterBase = { + id: number + uid: string + title: string + autoscorable: boolean + hints: string[] + index: number // Where does this come from?? + next_student: rails_path + number_of_hints: number + previous_student: rails_path + points: number + topics: string[] + + show_tests: boolean + tests?: string // Present if show_tests is true + + can_view_status: boolean + can_check_answer: boolean + can_view_explanation: boolean + submitted_challenge_answer_presenters: SubmittedChallengeAnswerPresenter[] + submitted_challenge_answer_presenter: SubmittedChallengeAnswerPresenter + } + + type ChallengeType + = 'checkbox' | 'code-snippet' | 'custom-snippet' + | 'multiple-choice' | 'number' | 'poll' + | 'short-answer' | 'testable-project' + | 'paragraph' | 'project' | 'local-snippet' + + type ChallengeWithSubmittedChallengeAnswersPresenter + = ChallengeWithSubmittedChallengeAnswersPresenterBase & + { + challenge_type: Exclude + + // explanation: any + placeholder: null | string + decimal: null | number + question: null | string + language: null | string + options: null | Array<{ + rendered_option: string, + option: any + }> + // grader_label: any + } + | LocalJsChallenge + + type LocalJsChallenge + = ChallengeWithSubmittedChallengeAnswersPresenterBase & + { + challenge_type: 'local-snippet' + tests: string + helpers: string[] + language: 'javascript' + question: string + placeholder: string + options: null | Array<{ + rendered_option: string, + option: any + }> + } + + type SubmittedChallengeAnswerPresenter = { + status: SubmitedChallengeAnswerStatus + overall_status: SubmittedChallengeAnswerOverallStatus + answer: null | string + last_graded_at: null | Date + challenge_id: number + student_comment_url: null | rails_path + test_results: string + cancel_url: null | string + submitted_challenge_answer_id: number + submitted_challenge_answer_url: null | rails_path + // increment_challenge_answer_url: any + attempted_on: null | string + challenge_explanation: null | string + rubric: null | string + incorrect_attempts: number + hints: string[] + grader_label: null | string + permissions: { update: boolean } + created_at: null | Date + increment_hints_shown_submitted_challenge_answer_url: null | rails_path + points: null | number + max_points: null | number + checkpoint_submission_id: null | number + } + + type SubmitedChallengeAnswerStatus + = 'correct' | 'incorrect' + | 'ungraded' | 'processing' | 'failed' | 'invalid_repo' + | 'canceled' | 'invalid_fork' | 'timeout' | 'missing_file' | 'unknown' | 'retry' | 'building' + + type SubmittedChallengeAnswerOverallStatus + = 'none' | 'graded' | 'ungraded' + + type CohortModeMastery = 'Mastery' + type CohortModePercentage = 'Percentage' + type CohortMode = CohortModePercentage | CohortModeMastery + + type UserForAvatar = { + id: number + initials: string + full_name: string + profile_image: null | string + } + + type StandardPresenter_ForCardBase = { + id: number + block_id: number + cohort_id: number + cohort_standard_progress: { + percent_done: number + percent_pending: number + remaining: number + total_completed: number + total_pending: number + } + cohort_standard_progress_data: any[] + title: string + description: string + has_progress: boolean + progress_complete: number + progress_percentage: number + } + + type StandardPresenter_ForCard + = StandardPresenter_ForCardBase & { + cohort_mode: 'Percentage' + progress_pending: number + remaining_to_completion: number + is_completed: boolean + has_progress: boolean + } + | StandardPresenter_ForCardBase & { + cohort_mode: 'Mastery' + mastery_score: number + // These exist but we shouldn't use them + // last_visited_by_user: '-is-current' | '-is-visited' | '-is-unvisited' + // mastery_class: '-score-1' | '-score-2' | '-score-3' | '-unscored' + } + + type LessonVisit = { + id: number + user_id: number + block_id: number + cohort_id: number + standard_uid: string + content_file_path: string + content_file_uid: null | string + + visit_count: number + } + + namespace CurriculumPage { + type release_id = number + type Section = { + id: number + title: string + release_ids: release_id[] + standard_count: number + standardsForSectionPath: string | null + } + } + + namespace CheckpointSubmissionShow { + type t = { + id: number + user_id: number + url: string + title: string + autoscorable: boolean + block_id: number + max_checkpoint_submissions: number + submission_count: number + content_file_url: string + grader: any + submitted_on: string + state: CheckpointSubmissionState + } + + type StandardCardProps = { + checkpoint_performance: null | CheckpointPerformance + standard: Standard + } + type CheckpointPerformance = { + score: number + // checkpoint_submission_id: number + created_at: string + standard_id: number + // updator_id: number + // user_id: number + updator_full_name: string + // updator_full_name: performance.updator.nil ? "Autoscored" : performance.updator.full_name + } + } + + namespace Submissions { + type block_id = number + type standard_uid = string + + type SubmissionData + = { + cohort_mode: 'Mastery' | 'Percentage' + blocks: Block[] + sections: Block[] + completion_progress: Record + > + > + } + + type Student = { + id: user_id + initials: string + name: string + avatar: null | string + challenges_path: rails_path + cohort_reg_date: string + } + + type Block = { + id: number + title: string + standards: Standard[] + } + + type Standard = { + id: number + uid: string + title: string + description: string + success_criteria?: string // Only present in mastery mode + student_performances: Record + content_files: ContentFile[] + submissions_path: rails_path + } + + type StudentPerformance = { + score?: number + student_id: number + standard_uid?: string + checkpoint_created_at?: null | string + checkpoint_state?: null | CheckpointSubmissionState + checkpoint_submission_id?: null | number + ungraded_submission_id?: number + ungraded_submission_state?: CheckpointSubmissionState + total_points: number + correct_points: number + } + } + + namespace Setup { + type Flash = { + title: string + msg: string | any + type: string + } + type ResyncJobBase = { + id: number + cohort_id: number + course_config_url: string + } + type SuccessfulResyncJob = ResyncJobBase & { + status: 'success' + synced_at: string + } + type CanceledResyncJob = ResyncJobBase & { + status: 'canceled' + } + type FailureResyncJob = ResyncJobBase & { + status: 'failure' + archived: boolean + error_type: string + error_data: any + } + type TimeoutResyncJob = ResyncJobBase & { + status: 'timeout' + archived: boolean + error_type: string + error_data: any + } + type PendingResyncJob = ResyncJobBase & { + status: 'pending' + } + type ResyncJob = SuccessfulResyncJob | FailureResyncJob | PendingResyncJob | CanceledResyncJob | TimeoutResyncJob + + type VisibilitySection = { + id: number + title: string + standards_with_lessons: VisibilityStandard[] + } + type VisibilityStandard = { + id: number + title: number + hidden: boolean + content_file_count: number + content_files: Array + } + type VisibilityContentFile = { + id: number, + uid: string, + title: string, + hidden: boolean, + path: string, + github_url: string, + visible: boolean, + } + } + + type BlockPresenter_ForReleases = { + cohorts_using_release: Cohort[] + last_update: string + name: string + release_id: number + version: string + repo_name: string + notes: string + repo_url: string + commit_url: string + github_sha: string + diff_url: string + block_id: number + user_avatar: UserForAvatar + } + + type BlockPresenter_ForBlocks = { + submission_count: number, + cohorts_using_this_block: object[], + cohort_count: number, + id: number, + last_update: string, + latest_release_id: number, + name: string, + readme_link: string, + readme_text: string, + release_count: number, + repo_description?: string, + repo_name: string, + repo_tags?: string[], + repo_url: string, + student_count: number, + sync_errors?: string[], + user_avatar: UserForAvatar + } + + type Cohort = { + id: number + uid: string + name: string + product_type: string + label: string + gcode: string + campus_name: string + slack_channel: string + starts_on: string + ends_on: string + created_at: string + updated_at: string + deleted_at: string + mode: string + } + + namespace Pairs { + type Student = { + uid: string + name: string + profile_image: string + } + + type Pairing = { + size: number + groups: Api.Pairs.Student[][] | string + id: number | null + title: string + } + } + + type ApiInteraction = { + id: number + user_id: number + ip: string + path: string + method: string + action_name: string + duration: number + controller_name: string + response_code: string + } + + type RepoDetails = { + repo_name: string + org: string + origin: string + } +} + +declare module 'objectify-array/*'{ + var _a: any; + export = _a; +} + +interface Array { + flat(): Array; + flatMap(func: (x: T) => T): Array; +} diff --git a/scripts/app/javascript/components/AceEditor.tsx b/scripts/app/javascript/components/AceEditor.tsx new file mode 100644 index 0000000000000000000000000000000000000000..525be56304b8964be3ddfc56c3a140cc340805be --- /dev/null +++ b/scripts/app/javascript/components/AceEditor.tsx @@ -0,0 +1,133 @@ +import * as React from 'react' +import ace from './lib/ace' + +type Props = { + mode: string + theme: string + name: string + width: string + height: string + fontSize: number + showGutter: boolean + + value: string + defaultValue: string + + maxLines?: number + readOnly: boolean + wrapEnabled: boolean + selectFirstLine: boolean + showPrintMargin: boolean + behavioursEnabled: boolean + highlightActiveLine: boolean + wrapBehavioursEnabled: boolean + + onLoad: (e: React.SyntheticEvent) => void + onChange: (input: string) => void + onFocus: (e: React.SyntheticEvent) => void + handleCodeSubmit: (e: React.SyntheticEvent) => void + + setShowPrintMargin: any // Not used? + editorProps: any // Not used? +} + +export default class AceEditor extends React.Component { + editor: any + + public static defaultProps: Partial = { + name: 'ace-editor', + mode: '', + theme: '', + height: '100%', + width: '100%', + defaultValue: '', + value: '', + fontSize: 14, + showGutter: true, + readOnly: false, + highlightActiveLine: true, + showPrintMargin: true, + selectFirstLine: false, + wrapEnabled: true, + behavioursEnabled: true, + } + + componentDidMount() { + this.editor = ace.edit(this.props.name); + this.editor.getSession().$useWorker = false; + this.editor.$blockScrolling = Infinity; + this.editor.getSession().setMode('ace/mode/' + this.props.mode); + this.editor.setTheme('ace/theme/' + this.props.theme); + this.editor.setFontSize(this.props.fontSize); + this.editor.on('change', this.onChange); + this.editor.on('focus', this.props.onFocus); + this.editor.setValue(this.props.defaultValue || this.props.value, (this.props.selectFirstLine === true ? -1 : null)); + this.editor.setOption('maxLines', this.props.maxLines); + this.editor.setOption('readOnly', this.props.readOnly); + this.editor.setOption('highlightActiveLine', this.props.highlightActiveLine); + this.editor.setOption('behavioursEnabled', this.props.behavioursEnabled); + this.editor.setOption('wrapBehavioursEnabled', this.props.wrapBehavioursEnabled); + this.editor.setShowPrintMargin(this.props.setShowPrintMargin); + this.editor.renderer.setShowGutter(this.props.showGutter); + this.editor.commands.addCommand({ + name: 'submit-code', + exec: this.props.handleCodeSubmit, + bindKey: { mac: 'cmd-enter', win: 'ctrl-enter' } + }); + + if (this.props.onLoad) this.props.onLoad(this.editor); + } + + componentWillReceiveProps(nextProps: Props) { + let currentRange = this.editor.selection.getRange(); + + // only update props if they are changed + if (nextProps.mode !== this.props.mode) { + this.editor.getSession().setMode('ace/mode/' + nextProps.mode); + } + if (nextProps.theme !== this.props.theme) { + this.editor.setTheme('ace/theme/' + nextProps.theme); + } + if (nextProps.fontSize !== this.props.fontSize) { + this.editor.setFontSize(nextProps.fontSize); + } + if (nextProps.maxLines !== this.props.maxLines) { + this.editor.setOption('maxLines', nextProps.maxLines); + } + if (nextProps.readOnly !== this.props.readOnly) { + this.editor.setOption('readOnly', nextProps.readOnly); + } + if (nextProps.highlightActiveLine !== this.props.highlightActiveLine) { + this.editor.setOption('highlightActiveLine', nextProps.highlightActiveLine); + } + if (nextProps.setShowPrintMargin !== this.props.setShowPrintMargin) { + this.editor.setShowPrintMargin(nextProps.setShowPrintMargin); + } + if (nextProps.wrapEnabled !== this.props.wrapEnabled) { + this.editor.getSession().setUseWrapMode(nextProps.wrapEnabled); + } + if (nextProps.value && this.editor.getValue() !== nextProps.value) { + this.editor.setValue(nextProps.value, (this.props.selectFirstLine === true ? -1 : null)); + if (currentRange && typeof currentRange === 'object') { + this.editor.getSession().getSelection().setSelectionRange(currentRange); + } + } + if (nextProps.showGutter !== this.props.showGutter) { + this.editor.renderer.setShowGutter(nextProps.showGutter); + } + } + + onChange = () => { + if (this.props.onChange) { + const value = this.editor.getValue(); + + this.props.onChange(value); + } + } + + render() { + const divStyle = { width: this.props.width, height: this.props.height }; + + return
+ } +} diff --git a/scripts/app/javascript/components/Badge.tsx b/scripts/app/javascript/components/Badge.tsx new file mode 100644 index 0000000000000000000000000000000000000000..35128741d6a8cbd829b297859099b530d71d7e4c --- /dev/null +++ b/scripts/app/javascript/components/Badge.tsx @@ -0,0 +1,18 @@ +import * as React from 'react' + +type Props = { + id?: string + text: string + className?: string +} + +export default (props: Props) => { + let classes = `lp-badge ${(props.className ? props.className : '')}`; + let badgeText = props.text.replace('_', ' '); + + return ( + + {badgeText} + + ); +}; diff --git a/scripts/app/javascript/components/Button.tsx b/scripts/app/javascript/components/Button.tsx new file mode 100644 index 0000000000000000000000000000000000000000..2323ff92334de9e4d52a860b9ac2cc63b1a5c10c --- /dev/null +++ b/scripts/app/javascript/components/Button.tsx @@ -0,0 +1,26 @@ +import * as React from 'react' + +type Props = { + className?: string + id?: string + type?: any + onClick?: any + children?: React.ReactNode + disabled?: boolean +} + +export default (props: Props) => { + let classes = `lp-style-button ${(props.className ? props.className : '')}`; + + return ( + + ); +}; diff --git a/scripts/app/javascript/components/ButtonTo.tsx b/scripts/app/javascript/components/ButtonTo.tsx new file mode 100644 index 0000000000000000000000000000000000000000..f8efe0eb7f45550824a29e92287cf032a6092c59 --- /dev/null +++ b/scripts/app/javascript/components/ButtonTo.tsx @@ -0,0 +1,75 @@ +import * as React from 'react' + +type Props = { + buttonText: string + method: string + paramName: string + url: string + disabled?: boolean + parentId?: string + paramVal: string + buttonClass: string +} + +export default (props: Props) => { + let formMethod = props.method; + let methodInput; + let csrfToken = $_('meta[name=csrf-token]', el => el.getAttribute('content')) || undefined; + let inputClass = props.buttonClass; + let hasMadeComment = false; + + if (props.disabled) { + inputClass = inputClass.concat(' -disabled'); + } + + if (props.method === 'patch') { + formMethod = 'post'; + methodInput = ( + + ); + } + + const submit = (e: any) => { + if (props.buttonText === 'Reject' && !hasMadeComment) { + e.preventDefault(); + $_('#comment-and-reject', el => el.style.display = 'block'); + } + }; + + const newCommentHandler = (e: any) => { + if (props.buttonText === 'Reject') { + e.preventDefault(); + console.error('Route does not exist: api_v1_checkpoint_submission_checkpoint_submission_comments_path') + // $.post(Routes.api_v1_checkpoint_submission_checkpoint_submission_comments_path(props.parentId), { + // comment: { + // content: $('.comment-and-reject textarea').text() + // } + // }).then((data) => { + // $(`#${props.buttonText}Form`).first().trigger('submit', { hasMadeComment: true }); + // }); + } + }; + + const injectCommentFormForReject = () => { + if (props.buttonText === 'Reject' && props.parentId !== null) { + console.error('NewCommentForm does not exist') + // return
; + } + }; + + return ( + + {injectCommentFormForReject()} +
+ {methodInput} + + + +
+
+ ); +}; diff --git a/scripts/app/javascript/components/Dropdown.tsx b/scripts/app/javascript/components/Dropdown.tsx new file mode 100644 index 0000000000000000000000000000000000000000..6f659f12c82fbee23fb483eb02f4b6a9bc0768eb --- /dev/null +++ b/scripts/app/javascript/components/Dropdown.tsx @@ -0,0 +1,40 @@ +import * as React from 'react' +import {isEmpty} from 'lodash-es' +import Icon from './Icon' + +type Props = { + placeholder?: string + selectedStandard?: string + options: { + id: string + savedValue?: null | string + items: Array<{ id: number | string, title: string }> + } + onChange: any +} + +export default (props: Props) => { + let placeholder; + let uniqueId = props.options.id; + let selected = props.options.savedValue || undefined; + let options = props.options.items.map((option) => { + return ( + + ); + }); + + if (!isEmpty(props.placeholder)) { + placeholder = ; + } + + return ( +
+ + +
+ ); +}; + diff --git a/scripts/app/javascript/components/Icon.tsx b/scripts/app/javascript/components/Icon.tsx new file mode 100644 index 0000000000000000000000000000000000000000..b8b61fb7533df12a3a0c1abbc870aa9dfad3d07d --- /dev/null +++ b/scripts/app/javascript/components/Icon.tsx @@ -0,0 +1,20 @@ +import * as React from 'react' + +type Props = { + id: string + sprite: string + color?: string + viewBox?: string +} + +export default (props: Props) => { + let url = `/assets/images/svg/svg-sprite-${props.sprite}-symbol.svg${props.id}`; + let classes = `svg svg-24px ${props.color || ''}`; + let viewBox = props.viewBox || '0 0 24 24'; + + return ( + + + + ); +}; diff --git a/scripts/app/javascript/components/Loading.tsx b/scripts/app/javascript/components/Loading.tsx new file mode 100644 index 0000000000000000000000000000000000000000..d3acbe5ec0524ca27303b22f4a0e48111c9fec8e --- /dev/null +++ b/scripts/app/javascript/components/Loading.tsx @@ -0,0 +1,59 @@ +import * as React from 'react' + +export default class LoadingLogo extends React.Component { + + componentDidMount() { + var {TimelineMax, Sine} = window + let bubbleAnimation1 = new TimelineMax({ paused: false }); + let bubbleAnimation2 = new TimelineMax({ paused: false }); + let bubbleAnimation3 = new TimelineMax({ repeat: -1, paused: false }); + + bubbleAnimation1 + .to('#bubble-1', 1.25, { scaleX: 0.74, scaleY: 0.74, ease: Sine.easeOut }) + .to('#bubble-1', 1.25, { x: -30, y: -20, transformOrigin: '50% 50%', ease: Sine.easeOut }, '-=1.25') + .to('#bubble-1', 1.25, { rotation: -100, transformOrigin: '50% 50%', ease: Sine.easeOut }, '-=1.25') + .to('#bubble-1', 1.25, { scaleX: 0.45, scaleY: 0.45, ease: Sine.easeOut }) + .to('#bubble-1', 1.25, { x: -10, y: -53, transformOrigin: '50% 50%', ease: Sine.easeOut }, '-=1.25') + .to('#bubble-1', 1.25, { rotation: 0, transformOrigin: '50% 50%', ease: Sine.easeOut }, '-=1.25') + .to('#bubble-1', 1.25, { scaleX: 0.0, scaleY: 0.0, transformOrigin: '50% 50%', ease: Sine.easeOut }); + + bubbleAnimation2 + .to('#bubble-2', 1.25, { scaleX: 0.69, scaleY: 0.69, ease: Sine.easeOut }) + .to('#bubble-2', 1.25, { x: 20, y: -31, transformOrigin: '50% 50%', ease: Sine.easeOut }, '-=1.25') + .to('#bubble-2', 1.25, { rotation: 100, transformOrigin: '50% 50%', ease: Sine.easeOut }, '-=1.25') + .to('#bubble-2', 1.25, { scaleX: 0.0, scaleY: 0.0, transformOrigin: '50% 50%', ease: Sine.easeOut }); + + bubbleAnimation3 + .to('#bubble-3', 1.25, { scaleX: 0.0, scaleY: 0.0, transformOrigin: '50% 50%', ease: Sine.easeOut }) + .to('#bubble-3', 0, { x: 10, y: 52, transformOrigin: '50% 50%' }) + .to('#bubble-3', 1.25, { scaleX: 2.25, scaleY: 2.25, transformOrigin: '50% 50%', ease: Sine.easeOut }) + .to('#bubble-3', 1.25, { scaleX: 1.58, scaleY: 1.58, ease: Sine.easeOut }) + .to('#bubble-3', 1.25, { x: -19, y: 31, transformOrigin: '50% 50%', ease: Sine.easeOut }, '-=1.25') + .to('#bubble-3', 1.25, { rotation: -92, transformOrigin: '50% 50%', ease: Sine.easeOut }, '-=1.25') + .to('#bubble-3', 1.25, { scaleX: 1.1, scaleY: 1.1, ease: Sine.easeOut }) + .to('#bubble-3', 1.25, { x: 0, y: 1, transformOrigin: '50% 50%', ease: Sine.easeOut }, '-=1.25') + .to('#bubble-3', 1.25, { rotation: 5, transformOrigin: '50% 50%', ease: Sine.easeOut }, '-=1.25'); + } + + render() { + return ( +
+ +
+ ); + } +} diff --git a/scripts/app/javascript/components/Marked.tsx b/scripts/app/javascript/components/Marked.tsx new file mode 100644 index 0000000000000000000000000000000000000000..c5e9a99f629f0b871b58e03a30b02aa01e38585a --- /dev/null +++ b/scripts/app/javascript/components/Marked.tsx @@ -0,0 +1,25 @@ +import * as React from 'react' +const marked = require('marked') + +type Props = { + source: null | string + className?: string +} + +export default class Marked extends React.Component { + + rawMarkup() { + if (this.props.source !== null) { + let rawMarkup = marked(this.props.source.toString()); + + rawMarkup = rawMarkup + .replace(//g, '
'); + + return { __html: rawMarkup }; + } + } + + render() { + return
; + } +} diff --git a/scripts/app/javascript/components/Menu.tsx b/scripts/app/javascript/components/Menu.tsx new file mode 100644 index 0000000000000000000000000000000000000000..43833cd29156c267db78b9a291e69f4a1ecb003f --- /dev/null +++ b/scripts/app/javascript/components/Menu.tsx @@ -0,0 +1,45 @@ +import * as React from 'react' + +type Props = { + items: Array<{ url: string, text: string }> + unmountMe: () => void +} + +export default class Menu extends React.Component { + + private nodeRef: any + + componentWillMount() { + document.addEventListener('click', this.handleClickOff, false); + } + + componentWillUnmount() { + document.removeEventListener('click', this.handleClickOff, false); + } + + handleClickOff = (e: MouseEvent | TouchEvent) => { + if (! this.nodeRef.contains(e.target)) { + this.props.unmountMe(); + } + } + + listItems() { + const listItems = this.props.items.map((item, i) => +
  • + {item.text} +
  • + ); + + return ( +
      { listItems }
    + ); + } + + render() { + return ( +
    this.nodeRef = node}> + { this.listItems() } +
    + ); + } +} diff --git a/scripts/app/javascript/components/Notifications.tsx b/scripts/app/javascript/components/Notifications.tsx new file mode 100644 index 0000000000000000000000000000000000000000..55f39679460ada9916ad2fa40fd566dfb5cb3b7f --- /dev/null +++ b/scripts/app/javascript/components/Notifications.tsx @@ -0,0 +1,116 @@ +import * as React from 'react' +import * as Routes from '../generated/routes' +import NotificationsIcon from './notifications/NotificationsIcon' +import http from '../lib/http' + + +type Props = {} +type State = { + loading: boolean + open: boolean + unreadCount: number + notifications: Api.Notification[] +} + +export default class Notifications extends React.Component { + private node: any + + constructor(props: Props) { + super(props); + + this.handleOutsideClick = this.handleOutsideClick.bind(this); + + this.state = { + loading: true, + open: false, + unreadCount: 0, + notifications: [] + }; + } + + componentDidMount() { + this.bootstrap(); + } + + async bootstrap() { + var data = await http('GET', Routes.notificationsPath()); + this.updateFromPayload(data); + } + + updateFromPayload(data: any) { + this.setState({ + notifications: data.notifications, + unreadCount: data.unreadCount, + loading: false + }); + } + + togglePanel() { + if (!this.state.open) { + document.addEventListener('touchmove', this.handleOutsideClick, false); + document.addEventListener('click', this.handleOutsideClick, false); + } else { + document.removeEventListener('touchmove', this.handleOutsideClick, false); + document.removeEventListener('click', this.handleOutsideClick, false); + } + + this.setState({ open: !this.state.open }); + } + + handleOutsideClick(e: any) { + if (this.node.contains(e.target)) { + return; + } + + this.togglePanel(); + } + + async markAllRead() { + var data = await http('POST', Routes.markAllReadNotificationsPath()); + this.updateFromPayload(data); + } + + render() { + let panel; + + if (!this.state.loading && this.state.open) { + let footerRow; + + if (this.state.unreadCount > 0) { + footerRow = (
    Mark all read
    ); + } + + let notifications = this.state.notifications.map((notification) => { + let klass = notification.read_at ? 'read' : 'unread'; + + return ( + +
    +
    {notification.tagline}
    +
    {window.moment(notification.created_at).tz(window.moment.tz.guess()).fromNow()}
    +
    +
    {notification.title}
    +
    {notification.description}
    +
    + ); + }); + + panel = ( +
    +
    Notifications
    +
    {notifications}
    + { footerRow } +
    + ); + } + + let openClass = this.state.open ? 'open' : ''; + + return ( +
    { this.node = node; }}> + + { panel } +
    + ); + } +} diff --git a/scripts/app/javascript/components/ProcessingIcon.tsx b/scripts/app/javascript/components/ProcessingIcon.tsx new file mode 100644 index 0000000000000000000000000000000000000000..f2d1db46279f74423029d18c8edbdf76ba703ba2 --- /dev/null +++ b/scripts/app/javascript/components/ProcessingIcon.tsx @@ -0,0 +1,31 @@ +import * as React from 'react' + +export default () => ( + + + + + +); diff --git a/scripts/app/javascript/components/RowKebab.tsx b/scripts/app/javascript/components/RowKebab.tsx new file mode 100644 index 0000000000000000000000000000000000000000..313fc028e1077632ffa2b6b4dc7a1442d68efb50 --- /dev/null +++ b/scripts/app/javascript/components/RowKebab.tsx @@ -0,0 +1,36 @@ +import * as React from 'react' +import SvgRenderer from './SvgRenderer' + +type Props = { + active: boolean + toggleAction:() => void +} + +type State = { + kebabClass: string +} + +export default class RowKebab extends React.Component { + constructor(props: Props) { + super(props) + this.state = { + kebabClass: this.props.active ? 'action-kebab active' : 'action-kebab' + } + } + + static getDerivedStateFromProps(nextProps: Props, prevState: State): State { + let kb:string = 'action-kebab' + if (nextProps.active) { + kb = kb + ' active' + } + return { kebabClass: kb } + } + + render() { + return ( + + + + ) + } +} diff --git a/scripts/app/javascript/components/ScoreCircle.tsx b/scripts/app/javascript/components/ScoreCircle.tsx new file mode 100644 index 0000000000000000000000000000000000000000..dc2baedf2db2c063a399bce5b90d5149bf57f2c9 --- /dev/null +++ b/scripts/app/javascript/components/ScoreCircle.tsx @@ -0,0 +1,23 @@ +import * as React from 'react' + +export default (n: number) => { + let circleClass = function(n: number) { + switch (n) { + case 1: + return ' -is-one'; + case 2: + return ' -is-two'; + case 3: + return ' -is-three'; + default: + return ''; + } + }; + + return ( +
    + {n} +
    + ); +}; + diff --git a/scripts/app/javascript/components/SidebarProgress.tsx b/scripts/app/javascript/components/SidebarProgress.tsx new file mode 100644 index 0000000000000000000000000000000000000000..bed2c830be5ea92455a3b28f703800ed18328dd1 --- /dev/null +++ b/scripts/app/javascript/components/SidebarProgress.tsx @@ -0,0 +1,28 @@ +import * as React from 'react' + +type Props = { + masteryCohort: boolean +} +type State = {} + +export default class SidebarProgress extends React.Component { + constructor(props: Props) { + super(props); + } + + render() { + if (this.props.masteryCohort) { + return ( +
    +
    + ); + } else { + return ( +
    +
    +
    +
    + ); + } + } + } diff --git a/scripts/app/javascript/components/SortDropdown.tsx b/scripts/app/javascript/components/SortDropdown.tsx new file mode 100644 index 0000000000000000000000000000000000000000..6b9a4636ca9d97f63489d7c881502b89de112550 --- /dev/null +++ b/scripts/app/javascript/components/SortDropdown.tsx @@ -0,0 +1,36 @@ +import * as React from 'react' +import Icon from './Icon' + +type Props = { + className: string, + selectedSort?: string, + options: { + id: any, + savedValue: string, + sorts: Array<{ + id: any, + title: string + }> + } + onChange: (e: any) => void +} + +export default (props: Props) => { + let uniqueId = props.options.id; + let selected = props.options.savedValue || props.selectedSort || ''; + let options = props.options.sorts.map((option) => { + return ( + + ); + }); + + return ( +
    +
    Sort by:
    + + +
    + ); +}; diff --git a/scripts/app/javascript/components/StandardBean.tsx b/scripts/app/javascript/components/StandardBean.tsx new file mode 100644 index 0000000000000000000000000000000000000000..1f918ccb25f0455b19cf445c6525fc52c898f342 --- /dev/null +++ b/scripts/app/javascript/components/StandardBean.tsx @@ -0,0 +1,36 @@ +import * as React from 'react' + +type Props = { + score: number, + isCompleted: boolean +} + +export default class StandardBean extends React.Component { + render() { + let className; + + if (this.props.score) { + switch (this.props.score) { + case 1: + className = '-score-1'; + break; + case 2: + className = '-score-2'; + break; + case 3: + className = '-score-3'; + break; + default: + className = '-unscored'; + } + } else if(this.props.isCompleted) { + className = '-is-completed'; + } else { + className = '-unscored'; + } + + return ( +
    + ); + } +} diff --git a/scripts/app/javascript/components/SvgRenderer.tsx b/scripts/app/javascript/components/SvgRenderer.tsx new file mode 100644 index 0000000000000000000000000000000000000000..f0e65b23aa6c43d2ee7307e9e71beb44cc6c06eb --- /dev/null +++ b/scripts/app/javascript/components/SvgRenderer.tsx @@ -0,0 +1,26 @@ +import * as React from 'react' + +type Props = { + viewBox?: string + url?: string + style?: object + onClick?: (e: any) => void + className?: string + dataAttrs?: any +} + +export default class SvgRenderer extends React.Component { + render() { + return ( + + + + ); + } +} diff --git a/scripts/app/javascript/components/Timestamp.tsx b/scripts/app/javascript/components/Timestamp.tsx new file mode 100644 index 0000000000000000000000000000000000000000..c32177792d06020f9e977ace0769a78ab3c59b07 --- /dev/null +++ b/scripts/app/javascript/components/Timestamp.tsx @@ -0,0 +1,16 @@ +import * as React from 'react' + +type Props = { + timestamp: any +} + +export default (props: Props) => ( +
    + {stampFromString(props.timestamp)} +
    +); + +export function stampFromString (date: string) { + const timezone = window.moment.tz.guess() + return `${window.moment(date).tz(timezone).calendar()} ${window.moment.tz(timezone).zoneAbbr()}` +} diff --git a/scripts/app/javascript/components/UserAvatar.tsx b/scripts/app/javascript/components/UserAvatar.tsx new file mode 100644 index 0000000000000000000000000000000000000000..3071827c8250a25d7ab3bf60e47664fd9e71281a --- /dev/null +++ b/scripts/app/javascript/components/UserAvatar.tsx @@ -0,0 +1,29 @@ +import * as React from 'react' + +type Props = { + src: null | string + initials: string +} + +export default (props: Props) => { + if (props.src) { + return ( +
    + ); + } else { + return ( +
    + + + + {props.initials} + + + +
    + ); + } +}; diff --git a/scripts/app/javascript/components/activities/NewActivityForm.tsx b/scripts/app/javascript/components/activities/NewActivityForm.tsx new file mode 100644 index 0000000000000000000000000000000000000000..a7d567e19c3dfe240a08db1bd10951e985978042 --- /dev/null +++ b/scripts/app/javascript/components/activities/NewActivityForm.tsx @@ -0,0 +1,138 @@ +import * as React from 'react' +import Textarea from 'react-textarea-autosize' +import Marked from '../Marked' +import http from '../../lib/http' + +type Props = { + afterActivityHandler: (activity: Api.Activity) => void + buttonType: string + maxRows: number + minRows: number + createActivityUrl: string + notify: boolean +} +type State = { + newActivity: string + showPreview: boolean +} + +export default class NewActivityForm extends React.Component { + constructor(props: Props) { + super(props); + } + + state = { + newActivity: '', + showPreview: false + } + + static get defaultProps() { + return { + afterActivityHandler: null, + buttonType: 'comment', + maxRows: 5, + minRows: 1 + }; + } + + createActivity = () => { + http('POST', this.props.createActivityUrl, { + body: { + activity: { + content: this.state.newActivity, + notify: this.props.notify + } + } + }) + .then((data: any) => { + if (this.props.afterActivityHandler) this.props.afterActivityHandler(data); + this.setState({ + newActivity: '', + showPreview: false + }); + }); + } + + get ActivityButton() { + let buttonClass = '', buttonText = ''; + let disabled = this.state.newActivity === ''; + + if (this.props.buttonType === 'comment') { + buttonClass = ''; + buttonText = 'Send'; + } else if (this.props.buttonType === 'reject') { + buttonClass = '-reject'; + buttonText = 'Comment & Reject'; + } + + if (disabled) { + buttonClass = buttonClass.concat(' -disabled'); + } + + return ( + + ); + } + + get PreviewButton() { + if (this.state.newActivity == '') { return null; } + + let buttonText = this.state.showPreview ? 'Edit' : 'Preview'; + + return ( + + {buttonText} + + ); + } + + toggleActivityPreview = () => { + this.setState({ showPreview: !this.state.showPreview }); + } + + setActivityState = (e: any) => { + this.setState({ newActivity: e.target.value }); + } + + getTextarea() { + let textArea = ( + + ); + + if (this.state.showPreview) { + textArea = ; + } + + return textArea; + } + + render() { + let commentFormClass = this.props.buttonType === 'reject' ? ' comment-and-reject' : ''; + + return ( +
    +
    + {this.getTextarea()} +
    +
    + + + + {this.PreviewButton} + {this.ActivityButton} +
    +
    + ); + } +} diff --git a/scripts/app/javascript/components/api/ApiInteractions.tsx b/scripts/app/javascript/components/api/ApiInteractions.tsx new file mode 100644 index 0000000000000000000000000000000000000000..903cf1756fee40aa38432e82e643f6fe889768bd --- /dev/null +++ b/scripts/app/javascript/components/api/ApiInteractions.tsx @@ -0,0 +1,142 @@ +import React, { Component, Fragment } from 'react'; +import JSONPretty from 'react-json-pretty'; +import StatusCircle from '../shared/Icons/StatusCircle'; +import Done from '../shared/Icons/Done'; +import Clear from '../shared/Icons/Clear'; +import Pagination from '../shared/Pagination/Pagination'; + +require('react-json-pretty/themes/1337.css'); + +type Props = { + apiInteractions: Api.ApiInteraction[] + page: string + perPage: number + totalCount: number +} + +type State = { + openDrawers: any +} + +export default class ApiInteractions extends Component { + state: State = { + openDrawers: {}, + }; + + toggleMetadataDrawer = (drawerIndex: number) => { + this.setState(prevState => ({ + openDrawers: { + ...prevState.openDrawers, + [drawerIndex]: !prevState.openDrawers[drawerIndex], + } + })); + } + + goToPage = (pageObj: any) => { + window.location.href = + `${window.location.href.split('?')[0]}?page=${pageObj.selected + 1}` + } + + render() { + const { apiInteractions, perPage, totalCount } = this.props; + const page = Number(this.props.page); + const totalPages = Math.ceil(totalCount / perPage); + + return ( +
    +
    +

    API Interactions

    +
    +
    + + + + + + + + + + + + + + {apiInteractions.map((interaction: any, i: number) => { + const anyMetadata = interaction.metadata ? Object.keys(interaction.metadata).length > 0 : false; + + return ( + + this.toggleMetadataDrawer(i)} + className="tr" + > + + + + + + + + + + {this.state.openDrawers[i] && ( + + + + )} + + )} + )} + +
    RESPONSE CODEMETHODPATHDURATIONUSER IDIP ADDRRECORD IDMETADATA
    +
    + +
    +
    + {interaction.response_code} +
    +
    {interaction.method}{interaction.path}{`${interaction.duration}ms`}{interaction.user_id}{interaction.ip}{interaction.id} + {anyMetadata ? ( +
    + +
    + ) : ( +
    + +
    + )} +
    +
    + {anyMetadata ? ( + + ) : ( +

    No metadata on this call

    + )} +
    +
    + + + {totalPages > 0 && ( + + )} + + ); + } +} + +const statusCodeColor = (responseCode: number) => { + const code = responseCode.toString(); + return code.startsWith('1') || code.startsWith('2') || code.startsWith('3') ? 'green' : 'red'; +} \ No newline at end of file diff --git a/scripts/app/javascript/components/api/ApiToken.tsx b/scripts/app/javascript/components/api/ApiToken.tsx new file mode 100644 index 0000000000000000000000000000000000000000..08107332039447ffc0aa8a41bd835f4e99f094e4 --- /dev/null +++ b/scripts/app/javascript/components/api/ApiToken.tsx @@ -0,0 +1,86 @@ +import React, { Component } from 'react' +import CopyToClipboard from 'react-copy-to-clipboard'; +import http from '../../lib/http' +import {apiV1RegenerateTokenPath} from '../../generated/routes' +import Button from '../shared/Button/Button' + +type Props = { + apiToken: string + regeneratePath: string +} + +type State = { + apiToken: string + copyButtonText: string +} + +export default class ApiToken extends Component { + state: State = { + apiToken: this.props.apiToken, + copyButtonText: 'Copy Token', + } + + regenerateApiToken = () => { + http('GET', apiV1RegenerateTokenPath()).then((resp: any) => { + this.setState({ + apiToken: resp["token"], + copyButtonText: 'Copy Token', + }) + }) + } + + onCopyClick = () => { + this.setState({ + copyButtonText: 'Copied!' + }); +} + + render() { + const { apiToken } = this.state + + return ( +
    +
    +

    Learn API

    +
    +

    Docs

    +

    + + API Documentation is auto-generated and contains samples that can be used for mocking tests. +

    +

    Token

    +

    + This token allows your app to authenticate requests to the Learn API. + Please don't share it with anyone, include it in any code repositories, or store it in an insecure way. + It will not expire unless you generate a new token. +

    +
    + + {apiToken !== "" && + +
    +
    +
    +
    + ) + } +} diff --git a/scripts/app/javascript/components/blocks/BlockPage-v2.tsx b/scripts/app/javascript/components/blocks/BlockPage-v2.tsx new file mode 100644 index 0000000000000000000000000000000000000000..a8a749eccf8e822b16e33f9ef81251a27fa5b510 --- /dev/null +++ b/scripts/app/javascript/components/blocks/BlockPage-v2.tsx @@ -0,0 +1,331 @@ +import * as React from 'react'; +import { debounce } from 'lodash'; +import * as Routes from '../../generated/routes' +import Avatar from '../UserAvatar'; +import UniversalList from '../shared/UniversalList/UniversalList'; +import UniversalRow from '../shared/UniversalRow/UniversalRow'; +import Button from '../shared/Button/Button'; +import UniversalTable from '../shared/UniversalTable/UniversalTable'; +import BlocksStats from './BlocksStats'; +import Icon from '../Icon'; +import Pill from '../shared/Pill/Pill'; +import ProcessingIcon from '../ProcessingIcon'; +import FlashAlert from '../shared/FlashAlert/FlashAlert'; +import http from '../../lib/http'; +import ErrorMessagesTable from '../shared/ErrorMessagesTable/ErrorMessagesTable'; +import BlocksNewRelease from './BlocksNewRelease'; + +const timezone = window.moment.tz.guess(); + +const ALERT_STATES = { + success: 'success', + queued: 'queued', + error: 'error' +} + +const pollingThrottleDelay = 2000; + +type Props = { + block: Api.BlockPresenter_ForBlocks, + releases: Api.BlockPresenter_ForReleases[], + github_url: string +} + +type State = { + alertState: string, + newReleaseModalVisible: boolean, + releases: Api.BlockPresenter_ForReleases[], + syncErrors: string[] +} + +class BlockPage extends React.Component { + releasePollingThrottled: any; + + constructor(props: Props) { + super(props) + + this.state = { + alertState: '', + newReleaseModalVisible: false, + releases: this.props.releases || [], + syncErrors: this.props.block.sync_errors || [] + } + + this.releasePollingThrottled = debounce(this.releasePolling.bind(this), pollingThrottleDelay); + } + + componentDidMount() { + if (this.state.releases[0] && this.state.releases[0].github_sha === 'pending') { + this.releasePollingThrottled(this.props.block.id); + } + } + + // TODO: call (debounced) after creating a new modal + releasePolling = () => { + http('GET', Routes.releasePollingBlockPath(this.props.block.id)) + .then((data: any) => { + if (data.block.sync_errors.length > 0) { + this.setState({ + releases: data.releases, + syncErrors: data.block.sync_errors, + alertState: ALERT_STATES.error + }); + } else if (data.releases[0] && data.releases[0].github_sha === 'pending') { + this.setState({ alertState: ALERT_STATES.queued }) + this.releasePollingThrottled(); + } else if (data.releases.length > 0) { + this.setState({ + releases: data.releases, + syncErrors: [], + alertState: ALERT_STATES.success + }); + } + }); + } + + generateAlert = (block: string) => ({ + success: { type: 'success', message: 'Release created.' }, + queued: { type: 'notice', message: 'Release creation queued.' }, + error: { type: 'error', message: <>{block}:{' Error. Unable to build release.'} }, + }) + + generateBody = () => { + const infoRow = this.generateInfoRow(); + const statsRow = this.generateStatsRow(); + const releasesRow = this.generateReleasesRow(); + const cohortsRow = this.generateCohortsRow(); + + return [infoRow, statsRow, releasesRow, cohortsRow] + } + + generateButtons = () => { + // TODO: implement other addt'l buttons + // TODO: implement modal work + const newReleaseBtn = ( + + + {this.renderResults()} + + ); + } +} diff --git a/scripts/app/javascript/components/cohorts/activity_dashboard/ActivityDashboard.tsx b/scripts/app/javascript/components/cohorts/activity_dashboard/ActivityDashboard.tsx new file mode 100644 index 0000000000000000000000000000000000000000..624c2a566bf44a0bd9a070ad2673d31f7a8252f1 --- /dev/null +++ b/scripts/app/javascript/components/cohorts/activity_dashboard/ActivityDashboard.tsx @@ -0,0 +1,207 @@ +import * as React from 'react' +import UserAvatar from '../../UserAvatar' +import Icon from '../../Icon' +import http from '../../../lib/http' + +type Props = any +type State = any + +export default class ActivityDashboard extends React.Component { + private handleScroll: any + + constructor(props: Props) { + super(props); + + this.state = { + loading: true, + currentDate: 'changeme', + students: [], + dates: [], + activity_data: {}, + grouped_dates: [], + queryOffset: 0, + showStudentLimitMessage: props.studentCount > 100 ? true : false, + }; + } + + componentDidMount() { + this.bootstrap(this.setScrollEvents); + } + + componentWillUnmount() { + window.removeEventListener('mousewheel', this.handleScroll); + } + + setScrollEvents = () => { + let headerSpacer = document.getElementById('headerspacer'); + let headerRow = document.getElementById('headerrow')!; + let headerMonths = document.getElementById('headermonths'); + let top = headerRow.offsetTop; + + this.handleScroll = () => { + this.handleScroll_unbound(top, headerRow, headerMonths, headerSpacer) + } + + window.addEventListener('mousewheel', this.handleScroll); + } + + handleScroll_unbound(top: number, headerRow: any, headerMonths: any, headerSpacer: any) { + if (window.pageYOffset >= top) { + headerRow.classList.add('-is-sticky'); + headerSpacer.classList.add('-is-sticky'); + let activityDashboard = document.getElementById('activitydashboard')!; + let x = activityDashboard.scrollLeft; + + headerMonths.style.left = -x + 'px'; + } else { + headerMonths.style.left = 0; + headerRow.classList.remove('-is-sticky'); + headerSpacer.classList.remove('-is-sticky'); + } + } + + bootstrap(callback: any) { + http('GET', this.props.url, { + urlParams: { offset: this.state.queryOffset } + }) + .then((data: any) => { + this.setState({ + students: this.state.students.concat(data.students), + activity_data: { ...this.state.activity_data, ...data.activity_data }, + dates: data.dates, + grouped_dates: data.grouped_dates, + loading: false, + currentDate: data.todays_date, + queryOffset: this.state.queryOffset + 100 + }); + $_('.' + CSS.escape(data.todays_date), el => el.classList.add('-is-highlighted')); + + let left = $_('.dateitem.' + CSS.escape(data.todays_date), el => el.getBoundingClientRect().left); + + if ( left && left > window.innerWidth) { + const scrollLeft = $_('.' + CSS.escape(data.todays_date), el => el.getBoundingClientRect().left - 500); + if (scrollLeft) { + $_('#activitydashboard', el => el.scrollLeft = scrollLeft) + } + } + }).then(callback); + return false; + } + + setCurrentDate(date: any) { + this.setState({ currentDate: date }); + } + + render() { + if (this.state.loading) { + return ( +
    + ); + } else { + let months = this.state.grouped_dates.map((grouped_date: any, index: number) => { + let dates = grouped_date[1].map((date: any, index: number) => { + let borderClass = index == 0 ? ' -is-first-of-month' : ''; + + let highlightedClass = this.state.currentDate == date.name ? ' -is-highlighted' : ''; + + return ( +
    this.setCurrentDate(date.name)}> +
    {date.day_of_month}
    +
    {date.day_of_week}
    +
    + ); + }); + + let maxWidth = dates.length * 40; + + return ( +
    +
    {grouped_date[0]}
    +
    {dates}
    +
    + ); + }); + + let dayWidth = 40; + let studentRows = this.state.students.map((student: any) => { + let points = `0,${dayWidth} `; + let index = dayWidth / 2; + + let width = (this.state.dates.length) * dayWidth; + + // build polyline + this.state.dates.map((date: any) => { + var score = this.state.activity_data[student.id][date.name] || 0; + points += `${index},${dayWidth - (score / 100 * dayWidth)} `; + index += dayWidth; + }); + + points += `${width},${dayWidth}`; + + // build cells + let studentData = this.state.grouped_dates.map((grouped_date: any, index: number) => { + var dates: any = []; + + grouped_date[1].map((date: any, idx: number) => { + let is_weekend_class = date.is_weekend ? ' -is-weekend' : ''; + let borderClass = idx == 0 ? ' -is-first-of-month' : ''; + let highlightedClass = this.state.currentDate == date.name ? ' -is-highlighted' : ''; + + dates.push(
    this.setCurrentDate(date.name)}>
    ); + }); + + return dates; + }); + + return ( +
    +
    + +
    {student.full_name}
    +
    +
    + + + +
    {studentData}
    +
    +
    + ); + }); + + let studentLimitMessage; + + if (this.state.showStudentLimitMessage) { + studentLimitMessage = ( +

    Only showing the latest 100 students to join this cohort. this.bootstrap(null) }> Load more

    + ); + } + + return ( +
    +
    + { studentLimitMessage } + +
    +
    Passive Engagement — 20% max
    +
    Active Engagement — 80% max
    +
    Submit Checkpoint for Mastery — 100%
    +
    +
    +
    +
    +
    +
    { months }
    +
    + { studentRows } +
    +
    + ); + } + } +} diff --git a/scripts/app/javascript/components/cohorts/activity_feed/ActivityFeed.tsx b/scripts/app/javascript/components/cohorts/activity_feed/ActivityFeed.tsx new file mode 100644 index 0000000000000000000000000000000000000000..bfc24eff38ca93df9e5889fb3dec330577e7c25a --- /dev/null +++ b/scripts/app/javascript/components/cohorts/activity_feed/ActivityFeed.tsx @@ -0,0 +1,67 @@ +import * as React from 'react' +import update from 'immutability-helper' +import UserAvatar from '../../UserAvatar' + +type Props = { + cohort_id: any + activities: any[] +} +type State = { + activities: any[] +} + +export default class ActivityFeed extends React.Component { + private interval: any + + constructor(props: Props) { + super(props); + + this.state = { + activities: this.props.activities, + }; + } + + addActivity(data: any) { + let new_activities = this.state.activities; + + this.setState({ + activities: update(this.state.activities, { $unshift: [data] }).slice().sort((x, y) => { + return new Date(y.created_at).getTime() - new Date(x.created_at).getTime(); + }).slice(0, 49) + }); + } + + componentDidMount() { + // action cable stuffs removed. + } + + componentWillUnmount() { + clearInterval(this.interval); + } + + activities() { + return ( + this.state.activities.map((activity) => { + return ( + + +
    + {activity.creator} {activity.message} + + {window.moment(activity.created_at).tz(window.moment.tz.guess()).fromNow()} + + ); + }) + ); + } + + render() { + return ( +
    + + {this.activities()} +
    +
    + ); + } +} diff --git a/scripts/app/javascript/components/cohorts/cohort_releases/ReleaseVersionsTable.tsx b/scripts/app/javascript/components/cohorts/cohort_releases/ReleaseVersionsTable.tsx new file mode 100644 index 0000000000000000000000000000000000000000..f50baf9398a14a62a8084244c9434580a07ef7cc --- /dev/null +++ b/scripts/app/javascript/components/cohorts/cohort_releases/ReleaseVersionsTable.tsx @@ -0,0 +1,81 @@ +import React, { Component } from 'react' +import * as Routes from '../../../generated/routes' +import http from '../../../lib/http' +import ReleaseVersionRow from '../settings/ReleaseVersionRow' + +type Props = { + afterSelectHandler?: (releaseId: any) => void + setSyncErrors?: (syncError: string[]) => void + releaseVersions: Api.BlockPresenter_ForReleases[] +} + +type State = { + releases: Api.BlockPresenter_ForReleases[] +} + +export default class ReleaseVersionsTable extends Component { + state = { + releases: this.props.releaseVersions, + }; + + releasePolling = (block_id: number) => { + http('GET', Routes.releasePollingBlockPath(block_id)) + .then((data: any) => { + if (data.block.sync_errors.length > 0) { + this.setState({ releases: data.releases }); + this.props.setSyncErrors!(data.block.sync_errors) + + // Targets Rails flash message which is outside component + $_('.alert-info', el => { + el.classList.remove('alert-info'); + el.classList.add('alert-danger'); + el.innerHTML = '' + data.block.title + ': Error. Unable to build release.'; + }) + $_('.new-release', el => { + el.classList.remove('disabled'); + }); + } else if (data.releases[0] && data.releases[0].github_sha === 'pending') { + setTimeout(() => { + this.releasePolling(data.block.id); + }, 1000); + } else if (data.releases.length > 0) { + this.setState({ releases: data.releases }); + + // Targets Rails flash message which is outside component + $_('.alert-info', el => { + el.classList.remove('alert-info'); + el.classList.add('alert-success'); + el.innerHTML = 'Release created.'; + }); + $_('.new-release', el => { + el.classList.remove('disabled'); + }); + } + }); + } + + render() { + return ( + + + + + + + + + + + {this.state.releases.map((release: Api.BlockPresenter_ForReleases) => ( + + ))} + +
    CreatedVersionCohorts
    + ); + } +} diff --git a/scripts/app/javascript/components/cohorts/cohort_submissions/CohortSubmissions.tsx b/scripts/app/javascript/components/cohorts/cohort_submissions/CohortSubmissions.tsx new file mode 100644 index 0000000000000000000000000000000000000000..9acd96e7bc56471546a6a37c038d07f21b217e3c --- /dev/null +++ b/scripts/app/javascript/components/cohorts/cohort_submissions/CohortSubmissions.tsx @@ -0,0 +1,602 @@ +import * as React from 'react' +import * as Routes from '../../../generated/routes' +import { extend } from 'lodash-es' +import http from '../../../lib/http' +import Icon from '../../Icon' +import SortDropdown from '../../SortDropdown' +import CohortSubmissionsTable from './CohortSubmissionsTable' +import CohortSubmissionsLessonRow from './CohortSubmissionsLessonRow' +import CohortSubmissionsStandardRow from './CohortSubmissionsStandardRow' +import ProgressThresholdsModal from '../../shared/ProgressThresholdsModal/ProgressThresholdsModal' +import ProgressThresholdsKey from '../../shared/ProgressThresholdsKey/ProgressThresholdsKey' +import SettingsCog from '../../shared/Icons/SettingsCog' + +import ReactTooltip from "react-tooltip"; + +type contentFileId = number +type standardId = number + +type Props = { + cohortMode: string + cohortId: number + students: Api.Submissions.Student[] + submissionData: Api.Submissions.SubmissionData + spinnerPath: string + checkpointIconPath: string + progressThresholds: number[] +} + +type State = { + open: Record + loaded: Record + openContentFiles: Record + data: Api.Submissions.Block[] + selectedSort: string + tablePositionY: number + standardsLoading: Record + showModal: boolean + progressThresholds: number[] +} + +export default class CohortSubmissions extends React.Component { + private challengeTableDiv: null | HTMLElement = null + + state = { + open: {}, + loaded: {}, + openContentFiles: {}, + data: this.props.submissionData.sections.length > 0 ? this.props.submissionData.sections : this.props.submissionData.blocks, + selectedSort: '1', + tablePositionY: this.challengeTableTop(), + standardsLoading: {}, + showModal: false, + progressThresholds: this.props.progressThresholds, + } + + componentDidMount() { + this.setSorts(); + this.resetTableHeight(); + this.reloadState(); + window.addEventListener('beforeunload', this.saveState); + } + + toggleModal = () => { + this.setState(prevState => ({ + ...prevState, + showModal: !prevState.showModal, + })); + } + + updateProgressThresholds = (progressThresholds: number[]) => { + this.setState({ progressThresholds }); + } + + reloadState() { + const standardsToFetch: number[] = []; + const blocks = extend(this.state.data); + let savedState: any + + try { + const savedStateKey = this.props.cohortId + '_submissions_state'; + savedState = JSON.parse(window.localStorage.getItem(savedStateKey) || 'null'); + + if (savedState) { + blocks.forEach((block) => { + block.standards.forEach((standard) => { + if (savedState.open[standard.id]) { + const content_file_count = standard.content_files.length; + + // TODO: Update to not modify state directly + this.state.open[standard.id] = true; + this.state.standardsLoading[standard.id] = true; + standard.content_files = []; + + for (let i = 0; i < content_file_count; i++) { + standard.content_files.push({ + challenges: [], + content_file_path: '', + content_file_type: '', + id: i, + title: '', + } as any); + } + + standardsToFetch.push(standard.id); + } + }); + }); + } + } catch (_error) { + savedState = null + } + + if (savedState) { + this.setState({ data: blocks, openContentFiles: savedState.openContentFiles }); + standardsToFetch.forEach((id) => { + this.fetchContentFilesForStandard(id); + }); + } + } + + expandAllStandards = () => { + const blocks = extend(this.state.data); + const standardsToFetch: number[] = []; + + blocks.forEach((block) => { + block.standards.forEach((standard) => { + const oldContentFilesCount = standard.content_files.length; + + // TODO: Update to not modify state directly + this.state.open[standard.id] = true; + this.state.standardsLoading[standard.id] = true; + standard.content_files = []; + + for (let i = 0; i < oldContentFilesCount; i++) { + standard.content_files.push({ + challenges: [], + content_file_path: '', + content_file_type: '', + id: i, + title: '', + } as any); + } + + standardsToFetch.push(standard.id); + }); + }); + + this.setState({ data: blocks }); + + standardsToFetch.forEach((id) => { + this.fetchContentFilesForStandard(id); + }); + } + + closeAllStandards = () => { + const blocks = extend(this.state.data); + + blocks.forEach((block) => { + block.standards.forEach((standard) => { + // TODO: Update to not modify state directly + this.state.standardsLoading[standard.id] = false; + this.state.open[standard.id] = false; + this.state.loaded[standard.id] = false; + standard.content_files = []; + this.setState({ data: blocks, openContentFiles: {} }); + }); + }); + } + + saveState = () => { + const savedStateKey = this.props.cohortId + '_submissions_state'; + + window.localStorage.setItem(savedStateKey, JSON.stringify(this.state)); + } + + setSorts() { + // Sorting A-Z / High To Low + const challengeIndexSortDropdown = this.savedValue('get', 'challengeIndexSortDropdown'); + + if (challengeIndexSortDropdown != null) { + this.setState({ selectedSort: challengeIndexSortDropdown }); + } + } + + savedValue(action: 'get' | 'set', id: string, value?: string) { + if (action == 'set') { + localStorage.setItem(this.props.cohortId + '_submissionsSelectValue_' + id, value!); + } else { + return localStorage.getItem(this.props.cohortId + '_submissionsSelectValue_' + id); + } + } + + resetTableHeight() { + this.setState({ tablePositionY: this.challengeTableTop() }); + } + + handleSortSelect = (e: any) => { + this.setState({ selectedSort: e.target.value }); + this.savedValue('set', e.target.id, e.target.value); + } + + sortedStudents() { + switch (this.state.selectedSort) { + case '1': + return this.sortAlpha(); + case '2': + return this.sortStandardProgress(); + case '3': + return this.sortNeedsGrading(); + case '4': + return this.sortStartDate(); + default: + return this.sortAlpha(); + } + } + + sortOptions() { + return [ + { + id: 1, + title: 'Alphabetical' + }, + { + id: 2, + title: 'Standards Progress' + }, + { + id: 3, + title: 'Needs grading' + }, + { + id: 4, + title: 'Start Date' + } + ]; + } + + sortAlpha() { + return this.props.students.sort((a, b) => { + const aName = a.name.toLowerCase(); + const bName = b.name.toLowerCase(); + + if (aName < bName) return -1; + if (aName > bName) return 1; + + return 0; + }); + } + + sortStandardProgress() { + return this.props.students.sort((a, b) => { + let aCount = 0; + let bCount = 0; + + if (this.props.cohortMode == 'Mastery') { + this.state.data.forEach((block) => { + block.standards.forEach((standard) => { + aCount += standard.student_performances[a.id].score === 3 ? 1 : 0; + bCount += standard.student_performances[b.id].score === 3 ? 1 : 0; + }); + }); + } else if (this.props.cohortMode == 'Percentage') { + const { completion_progress } = this.props.submissionData; + + this.state.data.forEach((block) => { + block.standards.forEach((standard) => { + const aCompleted = completion_progress[a.id][block.id][standard.uid].completed; + const bCompleted = completion_progress[b.id][block.id][standard.uid].completed; + const aTotal = completion_progress[a.id][block.id][standard.uid].total; + const bTotal = completion_progress[b.id][block.id][standard.uid].total; + + aCount += aCompleted === aTotal ? 1 : 0; + bCount += bCompleted === bTotal ? 1 : 0; + }); + }); + } + + if (aCount < bCount) return 1; + if (aCount > bCount) return -1; + + return 0; + }); + } + + sortNeedsGrading() { + return this.props.students.sort((a, b) => { + let aCount = 0; + let bCount = 0; + + this.state.data.forEach((block) => { + block.standards.forEach((standard) => { + aCount += standard.student_performances[a.id].ungraded_submission_state === 'needs_review' ? 1 : 0; + bCount += standard.student_performances[b.id].ungraded_submission_state === 'needs_review' ? 1 : 0; + }); + }); + + if (aCount < bCount) return 1; + if (aCount > bCount) return -1; + + return 0; + }); + } + + sortStartDate() { + return this.props.students.sort((a, b) => { + const aStartDate = new Date(a.cohort_reg_date); + const bStartDate = new Date(b.cohort_reg_date); + + if (bStartDate < aStartDate) return -1; + if (bStartDate > aStartDate) return 1; + + return 0; + }); + } + + toggleStandard(standardId: number) { + const blocks = extend(this.state.data); + + blocks.forEach((block) => { + block.standards.forEach((standard) => { + if (standard.id === standardId) { + if (this.state.open[standard.id]) { + + // TODO: Update to not modify state directly + this.state.standardsLoading[standard.id] = false; + this.state.open[standard.id] = false; + this.state.loaded[standard.id] = false; + + standard.content_files = []; + + this.setState({ data: blocks }); + } else { + const oldContentFilesLength = standard.content_files.length; + + // TODO: Update to not modify state directly + this.state.standardsLoading[standard.id] = true; + this.state.open[standard.id] = true; + this.state.loaded[standard.id] = false; + + standard.content_files = []; + + for (let i = 0; i < oldContentFilesLength; i++) { + standard.content_files.push({ + challenges: [], + content_file_path: '', + content_file_type: '', + id: i, + title: '', + } as any); + } + + this.setState({ data: blocks }); + this.fetchContentFilesForStandard(standardId); + } + } + }); + }); + + this.setSorts(); + } + + fetchContentFilesForStandard = (standardId: number) => { + http('GET', Routes.cohortStandardPath(this.props.cohortId, standardId)) + .then((contentFiles: any[]) => { + const blocks = extend(this.state.data); + + blocks.forEach((block) => { + block.standards.forEach((standard) => { + if (standard.id === standardId) { + // TODO: Update to not modify state directly + this.state.loaded[standard.id] = true; + standard.content_files = contentFiles.filter((cf) => { return cf.challenges.length > 0; }); + // TODO: Update to not modify state directly + this.state.standardsLoading[standard.id] = false; + this.setState({ data: blocks }); + } + }); + }); + + this.setSorts(); + }); + } + + toggleContentFile(contentFileId: number) { + const openContentFiles = extend(this.state.openContentFiles); + + if (openContentFiles[contentFileId]) { + delete openContentFiles[contentFileId]; + this.setState({ openContentFiles: openContentFiles }); + } else { + openContentFiles[contentFileId] = true; + this.setState({ openContentFiles }); + } + + this.setSorts(); + } + + challengeTableTop() { + if (!this.challengeTableDiv) return 0; + return this.challengeTableDiv.getBoundingClientRect().top; + } + + sortDropdown() { + if (this.props.students.length < 2) return null; + + return ( + + ); + } + + updateStudentPerformance(performance: any) { + const standardId = performance.standard_id; + const studentId = performance.user_id; + const blocks = extend(this.state.data); + + blocks.forEach((block) => { + block.standards.forEach((standard) => { + if (standard.id === standardId) { + standard.student_performances[studentId].score = performance.score; + } + }); + }); + + this.setState({ data: blocks }); + } + + blocks() { + const rows: React.ReactNode[] = []; + + this.state.data.forEach((block) => { + rows.push( +
    + {block.title} +
    + ); + + block.standards.forEach((standard) => { + if (!standard.content_files) return; + + const contentFiles: React.ReactNode[] = []; + let openStandardClass = ''; + + // push content files + if (this.state.open[standard.id] && standard.content_files.length > 0) { + openStandardClass = '-open'; + standard.content_files.forEach((content_file, i) => { + const challenges: React.ReactNode[] = []; + let openContentFileClass = ''; + let firstContentFileClass = ''; + let toggleIconElem; + + if (i === 0) firstContentFileClass = 'firstcontentfile'; + + if (content_file.title !== '') { + toggleIconElem = ( +
    + +
    + ); + } + + if (this.state.openContentFiles[content_file.id]) { + openContentFileClass = '-open'; + + // push challenges + content_file.challenges.forEach((challenge, j) => { + challenges.push( +
    +
    +
    + {`Challenge ${j + 1}`} +
    +
    + {challenge.title} +
    +
    +
    + ); + }); + } + + contentFiles.push( + + ); + }); + } + + rows.push( + + ); + }); + }); + + return rows; + } + + render() { + const { cohortMode, submissionData: { completion_progress } } = this.props; + + return ( +
    + {cohortMode == 'Percentage' && ( + <> + +
    + +
    + +
    +
    + + )} +
    + +
    +
    Black/Gray - Unscored
    +
    Blue - Correct
    +
    Red - Incorrect
    +
    +
    +
    +
    + + Expand All + + {` | `} + + Collapse All + + + Option-click to manually score any standard + +
    + {this.sortDropdown()} +
    +
    this.challengeTableDiv = el}> +
    + {this.blocks()} +
    + { this.toggleContentFile(contentFileId); } } + openContentFiles={this.state.openContentFiles} + cohortMode={this.props.submissionData.cohort_mode} + cohortId={this.props.cohortId} + progressThresholds={this.state.progressThresholds} + students={this.sortedStudents()} + submissionData={{ ...completion_progress, blocks: this.state.data }} + openStandards={this.state.open} + tablePositionY={this.state.tablePositionY} + checkpointIconPath={this.props.checkpointIconPath} + updateStudentPerformanceHandler={(performance) => { this.updateStudentPerformance(performance); } } + /> + +
    +
    + ); + } +} diff --git a/scripts/app/javascript/components/cohorts/cohort_submissions/CohortSubmissionsChallengeItem.tsx b/scripts/app/javascript/components/cohorts/cohort_submissions/CohortSubmissionsChallengeItem.tsx new file mode 100644 index 0000000000000000000000000000000000000000..ad15c7a7986b6f885b3061745b1882e6f7888035 --- /dev/null +++ b/scripts/app/javascript/components/cohorts/cohort_submissions/CohortSubmissionsChallengeItem.tsx @@ -0,0 +1,113 @@ +import * as React from 'react' +import * as Routes from '../../../generated/routes' +import Icon from '../../Icon' +import PartialCreditBaton from '../../shared/ChallengePoints/PartialCreditBaton'; + +type Props = any + +export default class CohortSubmissionsChallengeItem extends React.Component { + private studentAnswer: any + private anchorStyle: any + + constructor(props: Props) { + super(props); + + this.studentAnswer = {}; + this.anchorStyle = {}; + } + + createStatusElement(id: string, sprite: string, color?: string) { + let url; + + if (this.props.checkpointSubmissionLink) { + url = this.props.checkpointSubmissionLink; + } else { + url = Routes.cohortUserChallengePath(this.props.cohortId, this.props.studentId, this.props.challenge.id); + } + + if (id === '#ic_file_upload_24px') { + return ( + + {this.ungradedCircle()} + + ); + } + + if (color) { + return ( + + + + ); + } else { + return ( + + + + ); + } + } + + partialCreditBaton() { + if (this.props.checkpointSubmissionLink) { + return ( + + + + ); + } + } + + ungradedCircle() { + return ( + + + + + + + ); + } + + render() { + let statusElement; + let status = this.props.studentAnswer; + + if (status != 'n/a') { + this.anchorStyle = { + textDecoration: 'none', + color: 'inherit', + cursor: 'pointer' + }; + if (status === 'ungraded') { + statusElement = this.createStatusElement('#ic_file_upload_24px', 'file'); + } else if (this.props.cohortMode === "Percentage" && status === 'correct' && this.props.correctPoints > 0 && this.props.correctPoints !== this.props.totalPoints) { + statusElement = this.partialCreditBaton(); + } else if (status === 'correct') { + statusElement = this.createStatusElement('#ic_check_24px', 'navigation', 'primary-color'); + } else if (status === 'incorrect') { + statusElement = this.createStatusElement('#ic_close_24px', 'navigation', 'error-color'); + } else if (status === 'failed') { + statusElement = this.createStatusElement('#ic_warning_24px', 'alert', 'error-color'); + } + } + + return ( +
    + { statusElement } +
    + ); + } +} diff --git a/scripts/app/javascript/components/cohorts/cohort_submissions/CohortSubmissionsLessonRow.tsx b/scripts/app/javascript/components/cohorts/cohort_submissions/CohortSubmissionsLessonRow.tsx new file mode 100644 index 0000000000000000000000000000000000000000..a65fd6b5cdeeceebbe3e1495ac3e60f61b93b5a7 --- /dev/null +++ b/scripts/app/javascript/components/cohorts/cohort_submissions/CohortSubmissionsLessonRow.tsx @@ -0,0 +1,97 @@ +import * as React from 'react' +import SvgRenderer from '../../SvgRenderer' +import positionLabel from '../../../lib/utils' + +type Props = { + openContentFileClass: string + firstContentFileClass: string + contentFile: Api.ContentFile + toggleIconElem: JSX.Element | undefined + toggleContentFileHandler: any + challengeElems: React.ReactNode[] +} +type State = { + showActionMenu: boolean +} + +export default class CohortSubmissionsLessonRow extends React.Component { + constructor(props: Props) { + super(props); + this.state = { + showActionMenu: false + }; + + this.handleMenuOutsideClick = this.handleMenuOutsideClick.bind(this); + this.toggleActionMenu = this.toggleActionMenu.bind(this); + } + + addClickListeners(handler: any) { + document.addEventListener('touchmove', handler, false); + document.addEventListener('click', handler, false); + } + + removeClickListeners(handler: any) { + document.removeEventListener('touchmove', handler, false); + document.removeEventListener('click', handler, false); + } + + handleMenuOutsideClick(e: any) { + if (document.getElementById('action-menu') && document.getElementById('action-menu')!.contains(e.target)) { + return; + } + + this.toggleActionMenu(); + } + + toggleActionMenu() { + this.setState({ showActionMenu: !this.state.showActionMenu }); + } + + get actionMenu() { + if (this.state.showActionMenu) { + this.addClickListeners(this.handleMenuOutsideClick); + + return ( +
    + +
    + ); + } else { + this.removeClickListeners(this.handleMenuOutsideClick); + } + } + + get actionMenuWrapper() { + let kebabClass = this.state.showActionMenu ? 'action-kebab active' : 'action-kebab'; + + if (this.props.contentFile.title !== '') { + return ( +
    + + + + { this.actionMenu } +
    + ); + } + } + + render() { + let contentFile = this.props.contentFile; + let label = positionLabel(contentFile.type_relative_position, contentFile.content_file_type) + + return ( +
    +
    + { this.props.toggleIconElem } +
    { label }
    +
    { contentFile.title }
    + {this.actionMenuWrapper} +
    + {this.props.challengeElems} +
    + ); + } +} diff --git a/scripts/app/javascript/components/cohorts/cohort_submissions/CohortSubmissionsPerformanceModal.tsx b/scripts/app/javascript/components/cohorts/cohort_submissions/CohortSubmissionsPerformanceModal.tsx new file mode 100644 index 0000000000000000000000000000000000000000..34965af0f67aea9e4c1c18b7ee867417698a24e8 --- /dev/null +++ b/scripts/app/javascript/components/cohorts/cohort_submissions/CohortSubmissionsPerformanceModal.tsx @@ -0,0 +1,110 @@ +import * as React from 'react' +import http from '../../../lib/http' +import Icon from '../../Icon' + +type Props = any +type State = any + +export default class CohortSubmissionsPerformanceModal extends React.Component { + constructor(props: Props) { + super(props); + this.state = { showActionMenu: false, showSuccessCriteria: false }; + + this.handleMenuOutsideClick = this.handleMenuOutsideClick.bind(this); + this.toggleActionMenu = this.toggleActionMenu.bind(this); + this.toggleSuccessCriteria = this.toggleSuccessCriteria.bind(this); + this.scoreHandler = this.scoreHandler.bind(this); + } + + componentWillReceiveProps(nextProps: Props) { + this.setState({ showActionMenu: nextProps.showManualPerformanceModalBool }); + } + + addClickListeners(handler: any) { + document.addEventListener('touchmove', handler, false); + document.addEventListener('click', handler, false); + } + + removeClickListeners(handler: any) { + document.removeEventListener('touchmove', handler, false); + document.removeEventListener('click', handler, false); + } + + handleMenuOutsideClick(e: any) { + if (document.getElementById('manual-score-performance') && document.getElementById('manual-score-performance')!.contains(e.target)) { + return; + } + + this.toggleActionMenu(); + } + + toggleSuccessCriteria() { + this.setState({ showSuccessCriteria: !this.state.showSuccessCriteria }); + } + + toggleActionMenu() { + this.setState({ showActionMenu: !this.state.showActionMenu }); + this.props.clearOutOpenManualPerformanceModal(); + } + + scoreHandler(score: any) { + let url = '/cohorts/' + this.props.cohortId + '/users/' + this.props.studentStandard.student_id + '/performances'; + + http('POST', url, { + body: { + performance: { + score: score, + standard_ids: [this.props.standard.id] + } + } + }) + .then((data: any) => { + this.props.updateStudentPerformanceHandler(data[0]); + this.toggleActionMenu(); + }); + } + + get successCriteria() { + if (this.state.showSuccessCriteria) { + return ( +
    +

    {this.props.standard.description}

    +

    Success Criteria

    +
    +
    + ); + } + } + + get actionMenu() { + if (this.state.showActionMenu) { + this.addClickListeners(this.handleMenuOutsideClick); + + let currentScore = this.props.studentStandard.score; + + return ( +
    +
    +
    +
    this.scoreHandler(0)}>0
    +
    this.scoreHandler(1)}>1
    +
    this.scoreHandler(2)}>2
    +
    this.scoreHandler(3)}>3
    +
    + +
    +
    + {this.successCriteria} +
    + ); + } else { + this.removeClickListeners(this.handleMenuOutsideClick); + } + } + + render() { + return ( +
    {this.actionMenu}
    + ); + } +} diff --git a/scripts/app/javascript/components/cohorts/cohort_submissions/CohortSubmissionsStandardRow.tsx b/scripts/app/javascript/components/cohorts/cohort_submissions/CohortSubmissionsStandardRow.tsx new file mode 100644 index 0000000000000000000000000000000000000000..98d54728f35a867543451626b39d2c3188f7cf19 --- /dev/null +++ b/scripts/app/javascript/components/cohorts/cohort_submissions/CohortSubmissionsStandardRow.tsx @@ -0,0 +1,97 @@ +import * as React from 'react' +import Icon from '../../Icon' +import SvgRenderer from '../../SvgRenderer' + +type Props = any +type State = { + showActionMenu: boolean +} + +export default class CohortSubmissionsStandardRow extends React.Component { + constructor(props: Props) { + super(props); + this.state = { + showActionMenu: false + }; + + this.handleMenuOutsideClick = this.handleMenuOutsideClick.bind(this); + this.toggleActionMenu = this.toggleActionMenu.bind(this); + } + + addClickListeners(handler: any) { + document.addEventListener('touchmove', handler, false); + document.addEventListener('click', handler, false); + } + + removeClickListeners(handler: any) { + document.removeEventListener('touchmove', handler, false); + document.removeEventListener('click', handler, false); + } + + handleMenuOutsideClick(e: any) { + if (document.getElementById('action-menu') && document.getElementById('action-menu')!.contains(e.target)) { + return; + } + + this.toggleActionMenu(); + } + + toggleActionMenu() { + this.setState({ showActionMenu: !this.state.showActionMenu }); + } + + get actionMenu() { + if (this.state.showActionMenu) { + this.addClickListeners(this.handleMenuOutsideClick); + + return ( +
    + +
    + ); + } else { + this.removeClickListeners(this.handleMenuOutsideClick); + } + } + + actionMenuWrapper = () => { + let kebabClass = this.state.showActionMenu ? 'action-kebab active' : 'action-kebab'; + + return ( +
    + + + + { this.actionMenu } +
    + ); + } + + get LoadingSpinner() { + if (this.props.showLoadingIcon) { + return ( + loading + ); + } + } + + render() { + let standard = this.props.standard; + + return ( +
    +
    +
    +
    + { standard.title } +
    + {this.LoadingSpinner} + {this.props.standard.submissions_path && this.actionMenuWrapper()} +
    + {this.props.contentFiles} +
    + ); + } +} diff --git a/scripts/app/javascript/components/cohorts/cohort_submissions/CohortSubmissionsStudentColumn.tsx b/scripts/app/javascript/components/cohorts/cohort_submissions/CohortSubmissionsStudentColumn.tsx new file mode 100644 index 0000000000000000000000000000000000000000..33231cec093594e1f37edab3d9f96c95aaac79eb --- /dev/null +++ b/scripts/app/javascript/components/cohorts/cohort_submissions/CohortSubmissionsStudentColumn.tsx @@ -0,0 +1,226 @@ +import * as React from 'react' +import * as Routes from '../../../generated/routes' +import UserAvatar from '../../UserAvatar' +import CohortSubmissionsChallengeItem from './CohortSubmissionsChallengeItem' +import UnitPercent from './CohortSubmissionsUnitPercent' +import DonutRing from '../../shared/DonutRing/DonutRing' +import CohortSubmissionsStudentStandard from './CohortSubmissionsStudentStandard' +import { findThresholdColor } from '../../shared/ProgressThresholdsSlider/ProgressThresholdsSlider' + +type Props = { + key: string + student: Api.Submissions.Student + cohortId: number + progressThresholds: number[] + cohortMode: Api.CohortMode + openStandards: Record + submissionData: Api.Submissions.SubmissionData + openContentFiles: Record + checkpointIconPath: string + toggleContentFileHandler: (contentFileId: number) => void + updateStudentPerformanceHandler: (performance: any) => void +} + +type State = { + openContentFiles: Record + openedManualPerformanceModal: string +} + +export default class CohortSubmissionsStudentColumn extends React.Component { + state: State = { + openContentFiles: this.props.openContentFiles, + openedManualPerformanceModal: '', + } + + componentDidUpdate(_: Props, prevState: State) { + if (!this.openContentFilesEqual(prevState.openContentFiles, this.props.openContentFiles)) { + this.setState({ openContentFiles: this.props.openContentFiles }); + } + } + + openContentFilesEqual = (setOne: Record, setTwo: Record): boolean => { + return Object.keys(setOne).sort().join(',') === Object.keys(setTwo).sort().join(','); + } + + renderStudentData() { + const out: React.ReactNode[] = []; + const studentId = this.props.student.id; + + this.props.submissionData.blocks.map((block) => { + out.push( +
    + ); + + block.standards.forEach((standard) => { + if (!standard.content_files) return; + let progress: { completed: number, total: number }; + if (this.props.cohortMode == 'Percentage') { + progress = this.props.submissionData[this.props.student.id][block.id][standard.uid!] + } + + const studentStandard = standard.student_performances[studentId]; + const contentFileRollup: React.ReactNode[] = []; + + if (this.props.openStandards[standard.id] && standard.content_files.length > 0) { + standard.content_files.forEach((contentFile) => { + const studentSubmissionStates: any[] = []; + const contentFileOpen = this.state.openContentFiles[contentFile.id]; + const challengeRollup: React.ReactNode[] = []; + + contentFile.challenges.forEach((challenge) => { + const submissionState = challenge.student_submissions[studentId].status; + let checkpointSubmissionLink; + + if (contentFile.content_file_type === 'checkpoint') { + if (studentStandard.ungraded_submission_id) { + checkpointSubmissionLink = `${Routes.cohortCheckpointSubmissionPath(this.props.cohortId, studentStandard.ungraded_submission_id)}#challenge_${challenge.id}`; + } else if (studentStandard.checkpoint_submission_id) { + checkpointSubmissionLink = `${Routes.cohortCheckpointSubmissionPath(this.props.cohortId, studentStandard.checkpoint_submission_id)}#challenge_${challenge.id}`; + } + } + + studentSubmissionStates.push(submissionState); + + if (contentFileOpen) { + challengeRollup.push( + + ); + } + }); + + // add to content file rollup + const studentRollup = { + correct: studentSubmissionStates.filter((state) => state === 'correct' || state.status === 'correct').length, + incorrect: studentSubmissionStates.filter((state) => state === 'incorrect' || state.status === 'incorrect').length, + ungraded: studentSubmissionStates.filter((state) => { + return state.status !== undefined + ? (state.status.indexOf('correct') < 0 && state.status !== 'n/a') + : (state.indexOf('correct') < 0 && state !== 'n/a') + }).length + }; + + if (contentFile.content_file_type === 'checkpoint' && this.props.cohortMode == 'Percentage') { + contentFileRollup.push( +
    +
    + {progress && } +
    + {challengeRollup} +
    + ) + } else { + contentFileRollup.push( +
    +
    + {(studentRollup.correct > 0 || studentRollup.incorrect > 0 || studentRollup.ungraded > 0) && ( + + )} +
    + {challengeRollup} +
    + ); + } + }); + }; + + const showManualPerformanceModalBool = this.state.openedManualPerformanceModal === `${standard.id} ${this.props.student.id}`;; + let colorHighlight + + if (this.props.cohortMode == 'Percentage') { + const percentage = Math.floor(studentStandard.correct_points / studentStandard.total_points * 100) + + colorHighlight = findThresholdColor(this.props.progressThresholds, percentage) + + // @ts-ignore: Variable 'progress' is used before being assigned error from bad control flow check, is assigned + if (progress.completed === progress.total && !studentStandard.checkpoint_submission_id && !studentStandard.ungraded_submission_id) colorHighlight = "#88CBD1" + } + + out.push( +
    +
    +
    + +
    +
    + {contentFileRollup} +
    + ); + }); + }); + + return out; + } + + clearOutOpenManualPerformanceModal = () => { + this.setState({ openedManualPerformanceModal: '' }); + } + + showManualPerformanceModal = (e: any) => { + if (this.props.cohortMode !== 'Mastery') return; + + if (e.altKey) { + e.preventDefault(); + const clickedOnStandardCell = `${e.target.dataset.standardId} ${e.target.dataset.studentId}`; + + if (this.state.openedManualPerformanceModal === clickedOnStandardCell) { + this.setState({ openedManualPerformanceModal: '' }); + } else { + this.setState({ openedManualPerformanceModal: clickedOnStandardCell }); + } + } + } + + render() { + return ( +
    + + + + {this.renderStudentData()} +
    + ); + } +} diff --git a/scripts/app/javascript/components/cohorts/cohort_submissions/CohortSubmissionsStudentNameBar.tsx b/scripts/app/javascript/components/cohorts/cohort_submissions/CohortSubmissionsStudentNameBar.tsx new file mode 100644 index 0000000000000000000000000000000000000000..02b4fdf7c2390b9863067fdd41d43ce710c108fe --- /dev/null +++ b/scripts/app/javascript/components/cohorts/cohort_submissions/CohortSubmissionsStudentNameBar.tsx @@ -0,0 +1,70 @@ +import * as React from 'react' +import UserAvatar from '../../UserAvatar' + +type Props = any +type State = { + showFixedElement: boolean +} + +export default class CohortSubmissionsStudentNameBar extends React.Component { + private studentNameBarRef: any + + constructor(props: Props) { + super(props); + this.state = { + showFixedElement: false + }; + + this.handleScroll = this.handleScroll.bind(this); + } + + get StudentAvatars() { + return this.props.students.map((student: any) => { + return ( +
    + +
    + ); + }); + } + + shouldComponentUpdate(nextProps: Props, nextState: State) { + let fixedElementChanged = this.state.showFixedElement !== nextState.showFixedElement; + + return fixedElementChanged; + } + + componentDidMount() { + window.addEventListener('scroll', this.handleScroll); + } + + componentWillUnmount() { + window.removeEventListener('scroll', this.handleScroll); + } + + handleScroll() { + let scrollTop = window.pageYOffset || + document.documentElement!.scrollTop || + document.body.scrollTop; + + if (this.state.showFixedElement === false && scrollTop >= this.props.tablePositionY) { + this.setState({ showFixedElement: true }); + } else if (this.state.showFixedElement === true && scrollTop < this.props.tablePositionY) { + this.setState({ showFixedElement: false }); + } + } + + render() { + return ( +
    (this.studentNameBarRef = el)} > +
    + { this.StudentAvatars } +
    +
    + ); + } +} diff --git a/scripts/app/javascript/components/cohorts/cohort_submissions/CohortSubmissionsStudentStandard.tsx b/scripts/app/javascript/components/cohorts/cohort_submissions/CohortSubmissionsStudentStandard.tsx new file mode 100644 index 0000000000000000000000000000000000000000..f68a86bd9d18be79a2b0335f8b01510f687515ec --- /dev/null +++ b/scripts/app/javascript/components/cohorts/cohort_submissions/CohortSubmissionsStudentStandard.tsx @@ -0,0 +1,208 @@ +import * as React from 'react' +import * as Routes from '../../../generated/routes' +import DonutRing from '../../shared/DonutRing/DonutRing' +import CohortSubmissionsPerformanceModal from './CohortSubmissionsPerformanceModal' +import UnitPercent from './CohortSubmissionsUnitPercent' +import SvgRenderer from '../../SvgRenderer'; + +type Props = { + cohortId: number + blockId: number + student: Api.Submissions.Student + standard: Api.Submissions.Standard + cohortMode: Api.CohortMode + submissionData: Api.Submissions.SubmissionData | any + studentStandard: Api.Submissions.StudentPerformance + showManualPerformanceModalBool: boolean + updateStudentPerformanceHandler: (performance: any) => void + clearOutOpenManualPerformanceModal: () => void + showManualPerformanceModalHandler: (event: any) => void +} + +type State = {} + +export default class CohortSubmissionsStudentStandard extends React.Component { + constructor(props: Props) { + super(props); + this.state = { studentStandard: this.props.studentStandard }; + } + + masteryView() { + const perf = this.props.studentStandard; + + if (perf.ungraded_submission_state === 'rejected' && !perf.score) { + // render full exclamation + return ( + + + + ); + } else if ( + perf.score + && (perf.ungraded_submission_state === 'rejected' + || perf.checkpoint_state === 'rejected' + )) { + // render score with tiny exclamation- determine if you should use the ungraded_submission_id or checkpoint_submission_id + const submission_id = perf.ungraded_submission_id ? perf.ungraded_submission_id : perf.checkpoint_submission_id; + + return ( + + +
    + {perf.score} +
    +
    + ); + } else if (perf.score && !perf.ungraded_submission_id && perf.checkpoint_state === 'done') { + // render score circle + return ( + +
    + {perf.score} +
    +
    + ); + } else if (perf.checkpoint_state === 'needs_review') { + // render ring + const submissionLinkId = perf.ungraded_submission_id ? perf.ungraded_submission_id : perf.checkpoint_submission_id; + + return ( + +
    + {perf.score} +
    +
    + ); + } else if (perf.ungraded_submission_state === 'retry' && !perf.score) { + // render refresh symbol + return ( + + + + ); + } else if (perf.score && (perf.ungraded_submission_state === 'retry' || perf.checkpoint_state === 'retry') && perf.ungraded_submission_state !== "needs_review") { + // render refresh symbol with prior score + let submissionId = perf.ungraded_submission_id !== undefined ? perf.ungraded_submission_id : perf.checkpoint_submission_id + + return ( + + +
    + {perf.score} +
    +
    + ); + } else if (perf.ungraded_submission_id) { + // render ring, potentially with previous checkpoint score + const scoreClass = perf.score ? `-score-${perf.score}` : null; + + return ( + +
    + {perf.score} +
    +
    + ); + } else if (perf.score && !perf.checkpoint_submission_id) { + const scoreClass = perf.score ? `-score-${perf.score}` : null; + + return ( + +
    + {perf.score} +
    +
    + ); + } + } + + percentageView() { + const { student, studentStandard, submissionData, standard } = this.props + const perf = studentStandard; + const progress = submissionData[student.id][this.props.blockId][standard.uid!] + if (this.props.cohortMode !== 'Percentage') return + + if (!progress) return + + return + } + + render() { + const { cohortMode } = this.props; + + return ( + <> + {cohortMode === 'Percentage' ? this.percentageView() : this.masteryView()} + + + ); + } +} diff --git a/scripts/app/javascript/components/cohorts/cohort_submissions/CohortSubmissionsTable.tsx b/scripts/app/javascript/components/cohorts/cohort_submissions/CohortSubmissionsTable.tsx new file mode 100644 index 0000000000000000000000000000000000000000..22b9a9bafda58286ffb006facf623485c17b5175 --- /dev/null +++ b/scripts/app/javascript/components/cohorts/cohort_submissions/CohortSubmissionsTable.tsx @@ -0,0 +1,73 @@ +import * as React from 'react' +import CohortSubmissionsStudentColumn from './CohortSubmissionsStudentColumn' +import CohortSubmissionsStudentNameBar from './CohortSubmissionsStudentNameBar' + +const {ScrollSync, ScrollSyncPane} = require('react-scroll-sync') + +type Props = { + cohortId: number + submissionData: Api.Submissions.SubmissionData | any + students: Api.Submissions.Student[] + cohortMode: Api.CohortMode + progressThresholds: number[] + tablePositionY: number + openStandards: Record + openContentFiles: Record + checkpointIconPath: string + toggleContentFileHandler: (contentFileId: number) => void + updateStudentPerformanceHandler: (performance: any) => void +} + +type State = { + openContentFiles: Props['openContentFiles'] +} + +export default class CohortSubmissionsTable extends React.Component { + private studentPanelRef: any + + state: State = { + openContentFiles: this.props.openContentFiles, + } + + componentWillReceiveProps(nextProps: Props) { + this.setState({ openContentFiles: nextProps.openContentFiles }); + } + + renderStudentColumns() { + return this.props.students.map(student => ( + + )); + } + + render() { + return ( + +
    (this.studentPanelRef = el)} > + +
    + {this.renderStudentColumns()} +
    +
    + + + +
    +
    + ); + } +} diff --git a/scripts/app/javascript/components/cohorts/cohort_submissions/CohortSubmissionsUnitPercent.tsx b/scripts/app/javascript/components/cohorts/cohort_submissions/CohortSubmissionsUnitPercent.tsx new file mode 100644 index 0000000000000000000000000000000000000000..4bcbf3c119227938347fbfc04b5bbefd2acae412 --- /dev/null +++ b/scripts/app/javascript/components/cohorts/cohort_submissions/CohortSubmissionsUnitPercent.tsx @@ -0,0 +1,104 @@ +import React from 'react'; +import * as Routes from '../../../generated/routes' +import DonutRing from '../../shared/DonutRing/DonutRing' + +type Props = { + student: Api.Submissions.Student + perf: Api.Submissions.StudentPerformance + progress: { completed: number, total: number } + standard: Api.Submissions.Standard + cohortId: number + forceLink?: boolean + isForCheckpoint: boolean | false +} + +const UnitPercent = ({student, perf, progress, standard, cohortId, forceLink, isForCheckpoint}: Props) => { + if (perf.correct_points >= 0 && perf.total_points > 0 && perf.ungraded_submission_id && perf.ungraded_submission_id > 0 && perf.ungraded_submission_state !== "retry") { + const submissionLinkId = perf.ungraded_submission_id || perf.checkpoint_submission_id; + + return ( + +
    + + {Math.floor(perf.correct_points / perf.total_points * 100)}% + + + ) + } else if (progress.completed > 0 && perf.ungraded_submission_id && perf.ungraded_submission_id > 0 && perf.ungraded_submission_state !== "retry") { + const submissionLinkId = perf.ungraded_submission_id || perf.checkpoint_submission_id; + + return ( + +
    +
    +
    +
    + ) + } else if (perf.correct_points >= 0 && perf.total_points > 0 && forceLink) { + var submissionLinkId = perf.ungraded_submission_id || perf.checkpoint_submission_id; + + if (perf.ungraded_submission_state === "retry") { + submissionLinkId = perf.checkpoint_submission_id; + } + + return ( + + + {Math.floor(perf.correct_points / perf.total_points * 100)}% + + + ) + } else if (perf.correct_points >= 0 && perf.total_points > 0) { + return ( + + {Math.floor(perf.correct_points / perf.total_points * 100)}% + + ) + } else if (progress.completed > 0 && isForCheckpoint == false) { + return ( + +
    + +
    +
    + ) + } else if (perf.ungraded_submission_id && perf.ungraded_submission_id > 0) { + var submissionLinkId = perf.ungraded_submission_id || perf.checkpoint_submission_id; + + if (perf.ungraded_submission_state === "retry") { + submissionLinkId = perf.checkpoint_submission_id; + } + + return ( + +
    +
    +
    + ) + } else { + return null + } +} + +export default UnitPercent; diff --git a/scripts/app/javascript/components/cohorts/mastery/MasteryTable.tsx b/scripts/app/javascript/components/cohorts/mastery/MasteryTable.tsx new file mode 100644 index 0000000000000000000000000000000000000000..0396a0f8dc4a44bcafdeb7f0b494c23a486e6e2b --- /dev/null +++ b/scripts/app/javascript/components/cohorts/mastery/MasteryTable.tsx @@ -0,0 +1,312 @@ +import * as React from 'react' +import StudentHeader from './StudentHeader' +import ReleaseBody from './ReleaseBody' +import MetricsBody from './MetricsBody' +import http from '../../../lib/http' + +type Props = { + performancesUrl: string + spinnerPath: string +} +type State = any + +export default class MasteryTable extends React.Component { + constructor(props: any) { + super(props); + + this.state = { + releaseFilter: null, + loaded: false, + selectedCell: { standardId: null, studentId: null, releaseId: null }, + students: [], + releases: [], + indexedReleases: {}, + standards: [], + performances: [], + emptyState: null + }; + + this.moveLeft = this.moveLeft.bind(this); + this.moveRight = this.moveRight.bind(this); + this.moveDown = this.moveDown.bind(this); + this.moveUp = this.moveUp.bind(this); + this.selectedCellDidChange = this.selectedCellDidChange.bind(this); + this.releaseWasClicked = this.releaseWasClicked.bind(this); + this.releaseWasFiltered = this.releaseWasFiltered.bind(this); + this.clearFilters = this.clearFilters.bind(this); + } + + getStandardsInOrder(releases: any[]) { + return releases.reduce((result, release) => { + return result.concat(release.standards); + }, []); + } + + componentDidMount() { + http('GET', this.props.performancesUrl) + .then((response: any) => { + response.loaded = true; + + let noReleasesMsg = (

    Hmm....looks like no block releases have been added to this cohort.

    ); + + let noStudentsMsg = (

    Hmm....looks like no students have been added to the cohort yet.

    ); + + let noReleases = !response.releases || !response.releases.length; + let noStudents = !response.students || !response.students.length; + + if (noReleases && noStudents) { + response.emptyState = ( +
    + {noReleasesMsg} + {noStudentsMsg} +
    + ); + } else if (noReleases) { + response.emptyState = noReleasesMsg; + } else if (noStudents) { + response.emptyState = noStudentsMsg; + } else { + response.selectedCell = { + standardId: response.releases[0].standards[0].id, + studentId: response.students[0].id, + releaseId: response.releases[0].id + }; + } + + this.setState(response); + }); + + document.addEventListener('keydown', (e: any) => { + if (['0', '1', '2', '3'].indexOf(e.key) > -1) { this.handleScore(e.key); } + if (e.key == 'h' || (e.key == 'b' && e.ctrlKey)) { this.moveLeft(); } + if (e.key == 'l' || (e.key == 'f' && e.ctrlKey)) { this.moveRight(); } + if (e.key == 'j' || (e.key == 'n' && e.ctrlKey)) { this.moveDown(); } + if (e.key == 'k' || (e.key == 'p' && e.ctrlKey)) { this.moveUp(); } + }); + } + + handleScore(key: any) { + let studentUrl; + + for (var i = 0; i < this.state.students.length; i++) { + if (this.state.students[i].id === this.state.selectedCell.studentId) { + studentUrl = this.state.students[i].performance_path; + } + } + http('POST', studentUrl, { + body: { + performance: { + standard_ids: [this.state.selectedCell.standardId], + score: key + } + } + }).then((response: any) => { + var new_performance = response[0]; + this.state.performances[new_performance.user_id] = this.state.performances[new_performance.user_id] || {}; + this.state.performances[new_performance.user_id][new_performance.standard_id] = new_performance; + this.setState({ performances: this.state.performances }); + }); + } + + getStandardById(id: any) { + for (var i = 0; i < this.state.standards.length; i++) { + if (this.state.standards[i].id === id) { + return this.state.standards[i]; + } + } + } + + moveLeft() { + let studentIds = this.state.students.map((student: any) => { + return student.id; + }); + let index = studentIds.indexOf(this.state.selectedCell.studentId); + + if (index > 0) { + index--; + this.setState({ + selectedCell: { + standardId: this.state.selectedCell.standardId, + releaseId: this.state.selectedCell.releaseId, + studentId: studentIds[index] + } + }); + } + } + + moveRight() { + let studentIds = this.state.students.map((student: any) => { + return student.id; + }); + let index = studentIds.indexOf(this.state.selectedCell.studentId); + + if (index < studentIds.length - 1) { + index++; + this.setState({ + selectedCell: { + standardId: this.state.selectedCell.standardId, + releaseId: this.state.selectedCell.releaseId, + studentId: studentIds[index] + } + }); + } + } + + moveDown() { + let filteredReleases = this.getFilteredReleases(); + let standardIds = filteredReleases.reduce((result: any, release: any) => { + return result.concat(release.standards.map((o: any) => { + return o.id; + })); + }, []); + let index = standardIds.indexOf(this.state.selectedCell.standardId); + + if (index < standardIds.length - 1) { + this.setState({ + selectedCell: { + standardId: standardIds[index + 1], + releaseId: this.getStandardById(standardIds[index + 1]).release_id, + studentId: this.state.selectedCell.studentId + } + }); + } + } + + moveUp() { + let filteredReleases = this.getFilteredReleases(); + let standardIds = filteredReleases.reduce((result: any, release: any) => { + return result.concat(release.standards.map((o: any) => { + return o.id; + })); + }, []); + let index = standardIds.indexOf(this.state.selectedCell.standardId); + + if (index > 0) { + this.setState({ + selectedCell: { + standardId: standardIds[index - 1], + releaseId: this.getStandardById(standardIds[index - 1]).release_id, + studentId: this.state.selectedCell.studentId + } + }); + } + } + + selectedCellDidChange(e: any, selectedCell: any) { + this.setState({ selectedCell: selectedCell }); + } + + releaseWasClicked(e: any) { + e.preventDefault(); + + this.setState({ + releaseFilter: e.target.innerHTML + }); + + let filteredReleases = this.getFilteredReleases(); + + this.setState({ + selectedCell: { + releaseId: filteredReleases[0].id, + standardId: filteredReleases[0].standards[0].id, + studentId: this.state.students[0].id + } + }); + } + + releaseWasFiltered(e: any, release: any) { + e.preventDefault(); + + this.setState({ + releaseFilter: release.id, + selectedCell: { + releaseId: release.id, + standardId: release.standards[0].id, + studentId: this.state.students[0].id + } + }); + } + + clearFilters(e: any) { + e.preventDefault(); + + this.setState({ + releaseFilter: null, + selectedCell: { + releaseId: this.state.releases[0].id, + standardId: this.state.releases[0].standards[0].id, + studentId: this.state.students[0].id + } + }); + } + + getFilteredReleases() { + if (this.state.releaseFilter) { + return this.state.releases.filter((release: any) => { + return release.id === this.state.releaseFilter; + }); + } else { + return this.state.releases; + } + } + + buildStudentHeaders() { + let studentHeaders = this.state.students.map((student: any) => { + return ( + + ); + }); + + return ( + + + + + { studentHeaders } + + + ); + } + + render() { + if (this.state.emptyState) return this.state.emptyState; + + if (!this.state.loaded) return

    Loading...

    ; + + let clearFiltersLink; + + if (this.state.releaseFilter) { + clearFiltersLink = Clear Filters; + } + + let filteredReleases = this.getFilteredReleases(); + let studentHeaders = this.buildStudentHeaders(); + let releaseBodies = filteredReleases.map((release: any) => { + return ( + + ); + }); + + return ( +
    + { clearFiltersLink } + + { studentHeaders } + + { releaseBodies } +
    +
    + ); + } +} diff --git a/scripts/app/javascript/components/cohorts/mastery/MetricRow.tsx b/scripts/app/javascript/components/cohorts/mastery/MetricRow.tsx new file mode 100644 index 0000000000000000000000000000000000000000..de5e7d2dc23a86cf5752ce9c9c3d122a1eceb2b5 --- /dev/null +++ b/scripts/app/javascript/components/cohorts/mastery/MetricRow.tsx @@ -0,0 +1,25 @@ +import * as React from 'react' + +type Props = { + id: string + label: string + entries: any[] +} + +export default class MetricRow extends React.Component { + + render() { + let cells = this.props.entries.map((entry: any, i: number) => { + return ( + { entry.value } + ); + }); + + return ( + + { this.props.label } + { cells } + + ); + } +} diff --git a/scripts/app/javascript/components/cohorts/mastery/MetricsBody.tsx b/scripts/app/javascript/components/cohorts/mastery/MetricsBody.tsx new file mode 100644 index 0000000000000000000000000000000000000000..c2dc22f2cedc793a3e8254e34d94bde6fee77ea1 --- /dev/null +++ b/scripts/app/javascript/components/cohorts/mastery/MetricsBody.tsx @@ -0,0 +1,79 @@ +import * as React from 'react' +import {filter} from 'lodash-es' +import MetricRow from './MetricRow' + +type Props = { + students: any[] + performances: any[] +} +type StudentValue = { + student_id: any + value: any +} + +export default class MetricsBody extends React.Component { + + scorePercentage(scores: any[], scores_in_question: any[]) { + if (scores.length == 0) return '0%'; + + return ((filter(scores, function(score) { return scores_in_question.indexOf(score) > -1; }).length / scores.length * 100).toFixed(0) + '%'); + } + + masteryAverage(scores: any[]) { + let sum; + + if (scores.length == 0) return 0.0; + + sum = scores.reduce(function(accumulator, score) { + return accumulator + score; + }, 0); + + return (sum / scores.length).toFixed(1); + } + + render() { + let onesPercentages: StudentValue[] = []; + let twosPercentages: StudentValue[] = []; + let threesPercentages: StudentValue[] = []; + let masteryAverages: StudentValue[] = []; + + this.props.students.map((student) => { + let performances = this.props.performances[student.id] || []; + let scores; + + scores = Object.keys(performances).reduce(function(accumulator: any, standard_id: any) { + let score = performances[parseInt(standard_id)].score; + + accumulator.push(score); + + return accumulator; + }.bind(this), []); + + onesPercentages.push({ + student_id: student.id, + value: this.scorePercentage(scores, [1]) + }); + + twosPercentages.push({ + student_id: student.id, + value: this.scorePercentage(scores, [2]) + }); + + threesPercentages.push({ + student_id: student.id, + value: this.scorePercentage(scores, [3]) + }); + + masteryAverages.push({ student_id: student.id, value: this.masteryAverage(scores) }); + }); + + return ( + + + + + + + ); + } +} diff --git a/scripts/app/javascript/components/cohorts/mastery/PerformanceCell.tsx b/scripts/app/javascript/components/cohorts/mastery/PerformanceCell.tsx new file mode 100644 index 0000000000000000000000000000000000000000..debbfb74f97be66dd4db2646a0392510b3504133 --- /dev/null +++ b/scripts/app/javascript/components/cohorts/mastery/PerformanceCell.tsx @@ -0,0 +1,53 @@ +import * as React from 'react' + +type Props = { + student: any + standard: any + performances: any + selectedCell: any + selectedCellDidChange: any +} + +export default class PerformanceCell extends React.Component { + + shouldComponentUpdate(nextProps: Props) { + let currentlySelected = this.props.standard.id === this.props.selectedCell.standardId && + this.props.student.id === this.props.selectedCell.studentId; + + let willBeSelected = this.props.standard.id === nextProps.selectedCell.standardId && + this.props.student.id === nextProps.selectedCell.studentId; + + return currentlySelected || willBeSelected; + } + + didClickCell = (e: any) => { + let selectedCell = { + studentId: this.props.student.id, + standardId: this.props.standard.id, + releaseId: this.props.standard.release_id + }; + + this.props.selectedCellDidChange(e, selectedCell); + } + + render() { + let student = this.props.student; + let standard = this.props.standard; + let studentPerformances = this.props.performances[student.id] || {}; + let performance = studentPerformances[standard.id] || {}; + let score = performance.score || 0; + let className = 'score score-' + score; + let currentlySelected = this.props.standard.id === this.props.selectedCell.standardId && + this.props.student.id === this.props.selectedCell.studentId; + + if (currentlySelected) { + className += ' selected'; + } + + return ( + + { score } + + ); + } +} diff --git a/scripts/app/javascript/components/cohorts/mastery/PerformanceRow.tsx b/scripts/app/javascript/components/cohorts/mastery/PerformanceRow.tsx new file mode 100644 index 0000000000000000000000000000000000000000..e865f7628fe2a3b0aab4d4392543bcb7dc48e686 --- /dev/null +++ b/scripts/app/javascript/components/cohorts/mastery/PerformanceRow.tsx @@ -0,0 +1,65 @@ +import * as React from 'react' + +type Props = any + +export default class PerformanceRow extends React.Component { + + + standardWasSelected = (e: any) => { + this.props.standardWasSelected(e, this.props.standard.id); + } + + render() { + let displayPerformance; + let performance = this.props.performance ? this.props.performance : { score: 0, editable: true, id: null, updated_at: null }; + const scoreColors: Record = { + 0: 'none', + 1: '#F4C7C3', + 2: '#FCE8B2', + 3: '#B7E1CD' + }; + let tdStyles: any = { + background: scoreColors[performance.score] + }; + let className = `score-${performance.score.toString()}`; + let standardDescription = this.props.standard.description; + + if (this.props.selectedStandardId === this.props.standard.id) { + tdStyles.boxShadow = '0 0 0 2px black inset'; + } + // core and elective == standard_type + if (this.props.standard.success_criteria && this.props.standard.success_criteria.trim()[0] === '<') { + displayPerformance = ( + + +
    +
    +
    +
    + + ); + } else { + displayPerformance = { standardDescription }; + } + + return ( + + { displayPerformance } + + {performance.score} + + + ); + } +} diff --git a/scripts/app/javascript/components/cohorts/mastery/ReleaseBody.tsx b/scripts/app/javascript/components/cohorts/mastery/ReleaseBody.tsx new file mode 100644 index 0000000000000000000000000000000000000000..5f45f633138b08c42bb6ea8d53e93874020e78f5 --- /dev/null +++ b/scripts/app/javascript/components/cohorts/mastery/ReleaseBody.tsx @@ -0,0 +1,75 @@ +import * as React from 'react' +import StandardRow from './StandardRow' + +type Props = { + selectedCell: any + release: any + releaseWasFiltered: any + students: any[] + performances: any + selectedCellDidChange: any +} + +export default class ReleaseBody extends React.Component { + + shouldComponentUpdate(nextProps: Props) { + let iNowContainASelectedCell = this.props.selectedCell.releaseId === this.props.release.id; + let iWillContainASelectedCell = nextProps.selectedCell.releaseId === this.props.release.id; + + return iNowContainASelectedCell || iWillContainASelectedCell; + } + + releaseWasFiltered = (e: any) => { + this.props.releaseWasFiltered(e, this.props.release); + } + + render() { + let studentCells = this.props.students.map((student) => { + return  ; + }); + + var standardRows = this.props.release.standards.map((standard: any) => { + return ( + + + ); + }); + + var releaseLink = ( + {this.props.release.block_title} + + ); + + return ( + + + + { releaseLink } + +   + + + +   + + + { this.props.release.block_title } + + + + + + { studentCells } + + { standardRows } + + ); + } +} diff --git a/scripts/app/javascript/components/cohorts/mastery/StandardRow.tsx b/scripts/app/javascript/components/cohorts/mastery/StandardRow.tsx new file mode 100644 index 0000000000000000000000000000000000000000..6fb969d55c6178826b980d88cf8ec43144e8b6b7 --- /dev/null +++ b/scripts/app/javascript/components/cohorts/mastery/StandardRow.tsx @@ -0,0 +1,62 @@ +import * as React from 'react' +import PerformanceCell from './PerformanceCell' + +type Props = any + +export default class StandardRow extends React.Component { + + displayDescription(standard: any) { + let displayPerformance; + + if (standard.success_criteria && standard.success_criteria.trim()[0] === '<' && standard.success_criteria.indexOf('n/a') < 0) { + displayPerformance = ( + + + +
    +
    +
    +
    + + ); + } else { + displayPerformance = { standard.description }; + } + + return displayPerformance; + } + + render() { + let studentCells = this.props.students.map((student: any) => { + return ( + + + ); + }); + + return ( + + { this.displayDescription(this.props.standard) } + { studentCells } + + ); + } +} diff --git a/scripts/app/javascript/components/cohorts/mastery/StudentHeader.tsx b/scripts/app/javascript/components/cohorts/mastery/StudentHeader.tsx new file mode 100644 index 0000000000000000000000000000000000000000..7cb726e9c1609b97a64df12682de44bbcda3a2f0 --- /dev/null +++ b/scripts/app/javascript/components/cohorts/mastery/StudentHeader.tsx @@ -0,0 +1,30 @@ +import * as React from 'react' + +type Props = { + student: any + selectedCell: any +} + +export default class StudentHeader extends React.Component { + + shouldComponentUpdate(nextProps: Props) { + return this.props.selectedCell.studentId === this.props.student.id || + nextProps.selectedCell.studentId === this.props.student.id; + } + + render() { + let className = this.props.selectedCell.studentId === this.props.student.id ? 'selected' : ''; + + return ( + + + + ); + } +} diff --git a/scripts/app/javascript/components/cohorts/mastery/StudentMasteryTable.tsx b/scripts/app/javascript/components/cohorts/mastery/StudentMasteryTable.tsx new file mode 100644 index 0000000000000000000000000000000000000000..2a5c65ecb42d6b8a8af11ec968004ca7c0202db2 --- /dev/null +++ b/scripts/app/javascript/components/cohorts/mastery/StudentMasteryTable.tsx @@ -0,0 +1,310 @@ +import * as React from 'react' +import {forEach} from 'lodash-es' +import StudentReleaseRow from './StudentReleaseRow' +import http from '../../../lib/http' + +type Props = { + performancesPath: string + updatePerformancesPath: string + approveAllPath: string + userCanTrackPerformances: boolean + studentName: string + spinnerPath: string +} +type State = { + loaded: boolean + releaseFilter: null | any + updateable: boolean + selectedStandardId: null | any + selectedCell: null | any + standards: any[] + releases: any[] + performances: any[] +} + +export default class StudentMasteryTable extends React.Component { + constructor(props: Props) { + super(props); + + this.state = { + loaded: false, + releaseFilter: null, + updateable: false, + selectedStandardId: null, + selectedCell: null, + standards: [], + releases: [], + performances: [], + }; + + this.moveDown = this.moveDown.bind(this); + this.moveUp = this.moveUp.bind(this); + this.standardWasSelected = this.standardWasSelected.bind(this); + this.onSelectAll = this.onSelectAll.bind(this); + this.releaseWasFiltered = this.releaseWasFiltered.bind(this); + this.clearFilters = this.clearFilters.bind(this); + } + + componentDidMount() { + http('GET', this.props.performancesPath) + .then((data: any) => { + + if (data.updateable) { + this.setState({ selectedStandardId: data.standards[0].id }); + } + + this.setState(data); + + if (data.updateable) { + this.wireUpMoveEvents(); + } + + this.setState({ loaded: true }); + }); + } + + wireUpMoveEvents() { + document.addEventListener('keydown', (e: any) => { + if (['0', '1', '2', '3'].indexOf(e.key) > -1) { this.handleScore(e.key); } + if (e.key == 'j' || (e.key == 'n' && e.ctrlKey)) { this.moveDown(); } + if (e.key == 'k' || (e.key == 'p' && e.ctrlKey)) { this.moveUp(); } + }); + } + + moveDown() { + let filteredReleases = this.getFilteredReleases(); + let standardIds = filteredReleases.reduce((result: any, release: any) => { + return result.concat(release.standards.map((o: any) => { + return o.id; + })); + }, []); + let index = standardIds.indexOf(this.state.selectedStandardId); + + if (index < standardIds.length - 1) { + this.setState({ + selectedStandardId: standardIds[index + 1] + }); + } + } + + moveUp() { + let filteredReleases = this.getFilteredReleases(); + let standardIds = filteredReleases.reduce((result, release) => { + return result.concat(release.standards.map((o: any) => { + return o.id; + })); + }, []); + let index = standardIds.indexOf(this.state.selectedStandardId); + + if (index > 0) { + this.setState({ + selectedStandardId: standardIds[index - 1] + }); + } + } + + handleScore(num: number) { + let standardId = this.state.selectedStandardId; + + http('POST', this.props.updatePerformancesPath, { + body: { + performance: { + standard_ids: [standardId], + score: num + } + } + }) + .then((response: any) => { + let performances = this.state.performances!; + + performances[standardId] = response[0]; + this.setState({ performances: performances }); + }); + } + + standardWasSelected(e: any, standardId: any) { + if (this.state.updateable) { + this.setState({ selectedStandardId: standardId }); + } + } + + onSelectAll(e: any) { + e.preventDefault(); + + let performances = this.state.standards.reduce(function(result, standard) { + standard.performances.forEach(function(performance: any) { + result[performance.id] = performance; + }); + + return result; + }, {}); + + http('POST', this.props.approveAllPath, { + body: { + standard_ids: Object.keys(performances), + score: 3 + } + }) + .then((newPerformances: any[]) => { + newPerformances.forEach((newPerformance) => { + performances[newPerformance.id].score = newPerformance.score; + }); + this.setState({ standards: this.state.standards }); + }); + } + + releaseWasFiltered(e: any, release: any) { + e.preventDefault(); + + this.setState({ + releaseFilter: release, + selectedCell: { + standardId: release.standards[0].id + } + }); + } + + clearFilters(e: any) { + e.preventDefault(); + + this.setState({ + releaseFilter: null, + selectedCell: { + standardId: this.state.releases[0].standards[0].id + } + }); + } + + getFilteredReleases() { + if (this.state.releaseFilter) { + return [this.state.releaseFilter]; + } else { + return this.state.releases; + } + } + + averages(performances: any[]) { + let releases = this.getFilteredReleases(); + let total = 0; + let count = 0; + let ones = 0; + let twos = 0; + let threes = 0; + + forEach(releases, function(release) { + forEach(release.standards, function(standard) { + let performance = performances[standard.id]; + + if (performance) { + count++; + + if (performance.score == 1) { ones++; } + if (performance.score == 2) { twos++; } + if (performance.score == 3) { threes++; } + + total += performance.score; + } + }); + }); + + if (count == 0 || total == 0) { + return ({ + ones: 'N/A', + twos: 'N/A', + threes: 'N/A', + mastery: 'N/A' + }); + } + + return ({ + ones: (ones / count * 100).toFixed(0).toString() + '%', + twos: (twos / count * 100).toFixed(0).toString() + '%', + threes: (threes / count * 100).toFixed(0).toString() + '%', + mastery: (total / count).toFixed(1).toString() + }); + } + + render() { + let header; + + if (this.props.userCanTrackPerformances) { + header = ( +
    +

    {`${this.props.studentName} Performances`}

    +
    + ); + } + + if (this.state.loaded) { + let approveAll, clearFiltersLink, table; + let averages = this.averages(this.state.performances); + let filteredReleases = this.getFilteredReleases(); + let releaseRows = filteredReleases.map((release) => { + return + ; + }); + let scoreLabel = 'Average Mastery'; + + if (this.state.releaseFilter) { + clearFiltersLink = Clear Filters; + } + + table = { releaseRows }
    ; + + if (this.props.approveAllPath && this.state.updateable) { + approveAll = ( +

    + Approve All (3s) +

    + ); + } + + return ( +
    + { header } +
    +
    + { averages.ones } + % 1s +
    +
    + { averages.twos } + % 2s +
    +
    + { averages.threes } + % 3s +
    +
    + { averages.mastery } + { scoreLabel } +
    +
    +
    + { approveAll } + { clearFiltersLink } + { table } +
    +
    + ); + } else { + return ( +
    + { header } +
    +
    + loading +
    +
    +
    + ); + } + } +} diff --git a/scripts/app/javascript/components/cohorts/mastery/StudentReleaseRow.tsx b/scripts/app/javascript/components/cohorts/mastery/StudentReleaseRow.tsx new file mode 100644 index 0000000000000000000000000000000000000000..9699cdece1c4fa00dc45b5cba8e2035ab193a9e2 --- /dev/null +++ b/scripts/app/javascript/components/cohorts/mastery/StudentReleaseRow.tsx @@ -0,0 +1,50 @@ +import * as React from 'react' +import PerformanceRow from './PerformanceRow' + +type Props = { + release: any + performances: any[] + releaseWasFiltered: any + selectedStandardId: any + standardWasSelected: any +} + +export default class StudentReleaseRow extends React.Component { + + releaseWasFiltered = (e: any) => { + this.props.releaseWasFiltered(e, this.props.release); + } + + render() { + let performanceRows = this.props.release.standards.map((standard: any) => { + let performance = this.props.performances[standard.id]; + + return ( + + ); + }, this); + + return ( + + + + + + + +   + + { this.props.release.block_title } + + + + { performanceRows } + + ); + } +} diff --git a/scripts/app/javascript/components/cohorts/pairing/GroupPairs.tsx b/scripts/app/javascript/components/cohorts/pairing/GroupPairs.tsx new file mode 100644 index 0000000000000000000000000000000000000000..ea52b6a6c968786c22d947904ef74679e92836a3 --- /dev/null +++ b/scripts/app/javascript/components/cohorts/pairing/GroupPairs.tsx @@ -0,0 +1,243 @@ +import React, { Component } from 'react' +import CopyToClipboard from 'react-copy-to-clipboard'; +import ReactCSSTransitionGroup from 'react-addons-css-transition-group'; +import http from '../../../lib/http' +import { range } from '../../../lib/utils' +import NewPairing from './NewPairing' +import UserAvatar from '../../UserAvatar' +import { cohortPairingsPath, cohortPairingPath } from '../../../generated/routes'; + +type Props = { + save: (p: Api.Pairs.Pairing) => void + delete: (p: Api.Pairs.Pairing) => void + pairings: Api.Pairs.Pairing[] + cohort_id: string + students: Api.Pairs.Student[] +} + +type State = { + pairings: Api.Pairs.Pairing[] + mode: string + viewAllHistory: boolean + editing: Api.Pairs.Pairing | null + copied: { [key: number]: boolean } +} + +export default class GroupPairs extends Component { + state: State = { + mode: 'view', + viewAllHistory: false, + editing: null, + pairings: this.parsePairingGroups(this.props.pairings), + copied: {}, + } + + parsePairingGroups(pairings: Api.Pairs.Pairing[]){ + pairings.map( pairing => { + if (typeof(pairing.groups) === "string") { + pairing.groups = JSON.parse(pairing.groups) + } + }) + return pairings + } + + studentInitials(name: string): string { + if (typeof(name) !== 'string') return '' + let names = name.split(" ") + return names[0][0] + names[1][0] + } + + // TODO - this could be seperated into save and update functions, helps with what state we in + async savePairing(pairing: Api.Pairs.Pairing) { + pairing.groups = JSON.stringify(pairing.groups) + if (this.state.editing) { + let pairingId:number + if (pairing.id === null) { + alert("Cannot save pairing without id.") + return + } else { + pairingId = pairing.id + } + http('PATCH', cohortPairingPath(this.props.cohort_id, pairingId), {body: {"pairing": pairing}}).then((resp: any) => { + http('GET', cohortPairingsPath(this.props.cohort_id)).then((resp: Api.Pairs.Pairing[]) => { + resp.forEach((pairing: Api.Pairs.Pairing) => { + if (typeof(pairing.groups) === 'string') pairing.groups = JSON.parse(pairing.groups) + }) + this.setState({pairings: resp, mode: 'view', editing: null}) + }) + }) + } else { + http('POST', cohortPairingsPath(this.props.cohort_id), {body: {"pairing": pairing}}).then((resp: any) => { + let newPairings = this.state.pairings + resp.groups = JSON.parse(resp.groups) + newPairings.unshift(resp) + this.setState({pairings: newPairings, mode: 'view'}) + }) + } + } + + async deletePairing(pairing: Api.Pairs.Pairing) { + if (!pairing.id) return + + http('DELETE', cohortPairingPath(this.props.cohort_id, pairing.id)) + .then((resp: any) => { + http('GET', cohortPairingsPath(this.props.cohort_id)).then((resp: Api.Pairs.Pairing[]) => { + resp.forEach((pairing: Api.Pairs.Pairing) => { + if (typeof(pairing.groups) === 'string') pairing.groups = JSON.parse(pairing.groups) + }) + this.setState({pairings: resp, mode: 'view', editing: null}) + }) + }) + } + + async edit(pairing: Api.Pairs.Pairing) { + this.setState({ editing: pairing, mode: 'edit' }) + // TODO: Take a stab at fancy scroll to without this below + // setTimeout(() => scrollVerticalToElement(this.editDiv), 50) + } + + goToViewMode() { + this.setState({ mode: 'view', editing: null }) + } + + copyConfirm = (index: number) => { + setTimeout(() => { + this.setState(prevState => ({ + ...prevState, + copied: { + ...prevState.copied, + [index]: false, + }, + })) + }, 750); + } + + generatePairsToCopy(pairing: Api.Pairs.Pairing) { + let allPairs = 'Pairs:\n'; + + typeof(pairing.groups) !== "string" && + pairing.groups.forEach((pair: Api.Pairs.Student[]) => { + const singlePair = pair.map((user: Api.Pairs.Student) => user.name) + allPairs += ` ${singlePair.join(' & ')}\n` + }) + + return allPairs + } + + render() { + const { students } = this.props + const { pairings, mode, viewAllHistory, editing, copied } = this.state + const historyView = viewAllHistory ? pairings : pairings.slice(0,1) + + return ( +
    + {/* Navigations */} +
    +
    { this.goToViewMode() }}> + {`View${viewAllHistory ? ' All' : ''}`} +
    +
    { this.setState({ mode: 'edit' }) }}> + {editing ? 'Editing' : 'Create New'} +
    +
    + {/* Past Pairings */} +
    + {historyView.map((pairing, i:number) => ( +
    +
    + +

    {pairing.title}

    +
    + { + this.setState(prevState => ({ + ...prevState, + copied: { + ...prevState.copied, + [i]: true, + }, + })) + }} + > + + +
    + + {copied[i] ?

    Copied!

    : null} +
    +
    +
    +
    +
    + {typeof(pairing.groups) !== "string" && pairing.groups.map((g:Api.Pairs.Student[], i:number) => ( +
    + {g.map(student => +
    + +
    {student.name}
    +
    + )} + {range(0, pairing.size - g.length, (i: any) => ( +
    + +
    {'n/a'}
    +
    + ))} +
    + ))} +
    +
    + ))} + {/* No Pairings */} + {pairings.length === 0 && ( +
    +

    No pairings to show yet.

    +
    + )} + {/* View More Button */} + {pairings.length >= 2 && ( +
    + +
    + )} + {pairings.length < 2 && ( +
    + )} +
    + {/* When Editing or Creating New */} +
    +

    {editing ? editing.title : 'New Pairing'}

    + this.savePairing(p)} + onDelete={(p: Api.Pairs.Pairing) => this.deletePairing(p)} + onCancel={() => this.setState({ mode: 'view', editing: null })} + /> +
    +
    + ) + } +} diff --git a/scripts/app/javascript/components/cohorts/pairing/InnerStudentList.tsx b/scripts/app/javascript/components/cohorts/pairing/InnerStudentList.tsx new file mode 100644 index 0000000000000000000000000000000000000000..b76b88ab14366cbb14c8d877295c0b973261c190 --- /dev/null +++ b/scripts/app/javascript/components/cohorts/pairing/InnerStudentList.tsx @@ -0,0 +1,48 @@ +import React, { Component } from 'react' +import { Draggable } from 'react-beautiful-dnd' +import StudentItem from './StudentItem' +import { DroppableProvided } from 'react-beautiful-dnd/' + +type Props = { + students: Api.Pairs.Student[] + clashes: any + locks: any + dropProvided: DroppableProvided +} + +export default class InnerStudentList extends Component { + shouldComponentUpdate(nextProps: Props) { + if (nextProps.students.map(s => s.uid).join('') !== this.props.students.map(s => s.uid).join('')) { + return true; + } + + return false; + } + + render() { + const { students, clashes, locks, dropProvided } = this.props + + return ( +
    + {students.map((student, index) => ( + + {(dragProvided, dragSnapshot) => ( +
    + + {dragProvided.placeholder} +
    + )} +
    + ))} +
    + ); + } +} diff --git a/scripts/app/javascript/components/cohorts/pairing/NewPairing.tsx b/scripts/app/javascript/components/cohorts/pairing/NewPairing.tsx new file mode 100644 index 0000000000000000000000000000000000000000..3d4e1c522ee326992036532c9be8b025d61768b4 --- /dev/null +++ b/scripts/app/javascript/components/cohorts/pairing/NewPairing.tsx @@ -0,0 +1,294 @@ +import React, { Component } from 'react' +import { DraggableLocation } from 'react-beautiful-dnd' +import { range, calculateClashes, randomize, pairingSizeString } from '../../../lib/utils' +import PairingBoard from './PairingBoard' + +type Props = { + key: string + pairingToEdit: Api.Pairs.Pairing | null + students: Api.Pairs.Student[] + history: Api.Pairs.Pairing[] + onSave: (p: Api.Pairs.Pairing) => void + onDelete: (p: Api.Pairs.Pairing) => void + onCancel: () => void +} + +type State = { + pairing: any | null + pairingGroups: Api.Pairs.Student[][] | null | string + pairingSize: number // Form attribute, saves to pairing.size + title: string + loading: boolean + clashes: any // Previously paired + locks: any // Will not change positions + pastPairings: any // Calculated for detecting clashes +} + +export default class NewPairing extends Component { + constructor(props: Props) { + super(props) + this.state = { + loading: false, + clashes: {}, + locks: {}, + pairing: props.pairingToEdit ? props.pairingToEdit : null, + pairingSize: props.pairingToEdit ? props.pairingToEdit.size : 0, + title: props.pairingToEdit ? props.pairingToEdit.title : '', + pastPairings: this.calcPastPairings(), + pairingGroups: props.pairingToEdit ? props.pairingToEdit.groups : null + } + } + + componentWillReceiveProps(nextProps: Props) { + this.setState({ + locks: {}, + pairing: nextProps.pairingToEdit, + pairingSize: nextProps.pairingToEdit ? nextProps.pairingToEdit.size : 0, + title: nextProps.pairingToEdit ? nextProps.pairingToEdit.title : '', + pastPairings: this.calcPastPairings(nextProps.history), + pairingGroups: nextProps.pairingToEdit ? nextProps.pairingToEdit.groups : null + }) + } + + calcPastPairings(pastPairings: Api.Pairs.Pairing[] | null = null) { + // Calculate past pairings for each student + // This is an O(n^2) operation, but we only have to do it once-ish. + var pp: any = {} + let history = pastPairings == null ? this.props.history : pastPairings + history.forEach(p => { + if (this.props.pairingToEdit && this.props.pairingToEdit.id === p.id) { + // Irrelevant history item + return + } + if (typeof(p.groups) === "string") return + p.groups.forEach((g: Api.Pairs.Student[]) => { + for (var i = 0; i < g.length; i++) { + var student1 = g[i] + pp[student1.uid] || (pp[student1.uid] = {}) + + for (var k=i+1; k < g.length; k++) { + var student2 = g[k] + pp[student2.uid] || (pp[student2.uid] = {}) + + pp[student1.uid][student2.uid] = true + pp[student2.uid][student1.uid] = true + } + } + + // Special case for size 2 to help avoid multiple solo sprints + if (p.size === 2 && g.length === 1) { + pp[g[0].uid]['solo'] = true + } + }) + }) + + return pp + } + + grabGroupsFromNewOREditPairing() { + let groupsToSwap + if (this.state.pairing !== null) { // Editing + groupsToSwap = this.state.pairing.groups + } else { // New + groupsToSwap = this.state.pairingGroups + } + return groupsToSwap + } + + recalculateClashes() { + this.setState({ + clashes: calculateClashes(this.state.pastPairings, this.state.pairingSize, this.grabGroupsFromNewOREditPairing()) + }) + } + + setpairingSize(pairingSize: number) { + this.setState({ pairingSize }) + setTimeout(() => this.pairUp(true), 10) + } + + pairUp(fresh: boolean | EventTarget) { + const pairingGroups = randomize({ + students: this.props.students, + pairingSize: this.state.pairingSize, + pastPairings: this.state.pastPairings, + currentGroups: fresh ? null : this.grabGroupsFromNewOREditPairing(), + locks: !fresh ? this.state.locks : {}, + }) + + var clashes = calculateClashes(this.state.pastPairings, this.state.pairingSize, pairingGroups) + + if (this.state.pairing) { + let updatedPairing = this.state.pairing + updatedPairing.groups = pairingGroups + this.setState({ pairingGroups, clashes, pairing: updatedPairing, pairingSize: this.state.pairingSize }) + } else { + this.setState({ pairingGroups, clashes, pairingSize: this.state.pairingSize }) + } + } + + async save() { + if (this.state.loading) return + + this.setState({ loading: true }) + + await this.props.onSave({ + id: this.state.pairing ? this.state.pairing.id : null, + groups: this.grabGroupsFromNewOREditPairing(), + size: this.state.pairingSize, + title: this.state.title + }) + + this.setState({ + loading: false, + pairing: null, + pairingSize: 0, + title: '', + pairingGroups: [], + }) + } + + swap = (source: DraggableLocation, dest: DraggableLocation) => { + // TODO - describe this magic + let groupsToSwap = this.grabGroupsFromNewOREditPairing() + + const sourceGroupIndex = Number(source.droppableId.replace('Pairing ', '')) - 1 + const destGroupIndex = Number(dest.droppableId.replace('Pairing ', '')) - 1 + + if ( sourceGroupIndex === destGroupIndex ) { + // Simple swap + const g = groupsToSwap[sourceGroupIndex].slice() + const temp = g[source.index] + + g[source.index] = g[dest.index] + g[dest.index] = temp + + groupsToSwap[sourceGroupIndex] = g + + return this.recalculateClashes() + } + + const from_g = groupsToSwap[sourceGroupIndex].slice() + const to_g = groupsToSwap[destGroupIndex].slice() + + if ( to_g.length === this.state.pairingSize ) { + if ( dest.index === to_g.length ) { + // Spilled over but added to the end of the list. + // Shift out the first student so the mover stays. + to_g.push(from_g[source.index]) + from_g.splice(source.index, 1, to_g.shift()) + } + else { + // Added to middle of list. + // Pop out the item at the end of the list. + to_g.splice(dest.index, 0, from_g[source.index]) + from_g.splice(source.index, 1, to_g.pop()) + } + } + else { + // No overflow; just add + to_g.splice(dest.index, 0, from_g[source.index]) + from_g.splice(source.index, 1) + } + + // New arrays so dnd lib detects an update + groupsToSwap[sourceGroupIndex] = from_g + groupsToSwap[destGroupIndex] = to_g + + if (this.state.pairing !== null) { + let newPairing = this.state.pairing + newPairing.groups = groupsToSwap + this.setState({pairing: newPairing}) + } else { + this.setState({pairingGroups: groupsToSwap}) + } + this.recalculateClashes() + } + + render() { + const { students } = this.props + const { pairingSize, pairing, pairingGroups, clashes, locks, title, loading } = this.state + + return ( +
    +
    + +
    +
    +
    + +
    +
    + this.setState({ title: e.target.value })} + placeholder="Title" + className="form-input" + id="np-title" + /> +
    +
    + + {/* Pair Size */} +
    +
    + +
    +
    + +
    +
    +
    + + {/* List of pairings, the drag/drops */} + {pairingGroups && +
    + + + {pairing && + + } +
    + } +
    + + {pairingGroups && ( + + )} +
    + ) + } +} diff --git a/scripts/app/javascript/components/cohorts/pairing/PairingBoard.tsx b/scripts/app/javascript/components/cohorts/pairing/PairingBoard.tsx new file mode 100644 index 0000000000000000000000000000000000000000..15f1194b7fbf04400e52422ae3aec30b8a0ad675 --- /dev/null +++ b/scripts/app/javascript/components/cohorts/pairing/PairingBoard.tsx @@ -0,0 +1,62 @@ +import React, { Component } from 'react' +import { DragDropContext, DropResult, ResponderProvided } from 'react-beautiful-dnd' +import StudentList from './StudentList' + +type Props = { + onSwap: any + groups: any | null + clashes: any + locks: any +} + +type State = { autoFocusId: number | null } + +export default class PairingBoard extends Component { + state = { autoFocusId: null } + + onDragStart = () => { + this.setState({ autoFocusId: null }) + } + + onDragEnd = (result: DropResult): void => { + const { source, destination } = result + + if (!destination) return // dropped nowhere + + if ( // did not move anywhere - can bail early + source.droppableId === destination.droppableId && ( + source.index === destination.index || + source.index === 0 && destination.index === -1 + ) + ) { return } + + this.props.onSwap(source, destination) // The handles of drag/drop + } + + render() { + const { groups, clashes, locks } = this.props + + return ( +
    + +
    + {groups.map((students:any, i:number) => ( + + ))} +
    +
    +
    + ) + } +} diff --git a/scripts/app/javascript/components/cohorts/pairing/StudentItem.tsx b/scripts/app/javascript/components/cohorts/pairing/StudentItem.tsx new file mode 100644 index 0000000000000000000000000000000000000000..b079b8959068e3284d764a1fab05b887da0cd792 --- /dev/null +++ b/scripts/app/javascript/components/cohorts/pairing/StudentItem.tsx @@ -0,0 +1,77 @@ +import React, { PureComponent } from 'react' +import { DraggableProvided } from 'react-beautiful-dnd' +import UserAvatar from '../../UserAvatar'; +import ReactTooltip from "react-tooltip"; + +type Props = { + index: number + student: Api.Pairs.Student + clashes: any + locks: any + isDragging: boolean + dragProvided: DraggableProvided +} + +export default class StudentItem extends PureComponent { + studentInitials(name: string): string { + if (typeof(name) !== 'string') return '' + let names = name.split(" ") + return names[0][0] + names[1][0] + } + + render() { + const { student, clashes, locks, dragProvided } = this.props + + return ( +
    + +
    + {student.name} +
    +
    + { + // Just a dash of mutation TODO - fix! + locks[student.uid] = !locks[student.uid] + this.forceUpdate() + }}> + + + {clashes[student.uid] && + + + + } +
    + +
    + ); + } +} + +function clashMessage (users: any) { + if (users[0] && users[0] === 'solo') return 'Previously worked Solo' + let names = users.map((u: any) => { + return u.name.split(' ')[0] + }) + return `Previously paired with ${englishJoin(names)}` +} + +function englishJoin (elems: any) { + if (elems.length >= 3) { + return elems.slice(0, elems.length-2).join(', ') + ' and ' + elems[elems.length-1] + } else { + return elems.join(' and ') + } +} diff --git a/scripts/app/javascript/components/cohorts/pairing/StudentList.tsx b/scripts/app/javascript/components/cohorts/pairing/StudentList.tsx new file mode 100644 index 0000000000000000000000000000000000000000..873e2bfb7e41fb61fde4829e12c2316c1123cf67 --- /dev/null +++ b/scripts/app/javascript/components/cohorts/pairing/StudentList.tsx @@ -0,0 +1,51 @@ +import React from 'react' +import InnerStudentList from './InnerStudentList' +import { Droppable } from 'react-beautiful-dnd' + +type Props = { + ignoreContainerClipping?: boolean + index: number + listId: string + listType: string + students: Api.Pairs.Student[] + clashes: any + locks: any +} + +const StudentList = ({ + ignoreContainerClipping, + index, + listId, + listType, + students, + clashes, + locks, +}: Props) => ( +
    + + {(dropProvided, dropSnapshot) => +
    +
    Pair {index+1}
    + + {dropProvided.placeholder} +
    + } +
    +
    +) + +export default StudentList diff --git a/scripts/app/javascript/components/cohorts/progress/MasteryProgressionBar.tsx b/scripts/app/javascript/components/cohorts/progress/MasteryProgressionBar.tsx new file mode 100644 index 0000000000000000000000000000000000000000..f4b1cbf70e817af16225bb6e45e2b68bd4352ebb --- /dev/null +++ b/scripts/app/javascript/components/cohorts/progress/MasteryProgressionBar.tsx @@ -0,0 +1,40 @@ +import * as React from 'react' + +const MasteryProgressionBar = ({ + onePercentage, + twoPercentage, + threePercentage, + keyName, +}:{ + onePercentage: number, + twoPercentage: number, + threePercentage: number, + keyName: number, +}) => ( +
    + {threePercentage > 0 && ( +
    + )} + {twoPercentage > 0 && ( +
    + )} + {onePercentage > 0 && ( +
    + )} +
    +); + +export default MasteryProgressionBar; + diff --git a/scripts/app/javascript/components/cohorts/progress/ProgressThresholdCellHeaders.tsx b/scripts/app/javascript/components/cohorts/progress/ProgressThresholdCellHeaders.tsx new file mode 100644 index 0000000000000000000000000000000000000000..59929650b9079ddecbf1c048aec9096537437468 --- /dev/null +++ b/scripts/app/javascript/components/cohorts/progress/ProgressThresholdCellHeaders.tsx @@ -0,0 +1,89 @@ +import * as React from 'react' +import ScoreCircle from '../../ScoreCircle' +import { THRESHOLDS } from './StudentProgress' +import StudentProgressSortArrow from './StudentProgressSortArrow' + +type Props = { + cohortMode: string + sortByTopic: any + percentCellWidth: any + checkpointAvgSortArrow: any + sortByCheckpointAvg: any + descendingTopicOrder: any + topics: string[], + sortableMouse: any + sortHover: string + sortArrowShown: string + applyBoldFont: any +} + +const ProgressThresholdCellHeaders = ({ + cohortMode, + sortByTopic, + percentCellWidth, + checkpointAvgSortArrow, + sortByCheckpointAvg, + descendingTopicOrder, + topics, + sortableMouse, + sortHover, + sortArrowShown, + applyBoldFont, +}: Props) => ( + <> + {cohortMode == 'Mastery' ? ( +
    +
    + {ScoreCircle(3)} + {`(↑${THRESHOLDS['three']}%)`} +
    +
    + {ScoreCircle(2)} + {`(↑${THRESHOLDS['two']}%)`} +
    +
    + {ScoreCircle(1)} + {`(↑${THRESHOLDS['one']}%)`} +
    +
    + ) : ( + <> +
    +
    { sortableMouse('CheckpointAverage') }} + onMouseLeave={() => { sortableMouse('') }} + > +
    + Checkpoint Avg +
    + {checkpointAvgSortArrow} +
    +
    + {topics.length > 0 && topics.map((topic, i) => ( +
    +
    sortByTopic(topic)} + onMouseEnter={() => { sortableMouse(topic) }} + onMouseLeave={() => { sortableMouse('') }} + > +
    + {topic} +
    + +
    +
    + ))} + + )} + +); + +export default ProgressThresholdCellHeaders; diff --git a/scripts/app/javascript/components/cohorts/progress/StudentProgress.tsx b/scripts/app/javascript/components/cohorts/progress/StudentProgress.tsx new file mode 100644 index 0000000000000000000000000000000000000000..d397d3ed38b618ed0ac5ad1eeddd9146ec40ff54 --- /dev/null +++ b/scripts/app/javascript/components/cohorts/progress/StudentProgress.tsx @@ -0,0 +1,409 @@ +import React, { Component } from 'react' +import ProgressThresholdCellHeaders from './ProgressThresholdCellHeaders' +import StudentProgressRow from './StudentProgressRow' +import StudentProgressSortArrow from './StudentProgressSortArrow' +import ProgressThresholdsKey from '../../shared/ProgressThresholdsKey/ProgressThresholdsKey' +import SettingsCog from '../../shared/Icons/SettingsCog' +import ProgressThresholdsModal from '../../shared/ProgressThresholdsModal/ProgressThresholdsModal' +import http from '../../../lib/http' + +const { ScrollSync, ScrollSyncPane } = require('react-scroll-sync') + +export const THRESHOLDS: Record = { + 'one': 10, + 'two': 10, + 'three': 90, + 'complete': 80, +}; + +type Props = { + students: any[] + cohortMode: Api.CohortMode + progressThresholds: number[] + cohortId: number +} + +type State = { + students: any[], + nameCellWidth: string, + percentCellWidth: string, // percents for mastery mode, checkpoint average for percent mode + progressCellWidth: string, + averageCellWidth: string, + ascendingMasteryOrder: boolean, + descendingAverageOrder: boolean, // mastery performance average + descendingCheckpointAverageOrder: boolean, // checkpoint percentage mode average + descendingNameOrder: boolean, + descendingTopicOrder: boolean, + topicToSort: string + sortArrowShown: string + completionPercentageOrder: boolean, + showModal: boolean + progressThresholds: number[] + sortHover: string +} + +type scoreSet = { + one: number + two: number + three: number +} + +type Student = { + id: number + initials: string + cohort: any + full_name: string + avatar: string + total_scored_standards: number + average: string + performances_url: string + score_percentages: scoreSet + progress_percentages: scoreSet + total_standards: scoreSet + recent_performances: scoreSet +} + +export default class StudentProgress extends Component { + constructor(props: Props) { + super(props); + + let progressWidth; + let avgWidth; + let nameCellWidth; + let percentCellWidth; + + if (this.props.cohortMode === 'Mastery') { + progressWidth = '40%'; + avgWidth = '10%'; + nameCellWidth = '20%'; + percentCellWidth = '30%'; + } else { + progressWidth = '54%'; + avgWidth = '0%'; + nameCellWidth = '30%'; + percentCellWidth = '130px'; + } + + this.state = { + students: this.props.students, + nameCellWidth: nameCellWidth, + percentCellWidth: percentCellWidth, + progressCellWidth: progressWidth, + averageCellWidth: avgWidth, + ascendingMasteryOrder: false, + descendingAverageOrder: false, + descendingCheckpointAverageOrder: false, + descendingNameOrder: false, + completionPercentageOrder: false, + descendingTopicOrder: false, + topicToSort: '', + sortArrowShown: 'CheckpointAverage', + showModal: false, + progressThresholds: this.props.progressThresholds, + sortHover: '' + }; + } + + componentDidMount() { + let initSortCategory; + let initSortCallback; + + if (this.props.cohortMode === 'Mastery') { + initSortCategory = 'Mastery'; + initSortCallback = this.byMastery; + } else { + initSortCategory = 'CheckpointAverage'; + initSortCallback = this.byCheckpointAverage; + } + + if (this.props.students) this.sort(initSortCallback, initSortCategory); + } + + componentDidUpdate() { + const el = document.getElementsByClassName('progress-row')[1]; + if (el) this.forceRedraw(el); + } + + // https://stackoverflow.com/a/24753578/5610404 + forceRedraw = (element: any) => { + const disp = element.style.display; + element.style.display = 'none'; + element.offsetHeight; + element.style.display = disp; + }; + + toggleModal = () => { + this.setState(prevState => ({ + ...prevState, + showModal: !prevState.showModal, + })); + } + + updateProgressThresholds = (progressThresholds: number[]) => { + this.setState({ progressThresholds }); + } + + getRangeStyle(idName: string, width: string, color: string) { + return { + __html: `#${idName}::-webkit-slider-runnable-track {background-image: linear-gradient(90deg, ${color} 0, ${color} ${width}%, #EFEFEF ${width}%, #EFEFEF 100%);`, + }; + } + + byMastery = (a: any, b: any) => { + const ordering = this.state.ascendingMasteryOrder ? [-1, 1] : [1, -1]; + const scores = ['three', 'two', 'one'].map((key) => { + return [a.recent_performances[key], b.recent_performances[key]]; + }); + + for (let i = 0; i < 3; i++) { + if (scores[i][0] < scores[i][1]) return ordering[0]; + if (scores[i][0] > scores[i][1]) return ordering[1]; + } + + return 0; + } + + byAverage = (a: any, b: any) => { + const ordering = this.state.descendingAverageOrder ? [-1, 1] : [1, -1]; + + if (a.average < b.average) return ordering[0]; + if (a.average > b.average) return ordering[1]; + + return 0; + } + + byCheckpointAverage = (a: any, b: any) => { + const ordering = this.state.descendingCheckpointAverageOrder ? [-1, 1] : [1, -1]; + + const a_progress = a.progress_percentages.checkpoint_average === null ? -1 : a.progress_percentages.checkpoint_average + const b_progress = b.progress_percentages.checkpoint_average === null ? -1 : b.progress_percentages.checkpoint_average + if (a_progress < b_progress) return ordering[0]; + if (a_progress > b_progress) return ordering[1]; + + return 0; + } + + byFirstName = (a: any, b: any) => { + const ordering = this.state.descendingNameOrder ? [-1, 1] : [1, -1]; + + if (a.full_name.toLowerCase() < b.full_name.toLowerCase()) return ordering[0]; + if (a.full_name.toLowerCase() > b.full_name.toLowerCase()) return ordering[1]; + + return 0; + } + + byCompletion = (a: any, b: any) => { + const ordering = this.state.completionPercentageOrder ? [-1, 1] : [1, -1]; + + if (a.progress_percentages.percent_completed < b.progress_percentages.percent_completed) return ordering[0]; + if (a.progress_percentages.percent_completed > b.progress_percentages.percent_completed) return ordering[1]; + + return 0; + } + + byTopic = (a: any, b: any) => { + const { topicToSort } = this.state; + const ordering = this.state.descendingTopicOrder ? [-1, 1] : [1, -1]; + const aTopic = a.progress_percentages.topic_averages[topicToSort] + const bTopic = b.progress_percentages.topic_averages[topicToSort] + + if (aTopic && bTopic) { + if (aTopic.average < bTopic.average) return ordering[0]; + if (aTopic.average > bTopic.average) return ordering[1]; + } + + if (aTopic && aTopic.average >= 0) return ordering[1]; + if (bTopic && bTopic.average >= 0) return ordering[0]; + + return 0; + } + + sort = (callback: any, category: string) => { + this.setState({ + descendingNameOrder: category === 'Name' ? !this.state.descendingNameOrder : true, + ascendingMasteryOrder: category === 'Mastery' ? !this.state.ascendingMasteryOrder : true, + descendingAverageOrder: category === 'Average' ? !this.state.descendingAverageOrder : true, + descendingCheckpointAverageOrder: category === 'CheckpointAverage' ? !this.state.descendingCheckpointAverageOrder : true, + completionPercentageOrder: category === 'Completion' ? !this.state.completionPercentageOrder : true, + descendingTopicOrder: category.split('-')[0] === 'Topic' ? !this.state.descendingTopicOrder : true, + topicToSort: category.split('-')[0] === 'Topic' ? category.split('-')[1] : '', + }, () => { + this.setState({ + students: this.state.students.sort(callback), + sortArrowShown: category.split('-')[0] === 'Topic' ? category.split('-')[1] : category, + }) + }); + } + + startCSVexport() { + http('POST', `${window.location.pathname}_csv.json`) + .then(() => { alert("We are generating the CSV and will email it to you when it's complete.") }); + } + + sortableMouse = (columnName: string): void => { + this.setState({ sortHover: columnName }); + } + + applyBoldFont = (columnName: string): string => { + if (columnName !== this.state.sortArrowShown && columnName !== this.state.sortHover) return ''; + return 'bold-font'; + } + + render() { + if (this.props.students == null) { + return ( +
    +
    +
    { this.startCSVexport() }} className="lp-style-button"> + Export CSV +
    +
    +
    + Data not shown for cohorts with more than 100 students enrolled. Download CSV to see this data. +
    +
    + ) + } + + let cohortProgressLabel; + let avgCell; + let progressSortCallback = this.byMastery; + let progressSortCategory = 'Mastery'; + let topics: any[] = [] + let topicsArr: any[] = [] + + if (this.props.cohortMode == 'Mastery') { + cohortProgressLabel = 'Standard Mastery'; + avgCell = ( +
    +
    { this.sort(this.byAverage, 'Average'); }}> + Avg + +
    +
    + ); + } else { + cohortProgressLabel = 'Progress'; + progressSortCallback = this.byCompletion; + progressSortCategory = 'Completion'; + topicsArr = this.props.students.flatMap((student: any) => Object.keys(student.progress_percentages.topic_averages)); + topics = [...Array.from(new Set(topicsArr))].sort(); // Typescript doesn't support `[...new Set(topicsArr)]`, must use Array.from + } + + return ( +
    + {this.props.cohortMode === 'Percentage' && ( + + )} +
    + {this.props.cohortMode === 'Percentage' && ( +
    + +
    + +
    +
    + )} +
    { this.startCSVexport()}} className="lp-style-button"> + Export CSV +
    +
    + + <> +
    + +
    +
    +
    { this.sort(this.byFirstName, 'Name'); }} + onMouseEnter={() => { this.sortableMouse('Name') }} + onMouseLeave={() => { this.sortableMouse('') }} + > + Name + +
    +
    + this.sort(this.byCheckpointAverage, 'CheckpointAverage')} + sortByTopic={(topic: string) => this.sort(this.byTopic, `Topic-${topic}`)} + checkpointAvgSortArrow={( + + )} + percentCellWidth={this.state.percentCellWidth} + sortArrowShown={this.state.sortArrowShown} + sortHover={this.state.sortHover} + descendingTopicOrder={this.state.descendingTopicOrder} + sortableMouse={this.sortableMouse} + applyBoldFont={this.applyBoldFont} + /> +
    +
    { this.sort(progressSortCallback, progressSortCategory); }} + onMouseEnter={() => { this.sortableMouse(progressSortCategory) }} + onMouseLeave={() => { this.sortableMouse('') }} + > + {cohortProgressLabel} + +
    +
    + {avgCell} +
    +
    +
    +
    + {this.state.students.map((student: Student) => ( + + + + ))} +
    + +
    +
    + ); + } +} diff --git a/scripts/app/javascript/components/cohorts/progress/StudentProgressBar.tsx b/scripts/app/javascript/components/cohorts/progress/StudentProgressBar.tsx new file mode 100644 index 0000000000000000000000000000000000000000..3566f0342cd0fde60f838db4af3a34608d7b4cdd --- /dev/null +++ b/scripts/app/javascript/components/cohorts/progress/StudentProgressBar.tsx @@ -0,0 +1,40 @@ +import * as React from 'react' +import MasteryProgressionBar from './MasteryProgressionBar' + +const StudentProgressBar = ({ + cohortMode, + student, + progressCellWidth, +}:{ + cohortMode: string + student: any + progressCellWidth: any +}) => ( + <> + {cohortMode == 'Mastery' ? ( +
    + +
    + {`${student.total_scored_standards} Standards Scored`} +
    +
    + ) : ( +
    + {`${student.progress_percentages.percent_completed}%`} +
    +
    +
    +
    + )} + +); + +export default StudentProgressBar; diff --git a/scripts/app/javascript/components/cohorts/progress/StudentProgressCells.tsx b/scripts/app/javascript/components/cohorts/progress/StudentProgressCells.tsx new file mode 100644 index 0000000000000000000000000000000000000000..03203d78277eee7574c06bf0f9ca056994093172 --- /dev/null +++ b/scripts/app/javascript/components/cohorts/progress/StudentProgressCells.tsx @@ -0,0 +1,75 @@ +import * as React from 'react' +import { THRESHOLDS } from './StudentProgress' +import { findThresholdColor } from '../../shared/ProgressThresholdsSlider/ProgressThresholdsSlider' + +const StudentProgressCells = ({ + cohortMode, + student, + percentCellWidth, + progressThresholds, + topics, +}:{ + cohortMode: string + student: any + percentCellWidth: any + progressThresholds: number[] + topics: string[] +}) => { + if (cohortMode === 'Mastery') { + return ( +
    +
    + {`${student.score_percentages.three}%`} +
    +
    + {`${student.score_percentages.two}%`} +
    +
    + {`${student.score_percentages.one}%`} +
    +
    + ) + } + + const checkpointAverage = student.progress_percentages.checkpoint_average; + + return ( + <> +
    +
    + {checkpointAverage !== null ? `${checkpointAverage}%` : '—'} +
    +
    + {topics.length > 0 && topics.map((topic, i) => { + const topicAverages = student.progress_percentages.topic_averages[topic]; + return ( +
    +
    + {topicAverages ? `${topicAverages.average}%` : '—'} +
    +
    + ) + })} + + ) +} + +const filledClass = (thresholdCategory: string, currentScore: number) => { + return currentScore >= THRESHOLDS[thresholdCategory] ? '-is-filled' : ''; +} + +export default StudentProgressCells; \ No newline at end of file diff --git a/scripts/app/javascript/components/cohorts/progress/StudentProgressRow.tsx b/scripts/app/javascript/components/cohorts/progress/StudentProgressRow.tsx new file mode 100644 index 0000000000000000000000000000000000000000..bd4ec8746c7ceea4f3c9d5c4cc366f30be1f486e --- /dev/null +++ b/scripts/app/javascript/components/cohorts/progress/StudentProgressRow.tsx @@ -0,0 +1,64 @@ +import * as React from 'react' +import UserAvatar from '../../UserAvatar' +import StudentProgressCells from './StudentProgressCells' +import StudentProgressBar from './StudentProgressBar' + +const StudentProgressRow = ({ + cohortMode, + student, + percentCellWidth, + progressCellWidth, + nameCellWidth, + averageCellWidth, + progressThresholds, + topics, +}:{ + cohortMode: string + student: any + percentCellWidth: any + progressCellWidth: any + nameCellWidth: any + averageCellWidth: any + progressThresholds: number[] + topics: string[] +}) => { + // react-scroll-sync needs a little scroll nudge to stay in sync every + // time this component rerenders (usually from a sort) + const el = document.getElementsByClassName('progress-row')[0]; + if (el) el.dispatchEvent(new CustomEvent('scroll')); + + return ( +
    + + + + {cohortMode === 'Mastery' && ( +
    +
    + {student.average} +
    +
    + )} +
    + ); +} +export default StudentProgressRow; diff --git a/scripts/app/javascript/components/cohorts/progress/StudentProgressSortArrow.tsx b/scripts/app/javascript/components/cohorts/progress/StudentProgressSortArrow.tsx new file mode 100644 index 0000000000000000000000000000000000000000..0268d9cfb9cba6728357413dcf19f300d6b02627 --- /dev/null +++ b/scripts/app/javascript/components/cohorts/progress/StudentProgressSortArrow.tsx @@ -0,0 +1,22 @@ +import * as React from 'react' +import KeyboardArrowDown from '../../shared/Icons/KeyboardArrowDown' +import KeyboardArrowUp from '../../shared/Icons/KeyboardArrowUp' + +type Props = { + bool: boolean + columnName: string + sortArrowShown: string + sortHover: string +} + +const StudentProgressSortArrow = ({ bool, columnName, sortArrowShown, sortHover }: Props) => { + if (columnName !== sortArrowShown && columnName !== sortHover) return null; + + return ( +
    + {bool ? : } +
    + ) +} + +export default StudentProgressSortArrow; \ No newline at end of file diff --git a/scripts/app/javascript/components/cohorts/settings/BranchReleaseModal.tsx b/scripts/app/javascript/components/cohorts/settings/BranchReleaseModal.tsx new file mode 100644 index 0000000000000000000000000000000000000000..d78df467691ff322726dd980d24f1f4fe537c735 --- /dev/null +++ b/scripts/app/javascript/components/cohorts/settings/BranchReleaseModal.tsx @@ -0,0 +1,73 @@ +import * as React from 'react' +import * as Routes from '../../../generated/routes' +import SvgRenderer from '../../SvgRenderer' +import http from '../../../lib/http' + +type Props = any +type State = any + +export default class BranchReleaseModal extends React.Component { + constructor(props: Props) { + super(props); + + this.state = { + branchName: '' + }; + + this.handleChange = this.handleChange.bind(this); + this.updateBranchHandler = this.updateBranchHandler.bind(this); + } + + componentDidMount() { + $_('#feature-branch-name', el => el.focus()) + } + + handleChange(e: any) { + let newValue = e.target.value; + + this.setState({ + branchName: newValue + }); + } + + updateBranchHandler() { + http('PATCH', Routes.switchToBranchCohortCohortReleasePath(this.props.cohortId, this.props.currentCohortReleaseId), { + body: { branch_name: this.state.branchName, block_id: this.props.block.id } + }) + .then((data: any) => { + this.props.handleBranchUpdate(data.cohort_release, data.flash_info); + }); + } + + render() { + return ( +
    +
    +

    + Switch Branch +

    +
    + +
    +
    +
    +
    + + {this.props.block.name} +
    +
    + + +
    +
    +
    + +
    +
    + ); + } +} diff --git a/scripts/app/javascript/components/cohorts/settings/CohortBlockReleaseRow.tsx b/scripts/app/javascript/components/cohorts/settings/CohortBlockReleaseRow.tsx new file mode 100644 index 0000000000000000000000000000000000000000..fe0693dba800255407770514a996eb0b9ae92511 --- /dev/null +++ b/scripts/app/javascript/components/cohorts/settings/CohortBlockReleaseRow.tsx @@ -0,0 +1,374 @@ +import * as React from 'react' +import http from '../../../lib/http' +import SvgRenderer from '../../SvgRenderer' +import RowKebab from '../../RowKebab' +import ProcessingIcon from '../../ProcessingIcon' +import Timestamp from '../../Timestamp' +import ActionMenu from '../../shared/ActionMenu/ActionMenu' + +type Props = any +type State = any + +export default class CohortBlockReleaseRow extends React.Component { + constructor(props: Props) { + super(props); + + this.state = { + cohortRelease: props.cohortRelease, + showActionMenu: false, + showTooltip: false + }; + + this.handleAutoUpdateChange = this.handleAutoUpdateChange.bind(this); + this.handleMenuOutsideClick = this.handleMenuOutsideClick.bind(this); + this.handleMenuOutsideErrorClick = this.handleMenuOutsideErrorClick.bind(this); + this.handleResyncBranchRelease = this.handleResyncBranchRelease.bind(this); + this.handleVersionSelect = this.handleVersionSelect.bind(this); + this.handleFeatureBranch = this.handleFeatureBranch.bind(this); + this.switchToMaster = this.switchToMaster.bind(this); + this.toggleErrorTooltip = this.toggleErrorTooltip.bind(this); + this.toggleActionMenu = this.toggleActionMenu.bind(this); + } + + componentWillReceiveProps(nextProps: Props) { + if (nextProps.cohortRelease !== this.state.cohortRelease) { + this.setState({ cohortRelease: nextProps.cohortRelease }); + } + } + + handleFeatureBranch() { + this.setState({ showActionMenu: false }); + this.props.featureBranchHandler( + this.props.cohortRelease.id, + this.props.cohortRelease.block_id, + this.props.cohortRelease.block_name, + this.props.cohortRelease.block_url + ); + + if (localStorage.getItem('viewed_cohort_setup_tour') === null) { + let content = '
  • There are no webhooks with Github! Click the "recycle" icon on this page to rebuild after every push to Github.
  • '; + content += '
  • Student submissions are retained when switching to/from a branch.
  • '; + + let tour = { + id: 'hello-hopscotch', + i18n: { + stepNums:[""] + }, + steps: [{ + target: '.branch-modal', + placement: 'bottom', + xOffset: 'center', + title: 'Switching to a branch', + content: content + }] + }; + + // so dom element exists before init + setTimeout(() => { window.hopscotch.startTour(tour); }, 500); + } + } + + get actionMenu() { + if (this.state.showActionMenu) { + this.addClickListeners(this.handleMenuOutsideClick); + + let versionPrompt = 'Switch version'; + let featureBranchText = 'Switch to feature branch'; + let featureBranchMethod = this.handleFeatureBranch; + let versionBranchMethod = this.handleVersionSelect; + + if (this.state.cohortRelease.branch_name !== 'master') { + versionPrompt = 'Switch back to master'; + featureBranchText = 'Update from branch'; + featureBranchMethod = this.handleResyncBranchRelease; + versionBranchMethod = this.switchToMaster; + } + + return ( + + ) + } else { + this.removeClickListeners(this.handleMenuOutsideClick); + } + } + + handleMenuOutsideClick(e: any) { + if ($_('#action-menu-list', el => el.contains(e.target))) { + return; + } + + this.toggleActionMenu(); + } + + addClickListeners(handler: any) { + document.addEventListener('touchmove', handler, false); + document.addEventListener('click', handler, false); + } + + removeClickListeners(handler: any) { + document.removeEventListener('touchmove', handler, false); + document.removeEventListener('click', handler, false); + } + + get availableUpdates() { + let updateCount = this.state.cohortRelease.available_update_count; + + if (updateCount > 0) { + if (this.state.cohortRelease.branch_name === 'master') { + return ( + + {`${updateCount} ${window.pluralize('update', updateCount)}`} + + ); + } else { + return ( + + {`updates`} + + ); + } + } + } + + handleAutoUpdateChange(e: any) { + var use_latest_release = this.state.cohortRelease.available_update_count > 0 && e.target.value === 'true' + if ( + use_latest_release && + ! confirm('Selecting Auto Updates will also move this block to the latest version.') + ) { + // Only update the cohort release if they have accepted + return; + } + + http('PATCH', this.props.cohortRelease.url, { + body: { cohort_release: { use_latest_release: e.target.value === 'true' } }, + }) + .then((_data: any) => { + this.props.reloadCohortReleases(); + }); + } + + switchToMaster() { + this.setState({ showActionMenu: false }); + http('PATCH', `${this.props.cohortRelease.url}.json`, { + body: { cohort_release: { use_latest_release: true } }, + }) + .then((_data: any) => { + this.props.reloadCohortReleases(); + }); + } + + handleResyncBranchRelease() { + if (this.state.showActionMenu) { + this.setState({ showActionMenu: !this.state.showActionMenu }); + } + if (this.state.cohortRelease.pending_release_branch_name && this.state.cohortRelease.branch_name !== this.state.cohortRelease.pending_release_branch_name) { + // in this case we are moving off of master branch but haven't successfully switched to the branch release + // i.e. clicked on the resync icon next to the errors while the cohort release is still on a master release + this.props.resyncBranchRelease(this.state.cohortRelease.block_id, this.state.cohortRelease.pending_release_branch_name, this.state.cohortRelease.id); + } else { + this.props.resyncBranchRelease(this.state.cohortRelease.block_id, this.state.cohortRelease.branch_name, this.state.cohortRelease.id); + } + } + + handleVersionSelect() { + this.setState({ showActionMenu: false }); + this.props.selectVersionHandler( + this.props.cohortRelease.id, + this.props.cohortRelease.block_id, + this.props.cohortRelease.block_name + ); + } + + toggleActionMenu() { + if (this.state.cohortRelease.state == 'processing' || this.state.cohortRelease.state == 'pending') { return null; } + + this.setState({ showActionMenu: !this.state.showActionMenu }); + } + + buildAutoUpdateSelect() { + let cohortRelease = this.state.cohortRelease; + + if (cohortRelease.branch_name === 'master') { + return ( + + ); + } else { + return this.BuildSyncBlock; + } + } + + get BuildSyncBlock() { + let cohortRelease = this.state.cohortRelease; + + switch (cohortRelease.state) { + case 'success': + if (cohortRelease.pending_release_state === 'processing' || cohortRelease.pending_release_state === 'pending') { + return null; + } else { + return
    ; + } + break; + case 'failed': + return ( + + ); + break; + default: + return null; + } + } + + toggleErrorTooltip() { + this.setState({ showTooltip: !this.state.showTooltip }); + } + + get ErrorTooltip() { + if (this.state.showTooltip) { + let errors = this.state.cohortRelease.sync_errors.map((error: any, i: number) => { + return ( +
  • {error}
  • + ); + }); + + this.addClickListeners(this.handleMenuOutsideErrorClick); + + return ( +
    +
      {errors}
    +
    + ); + } else { + this.removeClickListeners(this.handleMenuOutsideErrorClick); + } + } + + handleMenuOutsideErrorClick(e: any) { + if ($_('#errors-tooltip', el => el.contains(e.target))) { + return; + } + + this.toggleErrorTooltip(); + } + + buildVersionLabel() { + let cohortRelease = this.state.cohortRelease; + + if (cohortRelease.sync_errors && cohortRelease.sync_errors.length > 0) { + + let versionLabel; + + if (cohortRelease.branch_name === 'master') { + versionLabel = ( + + {cohortRelease.version} + + ); + } + let errorPrompt; + + if (cohortRelease.sync_errors.length - 1 === 1) { + errorPrompt = `1 error on ${cohortRelease.pending_release_branch_name}`; + } else { + errorPrompt = `${cohortRelease.sync_errors.length - 1} errors on ${cohortRelease.pending_release_branch_name}`; + } + + return ( +
    + {versionLabel} + + + + + {errorPrompt} + {this.ErrorTooltip} + +
    + ); + } else if (cohortRelease.branch_name === 'master') { + return ( + + ); + } else { + let arrowSvg; + + if (cohortRelease.state === 'pending' || cohortRelease.state === 'processing') { + arrowSvg = ( + + Master + + + ); + } + + return ( + + ); + } + } + + get BranchSvg() { + return ( + + ); + } + + render() { + let cohortRelease = this.state.cohortRelease; + let processingIcon; + + if (cohortRelease.pending_release_state === 'processing' || cohortRelease.pending_release_state === 'pending') { + processingIcon = ; + } + + return ( + + + {processingIcon} + {cohortRelease.block_name} + {this.availableUpdates} + + + {this.props.cohortRelease.block_url.includes('github') ? + + + : + + + + } + + {cohortRelease.unit_count} + + {this.buildAutoUpdateSelect()} + + + {this.buildVersionLabel()} + + + + + {this.actionMenu} + + + ); + } +} diff --git a/scripts/app/javascript/components/cohorts/settings/CohortContentTab.tsx b/scripts/app/javascript/components/cohorts/settings/CohortContentTab.tsx new file mode 100644 index 0000000000000000000000000000000000000000..2f17a284ffa687d02d2fcfe5fdba529085e7183f --- /dev/null +++ b/scripts/app/javascript/components/cohorts/settings/CohortContentTab.tsx @@ -0,0 +1,204 @@ +import * as React from 'react' +import * as Routes from '../../../generated/routes' +import CohortVisibilitySection from './CohortVisibilitySection' +import Icon from '../../Icon' + +type Props = { + cohortId: number + sections: Api.Setup.VisibilitySection[] + userCount: number + eyeIconPath: string + releaseCount: number + githubIcon: string + sandboxCohort: boolean +} + +type State = { + visibleStandardsCount: number + contentFilePreview: { + contentFileId: number + html: string + path: string + title: string + githubUrl: string + visible: boolean + } +} + +export default class CohortContentTab extends React.Component { + constructor(props: Props) { + super(props); + + this.state = { + visibleStandardsCount: this.standardCount(), + contentFilePreview: { + contentFileId: 0, + html: "", + path: "", + title: "", + githubUrl: "", + visible: true + } + } + } + + standardCount = (): number => { + let ret = 0; + this.props.sections.forEach((section) => { + if (section.title == null) return + section.standards_with_lessons.forEach((standard) => { + if (standard.hidden) return + + const visibleContentFile = standard.content_files.find((file) => !file.hidden) + + if (visibleContentFile) ret++ + }) + }) + return ret + } + + standardCountChange = (num: number) => { + this.setState({visibleStandardsCount: this.state.visibleStandardsCount + num}) + } + + contentFileToggle = (contentFileId: number, visible: boolean) => { + if (this.state.contentFilePreview.contentFileId === contentFileId) { + this.setState(prevState => ( + { + ...prevState, + contentFilePreview: { + ...prevState.contentFilePreview, + visible: visible + } + } + ) + ) + } + } + + curriculum = () => { + if (this.props.releaseCount > 0) { + return this.props.sections.map(section => { + if (section.title != null) { + return ( + + ); + } + }); + } else { + return ( +

    + It looks like you don't have any curriculum yet. To fix this you can add some Curriculum Block Repos. +

    + ) + } + } + + handleContentFileHtml = (content_file: { contentFileId: number + html: string + path: string + githubUrl: string + title: string + visible: boolean }) => { + if (content_file.html == "") return + + this.setState({ contentFilePreview: content_file }) + + var linkElems = document.querySelectorAll('main .external-link') + for (let i=0; i < linkElems.length; i++) { + linkElems[i].insertAdjacentHTML('beforeend', ` + + + + `) + } + + $_('pre:not([lang])', el => el.classList.add('text')) + + var codeElems = document.querySelectorAll("div[id^='challenge']") + for (let i=0; i < codeElems.length; i++) { + window.hljs.highlightBlock(codeElems[i]) + } + + $_(".lesson-preview", el => el.classList.add('has-content')) + + var challengeDivs = document.querySelectorAll("div[id^='challenge']") + for (let i=0; i < challengeDivs.length; i++) { + let div = challengeDivs[i] + div.innerHTML = `
    ${div.getAttribute('id')}
    ` + } + } + + render() { + var previewHtml = (

    {(this.props.releaseCount > 0) && 'Select a lesson to preview'}

    ) + + var contentFile = this.state.contentFilePreview + + return ( +
    +
    +
    +
    +
    +

    Content ({this.state.visibleStandardsCount})

    +
    + { !this.props.sandboxCohort && + <> + + + { this.props.userCount < 500 && +
    +

    Partnerup

    +
    + } + + } +
    +
    +
    + { this.curriculum() } +
    +
    + { contentFile.html === "" && + previewHtml + } + { contentFile.html !== "" && +
    +
    +
    + { contentFile.title } {contentFile.visible ? "(visible)" : "(hidden)"} +
    + +
    +
    +
    +
    +
    +
    +
    + } +
    +
    +
    + ); + } +} diff --git a/scripts/app/javascript/components/cohorts/settings/CohortInfo.tsx b/scripts/app/javascript/components/cohorts/settings/CohortInfo.tsx new file mode 100644 index 0000000000000000000000000000000000000000..03dcabf573fd2f272658d3ef77aa2e803f0b42e6 --- /dev/null +++ b/scripts/app/javascript/components/cohorts/settings/CohortInfo.tsx @@ -0,0 +1,39 @@ +import * as React from 'react' +import SvgRenderer from '../../SvgRenderer'; + +type Props = { + cohort: Api.Cohort + cohortStartDateFormatted: string + editUrl: string + targetBlankIconSrc: string + sandboxCohort: boolean | false +} + +export default class CohortInfo extends React.Component { + render() { + const { cohort, cohortStartDateFormatted, editUrl, sandboxCohort, targetBlankIconSrc } = this.props + return ( +
    +
    +
    Title
    +

    {cohort.name}

    +
    +
    +
    +
    Campus
    +

    {cohort.campus_name}

    +
    +
    +
    Start Date
    +

    {cohortStartDateFormatted}

    +
    + { editUrl && !sandboxCohort && + + Edit + + } +
    +
    + ); + } +} diff --git a/scripts/app/javascript/components/cohorts/settings/CohortSettingsResync.tsx b/scripts/app/javascript/components/cohorts/settings/CohortSettingsResync.tsx new file mode 100644 index 0000000000000000000000000000000000000000..1d37c3539296f9f8bb4efea04a4af98011f7e3ad --- /dev/null +++ b/scripts/app/javascript/components/cohorts/settings/CohortSettingsResync.tsx @@ -0,0 +1,327 @@ +import * as React from 'react' +import * as Routes from '../../../generated/routes' +import http from '../../../lib/http' +import ReactTooltip from "react-tooltip"; + +type Job = Api.Setup.ResyncJob +type SuccessfulJob = Api.Setup.SuccessfulResyncJob +type CanceledResyncJob = Api.Setup.CanceledResyncJob +type FailedJob = Api.Setup.FailureResyncJob +type TimeoutResyncJob = Api.Setup.TimeoutResyncJob + +type Props = { + cohortId: number + resyncJob: null | Job + successfulResyncJob: null | SuccessfulJob + checkmarkPath: string + githubIconPath: string + setFlash: (flash: null | Api.Setup.Flash) => void + reloadCohortReleases: () => void +} +type State = { + fileUrl: string + working: false | 'submitting' | 'archiving' | 'canceling' + originalJobId: null | number + pollingTimeout: null | number + current: CurrentJob + buttonText: string +} + +type CurrentJob + = { state: 'none-ever' } + | { state: 'success', job: SuccessfulJob } + | { state: 'failure', job: FailedJob } + | { state: 'timeout', job: TimeoutResyncJob } + | { state: 'canceled', job: CanceledResyncJob } + | { state: 'pending', job: Api.Setup.PendingResyncJob } + | { state: 'archived-failure-past-success', job: FailedJob, pastJob: SuccessfulJob } + | { state: 'archived-failure-no-success', job: FailedJob } + +export default class CohortSettingsResync extends React.Component { + constructor(props: Props) { + super(props) + + var job = this.props.resyncJob + var history = this.computeCurrentJobState(job) + + this.state = { + fileUrl: this.computeFileUrl(job, history), + working: false, + originalJobId: job && job.id, + pollingTimeout: null, + current: history, + buttonText: history.state == "pending" ? 'CANCEL': 'RESYNC', + } + this.refresh() + + window.onbeforeunload = () => { + if (this.state.current.state === 'failure' && ! this.state.working) { + this.archiveError(); + } + } + } + + computeCurrentJobState(job: null | Job): CurrentJob { + if ( ! job ) { + return { state: 'none-ever' } + } + else if (job.status === 'success') { + return { state: 'success', job: job } + } + else if (job.status === 'pending') { + return { state: 'pending', job: job } + } + else if (job.status === 'canceled') { + return { state: 'canceled', job: job } + } + else if (job.status === 'timeout') { + return { state: 'timeout', job: job } + } + else if (! job.archived) { + return { state: 'failure', job: job } + } + else if (this.props.successfulResyncJob) { + return { + state: 'archived-failure-past-success', + job: job, + pastJob: this.props.successfulResyncJob + } + } + else { + return { state: 'archived-failure-no-success', job: job } + } + } + + computeFileUrl(job: null | Job, current: CurrentJob) { + var fileUrl = job && job.course_config_url || '' + if (current.state === 'archived-failure-past-success') { + fileUrl = current.pastJob.course_config_url + } + return fileUrl + } + + componentDidUpdate() { + ReactTooltip.rebuild(); + } + + submit = async (e: any) => { + e.preventDefault() + if (this.state.working || this.state.current.state == 'pending') return + + this.setState({ working: 'submitting', buttonText: 'CANCEL' }) + await http('POST', Routes.resyncCohortCohortReleasesPath(this.props.cohortId), { + body: { course_config_url: this.state.fileUrl } + }) + this.poll() + } + + cancel = async (e: any) => { + e.preventDefault() + if (this.state.pollingTimeout) { + window.clearTimeout(this.state.pollingTimeout) + } + + this.setState({ working: 'canceling', buttonText: 'RESYNC' }) + var path = Routes.resyncArchiveCohortCohortReleasesPath(this.props.cohortId) + var job: null | Job = await http('POST', path, {body: { cancel: true }}) + + var current = this.computeCurrentJobState(job) + this.setState({ + working: false, + current: current, + fileUrl: this.computeFileUrl(job, current) + }) + this.props.setFlash(null) + } + + refresh = async () => { + var path = Routes.resyncStatusCohortCohortReleasesPath(this.props.cohortId) + var job: null | Job = await http('GET', path) + var current = this.computeCurrentJobState(job) + + this.setState({ + working: false, // Only set to false at this point to avoid ui jank + current: current, + fileUrl: this.computeFileUrl(job, current) + }) + if (current.state === 'canceled') { return } + if (current.state === 'pending') { + this.poll() + } + if (current.state === 'failure' || current.state === 'timeout') { + if (current.state === 'timeout') this.setState({ buttonText: 'RESYNC' }) + // Do stuff with error data to format nicely + let flash = flashFromJobError(current.job) + if (flash) { + this.props.setFlash(flash) + } + } + else if (current.state === 'success' && current.job.id !== this.state.originalJobId) { + this.setState({ buttonText: 'RESYNC'}) + this.props.reloadCohortReleases() + } + } + + archiveError = async () => { + this.setState({ working: 'archiving' }) + var path = Routes.resyncArchiveCohortCohortReleasesPath(this.props.cohortId) + var job: null | Job = await http('POST', path) + + var current = this.computeCurrentJobState(job) + this.setState({ + working: false, + current: current, + fileUrl: this.computeFileUrl(job, current) + }) + this.props.setFlash(null) + } + + poll() { + if (this.state.pollingTimeout) { + window.clearTimeout(this.state.pollingTimeout) + } + + var pollingTimeout = window.setTimeout(this.refresh, 2000) + this.setState({ pollingTimeout }) + } + + jobStatusLabel(): string { + if (this.state.working === 'submitting') { + return 'Syncing...' + } + if (this.state.working === 'archiving') { + return 'Archiving...' + } + + switch (this.state.current.state) { + case 'none-ever': + return 'Never synced.' + + case 'pending': + return 'Syncing...' + + case 'timeout': + return 'Timeout' + + case 'failure': + case 'archived-failure-no-success': + return 'Failed' + + case 'archived-failure-past-success': + case 'success': + return '' + } + return '' + } + + render() { + var {fileUrl, working, current, buttonText} = this.state + var {githubIconPath} = this.props + + var urlIsValid = fileUrl.match(new RegExp('^https://')) + + var disable = !urlIsValid || working || current.state === 'pending' + var jobStatusLabel = this.jobStatusLabel() + + return
    + +
    + this.setState({ fileUrl: e.target.value })} + className="form-control" + placeholder="Enter your course.yaml GitHub url here." + /> +
    + + {jobStatusLabel && {jobStatusLabel}} + {urlIsValid && + + + + } + +
    + + + +
    + {this.props.successfulResyncJob && + + } +
    + } +} + +function flashFromJobError(job: FailedJob | TimeoutResyncJob) : Api.Setup.Flash | undefined { + switch(job.error_type) { + case 'top_course_http_fetch_error': + return { title: job.error_data.repo, msg: 'Yaml file not found at given url
    '+ job.error_data['message'], type: 'danger' } + case 'section_course_http_fetch_error': + return { title: job.error_data.repo, msg: 'Repository not found
    '+ job.error_data['message'], type: 'danger' } + case 'invalid_url': + return { title: job.course_config_url, msg: 'URL is not valid
    '+ job.error_data['message'], type: 'danger' } + case 'json_parse_error': + return { title: job.course_config_url, msg: 'JSON could not be parsed
    '+ job.error_data['message'], type: 'danger' } + case 'yaml_validation_error': + return { title: job.course_config_url, msg: 'YAML could not be parsed or was invalid
    '+ job.error_data['message'], type: 'danger' } + case 'yaml_parse_error': + return { title: job.course_config_url, msg: 'YAML could not be parsed
    '+ job.error_data['message'], type: 'danger' } + case 'yaml_format_error': + return { title: job.course_config_url, msg: 'YAML could be parsed but is in an unexpected format.
    '+ job.error_data['message'], type: 'danger' } + case 'course_validation_error': + var err = job.error_data.errors + + if (err.empty_course) { + return { title: job.course_config_url, msg: '`Course:` key is empty or invalid
    '+ job.error_data['message'], type: 'danger' } + } + if (err.section_missing_repos) { + return { title: err.section_missing_repos.title, msg: 'Each section must have at least one repo
    '+ job.error_data['message'], type: 'danger' } + } + + var msg = `Course validation error: ${JSON.stringify(err)}` + '
    '+ job.error_data['message'] + return { title: job.course_config_url, msg: msg, type: 'danger' } + case 'block_errors': + var failedBlockContents = job.error_data.failed_blocks.map((failed_block: any) => { + return ( +
  • {failed_block.repo_name} {failed_block.reason} (from section '{failed_block.section_title}')
  • + ) + }) + var blockMsg = ( +
      + {failedBlockContents } +
    + ) + return { title: 'Blocks Missing', msg: blockMsg, type: 'danger' } + case 'cohort_release_errors': + var failedReleases = job.error_data.failed_releases.map((failed_release: any) => { + return ( +
  • {failed_release.block.repo_name} {failed_release.validation_error} (from section '{failed_release.section_title}')
  • + ) + }) + var releasesMsg = ( +
      + { failedReleases } +
    + ) + return { title: 'Blocks Missing', msg: releasesMsg, type: 'danger' } + default: + } + if (job.status == "timeout" ) { + return { title: 'Timeout Error', msg: "Sycning took longer that 1 minute.", type: 'danger' } + } else { + return { title: 'Unexpected Error', msg: JSON.stringify(job), type: 'danger' } + } +} diff --git a/scripts/app/javascript/components/cohorts/settings/CohortSettingsTabs.tsx b/scripts/app/javascript/components/cohorts/settings/CohortSettingsTabs.tsx new file mode 100644 index 0000000000000000000000000000000000000000..d31944b3724f1eb23dc964ebefd9a7092c9359b9 --- /dev/null +++ b/scripts/app/javascript/components/cohorts/settings/CohortSettingsTabs.tsx @@ -0,0 +1,176 @@ +import * as React from 'react' +import * as Routes from '../../../generated/routes' +import http from '../../../lib/http' +import CohortSettingsResync from './CohortSettingsResync' +import CurriculumSettingsTab from './CurriculumSettingsTab' +import SvgRenderer from '../../SvgRenderer' + +type Props = { + cohortId: number + cohort: any + cohortReleases: any[] + availableBlocks: any[] + spinnerPath: string + userCount: number + branchIconSrc: string + githubIconPath: string + checkmarkPath: string + resyncJob: null | Api.Setup.ResyncJob + successfulResyncJob: null | Api.Setup.SuccessfulResyncJob + visibleStandardsCount: number +} +type State = { + showCohortModeModal: boolean + cohortReleases: any[] + availableBlocks: any[] + flashMessage: null | Api.Setup.Flash + cohort: any +} + +export default class CohortSettingsTabs extends React.Component { + constructor(props: Props) { + super(props); + + this.state = { + showCohortModeModal: false, + cohortReleases: this.props.cohortReleases, + availableBlocks: this.props.availableBlocks, + flashMessage: null, + cohort: this.props.cohort + }; + + this.setCohortReleases = this.setCohortReleases.bind(this); + this.hideFlash = this.hideFlash.bind(this); + this.reloadCohortReleases = this.reloadCohortReleases.bind(this); + this.releasePolling = this.releasePolling.bind(this); + this.resyncBranchRelease = this.resyncBranchRelease.bind(this); + } + + resyncBranchRelease(blockId: any, branchName: any, cohortReleaseId: any) { + http('PATCH', Routes.switchToBranchCohortCohortReleasePath(this.props.cohortId, cohortReleaseId), { + body: { branch_name: branchName, block_id: blockId } + }) + .then((data: any) => { + this.reloadCohortReleases(); + this.setFlash(data.flash_info); + if (data.flash_info.type === 'warning') { // 'warning' is the flash_info type for publishing, i.e. we need to poll + setTimeout(() => { + this.releasePolling(cohortReleaseId); + }, 2000); + } + }); + } + + reloadCohortReleases() { + http('GET', Routes.cohortCohortReleasesPath(this.props.cohortId)) + .then((data: any) => { + this.setState({ cohortReleases: data.cohort_releases }); + }); + } + + releasePolling(cohortReleaseId: number) { + http('GET', Routes.branchPollingCohortCohortReleasePath(this.props.cohortId, cohortReleaseId)) + .then((data: any) => { + let flashType; + let flashMessage; + let blockName = data.cohort_release.block_name; + + if (data.cohort_release.pending_release_state === 'processing' || data.cohort_release.pending_release_state === 'pending') { + setTimeout(() => { + this.releasePolling(data.cohort_release.id); + }, 2000); + } else if (data.cohort_release.pending_release_state === null && data.cohort_release.state === 'success') { + flashType = 'success'; + flashMessage = `Successfully published and switched to the latest commit on branch "${data.cohort_release.branch_name}"`; + this.setState({ cohortReleases: data.cohort_releases, flashMessage: { type: flashType, title: blockName, msg: flashMessage } }); + } else if (data.cohort_release.pending_release_state === 'failed') { + flashType = 'danger'; + flashMessage = `Unable to build '${data.cohort_release.pending_release_version}'. See errors below.`; + this.setState({ cohortReleases: data.cohort_releases, flashMessage: { type: flashType, title: blockName, msg: flashMessage } }); + } + }); + } + + setCohortReleases(cohortReleases: any) { + this.setState({ cohortReleases: cohortReleases }); + } + + setFlash = (flash: null | Api.Setup.Flash) => { + this.setState({ flashMessage: flash }) + } + + get BuildFlashMessage() { + let flash = this.state.flashMessage; + + if (flash == null) { + return null; + } else { + return ( +
    +
    + {flash.title + ':'}  + {typeof flash.msg == 'string' + ? + : flash.msg + } +
    +
    + +
    +
    + ); + } + } + + hideFlash() { + this.setState({ flashMessage: null }); + } + + render() { + return ( +
    + {this.BuildFlashMessage} +
    +
    +
    + +
    +

    Repos ({this.state.cohortReleases.length})

    +
    + + { this.props.userCount < 500 && +
    +

    Partnerup

    +
    + } +
    +
    + + +
    +
    + ); + } +} diff --git a/scripts/app/javascript/components/cohorts/settings/CohortVisibilitySection.tsx b/scripts/app/javascript/components/cohorts/settings/CohortVisibilitySection.tsx new file mode 100644 index 0000000000000000000000000000000000000000..7e71e1ddd034cffdcbce7d9f5649f0707c4fbda4 --- /dev/null +++ b/scripts/app/javascript/components/cohorts/settings/CohortVisibilitySection.tsx @@ -0,0 +1,197 @@ +import * as React from 'react' +import * as Routes from '../../../generated/routes' +import Icon from '../../Icon' +import SvgRenderer from '../../SvgRenderer' +import http from '../../../lib/http' + +type Props = { + section: Api.Setup.VisibilitySection + cohortId: number + eyeIconPath: string + contentFileHtmlHandler: any + updateVisibleCount: (num: number) => void + contentFileToggle: (contentFileId: number, hidden: boolean) => void +} + +type State = { + open: Record + hoveringStandard: Record + hoveringContentFile: Record +} + +export default class + extends React.Component { + constructor(props: Props) { + super(props); + this.state = { open: {}, hoveringStandard: {}, hoveringContentFile: {} } + } + + componentWillMount() { + window.hljs.configure({ + tabReplace: ' ', + languages: ['javascript', 'java', 'xml', 'html', 'sql', 'python', 'json', 'bash', 'text', 'cpp'] + }) + + window.hljs.initHighlightingOnLoad() + } + + toggleStandardOpenClosed = (standard_id: number) => { + this.state.open[standard_id] = ! this.state.open[standard_id] + this.forceUpdate() + } + + toggleStandardHoverState = (e: any) => { + var id = e.currentTarget.dataset.standardId + this.state.hoveringStandard[id] = e.type === 'mouseenter' + this.forceUpdate() + } + + toggleContentFileHoverState = (e: any) => { + var id = e.currentTarget.dataset.contentFileId + this.state.hoveringContentFile[id] = e.type === 'mouseenter' + this.forceUpdate() + } + + toggleStandardVisibility = async (standard: Api.Setup.VisibilityStandard) => { + var path = Routes.visibilityCohortStandardPath(this.props.cohortId, standard.id) + await http(standard.hidden ? 'DELETE' : 'PUT', path, { + body: { visibility_type: "hidden" } + }) + + if (!standard.content_files.map(file => file.hidden).every(hidden => hidden)) { + this.props.updateVisibleCount(standard.hidden ? 1 : -1) + } + + standard.hidden = !standard.hidden + this.forceUpdate() + } + + toggleContentFileVisibility = async (contentFile: Api.Setup.VisibilityContentFile, standard: Api.Setup.VisibilityStandard) => { + var path = Routes.visibilityCohortContentFilePath(this.props.cohortId, contentFile.id) + await http(contentFile.hidden ? 'DELETE' : 'PUT', path, { + body: { visibility_type: "hidden" } + }) + + let increaseStandardCount = standard.content_files.map(file => file.hidden).every(hidden => hidden) + + contentFile.hidden = !contentFile.hidden + + if (!standard.hidden && increaseStandardCount) { + this.props.updateVisibleCount(1) + } else if (!standard.hidden && standard.content_files.map(file => file.hidden).every(hidden => hidden)) { + this.props.updateVisibleCount(-1) + } + + this.props.contentFileToggle(contentFile.id, !contentFile.hidden) + this.forceUpdate() + } + + renderStandardsInSection = () => { + var {section, eyeIconPath} = this.props + var {open, hoveringStandard, hoveringContentFile} = this.state + + return section.standards_with_lessons.map(standard => { + var isOpen = open[standard.id] + var standardIsHovering = hoveringStandard[standard.id] + var standardEyeIconColor = 'inherit' + if (standardIsHovering && standard.hidden) { + standardEyeIconColor = '#3D7980' + } else if (standardIsHovering && !standard.hidden) { + standardEyeIconColor = '#34A8B3' + } + + return ( +
    +
    +
    {this.toggleStandardOpenClosed(standard.id)}}> + + { standard.title } +
    + {(standardIsHovering || standard.hidden) && ( + this.toggleStandardVisibility(standard)} + /> + )} +
    + + { isOpen && + standard.content_files.map(cf => { + var contentFileIsHovering = hoveringContentFile[cf.id] + var contentFileEyeIconColor = 'inherit' + + if (contentFileIsHovering && cf.hidden) { + contentFileEyeIconColor = '#3D7980' + } else if (contentFileIsHovering && !cf.hidden) { + contentFileEyeIconColor = '#34A8B3' + } + + return ( +
    +

    {this.previewContentFile(cf, cf.hidden)}} id={ `cf-${cf.uid}` }>{ cf.title }

    + {(contentFileIsHovering || cf.hidden) && ( + this.toggleContentFileVisibility(cf, standard)} + /> + )} +
    + ) + }) + } +
    + ); + }); + } + + previewContentFile = (cf: any, hidden: boolean) => { + // TODO: Remove direct DOM manipulation + $_(".previewed-contentfile", el => el.classList.remove("previewed-contentfile")) + $_("#cf-"+cf.uid, el => el.parentElement!.classList.add("previewed-contentfile")) + + http('GET', Routes.apiV1ContentFilePath(cf.id)) + .then((apicf: any) => { + var ret = { + contentFileId: cf.id, + html: apicf["html"], + title: apicf.title, + path: cf.path, + githubUrl: cf.github_url, + visible: !hidden + } + this.props.contentFileHtmlHandler(ret); + }) + } + + render() { + var {section} = this.props + + if (section.title === "") return null + + return ( +
    +
    {section.title}
    + { this.renderStandardsInSection() } +
    + ); + } +} diff --git a/scripts/app/javascript/components/cohorts/settings/CurriculumSettingsTab.tsx b/scripts/app/javascript/components/cohorts/settings/CurriculumSettingsTab.tsx new file mode 100644 index 0000000000000000000000000000000000000000..861c44aedcc153437f9d53bb1ffd59cf7db236d1 --- /dev/null +++ b/scripts/app/javascript/components/cohorts/settings/CurriculumSettingsTab.tsx @@ -0,0 +1,198 @@ +import * as React from 'react' +import CohortBlockReleaseRow from './CohortBlockReleaseRow' +import ReleaseVersionModal from './ReleaseVersionModal' +import BranchReleaseModal from './BranchReleaseModal' + +type Props = { + cohortId: number + setFlash(flash: null | Api.Setup.Flash): void + cohortReleases: any[] + reloadCohortReleases(): void + setCohortReleases(cohortReleases: any): void + spinnerPath: string + branchIconSrc: string + releasePolling(cohortReleaseId: number): void + resyncBranchRelease(blockId: any, branchName: any, cohortReleaseId: any): void +} +type State = any + +export default class CurriculumSettingsTab extends React.Component { + constructor(props: Props) { + super(props); + + this.state = { + cohortReleases: props.cohortReleases, + currentCohortReleaseId: null, + modalType: null, + releases: [], + selectedBlock: {} + }; + + this.handleModalOutsideClick = this.handleModalOutsideClick.bind(this); + this.showVersionModal = this.showVersionModal.bind(this); + this.hideModal = this.hideModal.bind(this); + this.showBranchModal = this.showBranchModal.bind(this); + this.handleBranchUpdate = this.handleBranchUpdate.bind(this); + } + + componentWillReceiveProps(nextProps: Props) { + if (this.props.cohortReleases !== nextProps.cohortReleases) { + this.setState({ cohortReleases: nextProps.cohortReleases }); + } + } + + cohortBlockReleases() { + var sectionTitle = ''; + var ret = [] as JSX.Element[]; + this.state.cohortReleases.forEach((cohort_release: any) => { + if (cohort_release.section_title != sectionTitle) { + ret.push( + + + {cohort_release.section_title} + + + ) + sectionTitle = cohort_release.section_title; + } + ret.push( + + ); + }); + return ret + } + + addModalClickListeners() { + document.addEventListener('touchmove', this.handleModalOutsideClick, false); + document.addEventListener('click', this.handleModalOutsideClick, false); + } + + removeModalClickListeners() { + document.removeEventListener('touchmove', this.handleModalOutsideClick, false); + document.removeEventListener('click', this.handleModalOutsideClick, false); + } + + handleModalOutsideClick(e: any) { + if (document.getElementById('curriculum-modal')!.contains(e.target)) { + return; + } + if (document.getElementsByClassName('hopscotch-bubble-container')[0] && + document.getElementsByClassName('hopscotch-bubble-container')[0].contains(e.target)) { + return; + } else if (e.target.className.indexOf('hopscotch') > -1) { + return; + } + + this.hideModal(); + } + + renderModal() { + if (this.state.modalType === 'version') { + return this.renderVersionModal(); + } else if (this.state.modalType === 'branch') { + return this.renderBranchModal(); + } + } + + renderVersionModal() { + this.addModalClickListeners(); + + return ( + + ); + } + + renderBranchModal() { + this.addModalClickListeners(); + + return ( + + ); + } + + showBranchModal(cohortReleaseId: number, blockId: number, blockName: string, blockURL: string) { + if (this.state.modalType !== null) { return null; } + + this.setState({ + currentCohortReleaseId: cohortReleaseId, + modalType: 'branch', + selectedBlock: { id: blockId, name: blockName, url: blockURL } + }); + } + + showVersionModal(cohortReleaseId: number, blockId: number, blockName: string) { + if (this.state.modalType !== null) { return null; } + + this.setState({ + currentCohortReleaseId: cohortReleaseId, + modalType: 'version', + selectedBlock: { id: blockId, name: blockName } + }); + } + + hideModal() { + this.removeModalClickListeners(); + + if (this.state.modalType === 'branch') { + window.hopscotch.endTour(); + } + + this.setState({ + currentCohortReleaseId: null, + modalType: null, + selectedBlock: {} + }); + } + + handleBranchUpdate(cohortRelease: any, flashInfo: any) { + this.hideModal(); + this.props.setFlash(flashInfo); + this.props.reloadCohortReleases(); + if (cohortRelease.pending_release_state === 'processing' || cohortRelease.pending_release_state === 'pending') { + this.props.releasePolling(cohortRelease.id); + } + } + + render() { + return ( +
    + { this.renderModal() } + + + + + + + + + + + + + { this.cohortBlockReleases() } + +
    BlockSourceUnitsUpdatesVersionUpdated
    +
    + ); + } +} diff --git a/scripts/app/javascript/components/cohorts/settings/ReleaseVersionModal.tsx b/scripts/app/javascript/components/cohorts/settings/ReleaseVersionModal.tsx new file mode 100644 index 0000000000000000000000000000000000000000..62bc7ed33af2a6821f42b090d0700daa9d776186 --- /dev/null +++ b/scripts/app/javascript/components/cohorts/settings/ReleaseVersionModal.tsx @@ -0,0 +1,97 @@ +import * as React from 'react' +import * as Routes from '../../../generated/routes' +import http from '../../../lib/http' +import SvgRenderer from '../../SvgRenderer' +import ReleaseVersionsTable from '../cohort_releases/ReleaseVersionsTable' + +type Props = any +type State = any + +export default class ReleaseVersionModal extends React.Component { + constructor(props: Props) { + super(props); + + this.state = { + loaded: false, + releaseVersions: [], + }; + + http('GET', `${Routes.blockReleasesPath(this.props.block.id)}?cohort_id=${this.props.cohortId}`) + .then((data: any) => { + this.setState({ + loaded: true, + releaseVersions: data.releases, + }); + }); + } + + componentDidUpdate() { + if (this.state.loaded) { + const tableWrapper = document.getElementsByClassName('tablewrapper')[0]; + const height = document.documentElement.clientHeight; + + tableWrapper.setAttribute('style', `max-height:${height - 205}px;`); + } + } + + handleHideClick = () => { + this.props.hideModalHandler(); + } + + updateCohortReleaseVersionHandler = (releaseId: number) => { + let onBranch = true; + + this.state.releaseVersions.forEach((release: any) => { + if (release.current) onBranch = false + }); + + // only when switching off a branch and only if we're switching to the latest version do we prompt + // making a lot of assumptions here- release versions are always ordered with the latest first, and that + // having no current release means you must be on a branch. + let useLatest = false; + + if (onBranch && this.state.releaseVersions.length > 0 && this.state.releaseVersions[0].release_id === releaseId) { + useLatest = confirm('Would you like to always adopt new versions as they are released?'); + } + + http( + 'PATCH', + Routes.cohortCohortReleasePath(this.props.cohortId, this.props.currentCohortReleaseId), + { body: { cohort_release: { release_id: releaseId, use_latest_release: useLatest } } }, + ) + .then(() => { + this.props.hideModalHandler(); + this.props.reloadCohortReleases(); + }); + } + + render() { + return ( +
    +
    +

    + Pick Version for {this.props.block.name} +

    +
    + +
    +
    + WARNING: Selecting a specific version will switch this block to Manual Updates. +
    +
    +
    + {this.state.loaded ? ( + + ) : ( +
    + Loading... +
    + )} +
    +
    + ); + } +} diff --git a/scripts/app/javascript/components/cohorts/settings/ReleaseVersionRow.tsx b/scripts/app/javascript/components/cohorts/settings/ReleaseVersionRow.tsx new file mode 100644 index 0000000000000000000000000000000000000000..c41c4a66ddebabffab1fad14912ced360c7da1fc --- /dev/null +++ b/scripts/app/javascript/components/cohorts/settings/ReleaseVersionRow.tsx @@ -0,0 +1,160 @@ +import * as React from 'react' +import * as Routes from '../../../generated/routes' +import SvgRenderer from '../../SvgRenderer' +import ProcessingIcon from '../../ProcessingIcon' +import Timestamp from '../../Timestamp' + +type Props = any +type State = any + +export default class ReleaseVersionRow extends React.Component { + private wrapperRef: any + + constructor(props: Props) { + super(props); + + this.state = { showTooltip: false } + + if (this.props.release.github_sha == 'pending') { + $_('#new-release-btn', el => el.classList.add('disabled')) + } + } + + handleVersionSelect = () => { + this.props.selectVersionHandler(this.props.release.release_id); + } + + hideCohortsTooltip = () => { + if (this.state.showTooltip === true) { + document.removeEventListener('touchmove', this.handleVersionRowOutsideClick, false); + document.removeEventListener('click', this.handleVersionRowOutsideClick, false); + this.setState({ showTooltip: false }); + } + } + + showCohortsTooltip = (e: any) => { + e.preventDefault(); + document.addEventListener('touchmove', this.handleVersionRowOutsideClick, false); + document.addEventListener('click', this.handleVersionRowOutsideClick, false); + this.setState({ showTooltip: true }); + } + + handleVersionRowOutsideClick = (e: any) => { + const className = `cohorts-tooltip-${this.props.release.release_id}`; + + if (document.getElementsByClassName(className)[0].contains(e.target)) return + + this.hideCohortsTooltip(); + } + + renderGithubLink() { + if (this.props.release.github_sha === 'pending') { + this.props.releasePollingHandler(this.props.release.block_id); + } else { + return ( + + ); + } + } + + diffLink() { + if (this.props.release.diff_url) { + if (this.props.selectVersionHandler) { + return ( + + {`View diff on Github (v${this.props.release.version} vs current version)`} + + ) + } else { + return ( + + {`View diff on Github (v${this.props.release.version} vs v${this.props.release.version - 1})`} + + ) + } + } + } + + selectButton() { + if (this.props.release.current) { + return Current; + } else if (this.props.selectVersionHandler) { + return ( + + SELECT + + ) + } + } + + render() { + const { release } = this.props + const rowClass = release.current ? 'releaserow current-release' : 'releaserow' + const released_yet = release.github_sha === 'pending' ? : + + return ( + + + {released_yet} + + +
    {`v${release.version}`}
    +

    {release.notes}

    + {this.renderGithubLink()} + + + + {release.cohorts_using_release.length} + + {this.state.showTooltip && this.props.release.cohorts_using_release.length > 0 && ( +
    this.wrapperRef = el} + > +
    +
    + {this.props.release.cohorts_using_release.map((cohort: any) => ( +

    + + {cohort.name} + + +

    + ))} +
    +
    +
    + )} + + + {this.selectButton()} + + + ); + } +} diff --git a/scripts/app/javascript/components/cohorts/settings/UserCohortKebab.tsx b/scripts/app/javascript/components/cohorts/settings/UserCohortKebab.tsx new file mode 100644 index 0000000000000000000000000000000000000000..7db7964469c49913c3e102cb6d1140392aa64602 --- /dev/null +++ b/scripts/app/javascript/components/cohorts/settings/UserCohortKebab.tsx @@ -0,0 +1,73 @@ +import * as React from 'react' +import * as Routes from '../../../generated/routes' +import RowKebab from '../../RowKebab' + +type Props = { + studentId: number + studentUid: string + authUrl: string + cohortId: number +} + +type State = { + showActionMenu: boolean +} + +export default class UserKebab extends React.Component { + constructor(props: Props) { + super(props) + this.state = { + showActionMenu: false + } + } + + toggleActionMenu = () => { + this.setState({showActionMenu: !this.state.showActionMenu}); + } + + handleMenuOutsideClick = (e: any) => { + if ($_('#action-menu', el => el.contains(e.target))) { + return; + } + + this.toggleActionMenu(); + } + + addClickListeners(handler: any) { + document.addEventListener('touchmove', handler, false); + document.addEventListener('click', handler, false); + } + + removeClickListeners(handler: any) { + document.removeEventListener('touchmove', handler, false); + document.removeEventListener('click', handler, false); + } + + get actionMenu() { + if (this.state.showActionMenu) { + this.addClickListeners(this.handleMenuOutsideClick); + + return ( + + ); + } else { + this.removeClickListeners(this.handleMenuOutsideClick); + } + } + + render() { + return ( + <> + + {this.actionMenu} + + ) + } +} diff --git a/scripts/app/javascript/components/cohorts/settings/UserImport.tsx b/scripts/app/javascript/components/cohorts/settings/UserImport.tsx new file mode 100644 index 0000000000000000000000000000000000000000..5f5b139da403426d5181e38753561960ae880e07 --- /dev/null +++ b/scripts/app/javascript/components/cohorts/settings/UserImport.tsx @@ -0,0 +1,138 @@ +import * as React from 'react' +import Modal from '../../shared/Modal/Modal' +import http from '../../../lib/http' +import ProcessingIcon from '../../ProcessingIcon' +import SvgRenderer from '../../SvgRenderer' +import { stampFromString } from '../../Timestamp' + +type Props = { + cohortName: string + userEmail: string + importPath: string + importStatusPath: string + importState: string | null + importCompletedAt: string | null +} + +type State = { + actionText: string + closeText: string + handleAction: any + modalHeader: any + modalBody: any + importing: boolean + importState: string | null + importCompletedAt: string | null + showModal: boolean +} + +export default class UserImport extends React.Component { + state: State = { + closeText: "", + actionText: "", + handleAction: null, + modalHeader: null, + modalBody: null, + importing: this.props.importState === "working" ? true : false, + importState: this.props.importState, + importCompletedAt: this.props.importCompletedAt, + showModal: false + }; + + finish = () => { + this.setState({showModal:false}) + } + + import = () => { + http('POST', this.props.importPath, {}).then((_data: any) => {}) + this.setState({ + closeText: "", + actionText: "DONE", + handleAction: this.finish, + modalHeader:

    Import Student Work

    , + modalBody:

    Import started. An email will be sent to {this.props.userEmail} when the import is complete

    , + showModal: true, + importing: true + }) + setTimeout(this.checkStatus, 2000) + } + + checkStatus = () => { + http('GET', this.props.importStatusPath, {}).then((data: any) => { + if (data.student_import_completed_at != null) { + let importingBool = data.student_import_state === "working" ? true : false + this.setState({ importState: data.student_import_state, importCompletedAt: data.student_import_completed_at, importing: importingBool}) + } else { + setTimeout(this.checkStatus, 2000) + } + }) + } + + showImportModal = () => { + this.setState({ + closeText: "CANCEL", + actionText: "IMPORT", + handleAction: this.import, + modalHeader:

    Import Student Work

    , + modalBody: (<> +

    This action imports overlap work from other cohorts

    +
      +
    • Work will be imported for all students currently enrolled in {this.props.cohortName}.
    • +
    • Work will be imported from curriculum that appears in both {this.props.cohortName} and other cohorts.
    • +
    • This action cannot be undone.
    • +
    + ), + showModal: true + }) + } + + statusIcon = () => { + if (this.state.importing) { + return + } else if (this.state.importState != null) { + return <> + {this.state.importState === "success" && this.state.importCompletedAt ? ( + <> + + Last imported successfully at { window.moment(this.state.importCompletedAt).format('MMMM Do YYYY, h:mm a') } + + ) : ( + <> + + Import failed. Contact dev@galvanize.com + + )} + + } + return null + } + + render() { + return ( + <> + {this.setState({showModal:false})}} + actionText={this.state.actionText} + handleAction={this.state.handleAction} + shouldCloseOnOutsideClick={true} + > + {this.state.modalHeader} {this.state.modalBody} + +
    + { this.statusIcon() } + Import Work +
    + + ) + } +} diff --git a/scripts/app/javascript/components/cohorts/settings/UserKebab.tsx b/scripts/app/javascript/components/cohorts/settings/UserKebab.tsx new file mode 100644 index 0000000000000000000000000000000000000000..ed8769e383347b70bbf66d665333093ecba21578 --- /dev/null +++ b/scripts/app/javascript/components/cohorts/settings/UserKebab.tsx @@ -0,0 +1,71 @@ +import * as React from 'react' +import * as Routes from '../../../generated/routes' +import RowKebab from '../../RowKebab' + +type Props = { + studentId: number + studentUid: string + authUrl: string + cohortId: number +} + +type State = { + showActionMenu: boolean +} + +export default class UserKebab extends React.Component { + constructor(props: Props) { + super(props) + this.state = { + showActionMenu: false + } + } + + toggleActionMenu = () => { + this.setState({ showActionMenu: !this.state.showActionMenu }); + } + + handleMenuOutsideClick = (e: any) => { + if ($_('#action-menu', el => el.contains(e.target))) { + return; + } + + this.toggleActionMenu(); + } + + addClickListeners(handler: any) { + document.addEventListener('touchmove', handler, false); + document.addEventListener('click', handler, false); + } + + removeClickListeners(handler: any) { + document.removeEventListener('touchmove', handler, false); + document.removeEventListener('click', handler, false); + } + + get actionMenu() { + if (this.state.showActionMenu) { + this.addClickListeners(this.handleMenuOutsideClick); + + return ( + + ); + } else { + this.removeClickListeners(this.handleMenuOutsideClick); + } + } + + render() { + return ( + + + {this.actionMenu} + + ) + } +} diff --git a/scripts/app/javascript/components/cohorts/submissions_dashboard/AnswerStatusRollup.tsx b/scripts/app/javascript/components/cohorts/submissions_dashboard/AnswerStatusRollup.tsx new file mode 100644 index 0000000000000000000000000000000000000000..e692f93ec9639c97a4b9130f612a87eb5752926e --- /dev/null +++ b/scripts/app/javascript/components/cohorts/submissions_dashboard/AnswerStatusRollup.tsx @@ -0,0 +1,150 @@ +import * as React from 'react' +import {isEqual,map} from 'lodash-es' +import Icon from '../../Icon' + +type Props = any + +export default class SubmissionsDashboardAnswerStatusRollup extends React.Component { + constructor(props: Props) { + super(props); + this.getStrokeDashData = this.getStrokeDashData.bind(this); + this.renderRollupCircle = this.renderRollupCircle.bind(this); + } + + shouldComponentUpdate(nextProps: Props) { + return ! isEqual(this.props.studentRollup, nextProps.studentRollup); + } + + renderRollupCircle(strokeData: any) { + switch (strokeData['category']) { + case 'ungraded': + return ( + + + ); + case 'correct': + return ( + + + ); + case 'incorrect': + return ( + + + ); + } + } + + getStrokeDashData(count: number, totalChallenges: number, offset: number, category: any) { + let strokeDashLength = (((count / totalChallenges) * 100) - 2); + let strokeOffset = (100 - offset); + + return { + category: category, + strokeDashLength: strokeDashLength, + strokeDashEnd: (100 - strokeDashLength), + strokeOffset: strokeOffset + }; + } + + get Circles() { + let newOffset = 0; + let counter = 0; + + return map(this.props.studentRollup, (value, category) => { + counter++; + + if (value > 0) { + if (category === 'correct' && value === this.props.challengeCount) { + return this.completeCircle(counter); + } else if (category === 'ungraded' && value === this.props.challengeCount) { + return ; + } + let strokeData = this.getStrokeDashData(value, this.props.challengeCount, newOffset, category); + + newOffset += strokeData['strokeDashLength'] + 2; + + return this.renderRollupCircle(strokeData); + } + }); + } + + get SvgCircles() { + return ( + + + + + + { this.Circles } + + ); + } + + completeCircle(counter: any) { + return ( + + + + + + + + + + + + + + + + + ); + } + + render() { + return this.SvgCircles; + } +} diff --git a/scripts/app/javascript/components/cohorts/submissions_dashboard/HeaderStandardContainer.tsx b/scripts/app/javascript/components/cohorts/submissions_dashboard/HeaderStandardContainer.tsx new file mode 100644 index 0000000000000000000000000000000000000000..b744dd3c3662abc9773a287d689353ecc9ec8f02 --- /dev/null +++ b/scripts/app/javascript/components/cohorts/submissions_dashboard/HeaderStandardContainer.tsx @@ -0,0 +1,79 @@ +import * as React from 'react' +import {includes} from 'lodash-es' +import Icon from '../../Icon' + +type Props = any +type State = any + +export default class HeaderStandardContainer extends React.Component { + + shouldComponentUpdate(nextProps: Props, nextState: State) { + return this.props.openedFiles !== nextProps.openedFiles || this.props.onlyShowCheckpoints !== nextProps.onlyShowCheckpoints; + } + + renderChallenges(content_file: any) { + if (includes(this.props.openedFiles, content_file.id.toString())) { + return content_file.challenges.map((challenge: any) => { + return ( +
    +
    +
    { challenge.label }
    +
    { challenge.title }
    +
    +
    + ); + }); + } + } + + get RenderContentFileRows() { + let contentFileRows: any[] = []; + + this.props.standard.content_files.map((content_file: any) => { + if (!this.props.onlyShowCheckpoints || content_file.content_file_type == 'checkpoint') { + let arrowClass, challenges, clickHandler, icon; + + if (content_file.content_file_type === 'checkpoint') { + icon =
    ; + arrowClass = ''; + } else { + challenges = this.renderChallenges(content_file); + arrowClass = includes(this.props.openedFiles, content_file.id.toString()) ? ' -is-expanded' : ''; + icon =
    ; + clickHandler = () => this.props.toggleOpenedFiles(content_file.id.toString()); + } + + contentFileRows.push( +
    +
    + {icon} +
    { content_file.relative_display_name }
    + +
    + { challenges } +
    + ); + } + }); + + return contentFileRows; + } + + render() { + let renderedContentFileRows = this.RenderContentFileRows; + + if (renderedContentFileRows.length > 0) { + return ( +
    +
    +
    { this.props.standard.position_label }
    +
    { this.props.standard.title }
    +
    + { renderedContentFileRows } +
    + ); + } else { + return null; + } + } +} diff --git a/scripts/app/javascript/components/cohorts/submissions_dashboard/StandardContainer.tsx b/scripts/app/javascript/components/cohorts/submissions_dashboard/StandardContainer.tsx new file mode 100644 index 0000000000000000000000000000000000000000..ef7023060bbc5347067555beeba13706db348ba4 --- /dev/null +++ b/scripts/app/javascript/components/cohorts/submissions_dashboard/StandardContainer.tsx @@ -0,0 +1,56 @@ +import * as React from 'react' +import SubmissionsDashboardContentFileRow from './SubmissionsDashboardContentFileRow' + +type Props = { + standard: any + onlyShowCheckpoints: any + studentAnswersHash: any + cohortId: any + student: any + toggleOpenedFiles: any + openedFiles: any +} + +export default class StandardContainer extends React.Component { + + shouldComponentUpdate(nextProps: Props) { + return this.props.openedFiles !== nextProps.openedFiles || this.props.onlyShowCheckpoints !== nextProps.onlyShowCheckpoints; + } + + get RenderContentFileRows() { + let contentFileRows: React.ReactNode[] = []; + + this.props.standard.content_files.map((contentFile: any) => { + if (!this.props.onlyShowCheckpoints || contentFile.content_file_type == 'checkpoint') { + contentFileRows.push( + + ); + } + }); + + return contentFileRows; + } + + render() { + let contentFileRows = this.RenderContentFileRows; + + if (contentFileRows.length > 0) { + return ( +
    +
    + { contentFileRows } +
    + ); + } else { + return null; + } + } +} diff --git a/scripts/app/javascript/components/cohorts/submissions_dashboard/SubmissionsDashboard.tsx b/scripts/app/javascript/components/cohorts/submissions_dashboard/SubmissionsDashboard.tsx new file mode 100644 index 0000000000000000000000000000000000000000..80572555e9bfa51415510016fd75be55b6672405 --- /dev/null +++ b/scripts/app/javascript/components/cohorts/submissions_dashboard/SubmissionsDashboard.tsx @@ -0,0 +1,389 @@ +import * as React from 'react' +import {includes,without,filter,isEmpty} from 'lodash-es' +import update from 'immutability-helper' + +import Icon from '../../Icon' +import Dropdown from '../../Dropdown' +import UserAvatar from '../../UserAvatar' +import SortDropdown from '../../SortDropdown' +import HeaderStandardContainer from './HeaderStandardContainer' +import SubmissionsDashboardTable from './SubmissionsDashboardTable' + +import ReactTooltip from "react-tooltip"; + +type Props = any +type State = any + +export default class SubmissionsDashboard extends React.Component { + private challengeTableDiv: null | HTMLElement = null + + constructor(props: Props) { + super(props); + + this.state = { + openedFiles: this.props.openedFiles || [], + selectedStandard: this.props.selectedStandard || '0', + selectedSort: '1', + tablePositionY: this.ChallengeTableTop, + onlyShowCheckpoints: false + }; + + this.toggleOpenedFiles = this.toggleOpenedFiles.bind(this); + this.handleStandardSelect = this.handleStandardSelect.bind(this); + this.handleCheckpointSelect = this.handleCheckpointSelect.bind(this); + this.resetTableHeight = this.resetTableHeight.bind(this); + this.handleSortSelect = this.handleSortSelect.bind(this); + this.activityFilterForSpecificStandard = this.activityFilterForSpecificStandard.bind(this); + this.activityFilterForAllStandards = this.activityFilterForAllStandards.bind(this); + } + + componentDidMount() { + this.setSorts(); + this.resetTableHeight(); + let contentFileId = this.getUrlParameter('scroll_to_content_file'); + + if (contentFileId) { + this.scrollToContentFile(contentFileId); + this.toggleOpenedFiles(contentFileId); + + // Prevent browser scroll override on refresh: + // https://stackoverflow.com/questions/7035331/prevent-automatic-browser-scroll-on-refresh + window.addEventListener('beforeunload', () => { + this.scrollToContentFile(contentFileId); + }) + } + } + + setSorts() { + let challengeIndexDropdown = this.savedValue('get', 'challengeIndexDropdown'); // What Standard + let challengeContentTypesDropdown = this.savedValue('get', 'challengeContentTypesDropdown'); // Is checkpoint + let challengeIndexSortDropdown = this.savedValue('get', 'challengeIndexSortDropdown'); // Sorting A-Z / High To Low + + if (challengeIndexDropdown != null) { + this.setState({ selectedStandard: challengeIndexDropdown }); + } + + if (challengeContentTypesDropdown != null) { + this.setState({ onlyShowCheckpoints: challengeContentTypesDropdown == 'only_checkpoints' }); + } + + if (challengeIndexSortDropdown != null) { + this.setState({ selectedSort: challengeIndexSortDropdown }); + } + } + + scrollToContentFile(contentFileId: any) { + $_(`#content_file_${contentFileId}`, el => { + window.scrollTo({ top: el.getBoundingClientRect().top - 75 }) + }) + } + + // From: https://stackoverflow.com/questions/19491336/get-url-parameter-jquery-or-how-to-get-query-string-values-in-js + getUrlParameter(sParam: any) { + let sPageURL = decodeURIComponent(window.location.search.substring(1)); + let sURLVariables = sPageURL.split('&'); + let sParameterName; + let i; + + for (i = 0; i < sURLVariables.length; i++) { + sParameterName = sURLVariables[i].split('='); + + if (sParameterName[0] === sParam) { + return sParameterName[1] === undefined ? true : sParameterName[1]; + } + } + }; + + resetTableHeight() { + this.setState({ tablePositionY: this.ChallengeTableTop }); + } + + toggleOpenedFiles(contentFileId: any) { + if (includes(this.state.openedFiles, contentFileId.toString())) { + this.setState({ + openedFiles: without(this.state.openedFiles, contentFileId) + }); + } else { + this.setState({ + openedFiles: update(this.state.openedFiles, { $push: [contentFileId] }) + }); + } + } + + shouldDisplayStandard(standardId: any) { + return this.state.selectedStandard === '0' || this.state.selectedStandard === standardId.toString(); + } + + handleStandardSelect(e: any) { + this.setState({ selectedStandard: e.target.value }); + this.savedValue('set', e.target.id, e.target.value); + } + + handleCheckpointSelect(e: any) { + this.setState({ onlyShowCheckpoints: e.target.value == 'only_checkpoints' }); + this.savedValue('set', e.target.id, e.target.value); + } + + handleSortSelect(e: any) { + this.setState({ selectedSort: e.target.value }); + this.savedValue('set', e.target.id, e.target.value); + } + + savedValue(action: 'get' | 'set', id: any, value?: string) { + if (action == 'set') { + localStorage.setItem(this.props.cohort_id + '_submissionsSelectValue_' + id, value!); + } else { + return localStorage.getItem(this.props.cohort_id + '_submissionsSelectValue_' + id); + } + } + + sortedStudents() { + switch (this.state.selectedSort) { + case '1': + return this.sortAToZ(); + break; + case '2': + return this.sortZToA(); + break; + case '3': + return this.sortActivityHighToLow(); + break; + case '4': + return this.sortActivityLowToHigh(); + break; + default: + return this.sortAToZ(); + } + } + + sortAToZ() { + return this.props.students.sort((a: any, b: any) => { + let aName = a.name.toLowerCase(); + let bName = b.name.toLowerCase(); + + if (aName < bName) return -1; + if (aName > bName) return 1; + + return 0; + }); + } + + sortZToA() { + return this.props.students.sort((a: any, b: any) => { + let aName = a.name.toLowerCase(); + let bName = b.name.toLowerCase(); + + if (aName > bName) return -1; + if (aName < bName) return 1; + + return 0; + }); + } + + sortActivityHighToLow() { + let studentStandardChallengesAttempted = this.props.student_standard_challenges_attempted; + let filterMethod = this.activityFilterForAllStandards; + + if (this.state.selectedStandard != 0) filterMethod = this.activityFilterForSpecificStandard; + + return this.props.students.sort((a: any, b: any) => { + let a_student = filter(studentStandardChallengesAttempted, (data) => filterMethod(data, a)).length; + let b_student = filter(studentStandardChallengesAttempted, (data) => filterMethod(data, b)).length; + + // Break score ties alphabetically + if (a_student == b_student) return a.name.toLowerCase() > b.name.toLowerCase() ? 1 : -1; + else if (a_student > b_student) return -1; + else return 1; + }); + } + + sortActivityLowToHigh() { + let studentStandardChallengesAttempted = this.props.student_standard_challenges_attempted; + let filterMethod = this.activityFilterForAllStandards; + + if (this.state.selectedStandard != 0) filterMethod = this.activityFilterForSpecificStandard; + + return this.props.students.sort((a: any, b: any) => { + let a_student = filter(studentStandardChallengesAttempted, (data) => filterMethod(data, a)).length; + let b_student = filter(studentStandardChallengesAttempted, (data) => filterMethod(data, b)).length; + + // Break score ties alphabetically + if (a_student == b_student) return a.name.toLowerCase() < b.name.toLowerCase() ? 1 : -1; + else if (a_student < b_student) return -1; + else return 1; + }); + } + + activityFilterForAllStandards(data: any, record: any) { + return data['student_id'] == record.id; + } + + activityFilterForSpecificStandard(data: any, record: any) { + return data['student_id'] == record.id && data['standard_id'] == this.state.selectedStandard; + } + + get sortOptions() { + return [ + { + id: 1, + title: 'Alphabetical: A to Z' + }, + { + id: 2, + title: 'Alphabetical: Z to A' + }, + { + id: 3, + title: 'Activity: High to Low' + }, + { + id: 4, + title: 'Activity: Low to High' + } + ]; + } + + get Standards() { + let out: any[] = []; + + this.props.releases.map((release: any) => { + out.push( +
    { release.block_title }
    + ); + + release.standards_presenter.standards.map((standard: any) => { + if (this.shouldDisplayStandard(standard.id)) { + out.push( + + ); + } + }); + }); + + return out; + } + + get ChallengeTableTop() { + if (! this.challengeTableDiv) return 0; + + return this.challengeTableDiv.getBoundingClientRect().top; + } + + get SortDropdown() { + if (this.props.students.length < 2) return null; + + return ( + + ); + } + + get StudentAvatar() { + if (this.props.students.length !== 1) return null; + if (!this.props.show_student_avatar) return null; + + let student = this.props.students[0]; + + return ( +
    +
    + +
    + {student.name} +
    + ); + } + + get CurriculumPanelClass() { + return this.props.single_student ? 'curriculumpanel single-student' : 'curriculumpanel'; + } + + get ChallengeIndexTableClass() { + return this.props.single_student ? 'challengeindex-table single-student' : 'challengeindex-table'; + } + + render() { + return ( +
    +
    + + +
    +
    + +
    Incorrect
    +
    +
    + +
    Correct
    +
    +
    + +
    Submitted
    +
    +
    + +
    Error
    +
    +
    +
    +
    + { this.StudentAvatar } + { this.SortDropdown } +
    +
    { this.challengeTableDiv = el }}> +
    + { this.Standards } +
    + + +
    +
    + ); + } +} diff --git a/scripts/app/javascript/components/cohorts/submissions_dashboard/SubmissionsDashboardChallengeItem.tsx b/scripts/app/javascript/components/cohorts/submissions_dashboard/SubmissionsDashboardChallengeItem.tsx new file mode 100644 index 0000000000000000000000000000000000000000..3d2089cfddfa3b57849aff5d051dbfd35228cc16 --- /dev/null +++ b/scripts/app/javascript/components/cohorts/submissions_dashboard/SubmissionsDashboardChallengeItem.tsx @@ -0,0 +1,69 @@ +import * as React from 'react' +import * as Routes from '../../../generated/routes' +import {isEmpty} from 'lodash-es' +import Icon from '../../Icon' + +type Props = { + cohortId: any + student: any + challenge: any + studentAnswersHash: any +} +type State = any + +export default class SubmissionsDashboardChallengeItem extends React.Component { + private anchorStyle: any + + constructor(props: Props) { + super(props); + this.anchorStyle = {}; + } + + createStatusElement(id: string, sprite: string, color?: string) { + var url = Routes.cohortUserChallengePath(this.props.cohortId, this.props.student.id, this.props.challenge.id); + + if (color) { + return ( + + + + ); + } else { + return ( + + + + ); + } + } + + render() { + let statusElement; + let studentAnswer = this.props.studentAnswersHash[this.props.student.id][this.props.challenge.uid]; + + if (!isEmpty(studentAnswer)) { + let status = studentAnswer['status']; + + this.anchorStyle = { + textDecoration: 'none', + color: 'inherit', + cursor: 'pointer' + }; + if (status === 'ungraded') { + statusElement = this.createStatusElement('#ic_file_upload_24px', 'file'); + } else if (status === 'correct') { + statusElement = this.createStatusElement('#ic_check_24px', 'navigation', 'primary-color'); + } else if (status === 'incorrect') { + statusElement = this.createStatusElement('#ic_close_24px', 'navigation', 'error-color'); + } else if (status === 'failed') { + statusElement = this.createStatusElement('#ic_warning_24px', 'alert', 'error-color'); + } + } + + return ( +
    + { statusElement } +
    + ); + } +} diff --git a/scripts/app/javascript/components/cohorts/submissions_dashboard/SubmissionsDashboardContentFileRow.tsx b/scripts/app/javascript/components/cohorts/submissions_dashboard/SubmissionsDashboardContentFileRow.tsx new file mode 100644 index 0000000000000000000000000000000000000000..81b79df7b54bc4ff7206bdd790f00ae1286b31f7 --- /dev/null +++ b/scripts/app/javascript/components/cohorts/submissions_dashboard/SubmissionsDashboardContentFileRow.tsx @@ -0,0 +1,100 @@ +import * as React from 'react' +import {isEmpty, includes} from 'lodash-es' +import SubmissionsDashboardChallengeItem from './SubmissionsDashboardChallengeItem' +import AnswerStatusRollup from './AnswerStatusRollup' + +type Props = { + openedFiles: any[] + contentFile: any + cohortId: any + studentAnswersHash: any + student: any + toggleOpenedFiles: any +} +type State = any + +export default class SubmissionsDashboardContentFileRow extends React.Component { + + shouldComponentUpdate(nextProps: Props) { + let state1 = includes(nextProps.openedFiles, nextProps.contentFile.id.toString()); + let state2 = includes(this.props.openedFiles, nextProps.contentFile.id.toString()); + + return state1 !== state2; + } + + get RenderedChallenges() { + if (includes(this.props.openedFiles, this.props.contentFile.id.toString())) { + return this.props.contentFile.challenges.map((challenge: any) => { + return ( + + ); + }); + } + } + + render() { + let clickHandler, rollup; + + if (isEmpty(this.props.contentFile.student_rollups)) { + rollup = ''; + } else if (this.props.contentFile.student_rollups[this.props.student.id]) { + let studentRollup = this.props.contentFile.student_rollups[this.props.student.id]; + + if (this.props.contentFile.content_file_type == 'checkpoint') { + let explanationText = 'Yellow, Orange, Green = "Scored", Red = "Rejected", Gray = "Pending"'; + let checkpointStatus; + + switch (studentRollup.checkpoint_submission_state) { + case 'needs_review': + checkpointStatus = 'submitted'; + break; + case 'rejected': + checkpointStatus = 'rejected'; + break; + case 'done': + checkpointStatus = 'scored'; + break; + } + + if (checkpointStatus === 'scored' && studentRollup.performance_score != null) { + rollup = ( + +
    {studentRollup.performance_score}
    +
    + ); + } else { + rollup = ( + + + + ); + } + } else { + clickHandler = () => this.props.toggleOpenedFiles(this.props.contentFile.id.toString()); + + rollup = ( + + ); + } + } + + return ( +
    +
    + { rollup } +
    + {this.RenderedChallenges} +
    + ); + } +} diff --git a/scripts/app/javascript/components/cohorts/submissions_dashboard/SubmissionsDashboardStudentColumn.tsx b/scripts/app/javascript/components/cohorts/submissions_dashboard/SubmissionsDashboardStudentColumn.tsx new file mode 100644 index 0000000000000000000000000000000000000000..c8885d0c566c8975052791ea86c0a5eb7e086491 --- /dev/null +++ b/scripts/app/javascript/components/cohorts/submissions_dashboard/SubmissionsDashboardStudentColumn.tsx @@ -0,0 +1,69 @@ +import * as React from 'react' +import UserAvatar from '../../UserAvatar' +import StandardContainer from './StandardContainer' + +type Props = any +type State = any + +export default class SubmissionsDashboardStudentColumn extends React.Component { + + shouldDisplayStandard(standardId: any) { + return this.props.selectedStandard === '0' || this.props.selectedStandard === standardId.toString(); + } + + shouldComponentUpdate(nextProps: Props, nextState: State) { + let newOpenedFiles = this.props.openedFiles !== nextProps.openedFiles; + let newSelectedStandard = this.props.selectedStandard !== nextProps.selectedStandard; + let newShowOnlyCheckpoints = this.props.onlyShowCheckpoints !== nextProps.onlyShowCheckpoints; + + return newOpenedFiles || newSelectedStandard || newShowOnlyCheckpoints; + } + + get RenderStudentData() { + let out: React.ReactNode[] = []; + + this.props.releases.map((release: any) => { + out.push( +
    + ); + + release.standards_presenter.standards.map((standard: any) => { + if (this.shouldDisplayStandard(standard.id)) { + out.push( + + ); + } + }); + }); + + return out; + } + + get StudentHeader() { + if (this.props.hideHeader) return null; + + return ( + + + + ); + } + + render() { + return ( +
    + { this.StudentHeader } + { this.RenderStudentData } +
    + ); + } +} diff --git a/scripts/app/javascript/components/cohorts/submissions_dashboard/SubmissionsDashboardStudentNameBar.tsx b/scripts/app/javascript/components/cohorts/submissions_dashboard/SubmissionsDashboardStudentNameBar.tsx new file mode 100644 index 0000000000000000000000000000000000000000..048e72c6468c69515f3522ff70d9a8fe18e9e359 --- /dev/null +++ b/scripts/app/javascript/components/cohorts/submissions_dashboard/SubmissionsDashboardStudentNameBar.tsx @@ -0,0 +1,68 @@ +import * as React from 'react' +import UserAvatar from '../../UserAvatar' + +type Props = any +type State = { + showFixedElement: boolean +} + +export default class SubmissionsDashboardStudentNameBar extends React.Component { + constructor(props: Props) { + super(props); + this.state = { + showFixedElement: false + }; + + this.handleScroll = this.handleScroll.bind(this); + } + + get StudentAvatars() { + return this.props.students.map((student: any) => { + return ( +
    + +
    + ); + }); + } + + shouldComponentUpdate(nextProps: Props, nextState: State) { + let fixedElementChanged = this.state.showFixedElement !== nextState.showFixedElement; + + return fixedElementChanged; + } + + componentDidMount() { + window.addEventListener('scroll', this.handleScroll); + } + + componentWillUnmount() { + window.removeEventListener('scroll', this.handleScroll); + } + + handleScroll() { + let scrollTop = window.pageYOffset || + document.documentElement!.scrollTop || + document.body.scrollTop; + + if (this.state.showFixedElement === false && scrollTop >= this.props.tablePositionY) { + this.setState({ showFixedElement: true }); + } else if (this.state.showFixedElement === true && scrollTop < this.props.tablePositionY) { + this.setState({ showFixedElement: false }); + } + } + + render() { + return ( +
    +
    + { this.StudentAvatars } +
    +
    + ); + } +} diff --git a/scripts/app/javascript/components/cohorts/submissions_dashboard/SubmissionsDashboardTable.tsx b/scripts/app/javascript/components/cohorts/submissions_dashboard/SubmissionsDashboardTable.tsx new file mode 100644 index 0000000000000000000000000000000000000000..9bc862dbfb40d64dbff1c2bc531bf3214c0286c5 --- /dev/null +++ b/scripts/app/javascript/components/cohorts/submissions_dashboard/SubmissionsDashboardTable.tsx @@ -0,0 +1,61 @@ +import * as React from 'react' +import SubmissionsDashboardStudentColumn from './SubmissionsDashboardStudentColumn' +import SubmissionsDashboardStudentNameBar from './SubmissionsDashboardStudentNameBar' + +const {ScrollSync, ScrollSyncPane} = require('react-scroll-sync') + +type Props = any + +export default class SubmissionsDashboardTable extends React.Component { + + get RenderStudentColumns() { + return this.props.students.map((student: any) => { + return ( + + ); + }); + } + + get StudentNameBar() { + if (this.props.singleStudent) return
    ; + + return ( + + ); + } + + get StudentsPanelClass() { + return this.props.singleStudent ? 'studentspanel single-student' : 'studentspanel'; + } + + render() { + return ( + +
    + +
    + { this.RenderStudentColumns } +
    +
    + + { this.StudentNameBar } + +
    +
    + ); + } +} diff --git a/scripts/app/javascript/components/content_files/ActionMenus.tsx b/scripts/app/javascript/components/content_files/ActionMenus.tsx new file mode 100644 index 0000000000000000000000000000000000000000..f07f2a0b89ec7698b4ebe33ddc6e08bfc81b62da --- /dev/null +++ b/scripts/app/javascript/components/content_files/ActionMenus.tsx @@ -0,0 +1,150 @@ +import React from 'react' +import Icon from '../Icon' +import ActionMenu from '../shared/ActionMenu/ActionMenu' +import { + studentSummaryCsvCohortContentFilePath, + challengeDataCsvCohortContentFilePath +} from '../../generated/routes' + +type Props = { + permalink: string + cohortId: number + contentFileId: number + githubUrl: string + repoDetails: Api.RepoDetails + isCheckpoint: boolean +} + +type State = { + showPermaLinkMenu: boolean + showGithubLinkMenu: boolean + showDataExportMenu: boolean +} + +export default class ActionMenus extends React.Component { + state: State = { + showPermaLinkMenu: false, + showGithubLinkMenu: false, + showDataExportMenu: false + } + + addClickListeners = (handler: any) => { + document.addEventListener('touchmove', handler, {capture: true}); + document.addEventListener('click', handler, {capture: true}); + } + + removeClickListeners = (handler: any) => { + document.removeEventListener('touchmove', handler, false); + document.removeEventListener('click', handler, false); + } + + handleMenuOutsideClick = (e: any) => { + if ($_('#action-menu-list ul', el => el.contains(e.target))) { + return + } + + this.closeAll() + } + + closeAll = () => { + this.setState({ + showPermaLinkMenu: false, + showGithubLinkMenu: false, + showDataExportMenu: false, + }) + } + + setClipboard = (value: string) => { + var tempInput = document.createElement("input"); + tempInput.style.position = "absolute"; + tempInput.style.left = "-1000px"; + tempInput.style.top = "-1000px"; + tempInput.value = value; + document.body.appendChild(tempInput); + tempInput.select(); + document.execCommand("copy"); + document.body.removeChild(tempInput); + } + + dataExportMenu = () => { + const { cohortId, contentFileId } = this.props + if (this.state.showDataExportMenu) { + this.addClickListeners(this.handleMenuOutsideClick) + return ( + + ) + } else { + this.removeClickListeners(this.handleMenuOutsideClick) + } + } + + githubLinkMenu = () => { + if (this.state.showGithubLinkMenu) { + this.addClickListeners(this.handleMenuOutsideClick) + let { repo_name, org, origin } = this.props.repoDetails + return ( + { this.closeAll(); this.setClipboard(`git@${origin}:${org}/${repo_name}.git`) }, text: "Copy SSH clone link"}, + {func: () => { this.closeAll(); this.setClipboard(`https://${origin}/${org}/${repo_name}.git`) }, text: "Copy HTTPS clone link"}, + {href: "/content_link/github/gSchool/learn-content/walkthrough/00-edit-existing-curriculum.md", func: this.closeAll, text: "Instructions to edit this page"} + ]} + /> + ) + } else { + this.removeClickListeners(this.handleMenuOutsideClick) + } + } + + permaLinkMenu = () => { + if (this.state.showPermaLinkMenu) { + this.addClickListeners(this.handleMenuOutsideClick) + return ( + { this.closeAll(); this.setClipboard(`${this.props.permalink}`) }, text: "Copy page permalink"} + ]} + /> + ) + } else { + this.removeClickListeners(this.handleMenuOutsideClick) + } + } + + render() { + return ( +
    +
    + { this.closeAll(); this.setState({showPermaLinkMenu: !this.state.showPermaLinkMenu}) }}> + + + {this.permaLinkMenu()} +
    +
    + { this.closeAll(); this.setState({showGithubLinkMenu: !this.state.showGithubLinkMenu}) }}> + + + {this.githubLinkMenu()} +
    + {this.props.isCheckpoint && +
    + { this.closeAll(); this.setState({showDataExportMenu: !this.state.showDataExportMenu}) }}> + + + {this.dataExportMenu()} +
    + } +
    + ) + } +} + diff --git a/scripts/app/javascript/components/content_files/Lesson.tsx b/scripts/app/javascript/components/content_files/Lesson.tsx new file mode 100644 index 0000000000000000000000000000000000000000..f1ccd6781ca069142987854aa705b3e0e7de3718 --- /dev/null +++ b/scripts/app/javascript/components/content_files/Lesson.tsx @@ -0,0 +1,158 @@ +import * as React from 'react' +import SideBar from './SideBar' +import ChallengeBlock from '../challenges/challenge_block/ChallengeBlock' +import SubmissionRenderer from './SubmissionRenderer' +import ActionMenu from '../shared/ActionMenu/ActionMenu' + +type Props = { + repoDetails: Api.RepoDetails + visitedLessonUids: string[] + standard: Api.StandardPresenter_ForCard + checkpointInfo: Api.CheckpointInfo + contentFilesSubmissions: Api.ContentFile[] + userId: number + masteryCohort: boolean + contentFileId: number + instructorOrAdmin: boolean + isIpynb: boolean + challenges: any[] + contentFileType: Api.ContentFileType + lessonIcon: string + doesNotCountIcon: string + checkpointIcon: string + allCompleteImagePath: string + awaitingGradeImagePath: string + rejectedImagePath: string + submissionUrl: string + cohortPath: string + renderSubmissionRenderer: boolean | null + visibilityPath: string + standardHidden: boolean + contentFileHidden: boolean + contentFileHtml: string + presenter: any + cohortMode: string + contentFileTitle: string + contentFilePath: string + checkpointIsAutoscored: boolean + isEnrolledStudent: boolean + spinnerPath: string + footer: string + isSandBoxCohort: boolean + curriculumWarningCount: number + cohortId: number + permalink: string + githubUrl: string +} + +type State = { + contentFilesSubmissions: Api.ContentFile[] +} + +type ChallengeRef = React.RefObject + +export default class Lesson extends React.Component { + state: State = { + contentFilesSubmissions: this.props.contentFilesSubmissions + } + + async componentDidMount() { + if (this.props.contentFileType && ['lesson','resource','instructor'].indexOf(this.props.contentFileType) > -1 ) { + const challengesWithIndex = this.props.challenges.map((ch, idx) => { + ch['index'] = this.props.challenges.length - idx; + return ch; + }); + await Promise.all(challengesWithIndex.map(this.renderChallenge)); + } + } + + renderChallenge = (challenge: any) => { + return new Promise(async (resolve, _reject) => { + const challengeDiv = document.getElementById(`challenge-${challenge.uid}`); + const ref = React.createRef() as ChallengeRef + + window.ReactDOM.render( + , + challengeDiv, + () => resolve(ref) + ); + }) + } + + onSubmissionGraded = (challengeId: number, submissionStatus: string) => { + let newChallengesWithAnswers = this.state.contentFilesSubmissions.slice(); + + // select content file containing challenge + newChallengesWithAnswers.some((contentFile: Api.ContentFile) => { + if (contentFile.id === this.props.contentFileId) { + // select challenge to modify its submissions + contentFile.challenges.some((challenge) => { + if (challenge.id === challengeId) { + challenge.student_submissions[this.props.userId]['status'] = submissionStatus; + this.setState({ contentFilesSubmissions: newChallengesWithAnswers }); + } + return challenge.id === challengeId + }) + } + return contentFile.id === this.props.contentFileId + }); + } + + render() { + return ( +
    +
    + +
    + + {this.props.footer &&
    } +
    +
    +
    + ) + } +} diff --git a/scripts/app/javascript/components/content_files/MarkdownRenderer.tsx b/scripts/app/javascript/components/content_files/MarkdownRenderer.tsx new file mode 100644 index 0000000000000000000000000000000000000000..dac38fce487121e9055797919946277963926d4e --- /dev/null +++ b/scripts/app/javascript/components/content_files/MarkdownRenderer.tsx @@ -0,0 +1,43 @@ +import React from 'react' +import ActionMenus from './ActionMenus' + +const MarkdownRenderer = ({ + html, + instructorOrAdmin, + isIpynb, + isInPreviewSandbox, + githubUrl, + permalink, + cohortId, + contentFileId, + repoDetails, +} : { + html: any, + instructorOrAdmin: boolean, + isIpynb: boolean, + isInPreviewSandbox: boolean, + githubUrl: string, + permalink: string, + cohortId: number, + contentFileId: number, + repoDetails: Api.RepoDetails, +}) => ( +
    + {isIpynb && + ipynb + } + {instructorOrAdmin && isInPreviewSandbox == false && ( + + )} +
    +
    +) + +export default MarkdownRenderer diff --git a/scripts/app/javascript/components/content_files/PDFRenderer.tsx b/scripts/app/javascript/components/content_files/PDFRenderer.tsx new file mode 100644 index 0000000000000000000000000000000000000000..803de67611f1638ac7b74942c0bee444d003f28f --- /dev/null +++ b/scripts/app/javascript/components/content_files/PDFRenderer.tsx @@ -0,0 +1,63 @@ +import React from 'react'; +import { SizeMe } from 'react-sizeme' +import { Document, Page, pdfjs } from 'react-pdf/dist/entry.webpack'; +pdfjs.GlobalWorkerOptions.workerSrc = `//cdnjs.cloudflare.com/ajax/libs/pdf.js/${pdfjs.version}/pdf.worker.js`; + +type Props = { + contentFileHtml: string + instructorOrAdmin: boolean + githubUrl: string +} + +type State = { + numPages: number | null +} + +export default class PDFRenderer extends React.Component { + state: State = { numPages: null } + + onDocumentLoadSuccess = (e: any) => { + this.setState({ numPages: e.numPages }); + } + + render() { + return ( +
    + {this.props.instructorOrAdmin && ( + + View on Github + + )} + + {({ size }) => ( +
    + + { + Array.from( + new Array(this.state.numPages), + (_el, index) => ( + + ), + ) + } + +
    + )} +
    +
    + ) + } +} diff --git a/scripts/app/javascript/components/content_files/SideBar.tsx b/scripts/app/javascript/components/content_files/SideBar.tsx new file mode 100644 index 0000000000000000000000000000000000000000..10af65f2508f41e0fadbf493bfcbd44546f808b2 --- /dev/null +++ b/scripts/app/javascript/components/content_files/SideBar.tsx @@ -0,0 +1,248 @@ +import * as React from 'react' +import Icon from '../Icon' +import MasteryProgressBar from '../shared/MasteryProgressBar/MasteryProgressBar' +import PercentageProgressBar from '../shared/PercentageProgressBar/PercentageProgressBar' +import SvgRenderer from '../SvgRenderer' +import DonutRing from '../shared/DonutRing/DonutRing' +import { checkpointSubmission } from '../../lib/utils'; + +type Props = { + standard: Api.StandardPresenter_ForCard + masteryCohort: boolean + contentFilesSubmissions: Api.ContentFile[] + visitedLessonUids: string[] + userId: number + lessonIcon: string + doesNotCountIcon: string + checkpointIcon: string + allCompleteImagePath: string + cohortPath: string + rejectedImagePath: string + awaitingGradeImagePath: string + checkpointInfo: Api.CheckpointInfo +} + +const $ = (query: string) => document.querySelector(query)! as HTMLElement + +export default class SideBar extends React.Component { + private lessonLayout!: HTMLElement["classList"] + + componentDidMount() { + this.lessonLayout = $_('.lesson-layout', el => el.classList)! + + if (window.localStorage.getItem('content_file_sidebar_closed') === 'true') { + this.toggleSidebar(); + } + this.adjustNavBarWidth(); + window.addEventListener("resize", this.adjustNavBarWidth) + window.addEventListener("resize", this.updateDimensions); + this.updateDimensions(); + } + + componentWillUnmount() { + window.removeEventListener("resize", this.updateDimensions); + } + + updateDimensions = () => { + if (window.innerWidth < 1024) { + if (!this.lessonLayout.contains('-collapsed')) { + this.lessonLayout.add('-collapsed'); + this.adjustNavBarWidth(); + } + } else { + if ( + this.lessonLayout.contains('-collapsed') + && window.localStorage.getItem('content_file_sidebar_closed') === 'false' + ) { + this.lessonLayout.remove('-collapsed'); + this.adjustNavBarWidth(); + } + } + } + + adjustNavBarWidth = () => { + const bodyWidth = $_('.body-container', el => el.clientWidth)!; + + $_('.primary-navigation', (el) => { + el.style.width = `${bodyWidth}px`; + el.classList.add('navigation-move-right'); + }); + + $_('.alert', (el) => { + el.style.width = `${bodyWidth}px`; + el.classList.add('navigation-move-right'); + }); + + $_('.secondary-navigation', (el) => { + el.style.width = `${bodyWidth}px`; + el.classList.add('navigation-move-right'); + }); + + $_('footer', (el) => { + el.style.width = `${bodyWidth}px`; + el.classList.add('navigation-move-right'); + }); + } + + challengeStateCount = (challenges: Api.ChallengeWithSubmissions[], states: string[]) => { + return challenges.filter((challenge) => { + let submissionState = challenge.student_submissions[this.props.userId]['status']; + return states.includes(submissionState) + }).length + } + + contentFiles() { + return this.props.contentFilesSubmissions.map((contentFile) => { + let activeClass = ""; + let challengeRingIcon: any; + + if (window.location.pathname === contentFile.content_file_path) { + activeClass = '-is-active'; + } + + let studentRollup = { + correct: this.challengeStateCount(contentFile.challenges, ['correct', 'ungraded']), + incorrect: this.challengeStateCount( + contentFile.challenges, + ['incorrect', 'failed', 'invalid_fork', 'repo_failed', 'timeout', 'missing_file'] + ), + ungraded: this.challengeStateCount(contentFile.challenges, ['n/a', 'canceled']) + }; + + if (contentFile.content_file_type === 'lesson') { + let challengeLength = contentFile.challenges.length; + if (challengeLength === 0 && contentFile.uid && this.props.visitedLessonUids.includes(contentFile.uid)) { + // fake the rollup data to look like a 100% complete + challengeLength = 1 + studentRollup = { correct: 1, incorrect: 0, ungraded: 0 } + } else if (challengeLength === 0 && contentFile.uid == null) { + challengeRingIcon = ( +
    + +
    + Completion not tracked +
    +
    + ); + } + if (challengeRingIcon == null && (studentRollup['correct'] > 0 || studentRollup['incorrect'] > 0)) { + challengeRingIcon = ( + + ); + } + } else if (contentFile.content_file_type === 'checkpoint') { + const submission = contentFile.checkpoint_submissions![this.props.userId]; + + if (!submission) { + challengeRingIcon = ; + } else if (submission.state === 'needs_review') { + challengeRingIcon = + } else if (submission.state === 'done') { + challengeRingIcon = this.props.masteryCohort ? ( + + ) : ( +

    + {`${this.props.checkpointInfo!.contentFilesSubmission!.last_performance_percent}%`} +

    + ) + } else if (submission.state === 'rejected') { + challengeRingIcon = + } + } else if (contentFile.content_file_type === 'resource') { + return null; + } + + return ( + +
    +
    +
    +
    +
    + {contentFile.title} +
    +
    + {challengeRingIcon} +
    +
    + ) + }); + } + + toggleSidebar = () => { + if (this.lessonLayout.contains('-collapsed') && window.innerWidth < 1024) return + + this.lessonLayout.toggle('-collapsed'); + this.adjustNavBarWidth(); + if (this.lessonLayout.contains('-collapsed')) { + window.localStorage.setItem('content_file_sidebar_closed', 'true'); + } else { + window.localStorage.setItem('content_file_sidebar_closed', 'false'); + } + } + + render() { + const { + standard, + masteryCohort, + allCompleteImagePath, + awaitingGradeImagePath, + checkpointInfo, + } = this.props; + + const checkpoint = checkpointSubmission(standard.cohort_standard_progress_data); + const isAwaitingGrade = (checkpoint && checkpoint.state == 'needs_review') + return ( +
    +
    +
    + + + +
    + +
    +
    + {masteryCohort ? ( + + ) : ( + + )} +
    + {this.contentFiles()} +
    +
    +
    + ) + } +} diff --git a/scripts/app/javascript/components/content_files/SubmissionRenderer.tsx b/scripts/app/javascript/components/content_files/SubmissionRenderer.tsx new file mode 100644 index 0000000000000000000000000000000000000000..8ce5ecc3f099623f2ec53a45e170b565cd409a3c --- /dev/null +++ b/scripts/app/javascript/components/content_files/SubmissionRenderer.tsx @@ -0,0 +1,92 @@ +import React from 'react' +import PDFRenderer from './PDFRenderer'; +import MarkdownRenderer from './MarkdownRenderer' +import { errorCohortPath } from '../../generated/routes' + +const SubmissionRenderer = ({ + standardHidden, + contentFileHidden, + visibilityPath, + instructorOrAdmin, + contentFileHtml, + presenter, + contentFileType, + contentFilePath, + isIpynb, + isInPreviewSandbox, + curriculumWarningCount, + cohortId, + contentFileId, + permalink, + repoDetails, + githubUrl, +} : { + standardHidden: any, + contentFileHidden: any, + visibilityPath: any, + instructorOrAdmin: any, + contentFileHtml: any, + presenter: any, + contentFileType: any, + contentFilePath: string, + isIpynb: boolean, + isInPreviewSandbox: boolean, + curriculumWarningCount: number, + cohortId: number, + contentFileId: number, + permalink: string, + repoDetails: Api.RepoDetails, + githubUrl: string, +}) => { + let contentHiddenMsg = null; + + if (standardHidden && contentFileHidden) { + contentHiddenMsg = (<>Both the Unit and Lesson are currently hidden from students. Change visibility here.) + } else if (standardHidden) { + contentHiddenMsg = (<>This Unit is currently hidden from students. Change visibility here.) + } else if (contentFileHidden) { + contentHiddenMsg = (<>This Lesson is currently hidden from students. Change visibility here.) + } else if (contentFileType === "instructor") { + contentHiddenMsg = 'This instructor content is not viewable by students.' + } + + if (contentFileHtml.includes('data:application/pdf')) { + return ( + + ) + } + + return ( + <> + {contentHiddenMsg && ( +

    + {contentHiddenMsg} +

    + )} + {curriculumWarningCount > 0 && ( +

    + {`${curriculumWarningCount} error${(curriculumWarningCount > 1 ? 's' : '')} on this lesson`} +

    + )} + { + + } + + ) +} + +export default SubmissionRenderer diff --git a/scripts/app/javascript/components/content_files/checkpoints/Checkpoint.tsx b/scripts/app/javascript/components/content_files/checkpoints/Checkpoint.tsx new file mode 100644 index 0000000000000000000000000000000000000000..6797c73e99448211bb836e0bd0cb5707fb239842 --- /dev/null +++ b/scripts/app/javascript/components/content_files/checkpoints/Checkpoint.tsx @@ -0,0 +1,680 @@ +import React, { Component } from 'react' +import SideBar from '../SideBar' +import CheckpointLanding from './CheckpointLanding' +import CheckpointActionBar from './CheckpointActionBar' +import http from '../../../lib/http' +import ChallengeBlock from '../../challenges/challenge_block/ChallengeBlock' +import Modal from '../../shared/Modal/Modal' +import Icon from '../../Icon' +import { + cohortContentFileSubmittedChallengeAnswersPath, + batchCreateCohortContentFileSubmittedChallengeAnswersPath as submitCheckpointUrl, + checkpointSubmissionResultsCohortContentFileSubmittedChallengeAnswersPath as pollingUrl, + cohortPath, +} from '../../../generated/routes' +import CheckpointLandingAttribute from './CheckpointLandingAttribute' +import CheckpointStudentScores from './student_scores/CheckpointStudentScores' +import { any } from 'prop-types' + +type Props = { + repoDetails: Api.RepoDetails + checkpointTitle: string + visitedLessonUids: string[] + standard: Api.StandardPresenter_ForCard + checkpointInfo: Api.CheckpointInfo + cohortId: number + cohortMode: string + contentFileId: number + takeAssessmentPath: string + contentFilesSubmissions: Api.ContentFile[] + userId: number + timeLimit: number | null + checkpointAttemptsRemaining: number | null + challengeTypes: string[] + cohortPath: string + permalink: string + masteryCohort: boolean + lessonIcon: string + doesNotCountIcon: string + checkpointIcon: string + allCompleteImagePath: string + awaitingGradeImagePath: string + spinnerPath: string + rejectedImagePath: string + instructorOrAdmin: boolean + isPreviewSandbox: boolean + githubUrl: string + progressThresholds: number[] + studentScoresEndpoint: string + loadStudentScores: boolean + studentsInCohort: Api.Student[] + showPairing: boolean + noAttemptsRemaining: boolean | undefined +} + +type State = { + assessmentStarted: boolean + showModal: boolean + pollingForResults: boolean + html: string + checkpointSubmissionId: number | null + checkpointSubmission: Api.CheckpointSubmissionShow.t | null + challengesWithAnswers: any[] // TOOD - better this in the api + challengeBlocks: any + modalHeading: JSX.Element + modalParagraph: JSX.Element + helperText: JSX.Element | null + modalCloseText: string + modalActionText: string + modalCloseFn: (e: any) => void + modalActionFn: (e: any) => void + hasUnsavedWork: {}, + checkpointCreatedAt: string + formattedTimeString: string + timeIsUp: boolean + submitted: boolean + timeLimit: number | null + pairLocalStorageKey: string + pairs: object +} + +type ChallengeRef = React.RefObject + +export default class Checkpoint extends React.Component { + + private countdownInterval: any + + state: State = { + html: "", + assessmentStarted: false, + pollingForResults: false, + showModal: false, + checkpointSubmissionId: null, + checkpointSubmission: null, + challengesWithAnswers: [], + challengeBlocks: [], + modalHeading: <>, + modalParagraph: <>, + helperText: null, + modalCloseText: "", + modalActionText: "", + modalCloseFn: () => { this.setState({ showModal: false }) }, + modalActionFn: () => { }, + hasUnsavedWork: {}, + checkpointCreatedAt: "", + formattedTimeString: "", + timeIsUp: false, + submitted: false, + timeLimit: this.props.timeLimit, + pairLocalStorageKey: `${this.props.cohortId}-${this.props.userId}-${this.props.contentFileId}`, + pairs: any + } + + componentDidMount() { + if (!window.location.toString().includes("assessment=true")) { + const newurl = window.location.toString() + "?assessment=true" + window.history.pushState({path:newurl},'',newurl) + } + let savedPairs = localStorage.getItem(this.state.pairLocalStorageKey) + if (savedPairs !== null) { + this.setState({pairs: JSON.parse(savedPairs)}) + } + } + + enterAssessment = () => { + const { checkpointInfo, checkpointAttemptsRemaining } = this.props + const { timeLimit } = this.state + + if (checkpointAttemptsRemaining != undefined && checkpointAttemptsRemaining <= 0) { + this.takeAssessment() + } else if (timeLimit !== null && checkpointInfo.checkpointsByState.started === null) { + this.setState({ + showModal: true, + modalHeading: , + modalParagraph:

    This assessment is timed. Once you start the assessment, if you navigate away from the page or close your browser, the timer will continue to run.

    , + modalActionText: "Start Assessment", + modalCloseText: "Cancel", + modalActionFn: this.takeAssessment, + }) + } else if (timeLimit !== null && checkpointInfo.checkpointsByState.started && checkpointInfo.checkpointsByState.started.created_at) { + // immediately submit the assessment if it is past due + const started = Math.round((new Date(checkpointInfo.checkpointsByState.started.created_at)).getTime() / 1000); + if (Math.round((new Date()).getTime() / 1000) > (started + (timeLimit*60))) { + this.timesUpSubmitAssessment() + return + } + this.takeAssessment() + } else { + this.takeAssessment() + } + } + + takeAssessment = async () => { + const { takeAssessmentPath } = this.props + const { timeLimit } = this.state + + const data = await http('POST', takeAssessmentPath, {}) + this.setState({ + assessmentStarted: true, + showModal: false, + html: data.content_file_html, + timeLimit: data.time_limit, + challengesWithAnswers: data.challenges_with_answers, + checkpointSubmissionId: data.submission_id, + checkpointCreatedAt: data.created_at // TODO don't set if in practice mode? + }) + toggleElementById("primary-navigation", false) + toggleElementById("secondary-navigation", false) + toggleElementByTagName("footer", false) + + const challengesWithIndex = data.challenges_with_answers.map((ch:any, idx:number) => { + ch['index'] = data.challenges_with_answers.length - idx + return ch + }) + + const challengeBlocks = await Promise.all(challengesWithIndex.map(this.renderChallenge)); + this.setState({challengeBlocks}) + if (data.time_limit !== null && data.created_at) { + this.timer() + this.countdownInterval = setInterval(this.timer, 1000) + } + + window.MathJax.Hub.Typeset() + window.highlightCodeBlocks() + + if(history.pushState) { + window.history.pushState("", document.title, location.href); + window.onpopstate = (e:any) => { + e.preventDefault() + location.reload(true) + } + } + } + + setPairs = (pairs:any) => { + this.setState({pairs: pairs}) + } + + timesUpSubmitAssessment = async () => { + const { takeAssessmentPath } = this.props + const data = await http('POST', takeAssessmentPath, {}) + this.setState({ + assessmentStarted: true, + showModal: true, + html: data.content_file_html, + timeLimit: data.time_limit, + timeIsUp: true, + challengesWithAnswers: data.challenges_with_answers, + checkpointSubmissionId: data.submission_id, + checkpointCreatedAt: data.created_at // TODO don't set if in practice mode? + }) + toggleElementById("primary-navigation", false) + toggleElementById("secondary-navigation", false) + toggleElementByTagName("footer", false) + + const challengesWithIndex = data.challenges_with_answers.map((ch:any, idx:number) => { + ch['index'] = data.challenges_with_answers.length - idx + return ch + }) + + const challengeBlocks = await Promise.all(challengesWithIndex.map(this.renderChallenge)); + this.setState({challengeBlocks}, () => { + this.submitCheckpoint(null) + this.showTimesUpModal() + }) + } + + updateUnsavedWork = (challengeId:number, unsavedWorkBool:boolean) => { + const clone = this.state.hasUnsavedWork; + clone[challengeId] = unsavedWorkBool; + this.setState({ hasUnsavedWork: clone }); + } + + renderChallenge = (challenge: any) => { + const { cohortId, contentFileId } = this.props + return new Promise(async (resolve, _reject) => { + const challengeDiv = document.getElementById(`challenge-${challenge.uid}`) + const ref = React.createRef() as ChallengeRef + + window.ReactDOM.render( + , + challengeDiv, + () => resolve(ref) + ); + }) + } + + onUpdateAnswered = (challengeId: any, answered: any) => { + const index = this.state.challengesWithAnswers.findIndex((challenge: any) => challenge.id == challengeId) + const newChallengesWithAnswers = this.state.challengesWithAnswers.slice(); + + newChallengesWithAnswers[index]['answered'] = answered; + + this.setState({ challengesWithAnswers: newChallengesWithAnswers }); + // post draft to backend with answer and challenge id + } + + openSubmitCheckpointModal = (_e: any) => { + const heading = (

    Submit your responses?

    ) + const timerPart = this.state.timeLimit ? and have {this.state.formattedTimeString} remaining : <> + const reachedMaxSubmissionsPart = this.props.checkpointAttemptsRemaining === 1 ? " Once you submit, you will not be able to submit again." : "" + let paragraph = (

    You have attempted {this.calcChallengesAttempted()} questions{timerPart}.{reachedMaxSubmissionsPart}

    ) + + if (Object.keys(this.state.pairs).length > 1) { + let names = Object.values(this.state.pairs).map((s:Api.Student)=>{ + return `${s.full_name}` + }) + + let joinedNames = names.slice(0,names.length-1).join(", ") + if (names.length == 2) { + joinedNames = joinedNames + ` and ${names[names.length-1]}` + } else { + joinedNames = joinedNames + `, and ${names[names.length-1]}` + } + + paragraph = (<>{paragraph}

    Submitting as {joinedNames}.

    ) + } + + this.setState({ + showModal: true, + modalHeading: heading, + modalParagraph: paragraph, + modalActionText: "Submit All Answers", + modalCloseText: "Back To Test", + modalActionFn: this.submitCheckpoint, + hasUnsavedWork: {} + }) + } + + openSaveAndExitModal = (e: any) => { + e.preventDefault() + if (this.props.checkpointAttemptsRemaining != undefined && this.props.checkpointAttemptsRemaining <= 0) { + this.submitCheckpoint(e, true) + } else { + const heading = (

    Exit without submitting?

    ) + const timerPart = this.state.timeLimit ? " The timer will continue to run in the background--additional time will not be available" : "" + const paragraph = (

    Save progress and exit this page. Your answers will not be submitted for scoring.{timerPart}

    ) + this.setState({ + showModal: true, + modalHeading: heading, + modalParagraph: paragraph, + modalActionText: "Save And Exit", + modalCloseText: "Back To Test", + modalActionFn: this.saveCheckpoint, + }) + } + } + + saveCheckpoint = async (e: any) => { + this.submitCheckpoint(e, true) + } + + submitCheckpoint = async (e: any, draft?: boolean) => { + const { cohortId, contentFileId } = this.props + const { pairs } = this.state + if (e !== undefined && e !== null) e.preventDefault(); + + const payload = this.state.challengeBlocks.map((challengeBlock: any) => { + if (challengeBlock.current!.props.challenge.challenge_type === "checkbox") { + const entries = challengeBlock.current!.state.checkboxInputs + const answer = Object + .entries(entries) + .filter(([_, value]) => value) + .map(entry => entry[0]); + return { + challenge_id: challengeBlock.current!.props.challenge.id, + answer: answer + }; + } + return { + challenge_id: challengeBlock.current!.props.challenge.id, + answer: challengeBlock.current!.state.input + }; + }); + + const url = submitCheckpointUrl(cohortId, contentFileId) + const response = await http('POST', url, { + body: { submitted_challenge_answers: payload, draft: draft ? draft : false, pairs: pairs }, + }) + + if (response.checkpointSubmission) { + this.setState({checkpointSubmission: response.checkpointSubmission, submitted: true}) + if (response.checkpointSubmission.autoscorable){ + this.startPollingForAutoScore(true) + } else { + this.setPendingStateModal(response.checkpointSubmission.submittedDate) + } + + let savedPairs = localStorage.getItem(this.state.pairLocalStorageKey) + if (savedPairs !== null) localStorage.removeItem(this.state.pairLocalStorageKey) + } else { + // Did you hit Save And Exit + this.setState({hasUnsavedWork: {} }) + location.reload() + } + } + + startPollingForAutoScore = async (setInitialModalState: boolean) => { + if (setInitialModalState) { + this.setPollingStateModal() + } + const { cohortId, contentFileId } = this.props + const data = await http('GET', pollingUrl(cohortId, contentFileId)) + if (data.errors && data.errors.length > 0) { + // TODO define error state modal + } else if (!data.url) { + setTimeout(this.startPollingForAutoScore.bind(this, false), 3000); + } else { + this.setAutoscoredStateModal(data); + } + } + + setAutoscoredStateModal(data: any) { + let modalParagraph: JSX.Element = <> + if (this.props.cohortMode == "Percentage") { + const { correct_points, total_points } = data + modalParagraph =

    You received {correct_points}/{total_points} pts ({Math.round((correct_points/total_points)*100)}%)

    + } else if (this.props.cohortMode == "Mastery") { + modalParagraph =

    You received a mastery score of {data.mastery_score}

    + } + if (data.state) { + this.setState({ + showModal: true, + modalHeading:

    Submission Scored!

    , + modalParagraph: modalParagraph, + modalCloseText: "", + modalActionText: "Continue", + modalActionFn: () => { + location.pathname = cohortPath(this.props.cohortId) + } + }); + } + } + + setPollingStateModal = () => { + const currentTime = window.moment(new Date()).tz(window.moment.tz.guess()); + const header = this.state.timeLimit && this.state.timeIsUp ?

    Time's up!

    :

    Scoring your submission!

    + const automatically = this.state.timeLimit && this.state.timeIsUp ? `Your assessment was automatically submitted at ${currentTime.format('h:mm A z')} and is being scored. ` : "" + this.setState({ + showModal: true, + modalHeading: header, + modalParagraph: (<>

    {automatically} This might take a minute...

    ), + modalCloseText: "", + modalActionText: "" + }) + } + + setPendingStateModal = (date: string) => { + const currentTime = window.moment(date).tz(window.moment.tz.guess()); + const header = this.state.timeLimit && this.state.timeIsUp ?

    Time's up!

    :

    Submitted!

    + const automatically = this.state.timeLimit && this.state.timeIsUp ? " automatically" : "" + this.setState({ + showModal: true, + modalHeading: header, + modalParagraph: (

    Your assessment was{automatically} submitted at {`${currentTime.format('h:mm A z')}`}.

    ), + helperText:

    Submitted, score pending

    , + modalCloseText: "", + modalActionText: "Continue", + modalActionFn: () => { + location.pathname = cohortPath(this.props.cohortId) + } + }) + } + + showTimesUpModal = () => { + let submittedTime = new Date() + if (this.state.timeLimit) { + submittedTime = new Date(submittedTime.valueOf() - this.state.timeLimit * 60000); + } + + this.setState({ + showModal: true, + modalHeading:

    Time's up!

    , + modalParagraph:

    Your assessment was automatically submitted at {`${window.moment(submittedTime).format('h:mm A z')}`}.

    , + helperText: null, + modalCloseText: "", + modalActionText: "Continue", + modalActionFn: () => { + location.pathname = cohortPath(this.props.cohortId) + } + }) + } + + calcChallengesAttempted = (): string => { + const challengeAttempted = this.state.challengesWithAnswers.filter((challengeWithAnswer: any) => challengeWithAnswer.answered).length + return `${challengeAttempted}/${this.props.checkpointInfo.challengeTotal}` + } + + timer = () => { + const { timeLimit } = this.state + + if (timeLimit !== null && this.state.checkpointCreatedAt) { + const time = window.timeRemaining(timeLimit, this.state.checkpointCreatedAt) + + if (time < 0) { + clearInterval(this.countdownInterval); + this.setState({timeIsUp: true}) + this.submitCheckpoint(null) + } else { + let formattedTimeString = window.fmtTime(time, true) + this.setState({formattedTimeString: formattedTimeString}) + } + } + } + + renderCheckpoint(): React.ReactNode { + const { + checkpointAttemptsRemaining, + studentsInCohort, + userId, + instructorOrAdmin, + showPairing + } = this.props + const { + checkpointCreatedAt, + showModal, + html, + timeLimit, + timeIsUp, + submitted, + pairLocalStorageKey, + pairs + } = this.state + + return ( + <> + +
    +
    +
    +
    + { ( (checkpointAttemptsRemaining === null) || (checkpointAttemptsRemaining !== null && checkpointAttemptsRemaining > 0) ) && +
    + That’s the end! When you’re ready, submit this assessment at the top of the page. +
    + } + + ) + } + + render(): React.ReactNode { + const { assessmentStarted, + showModal, + modalHeading, + modalParagraph, + modalActionText, + modalCloseText, + helperText, + modalCloseFn, + modalActionFn, + timeIsUp, + checkpointCreatedAt, + pairs, + pairLocalStorageKey + } = this.state + + const { standard, + masteryCohort, + cohortPath, + contentFilesSubmissions, + visitedLessonUids, + userId, + lessonIcon, + doesNotCountIcon, + checkpointIcon, + allCompleteImagePath, + awaitingGradeImagePath, + rejectedImagePath, + checkpointInfo, + repoDetails, + checkpointTitle, + timeLimit, + permalink, + checkpointAttemptsRemaining, + challengeTypes, + instructorOrAdmin, + isPreviewSandbox, + githubUrl, + cohortId, + contentFileId, + studentsInCohort, + showPairing, + loadStudentScores, + progressThresholds, + cohortMode, + studentScoresEndpoint + } = this.props + + return ( +
    +
    + {assessmentStarted === false && + + } +
    + {assessmentStarted ? this.renderCheckpoint() : + <> + + { this.props.instructorOrAdmin && + + } + + } + + {modalHeading} {modalParagraph} + +
    +
    +
    + ) + } +} + +function toggleElementById(elementId: string, visible: boolean) { + const element = document.getElementById(elementId); + if (element && element.parentNode) { + if (visible) { + (element as HTMLElement).style.display = 'flex'; + } else { + (element as HTMLElement).style.display = 'none'; + } + } +} + +function toggleElementByTagName(tagName: string, visible: boolean) { + const element = document.getElementsByTagName(tagName)[0]; + if (element !== null && element !== undefined) { + if (visible) { + (element as HTMLElement).style.display = 'flex'; + } else { + (element as HTMLElement).style.display = 'none'; + } + } +} diff --git a/scripts/app/javascript/components/content_files/checkpoints/CheckpointActionBar.tsx b/scripts/app/javascript/components/content_files/checkpoints/CheckpointActionBar.tsx new file mode 100644 index 0000000000000000000000000000000000000000..75525387c193b6a64ef8860e117b07dc5d811248 --- /dev/null +++ b/scripts/app/javascript/components/content_files/checkpoints/CheckpointActionBar.tsx @@ -0,0 +1,192 @@ +import React from 'react' +import Icon from '../../Icon' +import Button from '../../shared/Button/Button' +import { cohortCheckpointSubmissionPath } from '../../../generated/routes' +import CheckpointPairs from './CheckpointPairs' +import PairAvatars from './PairAvatars' + +import ReactTooltip from "react-tooltip"; + +type Props = { + timeLimit: number | null + challengesAttempted: string + onSubmitCheckpoint: (e:any)=>void + onSaveAndExit: (e:any)=>void + disableButtons: boolean + noAttemptsRemaining: boolean | undefined + checkpointCreatedAt: string + stopTimer: boolean + studentsInCohort: Api.Student[] + currentUserId: number + pairLocalStorageKey: string + instructorOrAdmin: boolean + showPairing: boolean + pairs: object + setPairs: (pairs:any)=>void + assessmentStarted: boolean +} + +type State = { + challengesAttempted: string + disableButtons: boolean + noAttemptsRemaining: boolean | undefined + formattedCountDownTime: JSX.Element + underFiveRemaining: boolean +} + +export default class CheckpointActionBar extends React.Component { + _isMounted = false + private countdownInterval: any + + state: State = { + challengesAttempted: this.props.challengesAttempted, + disableButtons: this.props.disableButtons, + noAttemptsRemaining: this.props.noAttemptsRemaining, + formattedCountDownTime: <>, + underFiveRemaining: false + } + + componentDidMount() { + this._isMounted = true + const { checkpointCreatedAt, timeLimit } = this.props + if (timeLimit !== null && checkpointCreatedAt) { + this.timer() + this.countdownInterval = setInterval(this.timer, 1000) + } + + window.onpopstate = (_event:any) => { + clearInterval(this.countdownInterval); + } + } + + componentWillUnmount() { + this._isMounted = false + } + + static getDerivedStateFromProps(nextProps: Props, prevState: State) { + let update: any = {}; + if (nextProps.challengesAttempted != prevState.challengesAttempted) { + update.challengesAttempted = nextProps.challengesAttempted + } + if (nextProps.disableButtons != prevState.disableButtons) { + update.disableButtons = nextProps.disableButtons + } + if (nextProps.noAttemptsRemaining != prevState.noAttemptsRemaining) { + update.noAttemptsRemaining = nextProps.noAttemptsRemaining + } + return Object.keys(update).length ? update : null; + } + + timer = () => { + if (!this._isMounted) { return } + const { checkpointCreatedAt, timeLimit, stopTimer } = this.props + + if (stopTimer) { + clearInterval(this.countdownInterval); + return + } + + if (timeLimit !== null && checkpointCreatedAt) { + const timeRemaining = window.timeRemaining(timeLimit, checkpointCreatedAt) + const timeRemainingSplit = window.splitTime(timeRemaining) + + let underFive = false + if (timeRemainingSplit.days <= 0 && timeRemainingSplit.hours <= 0 && timeRemainingSplit.minutes <= 5 && !this.state.underFiveRemaining) { + this.setState({underFiveRemaining: true}) + underFive = true + } + + let formattedCountDownTime = ( +
    + {window.fmtTime(timeRemaining, false)} +
    + ) + + if (timeRemaining <= 0) { + formattedCountDownTime = (
    Time's up!
    ) + clearInterval(this.countdownInterval); + } + this.setState({formattedCountDownTime: formattedCountDownTime}) + } + } + + + render() { + const { + onSubmitCheckpoint, + onSaveAndExit, + timeLimit, + instructorOrAdmin, + currentUserId, + pairLocalStorageKey, + studentsInCohort, + showPairing, + pairs, + assessmentStarted, + setPairs + } = this.props + + const { + challengesAttempted, + disableButtons, + noAttemptsRemaining, + formattedCountDownTime, + underFiveRemaining + } = this.state + + let dataTip = "" + if (noAttemptsRemaining){ + dataTip = 'You have submitted this checkpoint the maximum number of times. Please speak with your instructor to resubmit.' + } else if (Object.values(pairs).length > 1) { + dataTip = Object.values(pairs).map((s)=>{return s.full_name}).join("
    ") + } + + const submitButtonClass = Object.keys(pairs).length > 1 + + return ( + <> + +
    + + ) + } +} diff --git a/scripts/app/javascript/components/content_files/checkpoints/CheckpointDetails.tsx b/scripts/app/javascript/components/content_files/checkpoints/CheckpointDetails.tsx new file mode 100644 index 0000000000000000000000000000000000000000..d23c5cced37646f5f1a2c4bb79509517eda7bb2a --- /dev/null +++ b/scripts/app/javascript/components/content_files/checkpoints/CheckpointDetails.tsx @@ -0,0 +1,271 @@ +import * as React from 'react' +import Button from '../../shared/Button/Button' +import { cohortCheckpointSubmissionPath } from '../../../generated/routes' +import Icon from '../../Icon' +import CheckpointPairs from './CheckpointPairs' +import PairAvatars from './PairAvatars' + +type Props = { + checkpointInfo: Api.CheckpointInfo + checkpointAttemptsRemaining: number | null + enterAssessment: any + timesUpSubmitAssessment: any + timeLimit: number | null | undefined + timeIsUp: boolean + checkpointCreatedAt: string + studentsInCohort: Api.Student[] + currentUserId: number + pairLocalStorageKey: string + showPairing: boolean + instructorOrAdmin: boolean + setPairs: (pairs:any)=>void + pairs: object + noAttemptsRemaining: boolean | undefined + assessmentStarted: boolean +} + + +export default class CheckpointDetails extends React.Component { + pointsEarned = (submission:any): JSX.Element => { + const { checkpointInfo } = this.props + if ( checkpointInfo.cohortMode == "Mastery" ) { + if (checkpointInfo.masteryScore) { + return ( +
    + {checkpointInfo.masteryScore} +
    + ) + } + } else { + if (submission) { + const correctPoints = submission.correct_points + const totalPoints = submission.total_points + const lastPerformancePercent = Math.floor(correctPoints / totalPoints * 100) + return ( +
    {correctPoints}/{totalPoints} | {lastPerformancePercent}%
    + ) + } + } + return <> + } + + renderCheckpointPairs = () => { + if (this.props.showPairing && this.props.timeLimit == null && !this.props.instructorOrAdmin && !this.props.noAttemptsRemaining) { + return ( + + ) + } + } + + startCheckpoint = () => { + this.props.enterAssessment() + } + + hasTheTimeExpired = () => { + const { checkpointInfo, timeLimit, timesUpSubmitAssessment } = this.props + + if (timeLimit == null || checkpointInfo.checkpointsByState.started == null) { + this.startCheckpoint() + } else { + const timeRemaining = window.timeRemaining(timeLimit, checkpointInfo.checkpointsByState.started.created_at) + + if (timeRemaining <= 0) { + timesUpSubmitAssessment() + } else { + this.startCheckpoint() + } + } + } + + checkpointDetailsButton = () => { + const { checkpointInfo, checkpointAttemptsRemaining, timeLimit } = this.props + let buttonText = "Retake Assessment" + let buttonFormat: 'solid' | 'outline' = "solid" + let action = this.startCheckpoint + + if (checkpointInfo.checkpointsByState.started) { + action = this.hasTheTimeExpired + buttonText = "Continue Assessment" + buttonFormat = "solid" + } else if(checkpointAttemptsRemaining !== undefined && checkpointAttemptsRemaining === 0) { + buttonText = "Practice" + buttonFormat = "outline" + } + + if (timeLimit !== null && timeLimit !== undefined && checkpointInfo !== null && checkpointInfo.checkpointsByState.started !== null) { + const formattedTimeString = window.fmtTime(window.timeRemaining(timeLimit, checkpointInfo.checkpointsByState.started.created_at), true) + if (this.props.timeIsUp || formattedTimeString == "") { + action = this.hasTheTimeExpired + buttonText = "Submit Assessment" + buttonFormat = "solid" + } + } + return ( + <> +
    + { + + } + {this.props.showPairing && timeLimit == null && !this.props.instructorOrAdmin && !this.props.noAttemptsRemaining && buttonText !== "Practice" && + this.renderCheckpointPairs() + } +
    + ) + } + + doneCheckpointDetails = () => { + const { checkpointInfo} = this.props + if (checkpointInfo.checkpointsByState.done) { + if (checkpointInfo.checkpointsByState.retry && new Date(checkpointInfo.checkpointsByState.retry.created_at) > new Date(checkpointInfo.checkpointsByState.done.created_at)) { + return null // if the retry is newer than the done, the retry should get rendered + } + return ( +
    + ) + } else { + return null + } + } + + retryCheckpointDetails = () => { + const { checkpointInfo} = this.props + if (checkpointInfo.checkpointsByState.retry) { + if (checkpointInfo.checkpointsByState.done && new Date(checkpointInfo.checkpointsByState.done.created_at) > new Date(checkpointInfo.checkpointsByState.retry.created_at)) { + return null // if the done is newer than the retry, the done should get rendered + } + return ( +
    + { this.pointsEarned(checkpointInfo.checkpointsByState.retry) } +
    + Latest Score {window.moment(checkpointInfo.checkpointsByState.retry.updated_at).tz(window.moment.tz.guess()).calendar()} +
    +
    + {checkpointInfo.checkpointsByState.needs_review == null && checkpointInfo.checkpointsByState.retry && + View + } +
    +
    + ) + } else { + return null + } + } + + startedCheckpointDetails = () => { + const { checkpointInfo, timeLimit, checkpointCreatedAt} = this.props + let createdAt = checkpointInfo.checkpointsByState.started ? checkpointInfo.checkpointsByState.started.created_at : checkpointCreatedAt + if (createdAt) { + let formattedTimeString + if (timeLimit !== null && timeLimit !== undefined && checkpointInfo.checkpointsByState.started) { + formattedTimeString = window.fmtTime(window.timeRemaining(timeLimit, checkpointInfo.checkpointsByState.started.created_at), true) + if (formattedTimeString === "") { + formattedTimeString = "No time" + } + } + return ( + + {formattedTimeString !== undefined && + + + {formattedTimeString} remaining! + + } + Assessment Started {window.moment(createdAt).tz(window.moment.tz.guess()).calendar()} + + ) + } else { + return null + } + } + + needsReviewCheckpointDetails = () => { + const { checkpointInfo} = this.props + if (checkpointInfo.checkpointsByState.needs_review) { // this should only be true if the last submission needs review + if (checkpointInfo.checkpointsByState.done && new Date(checkpointInfo.checkpointsByState.needs_review.created_at) < new Date(checkpointInfo.checkpointsByState.done.created_at) ) { + return null; // Done supercedes the needs review on page + } else if (checkpointInfo.checkpointsByState.retry && new Date(checkpointInfo.checkpointsByState.needs_review.created_at) < new Date(checkpointInfo.checkpointsByState.retry.created_at) ) { + return null; // Done ("retry") supercedes the needs review on page + } + return ( +
    +
    Pending
    +
    + Latest Submission {window.moment(checkpointInfo.checkpointsByState.needs_review.created_at).tz(window.moment.tz.guess()).calendar()} +
    +
    + View +
    +
    + ) + } else { + return null + } + } + + render() { + const { + checkpointInfo, + timeLimit, + checkpointCreatedAt, + showPairing, + currentUserId, + pairLocalStorageKey, + studentsInCohort, + instructorOrAdmin, + assessmentStarted, + pairs + } = this.props + if (checkpointInfo.checkpointsByState.started === null && + checkpointInfo.checkpointsByState.needs_review === null && + checkpointInfo.checkpointsByState.done === null && + checkpointInfo.checkpointsByState.retry === null && checkpointCreatedAt === "") { + return ( + <> + + { this.renderCheckpointPairs() } + + ) + } + + const theDetails = (checkpointInfo.checkpointsByState.started !== null || checkpointCreatedAt) ? this.startedCheckpointDetails() : (<> + { this.needsReviewCheckpointDetails() } + { this.doneCheckpointDetails() } + { this.retryCheckpointDetails() } + ) + + const colorItYellow = (checkpointInfo.checkpointsByState.started !== null || checkpointCreatedAt !== "" ) && timeLimit !== null ? 'timed-checkpoint' : '' + + return ( +
    + { theDetails } + { this.checkpointDetailsButton() } +
    + ) + } +} diff --git a/scripts/app/javascript/components/content_files/checkpoints/CheckpointLanding.tsx b/scripts/app/javascript/components/content_files/checkpoints/CheckpointLanding.tsx new file mode 100644 index 0000000000000000000000000000000000000000..8106019475f1cf3ec5a882dd859f6e57904a4628 --- /dev/null +++ b/scripts/app/javascript/components/content_files/checkpoints/CheckpointLanding.tsx @@ -0,0 +1,159 @@ +import * as React from 'react' +import CheckpointLandingAttribute from './CheckpointLandingAttribute' +import CheckpointDetails from './CheckpointDetails' +import ActionMenus from '../ActionMenus' + +type Props = { + repoDetails: Api.RepoDetails + checkpointTitle: string + timeLimit: number | null | undefined + challengeCount: number + checkpointAttemptsRemaining: number | null + challengeTypes: string[] + checkpointInfo: Api.CheckpointInfo + instructorOrAdmin: boolean + isPreviewSandbox: boolean + githubUrl: string + permalink: string + enterAssessment: any + timesUpSubmitAssessment: any + checkpointCreatedAt: string + timeIsUp: boolean + cohortId: number + contentFileId: number + currentUserId: number + pairLocalStorageKey: string + studentsInCohort: Api.Student[] + pairs: object + setPairs: (pairs:any)=>void + showPairing: boolean + noAttemptsRemaining: boolean | undefined + assessmentStarted: boolean +} + +export default class CheckpointLanding extends React.Component { + + summary = (): string => { + let multipleChoice = false + let freeResponse = false + let coding = false + this.props.challengeTypes.forEach((challenge: string) => { + if (challenge === 'checkbox' || challenge === 'multiple-choice') { + multipleChoice = true + return + } + if (challenge === 'number' || challenge === 'short-answer' || challenge === 'paragraph') { + freeResponse = true + return + } + coding = true + }) + let typeStrings: string[] = [] + if (multipleChoice) { + typeStrings.push('multiple choice') + } + if (freeResponse) { + typeStrings.push('free-response') + } + if (coding) { + typeStrings.push('coding') + } + if (typeStrings.length == 3) { + return `${typeStrings[0]}, ${typeStrings[1]}, and ${typeStrings[2]}` + } + if (typeStrings.length == 2) { + return typeStrings.join(' and ') + } + return typeStrings[0] + } + + render() { + const { + checkpointTitle, + challengeCount, + checkpointAttemptsRemaining, + checkpointInfo, + enterAssessment, + timesUpSubmitAssessment, + timeLimit, + checkpointCreatedAt, + timeIsUp, + permalink, + cohortId, + contentFileId, + githubUrl, + repoDetails, + assessmentStarted, + showPairing, + instructorOrAdmin, + pairs, + studentsInCohort, + pairLocalStorageKey, + currentUserId + } = this.props + const remainingQuantity = checkpointAttemptsRemaining != null ? checkpointAttemptsRemaining : "Unlimited" + const remainingText = checkpointAttemptsRemaining != null ? `${window.pluralize("Attempt", checkpointAttemptsRemaining)} Remaining` : "Attempts Remaining" + + let timeText = "Time Limit" + let timerPath = "#ic_timer_24px" + if (timeLimit === null || timeLimit === undefined) { + timeText = "" + timerPath = "#ic_timer_off_24px"; + } + + let lockPath = "#ic_lock_open_24px" + if (checkpointAttemptsRemaining !== undefined && checkpointAttemptsRemaining === 0) { + lockPath = "#ic_lock_24px" + } + + return ( +
    + {this.props.instructorOrAdmin && this.props.isPreviewSandbox == false && ( + + )} +
    +
    {checkpointTitle}
    +
    + 1?'s':''}`} /> + + +
    +
    + {`This assessment contains ${this.summary()} challenges. `} + { challengeCount > 1 && (remainingQuantity == "Unlimited" || remainingQuantity > 0) && + `Answer as many questions as possible and submit the entire assessment at once when you’re done. ` + } + { (timeLimit !== null && timeLimit !== undefined) && (remainingQuantity == "Unlimited" || remainingQuantity > 0) && + `The timer will begin as soon as you click "Start Assessment" below.` + } +
    + +
    +
    + ) + } +} diff --git a/scripts/app/javascript/components/content_files/checkpoints/CheckpointLandingAttribute.tsx b/scripts/app/javascript/components/content_files/checkpoints/CheckpointLandingAttribute.tsx new file mode 100644 index 0000000000000000000000000000000000000000..35f96c2087003325ff55e859ba6f46502abde8fa --- /dev/null +++ b/scripts/app/javascript/components/content_files/checkpoints/CheckpointLandingAttribute.tsx @@ -0,0 +1,39 @@ +import React from 'react' +import Icon from '../../Icon' + +const CheckpointLandingAttribute = ({ + sprite, + spriteId, + quantity, + text, + time, + className, +} : { + sprite: string + spriteId: string + quantity: number | string | undefined + time?: number | null | string + text: string + className: string +}) => { + let quantityJsx + if (quantity !== null && quantity !== undefined) { + quantityJsx = (
    {quantity}
    ) + } else if (time !== null && time !== undefined && typeof(time) === "number") { + quantityJsx = (
    {window.fmtTime(time * 60 * 1000, false)}
    ) + } else { + quantityJsx = (
    No Time Limit
    ) + } + + return ( +
    +
    +
    + {quantityJsx} +
    {text}
    +
    +
    + ) +} + +export default CheckpointLandingAttribute diff --git a/scripts/app/javascript/components/content_files/checkpoints/CheckpointPairs.tsx b/scripts/app/javascript/components/content_files/checkpoints/CheckpointPairs.tsx new file mode 100644 index 0000000000000000000000000000000000000000..3dced3d23a2600c5596819f48dd1cf7ff183f445 --- /dev/null +++ b/scripts/app/javascript/components/content_files/checkpoints/CheckpointPairs.tsx @@ -0,0 +1,172 @@ +import * as React from 'react' +import SvgRenderer from '../../SvgRenderer' +import ActionMenu from '../../shared/ActionMenu/ActionMenu' +import Modal from '../../shared/Modal/Modal' +import { any } from 'prop-types' +import Button from '../../shared/Button/Button' + + +type Props = { + currentUserId: number + pairLocalStorageKey: string + studentsInCohort: Api.Student[] + setPairs: (pairs:any)=>void + useTextAction: boolean + assessmentStarted: boolean +} + +type State = { + pairs: any + setPairs: any + showPairModal: boolean + showActionMenu: boolean +} + +export default class CheckpointPairs extends React.Component { + state: State = { + pairs: any, + showPairModal: false, + showActionMenu: false, + setPairs: any + } + + componentDidMount() { + let savedPairs = localStorage.getItem(this.props.pairLocalStorageKey) + if (savedPairs !== null) { + this.setState({pairs: JSON.parse(savedPairs)}) + } else { + this.resetPairs() + } + } + + closeModal = () => { + this.setState({showPairModal:false}) + this.resetPairs() + } + + addClickListeners(handler: any) { + document.addEventListener('touchmove', handler, false); + document.addEventListener('click', handler, false); + } + + removeClickListeners(handler: any) { + document.removeEventListener('touchmove', handler, false); + document.removeEventListener('click', handler, false); + } + + handleMenuOutsideClick = (e: any) => { + if ($_('#action-menu-list', el => el.contains(e.target))) { + return; + } + + this.setState({showActionMenu: !this.state.showActionMenu}) + } + + get actionMenu() { + if (this.state.showActionMenu) { + this.addClickListeners(this.handleMenuOutsideClick); + if (Object.keys(this.state.pairs).length > 1){ + return ( + + ) + } else { + return ( + + ) + } + } else { + this.removeClickListeners(this.handleMenuOutsideClick); + } + } + + resetPairs = () => { + let pairClone = {} + let currentStudent = this.props.studentsInCohort.filter((s)=>{return s.id === this.props.currentUserId}) + pairClone[this.props.currentUserId] = currentStudent[0] + let savedPairs = localStorage.getItem(this.props.pairLocalStorageKey) + if (savedPairs !== null) localStorage.removeItem(this.props.pairLocalStorageKey) + this.setState({pairs: pairClone}) + this.props.setPairs(pairClone) + } + + togglePairMenu = (e:any) => { + e.preventDefault() + if (this.props.assessmentStarted === true) { + this.setState({showActionMenu: !this.state.showActionMenu}) + } else if (this.props.assessmentStarted === false) { + this.setState({showPairModal: !this.state.showPairModal}) + } + } + + showPairModal = () => { + this.setState({ + showPairModal: !this.state.showPairModal, + showActionMenu: !this.state.showActionMenu + }) + } + + studentList = () => { + return this.props.studentsInCohort.filter((s:Api.Student)=>{return s.id !== this.props.currentUserId}).map((s: Api.Student) => { + let isChecked = this.state.pairs[s.id] ? true : false + return ( +
    + + {s.full_name} +
    + ) + }) + } + + setPairs = () => { + localStorage.setItem(this.props.pairLocalStorageKey, JSON.stringify(this.state.pairs)) + this.setState({showPairModal: false}) + this.props.setPairs(this.state.pairs) + } + + updatePairs = (e: any) => { + let pairClone = this.state.pairs + if (e.target.checked) { + let student = this.props.studentsInCohort.filter((s)=>{return s.id === Number(e.target.value)}) + pairClone[e.target.value] = student[0] + } else { + delete pairClone[e.target.value] + } + this.setState({pairs: pairClone}) + } + + render() { + const { showPairModal, pairs } = this.state + const { assessmentStarted } = this.props + let action = this.props.useTextAction ? (Work with partner) : () + let startedAction = pairs && Object.keys(pairs).length > 1 ? () : + return ( + <> + { assessmentStarted === true && +
    + {action} + {this.actionMenu} +
    + // this is the toggle on the overflow buttons + } + { assessmentStarted === false && + startedAction + } + +

    Select Partner(s)

    + {this.studentList()} +
    + + ) + } +} \ No newline at end of file diff --git a/scripts/app/javascript/components/content_files/checkpoints/PairAvatars.tsx b/scripts/app/javascript/components/content_files/checkpoints/PairAvatars.tsx new file mode 100644 index 0000000000000000000000000000000000000000..fc78498601e66d961eaa183c82ac356ab6bdad42 --- /dev/null +++ b/scripts/app/javascript/components/content_files/checkpoints/PairAvatars.tsx @@ -0,0 +1,31 @@ +import React from 'react' +import UserAvatar from '../../UserAvatar' + +const PairAvatars= (pairs: object) => { + if (Object.keys(pairs).length > 1 && Object.keys(pairs).length < 4) { + return Object.values(pairs).map((s:Api.Student)=>{ + return ( + + ) + }) + } else if (Object.keys(pairs).length >= 4) { + let currentStudent = Object.values(pairs)[0] + return ( + <> + + +{Object.values(pairs).length - 1} + + ) + } + return null + } + + export default PairAvatars; \ No newline at end of file diff --git a/scripts/app/javascript/components/content_files/checkpoints/student_scores/CheckpointStudentRow.tsx b/scripts/app/javascript/components/content_files/checkpoints/student_scores/CheckpointStudentRow.tsx new file mode 100644 index 0000000000000000000000000000000000000000..c253f8e292c944caeaaa225f7474f7fd35120bba --- /dev/null +++ b/scripts/app/javascript/components/content_files/checkpoints/student_scores/CheckpointStudentRow.tsx @@ -0,0 +1,175 @@ +import React from 'react' +import UserAvatar from '../../../UserAvatar' +import { findThresholdColor } from '../../../shared/ProgressThresholdsSlider/ProgressThresholdsSlider' +import Icon from '../../../Icon' +import { cohortCheckpointSubmissionPath } from '../../../../generated/routes' + +type Props = { + student: Api.CheckpointStudentScore + selected: boolean + selectHandler: any + progressThresholds: number[] + cohortId: number + timeLimit: number | null +} + +export default class CheckpointStudentRow extends React.Component { + getStatus = () => { + const { student } = this.props + + if (student.has_signed_in === false) { + return never signed in + } + + switch(student.submissions.state) { + case "done": + case "retry": + return ( + + +
    scored
    +
    + ) + case "needs_review_and_done": + return ( + +
    +
    scored
    +
    + ) + case "needs_review": + return ( + +
    + needs review +
    + ) + case "started": + let time = null + if (this.props.timeLimit !== null) { + const timeMs = window.timeRemaining(this.props.timeLimit, student.submissions.created_at) + const {days, hours, minutes} = window.splitTime(timeMs); + if (timeMs < 0) { + time = 'live'; + } else if (days === 0 && hours === 0) { + time = `live: ${minutes}m` + } else if (days === 0 && hours === 1 && minutes === 0) { + time = `live: 1hr` + } else { + time = `live: 1hr+` + } + return ( + + +
    {time}
    +
    + ) + } else { + return ( + + +
    live
    +
    + ) + } + default: + return null + } + return null + } + + scoreBar = () => { + const { submissions } = this.props.student + let percentage = "0%" + let color = undefined + let checkpointSubmissionId = 0 + if (submissions.score !== null) { + percentage = `${submissions.score}%` + color = findThresholdColor(this.props.progressThresholds, submissions.score) + } else if (submissions.state == "needs_review") { + percentage = "100%" + color = "#eee" + } + + + return ( +
    +
    +
    + ) + } + + getStudentScore = () => { + const { submissions } = this.props.student + + switch(submissions.state) { + case "done": + case "retry": + case "needs_review_and_done": + return `${submissions.score}%` + case "needs_review": + if (submissions.score !== null) { + return `${submissions.score}%` + } + return '—' + default: + return null + } + } + + getScoreColumn = () => { + const { submissions } = this.props.student + + let id = 0 + if (submissions.state === "done") { + id = submissions.done_id + } else if (submissions.needs_review_id) { + id = submissions.needs_review_id + } + + if (id) { + return ( + +
    {this.getStudentScore()}
    + {this.scoreBar()} +
    + ) + } else { + return ( + +
    {this.getStudentScore()}
    + {this.scoreBar()} +
    + ) + } + } + + render(){ + const {student, selected, selectHandler} = this.props + let highlightClass = "" + if (student.has_signed_in === false) { + highlightClass = "never-signed-in" + } + if (selected) { + highlightClass = "row-selected" + } + + return( + + + + + + + +
    {student.first_name} {student.last_name}
    +
    + + + {this.getScoreColumn()} + + {this.getStatus()} + + ) + } +} diff --git a/scripts/app/javascript/components/content_files/checkpoints/student_scores/CheckpointStudentScores.tsx b/scripts/app/javascript/components/content_files/checkpoints/student_scores/CheckpointStudentScores.tsx new file mode 100644 index 0000000000000000000000000000000000000000..03dd19f55d9e230113ec71ca6af486f5542e8b09 --- /dev/null +++ b/scripts/app/javascript/components/content_files/checkpoints/student_scores/CheckpointStudentScores.tsx @@ -0,0 +1,187 @@ +import React from 'react' +import CheckpointStudentRow from './CheckpointStudentRow' +import http from '../../../../lib/http' +import ProgressThresholdsKey from '../../../shared/ProgressThresholdsKey/ProgressThresholdsKey' +import SettingsCog from '../../../shared/Icons/SettingsCog' +import ProgressThresholdsModal from '../../../shared/ProgressThresholdsModal/ProgressThresholdsModal' + +type Props = { + studentScoresEndpoint: string + progressThresholds: number[] + cohortId: number + cohortMode: string + loadStudentScores: boolean + timeLimit: number | null +} + +type State = { + students: any + selected: any + progressThresholds: number[] + showModal: boolean +} + +export default class CheckpointStudentScores extends React.Component { + + state: State = { + progressThresholds: this.props.progressThresholds, + students: [], + selected: {}, + showModal: false + } + + componentDidMount() { + this.fetchStudentScoresData() + } + + fetchStudentScoresData = async ()=> { + if (this.props.loadStudentScores) { + const students = await http('GET', this.props.studentScoresEndpoint, {}) + let selected = {} + students.forEach((student:Api.CheckpointStudentScore) => { selected[student.id] = false }) + this.setState({students: this.sort(students), selected: selected}) + } + } + + sort = (students:Api.CheckpointStudentScore[]) => { + let neverSignedIn:Api.CheckpointStudentScore[] = [] + let neverAttempted:Api.CheckpointStudentScore[] = [] + let started:Api.CheckpointStudentScore[] = [] + let needsReview:Api.CheckpointStudentScore[] = [] + let scored:Api.CheckpointStudentScore[] = [] + + students.forEach((student:any) => { + if (student.has_signed_in === false) { + neverSignedIn.push(student) + } else if (student.submissions.state === "started") { + if (student.submissions.done_id) { + scored.push(student) + } else if (student.submissions.needs_review_id) { + needsReview.push(student) + } else { + started.push(student) + } + } else if (student.submissions.state === "needs_review") { + needsReview.push(student) + } else if (student.submissions.state === "needs_review_and_done") { + scored.push(student) + } else if (student.submissions.state === "done") { + scored.push(student) + } else { + neverAttempted.push(student) + } + }) + + neverSignedIn.sort((a:any, b:any) => { + if (a.first_name.toUpperCase() < b.first_name.toUpperCase()) return -1 + if (a.first_name.toUpperCase() > b.first_name.toUpperCase()) return 1 + return 0 + }) + neverAttempted.sort((a:any, b:any) => { + if (a.first_name.toUpperCase() < b.first_name.toUpperCase()) return -1 + if (a.first_name.toUpperCase() > b.first_name.toUpperCase()) return 1 + return 0 + }) + started.sort((a:any, b:any) => { + if (a.first_name.toUpperCase() < b.first_name.toUpperCase()) return -1 + if (a.first_name.toUpperCase() > b.first_name.toUpperCase()) return 1 + return 0 + }) + needsReview.sort((a:any, b:any) => { + if (a.submissions.score < b.submissions.score) return 1 + if (a.submissions.score > b.submissions.score) return -1 + if (a.first_name.toUpperCase() < b.first_name.toUpperCase()) return -1 + if (a.first_name.toUpperCase() > b.first_name.toUpperCase()) return 1 + return 0 + }) + scored.sort((a:any, b:any) => { + if (a.submissions.score < b.submissions.score) return 1 + if (a.submissions.score > b.submissions.score) return -1 + return 0 + }) + + return [needsReview, scored, started, neverAttempted, neverSignedIn].flat() + } + + selectHandler = (e:any) => { + let selectedCopy = this.state.selected + selectedCopy[e.target.id] = e.target.checked + this.setState({selected: selectedCopy}) + } + + studentRows = () => { + return this.state.students.map((student:any,i:number) => { + return + }) + } + + selectAllRows = (e:any) => { + let selectedCopy = this.state.selected + for( const key in selectedCopy) selectedCopy[key] = e.target.checked + this.setState({selected: selectedCopy}) + } + + toggleModal = () => { + this.setState(prevState => ({ + ...prevState, + showModal: !prevState.showModal, + })); + } + + updateProgressThresholds = (progressThresholds: number[]) => { + this.setState({ progressThresholds }); + } + + render() { + const hasCheckedRows = Object.values(this.state.selected).indexOf(true) > -1 + return ( +
    +
    +
    +

    Results

    + {this.props.cohortMode === 'Percentage' && ( + + )} + {this.props.cohortMode === 'Percentage' && ( +
    + +
    + +
    +
    + )} +
    + + + + + + + + + + + {this.studentRows()} + +
    + + StudentScoreStatus
    +
    +
    + ) + } +} diff --git a/scripts/app/javascript/components/curriculum/CheckpointSummary.tsx b/scripts/app/javascript/components/curriculum/CheckpointSummary.tsx new file mode 100644 index 0000000000000000000000000000000000000000..fc064c01172eb8cdb3108fe38942bc424d9e0bdf --- /dev/null +++ b/scripts/app/javascript/components/curriculum/CheckpointSummary.tsx @@ -0,0 +1,36 @@ +import * as React from 'react' + +type Props = { + checkpointAvg: number + cohortHasCheckpoints: boolean + userHasCheckpoints: boolean +} + +const CheckpointSummary = ({ checkpointAvg, cohortHasCheckpoints, userHasCheckpoints }: Props) => { + if (cohortHasCheckpoints === false) return null + + const avg = Math.floor(checkpointAvg); + const showCheckpointAverage = userHasCheckpoints && checkpointAvg !== null; + + return ( +
    +
    +
    + {showCheckpointAverage ? `${avg}%` : '—'} +
    + {showCheckpointAverage && +
    + {[100, 80, 60, 40, 20].map((indicator, i) => ( +
    = indicator && 'fill'}`} /> + ))} +
    + } +
    +
    + Checkpoint Avg +
    +
    + ); +} + +export default CheckpointSummary diff --git a/scripts/app/javascript/components/curriculum/CohortCurriculum.tsx b/scripts/app/javascript/components/curriculum/CohortCurriculum.tsx new file mode 100644 index 0000000000000000000000000000000000000000..02bc114f04e1f4975002fa0bfadc5f6c6d49a927 --- /dev/null +++ b/scripts/app/javascript/components/curriculum/CohortCurriculum.tsx @@ -0,0 +1,186 @@ +import * as React from 'react' +import Slideshow from '../shared/Slideshow/Slideshow' +import Icon from '../Icon'; +import StandardBeans from './StandardBeans'; +import StandardsRenderer from './StandardsRenderer'; + +type Props = { + cohortMode: 'Percentage' | 'Mastery' + isSandbox: boolean + allCompleteImagePath: string + awaitingGradeImagePath: string + selectedStandardTitles: null | string[] + sections: Api.CurriculumPage.Section[] + cohortId: number + warningMessage: string + welcomeToLearn: boolean + lastContentView: any +} + +type State = { + cohortId: number + open: OpenState + standardBeans: any + emptySections: Array +} + +type OpenState = Record + +export default class CohortCurriculum extends React.Component { + constructor(props: Props) { + super(props); + + this.state = { + cohortId: this.props.cohortId, + open: {}, + standardBeans: {}, + emptySections: [] + }; + + window.addEventListener('beforeunload', this.saveState); + } + + componentDidMount() { + const openStateKey = getOpenStateKey(this.state.cohortId); + const openState: OpenState = JSON.parse(window.localStorage.getItem(openStateKey) || '{}'); + + this.setState({ open: openState }); + } + + saveState = () => { + const openStateKey = getOpenStateKey(this.state.cohortId); + window.localStorage.setItem(openStateKey, JSON.stringify(this.state.open)); + } + + toggleBlockVisibility = (section: any) => { + this.setState(prevState => ({ + ...prevState, + open: { + ...prevState.open, + [section.id]: !prevState.open[section.id], + } + })) + } + + isOpen(release_id: number) { + if (!this.state.open.hasOwnProperty(release_id)) return true + return this.state.open[release_id] + } + + setStandardBeans = (id: any, beans: any) => { + const clonedStandardBeans = this.state.standardBeans + clonedStandardBeans[id] = beans + this.setState({ standardBeans: clonedStandardBeans }) + } + + hideSection = (id: number) => { + this.setState(prevState => ({ + ...prevState, + emptySections: [...prevState.emptySections, id], + })) + } + + render() { + const { + warningMessage, + welcomeToLearn, + allCompleteImagePath, + awaitingGradeImagePath, + cohortMode, + lastContentView, + isSandbox, + } = this.props + + if (welcomeToLearn) { + return ( +
    + +
    + ) + } + + const sectionIds = this.props.sections.map(section => section.id) + + if (warningMessage !== '' || sectionIds.length === this.state.emptySections.length) { + return ( +
    +
    + {warningMessage === '' ? ( +
    It looks the curriculum is not visible yet.
    + ) : ( +
    + )} +
    +
    + ) + } + + return ( + <> + {isSandbox == false && !lastContentView.is_first_view && + + } +
    + {this.props.sections.map((section) => { + if (this.state.emptySections.indexOf(section.id) > -1 || section.standard_count === 0) return; + return ( +
    +
    +
    + this.toggleBlockVisibility(section)}> + + + {section.title} + +
    +
    + +
    + ) + })} +
    + + ); + } +} + +function getOpenStateKey (cohortId: number) { + return `${cohortId}_curriculum_state_3.0` +} diff --git a/scripts/app/javascript/components/curriculum/CurriculumStandardCard.tsx b/scripts/app/javascript/components/curriculum/CurriculumStandardCard.tsx new file mode 100644 index 0000000000000000000000000000000000000000..e79614c243df6b682afd13ecdacd424529d5dd87 --- /dev/null +++ b/scripts/app/javascript/components/curriculum/CurriculumStandardCard.tsx @@ -0,0 +1,126 @@ +import * as React from 'react' +import SvgRenderer from '../SvgRenderer' +import MasteryProgressBar from '../shared/MasteryProgressBar/MasteryProgressBar' +import PercentageProgressBar from '../shared/PercentageProgressBar/PercentageProgressBar' + +const CurriculumStandardCard = ({ + standard, + awaitingGradeImagePath, + allCompleteImagePath, + cohortMode, + checkpointSubmission, +} : { + standard: any, + awaitingGradeImagePath: string, + allCompleteImagePath: string, + cohortMode: 'Percentage' | 'Mastery', + checkpointSubmission: any +}) => { + const isAwaitingGrade = (checkpointSubmission && checkpointSubmission.state == 'needs_review') + let completedClass = ( + standard.mastery_score === 3 + || isAwaitingGrade + || standard.is_completed + ) ? '-completed-standard' : ''; + + if (cohortMode === 'Mastery') { + return ( + +
    {standard.title}
    +

    {standard.description}

    +
    + +
    +
    + ); + } + + let marginLeft = '85%'; + if ( + checkpointSubmission + && ( + checkpointSubmission.state === 'done' + || ( + checkpointSubmission.hasOwnProperty('last_performance_percent') + && checkpointSubmission.last_performance_percent != null + ) + ) + ) { + marginLeft = '75%'; + } + + const dataWithCheckpointSubmissions = standard.cohort_standard_progress_data.find((data: any) => data.checkpoint_submissions) + if (!dataWithCheckpointSubmissions || !checkpointSubmission) { // Standard has no checkpoint or has a checkpoint but it has never been submitted + const hStyle = standard.is_completed ? { overflow: 'hidden', textOverflow: 'ellipsis', width: '210px', whiteSpace: 'nowrap' as 'nowrap' } : {} + return ( + +
    {standard.title}
    +

    {standard.description}

    +
    + +
    +
    + ) + } + + return ( + +
    + {standard.title} +
    +

    + {standard.description} +

    +
    + {checkpointSubmission && (checkpointSubmission.state === 'needs_review' || checkpointSubmission.state === 'retry') && ( + + )} + {checkpointSubmission + && checkpointSubmission.state === 'done' + && ( + + ) + } + {checkpointSubmission + && checkpointSubmission.hasOwnProperty('last_performance_percent') + && checkpointSubmission.last_performance_percent != null + && ( + + {`${checkpointSubmission.last_performance_percent}%`} + + ) + } +
    +
    + ) +} + +export default CurriculumStandardCard; diff --git a/scripts/app/javascript/components/curriculum/ProgressIndicators.tsx b/scripts/app/javascript/components/curriculum/ProgressIndicators.tsx new file mode 100644 index 0000000000000000000000000000000000000000..05b50a65824b295d68b1d3e6727e0a3eccb14a92 --- /dev/null +++ b/scripts/app/javascript/components/curriculum/ProgressIndicators.tsx @@ -0,0 +1,134 @@ +import * as React from 'react' +import StudentOverallProgressDoughnut from './StudentOverallProgressDoughnut'; +import CheckpointSummary from './CheckpointSummary'; +import http from '../../lib/http' + +type Props = { + cohortMode: string + curriculumProgressPath: string + checkpointAvg: number + showMasteryAverage: boolean + cohortId: number +} + +type State = { + progressData: any | null + checkpointAvg: number + cohortUsesCheckpoints: boolean + userHasCheckpoints: boolean +} + +export default class ProgressIndicators extends React.Component { + state: State = { progressData: null, checkpointAvg: 0, cohortUsesCheckpoints: false, userHasCheckpoints: false } + + componentDidMount() { + http('GET', this.props.curriculumProgressPath) + .then((data: any) => { + this.setState({ + progressData: JSON.parse(data.progress), + checkpointAvg: data.checkpoint_average, + cohortUsesCheckpoints: data.cohort_uses_checkpoints, + userHasCheckpoints: data.user_has_checkpoints + }) + }) + } + + componentDidUpdate(_: Props, prevState: State) { + if ( + localStorage.getItem('ds_basic_prep') === null + && this.props.cohortId === 868 + && !prevState.cohortUsesCheckpoints + && this.state.cohortUsesCheckpoints + ) { + let tour = { + id: "welcome_tour", + i18n: { + stepNums:["", "", ""] + }, + steps: [ + { + target: document.querySelectorAll(".donutscore")[0], + placement: "left", + title: "Hello, we've made some changes here", + content: "We’ve changed the way course progress on this page is calculated. You’ll now see your progress increase every time you complete a Unit, rather than a Lesson. You might notice a change in your percent progress, but don’t worry! None of your work has been lost.", + yOffset: -10 + }, + { + target: document.querySelectorAll(".checkpoint-summary-wrapper")[0], + placement: "left", + title: "Checkpoint Average", + content: "You’ll also see your average Checkpoint score appear in the top right of this page, next to Progress.", + }, + { + target: document.querySelectorAll(".standardscount")[0], + placement: "right", + title: "Checkpoint Score", + content: "You’ll now see your score from each completed Checkpoint displayed on its corresponding unit.", + yOffset: 65, + xOffset: 175 + } + ] + }; + window.hopscotch.startTour(tour) + localStorage.setItem('ds_basic_prep', 'seent it') + } else if ( + localStorage.getItem('se_basic_prep') === null + && this.props.cohortId === 888 + && !prevState.progressData && this.state.progressData + ) { + + let tour = { + id: "welcome_tour", + i18n: { + stepNums:[""] + }, + steps: [ + { + target: document.querySelectorAll(".donutscore")[0], + placement: "left", + title: "Hello, we've made some changes here.", + content: "We’ve changed the way course progress on this page is calculated. You’ll now see your progress increase every time you complete a Unit, rather than a Lesson. You might notice a change in your percent progress, but don’t worry! None of your work has been lost.", + xOffset: -30, + yOffset: -10 + } + ] + }; + + window.hopscotch.startTour(tour) + localStorage.setItem('se_basic_prep', 'seent it') + } + } + + render() { + return ( +
    + {this.props.cohortMode == "Percentage" && ( +
    +
    + {this.state.cohortUsesCheckpoints && + <>Checkpoint Avg = average on all scored checkpoints.
    + } + {this.state.progressData !== null && + `Percent Complete = % of visible units completed.` + } +
    +
    + )} + {this.state.cohortUsesCheckpoints && this.props.cohortMode == "Percentage" && + + } + {this.state.progressData !== null && ( + + )} +
    + ); + } +} diff --git a/scripts/app/javascript/components/curriculum/StandardBeans.tsx b/scripts/app/javascript/components/curriculum/StandardBeans.tsx new file mode 100644 index 0000000000000000000000000000000000000000..47bbbd2578523089e7e9dde464cace6d598881e2 --- /dev/null +++ b/scripts/app/javascript/components/curriculum/StandardBeans.tsx @@ -0,0 +1,28 @@ +import * as React from 'react' +import StandardBean from '../StandardBean' + +type Props = { + standardPresenters: any[] +} + +export default class StandardBeans extends React.Component { + render() { + if (this.props.standardPresenters.length > 0) { + let cells = this.props.standardPresenters.map((standardsPresenter, index) => { + return (); + }); + + return ( +
    +
    {cells}
    +
    + ); + } else { + return ( +
    +
    Loading..
    +
    + ) + } + } +} diff --git a/scripts/app/javascript/components/curriculum/StandardsRenderer.tsx b/scripts/app/javascript/components/curriculum/StandardsRenderer.tsx new file mode 100644 index 0000000000000000000000000000000000000000..15bdb836ece70dee7b815aa474173ee6351f2a49 --- /dev/null +++ b/scripts/app/javascript/components/curriculum/StandardsRenderer.tsx @@ -0,0 +1,123 @@ +import * as React from 'react' +import http from '../../lib/http' +import CurriculumStandardCard from './CurriculumStandardCard'; +import { checkpointSubmission } from '../../lib/utils'; +import ProgressBar from '../shared/ProgressBar/ProgressBar'; +import { isThisSecond } from 'date-fns'; + +type Props = { + sectionId: number + standardsCount: number + setStandardBeans: any + standardsForSectionPath: string | null + isOpen: any + allCompleteImagePath: string + awaitingGradeImagePath: string + cohortMode: "Mastery" | "Percentage" + hideSection: any +} + +type State = { + standards: any + fetching: boolean +} + +export default class StandardsRenderer extends React.Component { + state: State = { standards: [], fetching: false } + + private standardsWrapper: any + + componentDidMount() { + if (this.standardsWrapper) { + window.addEventListener('scroll', this.handleScroll); + this.handleScroll() + } + } + + componentWillUnmount() { + if (this.standardsWrapper) { + window.removeEventListener('scroll', this.handleScroll); + } + } + + handleScroll = ()=>{ + const targetY = this.standardsWrapper.getBoundingClientRect().y; + + if (targetY < (window.innerHeight - 200) && !this.state.fetching) { + this.loadStandards() + } + } + + setStandardsWrapperRef = (el: React.ReactNode) => { + this.standardsWrapper = el; + } + + loadStandards = () => { + if (this.props.standardsForSectionPath && !this.state.fetching) { + this.setState({fetching: true}) + http('GET', this.props.standardsForSectionPath) + .then((standards: any) => { + var visibleStandards = standards.map((standard:any) => { + return standard.filter((standard:any) => standard.cohort_standard_progress_data.length > 0) + }).flat() + if (visibleStandards.length === 0) { + this.props.hideSection(this.props.sectionId) + } + this.setState({standards: visibleStandards}) + this.props.setStandardBeans(this.props.sectionId, visibleStandards) + }) + } + } + + render() { + let standards + + if (this.state.standards.length > 0) { + standards = this.state.standards.map((std:any) => { + return ( + + ) + }) + } else { + standards = Array.from(Array(this.props.standardsCount)).map((_, i) => { + return ( +
    +
    title
    +

    Lorem ipsum dolor sit amet consectetur adipisicing elit. Molestias laborum eveniet quaerat, harum laboriosam dolor! Quam maiores inventore recusandae, eaque vitae atque natus consectetur at voluptatum autem, reprehenderit, pariatur ipsam.

    +
    + Loading

    } + bar={( +
    +
    +
    +
    + )} + standardTitle="test" + sideBar={false} + masteryCohort={this.props.cohortMode == "Mastery"} + /> +
    +
    + ) + }) + } + + return ( + <> +
    + { standards } +
    + + ); + } +} diff --git a/scripts/app/javascript/components/curriculum/StudentOverallProgressDoughnut.tsx b/scripts/app/javascript/components/curriculum/StudentOverallProgressDoughnut.tsx new file mode 100644 index 0000000000000000000000000000000000000000..50e028482b8edfd7c04832ff10662e26b726ae2e --- /dev/null +++ b/scripts/app/javascript/components/curriculum/StudentOverallProgressDoughnut.tsx @@ -0,0 +1,105 @@ +import * as React from 'react' + +type Props = { + cohortMode: string + showMasteryAverage: boolean + progressData: any +} + +export default class StudentOverallProgressDoughnut extends React.Component { + buildMasteryDoughnut = () => { + if (this.props.progressData == null) return null; + + let threes = this.props.progressData['percent_threes'] + let threesCircle = this.props.progressData['score_3_dasharray'] + let twos = this.props.progressData['percent_twos'] + let twosCircle = this.props.progressData['score_2_dasharray'] + let twosCircleOffset = this.props.progressData['score_2_dashoffset'] + let ones = this.props.progressData['percent_ones'] + let onesCircle = this.props.progressData['score_1_dasharray'] + let onesCircleOffset = this.props.progressData['score_1_dashoffset'] + let nones = this.props.progressData['percent_unscored'] + + return ( +
    + {this.props.showMasteryAverage && +
    +
    {this.props.progressData['mastery_average']}
    +
    AVG
    +
    + } +
    +
    + {threes} + % +
    + + + {threes > 0 && + + } + {twos > 0 && + + } + {ones > 0 && + + } + +
    +
    +
    +
    + {threes}% +
    +
    +
    + {twos}% +
    +
    +
    + {ones}% +
    +
    +
    + {nones}% +
    +
    +
    + ) + } + + buildPercentageDoughnut = () => { + if (this.props.progressData == null) return null; + + let percentCompleted = this.props.progressData['percent_completed'] + let percentCompletedCircle = this.props.progressData['percent_completed_dasharray'] + + let donut + if(percentCompleted > 0) { + donut = ( + + + + + ) + } + + return ( +
    +
    +
    0 ? {} : {marginBottom: "10px"})}> + {percentCompleted > 0 ? percentCompleted+'%' : '—'} +
    + {donut} +
    +
    Course Progress
    +
    + ) + } + + render() { + let doughnut = this.props.cohortMode == "Mastery" ? this.buildMasteryDoughnut() : this.buildPercentageDoughnut() + + return doughnut + } +} diff --git a/scripts/app/javascript/components/lib/ace.ts b/scripts/app/javascript/components/lib/ace.ts new file mode 100644 index 0000000000000000000000000000000000000000..b6bc35ece12b258c3e3a893ef55cb2295fff1440 --- /dev/null +++ b/scripts/app/javascript/components/lib/ace.ts @@ -0,0 +1,92 @@ +import {each} from 'lodash-es' +var Dropzone = require('dropzone'); + +const ace = require('brace'); + +export default ace + +require('brace/theme/solarized_dark.js'); +require('brace/theme/tomorrow_night.js'); +// Whitelisting specific highlighter languages +require('brace/mode/csharp.js'); +require('brace/mode/html.js'); +require('brace/mode/java.js'); +require('brace/mode/javascript.js'); +require('brace/mode/json.js'); +require('brace/mode/markdown.js'); +require('brace/mode/python.js'); +require('brace/mode/ruby.js'); +require('brace/mode/sql.js'); +require('brace/mode/c_cpp.js'); + +var Editor = { + attachTo: function(selector: string) { + each(document.querySelectorAll(selector), function(elem, index) { + var original = elem as HTMLElement; + var newTextarea = document.createElement('textarea'); + newTextarea.id = original.id; + newTextarea.setAttribute('name', original.getAttribute('name')!); + var parentDiv = original.parentElement!; + parentDiv.setAttribute('id', `ace-editor-${index}`); + parentDiv.appendChild(newTextarea); + + var data = original.dataset; + var editor = ace.edit(original); + + editor.setOptions({ + showLineNumbers: true, + mode: `ace/mode/${data.editor}`, + theme: 'ace/theme/solarized_dark', + highlightActiveLine: true, + behavioursEnabled: true, + wrapBehavioursEnabled: true + }); + editor.$blockScrolling = Infinity; + editor.setValue(data.text || original.getAttribute('placeholder') || ''); + editor.commands.addCommand({ + name: 'replace', + bindKey: { win: 'F10', mac: 'F10' }, + exec: function(editor: any) { + (parentDiv as any).webkitRequestFullscreen(); + } + }); + + newTextarea.value = editor.getSession().getValue(); + + editor.getSession().on('change', function() { + newTextarea.value = editor.getSession().getValue(); + }); + + if (data.inlineUpload) { + var csrfToken = document.querySelector('meta[name=csrf-token]')!.getAttribute('content'); + var options = { + url: data.uploadUrl || '/uploaded_assets', + paramName: 'asset[file]', + createImageThumbnails: false, + headers: { + 'X-CSRF-Token': csrfToken, + 'Accept': 'application/json' + }, + params: { + 'asset[related_object_type]': data.relatedObjectType, + 'asset[related_object_id]': data.relatedObjectId + }, + addedfile: function() { + editor.setValue('Uploading.....'); + }, + success: function(file: any, response: any) { + editor.setValue(`![${file.name}](${response.filename})`); + }, + error: function(file: any, response: any) { + editor.setValue('Something went wrong with the upload.'); + } + }; + + new Dropzone(`#ace-editor-${index}`, options); + } + }); + } +}; +window.onDocumentReady(function() { + Editor.attachTo('[data-editor]'); +}); diff --git a/scripts/app/javascript/components/notifications/NotificationsIcon.tsx b/scripts/app/javascript/components/notifications/NotificationsIcon.tsx new file mode 100644 index 0000000000000000000000000000000000000000..1d1aae811856863cacbd3853de0c416e42cf5543 --- /dev/null +++ b/scripts/app/javascript/components/notifications/NotificationsIcon.tsx @@ -0,0 +1,26 @@ +import * as React from 'react' +import SvgRenderer from '../SvgRenderer' + +type Props = { + unreadCount: number + clickHandler: React.EventHandler // 'never' force ignores argument +} + +export default class NotificationsIcon extends React.Component { + render() { + var notificationsCount; + + if (this.props.unreadCount > 0) { + notificationsCount = ( +
    {this.props.unreadCount}
    + ); + } + + return ( +
    + + {notificationsCount} +
    + ); + } +} diff --git a/scripts/app/javascript/components/shared/ActionMenu/ActionMenu.tsx b/scripts/app/javascript/components/shared/ActionMenu/ActionMenu.tsx new file mode 100644 index 0000000000000000000000000000000000000000..e63a41251d6a35a06432ec98a19704ba11750366 --- /dev/null +++ b/scripts/app/javascript/components/shared/ActionMenu/ActionMenu.tsx @@ -0,0 +1,32 @@ +import * as React from 'react' + +type ListItem = { + func?: () => void + text: string + href?: string +} + +const ActionMenu= ({ + className, + id, + listItems +} : { + className?: string + id?: string + listItems: ListItem[] +}) => { + return ( +
    +
      + {listItems.map((item) => { return ( +
    • + {item.text} +
    • + )} + )} +
    +
    + ) +} + +export default ActionMenu diff --git a/scripts/app/javascript/components/shared/Button/Button.tsx b/scripts/app/javascript/components/shared/Button/Button.tsx new file mode 100644 index 0000000000000000000000000000000000000000..27b98c9e0b165fa9af94fd844f1cb418b06a5f66 --- /dev/null +++ b/scripts/app/javascript/components/shared/Button/Button.tsx @@ -0,0 +1,37 @@ +import React from 'react'; + +type Props = { + format: 'solid' | 'outline' + color: 'primary' | 'red' + style: any + onClick: any + disabled: boolean + text: string + children: React.ReactNode + className?: string +} + +export default Button = ({ format, color, style, onClick, disabled, text, children, className }: Props) => { + let classes = `button-wrapper button-${format}-${color} ${(className ? className : '')}`; + return ( + + ) +}; + +Button.defaultProps = { + format: 'solid', + color: 'primary', + style: null, + onClick: null, + disabled: false, + text: null, + children: null, + className: null, +}; diff --git a/scripts/app/javascript/components/shared/ChallengePoints/ChallengePoints.tsx b/scripts/app/javascript/components/shared/ChallengePoints/ChallengePoints.tsx new file mode 100644 index 0000000000000000000000000000000000000000..2b6af2c6fe328046a4257161dcd6dfb4a028ec9d --- /dev/null +++ b/scripts/app/javascript/components/shared/ChallengePoints/ChallengePoints.tsx @@ -0,0 +1,123 @@ +import React, { Component } from 'react' +import ColorDonut from '../DonutRing/components/ColorDonut/ColorDonut' +import SvgRenderer from '../../SvgRenderer' +import IntegerPicker from './components/IntegerPicker/IntegerPicker' + +type Props = { + readOnly: boolean + manuallyGradeChallenge: (submission:any, status: Api.SubmitedChallengeAnswerStatus, points?: number) => void + showTimestamp?: (showTimestamp: boolean) => void + maxPoints: number + points: number + submission: any +} + +type State = { + submissionStatus: string + points: number + maxPoints: number + showDropdown: boolean +} + +export default class ChallengePoints extends Component { + state = { + submissionStatus: this.props.submission.status, + points: this.props.points, + showDropdown: false, + maxPoints: this.props.maxPoints + } + + componentDidUpdate(prevProps: Props, _: State) { + if (prevProps.points !== this.props.points || prevProps.maxPoints !== this.props.maxPoints || prevProps.submission.status !== this.props.submission.status) { + this.setState({ + submissionStatus: this.props.submission.status, + points: this.props.points, + maxPoints: this.props.maxPoints, + }) + } + } + + toggleDropdown = (showDropdown: boolean) => { + if (this.props.showTimestamp) this.props.showTimestamp(!showDropdown); + + this.setState({ showDropdown }) + } + + gradeChallenge = (integer: number) => { + const status:Api.SubmitedChallengeAnswerStatus = integer > 0 ? 'correct' : 'incorrect'; + + if (this.props.showTimestamp) this.props.showTimestamp(true); + this.setState({ showDropdown: false, points: integer, submissionStatus: status }); + this.props.manuallyGradeChallenge(this.props.submission, status, integer); + } + + renderGradeSvg = () => { + const { maxPoints } = this.props + const { points, submissionStatus, showDropdown } = this.state + + let svg: null | JSX.Element + if (maxPoints === points && submissionStatus === 'correct') { + svg = ( + + ) + } else if (points === 0) { + svg = ( + + ) + } else { + svg = ( + + ) + } + return ( + <> + {showDropdown ? null : svg} + {showDropdown && ( + + )} + + ) + } + + render() { + const { readOnly } = this.props + const { submissionStatus, points, maxPoints } = this.state + const graded = submissionStatus === 'correct' || submissionStatus === 'incorrect' + + return ( +
    this.toggleDropdown(true) : undefined} + onMouseLeave={graded && !readOnly ? () => this.toggleDropdown(false) : undefined} + > + {graded ? this.renderGradeSvg() : ( + + )} +
    + {graded && {points}/{maxPoints}} pts +
    +
    + ) + } +} diff --git a/scripts/app/javascript/components/shared/ChallengePoints/PartialCreditBaton.tsx b/scripts/app/javascript/components/shared/ChallengePoints/PartialCreditBaton.tsx new file mode 100644 index 0000000000000000000000000000000000000000..e487a414855b0bb8ca1b0e030bdecd3eaf72ec88 --- /dev/null +++ b/scripts/app/javascript/components/shared/ChallengePoints/PartialCreditBaton.tsx @@ -0,0 +1,18 @@ +import * as React from 'react' + +type Props = { + correctPoints: number + totalPoints: number +} + +export default (props: Props) => { + let correctStroke = Math.floor( props.correctPoints / props.totalPoints * 20 ) + let incorrectStroke = 20 - correctStroke + + return ( + + + + + ); +}; diff --git a/scripts/app/javascript/components/shared/ChallengePoints/SpinText.tsx b/scripts/app/javascript/components/shared/ChallengePoints/SpinText.tsx new file mode 100644 index 0000000000000000000000000000000000000000..c6cf39d542325d66d8c6f7093e114fde70ccf84c --- /dev/null +++ b/scripts/app/javascript/components/shared/ChallengePoints/SpinText.tsx @@ -0,0 +1,79 @@ +import React, { Component } from 'react' + +type Props = { + text: string +} +type State = { + colors: Color[] + hyp: number + speed: number + blur: number + intervalId: any +} + +type Color = { + x: number + y: number + r: string + g: string + b: string + angle: number +} + +export default class SpinText extends Component { + constructor(props: Props) { + super(props) + this.state = { + colors: [ + { x: 0, y: 0, r: '9b', g: '9b', b: '9b', angle: 5*Math.PI/6 }, + { x: 0, y: 0, r: '9b', g: '9b', b: '9b', angle: Math.PI/6 }, + { x: 0, y: 0, r: '9b', g: '9b', b: '9b', angle: 3*Math.PI/6 } + ], + hyp: 3, + speed: 0.15, + blur: 3, + intervalId: 0 + } + } + + componentDidMount() { + var intervalId = setInterval(this.rotate, 80); + // store intervalId in the state so it can be accessed later: + this.setState({intervalId: intervalId}); + } + + componentWillUnmount() { + // use intervalId from the state to clear the interval + clearInterval(this.state.intervalId); + } + + rotate = () => { + let colors = [...this.state.colors]; + const updateColors = this.state.colors.map(color => this.angleCalc(color) ) + this.setState({colors: colors}) + } + + angleCalc = (color: Color) => { + let { hyp, speed } = this.state; + let xOut = Math.cos(color.angle)*hyp; + let yOut = Math.sin(color.angle)*hyp; + color.angle += speed; + color.x = Number(xOut.toFixed(2)); + color.y = Number(yOut.toFixed(2)); + return color + } + + styles = () => { + let { blur } = this.state + const styles = this.state.colors.map((c) => { + return `${c.x}px ${c.y}px ${blur}px #${c.r}${c.g}${c.b}` + }).join(', '); + return { "textShadow": styles } + } + + render() { + return ( + {this.props.text} + ) + } +} diff --git a/scripts/app/javascript/components/shared/ChallengePoints/components/IntegerPicker/IntegerPicker.tsx b/scripts/app/javascript/components/shared/ChallengePoints/components/IntegerPicker/IntegerPicker.tsx new file mode 100644 index 0000000000000000000000000000000000000000..71be5591aa188b47e886a215b404b09707026cb7 --- /dev/null +++ b/scripts/app/javascript/components/shared/ChallengePoints/components/IntegerPicker/IntegerPicker.tsx @@ -0,0 +1,29 @@ +import React from 'react' + +const IntegerPicker = ({ + maxInteger, + onIntegerClick, + vertical, +} : { + maxInteger: number, + onIntegerClick: (integer: number) => void, + vertical?: boolean, +}) => ( +
    + {[...Array(maxInteger + 1).fill({})].map((_, i) => { + const integer: any = i; + + return ( +
    onIntegerClick(integer) : undefined} + className="integer" + > + {integer} +
    + ) + })} +
    +); + +export default IntegerPicker \ No newline at end of file diff --git a/scripts/app/javascript/components/shared/DonutRing/DonutRing.tsx b/scripts/app/javascript/components/shared/DonutRing/DonutRing.tsx new file mode 100644 index 0000000000000000000000000000000000000000..f49dc6d19d29a5c06563ca4f8c77d68ffe5c3bab --- /dev/null +++ b/scripts/app/javascript/components/shared/DonutRing/DonutRing.tsx @@ -0,0 +1,177 @@ +import React, { Component } from 'react' +import SvgRenderer from '../../SvgRenderer' +import { map, isEqual } from 'lodash-es' +import UngradedDonut from '../UngradedDonut/UngradedDonut' + +type Props = { + total: number + answerCounts: { + correct: number + incorrect: number + ungraded: number + } + theme: 'dark' | 'light' + checkpointIcon?: string + checkFill?: string + standardPercentageMode?: boolean +} + +export default class DonutRing extends Component { + shouldComponentUpdate(nextProps: any) { + return ! isEqual(this.props.answerCounts, nextProps.answerCounts); + } + + renderRollupCircle = (strokeData: any) => { + let stroke: string; + switch (strokeData['category']) { + case 'ungraded': + stroke = this.props.theme === 'dark' ? '#666666' : '#AAAAAA'; + break; + case 'correct': + stroke="#00A5B5" + break; + case 'incorrect': + stroke="#E36875" + break; + default: + stroke="#00A5B5" + } + return ( + + + ); + } + + getStrokeDashData(count: number, totalChallenges: number, offset: number, category: any) { + let strokeDashLength = (((count / totalChallenges) * 100) - 2); + let strokeOffset = (100 - offset); + + return { + category: category, + strokeDashLength: strokeDashLength, + strokeDashEnd: (100 - strokeDashLength), + strokeOffset: strokeOffset + }; + } + + circles() { + let newOffset = 0; + let counter = 0; + + return map(this.props.answerCounts, (value, category) => { + counter++; + + if (value > 0) { + if (category === 'correct' && value === this.props.total) { + return this.completeCircle(counter); + } + let strokeData = this.getStrokeDashData(value, this.props.total, newOffset, category); + + newOffset += strokeData['strokeDashLength'] + 2; + + return this.renderRollupCircle(strokeData); + } + }); + } + + svgCircles() { + if (this.props.answerCounts.ungraded === this.props.total) { + return + } + + let incompleStrokeColor = this.props.theme === 'dark' ? '#666666' : '#E7E7E7'; + let checkpointIcon = null; + + if (this.props.total !== this.props.answerCounts.correct) { + checkpointIcon = ; + } else if (this.props.standardPercentageMode && this.props.total === this.props.answerCounts.correct) { + incompleStrokeColor = "#88CBD1"; + } + + return ( + + + + + + {this.circles()} + {checkpointIcon} + + ); + } + + completeCircle(counter: any) { + let circleFill = this.props.theme === 'dark' ? '#51AFB9' : 'transparent'; + let checkFill = this.props.theme === 'dark' ? '#3a4352' : '#51AFB9'; + let fillIcon = null; + + if (this.props.checkpointIcon) { + if (this.props.checkFill) checkFill = this.props.checkFill; + fillIcon = + } else { + let transform = this.props.standardPercentageMode ? "translate(2.0000000, 2.000000)" : "translate(6.0000000, 6.000000)" + let fill = this.props.standardPercentageMode ? "#879697" : checkFill + let scale = this.props.standardPercentageMode ? "scale(2)" : "scale(1.5)" + + fillIcon = ( + + + + + + + + + ); + + if (this.props.standardPercentageMode) { + return fillIcon + } + } + + return ( + + + + + + {fillIcon} + + + + ); + } + + render() { + return this.svgCircles(); + } +} diff --git a/scripts/app/javascript/components/shared/DonutRing/components/ColorDonut/ColorDonut.tsx b/scripts/app/javascript/components/shared/DonutRing/components/ColorDonut/ColorDonut.tsx new file mode 100644 index 0000000000000000000000000000000000000000..99fa2476f0a43c81d11f7d445ba7190309464ca3 --- /dev/null +++ b/scripts/app/javascript/components/shared/DonutRing/components/ColorDonut/ColorDonut.tsx @@ -0,0 +1,74 @@ +import React from 'react' + +const ColorDonut = ({ + colorHash, + total, + innerIcon, + ringStroke, +}: { + colorHash: Record + total: number + innerIcon?: string + ringStroke: string +}) => { + function circles() { + let newOffset = 0; + + return Object.keys(colorHash).map((color, i) => { + const colorCount = colorHash[color] + + if (colorCount > 0) { + const strokeData = getStrokeDashData(colorCount, total, newOffset); + + newOffset += strokeData.strokeDashLength + 2; + + return ( + + + ) + } + }) + } + + function getStrokeDashData(count: number, totalChallenges: number, offset: number) { + const strokeDashLength = ((count / totalChallenges) * 100) - 2; + + return { + strokeDashLength, + strokeDashEnd: 100 - strokeDashLength, + strokeOffset: 100 - offset + }; + } + + return ( + + + + + + {circles()} + {innerIcon} + + ); +}; + +export default ColorDonut diff --git a/scripts/app/javascript/components/shared/DonutRing/components/CompleteDonut/CompleteDonut.tsx b/scripts/app/javascript/components/shared/DonutRing/components/CompleteDonut/CompleteDonut.tsx new file mode 100644 index 0000000000000000000000000000000000000000..a6570c60f6ab49599e80848a9ec59eb6aec5106b --- /dev/null +++ b/scripts/app/javascript/components/shared/DonutRing/components/CompleteDonut/CompleteDonut.tsx @@ -0,0 +1,43 @@ +import React from 'react' +import SvgRenderer from '../../../../SvgRenderer' + +const CompleteDonut = ({ + theme, + checkpointIcon, +}: { + theme: string + checkpointIcon?: string +}) => ( + + + + + + {checkpointIcon ? ( + + ) : ( + + + + + + + + + )} + + + +); + +export default CompleteDonut diff --git a/scripts/app/javascript/components/shared/DonutRing/components/UngradedDonut/UngradedDonut.tsx b/scripts/app/javascript/components/shared/DonutRing/components/UngradedDonut/UngradedDonut.tsx new file mode 100644 index 0000000000000000000000000000000000000000..fe2efd6b07bea9a3955060e1600a67a7e7b29b8c --- /dev/null +++ b/scripts/app/javascript/components/shared/DonutRing/components/UngradedDonut/UngradedDonut.tsx @@ -0,0 +1,24 @@ +import React from 'react' + +const UngradedDonut = ({ theme }: { theme: string }) => ( + + + + + + +); + +export default UngradedDonut diff --git a/scripts/app/javascript/components/shared/DonutRing/components/UngradedDonut/UngradedDonut/UngradedDonut.tsx b/scripts/app/javascript/components/shared/DonutRing/components/UngradedDonut/UngradedDonut/UngradedDonut.tsx new file mode 100644 index 0000000000000000000000000000000000000000..b18302e11ab3a1dc2ca2878b6ea49e8cadf2e281 --- /dev/null +++ b/scripts/app/javascript/components/shared/DonutRing/components/UngradedDonut/UngradedDonut/UngradedDonut.tsx @@ -0,0 +1,31 @@ +import React from 'react' + +const UngradedDonut = ({ + theme +}: { + theme: string +}) => { + let ungradedStrokeColor = theme === 'dark' ? '#666666' : '#AAAAAA'; + let ungradedStrokeWidth = theme === 'dark' ? '3' : '3.5'; + return ( + + + + + + + ); +} +export default UngradedDonut diff --git a/scripts/app/javascript/components/shared/ErrorMessagesTable/ErrorMessagesTable.tsx b/scripts/app/javascript/components/shared/ErrorMessagesTable/ErrorMessagesTable.tsx new file mode 100644 index 0000000000000000000000000000000000000000..6c3177fd055d417806be67eb83e75a445a4cf56a --- /dev/null +++ b/scripts/app/javascript/components/shared/ErrorMessagesTable/ErrorMessagesTable.tsx @@ -0,0 +1,26 @@ +import * as React from 'react'; + +type Props = { + errors: string[] +} + +const ErrorMessagesTable = (props: Props) => ( +
    + + + + + + + +
    Failure + {props.errors.map((error: string, i: number) => ( +

    + {error} +

    + ))} +
    +
    +); + +export default ErrorMessagesTable; diff --git a/scripts/app/javascript/components/shared/FlashAlert/FlashAlert.tsx b/scripts/app/javascript/components/shared/FlashAlert/FlashAlert.tsx new file mode 100644 index 0000000000000000000000000000000000000000..4a2f7ce6976b722a31a02c8f581976ec6e93b59e --- /dev/null +++ b/scripts/app/javascript/components/shared/FlashAlert/FlashAlert.tsx @@ -0,0 +1,29 @@ +import * as React from 'react'; + +const TYPES = { + notice: 'alert-info', + success: 'alert-success', + error: 'alert-danger', + alert: 'alert-danger' +} + +type Errors = { + type: string, + message: string | React.ReactNode +} + +type Props = { + errors: Errors[] +} + +const FlashAlert = (props: Props) => ( +
    + {props.errors.map((error, index) => ( +
    + {error.message} +
    + ))} +
    +) + +export default FlashAlert; diff --git a/scripts/app/javascript/components/shared/FormatNumber/FormatNumber.tsx b/scripts/app/javascript/components/shared/FormatNumber/FormatNumber.tsx new file mode 100644 index 0000000000000000000000000000000000000000..b1ab0a1c6943a0276b4ab996cb70b9b730818aeb --- /dev/null +++ b/scripts/app/javascript/components/shared/FormatNumber/FormatNumber.tsx @@ -0,0 +1,20 @@ +import * as React from 'react'; + +type Props = { + number: number, +} + +/** + * + * Component for formatting numbers (as numbers or strings) to have commas + * @returns {string} a formatted string of the number + */ +export default (props: Props) => { + const formattedNum = new Intl.NumberFormat('en', {}).format(props.number) + + return ( + + {formattedNum} + + ) +}; diff --git a/scripts/app/javascript/components/shared/Icons/AlertSign.tsx b/scripts/app/javascript/components/shared/Icons/AlertSign.tsx new file mode 100644 index 0000000000000000000000000000000000000000..71ce4e0f1861aa1fc123a0421d613a36a67d0e6b --- /dev/null +++ b/scripts/app/javascript/components/shared/Icons/AlertSign.tsx @@ -0,0 +1,16 @@ +import React from 'react'; + +const AlertSign = () => ( + + + + +); + +export default AlertSign \ No newline at end of file diff --git a/scripts/app/javascript/components/shared/Icons/Clear.tsx b/scripts/app/javascript/components/shared/Icons/Clear.tsx new file mode 100644 index 0000000000000000000000000000000000000000..6fc1d533378d70967e307407f447922ddd5b9b5b --- /dev/null +++ b/scripts/app/javascript/components/shared/Icons/Clear.tsx @@ -0,0 +1,18 @@ +import React from 'react'; + +const Clear = ({ fill }: { fill: string }) => ( + + + + +); + +export default Clear; \ No newline at end of file diff --git a/scripts/app/javascript/components/shared/Icons/CloseButton.tsx b/scripts/app/javascript/components/shared/Icons/CloseButton.tsx new file mode 100644 index 0000000000000000000000000000000000000000..ceb4cdb094f30a425f734c4952a3ba2f24ff5996 --- /dev/null +++ b/scripts/app/javascript/components/shared/Icons/CloseButton.tsx @@ -0,0 +1,16 @@ +import React from 'react'; + +const CloseButton = () => ( + + + + +) + +export default CloseButton \ No newline at end of file diff --git a/scripts/app/javascript/components/shared/Icons/Done.tsx b/scripts/app/javascript/components/shared/Icons/Done.tsx new file mode 100644 index 0000000000000000000000000000000000000000..5de3f3127137210ed654aa2f12a675d3a23352c3 --- /dev/null +++ b/scripts/app/javascript/components/shared/Icons/Done.tsx @@ -0,0 +1,16 @@ +import React from 'react'; + +const Done = ({ fill }: { fill: string }) => ( + + + + +); + +export default Done; \ No newline at end of file diff --git a/scripts/app/javascript/components/shared/Icons/KeyboardArrowDown.tsx b/scripts/app/javascript/components/shared/Icons/KeyboardArrowDown.tsx new file mode 100644 index 0000000000000000000000000000000000000000..bf3a9146712c265bf0624fd769e2ce62fb064cfd --- /dev/null +++ b/scripts/app/javascript/components/shared/Icons/KeyboardArrowDown.tsx @@ -0,0 +1,16 @@ +import React from 'react'; + +const KeyboardArrowDown = () => ( + + + + +); + +export default KeyboardArrowDown \ No newline at end of file diff --git a/scripts/app/javascript/components/shared/Icons/KeyboardArrowUp.tsx b/scripts/app/javascript/components/shared/Icons/KeyboardArrowUp.tsx new file mode 100644 index 0000000000000000000000000000000000000000..1f17eb5ab7359dfc58b183e889997b79843c74b2 --- /dev/null +++ b/scripts/app/javascript/components/shared/Icons/KeyboardArrowUp.tsx @@ -0,0 +1,16 @@ +import React from 'react'; + +const KeyboardArrowUp = () => ( + + + + +); + +export default KeyboardArrowUp \ No newline at end of file diff --git a/scripts/app/javascript/components/shared/Icons/SettingsCog.tsx b/scripts/app/javascript/components/shared/Icons/SettingsCog.tsx new file mode 100644 index 0000000000000000000000000000000000000000..1fd69a94b290169ff502c1e4c2fdaee5556c9c90 --- /dev/null +++ b/scripts/app/javascript/components/shared/Icons/SettingsCog.tsx @@ -0,0 +1,15 @@ +import React from 'react'; + +const SettingsCog = () => ( + + + +) + +export default SettingsCog \ No newline at end of file diff --git a/scripts/app/javascript/components/shared/Icons/StatusCircle.tsx b/scripts/app/javascript/components/shared/Icons/StatusCircle.tsx new file mode 100644 index 0000000000000000000000000000000000000000..4b03332fd6561894550fd100f2e296f7d29dac86 --- /dev/null +++ b/scripts/app/javascript/components/shared/Icons/StatusCircle.tsx @@ -0,0 +1,17 @@ +import React from 'react'; + +const StatusCircle = ({ fill }: { fill: string }) => ( +
    +) + +export default StatusCircle + + diff --git a/scripts/app/javascript/components/shared/MasteryProgressBar/MasteryProgressBar.tsx b/scripts/app/javascript/components/shared/MasteryProgressBar/MasteryProgressBar.tsx new file mode 100644 index 0000000000000000000000000000000000000000..0282133db5eb8de4389ee4889f4a7ebbc06c86ec --- /dev/null +++ b/scripts/app/javascript/components/shared/MasteryProgressBar/MasteryProgressBar.tsx @@ -0,0 +1,74 @@ +import React from 'react'; +import SvgRenderer from '../../SvgRenderer' +import ProgressBar from '../ProgressBar/ProgressBar' + +const MasteryProgressBar = ({ + standard, + theme, + sideBar = false, + awaitingGradeImagePath, + standardTitle, + isAwaitingGrading, +} : { + standard: Api.StandardPresenter_ForCard + theme?: 'light' | 'dark' + sideBar: boolean + awaitingGradeImagePath: string + standardTitle?: string + isAwaitingGrading: boolean | undefined +}) => { + if (standard.cohort_mode !== 'Mastery') return null; + + if (standard.mastery_score && !isAwaitingGrading) { + return ( + + {standard.mastery_score} +
    + )} + label={null} + bar={null} + sideBar={sideBar} + standardTitle={standardTitle} + masteryCohort + /> + ) + } else if (isAwaitingGrading) { + return ( + + +
    + )} + label={

    Pending Score

    } + bar={null} + sideBar={sideBar} + standardTitle={standardTitle} + masteryCohort + /> + ) + } else { + return ( + {standard.progress_complete === 0 ? 'Start' : 'In Progress'}

    } + bar={( +
    +
    +
    +
    + )} + standardTitle={standardTitle} + sideBar={sideBar} + masteryCohort + /> + ) + } +} + +export default MasteryProgressBar diff --git a/scripts/app/javascript/components/shared/Modal/Modal.tsx b/scripts/app/javascript/components/shared/Modal/Modal.tsx new file mode 100644 index 0000000000000000000000000000000000000000..601ed74f6e3684b152b53c1af23743bd36007367 --- /dev/null +++ b/scripts/app/javascript/components/shared/Modal/Modal.tsx @@ -0,0 +1,77 @@ +import * as React from 'react'; +import Button from '../../shared/Button/Button' + +type Props ={ + helperText?: JSX.Element | null, + closeText?: string, + closeModal?: any, + visible: boolean, + children: any, + handleAction?: any, + actionText?: string, + shouldCloseOnOutsideClick?: boolean +} + +export default class Modal extends React.Component { + private node: any + + componentDidUpdate() { + if (!this.props.shouldCloseOnOutsideClick) return; + + if (this.props.visible) { + document.addEventListener('touchmove', this.handleOutsideClick, false); + document.addEventListener('click', this.handleOutsideClick, false); + } else if (!this.props.visible) { + document.removeEventListener('touchmove', this.handleOutsideClick, false); + document.removeEventListener('click', this.handleOutsideClick, false); + } + } + + handleOutsideClick = (e: any) => { + if (!this.props.shouldCloseOnOutsideClick) return; + if (this.node.contains(e.target)) return; + this.props.closeModal(); + } + + showModalActions = () => { + const { helperText, closeText, closeModal, handleAction, actionText } = this.props; + return ((closeText && closeModal) || (handleAction && actionText)) + } + + render() { + const { visible, helperText, closeText, closeModal, children, handleAction, actionText } = this.props; + + return ( +
    +
    +
    { this.node = node; }}> +
    {children}
    + {(this.showModalActions() && +
    + {(helperText && +
    {helperText}
    + )} + {(closeModal && closeText && +
    + )} +
    +
    +
    + ); + } +} diff --git a/scripts/app/javascript/components/shared/Pagination/Pagination.tsx b/scripts/app/javascript/components/shared/Pagination/Pagination.tsx new file mode 100644 index 0000000000000000000000000000000000000000..f1bd7b46d3649f825ba388043cb63b7914dd22bc --- /dev/null +++ b/scripts/app/javascript/components/shared/Pagination/Pagination.tsx @@ -0,0 +1,40 @@ +import React from 'react'; +import ReactPaginate from 'react-paginate'; + +type Props = { + totalPages: number + onPageChange: any + currentPage: number + pageRangeDisplayed: number + marginPagesDisplayed: number +} + +const Pagination = ({ + totalPages, + onPageChange, + currentPage, + pageRangeDisplayed, + marginPagesDisplayed, +}: Props) => ( + +); + +export default Pagination; + +Pagination.defaultProps = { + pageRangeDisplayed: 10, + marginPagesDisplayed: 0, +}; diff --git a/scripts/app/javascript/components/shared/PercentageProgressBar/PercentageProgressBar.tsx b/scripts/app/javascript/components/shared/PercentageProgressBar/PercentageProgressBar.tsx new file mode 100644 index 0000000000000000000000000000000000000000..24c8fc961319dbc3b701c7cae8edfe33b08e84aa --- /dev/null +++ b/scripts/app/javascript/components/shared/PercentageProgressBar/PercentageProgressBar.tsx @@ -0,0 +1,107 @@ +import React from 'react'; +import SvgRenderer from '../../SvgRenderer' +import ProgressBar from '../ProgressBar/ProgressBar' + +const PercentageProgressBar = ({ + standard, + theme, + sideBar = false, + allCompleteImagePath, + awaitingGradeImagePath, + standardTitle, + checkpointInfo, + isAwaitingGrading, +} : { + standard: Api.StandardPresenter_ForCard + theme?: 'light' | 'dark' + sideBar: boolean + allCompleteImagePath: string + awaitingGradeImagePath: string + standardTitle?: string + checkpointInfo?: Api.CheckpointInfo + isAwaitingGrading: boolean | undefined +}) => { + if (standard.cohort_mode !== 'Percentage') return null; + if (!sideBar && standard.progress_complete === 0.0) return null; + + if ( + sideBar + && checkpointInfo + && checkpointInfo.contentFilesSubmission + && checkpointInfo.contentFilesSubmission.hasOwnProperty('last_performance_percent') + && checkpointInfo.contentFilesSubmission.last_performance_percent != null + && checkpointInfo.contentFilesSubmission.state !== "retry" + ) { + return ( +
    +

    {standard.title}

    + +
    + ) + } + + if ((standard.is_completed && isAwaitingGrading) || + (checkpointInfo && checkpointInfo.contentFilesSubmission && checkpointInfo.contentFilesSubmission.state === "retry")) { + return ( + + +
    + )} + label={

    Pending Score

    } + bar={null} + sideBar={sideBar} + standardTitle={standardTitle} + masteryCohort={false} + /> + ) + } else if (standard.is_completed) { + if (!sideBar) { + return ( +
    + +
    + ) + } + + return ( + + +
    + )} + label={null} + bar={null} + sideBar={sideBar} + standardTitle={standardTitle} + masteryCohort={false} + /> + ) + } else { + const label = standard.progress_complete === 0.0 ? 'Start' : `${standard.remaining_to_completion} remaining` + return ( + {label}

    } + bar={( +
    +
    +
    + )} + sideBar={sideBar} + standardTitle={standardTitle} + masteryCohort={false} + /> + ) + } +} + +export default PercentageProgressBar diff --git a/scripts/app/javascript/components/shared/Pill/Pill.tsx b/scripts/app/javascript/components/shared/Pill/Pill.tsx new file mode 100644 index 0000000000000000000000000000000000000000..89654f2dd13e3e5e93e6c1b114f3426706eb72b2 --- /dev/null +++ b/scripts/app/javascript/components/shared/Pill/Pill.tsx @@ -0,0 +1,33 @@ +import React from 'react'; + +type Props = { + text: string | null + href: string | null + color: string + style: any + size: 'small' | 'medium' | 'large' + node: any +} + +export default ({ text = null, href = null, color = '#EBEBEB', style = {}, size = 'small', node = null }: Props) => ( +
    + {href ? ( + + {text || node} + + ) : ( +

    + {text || node} +

    + )} +
    +); + +// Pill.defaultProps = { +// text: null, +// color: '#EBEBEB', +// style: {}, +// href: null, +// node: null, +// size: 'small' +// }; diff --git a/scripts/app/javascript/components/shared/PointGradeButtons/PointGradeButtons.tsx b/scripts/app/javascript/components/shared/PointGradeButtons/PointGradeButtons.tsx new file mode 100644 index 0000000000000000000000000000000000000000..d21f79402eef6e0f93329022a5257f2186cc72bb --- /dev/null +++ b/scripts/app/javascript/components/shared/PointGradeButtons/PointGradeButtons.tsx @@ -0,0 +1,78 @@ +import React, { Component } from 'react' +import SpinText from './SpinText' + +type Props = { + readOnly: boolean + manuallyGradeChallenge: (status: Api.SubmitedChallengeAnswerStatus, points?: number) => void + maxPoints: number + points: number | null + submissionStatus: string +} + +type State = { + hover: number | null + submissionStatus: string +} + +export default class PointGradeButtons extends Component { + constructor(props: Props) { + super(props) + this.state = { + hover: null, + submissionStatus: this.props.submissionStatus, + } + } + + mouseEnter = (i: number) => { + this.setState({ hover: i }) + } + + mouseLeave = () => { + this.setState({ hover: null }) + } + + renderPoints = () => { + const { maxPoints, readOnly, manuallyGradeChallenge } = this.props; + const { hover } = this.state; + + return ( +
    + {[...Array(maxPoints + 1).fill({})].map((_, i) => { + let gradePoint: any = i; + + if (hover == gradePoint) { + gradePoint = ( + + ) + } + return ( +
    { this.mouseEnter(gradePoint) } } + onMouseLeave={() => { this.mouseLeave() } } + key={i} + onClick={readOnly ? undefined : () => manuallyGradeChallenge('correct', gradePoint + 1) } + className="point" + > + {gradePoint} +
    + ) + })} +
    + ) + } + + renderDonut = () => { + return
    donut
    + } + + render() { + const { maxPoints, readOnly, manuallyGradeChallenge } = this.props; + const { hover } = this.state; + + return ( + <> + {this.state.submissionStatus === "correct" ? this.renderDonut() : this.renderPoints()} + + ) + } +} diff --git a/scripts/app/javascript/components/shared/PointGradeButtons/SpinText.tsx b/scripts/app/javascript/components/shared/PointGradeButtons/SpinText.tsx new file mode 100644 index 0000000000000000000000000000000000000000..c6cf39d542325d66d8c6f7093e114fde70ccf84c --- /dev/null +++ b/scripts/app/javascript/components/shared/PointGradeButtons/SpinText.tsx @@ -0,0 +1,79 @@ +import React, { Component } from 'react' + +type Props = { + text: string +} +type State = { + colors: Color[] + hyp: number + speed: number + blur: number + intervalId: any +} + +type Color = { + x: number + y: number + r: string + g: string + b: string + angle: number +} + +export default class SpinText extends Component { + constructor(props: Props) { + super(props) + this.state = { + colors: [ + { x: 0, y: 0, r: '9b', g: '9b', b: '9b', angle: 5*Math.PI/6 }, + { x: 0, y: 0, r: '9b', g: '9b', b: '9b', angle: Math.PI/6 }, + { x: 0, y: 0, r: '9b', g: '9b', b: '9b', angle: 3*Math.PI/6 } + ], + hyp: 3, + speed: 0.15, + blur: 3, + intervalId: 0 + } + } + + componentDidMount() { + var intervalId = setInterval(this.rotate, 80); + // store intervalId in the state so it can be accessed later: + this.setState({intervalId: intervalId}); + } + + componentWillUnmount() { + // use intervalId from the state to clear the interval + clearInterval(this.state.intervalId); + } + + rotate = () => { + let colors = [...this.state.colors]; + const updateColors = this.state.colors.map(color => this.angleCalc(color) ) + this.setState({colors: colors}) + } + + angleCalc = (color: Color) => { + let { hyp, speed } = this.state; + let xOut = Math.cos(color.angle)*hyp; + let yOut = Math.sin(color.angle)*hyp; + color.angle += speed; + color.x = Number(xOut.toFixed(2)); + color.y = Number(yOut.toFixed(2)); + return color + } + + styles = () => { + let { blur } = this.state + const styles = this.state.colors.map((c) => { + return `${c.x}px ${c.y}px ${blur}px #${c.r}${c.g}${c.b}` + }).join(', '); + return { "textShadow": styles } + } + + render() { + return ( + {this.props.text} + ) + } +} diff --git a/scripts/app/javascript/components/shared/ProgressBar/ProgressBar.tsx b/scripts/app/javascript/components/shared/ProgressBar/ProgressBar.tsx new file mode 100644 index 0000000000000000000000000000000000000000..1ddd22c3fe75a8f8a4784221058dc0f632b320dd --- /dev/null +++ b/scripts/app/javascript/components/shared/ProgressBar/ProgressBar.tsx @@ -0,0 +1,43 @@ +import * as React from 'react' + +const ProgressBar = ({ + type, + icon, + bar, + label, + sideBar, + masteryCohort, + standardTitle, +} : { + type: string + icon: React.ReactNode + bar: React.ReactNode + label: React.ReactNode + sideBar: boolean + masteryCohort: boolean | undefined + standardTitle?: string +}) => ( + <> + {sideBar ? ( + <> +
    +
    + {standardTitle} +
    + {icon} +
    + {!masteryCohort && bar &&
    {bar}
    } + + ) : ( +
    +
    + {icon} + {bar} + {label} +
    +
    + )} + +) + +export default ProgressBar \ No newline at end of file diff --git a/scripts/app/javascript/components/shared/ProgressThresholdsKey/ProgressThresholdsKey.tsx b/scripts/app/javascript/components/shared/ProgressThresholdsKey/ProgressThresholdsKey.tsx new file mode 100644 index 0000000000000000000000000000000000000000..b8cf9a06fd85e21da6d96bbd3c9f82b43849670c --- /dev/null +++ b/scripts/app/javascript/components/shared/ProgressThresholdsKey/ProgressThresholdsKey.tsx @@ -0,0 +1,32 @@ +import React, { Fragment } from 'react'; +import { thresholdColors } from '../ProgressThresholdsSlider/ProgressThresholdsSlider' + +const ProgressThresholdsKey = ({ + progressThresholds, + includeUnscoredKey, +}:{ + progressThresholds: number[], + includeUnscoredKey?: boolean, +}) => ( +
    + {/* We don't show a color for 100 which is always the last element */} + {progressThresholds.slice(0, -1).map((threshold, i) => ( + +
    +

    + {`+${threshold}%`} +

    + + ))} + {includeUnscoredKey && ( +
    +
    +

    + unscored submission +

    +
    + )} +
    +) + +export default ProgressThresholdsKey \ No newline at end of file diff --git a/scripts/app/javascript/components/shared/ProgressThresholdsModal/ProgressThresholdsModal.tsx b/scripts/app/javascript/components/shared/ProgressThresholdsModal/ProgressThresholdsModal.tsx new file mode 100644 index 0000000000000000000000000000000000000000..2a67bf47fa218a6f5421782419e44eea655cc152 --- /dev/null +++ b/scripts/app/javascript/components/shared/ProgressThresholdsModal/ProgressThresholdsModal.tsx @@ -0,0 +1,70 @@ +import * as React from 'react' +import Modal from '../Modal/Modal' +import ProgressThresholdsSlider from '../ProgressThresholdsSlider/ProgressThresholdsSlider' +import http from '../../../lib/http' +import { cohortPath } from '../../../generated/routes' + +type Props ={ + visible: boolean + toggleModal: any + cohortId: number + progressThresholds: number[] + updateProgressThresholds: any +} + +export default class ProgressThresholdsModal extends React.Component { + state = { + progressThresholds: this.props.progressThresholds, + } + + resetProgressThresholds = () => { + this.setState({ progressThresholds: this.props.progressThresholds }); + this.props.toggleModal(); + } + + setProgressThresholds = (progressThresholds: number[]) => { + this.setState({ progressThresholds }); + } + + setCohortThresholds = () => { + http('PATCH', cohortPath(this.props.cohortId), { + body: { settings: { progress_thresholds: this.state.progressThresholds } }, + }) + .then(() => { + this.props.updateProgressThresholds(this.state.progressThresholds); + this.props.toggleModal(); + }) + } + + render() { + const { visible } = this.props; + const { progressThresholds } = this.state; + + return ( + +
    +

    + SET COLOR HIGHLIGHTING +

    +
    + this.setProgressThresholds(thresholds)} + progressThresholds={progressThresholds} + /> +
    +
    +
    + Colors will be updated for the Unit Progress and Course Stats page + for all users of this cohort +
    +
    +
    +
    + ); + } +} \ No newline at end of file diff --git a/scripts/app/javascript/components/shared/ProgressThresholdsSlider/ProgressThresholdsSlider.tsx b/scripts/app/javascript/components/shared/ProgressThresholdsSlider/ProgressThresholdsSlider.tsx new file mode 100644 index 0000000000000000000000000000000000000000..289c61ab169604b52806c703e76fe0ea99df644a --- /dev/null +++ b/scripts/app/javascript/components/shared/ProgressThresholdsSlider/ProgressThresholdsSlider.tsx @@ -0,0 +1,46 @@ +import React from 'react'; +import { Range, createSliderWithTooltip } from 'rc-slider'; + +const RangeWithTooltip = createSliderWithTooltip(Range); + +export const thresholdColors = [ + { backgroundColor: '#E89EA6' }, + { backgroundColor: '#E8C3C3' }, + { backgroundColor: '#C3E8E6' }, + { backgroundColor: '#A3D8D9' }, + { backgroundColor: '#88CBD1' }, +]; + +export const findThresholdColor = (range: number[], value: number) => { + if (value <= 0) return thresholdColors[0].backgroundColor; + if (value >= 100) return thresholdColors[thresholdColors.length - 1].backgroundColor; + + const len = range.length; + + for (let i = 0; i < len; i++) { + if (value < range[i]) { + return thresholdColors[i - 1].backgroundColor; + } + } +} + +const ProgressThresholdsSlider = ({ + onAfterChange, + progressThresholds +}:{ + onAfterChange: any, + progressThresholds: number[], +}) => ( +
    + `${value}%`} + onAfterChange={onAfterChange} + /> +
    +) + +export default ProgressThresholdsSlider \ No newline at end of file diff --git a/scripts/app/javascript/components/shared/SearchBar/SearchBar.tsx b/scripts/app/javascript/components/shared/SearchBar/SearchBar.tsx new file mode 100644 index 0000000000000000000000000000000000000000..3d2c332efd4f21f2ba05a859a56949eb8c1d71bd --- /dev/null +++ b/scripts/app/javascript/components/shared/SearchBar/SearchBar.tsx @@ -0,0 +1,102 @@ +import * as React from 'react'; +import { debounce } from 'lodash'; +import Icon from '../../Icon'; + +const debounceDelay = 200; + +type Props = { + searchAction?: Function, + collapse?: boolean, +} + +type State = { + barExpanded: boolean, + searchText: string +} + +/** + * + * reusable search bar component + * @param {object} props object recieves: + * - searchAction function that will receive the query string and be called on update (debounced) + * - collapse boolean to determine wether search bar should expand/collapse + */ +class SearchBar extends React.Component { + private inputRef: React.RefObject; + updateSearchTextDebounced: any; + + constructor(props: Props) { + super(props); + this.inputRef = React.createRef(); + + this.state = { + barExpanded: false, + searchText: '', + } + + this.updateSearchTextDebounced = debounce( + this.updateSearchText.bind(this), debounceDelay, { maxWait: 200 }); + } + + updateSearchText (search: string) { + this.props.searchAction && this.props.searchAction(search); + } + + handleTextChange = (e: any) => { + let search = e.target.value; + this.setState({ + searchText: search, + }) + this.updateSearchTextDebounced(search); + }; + + openSearchBar = () => { + this.setState({ + barExpanded: true + }, () => this.inputRef.current && this.inputRef.current.focus()); + }; + + closeSearchBar = () => { + this.updateSearchTextDebounced.cancel(); + this.setState({ + barExpanded: false, + searchText: '' + }, () => { + this.updateSearchTextDebounced(''); + if (this.inputRef.current) { + this.inputRef.current.focus(); + } + }); + }; + + componentWillUnmount() { + // cleanup any debounced calls + this.updateSearchTextDebounced.cancel(); + } + + render() { + const expanded = !this.props.collapse || this.state.barExpanded; + const classes = expanded ? "search-bar search-bar--expanded" : "search-bar"; + const closeIconBool = this.props.collapse ? this.state.barExpanded : !!this.state.searchText; + const input = + const closeIcon = ( +
    + +
    + ); + + return ( +
    +
    +
    + +
    + {expanded && input} + {closeIconBool && closeIcon} +
    +
    + ) + } +} + +export default SearchBar; diff --git a/scripts/app/javascript/components/shared/Slideshow/Slideshow.tsx b/scripts/app/javascript/components/shared/Slideshow/Slideshow.tsx new file mode 100644 index 0000000000000000000000000000000000000000..0c64d0804b57c942a5fdacb5a22930e2afbf55da --- /dev/null +++ b/scripts/app/javascript/components/shared/Slideshow/Slideshow.tsx @@ -0,0 +1,210 @@ +import React, { Component } from 'react'; +import SvgRenderer from '../../SvgRenderer' + +type Props = { + defaultIndex: number + slideInterval: number + showIndex: boolean + useDotIndex: boolean + showArrows: boolean + effect: 'fade' | 'left' | 'top' | 'right' | 'bottom' | 'bounce-right' | 'bounce-left' + autoplay: boolean + enableKeyboard: boolean + slides: any + children: any + height: string + width: string +} + +type State = { + currentSlide: number + slideInterval: number + showIndex: boolean + useDotIndex: boolean + showArrows: boolean + effect: 'fade' | 'left' | 'top' | 'right' | 'bottom' | 'bounce-right' | 'bounce-left' + autoplay: boolean + enableKeyboard: boolean + slides: any + intervalId: number | undefined +} + +export default class Slideshow extends Component { + state: State = { + currentSlide: this.props.defaultIndex, + slideInterval: this.props.slideInterval, + showIndex: this.props.showIndex, + useDotIndex: this.props.useDotIndex, + showArrows: this.props.showArrows, + effect: this.props.effect, + autoplay: this.props.autoplay, + enableKeyboard: this.props.enableKeyboard, + slides: this.props.slides.length > 0 ? this.props.slides : this.props.children, + intervalId: undefined + } + + static defaultProps: Props; + + componentDidMount() { + if (this.state.autoplay) this.runSlideShow(); + if (this.state.enableKeyboard) document.addEventListener('keydown', this.handleKeyboard); + } + + componentWillUnmount() { + window.clearInterval(this.state.intervalId); + document.removeEventListener('keydown', this.handleKeyboard); + } + + handleKeyboard = (e: any) => { + e.keyCode === 37 && this.decreaseCount() + e.keyCode === 39 && this.increaseCount() + } + + runSlideShow = () => { + const intervalId = window.setInterval(this.autoSlideshow, this.state.slideInterval); + this.setState({ intervalId }); + } + + autoSlideshow = () => { + this.setState({ + currentSlide: (this.state.currentSlide + 1) % this.state.slides.length + }); + } + + restartSlideshow = () => { + window.clearInterval(this.state.intervalId); + this.runSlideShow(); + } + + increaseCount = () => { + this.state.effect === 'left' && this.setState({ effect: 'right' }); + this.state.effect === 'bounce-left' && this.setState({ effect: 'bounce-right' }); + this.state.autoplay && this.restartSlideshow(); + + this.setState({ + currentSlide: (this.state.currentSlide + 1) % this.state.slides.length + }); + } + + decreaseCount = () => { + this.state.effect === 'right' && this.setState({ effect: 'left' }); + this.state.effect === 'bounce-right' && this.setState({ effect: 'bounce-left' }); + this.state.autoplay && this.restartSlideshow() ; + + let currentSlide; + + if (this.state.currentSlide === 0) { + currentSlide = this.state.slides.length - 1 + } else { + currentSlide = this.state.currentSlide - 1 + } + + this.setState({ currentSlide }); + } + + render() { + const { slides, showIndex, useDotIndex, effect, showArrows } = this.state; + + let slideEffect = effect === undefined ? 'fade' : effect; + let slideShowSlides; + let slideShowIndex; + + if (!this.props.children) { + slideShowSlides = slides.map((slide: any, i: number) => ( +
  • + )); + } else { + slideShowSlides = slides.map((slide: any, i: number) => ( +
  • + {slide} +
  • + )); + } + + if (useDotIndex) { + slideShowIndex = ( +
    + {slides.map((_: any, i: number) => { + return ( + + ); + })} +
    + ); + } else { + slideShowIndex = ( +
    +

    {`${this.state.currentSlide + 1} / ${slides.length}`}

    +
    + ); + } + + return ( +
    +
    +
      + {slideShowSlides} +
    + {showArrows && ( +
    + + +
    + )} + {showIndex && slideShowIndex} +
    +
    + ); + } +} + +Slideshow.defaultProps = { + /* boolean: show slide index in the bottom center */ + showIndex: false, + /* boolean: show arrows for slide navigation */ + showArrows: true, + /* boolean: automatically play through slideshow */ + autoplay: false, + /* boolean: enable arrow keys for slide navigation */ + enableKeyboard: true, + /* boolean: use dots to show index rather than numbers */ + useDotIndex: false, + /* number: if autoplay === true, interval for switching slides */ + slideInterval: 2000, + /* number: slide index to start on */ + defaultIndex: 0, + /* string: what slide transition you want to use */ + effect: 'fade', + /* array of strings: image urls */ + slides: [], + /* string: height of your slideshow presenter */ + height: '100%', + /* string: width of your slideshow presenter */ + width: '100%', + /* nodes: if you pass children they will be iterated through and used as the slides */ + children: null, +}; diff --git a/scripts/app/javascript/components/shared/UngradedDonut/UngradedDonut.tsx b/scripts/app/javascript/components/shared/UngradedDonut/UngradedDonut.tsx new file mode 100644 index 0000000000000000000000000000000000000000..b18302e11ab3a1dc2ca2878b6ea49e8cadf2e281 --- /dev/null +++ b/scripts/app/javascript/components/shared/UngradedDonut/UngradedDonut.tsx @@ -0,0 +1,31 @@ +import React from 'react' + +const UngradedDonut = ({ + theme +}: { + theme: string +}) => { + let ungradedStrokeColor = theme === 'dark' ? '#666666' : '#AAAAAA'; + let ungradedStrokeWidth = theme === 'dark' ? '3' : '3.5'; + return ( + + + + + + + ); +} +export default UngradedDonut diff --git a/scripts/app/javascript/components/shared/UniversalList/UniversalList.tsx b/scripts/app/javascript/components/shared/UniversalList/UniversalList.tsx new file mode 100644 index 0000000000000000000000000000000000000000..35df6f25b852df36f50c3144ce0d35a69e784e17 --- /dev/null +++ b/scripts/app/javascript/components/shared/UniversalList/UniversalList.tsx @@ -0,0 +1,43 @@ +import * as React from 'react'; +import Icon from '../../Icon'; +import SearchBar from '../SearchBar/SearchBar'; + +type listProps = { + body: Array, + canSearch?: boolean, + headerButtons?: Array, + searchFunction?: Function, + title: string | React.ReactNode +} + +/** + * + * Universal table component for displaying tables (or single items) + * consists of: + * title component (title string or element(S)) + * search bar (optional) + * header buttons (optional) + * table body (map over props.body array and render) + * recieves searchfunction to execute with on search (recieves search string) + * + */ +export default ({ body, canSearch, headerButtons, searchFunction, title }: listProps) => { + + return ( +
    +
    +
    + {typeof title === "string" ?

    {title}

    : title} +
    +
    + {canSearch ? : null} + {headerButtons ? headerButtons : null} +
    +
    + {body} +
    +
    +
    + ) +}; + diff --git a/scripts/app/javascript/components/shared/UniversalRow/UniversalRow.tsx b/scripts/app/javascript/components/shared/UniversalRow/UniversalRow.tsx new file mode 100644 index 0000000000000000000000000000000000000000..8e9afee63cbde5b712620757934cee2528fe658d --- /dev/null +++ b/scripts/app/javascript/components/shared/UniversalRow/UniversalRow.tsx @@ -0,0 +1,64 @@ +import * as React from 'react' + +type rowProps = { + columns?: Array, + description?: string | React.ReactNode, + footer?: Array, + hidden?: boolean, + mainTitle?: string | React.ReactNode, + mainTitleLink?: string, + rowClickable?: boolean, + styles?: React.CSSProperties, + subTitle?: string | React.ReactNode, + subTitleLink?: string +} + +/** + * + * Row object for rendering a row of data. + * designed to be reusable + * accepts: (on the props object) + * columns (array) of elements to render as columns in the row + * mainTitle (string) of title name + * mainTitleLink (string) of optional title link + * subTitle (string) of secondary title name and optional link + * subTitleLink (string) of optional secondary title link + * description (string or element) string description or body element(s) + * footer (array) of elements to render in the footer + * @param props + */ +export default (props: rowProps) => { + const { + columns, + description, + footer, + hidden, + mainTitle, + mainTitleLink, + rowClickable, + styles, + subTitle, + subTitleLink + } = props; + + const classes = hidden ? 'universal-row hidden' : 'universal-row'; + const titleElement = mainTitleLink ?

    {mainTitle}

    :

    {mainTitle}

    ; + const subTitleElement = subTitleLink ?

    {subTitle}

    :

    {subTitle}

    ; + + return ( +
    rowClickable && mainTitleLink ? window.location.href=mainTitleLink : null} + className={rowClickable ? 'universal-row-click-wrapper' : ''}> +
    +
    + {mainTitle && titleElement} + {subTitle && subTitleElement} + {description && (typeof description === 'string' ?

    {description}

    : description)} + {footer && (
    + {footer} +
    )} +
    + {columns && columns} +
    +
    + ) +} diff --git a/scripts/app/javascript/components/shared/UniversalTable/UniversalTable.tsx b/scripts/app/javascript/components/shared/UniversalTable/UniversalTable.tsx new file mode 100644 index 0000000000000000000000000000000000000000..977c717f870f60b5ae8bd07958c879c6009dcbe7 --- /dev/null +++ b/scripts/app/javascript/components/shared/UniversalTable/UniversalTable.tsx @@ -0,0 +1,163 @@ +import * as React from 'react'; + +type column = { + dataKey: string, + disableFiltering?: boolean, + disableSorting?: boolean, + styles?: {[key: string]: string}, + title: string, + width?: string, +} + +type data = { + disable?: boolean, + disableHoverHighlight?: boolean, + onClick?: () => void, + styles?: {[key: string]: string}, + [key: string]: number | string | React.ReactNode +} + +type Props = { + columns: column[], + data: data[], + emptyMsg?: string, + enableFiltering?: boolean, + enableSorting?: boolean, + hoverHighlight?: boolean, + maxRows?: number, + enableSeeMore?: boolean, + showBoxShadow?: boolean +} + +type State = { + rowsExpanded?: boolean +} + +/** + * Universal Table Component + * Interface for a universal table component to use across the app + * Accepts: + * columns array for each column (see type def) + * data array for each row (can be text or JSX - see type def) + * hoverHighlight bool indicating whether rows should highlight on hover + * + + * @param param0 + */ +class UniversalTable extends React.Component { + // TODO: implement resizing and width options + // TODO: implement sorting and filtering + // TODO: implement selecting singular and multiple rows + constructor(props: Props) { + super(props); + + this.state = { + rowsExpanded: false + } + } + + buildBody = () => { + const { + columns, + data, + emptyMsg, + hoverHighlight, + maxRows + } = this.props; + + const { rowsExpanded } = this.state; + const body = []; + const cols = columns.length; + + const emptyRow = ( + + + {emptyMsg || "No Data"} + + + ); + + if (data.length === 0) { + return [emptyRow]; + } + + for (let i = 0; i < data.length; i++) { + const row = data[i]; + + let classes = 'universal-table__row' + + (row.disable ? ' --disabled' : '') + + (hoverHighlight && !row.disableHoverHighlight ? ' --highlight' : ''); + + if (maxRows && !rowsExpanded && i >= maxRows) { + break; + } + + body.push( + + { + columns.map(col => { + let classes = `col col-${col.dataKey}`; + return row[col.dataKey] !== undefined ? + + {row[col.dataKey]} + : + ; + }) + } + + ); + } + + return body; + } + + toggleExpanded = () => { + this.setState({ + rowsExpanded: !this.state.rowsExpanded + }) + } + + render() { + const { + columns, + data, + enableSeeMore, + maxRows, + showBoxShadow + } = this.props; + + const { rowsExpanded } = this.state; + + return ( +
    + + + + {columns.map(col => { + let classes = `universal-table__col col-${col.dataKey}`; + + return ( + + ) + })} + + + + {this.buildBody()} + +
    {col.title}
    + {enableSeeMore && maxRows && data.length > maxRows && + (
    + {rowsExpanded ? 'view less' : 'view all releases'} +
    )} +
    + ) + } +} + +export default UniversalTable; diff --git a/scripts/app/javascript/components/standards/CompletionScoringBlock.tsx b/scripts/app/javascript/components/standards/CompletionScoringBlock.tsx new file mode 100644 index 0000000000000000000000000000000000000000..4a6eb1fb30bfca6dc6b2279b096e7e2f124ada0c --- /dev/null +++ b/scripts/app/javascript/components/standards/CompletionScoringBlock.tsx @@ -0,0 +1,128 @@ +import React from 'react' +import StandardTopicsRollups from './StandardTopicsRollups'; +import Button from '../shared/Button/Button'; + +type Props = { + cohortMode: 'Mastery' | 'Percentage' + checkpointSubmissionId: number + challenges: Api.ChallengeWithSubmittedChallengeAnswersPresenter[] + viewAsInstructor: boolean + checkpointSubmissionURL: string + checkpointIsDone: null | Api.CheckpointSubmissionShow.CheckpointPerformance + currentCheckpointSubmission: Api.CheckpointSubmissionShow.t | undefined + submitScore: any + retryCheckpoint: any + percentageGradeObj: {percentage: number, ungraded: number, correct: number, total: number, submissionCount: number} +} + +const CompletionScoringBlock = ({ + challenges, + viewAsInstructor, + cohortMode, + checkpointSubmissionId, + checkpointSubmissionURL, + currentCheckpointSubmission, + submitScore, + percentageGradeObj, + retryCheckpoint, +}: Props) => { + const {correct, total, percentage, ungraded } = percentageGradeObj + + const checkpointSubmissionGraded = currentCheckpointSubmission + && currentCheckpointSubmission.state + && (currentCheckpointSubmission.state === 'done'|| currentCheckpointSubmission.state === 'retry'); + + const grader = currentCheckpointSubmission ? currentCheckpointSubmission.grader : null + + let details =
    Pending
    + let info =
    + let topics + let button + + if (viewAsInstructor || checkpointSubmissionGraded) { + const graderLabel = grader && grader.name ? (grader.name + ', ') : ''; + let timezone = window.moment.tz.guess(); + + details = ( + <> +
    + {ungraded === 0 && ( +
    +

    {percentage}%

    +

    {correct}/{total} pts

    +
    + )} + {ungraded > 0 && ( + {ungraded} unscored + )} +
    + + ) + + button = ( + <> + {ungraded === 0 && viewAsInstructor && !checkpointSubmissionGraded && ( + "),e+="\n ",g.showCTA&&(e+='"),e+="\n ",g.showNext&&(e+='"),e+="\n \n ",g.showClose&&(e+='"),e+='\n\n
    \n
    \n
    \n
    \n'}}.call(m);var y=m;return y}); \ No newline at end of file diff --git a/scripts/vendor/assets/javascripts/react-tooltip.js b/scripts/vendor/assets/javascripts/react-tooltip.js new file mode 100644 index 0000000000000000000000000000000000000000..089ca5d58944c2f4d8733adc2aed488e273570a4 --- /dev/null +++ b/scripts/vendor/assets/javascripts/react-tooltip.js @@ -0,0 +1,2405 @@ +(function(f){if(typeof exports==="object"&&typeof module!=="undefined"){module.exports=f()}else if(typeof define==="function"&&define.amd){define([],f)}else{var g;if(typeof window!=="undefined"){g=window}else if(typeof global!=="undefined"){g=global}else if(typeof self!=="undefined"){g=self}else{g=this}g.ReactTooltip = f()}})(function(){var define,module,exports;return (function e(t,n,r){function s(o,u){if(!n[o]){if(!t[o]){var a=typeof require=="function"&&require;if(!u&&a)return a(o,!0);if(i)return i(o,!0);var f=new Error("Cannot find module '"+o+"'");throw f.code="MODULE_NOT_FOUND",f}var l=n[o]={exports:{}};t[o][0].call(l.exports,function(e){var n=t[o][1][e];return s(n?n:e)},l,l.exports,e,t,n,r)}return n[o].exports}var i=typeof require=="function"&&require;for(var o=0;o 1) { + for (var i = 1; i < arguments.length; i++) { + args[i - 1] = arguments[i]; + } + } + queue.push(new Item(fun, args)); + if (queue.length === 1 && !draining) { + runTimeout(drainQueue); + } + }; + +// v8 likes predictible objects + function Item(fun, array) { + this.fun = fun; + this.array = array; + } + Item.prototype.run = function () { + this.fun.apply(null, this.array); + }; + process.title = 'browser'; + process.browser = true; + process.env = {}; + process.argv = []; + process.version = ''; // empty string to avoid regexp issues + process.versions = {}; + + function noop() {} + + process.on = noop; + process.addListener = noop; + process.once = noop; + process.off = noop; + process.removeListener = noop; + process.removeAllListeners = noop; + process.emit = noop; + + process.binding = function (name) { + throw new Error('process.binding is not supported'); + }; + + process.cwd = function () { return '/' }; + process.chdir = function (dir) { + throw new Error('process.chdir is not supported'); + }; + process.umask = function() { return 0; }; + +},{}],2:[function(require,module,exports){ + "use strict"; + + /** + * Copyright (c) 2013-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + * + * + */ + + function makeEmptyFunction(arg) { + return function () { + return arg; + }; + } + + /** + * This function accepts and discards inputs; it has no side effects. This is + * primarily useful idiomatically for overridable function endpoints which + * always need to be callable, since JS lacks a null-call idiom ala Cocoa. + */ + var emptyFunction = function emptyFunction() {}; + + emptyFunction.thatReturns = makeEmptyFunction; + emptyFunction.thatReturnsFalse = makeEmptyFunction(false); + emptyFunction.thatReturnsTrue = makeEmptyFunction(true); + emptyFunction.thatReturnsNull = makeEmptyFunction(null); + emptyFunction.thatReturnsThis = function () { + return this; + }; + emptyFunction.thatReturnsArgument = function (arg) { + return arg; + }; + + module.exports = emptyFunction; +},{}],3:[function(require,module,exports){ + (function (process){ + /** + * Copyright (c) 2013-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + * + */ + + 'use strict'; + + /** + * Use invariant() to assert state which your program assumes to be true. + * + * Provide sprintf-style format (only %s is supported) and arguments + * to provide information about what broke and what you were + * expecting. + * + * The invariant message will be stripped in production, but the invariant + * will remain to ensure logic does not differ in production. + */ + + var validateFormat = function validateFormat(format) {}; + + if (process.env.NODE_ENV !== 'production') { + validateFormat = function validateFormat(format) { + if (format === undefined) { + throw new Error('invariant requires an error message argument'); + } + }; + } + + function invariant(condition, format, a, b, c, d, e, f) { + validateFormat(format); + + if (!condition) { + var error; + if (format === undefined) { + error = new Error('Minified exception occurred; use the non-minified dev environment ' + 'for the full error message and additional helpful warnings.'); + } else { + var args = [a, b, c, d, e, f]; + var argIndex = 0; + error = new Error(format.replace(/%s/g, function () { + return args[argIndex++]; + })); + error.name = 'Invariant Violation'; + } + + error.framesToPop = 1; // we don't care about invariant's own frame + throw error; + } + } + + module.exports = invariant; + }).call(this,require('_process')) +},{"_process":1}],4:[function(require,module,exports){ + (function (process){ + /** + * Copyright 2014-2015, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + * + */ + + 'use strict'; + + var emptyFunction = require('./emptyFunction'); + + /** + * Similar to invariant but only logs a warning if the condition is not met. + * This can be used to log issues in development environments in critical + * paths. Removing the logging code for production environments will keep the + * same logic and follow the same code paths. + */ + + var warning = emptyFunction; + + if (process.env.NODE_ENV !== 'production') { + (function () { + var printWarning = function printWarning(format) { + for (var _len = arguments.length, args = Array(_len > 1 ? _len - 1 : 0), _key = 1; _key < _len; _key++) { + args[_key - 1] = arguments[_key]; + } + + var argIndex = 0; + var message = 'Warning: ' + format.replace(/%s/g, function () { + return args[argIndex++]; + }); + if (typeof console !== 'undefined') { + console.error(message); + } + try { + // --- Welcome to debugging React --- + // This error was thrown as a convenience so that you can use this stack + // to find the callsite that caused this warning to fire. + throw new Error(message); + } catch (x) {} + }; + + warning = function warning(condition, format) { + if (format === undefined) { + throw new Error('`warning(condition, format, ...args)` requires a warning ' + 'message argument'); + } + + if (format.indexOf('Failed Composite propType: ') === 0) { + return; // Ignore CompositeComponent proptype check. + } + + if (!condition) { + for (var _len2 = arguments.length, args = Array(_len2 > 2 ? _len2 - 2 : 0), _key2 = 2; _key2 < _len2; _key2++) { + args[_key2 - 2] = arguments[_key2]; + } + + printWarning.apply(undefined, [format].concat(args)); + } + }; + })(); + } + + module.exports = warning; + }).call(this,require('_process')) +},{"./emptyFunction":2,"_process":1}],5:[function(require,module,exports){ + (function (process){ + /** + * Copyright 2013-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + + 'use strict'; + + if (process.env.NODE_ENV !== 'production') { + var invariant = require('fbjs/lib/invariant'); + var warning = require('fbjs/lib/warning'); + var ReactPropTypesSecret = require('./lib/ReactPropTypesSecret'); + var loggedTypeFailures = {}; + } + + /** + * Assert that the values match with the type specs. + * Error messages are memorized and will only be shown once. + * + * @param {object} typeSpecs Map of name to a ReactPropType + * @param {object} values Runtime values that need to be type-checked + * @param {string} location e.g. "prop", "context", "child context" + * @param {string} componentName Name of the component for error messages. + * @param {?Function} getStack Returns the component stack. + * @private + */ + function checkPropTypes(typeSpecs, values, location, componentName, getStack) { + if (process.env.NODE_ENV !== 'production') { + for (var typeSpecName in typeSpecs) { + if (typeSpecs.hasOwnProperty(typeSpecName)) { + var error; + // Prop type validation may throw. In case they do, we don't want to + // fail the render phase where it didn't fail before. So we log it. + // After these have been cleaned up, we'll let them throw. + try { + // This is intentionally an invariant that gets caught. It's the same + // behavior as without this statement except with a better message. + invariant(typeof typeSpecs[typeSpecName] === 'function', '%s: %s type `%s` is invalid; it must be a function, usually from ' + 'React.PropTypes.', componentName || 'React class', location, typeSpecName); + error = typeSpecs[typeSpecName](values, typeSpecName, componentName, location, null, ReactPropTypesSecret); + } catch (ex) { + error = ex; + } + warning(!error || error instanceof Error, '%s: type specification of %s `%s` is invalid; the type checker ' + 'function must return `null` or an `Error` but returned a %s. ' + 'You may have forgotten to pass an argument to the type checker ' + 'creator (arrayOf, instanceOf, objectOf, oneOf, oneOfType, and ' + 'shape all require an argument).', componentName || 'React class', location, typeSpecName, typeof error); + if (error instanceof Error && !(error.message in loggedTypeFailures)) { + // Only monitor this failure once because there tends to be a lot of the + // same error. + loggedTypeFailures[error.message] = true; + + var stack = getStack ? getStack() : ''; + + warning(false, 'Failed %s type: %s%s', location, error.message, stack != null ? stack : ''); + } + } + } + } + } + + module.exports = checkPropTypes; + + }).call(this,require('_process')) +},{"./lib/ReactPropTypesSecret":9,"_process":1,"fbjs/lib/invariant":3,"fbjs/lib/warning":4}],6:[function(require,module,exports){ + /** + * Copyright 2013-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + + 'use strict'; + + var emptyFunction = require('fbjs/lib/emptyFunction'); + var invariant = require('fbjs/lib/invariant'); + + module.exports = function() { + // Important! + // Keep this list in sync with production version in `./factoryWithTypeCheckers.js`. + function shim() { + invariant( + false, + 'Calling PropTypes validators directly is not supported by the `prop-types` package. ' + + 'Use PropTypes.checkPropTypes() to call them. ' + + 'Read more at http://fb.me/use-check-prop-types' + ); + }; + shim.isRequired = shim; + function getShim() { + return shim; + }; + var ReactPropTypes = { + array: shim, + bool: shim, + func: shim, + number: shim, + object: shim, + string: shim, + symbol: shim, + + any: shim, + arrayOf: getShim, + element: shim, + instanceOf: getShim, + node: shim, + objectOf: getShim, + oneOf: getShim, + oneOfType: getShim, + shape: getShim + }; + + ReactPropTypes.checkPropTypes = emptyFunction; + ReactPropTypes.PropTypes = ReactPropTypes; + + return ReactPropTypes; + }; + +},{"fbjs/lib/emptyFunction":2,"fbjs/lib/invariant":3}],7:[function(require,module,exports){ + (function (process){ + /** + * Copyright 2013-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + + 'use strict'; + + var emptyFunction = require('fbjs/lib/emptyFunction'); + var invariant = require('fbjs/lib/invariant'); + var warning = require('fbjs/lib/warning'); + + var ReactPropTypesSecret = require('./lib/ReactPropTypesSecret'); + var checkPropTypes = require('./checkPropTypes'); + + module.exports = function(isValidElement, throwOnDirectAccess) { + /* global Symbol */ + var ITERATOR_SYMBOL = typeof Symbol === 'function' && Symbol.iterator; + var FAUX_ITERATOR_SYMBOL = '@@iterator'; // Before Symbol spec. + + /** + * Returns the iterator method function contained on the iterable object. + * + * Be sure to invoke the function with the iterable as context: + * + * var iteratorFn = getIteratorFn(myIterable); + * if (iteratorFn) { + * var iterator = iteratorFn.call(myIterable); + * ... + * } + * + * @param {?object} maybeIterable + * @return {?function} + */ + function getIteratorFn(maybeIterable) { + var iteratorFn = maybeIterable && (ITERATOR_SYMBOL && maybeIterable[ITERATOR_SYMBOL] || maybeIterable[FAUX_ITERATOR_SYMBOL]); + if (typeof iteratorFn === 'function') { + return iteratorFn; + } + } + + /** + * Collection of methods that allow declaration and validation of props that are + * supplied to React components. Example usage: + * + * var Props = require('ReactPropTypes'); + * var MyArticle = React.createClass({ + * propTypes: { + * // An optional string prop named "description". + * description: Props.string, + * + * // A required enum prop named "category". + * category: Props.oneOf(['News','Photos']).isRequired, + * + * // A prop named "dialog" that requires an instance of Dialog. + * dialog: Props.instanceOf(Dialog).isRequired + * }, + * render: function() { ... } + * }); + * + * A more formal specification of how these methods are used: + * + * type := array|bool|func|object|number|string|oneOf([...])|instanceOf(...) + * decl := ReactPropTypes.{type}(.isRequired)? + * + * Each and every declaration produces a function with the same signature. This + * allows the creation of custom validation functions. For example: + * + * var MyLink = React.createClass({ + * propTypes: { + * // An optional string or URI prop named "href". + * href: function(props, propName, componentName) { + * var propValue = props[propName]; + * if (propValue != null && typeof propValue !== 'string' && + * !(propValue instanceof URI)) { + * return new Error( + * 'Expected a string or an URI for ' + propName + ' in ' + + * componentName + * ); + * } + * } + * }, + * render: function() {...} + * }); + * + * @internal + */ + + var ANONYMOUS = '<>'; + + // Important! + // Keep this list in sync with production version in `./factoryWithThrowingShims.js`. + var ReactPropTypes = { + array: createPrimitiveTypeChecker('array'), + bool: createPrimitiveTypeChecker('boolean'), + func: createPrimitiveTypeChecker('function'), + number: createPrimitiveTypeChecker('number'), + object: createPrimitiveTypeChecker('object'), + string: createPrimitiveTypeChecker('string'), + symbol: createPrimitiveTypeChecker('symbol'), + + any: createAnyTypeChecker(), + arrayOf: createArrayOfTypeChecker, + element: createElementTypeChecker(), + instanceOf: createInstanceTypeChecker, + node: createNodeChecker(), + objectOf: createObjectOfTypeChecker, + oneOf: createEnumTypeChecker, + oneOfType: createUnionTypeChecker, + shape: createShapeTypeChecker + }; + + /** + * inlined Object.is polyfill to avoid requiring consumers ship their own + * https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/is + */ + /*eslint-disable no-self-compare*/ + function is(x, y) { + // SameValue algorithm + if (x === y) { + // Steps 1-5, 7-10 + // Steps 6.b-6.e: +0 != -0 + return x !== 0 || 1 / x === 1 / y; + } else { + // Step 6.a: NaN == NaN + return x !== x && y !== y; + } + } + /*eslint-enable no-self-compare*/ + + /** + * We use an Error-like object for backward compatibility as people may call + * PropTypes directly and inspect their output. However, we don't use real + * Errors anymore. We don't inspect their stack anyway, and creating them + * is prohibitively expensive if they are created too often, such as what + * happens in oneOfType() for any type before the one that matched. + */ + function PropTypeError(message) { + this.message = message; + this.stack = ''; + } + // Make `instanceof Error` still work for returned errors. + PropTypeError.prototype = Error.prototype; + + function createChainableTypeChecker(validate) { + if (process.env.NODE_ENV !== 'production') { + var manualPropTypeCallCache = {}; + var manualPropTypeWarningCount = 0; + } + function checkType(isRequired, props, propName, componentName, location, propFullName, secret) { + componentName = componentName || ANONYMOUS; + propFullName = propFullName || propName; + + if (secret !== ReactPropTypesSecret) { + if (throwOnDirectAccess) { + // New behavior only for users of `prop-types` package + invariant( + false, + 'Calling PropTypes validators directly is not supported by the `prop-types` package. ' + + 'Use `PropTypes.checkPropTypes()` to call them. ' + + 'Read more at http://fb.me/use-check-prop-types' + ); + } else if (process.env.NODE_ENV !== 'production' && typeof console !== 'undefined') { + // Old behavior for people using React.PropTypes + var cacheKey = componentName + ':' + propName; + if ( + !manualPropTypeCallCache[cacheKey] && + // Avoid spamming the console because they are often not actionable except for lib authors + manualPropTypeWarningCount < 3 + ) { + warning( + false, + 'You are manually calling a React.PropTypes validation ' + + 'function for the `%s` prop on `%s`. This is deprecated ' + + 'and will throw in the standalone `prop-types` package. ' + + 'You may be seeing this warning due to a third-party PropTypes ' + + 'library. See https://fb.me/react-warning-dont-call-proptypes ' + 'for details.', + propFullName, + componentName + ); + manualPropTypeCallCache[cacheKey] = true; + manualPropTypeWarningCount++; + } + } + } + if (props[propName] == null) { + if (isRequired) { + if (props[propName] === null) { + return new PropTypeError('The ' + location + ' `' + propFullName + '` is marked as required ' + ('in `' + componentName + '`, but its value is `null`.')); + } + return new PropTypeError('The ' + location + ' `' + propFullName + '` is marked as required in ' + ('`' + componentName + '`, but its value is `undefined`.')); + } + return null; + } else { + return validate(props, propName, componentName, location, propFullName); + } + } + + var chainedCheckType = checkType.bind(null, false); + chainedCheckType.isRequired = checkType.bind(null, true); + + return chainedCheckType; + } + + function createPrimitiveTypeChecker(expectedType) { + function validate(props, propName, componentName, location, propFullName, secret) { + var propValue = props[propName]; + var propType = getPropType(propValue); + if (propType !== expectedType) { + // `propValue` being instance of, say, date/regexp, pass the 'object' + // check, but we can offer a more precise error message here rather than + // 'of type `object`'. + var preciseType = getPreciseType(propValue); + + return new PropTypeError('Invalid ' + location + ' `' + propFullName + '` of type ' + ('`' + preciseType + '` supplied to `' + componentName + '`, expected ') + ('`' + expectedType + '`.')); + } + return null; + } + return createChainableTypeChecker(validate); + } + + function createAnyTypeChecker() { + return createChainableTypeChecker(emptyFunction.thatReturnsNull); + } + + function createArrayOfTypeChecker(typeChecker) { + function validate(props, propName, componentName, location, propFullName) { + if (typeof typeChecker !== 'function') { + return new PropTypeError('Property `' + propFullName + '` of component `' + componentName + '` has invalid PropType notation inside arrayOf.'); + } + var propValue = props[propName]; + if (!Array.isArray(propValue)) { + var propType = getPropType(propValue); + return new PropTypeError('Invalid ' + location + ' `' + propFullName + '` of type ' + ('`' + propType + '` supplied to `' + componentName + '`, expected an array.')); + } + for (var i = 0; i < propValue.length; i++) { + var error = typeChecker(propValue, i, componentName, location, propFullName + '[' + i + ']', ReactPropTypesSecret); + if (error instanceof Error) { + return error; + } + } + return null; + } + return createChainableTypeChecker(validate); + } + + function createElementTypeChecker() { + function validate(props, propName, componentName, location, propFullName) { + var propValue = props[propName]; + if (!isValidElement(propValue)) { + var propType = getPropType(propValue); + return new PropTypeError('Invalid ' + location + ' `' + propFullName + '` of type ' + ('`' + propType + '` supplied to `' + componentName + '`, expected a single ReactElement.')); + } + return null; + } + return createChainableTypeChecker(validate); + } + + function createInstanceTypeChecker(expectedClass) { + function validate(props, propName, componentName, location, propFullName) { + if (!(props[propName] instanceof expectedClass)) { + var expectedClassName = expectedClass.name || ANONYMOUS; + var actualClassName = getClassName(props[propName]); + return new PropTypeError('Invalid ' + location + ' `' + propFullName + '` of type ' + ('`' + actualClassName + '` supplied to `' + componentName + '`, expected ') + ('instance of `' + expectedClassName + '`.')); + } + return null; + } + return createChainableTypeChecker(validate); + } + + function createEnumTypeChecker(expectedValues) { + if (!Array.isArray(expectedValues)) { + process.env.NODE_ENV !== 'production' ? warning(false, 'Invalid argument supplied to oneOf, expected an instance of array.') : void 0; + return emptyFunction.thatReturnsNull; + } + + function validate(props, propName, componentName, location, propFullName) { + var propValue = props[propName]; + for (var i = 0; i < expectedValues.length; i++) { + if (is(propValue, expectedValues[i])) { + return null; + } + } + + var valuesString = JSON.stringify(expectedValues); + return new PropTypeError('Invalid ' + location + ' `' + propFullName + '` of value `' + propValue + '` ' + ('supplied to `' + componentName + '`, expected one of ' + valuesString + '.')); + } + return createChainableTypeChecker(validate); + } + + function createObjectOfTypeChecker(typeChecker) { + function validate(props, propName, componentName, location, propFullName) { + if (typeof typeChecker !== 'function') { + return new PropTypeError('Property `' + propFullName + '` of component `' + componentName + '` has invalid PropType notation inside objectOf.'); + } + var propValue = props[propName]; + var propType = getPropType(propValue); + if (propType !== 'object') { + return new PropTypeError('Invalid ' + location + ' `' + propFullName + '` of type ' + ('`' + propType + '` supplied to `' + componentName + '`, expected an object.')); + } + for (var key in propValue) { + if (propValue.hasOwnProperty(key)) { + var error = typeChecker(propValue, key, componentName, location, propFullName + '.' + key, ReactPropTypesSecret); + if (error instanceof Error) { + return error; + } + } + } + return null; + } + return createChainableTypeChecker(validate); + } + + function createUnionTypeChecker(arrayOfTypeCheckers) { + if (!Array.isArray(arrayOfTypeCheckers)) { + process.env.NODE_ENV !== 'production' ? warning(false, 'Invalid argument supplied to oneOfType, expected an instance of array.') : void 0; + return emptyFunction.thatReturnsNull; + } + + function validate(props, propName, componentName, location, propFullName) { + for (var i = 0; i < arrayOfTypeCheckers.length; i++) { + var checker = arrayOfTypeCheckers[i]; + if (checker(props, propName, componentName, location, propFullName, ReactPropTypesSecret) == null) { + return null; + } + } + + return new PropTypeError('Invalid ' + location + ' `' + propFullName + '` supplied to ' + ('`' + componentName + '`.')); + } + return createChainableTypeChecker(validate); + } + + function createNodeChecker() { + function validate(props, propName, componentName, location, propFullName) { + if (!isNode(props[propName])) { + return new PropTypeError('Invalid ' + location + ' `' + propFullName + '` supplied to ' + ('`' + componentName + '`, expected a ReactNode.')); + } + return null; + } + return createChainableTypeChecker(validate); + } + + function createShapeTypeChecker(shapeTypes) { + function validate(props, propName, componentName, location, propFullName) { + var propValue = props[propName]; + var propType = getPropType(propValue); + if (propType !== 'object') { + return new PropTypeError('Invalid ' + location + ' `' + propFullName + '` of type `' + propType + '` ' + ('supplied to `' + componentName + '`, expected `object`.')); + } + for (var key in shapeTypes) { + var checker = shapeTypes[key]; + if (!checker) { + continue; + } + var error = checker(propValue, key, componentName, location, propFullName + '.' + key, ReactPropTypesSecret); + if (error) { + return error; + } + } + return null; + } + return createChainableTypeChecker(validate); + } + + function isNode(propValue) { + switch (typeof propValue) { + case 'number': + case 'string': + case 'undefined': + return true; + case 'boolean': + return !propValue; + case 'object': + if (Array.isArray(propValue)) { + return propValue.every(isNode); + } + if (propValue === null || isValidElement(propValue)) { + return true; + } + + var iteratorFn = getIteratorFn(propValue); + if (iteratorFn) { + var iterator = iteratorFn.call(propValue); + var step; + if (iteratorFn !== propValue.entries) { + while (!(step = iterator.next()).done) { + if (!isNode(step.value)) { + return false; + } + } + } else { + // Iterator will provide entry [k,v] tuples rather than values. + while (!(step = iterator.next()).done) { + var entry = step.value; + if (entry) { + if (!isNode(entry[1])) { + return false; + } + } + } + } + } else { + return false; + } + + return true; + default: + return false; + } + } + + function isSymbol(propType, propValue) { + // Native Symbol. + if (propType === 'symbol') { + return true; + } + + // 19.4.3.5 Symbol.prototype[@@toStringTag] === 'Symbol' + if (propValue['@@toStringTag'] === 'Symbol') { + return true; + } + + // Fallback for non-spec compliant Symbols which are polyfilled. + if (typeof Symbol === 'function' && propValue instanceof Symbol) { + return true; + } + + return false; + } + + // Equivalent of `typeof` but with special handling for array and regexp. + function getPropType(propValue) { + var propType = typeof propValue; + if (Array.isArray(propValue)) { + return 'array'; + } + if (propValue instanceof RegExp) { + // Old webkits (at least until Android 4.0) return 'function' rather than + // 'object' for typeof a RegExp. We'll normalize this here so that /bla/ + // passes PropTypes.object. + return 'object'; + } + if (isSymbol(propType, propValue)) { + return 'symbol'; + } + return propType; + } + + // This handles more types than `getPropType`. Only used for error messages. + // See `createPrimitiveTypeChecker`. + function getPreciseType(propValue) { + var propType = getPropType(propValue); + if (propType === 'object') { + if (propValue instanceof Date) { + return 'date'; + } else if (propValue instanceof RegExp) { + return 'regexp'; + } + } + return propType; + } + + // Returns class name of the object, if any. + function getClassName(propValue) { + if (!propValue.constructor || !propValue.constructor.name) { + return ANONYMOUS; + } + return propValue.constructor.name; + } + + ReactPropTypes.checkPropTypes = checkPropTypes; + ReactPropTypes.PropTypes = ReactPropTypes; + + return ReactPropTypes; + }; + + }).call(this,require('_process')) +},{"./checkPropTypes":5,"./lib/ReactPropTypesSecret":9,"_process":1,"fbjs/lib/emptyFunction":2,"fbjs/lib/invariant":3,"fbjs/lib/warning":4}],8:[function(require,module,exports){ + (function (process){ + /** + * Copyright 2013-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + + if (process.env.NODE_ENV !== 'production') { + var REACT_ELEMENT_TYPE = (typeof Symbol === 'function' && + Symbol.for && + Symbol.for('react.element')) || + 0xeac7; + + var isValidElement = function(object) { + return typeof object === 'object' && + object !== null && + object.$$typeof === REACT_ELEMENT_TYPE; + }; + + // By explicitly using `prop-types` you are opting into new development behavior. + // http://fb.me/prop-types-in-prod + var throwOnDirectAccess = true; + module.exports = require('./factoryWithTypeCheckers')(isValidElement, throwOnDirectAccess); + } else { + // By explicitly using `prop-types` you are opting into new production behavior. + // http://fb.me/prop-types-in-prod + module.exports = require('./factoryWithThrowingShims')(); + } + + }).call(this,require('_process')) +},{"./factoryWithThrowingShims":6,"./factoryWithTypeCheckers":7,"_process":1}],9:[function(require,module,exports){ + /** + * Copyright 2013-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + + 'use strict'; + + var ReactPropTypesSecret = 'SECRET_DO_NOT_PASS_THIS_OR_YOU_WILL_BE_FIRED'; + + module.exports = ReactPropTypesSecret; + +},{}],10:[function(require,module,exports){ + /*! + Copyright (c) 2016 Jed Watson. + Licensed under the MIT License (MIT), see + http://jedwatson.github.io/classnames + */ + /* global define */ + + (function () { + 'use strict'; + + var hasOwn = {}.hasOwnProperty; + + function classNames () { + var classes = []; + + for (var i = 0; i < arguments.length; i++) { + var arg = arguments[i]; + if (!arg) continue; + + var argType = typeof arg; + + if (argType === 'string' || argType === 'number') { + classes.push(arg); + } else if (Array.isArray(arg)) { + classes.push(classNames.apply(null, arg)); + } else if (argType === 'object') { + for (var key in arg) { + if (hasOwn.call(arg, key) && arg[key]) { + classes.push(key); + } + } + } + } + + return classes.join(' '); + } + + if (typeof module !== 'undefined' && module.exports) { + module.exports = classNames; + } else if (typeof define === 'function' && typeof define.amd === 'object' && define.amd) { + // register as 'classnames', consistent with npm package name + define('classnames', [], function () { + return classNames; + }); + } else { + window.classNames = classNames; + } + }()); + +},{}],11:[function(require,module,exports){ + 'use strict'; + + Object.defineProperty(exports, "__esModule", { + value: true + }); + exports.default = { + + GLOBAL: { + HIDE: '__react_tooltip_hide_event', + REBUILD: '__react_tooltip_rebuild_event', + SHOW: '__react_tooltip_show_event' + } + }; + +},{}],12:[function(require,module,exports){ + 'use strict'; + + Object.defineProperty(exports, "__esModule", { + value: true + }); + + exports.default = function (target) { + target.prototype.isCustomEvent = function (ele) { + var event = this.state.event; + + return event || !!ele.getAttribute('data-event'); + }; + + /* Bind listener for custom event */ + target.prototype.customBindListener = function (ele) { + var _this = this; + + var _state = this.state, + event = _state.event, + eventOff = _state.eventOff; + + var dataEvent = ele.getAttribute('data-event') || event; + var dataEventOff = ele.getAttribute('data-event-off') || eventOff; + + dataEvent.split(' ').forEach(function (event) { + ele.removeEventListener(event, customListener); + customListener = checkStatus.bind(_this, dataEventOff); + ele.addEventListener(event, customListener, false); + }); + if (dataEventOff) { + dataEventOff.split(' ').forEach(function (event) { + ele.removeEventListener(event, _this.hideTooltip); + ele.addEventListener(event, _this.hideTooltip, false); + }); + } + }; + + /* Unbind listener for custom event */ + target.prototype.customUnbindListener = function (ele) { + var _state2 = this.state, + event = _state2.event, + eventOff = _state2.eventOff; + + var dataEvent = event || ele.getAttribute('data-event'); + var dataEventOff = eventOff || ele.getAttribute('data-event-off'); + + ele.removeEventListener(dataEvent, customListener); + if (dataEventOff) ele.removeEventListener(dataEventOff, this.hideTooltip); + }; + }; + + /** + * Custom events to control showing and hiding of tooltip + * + * @attributes + * - `event` {String} + * - `eventOff` {String} + */ + + var checkStatus = function checkStatus(dataEventOff, e) { + var show = this.state.show; + var id = this.props.id; + + var dataIsCapture = e.currentTarget.getAttribute('data-iscapture'); + var isCapture = dataIsCapture && dataIsCapture === 'true' || this.props.isCapture; + var currentItem = e.currentTarget.getAttribute('currentItem'); + + if (!isCapture) e.stopPropagation(); + if (show && currentItem === 'true') { + if (!dataEventOff) this.hideTooltip(e); + } else { + e.currentTarget.setAttribute('currentItem', 'true'); + setUntargetItems(e.currentTarget, this.getTargetArray(id)); + this.showTooltip(e); + } + }; + + var setUntargetItems = function setUntargetItems(currentTarget, targetArray) { + for (var i = 0; i < targetArray.length; i++) { + if (currentTarget !== targetArray[i]) { + targetArray[i].setAttribute('currentItem', 'false'); + } else { + targetArray[i].setAttribute('currentItem', 'true'); + } + } + }; + + var customListener = void 0; + +},{}],13:[function(require,module,exports){ + 'use strict'; + + Object.defineProperty(exports, "__esModule", { + value: true + }); + + exports.default = function (target) { + target.prototype.getEffect = function (currentTarget) { + var dataEffect = currentTarget.getAttribute('data-effect'); + return dataEffect || this.props.effect || 'float'; + }; + }; + +},{}],14:[function(require,module,exports){ + 'use strict'; + + Object.defineProperty(exports, "__esModule", { + value: true + }); + + exports.default = function (target) { + target.prototype.isCapture = function (currentTarget) { + var dataIsCapture = currentTarget.getAttribute('data-iscapture'); + return dataIsCapture && dataIsCapture === 'true' || this.props.isCapture || false; + }; + }; + +},{}],15:[function(require,module,exports){ + 'use strict'; + + Object.defineProperty(exports, "__esModule", { + value: true + }); + + exports.default = function (target) { + /** + * Hide all tooltip + * @trigger ReactTooltip.hide() + */ + target.hide = function (target) { + dispatchGlobalEvent(_constant2.default.GLOBAL.HIDE, { target: target }); + }; + + /** + * Rebuild all tooltip + * @trigger ReactTooltip.rebuild() + */ + target.rebuild = function () { + dispatchGlobalEvent(_constant2.default.GLOBAL.REBUILD); + }; + + /** + * Show specific tooltip + * @trigger ReactTooltip.show() + */ + target.show = function (target) { + dispatchGlobalEvent(_constant2.default.GLOBAL.SHOW, { target: target }); + }; + + target.prototype.globalRebuild = function () { + if (this.mount) { + this.unbindListener(); + this.bindListener(); + } + }; + + target.prototype.globalShow = function (event) { + if (this.mount) { + // Create a fake event, specific show will limit the type to `solid` + // only `float` type cares e.clientX e.clientY + var e = { currentTarget: event.detail.target }; + this.showTooltip(e, true); + } + }; + + target.prototype.globalHide = function (event) { + if (this.mount) { + var hasTarget = event && event.detail && event.detail.target && true || false; + this.hideTooltip({ currentTarget: hasTarget && event.detail.target }, hasTarget); + } + }; + }; + + var _constant = require('../constant'); + + var _constant2 = _interopRequireDefault(_constant); + + function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } + + var dispatchGlobalEvent = function dispatchGlobalEvent(eventName, opts) { + // Compatibale with IE + // @see http://stackoverflow.com/questions/26596123/internet-explorer-9-10-11-event-constructor-doesnt-work + var event = void 0; + + if (typeof window.CustomEvent === 'function') { + event = new window.CustomEvent(eventName, { detail: opts }); + } else { + event = document.createEvent('Event'); + event.initEvent(eventName, false, true); + event.detail = opts; + } + + window.dispatchEvent(event); + }; /** + * Static methods for react-tooltip + */ + +},{"../constant":11}],16:[function(require,module,exports){ + "use strict"; + + Object.defineProperty(exports, "__esModule", { + value: true + }); + + exports.default = function (target) { + target.prototype.bindRemovalTracker = function () { + var _this = this; + + var MutationObserver = getMutationObserverClass(); + if (MutationObserver == null) return; + + var observer = new MutationObserver(function (mutations) { + var _iteratorNormalCompletion = true; + var _didIteratorError = false; + var _iteratorError = undefined; + + try { + for (var _iterator = mutations[Symbol.iterator](), _step; !(_iteratorNormalCompletion = (_step = _iterator.next()).done); _iteratorNormalCompletion = true) { + var mutation = _step.value; + var _iteratorNormalCompletion2 = true; + var _didIteratorError2 = false; + var _iteratorError2 = undefined; + + try { + for (var _iterator2 = mutation.removedNodes[Symbol.iterator](), _step2; !(_iteratorNormalCompletion2 = (_step2 = _iterator2.next()).done); _iteratorNormalCompletion2 = true) { + var element = _step2.value; + + if (element === _this.state.currentTarget) { + _this.hideTooltip(); + return; + } + } + } catch (err) { + _didIteratorError2 = true; + _iteratorError2 = err; + } finally { + try { + if (!_iteratorNormalCompletion2 && _iterator2.return) { + _iterator2.return(); + } + } finally { + if (_didIteratorError2) { + throw _iteratorError2; + } + } + } + } + } catch (err) { + _didIteratorError = true; + _iteratorError = err; + } finally { + try { + if (!_iteratorNormalCompletion && _iterator.return) { + _iterator.return(); + } + } finally { + if (_didIteratorError) { + throw _iteratorError; + } + } + } + }); + + observer.observe(window.document, { childList: true, subtree: true }); + + this.removalTracker = observer; + }; + + target.prototype.unbindRemovalTracker = function () { + if (this.removalTracker) { + this.removalTracker.disconnect(); + this.removalTracker = null; + } + }; + }; + + /** + * Tracking target removing from DOM. + * It's nessesary to hide tooltip when it's target disappears. + * Otherwise, the tooltip would be shown forever until another target + * is triggered. + * + * If MutationObserver is not available, this feature just doesn't work. + */ + +// https://hacks.mozilla.org/2012/05/dom-mutationobserver-reacting-to-dom-changes-without-killing-browser-performance/ + var getMutationObserverClass = function getMutationObserverClass() { + return window.MutationObserver || window.WebKitMutationObserver || window.MozMutationObserver; + }; + +},{}],17:[function(require,module,exports){ + 'use strict'; + + Object.defineProperty(exports, "__esModule", { + value: true + }); + + exports.default = function (target) { + target.prototype.bindWindowEvents = function (resizeHide) { + // ReactTooltip.hide + window.removeEventListener(_constant2.default.GLOBAL.HIDE, this.globalHide); + window.addEventListener(_constant2.default.GLOBAL.HIDE, this.globalHide, false); + + // ReactTooltip.rebuild + window.removeEventListener(_constant2.default.GLOBAL.REBUILD, this.globalRebuild); + window.addEventListener(_constant2.default.GLOBAL.REBUILD, this.globalRebuild, false); + + // ReactTooltip.show + window.removeEventListener(_constant2.default.GLOBAL.SHOW, this.globalShow); + window.addEventListener(_constant2.default.GLOBAL.SHOW, this.globalShow, false); + + // Resize + if (resizeHide) { + window.removeEventListener('resize', this.onWindowResize); + window.addEventListener('resize', this.onWindowResize, false); + } + }; + + target.prototype.unbindWindowEvents = function () { + window.removeEventListener(_constant2.default.GLOBAL.HIDE, this.globalHide); + window.removeEventListener(_constant2.default.GLOBAL.REBUILD, this.globalRebuild); + window.removeEventListener(_constant2.default.GLOBAL.SHOW, this.globalShow); + window.removeEventListener('resize', this.onWindowResize); + }; + + /** + * invoked by resize event of window + */ + target.prototype.onWindowResize = function () { + if (!this.mount) return; + this.hideTooltip(); + }; + }; + + var _constant = require('../constant'); + + var _constant2 = _interopRequireDefault(_constant); + + function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } + +},{"../constant":11}],18:[function(require,module,exports){ + (function (global){ + 'use strict'; + + var _extends = Object.assign || function (target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i]; for (var key in source) { if (Object.prototype.hasOwnProperty.call(source, key)) { target[key] = source[key]; } } } return target; }; + + var _createClass = function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; }(); + + var _class, _class2, _temp; + + /* Decoraters */ + + + /* Utils */ + + + /* CSS */ + + + var _react = (typeof window !== "undefined" ? window['React'] : typeof global !== "undefined" ? global['React'] : null); + + var _react2 = _interopRequireDefault(_react); + + var _propTypes = require('prop-types'); + + var _propTypes2 = _interopRequireDefault(_propTypes); + + var _reactDom = (typeof window !== "undefined" ? window['ReactDOM'] : typeof global !== "undefined" ? global['ReactDOM'] : null); + + var _reactDom2 = _interopRequireDefault(_reactDom); + + var _classnames = require('classnames'); + + var _classnames2 = _interopRequireDefault(_classnames); + + var _staticMethods = require('./decorators/staticMethods'); + + var _staticMethods2 = _interopRequireDefault(_staticMethods); + + var _windowListener = require('./decorators/windowListener'); + + var _windowListener2 = _interopRequireDefault(_windowListener); + + var _customEvent = require('./decorators/customEvent'); + + var _customEvent2 = _interopRequireDefault(_customEvent); + + var _isCapture = require('./decorators/isCapture'); + + var _isCapture2 = _interopRequireDefault(_isCapture); + + var _getEffect = require('./decorators/getEffect'); + + var _getEffect2 = _interopRequireDefault(_getEffect); + + var _trackRemoval = require('./decorators/trackRemoval'); + + var _trackRemoval2 = _interopRequireDefault(_trackRemoval); + + var _getPosition = require('./utils/getPosition'); + + var _getPosition2 = _interopRequireDefault(_getPosition); + + var _getTipContent = require('./utils/getTipContent'); + + var _getTipContent2 = _interopRequireDefault(_getTipContent); + + var _aria = require('./utils/aria'); + + var _nodeListToArray = require('./utils/nodeListToArray'); + + var _nodeListToArray2 = _interopRequireDefault(_nodeListToArray); + + var _style = require('./style'); + + var _style2 = _interopRequireDefault(_style); + + function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } + + function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } } + + function _possibleConstructorReturn(self, call) { if (!self) { throw new ReferenceError("this hasn't been initialised - super() hasn't been called"); } return call && (typeof call === "object" || typeof call === "function") ? call : self; } + + function _inherits(subClass, superClass) { if (typeof superClass !== "function" && superClass !== null) { throw new TypeError("Super expression must either be null or a function, not " + typeof superClass); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, enumerable: false, writable: true, configurable: true } }); if (superClass) Object.setPrototypeOf ? Object.setPrototypeOf(subClass, superClass) : subClass.__proto__ = superClass; } + + var ReactTooltip = (0, _staticMethods2.default)(_class = (0, _windowListener2.default)(_class = (0, _customEvent2.default)(_class = (0, _isCapture2.default)(_class = (0, _getEffect2.default)(_class = (0, _trackRemoval2.default)(_class = (_temp = _class2 = function (_Component) { + _inherits(ReactTooltip, _Component); + + function ReactTooltip(props) { + _classCallCheck(this, ReactTooltip); + + var _this = _possibleConstructorReturn(this, (ReactTooltip.__proto__ || Object.getPrototypeOf(ReactTooltip)).call(this, props)); + + _this.state = { + place: 'top', // Direction of tooltip + type: 'dark', // Color theme of tooltip + effect: 'float', // float or fixed + show: false, + border: false, + placeholder: '', + offset: {}, + extraClass: '', + html: false, + delayHide: 0, + delayShow: 0, + event: props.event || null, + eventOff: props.eventOff || null, + currentEvent: null, // Current mouse event + currentTarget: null, // Current target of mouse event + ariaProps: (0, _aria.parseAria)(props), // aria- and role attributes + isEmptyTip: false, + disable: false + }; + + _this.bind(['showTooltip', 'updateTooltip', 'hideTooltip', 'globalRebuild', 'globalShow', 'globalHide', 'onWindowResize']); + + _this.mount = true; + _this.delayShowLoop = null; + _this.delayHideLoop = null; + _this.intervalUpdateContent = null; + return _this; + } + + /** + * For unify the bind and unbind listener + */ + + + _createClass(ReactTooltip, [{ + key: 'bind', + value: function bind(methodArray) { + var _this2 = this; + + methodArray.forEach(function (method) { + _this2[method] = _this2[method].bind(_this2); + }); + } + }, { + key: 'componentDidMount', + value: function componentDidMount() { + var _props = this.props, + insecure = _props.insecure, + resizeHide = _props.resizeHide; + + if (insecure) { + this.setStyleHeader(); // Set the style to the + } + this.bindListener(); // Bind listener for tooltip + this.bindWindowEvents(resizeHide); // Bind global event for static method + } + }, { + key: 'componentWillReceiveProps', + value: function componentWillReceiveProps(props) { + var ariaProps = this.state.ariaProps; + + var newAriaProps = (0, _aria.parseAria)(props); + + var isChanged = Object.keys(newAriaProps).some(function (props) { + return newAriaProps[props] !== ariaProps[props]; + }); + if (isChanged) { + this.setState({ ariaProps: newAriaProps }); + } + } + }, { + key: 'componentWillUnmount', + value: function componentWillUnmount() { + this.mount = false; + + this.clearTimer(); + + this.unbindListener(); + this.removeScrollListener(); + this.unbindWindowEvents(); + } + + /** + * Pick out corresponded target elements + */ + + }, { + key: 'getTargetArray', + value: function getTargetArray(id) { + var targetArray = void 0; + if (!id) { + targetArray = document.querySelectorAll('[data-tip]:not([data-for])'); + } else { + var escaped = id.replace(/\\/g, '\\\\').replace(/"/g, '\\"'); + targetArray = document.querySelectorAll('[data-tip][data-for="' + escaped + '"]'); + } + // targetArray is a NodeList, convert it to a real array + return (0, _nodeListToArray2.default)(targetArray); + } + + /** + * Bind listener to the target elements + * These listeners used to trigger showing or hiding the tooltip + */ + + }, { + key: 'bindListener', + value: function bindListener() { + var _this3 = this; + + var _props2 = this.props, + id = _props2.id, + globalEventOff = _props2.globalEventOff; + + var targetArray = this.getTargetArray(id); + + targetArray.forEach(function (target) { + var isCaptureMode = _this3.isCapture(target); + var effect = _this3.getEffect(target); + if (target.getAttribute('currentItem') === null) { + target.setAttribute('currentItem', 'false'); + } + _this3.unbindBasicListener(target); + + if (_this3.isCustomEvent(target)) { + _this3.customBindListener(target); + return; + } + + target.addEventListener('mouseenter', _this3.showTooltip, isCaptureMode); + if (effect === 'float') { + target.addEventListener('mousemove', _this3.updateTooltip, isCaptureMode); + } + target.addEventListener('mouseleave', _this3.hideTooltip, isCaptureMode); + }); + + // Global event to hide tooltip + if (globalEventOff) { + window.removeEventListener(globalEventOff, this.hideTooltip); + window.addEventListener(globalEventOff, this.hideTooltip, false); + } + + // Track removal of targetArray elements from DOM + this.bindRemovalTracker(); + } + + /** + * Unbind listeners on target elements + */ + + }, { + key: 'unbindListener', + value: function unbindListener() { + var _this4 = this; + + var _props3 = this.props, + id = _props3.id, + globalEventOff = _props3.globalEventOff; + + var targetArray = this.getTargetArray(id); + targetArray.forEach(function (target) { + _this4.unbindBasicListener(target); + if (_this4.isCustomEvent(target)) _this4.customUnbindListener(target); + }); + + if (globalEventOff) window.removeEventListener(globalEventOff, this.hideTooltip); + this.unbindRemovalTracker(); + } + + /** + * Invoke this before bind listener and ummount the compont + * it is necessary to invloke this even when binding custom event + * so that the tooltip can switch between custom and default listener + */ + + }, { + key: 'unbindBasicListener', + value: function unbindBasicListener(target) { + var isCaptureMode = this.isCapture(target); + target.removeEventListener('mouseenter', this.showTooltip, isCaptureMode); + target.removeEventListener('mousemove', this.updateTooltip, isCaptureMode); + target.removeEventListener('mouseleave', this.hideTooltip, isCaptureMode); + } + + /** + * When mouse enter, show the tooltip + */ + + }, { + key: 'showTooltip', + value: function showTooltip(e, isGlobalCall) { + var _this5 = this; + + if (isGlobalCall) { + // Don't trigger other elements belongs to other ReactTooltip + var targetArray = this.getTargetArray(this.props.id); + var isMyElement = targetArray.some(function (ele) { + return ele === e.currentTarget; + }); + if (!isMyElement || this.state.show) return; + } + // Get the tooltip content + // calculate in this phrase so that tip width height can be detected + var _props4 = this.props, + children = _props4.children, + multiline = _props4.multiline, + getContent = _props4.getContent; + + var originTooltip = e.currentTarget.getAttribute('data-tip'); + var isMultiline = e.currentTarget.getAttribute('data-multiline') || multiline || false; + + // Generate tootlip content + var content = void 0; + if (getContent) { + if (Array.isArray(getContent)) { + content = getContent[0] && getContent[0](); + } else { + content = getContent(); + } + } + var placeholder = (0, _getTipContent2.default)(originTooltip, children, content, isMultiline); + var isEmptyTip = typeof placeholder === 'string' && placeholder === '' || placeholder === null; + + // If it is focus event or called by ReactTooltip.show, switch to `solid` effect + var switchToSolid = e instanceof window.FocusEvent || isGlobalCall; + + // if it need to skip adding hide listener to scroll + var scrollHide = true; + if (e.currentTarget.getAttribute('data-scroll-hide')) { + scrollHide = e.currentTarget.getAttribute('data-scroll-hide') === 'true'; + } else if (this.props.scrollHide != null) { + scrollHide = this.props.scrollHide; + } + + // To prevent previously created timers from triggering + this.clearTimer(); + + this.setState({ + placeholder: placeholder, + isEmptyTip: isEmptyTip, + place: e.currentTarget.getAttribute('data-place') || this.props.place || 'top', + type: e.currentTarget.getAttribute('data-type') || this.props.type || 'dark', + effect: switchToSolid && 'solid' || this.getEffect(e.currentTarget), + offset: e.currentTarget.getAttribute('data-offset') || this.props.offset || {}, + html: e.currentTarget.getAttribute('data-html') ? e.currentTarget.getAttribute('data-html') === 'true' : this.props.html || false, + delayShow: e.currentTarget.getAttribute('data-delay-show') || this.props.delayShow || 0, + delayHide: e.currentTarget.getAttribute('data-delay-hide') || this.props.delayHide || 0, + border: e.currentTarget.getAttribute('data-border') ? e.currentTarget.getAttribute('data-border') === 'true' : this.props.border || false, + extraClass: e.currentTarget.getAttribute('data-class') || this.props.class || this.props.className || '', + disable: e.currentTarget.getAttribute('data-tip-disable') ? e.currentTarget.getAttribute('data-tip-disable') === 'true' : this.props.disable || false + }, function () { + if (scrollHide) _this5.addScrollListener(e); + _this5.updateTooltip(e); + + if (getContent && Array.isArray(getContent)) { + _this5.intervalUpdateContent = setInterval(function () { + if (_this5.mount) { + var _getContent = _this5.props.getContent; + + var _placeholder = (0, _getTipContent2.default)(originTooltip, _getContent[0](), isMultiline); + var _isEmptyTip = typeof _placeholder === 'string' && _placeholder === ''; + _this5.setState({ + placeholder: _placeholder, + isEmptyTip: _isEmptyTip + }); + } + }, getContent[1]); + } + }); + } + + /** + * When mouse hover, updatetooltip + */ + + }, { + key: 'updateTooltip', + value: function updateTooltip(e) { + var _this6 = this; + + var _state = this.state, + delayShow = _state.delayShow, + show = _state.show, + isEmptyTip = _state.isEmptyTip, + disable = _state.disable; + var afterShow = this.props.afterShow; + var placeholder = this.state.placeholder; + + var delayTime = show ? 0 : parseInt(delayShow, 10); + var eventTarget = e.currentTarget; + + if (isEmptyTip || disable) return; // if the tooltip is empty, disable the tooltip + var updateState = function updateState() { + if (Array.isArray(placeholder) && placeholder.length > 0 || placeholder) { + (function () { + var isInvisible = !_this6.state.show; + _this6.setState({ + currentEvent: e, + currentTarget: eventTarget, + show: true + }, function () { + _this6.updatePosition(); + if (isInvisible && afterShow) afterShow(); + }); + })(); + } + }; + + clearTimeout(this.delayShowLoop); + if (delayShow) { + this.delayShowLoop = setTimeout(updateState, delayTime); + } else { + updateState(); + } + } + + /** + * When mouse leave, hide tooltip + */ + + }, { + key: 'hideTooltip', + value: function hideTooltip(e, hasTarget) { + var _this7 = this; + + var _state2 = this.state, + delayHide = _state2.delayHide, + isEmptyTip = _state2.isEmptyTip, + disable = _state2.disable; + var afterHide = this.props.afterHide; + + if (!this.mount) return; + if (isEmptyTip || disable) return; // if the tooltip is empty, disable the tooltip + if (hasTarget) { + // Don't trigger other elements belongs to other ReactTooltip + var targetArray = this.getTargetArray(this.props.id); + var isMyElement = targetArray.some(function (ele) { + return ele === e.currentTarget; + }); + if (!isMyElement || !this.state.show) return; + } + var resetState = function resetState() { + var isVisible = _this7.state.show; + _this7.setState({ + show: false + }, function () { + _this7.removeScrollListener(); + if (isVisible && afterHide) afterHide(); + }); + }; + + this.clearTimer(); + if (delayHide) { + this.delayHideLoop = setTimeout(resetState, parseInt(delayHide, 10)); + } else { + resetState(); + } + } + + /** + * Add scroll eventlistener when tooltip show + * automatically hide the tooltip when scrolling + */ + + }, { + key: 'addScrollListener', + value: function addScrollListener(e) { + var isCaptureMode = this.isCapture(e.currentTarget); + window.addEventListener('scroll', this.hideTooltip, isCaptureMode); + } + }, { + key: 'removeScrollListener', + value: function removeScrollListener() { + window.removeEventListener('scroll', this.hideTooltip); + } + + // Calculation the position + + }, { + key: 'updatePosition', + value: function updatePosition() { + var _this8 = this; + + var _state3 = this.state, + currentEvent = _state3.currentEvent, + currentTarget = _state3.currentTarget, + place = _state3.place, + effect = _state3.effect, + offset = _state3.offset; + + var node = _reactDom2.default.findDOMNode(this); + var result = (0, _getPosition2.default)(currentEvent, currentTarget, node, place, effect, offset); + + if (result.isNewState) { + // Switch to reverse placement + return this.setState(result.newState, function () { + _this8.updatePosition(); + }); + } + // Set tooltip position + node.style.left = result.position.left + 'px'; + node.style.top = result.position.top + 'px'; + } + + /** + * Set style tag in header + * in this way we can insert default css + */ + + }, { + key: 'setStyleHeader', + value: function setStyleHeader() { + if (!document.getElementsByTagName('head')[0].querySelector('style[id="react-tooltip"]')) { + var tag = document.createElement('style'); + tag.id = 'react-tooltip'; + tag.innerHTML = _style2.default; + document.getElementsByTagName('head')[0].appendChild(tag); + } + } + + /** + * CLear all kinds of timeout of interval + */ + + }, { + key: 'clearTimer', + value: function clearTimer() { + clearTimeout(this.delayShowLoop); + clearTimeout(this.delayHideLoop); + clearInterval(this.intervalUpdateContent); + } + }, { + key: 'render', + value: function render() { + var _state4 = this.state, + placeholder = _state4.placeholder, + extraClass = _state4.extraClass, + html = _state4.html, + ariaProps = _state4.ariaProps, + disable = _state4.disable, + isEmptyTip = _state4.isEmptyTip; + + var tooltipClass = (0, _classnames2.default)('__react_component_tooltip', { 'show': this.state.show && !disable && !isEmptyTip }, { 'border': this.state.border }, { 'place-top': this.state.place === 'top' }, { 'place-bottom': this.state.place === 'bottom' }, { 'place-left': this.state.place === 'left' }, { 'place-right': this.state.place === 'right' }, { 'type-dark': this.state.type === 'dark' }, { 'type-success': this.state.type === 'success' }, { 'type-warning': this.state.type === 'warning' }, { 'type-error': this.state.type === 'error' }, { 'type-info': this.state.type === 'info' }, { 'type-light': this.state.type === 'light' }); + + var Wrapper = this.props.wrapper; + if (ReactTooltip.supportedWrappers.indexOf(Wrapper) < 0) { + Wrapper = ReactTooltip.defaultProps.wrapper; + } + + if (html) { + return _react2.default.createElement(Wrapper, _extends({ className: tooltipClass + ' ' + extraClass + }, ariaProps, { + 'data-id': 'tooltip', + dangerouslySetInnerHTML: { __html: placeholder } })); + } else { + return _react2.default.createElement( + Wrapper, + _extends({ className: tooltipClass + ' ' + extraClass + }, ariaProps, { + 'data-id': 'tooltip' }), + placeholder + ); + } + } + }]); + + return ReactTooltip; + }(_react.Component), _class2.propTypes = { + children: _propTypes2.default.any, + place: _propTypes2.default.string, + type: _propTypes2.default.string, + effect: _propTypes2.default.string, + offset: _propTypes2.default.object, + multiline: _propTypes2.default.bool, + border: _propTypes2.default.bool, + insecure: _propTypes2.default.bool, + class: _propTypes2.default.string, + className: _propTypes2.default.string, + id: _propTypes2.default.string, + html: _propTypes2.default.bool, + delayHide: _propTypes2.default.number, + delayShow: _propTypes2.default.number, + event: _propTypes2.default.string, + eventOff: _propTypes2.default.string, + watchWindow: _propTypes2.default.bool, + isCapture: _propTypes2.default.bool, + globalEventOff: _propTypes2.default.string, + getContent: _propTypes2.default.any, + afterShow: _propTypes2.default.func, + afterHide: _propTypes2.default.func, + disable: _propTypes2.default.bool, + scrollHide: _propTypes2.default.bool, + resizeHide: _propTypes2.default.bool, + wrapper: _propTypes2.default.string + }, _class2.defaultProps = { + insecure: true, + resizeHide: true, + wrapper: 'div' + }, _class2.supportedWrappers = ['div', 'span'], _temp)) || _class) || _class) || _class) || _class) || _class) || _class; + + /* export default not fit for standalone, it will exports {default:...} */ + + + module.exports = ReactTooltip; + + }).call(this,typeof global !== "undefined" ? global : typeof self !== "undefined" ? self : typeof window !== "undefined" ? window : {}) +},{"./decorators/customEvent":12,"./decorators/getEffect":13,"./decorators/isCapture":14,"./decorators/staticMethods":15,"./decorators/trackRemoval":16,"./decorators/windowListener":17,"./style":19,"./utils/aria":20,"./utils/getPosition":21,"./utils/getTipContent":22,"./utils/nodeListToArray":23,"classnames":10,"prop-types":8}],19:[function(require,module,exports){ + 'use strict'; + + Object.defineProperty(exports, "__esModule", { + value: true + }); + exports.default = '.__react_component_tooltip{border-radius:3px;display:inline-block;font-size:13px;left:-999em;opacity:0;padding:8px 21px;position:fixed;pointer-events:none;transition:opacity 0.3s ease-out;top:-999em;visibility:hidden;z-index:999}.__react_component_tooltip:before,.__react_component_tooltip:after{content:"";width:0;height:0;position:absolute}.__react_component_tooltip.show{opacity:0.9;margin-top:0px;margin-left:0px;visibility:visible}.__react_component_tooltip.type-dark{color:#fff;background-color:#222}.__react_component_tooltip.type-dark.place-top:after{border-top-color:#222;border-top-style:solid;border-top-width:6px}.__react_component_tooltip.type-dark.place-bottom:after{border-bottom-color:#222;border-bottom-style:solid;border-bottom-width:6px}.__react_component_tooltip.type-dark.place-left:after{border-left-color:#222;border-left-style:solid;border-left-width:6px}.__react_component_tooltip.type-dark.place-right:after{border-right-color:#222;border-right-style:solid;border-right-width:6px}.__react_component_tooltip.type-dark.border{border:1px solid #fff}.__react_component_tooltip.type-dark.border.place-top:before{border-top:8px solid #fff}.__react_component_tooltip.type-dark.border.place-bottom:before{border-bottom:8px solid #fff}.__react_component_tooltip.type-dark.border.place-left:before{border-left:8px solid #fff}.__react_component_tooltip.type-dark.border.place-right:before{border-right:8px solid #fff}.__react_component_tooltip.type-success{color:#fff;background-color:#8DC572}.__react_component_tooltip.type-success.place-top:after{border-top-color:#8DC572;border-top-style:solid;border-top-width:6px}.__react_component_tooltip.type-success.place-bottom:after{border-bottom-color:#8DC572;border-bottom-style:solid;border-bottom-width:6px}.__react_component_tooltip.type-success.place-left:after{border-left-color:#8DC572;border-left-style:solid;border-left-width:6px}.__react_component_tooltip.type-success.place-right:after{border-right-color:#8DC572;border-right-style:solid;border-right-width:6px}.__react_component_tooltip.type-success.border{border:1px solid #fff}.__react_component_tooltip.type-success.border.place-top:before{border-top:8px solid #fff}.__react_component_tooltip.type-success.border.place-bottom:before{border-bottom:8px solid #fff}.__react_component_tooltip.type-success.border.place-left:before{border-left:8px solid #fff}.__react_component_tooltip.type-success.border.place-right:before{border-right:8px solid #fff}.__react_component_tooltip.type-warning{color:#fff;background-color:#F0AD4E}.__react_component_tooltip.type-warning.place-top:after{border-top-color:#F0AD4E;border-top-style:solid;border-top-width:6px}.__react_component_tooltip.type-warning.place-bottom:after{border-bottom-color:#F0AD4E;border-bottom-style:solid;border-bottom-width:6px}.__react_component_tooltip.type-warning.place-left:after{border-left-color:#F0AD4E;border-left-style:solid;border-left-width:6px}.__react_component_tooltip.type-warning.place-right:after{border-right-color:#F0AD4E;border-right-style:solid;border-right-width:6px}.__react_component_tooltip.type-warning.border{border:1px solid #fff}.__react_component_tooltip.type-warning.border.place-top:before{border-top:8px solid #fff}.__react_component_tooltip.type-warning.border.place-bottom:before{border-bottom:8px solid #fff}.__react_component_tooltip.type-warning.border.place-left:before{border-left:8px solid #fff}.__react_component_tooltip.type-warning.border.place-right:before{border-right:8px solid #fff}.__react_component_tooltip.type-error{color:#fff;background-color:#BE6464}.__react_component_tooltip.type-error.place-top:after{border-top-color:#BE6464;border-top-style:solid;border-top-width:6px}.__react_component_tooltip.type-error.place-bottom:after{border-bottom-color:#BE6464;border-bottom-style:solid;border-bottom-width:6px}.__react_component_tooltip.type-error.place-left:after{border-left-color:#BE6464;border-left-style:solid;border-left-width:6px}.__react_component_tooltip.type-error.place-right:after{border-right-color:#BE6464;border-right-style:solid;border-right-width:6px}.__react_component_tooltip.type-error.border{border:1px solid #fff}.__react_component_tooltip.type-error.border.place-top:before{border-top:8px solid #fff}.__react_component_tooltip.type-error.border.place-bottom:before{border-bottom:8px solid #fff}.__react_component_tooltip.type-error.border.place-left:before{border-left:8px solid #fff}.__react_component_tooltip.type-error.border.place-right:before{border-right:8px solid #fff}.__react_component_tooltip.type-info{color:#fff;background-color:#337AB7}.__react_component_tooltip.type-info.place-top:after{border-top-color:#337AB7;border-top-style:solid;border-top-width:6px}.__react_component_tooltip.type-info.place-bottom:after{border-bottom-color:#337AB7;border-bottom-style:solid;border-bottom-width:6px}.__react_component_tooltip.type-info.place-left:after{border-left-color:#337AB7;border-left-style:solid;border-left-width:6px}.__react_component_tooltip.type-info.place-right:after{border-right-color:#337AB7;border-right-style:solid;border-right-width:6px}.__react_component_tooltip.type-info.border{border:1px solid #fff}.__react_component_tooltip.type-info.border.place-top:before{border-top:8px solid #fff}.__react_component_tooltip.type-info.border.place-bottom:before{border-bottom:8px solid #fff}.__react_component_tooltip.type-info.border.place-left:before{border-left:8px solid #fff}.__react_component_tooltip.type-info.border.place-right:before{border-right:8px solid #fff}.__react_component_tooltip.type-light{color:#222;background-color:#fff}.__react_component_tooltip.type-light.place-top:after{border-top-color:#fff;border-top-style:solid;border-top-width:6px}.__react_component_tooltip.type-light.place-bottom:after{border-bottom-color:#fff;border-bottom-style:solid;border-bottom-width:6px}.__react_component_tooltip.type-light.place-left:after{border-left-color:#fff;border-left-style:solid;border-left-width:6px}.__react_component_tooltip.type-light.place-right:after{border-right-color:#fff;border-right-style:solid;border-right-width:6px}.__react_component_tooltip.type-light.border{border:1px solid #222}.__react_component_tooltip.type-light.border.place-top:before{border-top:8px solid #222}.__react_component_tooltip.type-light.border.place-bottom:before{border-bottom:8px solid #222}.__react_component_tooltip.type-light.border.place-left:before{border-left:8px solid #222}.__react_component_tooltip.type-light.border.place-right:before{border-right:8px solid #222}.__react_component_tooltip.place-top{margin-top:-10px}.__react_component_tooltip.place-top:before{border-left:10px solid transparent;border-right:10px solid transparent;bottom:-8px;left:50%;margin-left:-10px}.__react_component_tooltip.place-top:after{border-left:8px solid transparent;border-right:8px solid transparent;bottom:-6px;left:50%;margin-left:-8px}.__react_component_tooltip.place-bottom{margin-top:10px}.__react_component_tooltip.place-bottom:before{border-left:10px solid transparent;border-right:10px solid transparent;top:-8px;left:50%;margin-left:-10px}.__react_component_tooltip.place-bottom:after{border-left:8px solid transparent;border-right:8px solid transparent;top:-6px;left:50%;margin-left:-8px}.__react_component_tooltip.place-left{margin-left:-10px}.__react_component_tooltip.place-left:before{border-top:6px solid transparent;border-bottom:6px solid transparent;right:-8px;top:50%;margin-top:-5px}.__react_component_tooltip.place-left:after{border-top:5px solid transparent;border-bottom:5px solid transparent;right:-6px;top:50%;margin-top:-4px}.__react_component_tooltip.place-right{margin-left:10px}.__react_component_tooltip.place-right:before{border-top:6px solid transparent;border-bottom:6px solid transparent;left:-8px;top:50%;margin-top:-5px}.__react_component_tooltip.place-right:after{border-top:5px solid transparent;border-bottom:5px solid transparent;left:-6px;top:50%;margin-top:-4px}.__react_component_tooltip .multi-line{display:block;padding:2px 0px;text-align:center}'; + +},{}],20:[function(require,module,exports){ + "use strict"; + + Object.defineProperty(exports, "__esModule", { + value: true + }); + exports.parseAria = parseAria; + /** + * Support aria- and role in ReactTooltip + * + * @params props {Object} + * @return {Object} + */ + function parseAria(props) { + var ariaObj = {}; + Object.keys(props).filter(function (prop) { + // aria-xxx and role is acceptable + return (/(^aria-\w+$|^role$)/.test(prop) + ); + }).forEach(function (prop) { + ariaObj[prop] = props[prop]; + }); + + return ariaObj; + } + +},{}],21:[function(require,module,exports){ + 'use strict'; + + Object.defineProperty(exports, "__esModule", { + value: true + }); + + exports.default = function (e, target, node, place, effect, offset) { + var tipWidth = node.clientWidth; + var tipHeight = node.clientHeight; + + var _getCurrentOffset = getCurrentOffset(e, target, effect), + mouseX = _getCurrentOffset.mouseX, + mouseY = _getCurrentOffset.mouseY; + + var defaultOffset = getDefaultPosition(effect, target.clientWidth, target.clientHeight, tipWidth, tipHeight); + + var _calculateOffset = calculateOffset(offset), + extraOffset_X = _calculateOffset.extraOffset_X, + extraOffset_Y = _calculateOffset.extraOffset_Y; + + var windowWidth = window.innerWidth; + var windowHeight = window.innerHeight; + + var _getParent = getParent(node), + parentTop = _getParent.parentTop, + parentLeft = _getParent.parentLeft; + + // Get the edge offset of the tooltip + + + var getTipOffsetLeft = function getTipOffsetLeft(place) { + var offset_X = defaultOffset[place].l; + return mouseX + offset_X + extraOffset_X; + }; + var getTipOffsetRight = function getTipOffsetRight(place) { + var offset_X = defaultOffset[place].r; + return mouseX + offset_X + extraOffset_X; + }; + var getTipOffsetTop = function getTipOffsetTop(place) { + var offset_Y = defaultOffset[place].t; + return mouseY + offset_Y + extraOffset_Y; + }; + var getTipOffsetBottom = function getTipOffsetBottom(place) { + var offset_Y = defaultOffset[place].b; + return mouseY + offset_Y + extraOffset_Y; + }; + + // Judge if the tooltip has over the window(screen) + var outsideVertical = function outsideVertical() { + var result = false; + var newPlace = void 0; + if (getTipOffsetTop('left') < 0 && getTipOffsetBottom('left') <= windowHeight && getTipOffsetBottom('bottom') <= windowHeight) { + result = true; + newPlace = 'bottom'; + } else if (getTipOffsetBottom('left') > windowHeight && getTipOffsetTop('left') >= 0 && getTipOffsetTop('top') >= 0) { + result = true; + newPlace = 'top'; + } + return { result: result, newPlace: newPlace }; + }; + var outsideLeft = function outsideLeft() { + var _outsideVertical = outsideVertical(), + result = _outsideVertical.result, + newPlace = _outsideVertical.newPlace; // Deal with vertical as first priority + + + if (result && outsideHorizontal().result) { + return { result: false }; // No need to change, if change to vertical will out of space + } + if (!result && getTipOffsetLeft('left') < 0 && getTipOffsetRight('right') <= windowWidth) { + result = true; // If vertical ok, but let out of side and right won't out of side + newPlace = 'right'; + } + return { result: result, newPlace: newPlace }; + }; + var outsideRight = function outsideRight() { + var _outsideVertical2 = outsideVertical(), + result = _outsideVertical2.result, + newPlace = _outsideVertical2.newPlace; + + if (result && outsideHorizontal().result) { + return { result: false }; // No need to change, if change to vertical will out of space + } + if (!result && getTipOffsetRight('right') > windowWidth && getTipOffsetLeft('left') >= 0) { + result = true; + newPlace = 'left'; + } + return { result: result, newPlace: newPlace }; + }; + + var outsideHorizontal = function outsideHorizontal() { + var result = false; + var newPlace = void 0; + if (getTipOffsetLeft('top') < 0 && getTipOffsetRight('top') <= windowWidth && getTipOffsetRight('right') <= windowWidth) { + result = true; + newPlace = 'right'; + } else if (getTipOffsetRight('top') > windowWidth && getTipOffsetLeft('top') >= 0 && getTipOffsetLeft('left') >= 0) { + result = true; + newPlace = 'left'; + } + return { result: result, newPlace: newPlace }; + }; + var outsideTop = function outsideTop() { + var _outsideHorizontal = outsideHorizontal(), + result = _outsideHorizontal.result, + newPlace = _outsideHorizontal.newPlace; + + if (result && outsideVertical().result) { + return { result: false }; + } + if (!result && getTipOffsetTop('top') < 0 && getTipOffsetBottom('bottom') <= windowHeight) { + result = true; + newPlace = 'bottom'; + } + return { result: result, newPlace: newPlace }; + }; + var outsideBottom = function outsideBottom() { + var _outsideHorizontal2 = outsideHorizontal(), + result = _outsideHorizontal2.result, + newPlace = _outsideHorizontal2.newPlace; + + if (result && outsideVertical().result) { + return { result: false }; + } + if (!result && getTipOffsetBottom('bottom') > windowHeight && getTipOffsetTop('top') >= 0) { + result = true; + newPlace = 'top'; + } + return { result: result, newPlace: newPlace }; + }; + + // Return new state to change the placement to the reverse if possible + var outsideLeftResult = outsideLeft(); + var outsideRightResult = outsideRight(); + var outsideTopResult = outsideTop(); + var outsideBottomResult = outsideBottom(); + + if (place === 'left' && outsideLeftResult.result) { + return { + isNewState: true, + newState: { place: outsideLeftResult.newPlace } + }; + } else if (place === 'right' && outsideRightResult.result) { + return { + isNewState: true, + newState: { place: outsideRightResult.newPlace } + }; + } else if (place === 'top' && outsideTopResult.result) { + return { + isNewState: true, + newState: { place: outsideTopResult.newPlace } + }; + } else if (place === 'bottom' && outsideBottomResult.result) { + return { + isNewState: true, + newState: { place: outsideBottomResult.newPlace } + }; + } + + // Return tooltip offset position + return { + isNewState: false, + position: { + left: parseInt(getTipOffsetLeft(place) - parentLeft, 10), + top: parseInt(getTipOffsetTop(place) - parentTop, 10) + } + }; + }; + +// Get current mouse offset + var getCurrentOffset = function getCurrentOffset(e, currentTarget, effect) { + var boundingClientRect = currentTarget.getBoundingClientRect(); + var targetTop = boundingClientRect.top; + var targetLeft = boundingClientRect.left; + var targetWidth = currentTarget.clientWidth; + var targetHeight = currentTarget.clientHeight; + + if (effect === 'float') { + return { + mouseX: e.clientX, + mouseY: e.clientY + }; + } + return { + mouseX: targetLeft + targetWidth / 2, + mouseY: targetTop + targetHeight / 2 + }; + }; + +// List all possibility of tooltip final offset +// This is useful in judging if it is necessary for tooltip to switch position when out of window + /** + * Calculate the position of tooltip + * + * @params + * - `e` {Event} the event of current mouse + * - `target` {Element} the currentTarget of the event + * - `node` {DOM} the react-tooltip object + * - `place` {String} top / right / bottom / left + * - `effect` {String} float / solid + * - `offset` {Object} the offset to default position + * + * @return {Object + * - `isNewState` {Bool} required + * - `newState` {Object} + * - `position` {OBject} {left: {Number}, top: {Number}} + */ + var getDefaultPosition = function getDefaultPosition(effect, targetWidth, targetHeight, tipWidth, tipHeight) { + var top = void 0; + var right = void 0; + var bottom = void 0; + var left = void 0; + var disToMouse = 3; + var triangleHeight = 2; + var cursorHeight = 12; // Optimize for float bottom only, cause the cursor will hide the tooltip + + if (effect === 'float') { + top = { + l: -(tipWidth / 2), + r: tipWidth / 2, + t: -(tipHeight + disToMouse + triangleHeight), + b: -disToMouse + }; + bottom = { + l: -(tipWidth / 2), + r: tipWidth / 2, + t: disToMouse + cursorHeight, + b: tipHeight + disToMouse + triangleHeight + cursorHeight + }; + left = { + l: -(tipWidth + disToMouse + triangleHeight), + r: -disToMouse, + t: -(tipHeight / 2), + b: tipHeight / 2 + }; + right = { + l: disToMouse, + r: tipWidth + disToMouse + triangleHeight, + t: -(tipHeight / 2), + b: tipHeight / 2 + }; + } else if (effect === 'solid') { + top = { + l: -(tipWidth / 2), + r: tipWidth / 2, + t: -(targetHeight / 2 + tipHeight + triangleHeight), + b: -(targetHeight / 2) + }; + bottom = { + l: -(tipWidth / 2), + r: tipWidth / 2, + t: targetHeight / 2, + b: targetHeight / 2 + tipHeight + triangleHeight + }; + left = { + l: -(tipWidth + targetWidth / 2 + triangleHeight), + r: -(targetWidth / 2), + t: -(tipHeight / 2), + b: tipHeight / 2 + }; + right = { + l: targetWidth / 2, + r: tipWidth + targetWidth / 2 + triangleHeight, + t: -(tipHeight / 2), + b: tipHeight / 2 + }; + } + + return { top: top, bottom: bottom, left: left, right: right }; + }; + +// Consider additional offset into position calculation + var calculateOffset = function calculateOffset(offset) { + var extraOffset_X = 0; + var extraOffset_Y = 0; + + if (Object.prototype.toString.apply(offset) === '[object String]') { + offset = JSON.parse(offset.toString().replace(/\'/g, '\"')); + } + for (var key in offset) { + if (key === 'top') { + extraOffset_Y -= parseInt(offset[key], 10); + } else if (key === 'bottom') { + extraOffset_Y += parseInt(offset[key], 10); + } else if (key === 'left') { + extraOffset_X -= parseInt(offset[key], 10); + } else if (key === 'right') { + extraOffset_X += parseInt(offset[key], 10); + } + } + + return { extraOffset_X: extraOffset_X, extraOffset_Y: extraOffset_Y }; + }; + +// Get the offset of the parent elements + var getParent = function getParent(currentTarget) { + var currentParent = currentTarget; + while (currentParent) { + if (window.getComputedStyle(currentParent).getPropertyValue('transform') !== 'none') break; + currentParent = currentParent.parentElement; + } + + var parentTop = currentParent && currentParent.getBoundingClientRect().top || 0; + var parentLeft = currentParent && currentParent.getBoundingClientRect().left || 0; + + return { parentTop: parentTop, parentLeft: parentLeft }; + }; + +},{}],22:[function(require,module,exports){ + (function (global){ + 'use strict'; + + Object.defineProperty(exports, "__esModule", { + value: true + }); + + exports.default = function (tip, children, getContent, multiline) { + if (children) return children; + if (getContent !== undefined && getContent !== null) return getContent; // getContent can be 0, '', etc. + if (getContent === null) return null; // Tip not exist and childern is null or undefined + + var regexp = //; + if (!multiline || multiline === 'false' || !regexp.test(tip)) { + // No trim(), so that user can keep their input + return tip; + } + + // Multiline tooltip content + return tip.split(regexp).map(function (d, i) { + return _react2.default.createElement( + 'span', + { key: i, className: 'multi-line' }, + d + ); + }); + }; + + var _react = (typeof window !== "undefined" ? window['React'] : typeof global !== "undefined" ? global['React'] : null); + + var _react2 = _interopRequireDefault(_react); + + function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } + + }).call(this,typeof global !== "undefined" ? global : typeof self !== "undefined" ? self : typeof window !== "undefined" ? window : {}) +},{}],23:[function(require,module,exports){ + "use strict"; + + Object.defineProperty(exports, "__esModule", { + value: true + }); + + exports.default = function (nodeList) { + var length = nodeList.length; + if (nodeList.hasOwnProperty) { + return Array.prototype.slice.call(nodeList); + } + return new Array(length).fill().map(function (index) { + return nodeList[index]; + }); + }; + +},{}]},{},[18])(18) +}); \ No newline at end of file diff --git a/scripts/vendor/assets/stylesheets/bootstrap-datepicker.css b/scripts/vendor/assets/stylesheets/bootstrap-datepicker.css new file mode 100644 index 0000000000000000000000000000000000000000..408f3afa97f78e22ce09b9173ca376589209f20d --- /dev/null +++ b/scripts/vendor/assets/stylesheets/bootstrap-datepicker.css @@ -0,0 +1,7 @@ +/*! + * Datepicker for Bootstrap v1.8.0 (https://github.com/uxsolutions/bootstrap-datepicker) + * + * Licensed under the Apache License v2.0 (http://www.apache.org/licenses/LICENSE-2.0) + */ + +.datepicker{padding:4px;-webkit-border-radius:4px;-moz-border-radius:4px;border-radius:4px;direction:ltr}.datepicker-inline{width:220px}.datepicker-rtl{direction:rtl}.datepicker-rtl.dropdown-menu{left:auto}.datepicker-rtl table tr td span{float:right}.datepicker-dropdown{top:0;left:0}.datepicker-dropdown:before{content:'';display:inline-block;border-left:7px solid transparent;border-right:7px solid transparent;border-bottom:7px solid #999;border-top:0;border-bottom-color:rgba(0,0,0,.2);position:absolute}.datepicker-dropdown:after{content:'';display:inline-block;border-left:6px solid transparent;border-right:6px solid transparent;border-bottom:6px solid #fff;border-top:0;position:absolute}.datepicker-dropdown.datepicker-orient-left:before{left:6px}.datepicker-dropdown.datepicker-orient-left:after{left:7px}.datepicker-dropdown.datepicker-orient-right:before{right:6px}.datepicker-dropdown.datepicker-orient-right:after{right:7px}.datepicker-dropdown.datepicker-orient-bottom:before{top:-7px}.datepicker-dropdown.datepicker-orient-bottom:after{top:-6px}.datepicker-dropdown.datepicker-orient-top:before{bottom:-7px;border-bottom:0;border-top:7px solid #999}.datepicker-dropdown.datepicker-orient-top:after{bottom:-6px;border-bottom:0;border-top:6px solid #fff}.datepicker table{margin:0;-webkit-touch-callout:none;-webkit-user-select:none;-khtml-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none}.datepicker td,.datepicker th{text-align:center;width:20px;height:20px;-webkit-border-radius:4px;-moz-border-radius:4px;border-radius:4px;border:none}.table-striped .datepicker table tr td,.table-striped .datepicker table tr th{background-color:transparent}.datepicker table tr td.day.focused,.datepicker table tr td.day:hover{background:#eee;cursor:pointer}.datepicker table tr td.new,.datepicker table tr td.old{color:#999}.datepicker table tr td.disabled,.datepicker table tr td.disabled:hover{background:0 0;color:#999;cursor:default}.datepicker table tr td.highlighted{background:#d9edf7;border-radius:0}.datepicker table tr td.today,.datepicker table tr td.today.disabled,.datepicker table tr td.today.disabled:hover,.datepicker table tr td.today:hover{background-color:#fde19a;background-image:-moz-linear-gradient(to bottom,#fdd49a,#fdf59a);background-image:-ms-linear-gradient(to bottom,#fdd49a,#fdf59a);background-image:-webkit-gradient(linear,0 0,0 100%,from(#fdd49a),to(#fdf59a));background-image:-webkit-linear-gradient(to bottom,#fdd49a,#fdf59a);background-image:-o-linear-gradient(to bottom,#fdd49a,#fdf59a);background-image:linear-gradient(to bottom,#fdd49a,#fdf59a);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#fdd49a', endColorstr='#fdf59a', GradientType=0);border-color:#fdf59a #fdf59a #fbed50;border-color:rgba(0,0,0,.1) rgba(0,0,0,.1) rgba(0,0,0,.25);filter:progid:DXImageTransform.Microsoft.gradient(enabled=false);color:#000}.datepicker table tr td.today.active,.datepicker table tr td.today.disabled,.datepicker table tr td.today.disabled.active,.datepicker table tr td.today.disabled.disabled,.datepicker table tr td.today.disabled:active,.datepicker table tr td.today.disabled:hover,.datepicker table tr td.today.disabled:hover.active,.datepicker table tr td.today.disabled:hover.disabled,.datepicker table tr td.today.disabled:hover:active,.datepicker table tr td.today.disabled:hover:hover,.datepicker table tr td.today.disabled:hover[disabled],.datepicker table tr td.today.disabled[disabled],.datepicker table tr td.today:active,.datepicker table tr td.today:hover,.datepicker table tr td.today:hover.active,.datepicker table tr td.today:hover.disabled,.datepicker table tr td.today:hover:active,.datepicker table tr td.today:hover:hover,.datepicker table tr td.today:hover[disabled],.datepicker table tr td.today[disabled]{background-color:#fdf59a}.datepicker table tr td.today.active,.datepicker table tr td.today.disabled.active,.datepicker table tr td.today.disabled:active,.datepicker table tr td.today.disabled:hover.active,.datepicker table tr td.today.disabled:hover:active,.datepicker table tr td.today:active,.datepicker table tr td.today:hover.active,.datepicker table tr td.today:hover:active{background-color:#fbf069\9}.datepicker table tr td.today:hover:hover{color:#000}.datepicker table tr td.today.active:hover{color:#fff}.datepicker table tr td.range,.datepicker table tr td.range.disabled,.datepicker table tr td.range.disabled:hover,.datepicker table tr td.range:hover{background:#eee;-webkit-border-radius:0;-moz-border-radius:0;border-radius:0}.datepicker table tr td.range.today,.datepicker table tr td.range.today.disabled,.datepicker table tr td.range.today.disabled:hover,.datepicker table tr td.range.today:hover{background-color:#f3d17a;background-image:-moz-linear-gradient(to bottom,#f3c17a,#f3e97a);background-image:-ms-linear-gradient(to bottom,#f3c17a,#f3e97a);background-image:-webkit-gradient(linear,0 0,0 100%,from(#f3c17a),to(#f3e97a));background-image:-webkit-linear-gradient(to bottom,#f3c17a,#f3e97a);background-image:-o-linear-gradient(to bottom,#f3c17a,#f3e97a);background-image:linear-gradient(to bottom,#f3c17a,#f3e97a);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#f3c17a', endColorstr='#f3e97a', GradientType=0);border-color:#f3e97a #f3e97a #edde34;border-color:rgba(0,0,0,.1) rgba(0,0,0,.1) rgba(0,0,0,.25);filter:progid:DXImageTransform.Microsoft.gradient(enabled=false);-webkit-border-radius:0;-moz-border-radius:0;border-radius:0}.datepicker table tr td.range.today.active,.datepicker table tr td.range.today.disabled,.datepicker table tr td.range.today.disabled.active,.datepicker table tr td.range.today.disabled.disabled,.datepicker table tr td.range.today.disabled:active,.datepicker table tr td.range.today.disabled:hover,.datepicker table tr td.range.today.disabled:hover.active,.datepicker table tr td.range.today.disabled:hover.disabled,.datepicker table tr td.range.today.disabled:hover:active,.datepicker table tr td.range.today.disabled:hover:hover,.datepicker table tr td.range.today.disabled:hover[disabled],.datepicker table tr td.range.today.disabled[disabled],.datepicker table tr td.range.today:active,.datepicker table tr td.range.today:hover,.datepicker table tr td.range.today:hover.active,.datepicker table tr td.range.today:hover.disabled,.datepicker table tr td.range.today:hover:active,.datepicker table tr td.range.today:hover:hover,.datepicker table tr td.range.today:hover[disabled],.datepicker table tr td.range.today[disabled]{background-color:#f3e97a}.datepicker table tr td.range.today.active,.datepicker table tr td.range.today.disabled.active,.datepicker table tr td.range.today.disabled:active,.datepicker table tr td.range.today.disabled:hover.active,.datepicker table tr td.range.today.disabled:hover:active,.datepicker table tr td.range.today:active,.datepicker table tr td.range.today:hover.active,.datepicker table tr td.range.today:hover:active{background-color:#efe24b\9}.datepicker table tr td.selected,.datepicker table tr td.selected.disabled,.datepicker table tr td.selected.disabled:hover,.datepicker table tr td.selected:hover{background-color:#9e9e9e;background-image:-moz-linear-gradient(to bottom,#b3b3b3,grey);background-image:-ms-linear-gradient(to bottom,#b3b3b3,grey);background-image:-webkit-gradient(linear,0 0,0 100%,from(#b3b3b3),to(grey));background-image:-webkit-linear-gradient(to bottom,#b3b3b3,grey);background-image:-o-linear-gradient(to bottom,#b3b3b3,grey);background-image:linear-gradient(to bottom,#b3b3b3,grey);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#b3b3b3', endColorstr='#808080', GradientType=0);border-color:grey grey #595959;border-color:rgba(0,0,0,.1) rgba(0,0,0,.1) rgba(0,0,0,.25);filter:progid:DXImageTransform.Microsoft.gradient(enabled=false);color:#fff;text-shadow:0 -1px 0 rgba(0,0,0,.25)}.datepicker table tr td.selected.active,.datepicker table tr td.selected.disabled,.datepicker table tr td.selected.disabled.active,.datepicker table tr td.selected.disabled.disabled,.datepicker table tr td.selected.disabled:active,.datepicker table tr td.selected.disabled:hover,.datepicker table tr td.selected.disabled:hover.active,.datepicker table tr td.selected.disabled:hover.disabled,.datepicker table tr td.selected.disabled:hover:active,.datepicker table tr td.selected.disabled:hover:hover,.datepicker table tr td.selected.disabled:hover[disabled],.datepicker table tr td.selected.disabled[disabled],.datepicker table tr td.selected:active,.datepicker table tr td.selected:hover,.datepicker table tr td.selected:hover.active,.datepicker table tr td.selected:hover.disabled,.datepicker table tr td.selected:hover:active,.datepicker table tr td.selected:hover:hover,.datepicker table tr td.selected:hover[disabled],.datepicker table tr td.selected[disabled]{background-color:grey}.datepicker table tr td.selected.active,.datepicker table tr td.selected.disabled.active,.datepicker table tr td.selected.disabled:active,.datepicker table tr td.selected.disabled:hover.active,.datepicker table tr td.selected.disabled:hover:active,.datepicker table tr td.selected:active,.datepicker table tr td.selected:hover.active,.datepicker table tr td.selected:hover:active{background-color:#666\9}.datepicker table tr td.active,.datepicker table tr td.active.disabled,.datepicker table tr td.active.disabled:hover,.datepicker table tr td.active:hover{background-color:#006dcc;background-image:-moz-linear-gradient(to bottom,#08c,#04c);background-image:-ms-linear-gradient(to bottom,#08c,#04c);background-image:-webkit-gradient(linear,0 0,0 100%,from(#08c),to(#04c));background-image:-webkit-linear-gradient(to bottom,#08c,#04c);background-image:-o-linear-gradient(to bottom,#08c,#04c);background-image:linear-gradient(to bottom,#08c,#04c);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#08c', endColorstr='#0044cc', GradientType=0);border-color:#04c #04c #002a80;border-color:rgba(0,0,0,.1) rgba(0,0,0,.1) rgba(0,0,0,.25);filter:progid:DXImageTransform.Microsoft.gradient(enabled=false);color:#fff;text-shadow:0 -1px 0 rgba(0,0,0,.25)}.datepicker table tr td.active.active,.datepicker table tr td.active.disabled,.datepicker table tr td.active.disabled.active,.datepicker table tr td.active.disabled.disabled,.datepicker table tr td.active.disabled:active,.datepicker table tr td.active.disabled:hover,.datepicker table tr td.active.disabled:hover.active,.datepicker table tr td.active.disabled:hover.disabled,.datepicker table tr td.active.disabled:hover:active,.datepicker table tr td.active.disabled:hover:hover,.datepicker table tr td.active.disabled:hover[disabled],.datepicker table tr td.active.disabled[disabled],.datepicker table tr td.active:active,.datepicker table tr td.active:hover,.datepicker table tr td.active:hover.active,.datepicker table tr td.active:hover.disabled,.datepicker table tr td.active:hover:active,.datepicker table tr td.active:hover:hover,.datepicker table tr td.active:hover[disabled],.datepicker table tr td.active[disabled]{background-color:#04c}.datepicker table tr td.active.active,.datepicker table tr td.active.disabled.active,.datepicker table tr td.active.disabled:active,.datepicker table tr td.active.disabled:hover.active,.datepicker table tr td.active.disabled:hover:active,.datepicker table tr td.active:active,.datepicker table tr td.active:hover.active,.datepicker table tr td.active:hover:active{background-color:#039\9}.datepicker table tr td span{display:block;width:23%;height:54px;line-height:54px;float:left;margin:1%;cursor:pointer;-webkit-border-radius:4px;-moz-border-radius:4px;border-radius:4px}.datepicker table tr td span.focused,.datepicker table tr td span:hover{background:#eee}.datepicker table tr td span.disabled,.datepicker table tr td span.disabled:hover{background:0 0;color:#999;cursor:default}.datepicker table tr td span.active,.datepicker table tr td span.active.disabled,.datepicker table tr td span.active.disabled:hover,.datepicker table tr td span.active:hover{background-color:#006dcc;background-image:-moz-linear-gradient(to bottom,#08c,#04c);background-image:-ms-linear-gradient(to bottom,#08c,#04c);background-image:-webkit-gradient(linear,0 0,0 100%,from(#08c),to(#04c));background-image:-webkit-linear-gradient(to bottom,#08c,#04c);background-image:-o-linear-gradient(to bottom,#08c,#04c);background-image:linear-gradient(to bottom,#08c,#04c);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#08c', endColorstr='#0044cc', GradientType=0);border-color:#04c #04c #002a80;border-color:rgba(0,0,0,.1) rgba(0,0,0,.1) rgba(0,0,0,.25);filter:progid:DXImageTransform.Microsoft.gradient(enabled=false);color:#fff;text-shadow:0 -1px 0 rgba(0,0,0,.25)}.datepicker table tr td span.active.active,.datepicker table tr td span.active.disabled,.datepicker table tr td span.active.disabled.active,.datepicker table tr td span.active.disabled.disabled,.datepicker table tr td span.active.disabled:active,.datepicker table tr td span.active.disabled:hover,.datepicker table tr td span.active.disabled:hover.active,.datepicker table tr td span.active.disabled:hover.disabled,.datepicker table tr td span.active.disabled:hover:active,.datepicker table tr td span.active.disabled:hover:hover,.datepicker table tr td span.active.disabled:hover[disabled],.datepicker table tr td span.active.disabled[disabled],.datepicker table tr td span.active:active,.datepicker table tr td span.active:hover,.datepicker table tr td span.active:hover.active,.datepicker table tr td span.active:hover.disabled,.datepicker table tr td span.active:hover:active,.datepicker table tr td span.active:hover:hover,.datepicker table tr td span.active:hover[disabled],.datepicker table tr td span.active[disabled]{background-color:#04c}.datepicker table tr td span.active.active,.datepicker table tr td span.active.disabled.active,.datepicker table tr td span.active.disabled:active,.datepicker table tr td span.active.disabled:hover.active,.datepicker table tr td span.active.disabled:hover:active,.datepicker table tr td span.active:active,.datepicker table tr td span.active:hover.active,.datepicker table tr td span.active:hover:active{background-color:#039\9}.datepicker table tr td span.new,.datepicker table tr td span.old{color:#999}.datepicker .datepicker-switch{width:145px}.datepicker .datepicker-switch,.datepicker .next,.datepicker .prev,.datepicker tfoot tr th{cursor:pointer}.datepicker .datepicker-switch:hover,.datepicker .next:hover,.datepicker .prev:hover,.datepicker tfoot tr th:hover{background:#eee}.datepicker .next.disabled,.datepicker .prev.disabled{visibility:hidden}.datepicker .cw{font-size:10px;width:12px;padding:0 2px 0 5px;vertical-align:middle}.input-append.date .add-on,.input-prepend.date .add-on{cursor:pointer}.input-append.date .add-on i,.input-prepend.date .add-on i{margin-top:3px}.input-daterange input{text-align:center}.input-daterange input:first-child{-webkit-border-radius:3px 0 0 3px;-moz-border-radius:3px 0 0 3px;border-radius:3px 0 0 3px}.input-daterange input:last-child{-webkit-border-radius:0 3px 3px 0;-moz-border-radius:0 3px 3px 0;border-radius:0 3px 3px 0}.input-daterange .add-on{display:inline-block;width:auto;min-width:16px;height:18px;padding:4px 5px;font-weight:400;line-height:18px;text-align:center;text-shadow:0 1px 0 #fff;vertical-align:middle;background-color:#eee;border:1px solid #ccc;margin-left:-5px;margin-right:-5px} \ No newline at end of file diff --git a/scripts/vendor/assets/stylesheets/hopscotch.css b/scripts/vendor/assets/stylesheets/hopscotch.css new file mode 100755 index 0000000000000000000000000000000000000000..72c12340ff640f3df37cd8294e470b53744a613d --- /dev/null +++ b/scripts/vendor/assets/stylesheets/hopscotch.css @@ -0,0 +1,17 @@ +/**! hopscotch - v0.3.1 +* +* Copyright 2017 LinkedIn Corp. All rights reserved. +* +* Licensed under the Apache License, Version 2.0 (the "License"); +* you may not use this file except in compliance with the License. +* You may obtain a copy of the License at +* +* http://www.apache.org/licenses/LICENSE-2.0 +* +* Unless required by applicable law or agreed to in writing, software +* distributed under the License is distributed on an "AS IS" BASIS, +* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +* See the License for the specific language governing permissions and +* limitations under the License. +*/ +.animated{-webkit-animation-fill-mode:both;-moz-animation-fill-mode:both;-ms-animation-fill-mode:both;-o-animation-fill-mode:both;animation-fill-mode:both;-webkit-animation-duration:1s;-moz-animation-duration:1s;-ms-animation-duration:1s;-o-animation-duration:1s;animation-duration:1s}@-webkit-keyframes fadeInUp{0%{opacity:0;-webkit-transform:translateY(20px)}100%{opacity:1;-webkit-transform:translateY(0)}}@-moz-keyframes fadeInUp{0%{opacity:0;-moz-transform:translateY(20px)}100%{opacity:1;-moz-transform:translateY(0)}}@-o-keyframes fadeInUp{0%{opacity:0;-o-transform:translateY(20px)}100%{opacity:1;-o-transform:translateY(0)}}@keyframes fadeInUp{0%{opacity:0;transform:translateY(20px)}100%{opacity:1;transform:translateY(0)}}.fade-in-up{-webkit-animation-name:fadeInUp;-moz-animation-name:fadeInUp;-o-animation-name:fadeInUp;animation-name:fadeInUp}@-webkit-keyframes fadeInDown{0%{opacity:0;-webkit-transform:translateY(-20px)}100%{opacity:1;-webkit-transform:translateY(0)}}@-moz-keyframes fadeInDown{0%{opacity:0;-moz-transform:translateY(-20px)}100%{opacity:1;-moz-transform:translateY(0)}}@-o-keyframes fadeInDown{0%{opacity:0;-ms-transform:translateY(-20px)}100%{opacity:1;-ms-transform:translateY(0)}}@keyframes fadeInDown{0%{opacity:0;transform:translateY(-20px)}100%{opacity:1;transform:translateY(0)}}.fade-in-down{-webkit-animation-name:fadeInDown;-moz-animation-name:fadeInDown;-o-animation-name:fadeInDown;animation-name:fadeInDown}@-webkit-keyframes fadeInRight{0%{opacity:0;-webkit-transform:translateX(-20px)}100%{opacity:1;-webkit-transform:translateX(0)}}@-moz-keyframes fadeInRight{0%{opacity:0;-moz-transform:translateX(-20px)}100%{opacity:1;-moz-transform:translateX(0)}}@-o-keyframes fadeInRight{0%{opacity:0;-o-transform:translateX(-20px)}100%{opacity:1;-o-transform:translateX(0)}}@keyframes fadeInRight{0%{opacity:0;transform:translateX(-20px)}100%{opacity:1;transform:translateX(0)}}.fade-in-right{-webkit-animation-name:fadeInRight;-moz-animation-name:fadeInRight;-o-animation-name:fadeInRight;animation-name:fadeInRight}@-webkit-keyframes fadeInLeft{0%{opacity:0;-webkit-transform:translateX(20px)}100%{opacity:1;-webkit-transform:translateX(0)}}@-moz-keyframes fadeInLeft{0%{opacity:0;-moz-transform:translateX(20px)}100%{opacity:1;-moz-transform:translateX(0)}}@-o-keyframes fadeInLeft{0%{opacity:0;-o-transform:translateX(20px)}100%{opacity:1;-o-transform:translateX(0)}}@keyframes fadeInLeft{0%{opacity:0;transform:translateX(20px)}100%{opacity:1;transform:translateX(0)}}.fade-in-left{-webkit-animation-name:fadeInLeft;-moz-animation-name:fadeInLeft;-o-animation-name:fadeInLeft;animation-name:fadeInLeft}div.hopscotch-bubble .hopscotch-nav-button{font-weight:700;border-width:1px;border-style:solid;cursor:pointer;margin:0;overflow:visible;text-decoration:none!important;width:auto;padding:0 10px;height:26px;line-height:24px;font-size:12px;*zoom:1;white-space:nowrap;display:-moz-inline-stack;display:inline-block;*vertical-align:auto;zoom:1;*display:inline;vertical-align:middle;-moz-border-radius:3px;-ms-border-radius:3px;-o-border-radius:3px;-webkit-border-radius:3px;border-radius:3px;-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box}div.hopscotch-bubble .hopscotch-nav-button:hover{*zoom:1;-webkit-box-shadow:0 1px 3px rgba(0,0,0,.25);-moz-box-shadow:0 1px 3px rgba(0,0,0,.25);box-shadow:0 1px 3px rgba(0,0,0,.25)}div.hopscotch-bubble .hopscotch-nav-button:active{-webkit-box-shadow:0 1px 2px rgba(0,0,0,.25) inset;-moz-box-shadow:0 1px 2px rgba(0,0,0,.25) inset;box-shadow:0 1px 2px rgba(0,0,0,.25) inset}div.hopscotch-bubble .hopscotch-nav-button.next{border-color:#1b5480;color:#fff;margin:0 0 0 10px;text-shadow:0 1px 1px rgba(0,0,0,.35);background-color:#287bbc;filter:progid:DXImageTransform.Microsoft.gradient(gradientType=0, startColorstr='#287bbc', endColorstr='#23639a');background-image:-webkit-gradient(linear,50% 0,50% 100%,color-stop(0%,#287bbc),color-stop(100%,#23639a));background-image:-webkit-linear-gradient(to bottom,#287bbc 0,#23639a 100%);background-image:-moz-linear-gradient(to bottom,#287bbc 0,#23639a 100%);background-image:-o-linear-gradient(to bottom,#287bbc 0,#23639a 100%);background-image:linear-gradient(to bottom,#287bbc 0,#23639a 100%)}div.hopscotch-bubble .hopscotch-nav-button.next:hover{background-color:#2672ae;filter:progid:DXImageTransform.Microsoft.gradient(gradientType=0, startColorstr='#2672ae', endColorstr='#1e4f7e');background-image:-webkit-gradient(linear,50% 0,50% 100%,color-stop(0%,#2672ae),color-stop(100%,#1e4f7e));background-image:-webkit-linear-gradient(to bottom,#2672ae 0,#1e4f7e 100%);background-image:-moz-linear-gradient(to bottom,#2672ae 0,#1e4f7e 100%);background-image:-o-linear-gradient(to bottom,#2672ae 0,#1e4f7e 100%);background-image:linear-gradient(to bottom,#2672ae 0,#1e4f7e 100%)}div.hopscotch-bubble .hopscotch-nav-button.prev{border-color:#a7a7a7;color:#444;text-shadow:0 1px 1px rgba(255,255,255,.75);background-color:#f2f2f2;filter:progid:DXImageTransform.Microsoft.gradient(gradientType=0, startColorstr='#f2f2f2', endColorstr='#e9e9e9');background-image:-webkit-gradient(linear,50% 0,50% 100%,color-stop(0%,#f2f2f2),color-stop(100%,#e9e9e9));background-image:-webkit-linear-gradient(to bottom,#f2f2f2 0,#e9e9e9 100%);background-image:-moz-linear-gradient(to bottom,#f2f2f2 0,#e9e9e9 100%);background-image:-o-linear-gradient(to bottom,#f2f2f2 0,#e9e9e9 100%);background-image:linear-gradient(to bottom,#f2f2f2 0,#e9e9e9 100%)}div.hopscotch-bubble .hopscotch-nav-button.prev:hover{background-color:#e8e8e8;filter:progid:DXImageTransform.Microsoft.gradient(gradientType=0, startColorstr='#FFE8E8E8', endColorstr='#FFA9A9A9');background-image:-webkit-gradient(linear,50% 0,50% 100%,color-stop(0%,#e8e8e8),color-stop(13%,#e3e3e3),color-stop(32%,#d7d7d7),color-stop(71%,#b9b9b9),color-stop(100%,#a9a9a9));background-image:-webkit-linear-gradient(to bottom,#e8e8e8 0,#e3e3e3 13%,#d7d7d7 32%,#b9b9b9 71%,#a9a9a9 100%);background-image:-moz-linear-gradient(to bottom,#e8e8e8 0,#e3e3e3 13%,#d7d7d7 32%,#b9b9b9 71%,#a9a9a9 100%);background-image:-o-linear-gradient(to bottom,#e8e8e8 0,#e3e3e3 13%,#d7d7d7 32%,#b9b9b9 71%,#a9a9a9 100%);background-image:linear-gradient(to bottom,#e8e8e8 0,#e3e3e3 13%,#d7d7d7 32%,#b9b9b9 71%,#a9a9a9 100%)}div.hopscotch-bubble{background-color:#fff;border:5px solid #000;border:5px solid rgba(0,0,0,.5);color:#333;font-family:Helvetica,Arial;font-size:13px;position:absolute;z-index:999999;-webkit-box-sizing:content-box;-moz-box-sizing:content-box;box-sizing:content-box;-moz-background-clip:padding;-webkit-background-clip:padding;background-clip:padding-box}div.hopscotch-bubble *{-webkit-box-sizing:content-box;-moz-box-sizing:content-box;box-sizing:content-box}div.hopscotch-bubble.animate{-moz-transition-property:top,left;-moz-transition-duration:1s;-moz-transition-timing-function:ease-in-out;-ms-transition-property:top,left;-ms-transition-duration:1s;-ms-transition-timing-function:ease-in-out;-o-transition-property:top,left;-o-transition-duration:1s;-o-transition-timing-function:ease-in-out;-webkit-transition-property:top,left;-webkit-transition-duration:1s;-webkit-transition-timing-function:ease-in-out;transition-property:top,left;transition-duration:1s;transition-timing-function:ease-in-out}div.hopscotch-bubble.invisible{opacity:0}div.hopscotch-bubble.hide,div.hopscotch-bubble .hide,div.hopscotch-bubble .hide-all{display:none}div.hopscotch-bubble h3{color:#000;font-family:Helvetica,Arial;font-size:16px;font-weight:700;line-height:19px;margin:-1px 15px 0 0;padding:0}div.hopscotch-bubble .hopscotch-bubble-container{padding:15px;position:relative;text-align:left;-webkit-font-smoothing:antialiased}div.hopscotch-bubble .hopscotch-content{font-family:Helvetica,Arial;font-weight:400;line-height:17px;margin:-5px 0 11px;padding-top:8px}div.hopscotch-bubble .hopscotch-bubble-content{margin:0 0 0 40px}div.hopscotch-bubble.no-number .hopscotch-bubble-content{margin:0}div.hopscotch-bubble .hopscotch-bubble-close{-webkit-appearance:none;-moz-appearance:none;appearance:none;border:0;color:#000;background:transparent url(/assets/images/hopscotch-sprite-green.png) -192px -92px no-repeat;display:block;padding:8px;position:absolute;text-decoration:none;text-indent:-9999px;width:8px;height:8px;top:0;right:0}div.hopscotch-bubble .hopscotch-bubble-close.hide,div.hopscotch-bubble .hopscotch-bubble-close.hide-all{display:none}div.hopscotch-bubble .hopscotch-bubble-number{background:transparent url(/assets/images/hopscotch-sprite-green.png) 0 0 no-repeat;color:#fff;display:block;float:left;font-size:17px;font-weight:700;line-height:31px;padding:0 10px 0 0;text-align:center;width:30px;height:30px}div.hopscotch-bubble .hopscotch-bubble-arrow-container{position:absolute;width:34px;height:34px}div.hopscotch-bubble .hopscotch-bubble-arrow-container .hopscotch-bubble-arrow,div.hopscotch-bubble .hopscotch-bubble-arrow-container .hopscotch-bubble-arrow-border{width:0;height:0}div.hopscotch-bubble .hopscotch-bubble-arrow-container.up{top:-22px;left:10px}div.hopscotch-bubble .hopscotch-bubble-arrow-container.up .hopscotch-bubble-arrow{border-bottom:17px solid #fff;border-left:17px solid transparent;border-right:17px solid transparent;position:relative;top:-10px}div.hopscotch-bubble .hopscotch-bubble-arrow-container.up .hopscotch-bubble-arrow-border{border-bottom:17px solid #000;border-bottom:17px solid rgba(0,0,0,.5);border-left:17px solid transparent;border-right:17px solid transparent}div.hopscotch-bubble .hopscotch-bubble-arrow-container.down{bottom:-39px;left:10px}div.hopscotch-bubble .hopscotch-bubble-arrow-container.down .hopscotch-bubble-arrow{border-top:17px solid #fff;border-left:17px solid transparent;border-right:17px solid transparent;position:relative;top:-24px}div.hopscotch-bubble .hopscotch-bubble-arrow-container.down .hopscotch-bubble-arrow-border{border-top:17px solid #000;border-top:17px solid rgba(0,0,0,.5);border-left:17px solid transparent;border-right:17px solid transparent}div.hopscotch-bubble .hopscotch-bubble-arrow-container.left{top:10px;left:-22px}div.hopscotch-bubble .hopscotch-bubble-arrow-container.left .hopscotch-bubble-arrow{border-bottom:17px solid transparent;border-right:17px solid #fff;border-top:17px solid transparent;position:relative;left:7px;top:-34px}div.hopscotch-bubble .hopscotch-bubble-arrow-container.left .hopscotch-bubble-arrow-border{border-right:17px solid #000;border-right:17px solid rgba(0,0,0,.5);border-bottom:17px solid transparent;border-top:17px solid transparent}div.hopscotch-bubble .hopscotch-bubble-arrow-container.right{top:10px;right:-39px}div.hopscotch-bubble .hopscotch-bubble-arrow-container.right .hopscotch-bubble-arrow{border-bottom:17px solid transparent;border-left:17px solid #fff;border-top:17px solid transparent;position:relative;left:-7px;top:-34px}div.hopscotch-bubble .hopscotch-bubble-arrow-container.right .hopscotch-bubble-arrow-border{border-left:17px solid #000;border-left:17px solid rgba(0,0,0,.5);border-bottom:17px solid transparent;border-top:17px solid transparent}div.hopscotch-bubble .hopscotch-actions{margin:10px 0 0;text-align:right} \ No newline at end of file diff --git a/scripts/yarn.lock b/scripts/yarn.lock new file mode 100644 index 0000000000000000000000000000000000000000..df7585c9933e1079aad4d95b736051eff0bbae3f --- /dev/null +++ b/scripts/yarn.lock @@ -0,0 +1,9472 @@ +# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. +# yarn lockfile v1 + + +"@babel/code-frame@^7.0.0", "@babel/code-frame@^7.10.4": + version "7.10.4" + resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.10.4.tgz#168da1a36e90da68ae8d49c0f1b48c7c6249213a" + integrity sha512-vG6SvB6oYEhvgisZNFRmRCUkLz11c7rp+tbNTynGqc6mS1d5ATd/sGyV6W0KZZnXRKMTzZDRgQT3Ou9jhpAfUg== + dependencies: + "@babel/highlight" "^7.10.4" + +"@babel/compat-data@^7.10.4", "@babel/compat-data@^7.11.0": + version "7.11.0" + resolved "https://registry.yarnpkg.com/@babel/compat-data/-/compat-data-7.11.0.tgz#e9f73efe09af1355b723a7f39b11bad637d7c99c" + integrity sha512-TPSvJfv73ng0pfnEOh17bYMPQbI95+nGWc71Ss4vZdRBHTDqmM9Z8ZV4rYz8Ks7sfzc95n30k6ODIq5UGnXcYQ== + dependencies: + browserslist "^4.12.0" + invariant "^2.2.4" + semver "^5.5.0" + +"@babel/core@^7.11.1": + version "7.11.6" + resolved "https://registry.yarnpkg.com/@babel/core/-/core-7.11.6.tgz#3a9455dc7387ff1bac45770650bc13ba04a15651" + integrity sha512-Wpcv03AGnmkgm6uS6k8iwhIwTrcP0m17TL1n1sy7qD0qelDu4XNeW0dN0mHfa+Gei211yDaLoEe/VlbXQzM4Bg== + dependencies: + "@babel/code-frame" "^7.10.4" + "@babel/generator" "^7.11.6" + "@babel/helper-module-transforms" "^7.11.0" + "@babel/helpers" "^7.10.4" + "@babel/parser" "^7.11.5" + "@babel/template" "^7.10.4" + "@babel/traverse" "^7.11.5" + "@babel/types" "^7.11.5" + convert-source-map "^1.7.0" + debug "^4.1.0" + gensync "^1.0.0-beta.1" + json5 "^2.1.2" + lodash "^4.17.19" + resolve "^1.3.2" + semver "^5.4.1" + source-map "^0.5.0" + +"@babel/generator@^7.11.5", "@babel/generator@^7.11.6": + version "7.11.6" + resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-7.11.6.tgz#b868900f81b163b4d464ea24545c61cbac4dc620" + integrity sha512-DWtQ1PV3r+cLbySoHrwn9RWEgKMBLLma4OBQloPRyDYvc5msJM9kvTLo1YnlJd1P/ZuKbdli3ijr5q3FvAF3uA== + dependencies: + "@babel/types" "^7.11.5" + jsesc "^2.5.1" + source-map "^0.5.0" + +"@babel/helper-annotate-as-pure@^7.10.4": + version "7.10.4" + resolved "https://registry.yarnpkg.com/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.10.4.tgz#5bf0d495a3f757ac3bda48b5bf3b3ba309c72ba3" + integrity sha512-XQlqKQP4vXFB7BN8fEEerrmYvHp3fK/rBkRFz9jaJbzK0B1DSfej9Kc7ZzE8Z/OnId1jpJdNAZ3BFQjWG68rcA== + dependencies: + "@babel/types" "^7.10.4" + +"@babel/helper-builder-binary-assignment-operator-visitor@^7.10.4": + version "7.10.4" + resolved "https://registry.yarnpkg.com/@babel/helper-builder-binary-assignment-operator-visitor/-/helper-builder-binary-assignment-operator-visitor-7.10.4.tgz#bb0b75f31bf98cbf9ff143c1ae578b87274ae1a3" + integrity sha512-L0zGlFrGWZK4PbT8AszSfLTM5sDU1+Az/En9VrdT8/LmEiJt4zXt+Jve9DCAnQcbqDhCI+29y/L93mrDzddCcg== + dependencies: + "@babel/helper-explode-assignable-expression" "^7.10.4" + "@babel/types" "^7.10.4" + +"@babel/helper-compilation-targets@^7.10.4": + version "7.10.4" + resolved "https://registry.yarnpkg.com/@babel/helper-compilation-targets/-/helper-compilation-targets-7.10.4.tgz#804ae8e3f04376607cc791b9d47d540276332bd2" + integrity sha512-a3rYhlsGV0UHNDvrtOXBg8/OpfV0OKTkxKPzIplS1zpx7CygDcWWxckxZeDd3gzPzC4kUT0A4nVFDK0wGMh4MQ== + dependencies: + "@babel/compat-data" "^7.10.4" + browserslist "^4.12.0" + invariant "^2.2.4" + levenary "^1.1.1" + semver "^5.5.0" + +"@babel/helper-create-class-features-plugin@^7.10.4": + version "7.10.5" + resolved "https://registry.yarnpkg.com/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.10.5.tgz#9f61446ba80e8240b0a5c85c6fdac8459d6f259d" + integrity sha512-0nkdeijB7VlZoLT3r/mY3bUkw3T8WG/hNw+FATs/6+pG2039IJWjTYL0VTISqsNHMUTEnwbVnc89WIJX9Qed0A== + dependencies: + "@babel/helper-function-name" "^7.10.4" + "@babel/helper-member-expression-to-functions" "^7.10.5" + "@babel/helper-optimise-call-expression" "^7.10.4" + "@babel/helper-plugin-utils" "^7.10.4" + "@babel/helper-replace-supers" "^7.10.4" + "@babel/helper-split-export-declaration" "^7.10.4" + +"@babel/helper-create-regexp-features-plugin@^7.10.4": + version "7.10.4" + resolved "https://registry.yarnpkg.com/@babel/helper-create-regexp-features-plugin/-/helper-create-regexp-features-plugin-7.10.4.tgz#fdd60d88524659a0b6959c0579925e425714f3b8" + integrity sha512-2/hu58IEPKeoLF45DBwx3XFqsbCXmkdAay4spVr2x0jYgRxrSNp+ePwvSsy9g6YSaNDcKIQVPXk1Ov8S2edk2g== + dependencies: + "@babel/helper-annotate-as-pure" "^7.10.4" + "@babel/helper-regex" "^7.10.4" + regexpu-core "^4.7.0" + +"@babel/helper-define-map@^7.10.4": + version "7.10.5" + resolved "https://registry.yarnpkg.com/@babel/helper-define-map/-/helper-define-map-7.10.5.tgz#b53c10db78a640800152692b13393147acb9bb30" + integrity sha512-fMw4kgFB720aQFXSVaXr79pjjcW5puTCM16+rECJ/plGS+zByelE8l9nCpV1GibxTnFVmUuYG9U8wYfQHdzOEQ== + dependencies: + "@babel/helper-function-name" "^7.10.4" + "@babel/types" "^7.10.5" + lodash "^4.17.19" + +"@babel/helper-explode-assignable-expression@^7.10.4": + version "7.11.4" + resolved "https://registry.yarnpkg.com/@babel/helper-explode-assignable-expression/-/helper-explode-assignable-expression-7.11.4.tgz#2d8e3470252cc17aba917ede7803d4a7a276a41b" + integrity sha512-ux9hm3zR4WV1Y3xXxXkdG/0gxF9nvI0YVmKVhvK9AfMoaQkemL3sJpXw+Xbz65azo8qJiEz2XVDUpK3KYhH3ZQ== + dependencies: + "@babel/types" "^7.10.4" + +"@babel/helper-function-name@^7.10.4": + version "7.10.4" + resolved "https://registry.yarnpkg.com/@babel/helper-function-name/-/helper-function-name-7.10.4.tgz#d2d3b20c59ad8c47112fa7d2a94bc09d5ef82f1a" + integrity sha512-YdaSyz1n8gY44EmN7x44zBn9zQ1Ry2Y+3GTA+3vH6Mizke1Vw0aWDM66FOYEPw8//qKkmqOckrGgTYa+6sceqQ== + dependencies: + "@babel/helper-get-function-arity" "^7.10.4" + "@babel/template" "^7.10.4" + "@babel/types" "^7.10.4" + +"@babel/helper-get-function-arity@^7.10.4": + version "7.10.4" + resolved "https://registry.yarnpkg.com/@babel/helper-get-function-arity/-/helper-get-function-arity-7.10.4.tgz#98c1cbea0e2332f33f9a4661b8ce1505b2c19ba2" + integrity sha512-EkN3YDB+SRDgiIUnNgcmiD361ti+AVbL3f3Henf6dqqUyr5dMsorno0lJWJuLhDhkI5sYEpgj6y9kB8AOU1I2A== + dependencies: + "@babel/types" "^7.10.4" + +"@babel/helper-hoist-variables@^7.10.4": + version "7.10.4" + resolved "https://registry.yarnpkg.com/@babel/helper-hoist-variables/-/helper-hoist-variables-7.10.4.tgz#d49b001d1d5a68ca5e6604dda01a6297f7c9381e" + integrity sha512-wljroF5PgCk2juF69kanHVs6vrLwIPNp6DLD+Lrl3hoQ3PpPPikaDRNFA+0t81NOoMt2DL6WW/mdU8k4k6ZzuA== + dependencies: + "@babel/types" "^7.10.4" + +"@babel/helper-member-expression-to-functions@^7.10.4", "@babel/helper-member-expression-to-functions@^7.10.5": + version "7.11.0" + resolved "https://registry.yarnpkg.com/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.11.0.tgz#ae69c83d84ee82f4b42f96e2a09410935a8f26df" + integrity sha512-JbFlKHFntRV5qKw3YC0CvQnDZ4XMwgzzBbld7Ly4Mj4cbFy3KywcR8NtNctRToMWJOVvLINJv525Gd6wwVEx/Q== + dependencies: + "@babel/types" "^7.11.0" + +"@babel/helper-module-imports@^7.10.4": + version "7.10.4" + resolved "https://registry.yarnpkg.com/@babel/helper-module-imports/-/helper-module-imports-7.10.4.tgz#4c5c54be04bd31670a7382797d75b9fa2e5b5620" + integrity sha512-nEQJHqYavI217oD9+s5MUBzk6x1IlvoS9WTPfgG43CbMEeStE0v+r+TucWdx8KFGowPGvyOkDT9+7DHedIDnVw== + dependencies: + "@babel/types" "^7.10.4" + +"@babel/helper-module-transforms@^7.10.4", "@babel/helper-module-transforms@^7.10.5", "@babel/helper-module-transforms@^7.11.0": + version "7.11.0" + resolved "https://registry.yarnpkg.com/@babel/helper-module-transforms/-/helper-module-transforms-7.11.0.tgz#b16f250229e47211abdd84b34b64737c2ab2d359" + integrity sha512-02EVu8COMuTRO1TAzdMtpBPbe6aQ1w/8fePD2YgQmxZU4gpNWaL9gK3Jp7dxlkUlUCJOTaSeA+Hrm1BRQwqIhg== + dependencies: + "@babel/helper-module-imports" "^7.10.4" + "@babel/helper-replace-supers" "^7.10.4" + "@babel/helper-simple-access" "^7.10.4" + "@babel/helper-split-export-declaration" "^7.11.0" + "@babel/template" "^7.10.4" + "@babel/types" "^7.11.0" + lodash "^4.17.19" + +"@babel/helper-optimise-call-expression@^7.10.4": + version "7.10.4" + resolved "https://registry.yarnpkg.com/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.10.4.tgz#50dc96413d594f995a77905905b05893cd779673" + integrity sha512-n3UGKY4VXwXThEiKrgRAoVPBMqeoPgHVqiHZOanAJCG9nQUL2pLRQirUzl0ioKclHGpGqRgIOkgcIJaIWLpygg== + dependencies: + "@babel/types" "^7.10.4" + +"@babel/helper-plugin-utils@^7.0.0", "@babel/helper-plugin-utils@^7.10.4", "@babel/helper-plugin-utils@^7.8.0", "@babel/helper-plugin-utils@^7.8.3": + version "7.10.4" + resolved "https://registry.yarnpkg.com/@babel/helper-plugin-utils/-/helper-plugin-utils-7.10.4.tgz#2f75a831269d4f677de49986dff59927533cf375" + integrity sha512-O4KCvQA6lLiMU9l2eawBPMf1xPP8xPfB3iEQw150hOVTqj/rfXz0ThTb4HEzqQfs2Bmo5Ay8BzxfzVtBrr9dVg== + +"@babel/helper-regex@^7.10.4": + version "7.10.5" + resolved "https://registry.yarnpkg.com/@babel/helper-regex/-/helper-regex-7.10.5.tgz#32dfbb79899073c415557053a19bd055aae50ae0" + integrity sha512-68kdUAzDrljqBrio7DYAEgCoJHxppJOERHOgOrDN7WjOzP0ZQ1LsSDRXcemzVZaLvjaJsJEESb6qt+znNuENDg== + dependencies: + lodash "^4.17.19" + +"@babel/helper-remap-async-to-generator@^7.10.4": + version "7.11.4" + resolved "https://registry.yarnpkg.com/@babel/helper-remap-async-to-generator/-/helper-remap-async-to-generator-7.11.4.tgz#4474ea9f7438f18575e30b0cac784045b402a12d" + integrity sha512-tR5vJ/vBa9wFy3m5LLv2faapJLnDFxNWff2SAYkSE4rLUdbp7CdObYFgI7wK4T/Mj4UzpjPwzR8Pzmr5m7MHGA== + dependencies: + "@babel/helper-annotate-as-pure" "^7.10.4" + "@babel/helper-wrap-function" "^7.10.4" + "@babel/template" "^7.10.4" + "@babel/types" "^7.10.4" + +"@babel/helper-replace-supers@^7.10.4": + version "7.10.4" + resolved "https://registry.yarnpkg.com/@babel/helper-replace-supers/-/helper-replace-supers-7.10.4.tgz#d585cd9388ea06e6031e4cd44b6713cbead9e6cf" + integrity sha512-sPxZfFXocEymYTdVK1UNmFPBN+Hv5mJkLPsYWwGBxZAxaWfFu+xqp7b6qWD0yjNuNL2VKc6L5M18tOXUP7NU0A== + dependencies: + "@babel/helper-member-expression-to-functions" "^7.10.4" + "@babel/helper-optimise-call-expression" "^7.10.4" + "@babel/traverse" "^7.10.4" + "@babel/types" "^7.10.4" + +"@babel/helper-simple-access@^7.10.4": + version "7.10.4" + resolved "https://registry.yarnpkg.com/@babel/helper-simple-access/-/helper-simple-access-7.10.4.tgz#0f5ccda2945277a2a7a2d3a821e15395edcf3461" + integrity sha512-0fMy72ej/VEvF8ULmX6yb5MtHG4uH4Dbd6I/aHDb/JVg0bbivwt9Wg+h3uMvX+QSFtwr5MeItvazbrc4jtRAXw== + dependencies: + "@babel/template" "^7.10.4" + "@babel/types" "^7.10.4" + +"@babel/helper-skip-transparent-expression-wrappers@^7.11.0": + version "7.11.0" + resolved "https://registry.yarnpkg.com/@babel/helper-skip-transparent-expression-wrappers/-/helper-skip-transparent-expression-wrappers-7.11.0.tgz#eec162f112c2f58d3af0af125e3bb57665146729" + integrity sha512-0XIdiQln4Elglgjbwo9wuJpL/K7AGCY26kmEt0+pRP0TAj4jjyNq1MjoRvikrTVqKcx4Gysxt4cXvVFXP/JO2Q== + dependencies: + "@babel/types" "^7.11.0" + +"@babel/helper-split-export-declaration@^7.10.4", "@babel/helper-split-export-declaration@^7.11.0": + version "7.11.0" + resolved "https://registry.yarnpkg.com/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.11.0.tgz#f8a491244acf6a676158ac42072911ba83ad099f" + integrity sha512-74Vejvp6mHkGE+m+k5vHY93FX2cAtrw1zXrZXRlG4l410Nm9PxfEiVTn1PjDPV5SnmieiueY4AFg2xqhNFuuZg== + dependencies: + "@babel/types" "^7.11.0" + +"@babel/helper-validator-identifier@^7.10.4": + version "7.10.4" + resolved "https://registry.yarnpkg.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.10.4.tgz#a78c7a7251e01f616512d31b10adcf52ada5e0d2" + integrity sha512-3U9y+43hz7ZM+rzG24Qe2mufW5KhvFg/NhnNph+i9mgCtdTCtMJuI1TMkrIUiK7Ix4PYlRF9I5dhqaLYA/ADXw== + +"@babel/helper-wrap-function@^7.10.4": + version "7.10.4" + resolved "https://registry.yarnpkg.com/@babel/helper-wrap-function/-/helper-wrap-function-7.10.4.tgz#8a6f701eab0ff39f765b5a1cfef409990e624b87" + integrity sha512-6py45WvEF0MhiLrdxtRjKjufwLL1/ob2qDJgg5JgNdojBAZSAKnAjkyOCNug6n+OBl4VW76XjvgSFTdaMcW0Ug== + dependencies: + "@babel/helper-function-name" "^7.10.4" + "@babel/template" "^7.10.4" + "@babel/traverse" "^7.10.4" + "@babel/types" "^7.10.4" + +"@babel/helpers@^7.10.4": + version "7.10.4" + resolved "https://registry.yarnpkg.com/@babel/helpers/-/helpers-7.10.4.tgz#2abeb0d721aff7c0a97376b9e1f6f65d7a475044" + integrity sha512-L2gX/XeUONeEbI78dXSrJzGdz4GQ+ZTA/aazfUsFaWjSe95kiCuOZ5HsXvkiw3iwF+mFHSRUfJU8t6YavocdXA== + dependencies: + "@babel/template" "^7.10.4" + "@babel/traverse" "^7.10.4" + "@babel/types" "^7.10.4" + +"@babel/highlight@^7.10.4": + version "7.10.4" + resolved "https://registry.yarnpkg.com/@babel/highlight/-/highlight-7.10.4.tgz#7d1bdfd65753538fabe6c38596cdb76d9ac60143" + integrity sha512-i6rgnR/YgPEQzZZnbTHHuZdlE8qyoBNalD6F+q4vAFlcMEcqmkoG+mPqJYJCo63qPf74+Y1UZsl3l6f7/RIkmA== + dependencies: + "@babel/helper-validator-identifier" "^7.10.4" + chalk "^2.0.0" + js-tokens "^4.0.0" + +"@babel/parser@^7.10.4", "@babel/parser@^7.11.5": + version "7.11.5" + resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.11.5.tgz#c7ff6303df71080ec7a4f5b8c003c58f1cf51037" + integrity sha512-X9rD8qqm695vgmeaQ4fvz/o3+Wk4ZzQvSHkDBgpYKxpD4qTAUm88ZKtHkVqIOsYFFbIQ6wQYhC6q7pjqVK0E0Q== + +"@babel/plugin-proposal-async-generator-functions@^7.10.4": + version "7.10.5" + resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-async-generator-functions/-/plugin-proposal-async-generator-functions-7.10.5.tgz#3491cabf2f7c179ab820606cec27fed15e0e8558" + integrity sha512-cNMCVezQbrRGvXJwm9fu/1sJj9bHdGAgKodZdLqOQIpfoH3raqmRPBM17+lh7CzhiKRRBrGtZL9WcjxSoGYUSg== + dependencies: + "@babel/helper-plugin-utils" "^7.10.4" + "@babel/helper-remap-async-to-generator" "^7.10.4" + "@babel/plugin-syntax-async-generators" "^7.8.0" + +"@babel/plugin-proposal-class-properties@^7.10.4": + version "7.10.4" + resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-class-properties/-/plugin-proposal-class-properties-7.10.4.tgz#a33bf632da390a59c7a8c570045d1115cd778807" + integrity sha512-vhwkEROxzcHGNu2mzUC0OFFNXdZ4M23ib8aRRcJSsW8BZK9pQMD7QB7csl97NBbgGZO7ZyHUyKDnxzOaP4IrCg== + dependencies: + "@babel/helper-create-class-features-plugin" "^7.10.4" + "@babel/helper-plugin-utils" "^7.10.4" + +"@babel/plugin-proposal-dynamic-import@^7.10.4": + version "7.10.4" + resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-dynamic-import/-/plugin-proposal-dynamic-import-7.10.4.tgz#ba57a26cb98b37741e9d5bca1b8b0ddf8291f17e" + integrity sha512-up6oID1LeidOOASNXgv/CFbgBqTuKJ0cJjz6An5tWD+NVBNlp3VNSBxv2ZdU7SYl3NxJC7agAQDApZusV6uFwQ== + dependencies: + "@babel/helper-plugin-utils" "^7.10.4" + "@babel/plugin-syntax-dynamic-import" "^7.8.0" + +"@babel/plugin-proposal-export-namespace-from@^7.10.4": + version "7.10.4" + resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-export-namespace-from/-/plugin-proposal-export-namespace-from-7.10.4.tgz#570d883b91031637b3e2958eea3c438e62c05f54" + integrity sha512-aNdf0LY6/3WXkhh0Fdb6Zk9j1NMD8ovj3F6r0+3j837Pn1S1PdNtcwJ5EG9WkVPNHPxyJDaxMaAOVq4eki0qbg== + dependencies: + "@babel/helper-plugin-utils" "^7.10.4" + "@babel/plugin-syntax-export-namespace-from" "^7.8.3" + +"@babel/plugin-proposal-json-strings@^7.10.4": + version "7.10.4" + resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-json-strings/-/plugin-proposal-json-strings-7.10.4.tgz#593e59c63528160233bd321b1aebe0820c2341db" + integrity sha512-fCL7QF0Jo83uy1K0P2YXrfX11tj3lkpN7l4dMv9Y9VkowkhkQDwFHFd8IiwyK5MZjE8UpbgokkgtcReH88Abaw== + dependencies: + "@babel/helper-plugin-utils" "^7.10.4" + "@babel/plugin-syntax-json-strings" "^7.8.0" + +"@babel/plugin-proposal-logical-assignment-operators@^7.11.0": + version "7.11.0" + resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-logical-assignment-operators/-/plugin-proposal-logical-assignment-operators-7.11.0.tgz#9f80e482c03083c87125dee10026b58527ea20c8" + integrity sha512-/f8p4z+Auz0Uaf+i8Ekf1iM7wUNLcViFUGiPxKeXvxTSl63B875YPiVdUDdem7hREcI0E0kSpEhS8tF5RphK7Q== + dependencies: + "@babel/helper-plugin-utils" "^7.10.4" + "@babel/plugin-syntax-logical-assignment-operators" "^7.10.4" + +"@babel/plugin-proposal-nullish-coalescing-operator@^7.10.4": + version "7.10.4" + resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-nullish-coalescing-operator/-/plugin-proposal-nullish-coalescing-operator-7.10.4.tgz#02a7e961fc32e6d5b2db0649e01bf80ddee7e04a" + integrity sha512-wq5n1M3ZUlHl9sqT2ok1T2/MTt6AXE0e1Lz4WzWBr95LsAZ5qDXe4KnFuauYyEyLiohvXFMdbsOTMyLZs91Zlw== + dependencies: + "@babel/helper-plugin-utils" "^7.10.4" + "@babel/plugin-syntax-nullish-coalescing-operator" "^7.8.0" + +"@babel/plugin-proposal-numeric-separator@^7.10.4": + version "7.10.4" + resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-numeric-separator/-/plugin-proposal-numeric-separator-7.10.4.tgz#ce1590ff0a65ad12970a609d78855e9a4c1aef06" + integrity sha512-73/G7QoRoeNkLZFxsoCCvlg4ezE4eM+57PnOqgaPOozd5myfj7p0muD1mRVJvbUWbOzD+q3No2bWbaKy+DJ8DA== + dependencies: + "@babel/helper-plugin-utils" "^7.10.4" + "@babel/plugin-syntax-numeric-separator" "^7.10.4" + +"@babel/plugin-proposal-object-rest-spread@^7.10.1", "@babel/plugin-proposal-object-rest-spread@^7.11.0": + version "7.11.0" + resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-object-rest-spread/-/plugin-proposal-object-rest-spread-7.11.0.tgz#bd81f95a1f746760ea43b6c2d3d62b11790ad0af" + integrity sha512-wzch41N4yztwoRw0ak+37wxwJM2oiIiy6huGCoqkvSTA9acYWcPfn9Y4aJqmFFJ70KTJUu29f3DQ43uJ9HXzEA== + dependencies: + "@babel/helper-plugin-utils" "^7.10.4" + "@babel/plugin-syntax-object-rest-spread" "^7.8.0" + "@babel/plugin-transform-parameters" "^7.10.4" + +"@babel/plugin-proposal-optional-catch-binding@^7.10.4": + version "7.10.4" + resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-optional-catch-binding/-/plugin-proposal-optional-catch-binding-7.10.4.tgz#31c938309d24a78a49d68fdabffaa863758554dd" + integrity sha512-LflT6nPh+GK2MnFiKDyLiqSqVHkQnVf7hdoAvyTnnKj9xB3docGRsdPuxp6qqqW19ifK3xgc9U5/FwrSaCNX5g== + dependencies: + "@babel/helper-plugin-utils" "^7.10.4" + "@babel/plugin-syntax-optional-catch-binding" "^7.8.0" + +"@babel/plugin-proposal-optional-chaining@^7.11.0": + version "7.11.0" + resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-optional-chaining/-/plugin-proposal-optional-chaining-7.11.0.tgz#de5866d0646f6afdaab8a566382fe3a221755076" + integrity sha512-v9fZIu3Y8562RRwhm1BbMRxtqZNFmFA2EG+pT2diuU8PT3H6T/KXoZ54KgYisfOFZHV6PfvAiBIZ9Rcz+/JCxA== + dependencies: + "@babel/helper-plugin-utils" "^7.10.4" + "@babel/helper-skip-transparent-expression-wrappers" "^7.11.0" + "@babel/plugin-syntax-optional-chaining" "^7.8.0" + +"@babel/plugin-proposal-private-methods@^7.10.4": + version "7.10.4" + resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-private-methods/-/plugin-proposal-private-methods-7.10.4.tgz#b160d972b8fdba5c7d111a145fc8c421fc2a6909" + integrity sha512-wh5GJleuI8k3emgTg5KkJK6kHNsGEr0uBTDBuQUBJwckk9xs1ez79ioheEVVxMLyPscB0LfkbVHslQqIzWV6Bw== + dependencies: + "@babel/helper-create-class-features-plugin" "^7.10.4" + "@babel/helper-plugin-utils" "^7.10.4" + +"@babel/plugin-proposal-unicode-property-regex@^7.10.4", "@babel/plugin-proposal-unicode-property-regex@^7.4.4": + version "7.10.4" + resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-unicode-property-regex/-/plugin-proposal-unicode-property-regex-7.10.4.tgz#4483cda53041ce3413b7fe2f00022665ddfaa75d" + integrity sha512-H+3fOgPnEXFL9zGYtKQe4IDOPKYlZdF1kqFDQRRb8PK4B8af1vAGK04tF5iQAAsui+mHNBQSAtd2/ndEDe9wuA== + dependencies: + "@babel/helper-create-regexp-features-plugin" "^7.10.4" + "@babel/helper-plugin-utils" "^7.10.4" + +"@babel/plugin-syntax-async-generators@^7.8.0": + version "7.8.4" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-async-generators/-/plugin-syntax-async-generators-7.8.4.tgz#a983fb1aeb2ec3f6ed042a210f640e90e786fe0d" + integrity sha512-tycmZxkGfZaxhMRbXlPXuVFpdWlXpir2W4AMhSJgRKzk/eDlIXOhb2LHWoLpDF7TEHylV5zNhykX6KAgHJmTNw== + dependencies: + "@babel/helper-plugin-utils" "^7.8.0" + +"@babel/plugin-syntax-class-properties@^7.10.4": + version "7.10.4" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-class-properties/-/plugin-syntax-class-properties-7.10.4.tgz#6644e6a0baa55a61f9e3231f6c9eeb6ee46c124c" + integrity sha512-GCSBF7iUle6rNugfURwNmCGG3Z/2+opxAMLs1nND4bhEG5PuxTIggDBoeYYSujAlLtsupzOHYJQgPS3pivwXIA== + dependencies: + "@babel/helper-plugin-utils" "^7.10.4" + +"@babel/plugin-syntax-dynamic-import@^7.8.0", "@babel/plugin-syntax-dynamic-import@^7.8.3": + version "7.8.3" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-dynamic-import/-/plugin-syntax-dynamic-import-7.8.3.tgz#62bf98b2da3cd21d626154fc96ee5b3cb68eacb3" + integrity sha512-5gdGbFon+PszYzqs83S3E5mpi7/y/8M9eC90MRTZfduQOYW76ig6SOSPNe41IG5LoP3FGBn2N0RjVDSQiS94kQ== + dependencies: + "@babel/helper-plugin-utils" "^7.8.0" + +"@babel/plugin-syntax-export-namespace-from@^7.8.3": + version "7.8.3" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-export-namespace-from/-/plugin-syntax-export-namespace-from-7.8.3.tgz#028964a9ba80dbc094c915c487ad7c4e7a66465a" + integrity sha512-MXf5laXo6c1IbEbegDmzGPwGNTsHZmEy6QGznu5Sh2UCWvueywb2ee+CCE4zQiZstxU9BMoQO9i6zUFSY0Kj0Q== + dependencies: + "@babel/helper-plugin-utils" "^7.8.3" + +"@babel/plugin-syntax-json-strings@^7.8.0": + version "7.8.3" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-json-strings/-/plugin-syntax-json-strings-7.8.3.tgz#01ca21b668cd8218c9e640cb6dd88c5412b2c96a" + integrity sha512-lY6kdGpWHvjoe2vk4WrAapEuBR69EMxZl+RoGRhrFGNYVK8mOPAW8VfbT/ZgrFbXlDNiiaxQnAtgVCZ6jv30EA== + dependencies: + "@babel/helper-plugin-utils" "^7.8.0" + +"@babel/plugin-syntax-logical-assignment-operators@^7.10.4": + version "7.10.4" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-logical-assignment-operators/-/plugin-syntax-logical-assignment-operators-7.10.4.tgz#ca91ef46303530448b906652bac2e9fe9941f699" + integrity sha512-d8waShlpFDinQ5MtvGU9xDAOzKH47+FFoney2baFIoMr952hKOLp1HR7VszoZvOsV/4+RRszNY7D17ba0te0ig== + dependencies: + "@babel/helper-plugin-utils" "^7.10.4" + +"@babel/plugin-syntax-nullish-coalescing-operator@^7.8.0": + version "7.8.3" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-nullish-coalescing-operator/-/plugin-syntax-nullish-coalescing-operator-7.8.3.tgz#167ed70368886081f74b5c36c65a88c03b66d1a9" + integrity sha512-aSff4zPII1u2QD7y+F8oDsz19ew4IGEJg9SVW+bqwpwtfFleiQDMdzA/R+UlWDzfnHFCxxleFT0PMIrR36XLNQ== + dependencies: + "@babel/helper-plugin-utils" "^7.8.0" + +"@babel/plugin-syntax-numeric-separator@^7.10.4": + version "7.10.4" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-numeric-separator/-/plugin-syntax-numeric-separator-7.10.4.tgz#b9b070b3e33570cd9fd07ba7fa91c0dd37b9af97" + integrity sha512-9H6YdfkcK/uOnY/K7/aA2xpzaAgkQn37yzWUMRK7OaPOqOpGS1+n0H5hxT9AUw9EsSjPW8SVyMJwYRtWs3X3ug== + dependencies: + "@babel/helper-plugin-utils" "^7.10.4" + +"@babel/plugin-syntax-object-rest-spread@^7.8.0": + version "7.8.3" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-object-rest-spread/-/plugin-syntax-object-rest-spread-7.8.3.tgz#60e225edcbd98a640332a2e72dd3e66f1af55871" + integrity sha512-XoqMijGZb9y3y2XskN+P1wUGiVwWZ5JmoDRwx5+3GmEplNyVM2s2Dg8ILFQm8rWM48orGy5YpI5Bl8U1y7ydlA== + dependencies: + "@babel/helper-plugin-utils" "^7.8.0" + +"@babel/plugin-syntax-optional-catch-binding@^7.8.0": + version "7.8.3" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-optional-catch-binding/-/plugin-syntax-optional-catch-binding-7.8.3.tgz#6111a265bcfb020eb9efd0fdfd7d26402b9ed6c1" + integrity sha512-6VPD0Pc1lpTqw0aKoeRTMiB+kWhAoT24PA+ksWSBrFtl5SIRVpZlwN3NNPQjehA2E/91FV3RjLWoVTglWcSV3Q== + dependencies: + "@babel/helper-plugin-utils" "^7.8.0" + +"@babel/plugin-syntax-optional-chaining@^7.8.0": + version "7.8.3" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-optional-chaining/-/plugin-syntax-optional-chaining-7.8.3.tgz#4f69c2ab95167e0180cd5336613f8c5788f7d48a" + integrity sha512-KoK9ErH1MBlCPxV0VANkXW2/dw4vlbGDrFgz8bmUsBGYkFRcbRwMh6cIJubdPrkxRwuGdtCk0v/wPTKbQgBjkg== + dependencies: + "@babel/helper-plugin-utils" "^7.8.0" + +"@babel/plugin-syntax-top-level-await@^7.10.4": + version "7.10.4" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-top-level-await/-/plugin-syntax-top-level-await-7.10.4.tgz#4bbeb8917b54fcf768364e0a81f560e33a3ef57d" + integrity sha512-ni1brg4lXEmWyafKr0ccFWkJG0CeMt4WV1oyeBW6EFObF4oOHclbkj5cARxAPQyAQ2UTuplJyK4nfkXIMMFvsQ== + dependencies: + "@babel/helper-plugin-utils" "^7.10.4" + +"@babel/plugin-transform-arrow-functions@^7.10.4": + version "7.10.4" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-arrow-functions/-/plugin-transform-arrow-functions-7.10.4.tgz#e22960d77e697c74f41c501d44d73dbf8a6a64cd" + integrity sha512-9J/oD1jV0ZCBcgnoFWFq1vJd4msoKb/TCpGNFyyLt0zABdcvgK3aYikZ8HjzB14c26bc7E3Q1yugpwGy2aTPNA== + dependencies: + "@babel/helper-plugin-utils" "^7.10.4" + +"@babel/plugin-transform-async-to-generator@^7.10.4": + version "7.10.4" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-async-to-generator/-/plugin-transform-async-to-generator-7.10.4.tgz#41a5017e49eb6f3cda9392a51eef29405b245a37" + integrity sha512-F6nREOan7J5UXTLsDsZG3DXmZSVofr2tGNwfdrVwkDWHfQckbQXnXSPfD7iO+c/2HGqycwyLST3DnZ16n+cBJQ== + dependencies: + "@babel/helper-module-imports" "^7.10.4" + "@babel/helper-plugin-utils" "^7.10.4" + "@babel/helper-remap-async-to-generator" "^7.10.4" + +"@babel/plugin-transform-block-scoped-functions@^7.10.4": + version "7.10.4" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-block-scoped-functions/-/plugin-transform-block-scoped-functions-7.10.4.tgz#1afa595744f75e43a91af73b0d998ecfe4ebc2e8" + integrity sha512-WzXDarQXYYfjaV1szJvN3AD7rZgZzC1JtjJZ8dMHUyiK8mxPRahynp14zzNjU3VkPqPsO38CzxiWO1c9ARZ8JA== + dependencies: + "@babel/helper-plugin-utils" "^7.10.4" + +"@babel/plugin-transform-block-scoping@^7.10.4": + version "7.11.1" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-block-scoping/-/plugin-transform-block-scoping-7.11.1.tgz#5b7efe98852bef8d652c0b28144cd93a9e4b5215" + integrity sha512-00dYeDE0EVEHuuM+26+0w/SCL0BH2Qy7LwHuI4Hi4MH5gkC8/AqMN5uWFJIsoXZrAphiMm1iXzBw6L2T+eA0ew== + dependencies: + "@babel/helper-plugin-utils" "^7.10.4" + +"@babel/plugin-transform-classes@^7.10.4": + version "7.10.4" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-classes/-/plugin-transform-classes-7.10.4.tgz#405136af2b3e218bc4a1926228bc917ab1a0adc7" + integrity sha512-2oZ9qLjt161dn1ZE0Ms66xBncQH4In8Sqw1YWgBUZuGVJJS5c0OFZXL6dP2MRHrkU/eKhWg8CzFJhRQl50rQxA== + dependencies: + "@babel/helper-annotate-as-pure" "^7.10.4" + "@babel/helper-define-map" "^7.10.4" + "@babel/helper-function-name" "^7.10.4" + "@babel/helper-optimise-call-expression" "^7.10.4" + "@babel/helper-plugin-utils" "^7.10.4" + "@babel/helper-replace-supers" "^7.10.4" + "@babel/helper-split-export-declaration" "^7.10.4" + globals "^11.1.0" + +"@babel/plugin-transform-computed-properties@^7.10.4": + version "7.10.4" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-computed-properties/-/plugin-transform-computed-properties-7.10.4.tgz#9ded83a816e82ded28d52d4b4ecbdd810cdfc0eb" + integrity sha512-JFwVDXcP/hM/TbyzGq3l/XWGut7p46Z3QvqFMXTfk6/09m7xZHJUN9xHfsv7vqqD4YnfI5ueYdSJtXqqBLyjBw== + dependencies: + "@babel/helper-plugin-utils" "^7.10.4" + +"@babel/plugin-transform-destructuring@^7.10.1", "@babel/plugin-transform-destructuring@^7.10.4": + version "7.10.4" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-destructuring/-/plugin-transform-destructuring-7.10.4.tgz#70ddd2b3d1bea83d01509e9bb25ddb3a74fc85e5" + integrity sha512-+WmfvyfsyF603iPa6825mq6Qrb7uLjTOsa3XOFzlYcYDHSS4QmpOWOL0NNBY5qMbvrcf3tq0Cw+v4lxswOBpgA== + dependencies: + "@babel/helper-plugin-utils" "^7.10.4" + +"@babel/plugin-transform-dotall-regex@^7.10.4", "@babel/plugin-transform-dotall-regex@^7.4.4": + version "7.10.4" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-dotall-regex/-/plugin-transform-dotall-regex-7.10.4.tgz#469c2062105c1eb6a040eaf4fac4b488078395ee" + integrity sha512-ZEAVvUTCMlMFAbASYSVQoxIbHm2OkG2MseW6bV2JjIygOjdVv8tuxrCTzj1+Rynh7ODb8GivUy7dzEXzEhuPaA== + dependencies: + "@babel/helper-create-regexp-features-plugin" "^7.10.4" + "@babel/helper-plugin-utils" "^7.10.4" + +"@babel/plugin-transform-duplicate-keys@^7.10.4": + version "7.10.4" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-duplicate-keys/-/plugin-transform-duplicate-keys-7.10.4.tgz#697e50c9fee14380fe843d1f306b295617431e47" + integrity sha512-GL0/fJnmgMclHiBTTWXNlYjYsA7rDrtsazHG6mglaGSTh0KsrW04qml+Bbz9FL0LcJIRwBWL5ZqlNHKTkU3xAA== + dependencies: + "@babel/helper-plugin-utils" "^7.10.4" + +"@babel/plugin-transform-exponentiation-operator@^7.10.4": + version "7.10.4" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-exponentiation-operator/-/plugin-transform-exponentiation-operator-7.10.4.tgz#5ae338c57f8cf4001bdb35607ae66b92d665af2e" + integrity sha512-S5HgLVgkBcRdyQAHbKj+7KyuWx8C6t5oETmUuwz1pt3WTWJhsUV0WIIXuVvfXMxl/QQyHKlSCNNtaIamG8fysw== + dependencies: + "@babel/helper-builder-binary-assignment-operator-visitor" "^7.10.4" + "@babel/helper-plugin-utils" "^7.10.4" + +"@babel/plugin-transform-for-of@^7.10.4": + version "7.10.4" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-for-of/-/plugin-transform-for-of-7.10.4.tgz#c08892e8819d3a5db29031b115af511dbbfebae9" + integrity sha512-ItdQfAzu9AlEqmusA/65TqJ79eRcgGmpPPFvBnGILXZH975G0LNjP1yjHvGgfuCxqrPPueXOPe+FsvxmxKiHHQ== + dependencies: + "@babel/helper-plugin-utils" "^7.10.4" + +"@babel/plugin-transform-function-name@^7.10.4": + version "7.10.4" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-function-name/-/plugin-transform-function-name-7.10.4.tgz#6a467880e0fc9638514ba369111811ddbe2644b7" + integrity sha512-OcDCq2y5+E0dVD5MagT5X+yTRbcvFjDI2ZVAottGH6tzqjx/LKpgkUepu3hp/u4tZBzxxpNGwLsAvGBvQ2mJzg== + dependencies: + "@babel/helper-function-name" "^7.10.4" + "@babel/helper-plugin-utils" "^7.10.4" + +"@babel/plugin-transform-literals@^7.10.4": + version "7.10.4" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-literals/-/plugin-transform-literals-7.10.4.tgz#9f42ba0841100a135f22712d0e391c462f571f3c" + integrity sha512-Xd/dFSTEVuUWnyZiMu76/InZxLTYilOSr1UlHV+p115Z/Le2Fi1KXkJUYz0b42DfndostYlPub3m8ZTQlMaiqQ== + dependencies: + "@babel/helper-plugin-utils" "^7.10.4" + +"@babel/plugin-transform-member-expression-literals@^7.10.4": + version "7.10.4" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-member-expression-literals/-/plugin-transform-member-expression-literals-7.10.4.tgz#b1ec44fcf195afcb8db2c62cd8e551c881baf8b7" + integrity sha512-0bFOvPyAoTBhtcJLr9VcwZqKmSjFml1iVxvPL0ReomGU53CX53HsM4h2SzckNdkQcHox1bpAqzxBI1Y09LlBSw== + dependencies: + "@babel/helper-plugin-utils" "^7.10.4" + +"@babel/plugin-transform-modules-amd@^7.10.4": + version "7.10.5" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-modules-amd/-/plugin-transform-modules-amd-7.10.5.tgz#1b9cddaf05d9e88b3aad339cb3e445c4f020a9b1" + integrity sha512-elm5uruNio7CTLFItVC/rIzKLfQ17+fX7EVz5W0TMgIHFo1zY0Ozzx+lgwhL4plzl8OzVn6Qasx5DeEFyoNiRw== + dependencies: + "@babel/helper-module-transforms" "^7.10.5" + "@babel/helper-plugin-utils" "^7.10.4" + babel-plugin-dynamic-import-node "^2.3.3" + +"@babel/plugin-transform-modules-commonjs@^7.10.4": + version "7.10.4" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-modules-commonjs/-/plugin-transform-modules-commonjs-7.10.4.tgz#66667c3eeda1ebf7896d41f1f16b17105a2fbca0" + integrity sha512-Xj7Uq5o80HDLlW64rVfDBhao6OX89HKUmb+9vWYaLXBZOma4gA6tw4Ni1O5qVDoZWUV0fxMYA0aYzOawz0l+1w== + dependencies: + "@babel/helper-module-transforms" "^7.10.4" + "@babel/helper-plugin-utils" "^7.10.4" + "@babel/helper-simple-access" "^7.10.4" + babel-plugin-dynamic-import-node "^2.3.3" + +"@babel/plugin-transform-modules-systemjs@^7.10.4": + version "7.10.5" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-modules-systemjs/-/plugin-transform-modules-systemjs-7.10.5.tgz#6270099c854066681bae9e05f87e1b9cadbe8c85" + integrity sha512-f4RLO/OL14/FP1AEbcsWMzpbUz6tssRaeQg11RH1BP/XnPpRoVwgeYViMFacnkaw4k4wjRSjn3ip1Uw9TaXuMw== + dependencies: + "@babel/helper-hoist-variables" "^7.10.4" + "@babel/helper-module-transforms" "^7.10.5" + "@babel/helper-plugin-utils" "^7.10.4" + babel-plugin-dynamic-import-node "^2.3.3" + +"@babel/plugin-transform-modules-umd@^7.10.4": + version "7.10.4" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-modules-umd/-/plugin-transform-modules-umd-7.10.4.tgz#9a8481fe81b824654b3a0b65da3df89f3d21839e" + integrity sha512-mohW5q3uAEt8T45YT7Qc5ws6mWgJAaL/8BfWD9Dodo1A3RKWli8wTS+WiQ/knF+tXlPirW/1/MqzzGfCExKECA== + dependencies: + "@babel/helper-module-transforms" "^7.10.4" + "@babel/helper-plugin-utils" "^7.10.4" + +"@babel/plugin-transform-named-capturing-groups-regex@^7.10.4": + version "7.10.4" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-named-capturing-groups-regex/-/plugin-transform-named-capturing-groups-regex-7.10.4.tgz#78b4d978810b6f3bcf03f9e318f2fc0ed41aecb6" + integrity sha512-V6LuOnD31kTkxQPhKiVYzYC/Jgdq53irJC/xBSmqcNcqFGV+PER4l6rU5SH2Vl7bH9mLDHcc0+l9HUOe4RNGKA== + dependencies: + "@babel/helper-create-regexp-features-plugin" "^7.10.4" + +"@babel/plugin-transform-new-target@^7.10.4": + version "7.10.4" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-new-target/-/plugin-transform-new-target-7.10.4.tgz#9097d753cb7b024cb7381a3b2e52e9513a9c6888" + integrity sha512-YXwWUDAH/J6dlfwqlWsztI2Puz1NtUAubXhOPLQ5gjR/qmQ5U96DY4FQO8At33JN4XPBhrjB8I4eMmLROjjLjw== + dependencies: + "@babel/helper-plugin-utils" "^7.10.4" + +"@babel/plugin-transform-object-super@^7.10.4": + version "7.10.4" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-object-super/-/plugin-transform-object-super-7.10.4.tgz#d7146c4d139433e7a6526f888c667e314a093894" + integrity sha512-5iTw0JkdRdJvr7sY0vHqTpnruUpTea32JHmq/atIWqsnNussbRzjEDyWep8UNztt1B5IusBYg8Irb0bLbiEBCQ== + dependencies: + "@babel/helper-plugin-utils" "^7.10.4" + "@babel/helper-replace-supers" "^7.10.4" + +"@babel/plugin-transform-parameters@^7.10.4": + version "7.10.5" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-parameters/-/plugin-transform-parameters-7.10.5.tgz#59d339d58d0b1950435f4043e74e2510005e2c4a" + integrity sha512-xPHwUj5RdFV8l1wuYiu5S9fqWGM2DrYc24TMvUiRrPVm+SM3XeqU9BcokQX/kEUe+p2RBwy+yoiR1w/Blq6ubw== + dependencies: + "@babel/helper-get-function-arity" "^7.10.4" + "@babel/helper-plugin-utils" "^7.10.4" + +"@babel/plugin-transform-property-literals@^7.10.4": + version "7.10.4" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-property-literals/-/plugin-transform-property-literals-7.10.4.tgz#f6fe54b6590352298785b83edd815d214c42e3c0" + integrity sha512-ofsAcKiUxQ8TY4sScgsGeR2vJIsfrzqvFb9GvJ5UdXDzl+MyYCaBj/FGzXuv7qE0aJcjWMILny1epqelnFlz8g== + dependencies: + "@babel/helper-plugin-utils" "^7.10.4" + +"@babel/plugin-transform-regenerator@^7.10.1", "@babel/plugin-transform-regenerator@^7.10.4": + version "7.10.4" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-regenerator/-/plugin-transform-regenerator-7.10.4.tgz#2015e59d839074e76838de2159db421966fd8b63" + integrity sha512-3thAHwtor39A7C04XucbMg17RcZ3Qppfxr22wYzZNcVIkPHfpM9J0SO8zuCV6SZa265kxBJSrfKTvDCYqBFXGw== + dependencies: + regenerator-transform "^0.14.2" + +"@babel/plugin-transform-reserved-words@^7.10.4": + version "7.10.4" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-reserved-words/-/plugin-transform-reserved-words-7.10.4.tgz#8f2682bcdcef9ed327e1b0861585d7013f8a54dd" + integrity sha512-hGsw1O6Rew1fkFbDImZIEqA8GoidwTAilwCyWqLBM9f+e/u/sQMQu7uX6dyokfOayRuuVfKOW4O7HvaBWM+JlQ== + dependencies: + "@babel/helper-plugin-utils" "^7.10.4" + +"@babel/plugin-transform-runtime@^7.11.0": + version "7.11.5" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-runtime/-/plugin-transform-runtime-7.11.5.tgz#f108bc8e0cf33c37da031c097d1df470b3a293fc" + integrity sha512-9aIoee+EhjySZ6vY5hnLjigHzunBlscx9ANKutkeWTJTx6m5Rbq6Ic01tLvO54lSusR+BxV7u4UDdCmXv5aagg== + dependencies: + "@babel/helper-module-imports" "^7.10.4" + "@babel/helper-plugin-utils" "^7.10.4" + resolve "^1.8.1" + semver "^5.5.1" + +"@babel/plugin-transform-shorthand-properties@^7.10.4": + version "7.10.4" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-shorthand-properties/-/plugin-transform-shorthand-properties-7.10.4.tgz#9fd25ec5cdd555bb7f473e5e6ee1c971eede4dd6" + integrity sha512-AC2K/t7o07KeTIxMoHneyX90v3zkm5cjHJEokrPEAGEy3UCp8sLKfnfOIGdZ194fyN4wfX/zZUWT9trJZ0qc+Q== + dependencies: + "@babel/helper-plugin-utils" "^7.10.4" + +"@babel/plugin-transform-spread@^7.11.0": + version "7.11.0" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-spread/-/plugin-transform-spread-7.11.0.tgz#fa84d300f5e4f57752fe41a6d1b3c554f13f17cc" + integrity sha512-UwQYGOqIdQJe4aWNyS7noqAnN2VbaczPLiEtln+zPowRNlD+79w3oi2TWfYe0eZgd+gjZCbsydN7lzWysDt+gw== + dependencies: + "@babel/helper-plugin-utils" "^7.10.4" + "@babel/helper-skip-transparent-expression-wrappers" "^7.11.0" + +"@babel/plugin-transform-sticky-regex@^7.10.4": + version "7.10.4" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-sticky-regex/-/plugin-transform-sticky-regex-7.10.4.tgz#8f3889ee8657581130a29d9cc91d7c73b7c4a28d" + integrity sha512-Ddy3QZfIbEV0VYcVtFDCjeE4xwVTJWTmUtorAJkn6u/92Z/nWJNV+mILyqHKrUxXYKA2EoCilgoPePymKL4DvQ== + dependencies: + "@babel/helper-plugin-utils" "^7.10.4" + "@babel/helper-regex" "^7.10.4" + +"@babel/plugin-transform-template-literals@^7.10.4": + version "7.10.5" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-template-literals/-/plugin-transform-template-literals-7.10.5.tgz#78bc5d626a6642db3312d9d0f001f5e7639fde8c" + integrity sha512-V/lnPGIb+KT12OQikDvgSuesRX14ck5FfJXt6+tXhdkJ+Vsd0lDCVtF6jcB4rNClYFzaB2jusZ+lNISDk2mMMw== + dependencies: + "@babel/helper-annotate-as-pure" "^7.10.4" + "@babel/helper-plugin-utils" "^7.10.4" + +"@babel/plugin-transform-typeof-symbol@^7.10.4": + version "7.10.4" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-typeof-symbol/-/plugin-transform-typeof-symbol-7.10.4.tgz#9509f1a7eec31c4edbffe137c16cc33ff0bc5bfc" + integrity sha512-QqNgYwuuW0y0H+kUE/GWSR45t/ccRhe14Fs/4ZRouNNQsyd4o3PG4OtHiIrepbM2WKUBDAXKCAK/Lk4VhzTaGA== + dependencies: + "@babel/helper-plugin-utils" "^7.10.4" + +"@babel/plugin-transform-unicode-escapes@^7.10.4": + version "7.10.4" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-unicode-escapes/-/plugin-transform-unicode-escapes-7.10.4.tgz#feae523391c7651ddac115dae0a9d06857892007" + integrity sha512-y5XJ9waMti2J+e7ij20e+aH+fho7Wb7W8rNuu72aKRwCHFqQdhkdU2lo3uZ9tQuboEJcUFayXdARhcxLQ3+6Fg== + dependencies: + "@babel/helper-plugin-utils" "^7.10.4" + +"@babel/plugin-transform-unicode-regex@^7.10.4": + version "7.10.4" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-unicode-regex/-/plugin-transform-unicode-regex-7.10.4.tgz#e56d71f9282fac6db09c82742055576d5e6d80a8" + integrity sha512-wNfsc4s8N2qnIwpO/WP2ZiSyjfpTamT2C9V9FDH/Ljub9zw6P3SjkXcFmc0RQUt96k2fmIvtla2MMjgTwIAC+A== + dependencies: + "@babel/helper-create-regexp-features-plugin" "^7.10.4" + "@babel/helper-plugin-utils" "^7.10.4" + +"@babel/preset-env@^7.11.0": + version "7.11.5" + resolved "https://registry.yarnpkg.com/@babel/preset-env/-/preset-env-7.11.5.tgz#18cb4b9379e3e92ffea92c07471a99a2914e4272" + integrity sha512-kXqmW1jVcnB2cdueV+fyBM8estd5mlNfaQi6lwLgRwCby4edpavgbFhiBNjmWA3JpB/yZGSISa7Srf+TwxDQoA== + dependencies: + "@babel/compat-data" "^7.11.0" + "@babel/helper-compilation-targets" "^7.10.4" + "@babel/helper-module-imports" "^7.10.4" + "@babel/helper-plugin-utils" "^7.10.4" + "@babel/plugin-proposal-async-generator-functions" "^7.10.4" + "@babel/plugin-proposal-class-properties" "^7.10.4" + "@babel/plugin-proposal-dynamic-import" "^7.10.4" + "@babel/plugin-proposal-export-namespace-from" "^7.10.4" + "@babel/plugin-proposal-json-strings" "^7.10.4" + "@babel/plugin-proposal-logical-assignment-operators" "^7.11.0" + "@babel/plugin-proposal-nullish-coalescing-operator" "^7.10.4" + "@babel/plugin-proposal-numeric-separator" "^7.10.4" + "@babel/plugin-proposal-object-rest-spread" "^7.11.0" + "@babel/plugin-proposal-optional-catch-binding" "^7.10.4" + "@babel/plugin-proposal-optional-chaining" "^7.11.0" + "@babel/plugin-proposal-private-methods" "^7.10.4" + "@babel/plugin-proposal-unicode-property-regex" "^7.10.4" + "@babel/plugin-syntax-async-generators" "^7.8.0" + "@babel/plugin-syntax-class-properties" "^7.10.4" + "@babel/plugin-syntax-dynamic-import" "^7.8.0" + "@babel/plugin-syntax-export-namespace-from" "^7.8.3" + "@babel/plugin-syntax-json-strings" "^7.8.0" + "@babel/plugin-syntax-logical-assignment-operators" "^7.10.4" + "@babel/plugin-syntax-nullish-coalescing-operator" "^7.8.0" + "@babel/plugin-syntax-numeric-separator" "^7.10.4" + "@babel/plugin-syntax-object-rest-spread" "^7.8.0" + "@babel/plugin-syntax-optional-catch-binding" "^7.8.0" + "@babel/plugin-syntax-optional-chaining" "^7.8.0" + "@babel/plugin-syntax-top-level-await" "^7.10.4" + "@babel/plugin-transform-arrow-functions" "^7.10.4" + "@babel/plugin-transform-async-to-generator" "^7.10.4" + "@babel/plugin-transform-block-scoped-functions" "^7.10.4" + "@babel/plugin-transform-block-scoping" "^7.10.4" + "@babel/plugin-transform-classes" "^7.10.4" + "@babel/plugin-transform-computed-properties" "^7.10.4" + "@babel/plugin-transform-destructuring" "^7.10.4" + "@babel/plugin-transform-dotall-regex" "^7.10.4" + "@babel/plugin-transform-duplicate-keys" "^7.10.4" + "@babel/plugin-transform-exponentiation-operator" "^7.10.4" + "@babel/plugin-transform-for-of" "^7.10.4" + "@babel/plugin-transform-function-name" "^7.10.4" + "@babel/plugin-transform-literals" "^7.10.4" + "@babel/plugin-transform-member-expression-literals" "^7.10.4" + "@babel/plugin-transform-modules-amd" "^7.10.4" + "@babel/plugin-transform-modules-commonjs" "^7.10.4" + "@babel/plugin-transform-modules-systemjs" "^7.10.4" + "@babel/plugin-transform-modules-umd" "^7.10.4" + "@babel/plugin-transform-named-capturing-groups-regex" "^7.10.4" + "@babel/plugin-transform-new-target" "^7.10.4" + "@babel/plugin-transform-object-super" "^7.10.4" + "@babel/plugin-transform-parameters" "^7.10.4" + "@babel/plugin-transform-property-literals" "^7.10.4" + "@babel/plugin-transform-regenerator" "^7.10.4" + "@babel/plugin-transform-reserved-words" "^7.10.4" + "@babel/plugin-transform-shorthand-properties" "^7.10.4" + "@babel/plugin-transform-spread" "^7.11.0" + "@babel/plugin-transform-sticky-regex" "^7.10.4" + "@babel/plugin-transform-template-literals" "^7.10.4" + "@babel/plugin-transform-typeof-symbol" "^7.10.4" + "@babel/plugin-transform-unicode-escapes" "^7.10.4" + "@babel/plugin-transform-unicode-regex" "^7.10.4" + "@babel/preset-modules" "^0.1.3" + "@babel/types" "^7.11.5" + browserslist "^4.12.0" + core-js-compat "^3.6.2" + invariant "^2.2.2" + levenary "^1.1.1" + semver "^5.5.0" + +"@babel/preset-modules@^0.1.3": + version "0.1.4" + resolved "https://registry.yarnpkg.com/@babel/preset-modules/-/preset-modules-0.1.4.tgz#362f2b68c662842970fdb5e254ffc8fc1c2e415e" + integrity sha512-J36NhwnfdzpmH41M1DrnkkgAqhZaqr/NBdPfQ677mLzlaXo+oDiv1deyCDtgAhz8p328otdob0Du7+xgHGZbKg== + dependencies: + "@babel/helper-plugin-utils" "^7.0.0" + "@babel/plugin-proposal-unicode-property-regex" "^7.4.4" + "@babel/plugin-transform-dotall-regex" "^7.4.4" + "@babel/types" "^7.4.4" + esutils "^2.0.2" + +"@babel/runtime-corejs3@^7.10.2": + version "7.11.2" + resolved "https://registry.yarnpkg.com/@babel/runtime-corejs3/-/runtime-corejs3-7.11.2.tgz#02c3029743150188edeb66541195f54600278419" + integrity sha512-qh5IR+8VgFz83VBa6OkaET6uN/mJOhHONuy3m1sgF0CV6mXdPSEBdA7e1eUbVvyNtANjMbg22JUv71BaDXLY6A== + dependencies: + core-js-pure "^3.0.0" + regenerator-runtime "^0.13.4" + +"@babel/runtime@^7.0.0": + version "7.6.3" + resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.6.3.tgz#935122c74c73d2240cafd32ddb5fc2a6cd35cf1f" + dependencies: + regenerator-runtime "^0.13.2" + +"@babel/runtime@^7.1.2": + version "7.5.5" + resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.5.5.tgz#74fba56d35efbeca444091c7850ccd494fd2f132" + dependencies: + regenerator-runtime "^0.13.2" + +"@babel/runtime@^7.10.2", "@babel/runtime@^7.11.2", "@babel/runtime@^7.5.5", "@babel/runtime@^7.7.2", "@babel/runtime@^7.8.4": + version "7.11.2" + resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.11.2.tgz#f549c13c754cc40b87644b9fa9f09a6a95fe0736" + integrity sha512-TeWkU52so0mPtDcaCTxNBI/IHiz0pZgr8VEFqXFtZWpYD08ZB6FaSwVAS8MKRQAP3bYKiVjwysOJgMFY28o6Tw== + dependencies: + regenerator-runtime "^0.13.4" + +"@babel/template@^7.10.4": + version "7.10.4" + resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.10.4.tgz#3251996c4200ebc71d1a8fc405fba940f36ba278" + integrity sha512-ZCjD27cGJFUB6nmCB1Enki3r+L5kJveX9pq1SvAUKoICy6CZ9yD8xO086YXdYhvNjBdnekm4ZnaP5yC8Cs/1tA== + dependencies: + "@babel/code-frame" "^7.10.4" + "@babel/parser" "^7.10.4" + "@babel/types" "^7.10.4" + +"@babel/traverse@^7.10.4", "@babel/traverse@^7.11.5": + version "7.11.5" + resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.11.5.tgz#be777b93b518eb6d76ee2e1ea1d143daa11e61c3" + integrity sha512-EjiPXt+r7LiCZXEfRpSJd+jUMnBd4/9OUv7Nx3+0u9+eimMwJmG0Q98lw4/289JCoxSE8OolDMNZaaF/JZ69WQ== + dependencies: + "@babel/code-frame" "^7.10.4" + "@babel/generator" "^7.11.5" + "@babel/helper-function-name" "^7.10.4" + "@babel/helper-split-export-declaration" "^7.11.0" + "@babel/parser" "^7.11.5" + "@babel/types" "^7.11.5" + debug "^4.1.0" + globals "^11.1.0" + lodash "^4.17.19" + +"@babel/types@^7.10.4", "@babel/types@^7.10.5", "@babel/types@^7.11.0", "@babel/types@^7.11.5", "@babel/types@^7.4.4": + version "7.11.5" + resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.11.5.tgz#d9de577d01252d77c6800cee039ee64faf75662d" + integrity sha512-bvM7Qz6eKnJVFIn+1LPtjlBFPVN5jNDc1XmN15vWe7Q3DPBufWWsLiIvUu7xW87uTG6QoggpIDnUgLQvPheU+Q== + dependencies: + "@babel/helper-validator-identifier" "^7.10.4" + lodash "^4.17.19" + to-fast-properties "^2.0.0" + +"@csstools/convert-colors@^1.4.0": + version "1.4.0" + resolved "https://registry.yarnpkg.com/@csstools/convert-colors/-/convert-colors-1.4.0.tgz#ad495dc41b12e75d588c6db8b9834f08fa131eb7" + integrity sha512-5a6wqoJV/xEdbRNKVo6I4hO3VjyDq//8q2f9I6PBAvMesJHFauXDorcNCsr9RzvsZnaWi5NYCcfyqP1QeFHFbw== + +"@eslint/eslintrc@^0.3.0": + version "0.3.0" + resolved "https://registry.yarnpkg.com/@eslint/eslintrc/-/eslintrc-0.3.0.tgz#d736d6963d7003b6514e6324bec9c602ac340318" + integrity sha512-1JTKgrOKAHVivSvOYw+sJOunkBjUOvjqWk1DPja7ZFhIS2mX/4EgTT8M7eTK9jrKhL/FvXXEbQwIs3pg1xp3dg== + dependencies: + ajv "^6.12.4" + debug "^4.1.1" + espree "^7.3.0" + globals "^12.1.0" + ignore "^4.0.6" + import-fresh "^3.2.1" + js-yaml "^3.13.1" + lodash "^4.17.20" + minimatch "^3.0.4" + strip-json-comments "^3.1.1" + +"@npmcli/move-file@^1.0.1": + version "1.0.1" + resolved "https://registry.yarnpkg.com/@npmcli/move-file/-/move-file-1.0.1.tgz#de103070dac0f48ce49cf6693c23af59c0f70464" + integrity sha512-Uv6h1sT+0DrblvIrolFtbvM1FgWm+/sy4B3pvLp67Zys+thcukzS5ekn7HsZFGpWP4Q3fYJCljbWQE/XivMRLw== + dependencies: + mkdirp "^1.0.4" + +"@rails/webpacker@5.2.1": + version "5.2.1" + resolved "https://registry.yarnpkg.com/@rails/webpacker/-/webpacker-5.2.1.tgz#87cdbd4af2090ae2d74bdc51f6f04717d907c5b3" + integrity sha512-rO0kOv0o4ESB8ZnKX+b54ZKogNJGWSMULGmsJacREfm9SahKEQwXBeHNsqSGtS9NAPsU6YUFhGKRd4i/kbMNrQ== + dependencies: + "@babel/core" "^7.11.1" + "@babel/plugin-proposal-class-properties" "^7.10.4" + "@babel/plugin-proposal-object-rest-spread" "^7.10.1" + "@babel/plugin-syntax-dynamic-import" "^7.8.3" + "@babel/plugin-transform-destructuring" "^7.10.1" + "@babel/plugin-transform-regenerator" "^7.10.1" + "@babel/plugin-transform-runtime" "^7.11.0" + "@babel/preset-env" "^7.11.0" + "@babel/runtime" "^7.11.2" + babel-loader "^8.1.0" + babel-plugin-dynamic-import-node "^2.3.3" + babel-plugin-macros "^2.8.0" + case-sensitive-paths-webpack-plugin "^2.3.0" + compression-webpack-plugin "^4.0.0" + core-js "^3.6.5" + css-loader "^3.5.3" + file-loader "^6.0.0" + flatted "^3.0.4" + glob "^7.1.6" + js-yaml "^3.14.0" + mini-css-extract-plugin "^0.9.0" + node-sass "^4.14.1" + optimize-css-assets-webpack-plugin "^5.0.3" + path-complete-extname "^1.0.0" + pnp-webpack-plugin "^1.6.4" + postcss-flexbugs-fixes "^4.2.1" + postcss-import "^12.0.1" + postcss-loader "^3.0.0" + postcss-preset-env "^6.7.0" + postcss-safe-parser "^4.0.2" + regenerator-runtime "^0.13.7" + sass-loader "^8.0.2" + style-loader "^1.2.1" + terser-webpack-plugin "^4.0.0" + webpack "^4.44.1" + webpack-assets-manifest "^3.1.1" + webpack-cli "^3.3.12" + webpack-sources "^1.4.3" + +"@types/glob@^7.1.1": + version "7.1.3" + resolved "https://registry.yarnpkg.com/@types/glob/-/glob-7.1.3.tgz#e6ba80f36b7daad2c685acd9266382e68985c183" + integrity sha512-SEYeGAIQIQX8NN6LDKprLjbrd5dARM5EXsd8GI/A5l0apYI1fGMWgPHSe4ZKL4eozlAyI+doUE9XbYS4xCkQ1w== + dependencies: + "@types/minimatch" "*" + "@types/node" "*" + +"@types/json-schema@^7.0.5": + version "7.0.6" + resolved "https://registry.yarnpkg.com/@types/json-schema/-/json-schema-7.0.6.tgz#f4c7ec43e81b319a9815115031709f26987891f0" + integrity sha512-3c+yGKvVP5Y9TYBEibGNR+kLtijnj7mYrXRg+WpFb2X9xm04g/DXYkfg4hmzJQosc9snFNUPkbYIhu+KAm6jJw== + +"@types/json5@^0.0.29": + version "0.0.29" + resolved "https://registry.yarnpkg.com/@types/json5/-/json5-0.0.29.tgz#ee28707ae94e11d2b827bcbe5270bcea7f3e71ee" + integrity sha1-7ihweulOEdK4J7y+UnC86n8+ce4= + +"@types/lodash-es@^4.17.3": + version "4.17.3" + resolved "https://registry.yarnpkg.com/@types/lodash-es/-/lodash-es-4.17.3.tgz#87eb0b3673b076b8ee655f1890260a136af09a2d" + integrity sha512-iHI0i7ZAL1qepz1Y7f3EKg/zUMDwDfTzitx+AlHhJJvXwenP682ZyGbgPSc5Ej3eEAKVbNWKFuwOadCj5vBbYQ== + dependencies: + "@types/lodash" "*" + +"@types/lodash@*": + version "4.14.137" + resolved "https://registry.yarnpkg.com/@types/lodash/-/lodash-4.14.137.tgz#8a4804937dc6462274ffcc088df8f14fc1b368e2" + +"@types/minimatch@*": + version "3.0.3" + resolved "https://registry.yarnpkg.com/@types/minimatch/-/minimatch-3.0.3.tgz#3dca0e3f33b200fc7d1139c0cd96c1268cadfd9d" + integrity sha512-tHq6qdbT9U1IRSGf14CL0pUlULksvY9OZ+5eEgl1N7t+OA3tGvNpxJCzuKQlsNgCVwbAs670L1vcVQi8j9HjnA== + +"@types/node-fetch@^2.5.8": + version "2.5.8" + resolved "https://registry.yarnpkg.com/@types/node-fetch/-/node-fetch-2.5.8.tgz#e199c835d234c7eb0846f6618012e558544ee2fb" + integrity sha512-fbjI6ja0N5ZA8TV53RUqzsKNkl9fv8Oj3T7zxW7FGv1GSH7gwJaNF8dzCjrqKaxKeUpTz4yT1DaJFq/omNpGfw== + dependencies: + "@types/node" "*" + form-data "^3.0.0" + +"@types/node@*": + version "12.7.2" + resolved "https://registry.yarnpkg.com/@types/node/-/node-12.7.2.tgz#c4e63af5e8823ce9cc3f0b34f7b998c2171f0c44" + +"@types/parse-json@^4.0.0": + version "4.0.0" + resolved "https://registry.yarnpkg.com/@types/parse-json/-/parse-json-4.0.0.tgz#2f8bb441434d163b35fb8ffdccd7138927ffb8c0" + integrity sha512-//oorEZjL6sbPcKUaCdIGlIUeH26mgzimjBB77G6XRgnDl/L5wOnpyBGRe/Mmf5CVW3PwEBE1NjiMZ/ssFh4wA== + +"@types/pdfjs-dist@*": + version "2.1.2" + resolved "https://registry.yarnpkg.com/@types/pdfjs-dist/-/pdfjs-dist-2.1.2.tgz#fd4013eaa8fb8ac2bc96c9df6a1b2346121dabc8" + +"@types/prop-types@*": + version "15.7.1" + resolved "https://registry.yarnpkg.com/@types/prop-types/-/prop-types-15.7.1.tgz#f1a11e7babb0c3cad68100be381d1e064c68f1f6" + +"@types/q@^1.5.1": + version "1.5.2" + resolved "https://registry.yarnpkg.com/@types/q/-/q-1.5.2.tgz#690a1475b84f2a884fd07cd797c00f5f31356ea8" + +"@types/rc-slider@^8.6.6": + version "8.6.6" + resolved "https://registry.yarnpkg.com/@types/rc-slider/-/rc-slider-8.6.6.tgz#961ccfd0b8c632a9d8cdcc28b54d6be4781c419f" + integrity sha512-2Q3vwKrSm3PbgiMNwzxMkOaMtcAGi0xQ8WPeVKoabk1vNYHiVR44DMC3mr9jC2lhbxCBgGBJWF9sBhmnSDQ8Bg== + dependencies: + "@types/rc-tooltip" "*" + "@types/react" "*" + +"@types/rc-tooltip@*": + version "3.7.1" + resolved "https://registry.yarnpkg.com/@types/rc-tooltip/-/rc-tooltip-3.7.1.tgz#20bde0f9c872f824c1dea86d0fc741dfd9aca3d7" + dependencies: + "@types/react" "*" + +"@types/react-addons-css-transition-group@^15.0.5": + version "15.0.5" + resolved "https://registry.yarnpkg.com/@types/react-addons-css-transition-group/-/react-addons-css-transition-group-15.0.5.tgz#73665af6b8efb47730ab583ead4bed5373dae686" + dependencies: + "@types/react" "*" + "@types/react-addons-transition-group" "*" + +"@types/react-addons-transition-group@*": + version "15.0.4" + resolved "https://registry.yarnpkg.com/@types/react-addons-transition-group/-/react-addons-transition-group-15.0.4.tgz#5fb10a686e6f0899fecdc0efc63ea7166c24638e" + dependencies: + "@types/react" "*" + +"@types/react-beautiful-dnd@^13.0.0": + version "13.0.0" + resolved "https://registry.yarnpkg.com/@types/react-beautiful-dnd/-/react-beautiful-dnd-13.0.0.tgz#e60d3d965312fcf1516894af92dc3e9249587db4" + integrity sha512-by80tJ8aTTDXT256Gl+RfLRtFjYbUWOnZuEigJgNsJrSEGxvFe5eY6k3g4VIvf0M/6+xoLgfYWoWonlOo6Wqdg== + dependencies: + "@types/react" "*" + +"@types/react-copy-to-clipboard@^5.0.0": + version "5.0.0" + resolved "https://registry.yarnpkg.com/@types/react-copy-to-clipboard/-/react-copy-to-clipboard-5.0.0.tgz#38b035ca0c28334d3e0efaf3f319b81eea9690cd" + integrity sha512-faUg6Kx3Dfv0MBIcs+xzIptlRtjEVSaNjqyC14YAp4UwSiTHghnKtBOt9ERRTZZJfoJgnw10tomVaqG86GzdAw== + dependencies: + "@types/react" "*" + +"@types/react-datepicker@^3.1.3": + version "3.1.3" + resolved "https://registry.yarnpkg.com/@types/react-datepicker/-/react-datepicker-3.1.3.tgz#4525475c451091477de27ed52c029a9043ec6b11" + integrity sha512-Gca56Pa8hpy3eO2naRLdC63iflER7JXxgdHTGJ6yF5DjvhQoirRPqCJerWLq6TOWd+sEsZWwMm17Q17YRVLFtw== + dependencies: + "@types/react" "*" + date-fns "^2.0.1" + popper.js "^1.14.1" + +"@types/react-dom@^17.0.0": + version "17.0.0" + resolved "https://registry.yarnpkg.com/@types/react-dom/-/react-dom-17.0.0.tgz#b3b691eb956c4b3401777ee67b900cb28415d95a" + integrity sha512-lUqY7OlkF/RbNtD5nIq7ot8NquXrdFrjSOR6+w9a9RFQevGi1oZO1dcJbXMeONAPKtZ2UrZOEJ5UOCVsxbLk/g== + dependencies: + "@types/react" "*" + +"@types/react-paginate@^6.2.1": + version "6.2.1" + resolved "https://registry.yarnpkg.com/@types/react-paginate/-/react-paginate-6.2.1.tgz#2329088d78010ea49661559c8467a24ecd942784" + integrity sha512-+q8k1N0WzbMyOCsIEH/p5D6/KQD8dXYLzfvSvriYn//94icd2sqhAL2rWXkgwGvqHGCSTU9AoHtsWCJxPfquUQ== + dependencies: + "@types/react" "*" + +"@types/react-pdf@^5.0.1": + version "5.0.1" + resolved "https://registry.yarnpkg.com/@types/react-pdf/-/react-pdf-5.0.1.tgz#bfa1a2e052dabe76c2fcc30162e1d3e33a4c2abe" + integrity sha512-iDpGh45UAyLx4udkzJy0lxMeCLBrAfO08fEL4FKfeSXXvBTnax60ueTTysJ4XY0XqEJn5gYDuMH93nlJhAjFoQ== + dependencies: + "@types/pdfjs-dist" "*" + "@types/react" "*" + +"@types/react-textarea-autosize@^4.3.5": + version "4.3.5" + resolved "https://registry.yarnpkg.com/@types/react-textarea-autosize/-/react-textarea-autosize-4.3.5.tgz#6c4d2753fa1864c98c0b2b517f67bb1f6e4c46de" + integrity sha512-PiDL83kPMTolyZAWW3lyzO6ktooTb9tFTntVy7CA83/qFLWKLJ5bLeRboy6J6j3b1e8h2Eec6gBTEOOJRjV14A== + dependencies: + "@types/react" "*" + +"@types/react@*": + version "16.9.2" + resolved "https://registry.yarnpkg.com/@types/react/-/react-16.9.2.tgz#6d1765431a1ad1877979013906731aae373de268" + dependencies: + "@types/prop-types" "*" + csstype "^2.2.0" + +"@types/react@^17.0.0": + version "17.0.0" + resolved "https://registry.yarnpkg.com/@types/react/-/react-17.0.0.tgz#5af3eb7fad2807092f0046a1302b7823e27919b8" + integrity sha512-aj/L7RIMsRlWML3YB6KZiXB3fV2t41+5RBGYF8z+tAKU43Px8C3cYUZsDvf1/+Bm4FK21QWBrDutu8ZJ/70qOw== + dependencies: + "@types/prop-types" "*" + csstype "^3.0.2" + +"@types/vimeo__player@^2.10.0": + version "2.10.0" + resolved "https://registry.yarnpkg.com/@types/vimeo__player/-/vimeo__player-2.10.0.tgz#dee8934c59d0a01a4252e6fb143c8807d3235502" + integrity sha512-uhKJDgEfQiEhKHw9bP7+wZSvXTyN4iw4Yb+7Omo76jCJZp1NZkWfZYaaobXrkiuFT6k2ld2eqaXKSpvqfmDUPQ== + +"@vimeo/player@^2.15.0": + version "2.15.0" + resolved "https://registry.yarnpkg.com/@vimeo/player/-/player-2.15.0.tgz#c1037706f0a95abf87f6aa960d058e11efe6510e" + integrity sha512-PH+H7G7pIUbrw8AHeigOc13cUoe8E78y4XCVqSP7pLFj/kQm0EvHQGBO3224Qq7a0XMEmNR6kH+n+8o0GslWCg== + dependencies: + native-promise-only "0.8.1" + weakmap-polyfill "2.0.1" + +"@webassemblyjs/ast@1.9.0": + version "1.9.0" + resolved "https://registry.yarnpkg.com/@webassemblyjs/ast/-/ast-1.9.0.tgz#bd850604b4042459a5a41cd7d338cbed695ed964" + integrity sha512-C6wW5L+b7ogSDVqymbkkvuW9kruN//YisMED04xzeBBqjHa2FYnmvOlS6Xj68xWQRgWvI9cIglsjFowH/RJyEA== + dependencies: + "@webassemblyjs/helper-module-context" "1.9.0" + "@webassemblyjs/helper-wasm-bytecode" "1.9.0" + "@webassemblyjs/wast-parser" "1.9.0" + +"@webassemblyjs/floating-point-hex-parser@1.9.0": + version "1.9.0" + resolved "https://registry.yarnpkg.com/@webassemblyjs/floating-point-hex-parser/-/floating-point-hex-parser-1.9.0.tgz#3c3d3b271bddfc84deb00f71344438311d52ffb4" + integrity sha512-TG5qcFsS8QB4g4MhrxK5TqfdNe7Ey/7YL/xN+36rRjl/BlGE/NcBvJcqsRgCP6Z92mRE+7N50pRIi8SmKUbcQA== + +"@webassemblyjs/helper-api-error@1.9.0": + version "1.9.0" + resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-api-error/-/helper-api-error-1.9.0.tgz#203f676e333b96c9da2eeab3ccef33c45928b6a2" + integrity sha512-NcMLjoFMXpsASZFxJ5h2HZRcEhDkvnNFOAKneP5RbKRzaWJN36NC4jqQHKwStIhGXu5mUWlUUk7ygdtrO8lbmw== + +"@webassemblyjs/helper-buffer@1.9.0": + version "1.9.0" + resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-buffer/-/helper-buffer-1.9.0.tgz#a1442d269c5feb23fcbc9ef759dac3547f29de00" + integrity sha512-qZol43oqhq6yBPx7YM3m9Bv7WMV9Eevj6kMi6InKOuZxhw+q9hOkvq5e/PpKSiLfyetpaBnogSbNCfBwyB00CA== + +"@webassemblyjs/helper-code-frame@1.9.0": + version "1.9.0" + resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-code-frame/-/helper-code-frame-1.9.0.tgz#647f8892cd2043a82ac0c8c5e75c36f1d9159f27" + integrity sha512-ERCYdJBkD9Vu4vtjUYe8LZruWuNIToYq/ME22igL+2vj2dQ2OOujIZr3MEFvfEaqKoVqpsFKAGsRdBSBjrIvZA== + dependencies: + "@webassemblyjs/wast-printer" "1.9.0" + +"@webassemblyjs/helper-fsm@1.9.0": + version "1.9.0" + resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-fsm/-/helper-fsm-1.9.0.tgz#c05256b71244214671f4b08ec108ad63b70eddb8" + integrity sha512-OPRowhGbshCb5PxJ8LocpdX9Kl0uB4XsAjl6jH/dWKlk/mzsANvhwbiULsaiqT5GZGT9qinTICdj6PLuM5gslw== + +"@webassemblyjs/helper-module-context@1.9.0": + version "1.9.0" + resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-module-context/-/helper-module-context-1.9.0.tgz#25d8884b76839871a08a6c6f806c3979ef712f07" + integrity sha512-MJCW8iGC08tMk2enck1aPW+BE5Cw8/7ph/VGZxwyvGbJwjktKkDK7vy7gAmMDx88D7mhDTCNKAW5tED+gZ0W8g== + dependencies: + "@webassemblyjs/ast" "1.9.0" + +"@webassemblyjs/helper-wasm-bytecode@1.9.0": + version "1.9.0" + resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-wasm-bytecode/-/helper-wasm-bytecode-1.9.0.tgz#4fed8beac9b8c14f8c58b70d124d549dd1fe5790" + integrity sha512-R7FStIzyNcd7xKxCZH5lE0Bqy+hGTwS3LJjuv1ZVxd9O7eHCedSdrId/hMOd20I+v8wDXEn+bjfKDLzTepoaUw== + +"@webassemblyjs/helper-wasm-section@1.9.0": + version "1.9.0" + resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-wasm-section/-/helper-wasm-section-1.9.0.tgz#5a4138d5a6292ba18b04c5ae49717e4167965346" + integrity sha512-XnMB8l3ek4tvrKUUku+IVaXNHz2YsJyOOmz+MMkZvh8h1uSJpSen6vYnw3IoQ7WwEuAhL8Efjms1ZWjqh2agvw== + dependencies: + "@webassemblyjs/ast" "1.9.0" + "@webassemblyjs/helper-buffer" "1.9.0" + "@webassemblyjs/helper-wasm-bytecode" "1.9.0" + "@webassemblyjs/wasm-gen" "1.9.0" + +"@webassemblyjs/ieee754@1.9.0": + version "1.9.0" + resolved "https://registry.yarnpkg.com/@webassemblyjs/ieee754/-/ieee754-1.9.0.tgz#15c7a0fbaae83fb26143bbacf6d6df1702ad39e4" + integrity sha512-dcX8JuYU/gvymzIHc9DgxTzUUTLexWwt8uCTWP3otys596io0L5aW02Gb1RjYpx2+0Jus1h4ZFqjla7umFniTg== + dependencies: + "@xtuc/ieee754" "^1.2.0" + +"@webassemblyjs/leb128@1.9.0": + version "1.9.0" + resolved "https://registry.yarnpkg.com/@webassemblyjs/leb128/-/leb128-1.9.0.tgz#f19ca0b76a6dc55623a09cffa769e838fa1e1c95" + integrity sha512-ENVzM5VwV1ojs9jam6vPys97B/S65YQtv/aanqnU7D8aSoHFX8GyhGg0CMfyKNIHBuAVjy3tlzd5QMMINa7wpw== + dependencies: + "@xtuc/long" "4.2.2" + +"@webassemblyjs/utf8@1.9.0": + version "1.9.0" + resolved "https://registry.yarnpkg.com/@webassemblyjs/utf8/-/utf8-1.9.0.tgz#04d33b636f78e6a6813227e82402f7637b6229ab" + integrity sha512-GZbQlWtopBTP0u7cHrEx+73yZKrQoBMpwkGEIqlacljhXCkVM1kMQge/Mf+csMJAjEdSwhOyLAS0AoR3AG5P8w== + +"@webassemblyjs/wasm-edit@1.9.0": + version "1.9.0" + resolved "https://registry.yarnpkg.com/@webassemblyjs/wasm-edit/-/wasm-edit-1.9.0.tgz#3fe6d79d3f0f922183aa86002c42dd256cfee9cf" + integrity sha512-FgHzBm80uwz5M8WKnMTn6j/sVbqilPdQXTWraSjBwFXSYGirpkSWE2R9Qvz9tNiTKQvoKILpCuTjBKzOIm0nxw== + dependencies: + "@webassemblyjs/ast" "1.9.0" + "@webassemblyjs/helper-buffer" "1.9.0" + "@webassemblyjs/helper-wasm-bytecode" "1.9.0" + "@webassemblyjs/helper-wasm-section" "1.9.0" + "@webassemblyjs/wasm-gen" "1.9.0" + "@webassemblyjs/wasm-opt" "1.9.0" + "@webassemblyjs/wasm-parser" "1.9.0" + "@webassemblyjs/wast-printer" "1.9.0" + +"@webassemblyjs/wasm-gen@1.9.0": + version "1.9.0" + resolved "https://registry.yarnpkg.com/@webassemblyjs/wasm-gen/-/wasm-gen-1.9.0.tgz#50bc70ec68ded8e2763b01a1418bf43491a7a49c" + integrity sha512-cPE3o44YzOOHvlsb4+E9qSqjc9Qf9Na1OO/BHFy4OI91XDE14MjFN4lTMezzaIWdPqHnsTodGGNP+iRSYfGkjA== + dependencies: + "@webassemblyjs/ast" "1.9.0" + "@webassemblyjs/helper-wasm-bytecode" "1.9.0" + "@webassemblyjs/ieee754" "1.9.0" + "@webassemblyjs/leb128" "1.9.0" + "@webassemblyjs/utf8" "1.9.0" + +"@webassemblyjs/wasm-opt@1.9.0": + version "1.9.0" + resolved "https://registry.yarnpkg.com/@webassemblyjs/wasm-opt/-/wasm-opt-1.9.0.tgz#2211181e5b31326443cc8112eb9f0b9028721a61" + integrity sha512-Qkjgm6Anhm+OMbIL0iokO7meajkzQD71ioelnfPEj6r4eOFuqm4YC3VBPqXjFyyNwowzbMD+hizmprP/Fwkl2A== + dependencies: + "@webassemblyjs/ast" "1.9.0" + "@webassemblyjs/helper-buffer" "1.9.0" + "@webassemblyjs/wasm-gen" "1.9.0" + "@webassemblyjs/wasm-parser" "1.9.0" + +"@webassemblyjs/wasm-parser@1.9.0": + version "1.9.0" + resolved "https://registry.yarnpkg.com/@webassemblyjs/wasm-parser/-/wasm-parser-1.9.0.tgz#9d48e44826df4a6598294aa6c87469d642fff65e" + integrity sha512-9+wkMowR2AmdSWQzsPEjFU7njh8HTO5MqO8vjwEHuM+AMHioNqSBONRdr0NQQ3dVQrzp0s8lTcYqzUdb7YgELA== + dependencies: + "@webassemblyjs/ast" "1.9.0" + "@webassemblyjs/helper-api-error" "1.9.0" + "@webassemblyjs/helper-wasm-bytecode" "1.9.0" + "@webassemblyjs/ieee754" "1.9.0" + "@webassemblyjs/leb128" "1.9.0" + "@webassemblyjs/utf8" "1.9.0" + +"@webassemblyjs/wast-parser@1.9.0": + version "1.9.0" + resolved "https://registry.yarnpkg.com/@webassemblyjs/wast-parser/-/wast-parser-1.9.0.tgz#3031115d79ac5bd261556cecc3fa90a3ef451914" + integrity sha512-qsqSAP3QQ3LyZjNC/0jBJ/ToSxfYJ8kYyuiGvtn/8MK89VrNEfwj7BPQzJVHi0jGTRK2dGdJ5PRqhtjzoww+bw== + dependencies: + "@webassemblyjs/ast" "1.9.0" + "@webassemblyjs/floating-point-hex-parser" "1.9.0" + "@webassemblyjs/helper-api-error" "1.9.0" + "@webassemblyjs/helper-code-frame" "1.9.0" + "@webassemblyjs/helper-fsm" "1.9.0" + "@xtuc/long" "4.2.2" + +"@webassemblyjs/wast-printer@1.9.0": + version "1.9.0" + resolved "https://registry.yarnpkg.com/@webassemblyjs/wast-printer/-/wast-printer-1.9.0.tgz#4935d54c85fef637b00ce9f52377451d00d47899" + integrity sha512-2J0nE95rHXHyQ24cWjMKJ1tqB/ds8z/cyeOZxJhcb+rW+SQASVjuznUSmdz5GpVJTzU8JkhYut0D3siFDD6wsA== + dependencies: + "@webassemblyjs/ast" "1.9.0" + "@webassemblyjs/wast-parser" "1.9.0" + "@xtuc/long" "4.2.2" + +"@xtuc/ieee754@^1.2.0": + version "1.2.0" + resolved "https://registry.yarnpkg.com/@xtuc/ieee754/-/ieee754-1.2.0.tgz#eef014a3145ae477a1cbc00cd1e552336dceb790" + integrity sha512-DX8nKgqcGwsc0eJSqYt5lwP4DH5FlHnmuWWBRy7X0NcaGR0ZtuyeESgMwTYVEtxmsNGY+qit4QYT/MIYTOTPeA== + +"@xtuc/long@4.2.2": + version "4.2.2" + resolved "https://registry.yarnpkg.com/@xtuc/long/-/long-4.2.2.tgz#d291c6a4e97989b5c61d9acf396ae4fe133a718d" + integrity sha512-NuHqBY1PB/D8xU6s/thBgOAiAP7HOYDQ32+BFZILJ8ivkUkAHQnWfn6WhL79Owj1qmUnoN/YPhktdIoucipkAQ== + +JSONStream@^0.10.0: + version "0.10.0" + resolved "https://registry.yarnpkg.com/JSONStream/-/JSONStream-0.10.0.tgz#74349d0d89522b71f30f0a03ff9bd20ca6f12ac0" + dependencies: + jsonparse "0.0.5" + through ">=2.2.7 <3" + +JSONStream@^1.0.3: + version "1.3.5" + resolved "https://registry.yarnpkg.com/JSONStream/-/JSONStream-1.3.5.tgz#3208c1f08d3a4d99261ab64f92302bc15e111ca0" + dependencies: + jsonparse "^1.2.0" + through ">=2.2.7 <3" + +abbrev@1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/abbrev/-/abbrev-1.1.1.tgz#f8f2c887ad10bf67f634f005b6987fed3179aac8" + +accepts@~1.3.4, accepts@~1.3.5, accepts@~1.3.7: + version "1.3.7" + resolved "https://registry.yarnpkg.com/accepts/-/accepts-1.3.7.tgz#531bc726517a3b2b41f850021c6cc15eaab507cd" + dependencies: + mime-types "~2.1.24" + negotiator "0.6.2" + +acorn-jsx@^5.2.0, acorn-jsx@^5.3.1: + version "5.3.1" + resolved "https://registry.yarnpkg.com/acorn-jsx/-/acorn-jsx-5.3.1.tgz#fc8661e11b7ac1539c47dbfea2e72b3af34d267b" + integrity sha512-K0Ptm/47OKfQRpNQ2J/oIN/3QYiK6FwW+eJbILhsdxh2WTLdl+30o8aGdTbm5JbffpFFAg/g+zi1E+jvJha5ng== + +acorn-node@^1.2.0, acorn-node@^1.3.0, acorn-node@^1.5.2, acorn-node@^1.6.1: + version "1.8.2" + resolved "https://registry.yarnpkg.com/acorn-node/-/acorn-node-1.8.2.tgz#114c95d64539e53dede23de8b9d96df7c7ae2af8" + dependencies: + acorn "^7.0.0" + acorn-walk "^7.0.0" + xtend "^4.0.2" + +acorn-walk@^7.0.0: + version "7.0.0" + resolved "https://registry.yarnpkg.com/acorn-walk/-/acorn-walk-7.0.0.tgz#c8ba6f0f1aac4b0a9e32d1f0af12be769528f36b" + +acorn@^5.2.1, acorn@^6.4.1, acorn@^7.0.0, acorn@^7.1.1, acorn@^7.4.0: + version "7.4.1" + resolved "https://registry.yarnpkg.com/acorn/-/acorn-7.4.1.tgz#feaed255973d2e77555b83dbc08851a6c63520fa" + integrity sha512-nQyp0o1/mNdbTO1PO6kHkwSrmgZ0MT/jCCpNiwbUjGoRN4dlBhqJtoQuCnEOKzgTVwg0ZWiCoQy6SxMebQVh8A== + +add-dom-event-listener@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/add-dom-event-listener/-/add-dom-event-listener-1.1.0.tgz#6a92db3a0dd0abc254e095c0f1dc14acbbaae310" + dependencies: + object-assign "4.x" + +aggregate-error@^3.0.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/aggregate-error/-/aggregate-error-3.1.0.tgz#92670ff50f5359bdb7a3e0d40d0ec30c5737687a" + integrity sha512-4I7Td01quW/RpocfNayFdFVk1qSuoh0E7JrbRJ16nH01HhKFQ88INq9Sd+nd72zqRySlr9BmDA8xlEJ6vJMrYA== + dependencies: + clean-stack "^2.0.0" + indent-string "^4.0.0" + +ajv-errors@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/ajv-errors/-/ajv-errors-1.0.1.tgz#f35986aceb91afadec4102fbd85014950cefa64d" + integrity sha512-DCRfO/4nQ+89p/RK43i8Ezd41EqdGIU4ld7nGF8OQ14oc/we5rEntLCUa7+jrn3nn83BosfwZA0wb4pon2o8iQ== + +ajv-keywords@^3.1.0: + version "3.4.1" + resolved "https://registry.yarnpkg.com/ajv-keywords/-/ajv-keywords-3.4.1.tgz#ef916e271c64ac12171fd8384eaae6b2345854da" + +ajv-keywords@^3.4.1, ajv-keywords@^3.5.2: + version "3.5.2" + resolved "https://registry.yarnpkg.com/ajv-keywords/-/ajv-keywords-3.5.2.tgz#31f29da5ab6e00d1c2d329acf7b5929614d5014d" + integrity sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ== + +ajv@^6.1.0, ajv@^6.5.5: + version "6.10.2" + resolved "https://registry.yarnpkg.com/ajv/-/ajv-6.10.2.tgz#d3cea04d6b017b2894ad69040fec8b623eb4bd52" + dependencies: + fast-deep-equal "^2.0.1" + fast-json-stable-stringify "^2.0.0" + json-schema-traverse "^0.4.1" + uri-js "^4.2.2" + +ajv@^6.10.0, ajv@^6.10.2, ajv@^6.12.4: + version "6.12.5" + resolved "https://registry.yarnpkg.com/ajv/-/ajv-6.12.5.tgz#19b0e8bae8f476e5ba666300387775fb1a00a4da" + integrity sha512-lRF8RORchjpKG50/WFf8xmg7sgCLFiYNNnqdKflk63whMQcWR5ngGjiSXkL9bjxy6B2npOK2HSMN49jEBMSkag== + dependencies: + fast-deep-equal "^3.1.1" + fast-json-stable-stringify "^2.0.0" + json-schema-traverse "^0.4.1" + uri-js "^4.2.2" + +ajv@^7.0.2: + version "7.0.3" + resolved "https://registry.yarnpkg.com/ajv/-/ajv-7.0.3.tgz#13ae747eff125cafb230ac504b2406cf371eece2" + integrity sha512-R50QRlXSxqXcQP5SvKUrw8VZeypvo12i2IX0EeR5PiZ7bEKeHWgzgo264LDadUsCU42lTJVhFikTqJwNeH34gQ== + dependencies: + fast-deep-equal "^3.1.1" + json-schema-traverse "^1.0.0" + require-from-string "^2.0.2" + uri-js "^4.2.2" + +alphanum-sort@^1.0.0: + version "1.0.2" + resolved "https://registry.yarnpkg.com/alphanum-sort/-/alphanum-sort-1.0.2.tgz#97a1119649b211ad33691d9f9f486a8ec9fbe0a3" + +amdefine@>=0.0.4: + version "1.0.1" + resolved "https://registry.yarnpkg.com/amdefine/-/amdefine-1.0.1.tgz#4a5282ac164729e93619bcfd3ad151f817ce91f5" + +ansi-colors@^3.0.0: + version "3.2.4" + resolved "https://registry.yarnpkg.com/ansi-colors/-/ansi-colors-3.2.4.tgz#e3a3da4bfbae6c86a9c285625de124a234026fbf" + integrity sha512-hHUXGagefjN2iRrID63xckIvotOXOojhQKWIPUZ4mNUZ9nLZW+7FMNoE1lOkEhNWYsx/7ysGIuJYCiMAA9FnrA== + +ansi-colors@^4.1.1: + version "4.1.1" + resolved "https://registry.yarnpkg.com/ansi-colors/-/ansi-colors-4.1.1.tgz#cbb9ae256bf750af1eab344f229aa27fe94ba348" + integrity sha512-JoX0apGbHaUJBNl6yF+p6JAFYZ666/hhCGKN5t9QFjbJQKUU/g8MNbFDbvfrgKXvI1QpZplPOnwIo99lX/AAmA== + +ansi-html@0.0.7: + version "0.0.7" + resolved "https://registry.yarnpkg.com/ansi-html/-/ansi-html-0.0.7.tgz#813584021962a9e9e6fd039f940d12f56ca7859e" + +ansi-regex@^2.0.0: + version "2.1.1" + resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-2.1.1.tgz#c3b33ab5ee360d86e0e628f0468ae7ef27d654df" + +ansi-regex@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-3.0.0.tgz#ed0317c322064f79466c02966bddb605ab37d998" + +ansi-regex@^4.1.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-4.1.0.tgz#8b9f8f08cf1acb843756a839ca8c7e3168c51997" + integrity sha512-1apePfXM1UOSqw0o9IiFAovVz9M5S1Dg+4TrDwfMewQ6p/rmMueb7tWZjQ1rx4Loy1ArBggoqGpfqqdI4rondg== + +ansi-regex@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-5.0.0.tgz#388539f55179bf39339c81af30a654d69f87cb75" + integrity sha512-bY6fj56OUQ0hU1KjFNDQuJFezqKdrAyFdIevADiqrWHwSlbmBNMHp5ak2f40Pm8JTFyM2mqxkG6ngkHO11f/lg== + +ansi-styles@^2.2.1: + version "2.2.1" + resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-2.2.1.tgz#b432dd3358b634cf75e1e4664368240533c1ddbe" + +ansi-styles@^3.2.0, ansi-styles@^3.2.1: + version "3.2.1" + resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-3.2.1.tgz#41fbb20243e50b12be0f04b8dedbf07520ce841d" + dependencies: + color-convert "^1.9.0" + +ansi-styles@^4.0.0, ansi-styles@^4.1.0: + version "4.3.0" + resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-4.3.0.tgz#edd803628ae71c04c85ae7a0906edad34b648937" + integrity sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg== + dependencies: + color-convert "^2.0.1" + +anymatch@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/anymatch/-/anymatch-2.0.0.tgz#bcb24b4f37934d9aa7ac17b4adaf89e7c76ef2eb" + dependencies: + micromatch "^3.1.4" + normalize-path "^2.1.1" + +anymatch@~3.1.1: + version "3.1.1" + resolved "https://registry.yarnpkg.com/anymatch/-/anymatch-3.1.1.tgz#c55ecf02185e2469259399310c173ce31233b142" + integrity sha512-mM8522psRCqzV+6LhomX5wgp25YVibjh8Wj23I5RPkPppSVSjyKD2A2mBJmWGa+KN7f2D6LNh9jkBCeyLktzjg== + dependencies: + normalize-path "^3.0.0" + picomatch "^2.0.4" + +aproba@^1.0.3, aproba@^1.1.1: + version "1.2.0" + resolved "https://registry.yarnpkg.com/aproba/-/aproba-1.2.0.tgz#6802e6264efd18c790a1b0d517f0f2627bf2c94a" + +are-we-there-yet@~1.1.2: + version "1.1.5" + resolved "https://registry.yarnpkg.com/are-we-there-yet/-/are-we-there-yet-1.1.5.tgz#4b35c2944f062a8bfcda66410760350fe9ddfc21" + dependencies: + delegates "^1.0.0" + readable-stream "^2.0.6" + +argparse@^1.0.7: + version "1.0.10" + resolved "https://registry.yarnpkg.com/argparse/-/argparse-1.0.10.tgz#bcd6791ea5ae09725e17e5ad988134cd40b3d911" + dependencies: + sprintf-js "~1.0.2" + +aria-query@^4.2.2: + version "4.2.2" + resolved "https://registry.yarnpkg.com/aria-query/-/aria-query-4.2.2.tgz#0d2ca6c9aceb56b8977e9fed6aed7e15bbd2f83b" + integrity sha512-o/HelwhuKpTj/frsOsbNLNgnNGVIFsVP/SW2BSF14gVl7kAfMOJ6/8wUAUvG1R1NHKrfG+2sHZTu0yauT1qBrA== + dependencies: + "@babel/runtime" "^7.10.2" + "@babel/runtime-corejs3" "^7.10.2" + +arr-diff@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/arr-diff/-/arr-diff-4.0.0.tgz#d6461074febfec71e7e15235761a329a5dc7c520" + +arr-flatten@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/arr-flatten/-/arr-flatten-1.1.0.tgz#36048bbff4e7b47e136644316c99669ea5ae91f1" + +arr-union@^3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/arr-union/-/arr-union-3.1.0.tgz#e39b09aea9def866a8f206e288af63919bae39c4" + +array-find-index@^1.0.1: + version "1.0.2" + resolved "https://registry.yarnpkg.com/array-find-index/-/array-find-index-1.0.2.tgz#df010aa1287e164bbda6f9723b0a96a1ec4187a1" + +array-flatten@1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/array-flatten/-/array-flatten-1.1.1.tgz#9a5f699051b1e7073328f2a008968b64ea2955d2" + +array-flatten@^2.1.0: + version "2.1.2" + resolved "https://registry.yarnpkg.com/array-flatten/-/array-flatten-2.1.2.tgz#24ef80a28c1a893617e2149b0c6d0d788293b099" + +array-includes@^3.1.1: + version "3.1.1" + resolved "https://registry.yarnpkg.com/array-includes/-/array-includes-3.1.1.tgz#cdd67e6852bdf9c1215460786732255ed2459348" + integrity sha512-c2VXaCHl7zPsvpkFsw4nxvFie4fh1ur9bpcgsVkIjqn0H/Xwdg+7fv3n2r/isyS8EBj5b06M9kHyZuIr4El6WQ== + dependencies: + define-properties "^1.1.3" + es-abstract "^1.17.0" + is-string "^1.0.5" + +array-includes@^3.1.2: + version "3.1.2" + resolved "https://registry.yarnpkg.com/array-includes/-/array-includes-3.1.2.tgz#a8db03e0b88c8c6aeddc49cb132f9bcab4ebf9c8" + integrity sha512-w2GspexNQpx+PutG3QpT437/BenZBj0M/MZGn5mzv/MofYqo0xmRHzn4lFsoDlWJ+THYsGJmFlW68WlDFx7VRw== + dependencies: + call-bind "^1.0.0" + define-properties "^1.1.3" + es-abstract "^1.18.0-next.1" + get-intrinsic "^1.0.1" + is-string "^1.0.5" + +array-union@^1.0.1: + version "1.0.2" + resolved "https://registry.yarnpkg.com/array-union/-/array-union-1.0.2.tgz#9a34410e4f4e3da23dea375be5be70f24778ec39" + dependencies: + array-uniq "^1.0.1" + +array-uniq@^1.0.1: + version "1.0.3" + resolved "https://registry.yarnpkg.com/array-uniq/-/array-uniq-1.0.3.tgz#af6ac877a25cc7f74e058894753858dfdb24fdb6" + +array-unique@^0.3.2: + version "0.3.2" + resolved "https://registry.yarnpkg.com/array-unique/-/array-unique-0.3.2.tgz#a894b75d4bc4f6cd679ef3244a9fd8f46ae2d428" + +array.prototype.flat@^1.2.3: + version "1.2.3" + resolved "https://registry.yarnpkg.com/array.prototype.flat/-/array.prototype.flat-1.2.3.tgz#0de82b426b0318dbfdb940089e38b043d37f6c7b" + integrity sha512-gBlRZV0VSmfPIeWfuuy56XZMvbVfbEUnOXUvt3F/eUUUSyzlgLxhEX4YAEpxNAogRGehPSnfXyPtYyKAhkzQhQ== + dependencies: + define-properties "^1.1.3" + es-abstract "^1.17.0-next.1" + +array.prototype.flatmap@^1.2.3: + version "1.2.3" + resolved "https://registry.yarnpkg.com/array.prototype.flatmap/-/array.prototype.flatmap-1.2.3.tgz#1c13f84a178566042dd63de4414440db9222e443" + integrity sha512-OOEk+lkePcg+ODXIpvuU9PAryCikCJyo7GlDG1upleEpQRx6mzL9puEBkozQ5iAx20KV0l3DbyQwqciJtqe5Pg== + dependencies: + define-properties "^1.1.3" + es-abstract "^1.17.0-next.1" + function-bind "^1.1.1" + +asn1.js@^4.0.0: + version "4.10.1" + resolved "https://registry.yarnpkg.com/asn1.js/-/asn1.js-4.10.1.tgz#b9c2bf5805f1e64aadeed6df3a2bfafb5a73f5a0" + dependencies: + bn.js "^4.0.0" + inherits "^2.0.1" + minimalistic-assert "^1.0.0" + +asn1@~0.2.3: + version "0.2.4" + resolved "https://registry.yarnpkg.com/asn1/-/asn1-0.2.4.tgz#8d2475dfab553bb33e77b54e59e880bb8ce23136" + dependencies: + safer-buffer "~2.1.0" + +assert-plus@1.0.0, assert-plus@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/assert-plus/-/assert-plus-1.0.0.tgz#f12e0f3c5d77b0b1cdd9146942e4e96c1e4dd525" + +assert@^1.1.1, assert@^1.4.0: + version "1.5.0" + resolved "https://registry.yarnpkg.com/assert/-/assert-1.5.0.tgz#55c109aaf6e0aefdb3dc4b71240c70bf574b18eb" + dependencies: + object-assign "^4.1.1" + util "0.10.3" + +assign-symbols@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/assign-symbols/-/assign-symbols-1.0.0.tgz#59667f41fadd4f20ccbc2bb96b8d4f7f78ec0367" + +ast-types-flow@^0.0.7: + version "0.0.7" + resolved "https://registry.yarnpkg.com/ast-types-flow/-/ast-types-flow-0.0.7.tgz#f70b735c6bca1a5c9c22d982c3e39e7feba3bdad" + integrity sha1-9wtzXGvKGlycItmCw+Oef+ujva0= + +ast-types@0.9.6: + version "0.9.6" + resolved "https://registry.yarnpkg.com/ast-types/-/ast-types-0.9.6.tgz#102c9e9e9005d3e7e3829bf0c4fa24ee862ee9b9" + +astral-regex@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/astral-regex/-/astral-regex-2.0.0.tgz#483143c567aeed4785759c0865786dc77d7d2e31" + integrity sha512-Z7tMw1ytTXt5jqMcOP+OQteU1VuNK9Y02uuJtKQ1Sv69jXQKKg5cibLwGJow8yzZP+eAc18EmLGPal0bp36rvQ== + +async-each@^1.0.1: + version "1.0.3" + resolved "https://registry.yarnpkg.com/async-each/-/async-each-1.0.3.tgz#b727dbf87d7651602f06f4d4ac387f47d91b0cbf" + +async-foreach@^0.1.3: + version "0.1.3" + resolved "https://registry.yarnpkg.com/async-foreach/-/async-foreach-0.1.3.tgz#36121f845c0578172de419a97dbeb1d16ec34542" + +async-limiter@~1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/async-limiter/-/async-limiter-1.0.1.tgz#dd379e94f0db8310b08291f9d64c3209766617fd" + integrity sha512-csOlWGAcRFJaI6m+F2WKdnMKr4HhdhFVBk0H/QbJFMCr+uO2kwohwXQPxw/9OCxp05r5ghVBFSyioixx3gfkNQ== + +async@^1.5.2: + version "1.5.2" + resolved "https://registry.yarnpkg.com/async/-/async-1.5.2.tgz#ec6a61ae56480c0c3cb241c95618e20892f9672a" + +async@^2.6.1, async@^2.6.2: + version "2.6.3" + resolved "https://registry.yarnpkg.com/async/-/async-2.6.3.tgz#d72625e2344a3656e3a3ad4fa749fa83299d82ff" + dependencies: + lodash "^4.17.14" + +asynckit@^0.4.0: + version "0.4.0" + resolved "https://registry.yarnpkg.com/asynckit/-/asynckit-0.4.0.tgz#c79ed97f7f34cb8f2ba1bc9790bcc366474b4b79" + +atob@^2.1.1: + version "2.1.2" + resolved "https://registry.yarnpkg.com/atob/-/atob-2.1.2.tgz#6d9517eb9e030d2436666651e86bd9f6f13533c9" + +autoprefixer@^9.6.1: + version "9.8.6" + resolved "https://registry.yarnpkg.com/autoprefixer/-/autoprefixer-9.8.6.tgz#3b73594ca1bf9266320c5acf1588d74dea74210f" + integrity sha512-XrvP4VVHdRBCdX1S3WXVD8+RyG9qeb1D5Sn1DeLiG2xfSpzellk5k54xbUERJ3M5DggQxes39UGOTP8CFrEGbg== + dependencies: + browserslist "^4.12.0" + caniuse-lite "^1.0.30001109" + colorette "^1.2.1" + normalize-range "^0.1.2" + num2fraction "^1.2.2" + postcss "^7.0.32" + postcss-value-parser "^4.1.0" + +aws-sign2@~0.7.0: + version "0.7.0" + resolved "https://registry.yarnpkg.com/aws-sign2/-/aws-sign2-0.7.0.tgz#b46e890934a9591f2d2f6f86d7e6a9f1b3fe76a8" + +aws4@^1.8.0: + version "1.8.0" + resolved "https://registry.yarnpkg.com/aws4/-/aws4-1.8.0.tgz#f0e003d9ca9e7f59c7a508945d7b2ef9a04a542f" + +axe-core@^4.0.2: + version "4.1.1" + resolved "https://registry.yarnpkg.com/axe-core/-/axe-core-4.1.1.tgz#70a7855888e287f7add66002211a423937063eaf" + integrity sha512-5Kgy8Cz6LPC9DJcNb3yjAXTu3XihQgEdnIg50c//zOC/MyLP0Clg+Y8Sh9ZjjnvBrDZU4DgXS9C3T9r4/scGZQ== + +axobject-query@^2.2.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/axobject-query/-/axobject-query-2.2.0.tgz#943d47e10c0b704aa42275e20edf3722648989be" + integrity sha512-Td525n+iPOOyUQIeBfcASuG6uJsDOITl7Mds5gFyerkWiX7qhUTdYUBlSgNMyVqtSJqwpt1kXGLdUt6SykLMRA== + +babel-helper-builder-react-jsx@^6.24.1: + version "6.26.0" + resolved "https://registry.yarnpkg.com/babel-helper-builder-react-jsx/-/babel-helper-builder-react-jsx-6.26.0.tgz#39ff8313b75c8b65dceff1f31d383e0ff2a408a0" + dependencies: + babel-runtime "^6.26.0" + babel-types "^6.26.0" + esutils "^2.0.2" + +babel-loader@^8.1.0: + version "8.1.0" + resolved "https://registry.yarnpkg.com/babel-loader/-/babel-loader-8.1.0.tgz#c611d5112bd5209abe8b9fa84c3e4da25275f1c3" + integrity sha512-7q7nC1tYOrqvUrN3LQK4GwSk/TQorZSOlO9C+RZDZpODgyN4ZlCqE5q9cDsyWOliN+aU9B4JX01xK9eJXowJLw== + dependencies: + find-cache-dir "^2.1.0" + loader-utils "^1.4.0" + mkdirp "^0.5.3" + pify "^4.0.1" + schema-utils "^2.6.5" + +babel-plugin-dynamic-import-node@^2.3.3: + version "2.3.3" + resolved "https://registry.yarnpkg.com/babel-plugin-dynamic-import-node/-/babel-plugin-dynamic-import-node-2.3.3.tgz#84fda19c976ec5c6defef57f9427b3def66e17a3" + integrity sha512-jZVI+s9Zg3IqA/kdi0i6UDCybUI3aSBLnglhYbSSjKlV7yF1F/5LWv8MakQmvYpnbJDS6fcBL2KzHSxNCMtWSQ== + dependencies: + object.assign "^4.1.0" + +babel-plugin-macros@^2.8.0: + version "2.8.0" + resolved "https://registry.yarnpkg.com/babel-plugin-macros/-/babel-plugin-macros-2.8.0.tgz#0f958a7cc6556b1e65344465d99111a1e5e10138" + integrity sha512-SEP5kJpfGYqYKpBrj5XU3ahw5p5GOHJ0U5ssOSQ/WBVdwkD2Dzlce95exQTs3jOVWPPKLBN2rlEWkCK7dSmLvg== + dependencies: + "@babel/runtime" "^7.7.2" + cosmiconfig "^6.0.0" + resolve "^1.12.0" + +babel-plugin-syntax-flow@^6.18.0: + version "6.18.0" + resolved "https://registry.yarnpkg.com/babel-plugin-syntax-flow/-/babel-plugin-syntax-flow-6.18.0.tgz#4c3ab20a2af26aa20cd25995c398c4eb70310c8d" + +babel-plugin-syntax-jsx@^6.3.13, babel-plugin-syntax-jsx@^6.8.0: + version "6.18.0" + resolved "https://registry.yarnpkg.com/babel-plugin-syntax-jsx/-/babel-plugin-syntax-jsx-6.18.0.tgz#0af32a9a6e13ca7a3fd5069e62d7b0f58d0d8946" + +babel-plugin-transform-flow-strip-types@^6.22.0: + version "6.22.0" + resolved "https://registry.yarnpkg.com/babel-plugin-transform-flow-strip-types/-/babel-plugin-transform-flow-strip-types-6.22.0.tgz#84cb672935d43714fdc32bce84568d87441cf7cf" + dependencies: + babel-plugin-syntax-flow "^6.18.0" + babel-runtime "^6.22.0" + +babel-plugin-transform-react-display-name@^6.23.0: + version "6.25.0" + resolved "https://registry.yarnpkg.com/babel-plugin-transform-react-display-name/-/babel-plugin-transform-react-display-name-6.25.0.tgz#67e2bf1f1e9c93ab08db96792e05392bf2cc28d1" + dependencies: + babel-runtime "^6.22.0" + +babel-plugin-transform-react-jsx-self@^6.22.0: + version "6.22.0" + resolved "https://registry.yarnpkg.com/babel-plugin-transform-react-jsx-self/-/babel-plugin-transform-react-jsx-self-6.22.0.tgz#df6d80a9da2612a121e6ddd7558bcbecf06e636e" + dependencies: + babel-plugin-syntax-jsx "^6.8.0" + babel-runtime "^6.22.0" + +babel-plugin-transform-react-jsx-source@^6.22.0: + version "6.22.0" + resolved "https://registry.yarnpkg.com/babel-plugin-transform-react-jsx-source/-/babel-plugin-transform-react-jsx-source-6.22.0.tgz#66ac12153f5cd2d17b3c19268f4bf0197f44ecd6" + dependencies: + babel-plugin-syntax-jsx "^6.8.0" + babel-runtime "^6.22.0" + +babel-plugin-transform-react-jsx@^6.24.1: + version "6.24.1" + resolved "https://registry.yarnpkg.com/babel-plugin-transform-react-jsx/-/babel-plugin-transform-react-jsx-6.24.1.tgz#840a028e7df460dfc3a2d29f0c0d91f6376e66a3" + dependencies: + babel-helper-builder-react-jsx "^6.24.1" + babel-plugin-syntax-jsx "^6.8.0" + babel-runtime "^6.22.0" + +babel-preset-flow@^6.23.0: + version "6.23.0" + resolved "https://registry.yarnpkg.com/babel-preset-flow/-/babel-preset-flow-6.23.0.tgz#e71218887085ae9a24b5be4169affb599816c49d" + dependencies: + babel-plugin-transform-flow-strip-types "^6.22.0" + +babel-preset-react@^6.24.1: + version "6.24.1" + resolved "https://registry.yarnpkg.com/babel-preset-react/-/babel-preset-react-6.24.1.tgz#ba69dfaea45fc3ec639b6a4ecea6e17702c91380" + dependencies: + babel-plugin-syntax-jsx "^6.3.13" + babel-plugin-transform-react-display-name "^6.23.0" + babel-plugin-transform-react-jsx "^6.24.1" + babel-plugin-transform-react-jsx-self "^6.22.0" + babel-plugin-transform-react-jsx-source "^6.22.0" + babel-preset-flow "^6.23.0" + +babel-runtime@6.x, babel-runtime@^6.22.0, babel-runtime@^6.26.0: + version "6.26.0" + resolved "https://registry.yarnpkg.com/babel-runtime/-/babel-runtime-6.26.0.tgz#965c7058668e82b55d7bfe04ff2337bc8b5647fe" + dependencies: + core-js "^2.4.0" + regenerator-runtime "^0.11.0" + +babel-types@^6.26.0: + version "6.26.0" + resolved "https://registry.yarnpkg.com/babel-types/-/babel-types-6.26.0.tgz#a3b073f94ab49eb6fa55cd65227a334380632497" + dependencies: + babel-runtime "^6.26.0" + esutils "^2.0.2" + lodash "^4.17.4" + to-fast-properties "^1.0.3" + +balanced-match@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.0.tgz#89b4d199ab2bee49de164ea02b89ce462d71b767" + +base62@0.1.1: + version "0.1.1" + resolved "https://registry.yarnpkg.com/base62/-/base62-0.1.1.tgz#7b4174c2f94449753b11c2651c083da841a7b084" + +base64-js@^1.0.2: + version "1.3.1" + resolved "https://registry.yarnpkg.com/base64-js/-/base64-js-1.3.1.tgz#58ece8cb75dd07e71ed08c736abc5fac4dbf8df1" + +base@^0.11.1: + version "0.11.2" + resolved "https://registry.yarnpkg.com/base/-/base-0.11.2.tgz#7bde5ced145b6d551a90db87f83c558b4eb48a8f" + dependencies: + cache-base "^1.0.1" + class-utils "^0.3.5" + component-emitter "^1.2.1" + define-property "^1.0.0" + isobject "^3.0.1" + mixin-deep "^1.2.0" + pascalcase "^0.1.1" + +batch-processor@1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/batch-processor/-/batch-processor-1.0.0.tgz#75c95c32b748e0850d10c2b168f6bdbe9891ace8" + integrity sha1-dclcMrdI4IUNEMKxaPa9vpiRrOg= + +batch@0.6.1: + version "0.6.1" + resolved "https://registry.yarnpkg.com/batch/-/batch-0.6.1.tgz#dc34314f4e679318093fc760272525f94bf25c16" + +bcrypt-pbkdf@^1.0.0: + version "1.0.2" + resolved "https://registry.yarnpkg.com/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.2.tgz#a4301d389b6a43f9b67ff3ca11a3f6637e360e9e" + dependencies: + tweetnacl "^0.14.3" + +big.js@^5.2.2: + version "5.2.2" + resolved "https://registry.yarnpkg.com/big.js/-/big.js-5.2.2.tgz#65f0af382f578bcdc742bd9c281e9cb2d7768328" + +binary-extensions@^1.0.0: + version "1.13.1" + resolved "https://registry.yarnpkg.com/binary-extensions/-/binary-extensions-1.13.1.tgz#598afe54755b2868a5330d2aff9d4ebb53209b65" + +binary-extensions@^2.0.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/binary-extensions/-/binary-extensions-2.1.0.tgz#30fa40c9e7fe07dbc895678cd287024dea241dd9" + integrity sha512-1Yj8h9Q+QDF5FzhMs/c9+6UntbD5MkRfRwac8DoEm9ZfUBZ7tZ55YcGVAzEe4bXsdQHEk+s9S5wsOKVdZrw0tQ== + +block-stream@*: + version "0.0.9" + resolved "https://registry.yarnpkg.com/block-stream/-/block-stream-0.0.9.tgz#13ebfe778a03205cfe03751481ebb4b3300c126a" + dependencies: + inherits "~2.0.0" + +bluebird@^3.5.5: + version "3.7.2" + resolved "https://registry.yarnpkg.com/bluebird/-/bluebird-3.7.2.tgz#9f229c15be272454ffa973ace0dbee79a1b0c36f" + integrity sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg== + +bn.js@^4.0.0, bn.js@^4.1.0, bn.js@^4.1.1, bn.js@^4.4.0: + version "4.11.8" + resolved "https://registry.yarnpkg.com/bn.js/-/bn.js-4.11.8.tgz#2cde09eb5ee341f484746bb0309b3253b1b1442f" + +body-parser@1.19.0: + version "1.19.0" + resolved "https://registry.yarnpkg.com/body-parser/-/body-parser-1.19.0.tgz#96b2709e57c9c4e09a6fd66a8fd979844f69f08a" + dependencies: + bytes "3.1.0" + content-type "~1.0.4" + debug "2.6.9" + depd "~1.1.2" + http-errors "1.7.2" + iconv-lite "0.4.24" + on-finished "~2.3.0" + qs "6.7.0" + raw-body "2.4.0" + type-is "~1.6.17" + +bonjour@^3.5.0: + version "3.5.0" + resolved "https://registry.yarnpkg.com/bonjour/-/bonjour-3.5.0.tgz#8e890a183d8ee9a2393b3844c691a42bcf7bc9f5" + dependencies: + array-flatten "^2.1.0" + deep-equal "^1.0.1" + dns-equal "^1.0.0" + dns-txt "^2.0.2" + multicast-dns "^6.0.1" + multicast-dns-service-types "^1.1.0" + +boolbase@^1.0.0, boolbase@~1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/boolbase/-/boolbase-1.0.0.tgz#68dff5fbe60c51eb37725ea9e3ed310dcc1e776e" + +brace-expansion@^1.1.7: + version "1.1.11" + resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-1.1.11.tgz#3c7fcbf529d87226f3d2f52b966ff5271eb441dd" + dependencies: + balanced-match "^1.0.0" + concat-map "0.0.1" + +brace@^0.11.1: + version "0.11.1" + resolved "https://registry.yarnpkg.com/brace/-/brace-0.11.1.tgz#4896fcc9d544eef45f4bb7660db320d3b379fe58" + integrity sha1-SJb8ydVE7vRfS7dmDbMg07N5/lg= + +braces@^2.3.1, braces@^2.3.2: + version "2.3.2" + resolved "https://registry.yarnpkg.com/braces/-/braces-2.3.2.tgz#5979fd3f14cd531565e5fa2df1abfff1dfaee729" + dependencies: + arr-flatten "^1.1.0" + array-unique "^0.3.2" + extend-shallow "^2.0.1" + fill-range "^4.0.0" + isobject "^3.0.1" + repeat-element "^1.1.2" + snapdragon "^0.8.1" + snapdragon-node "^2.0.1" + split-string "^3.0.2" + to-regex "^3.0.1" + +braces@^3.0.1, braces@~3.0.2: + version "3.0.2" + resolved "https://registry.yarnpkg.com/braces/-/braces-3.0.2.tgz#3454e1a462ee8d599e236df336cd9ea4f8afe107" + integrity sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A== + dependencies: + fill-range "^7.0.1" + +brorand@^1.0.1: + version "1.1.0" + resolved "https://registry.yarnpkg.com/brorand/-/brorand-1.1.0.tgz#12c25efe40a45e3c323eb8675a0a0ce57b22371f" + +browser-pack@^6.0.1: + version "6.1.0" + resolved "https://registry.yarnpkg.com/browser-pack/-/browser-pack-6.1.0.tgz#c34ba10d0b9ce162b5af227c7131c92c2ecd5774" + dependencies: + JSONStream "^1.0.3" + combine-source-map "~0.8.0" + defined "^1.0.0" + safe-buffer "^5.1.1" + through2 "^2.0.0" + umd "^3.0.0" + +browser-resolve@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/browser-resolve/-/browser-resolve-2.0.0.tgz#99b7304cb392f8d73dba741bb2d7da28c6d7842b" + integrity sha512-7sWsQlYL2rGLy2IWm8WL8DCTJvYLc/qlOnsakDac87SOoCd16WLsaAMdCiAqsTNHIe+SXfaqyxyo6THoWqs8WQ== + dependencies: + resolve "^1.17.0" + +browserify-aes@^1.0.0, browserify-aes@^1.0.4: + version "1.2.0" + resolved "https://registry.yarnpkg.com/browserify-aes/-/browserify-aes-1.2.0.tgz#326734642f403dabc3003209853bb70ad428ef48" + dependencies: + buffer-xor "^1.0.3" + cipher-base "^1.0.0" + create-hash "^1.1.0" + evp_bytestokey "^1.0.3" + inherits "^2.0.1" + safe-buffer "^5.0.1" + +browserify-cache-api@^3.0.0: + version "3.0.1" + resolved "https://registry.yarnpkg.com/browserify-cache-api/-/browserify-cache-api-3.0.1.tgz#96247e853f068fd6e0d45cc73f0bb2cd9778ef02" + dependencies: + async "^1.5.2" + through2 "^2.0.0" + xtend "^4.0.0" + +browserify-cipher@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/browserify-cipher/-/browserify-cipher-1.0.1.tgz#8d6474c1b870bfdabcd3bcfcc1934a10e94f15f0" + dependencies: + browserify-aes "^1.0.4" + browserify-des "^1.0.0" + evp_bytestokey "^1.0.0" + +browserify-des@^1.0.0: + version "1.0.2" + resolved "https://registry.yarnpkg.com/browserify-des/-/browserify-des-1.0.2.tgz#3af4f1f59839403572f1c66204375f7a7f703e9c" + dependencies: + cipher-base "^1.0.1" + des.js "^1.0.0" + inherits "^2.0.1" + safe-buffer "^5.1.2" + +browserify-incremental@^3.1.1: + version "3.1.1" + resolved "https://registry.yarnpkg.com/browserify-incremental/-/browserify-incremental-3.1.1.tgz#0713cb7587247a632a9f08cf1bd169b878b62a8a" + dependencies: + JSONStream "^0.10.0" + browserify-cache-api "^3.0.0" + through2 "^2.0.0" + xtend "^4.0.0" + +browserify-rsa@^4.0.0: + version "4.0.1" + resolved "https://registry.yarnpkg.com/browserify-rsa/-/browserify-rsa-4.0.1.tgz#21e0abfaf6f2029cf2fafb133567a701d4135524" + dependencies: + bn.js "^4.1.0" + randombytes "^2.0.1" + +browserify-sign@^4.0.0: + version "4.0.4" + resolved "https://registry.yarnpkg.com/browserify-sign/-/browserify-sign-4.0.4.tgz#aa4eb68e5d7b658baa6bf6a57e630cbd7a93d298" + dependencies: + bn.js "^4.1.1" + browserify-rsa "^4.0.0" + create-hash "^1.1.0" + create-hmac "^1.1.2" + elliptic "^6.0.0" + inherits "^2.0.1" + parse-asn1 "^5.0.0" + +browserify-zlib@^0.2.0, browserify-zlib@~0.2.0: + version "0.2.0" + resolved "https://registry.yarnpkg.com/browserify-zlib/-/browserify-zlib-0.2.0.tgz#2869459d9aa3be245fe8fe2ca1f46e2e7f54d73f" + dependencies: + pako "~1.0.5" + +browserify@^16.5.2: + version "16.5.2" + resolved "https://registry.yarnpkg.com/browserify/-/browserify-16.5.2.tgz#d926835e9280fa5fd57f5bc301f2ef24a972ddfe" + integrity sha512-TkOR1cQGdmXU9zW4YukWzWVSJwrxmNdADFbqbE3HFgQWe5wqZmOawqZ7J/8MPCwk/W8yY7Y0h+7mOtcZxLP23g== + dependencies: + JSONStream "^1.0.3" + assert "^1.4.0" + browser-pack "^6.0.1" + browser-resolve "^2.0.0" + browserify-zlib "~0.2.0" + buffer "~5.2.1" + cached-path-relative "^1.0.0" + concat-stream "^1.6.0" + console-browserify "^1.1.0" + constants-browserify "~1.0.0" + crypto-browserify "^3.0.0" + defined "^1.0.0" + deps-sort "^2.0.0" + domain-browser "^1.2.0" + duplexer2 "~0.1.2" + events "^2.0.0" + glob "^7.1.0" + has "^1.0.0" + htmlescape "^1.1.0" + https-browserify "^1.0.0" + inherits "~2.0.1" + insert-module-globals "^7.0.0" + labeled-stream-splicer "^2.0.0" + mkdirp-classic "^0.5.2" + module-deps "^6.2.3" + os-browserify "~0.3.0" + parents "^1.0.1" + path-browserify "~0.0.0" + process "~0.11.0" + punycode "^1.3.2" + querystring-es3 "~0.2.0" + read-only-stream "^2.0.0" + readable-stream "^2.0.2" + resolve "^1.1.4" + shasum "^1.0.0" + shell-quote "^1.6.1" + stream-browserify "^2.0.0" + stream-http "^3.0.0" + string_decoder "^1.1.1" + subarg "^1.0.0" + syntax-error "^1.1.1" + through2 "^2.0.0" + timers-browserify "^1.0.1" + tty-browserify "0.0.1" + url "~0.11.0" + util "~0.10.1" + vm-browserify "^1.0.0" + xtend "^4.0.0" + +browserslist@^4.0.0: + version "4.6.6" + resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-4.6.6.tgz#6e4bf467cde520bc9dbdf3747dafa03531cec453" + dependencies: + caniuse-lite "^1.0.30000984" + electron-to-chromium "^1.3.191" + node-releases "^1.1.25" + +browserslist@^4.12.0, browserslist@^4.6.4, browserslist@^4.8.5: + version "4.14.5" + resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-4.14.5.tgz#1c751461a102ddc60e40993639b709be7f2c4015" + integrity sha512-Z+vsCZIvCBvqLoYkBFTwEYH3v5MCQbsAjp50ERycpOjnPmolg1Gjy4+KaWWpm8QOJt9GHkhdqAl14NpCX73CWA== + dependencies: + caniuse-lite "^1.0.30001135" + electron-to-chromium "^1.3.571" + escalade "^3.1.0" + node-releases "^1.1.61" + +buffer-from@^1.0.0: + version "1.1.1" + resolved "https://registry.yarnpkg.com/buffer-from/-/buffer-from-1.1.1.tgz#32713bc028f75c02fdb710d7c7bcec1f2c6070ef" + +buffer-indexof@^1.0.0: + version "1.1.1" + resolved "https://registry.yarnpkg.com/buffer-indexof/-/buffer-indexof-1.1.1.tgz#52fabcc6a606d1a00302802648ef68f639da268c" + +buffer-xor@^1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/buffer-xor/-/buffer-xor-1.0.3.tgz#26e61ed1422fb70dd42e6e36729ed51d855fe8d9" + +buffer@^4.3.0: + version "4.9.1" + resolved "https://registry.yarnpkg.com/buffer/-/buffer-4.9.1.tgz#6d1bb601b07a4efced97094132093027c95bc298" + dependencies: + base64-js "^1.0.2" + ieee754 "^1.1.4" + isarray "^1.0.0" + +buffer@~5.2.1: + version "5.2.1" + resolved "https://registry.yarnpkg.com/buffer/-/buffer-5.2.1.tgz#dd57fa0f109ac59c602479044dca7b8b3d0b71d6" + integrity sha512-c+Ko0loDaFfuPWiL02ls9Xd3GO3cPVmUobQ6t3rXNUk304u6hGq+8N/kFi+QEIKhzK3uwolVhLzszmfLmMLnqg== + dependencies: + base64-js "^1.0.2" + ieee754 "^1.1.4" + +builtin-status-codes@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/builtin-status-codes/-/builtin-status-codes-3.0.0.tgz#85982878e21b98e1c66425e03d0174788f569ee8" + +bytes@3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/bytes/-/bytes-3.0.0.tgz#d32815404d689699f85a4ea4fa8755dd13a96048" + +bytes@3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/bytes/-/bytes-3.1.0.tgz#f6cf7933a360e0588fa9fde85651cdc7f805d1f6" + +cacache@^12.0.2: + version "12.0.4" + resolved "https://registry.yarnpkg.com/cacache/-/cacache-12.0.4.tgz#668bcbd105aeb5f1d92fe25570ec9525c8faa40c" + integrity sha512-a0tMB40oefvuInr4Cwb3GerbL9xTj1D5yg0T5xrjGCGyfvbxseIXX7BAO/u/hIXdafzOI5JC3wDwHyf24buOAQ== + dependencies: + bluebird "^3.5.5" + chownr "^1.1.1" + figgy-pudding "^3.5.1" + glob "^7.1.4" + graceful-fs "^4.1.15" + infer-owner "^1.0.3" + lru-cache "^5.1.1" + mississippi "^3.0.0" + mkdirp "^0.5.1" + move-concurrently "^1.0.1" + promise-inflight "^1.0.1" + rimraf "^2.6.3" + ssri "^6.0.1" + unique-filename "^1.1.1" + y18n "^4.0.0" + +cacache@^15.0.5: + version "15.0.5" + resolved "https://registry.yarnpkg.com/cacache/-/cacache-15.0.5.tgz#69162833da29170d6732334643c60e005f5f17d0" + integrity sha512-lloiL22n7sOjEEXdL8NAjTgv9a1u43xICE9/203qonkZUCj5X1UEWIdf2/Y0d6QcCtMzbKQyhrcDbdvlZTs/+A== + dependencies: + "@npmcli/move-file" "^1.0.1" + chownr "^2.0.0" + fs-minipass "^2.0.0" + glob "^7.1.4" + infer-owner "^1.0.4" + lru-cache "^6.0.0" + minipass "^3.1.1" + minipass-collect "^1.0.2" + minipass-flush "^1.0.5" + minipass-pipeline "^1.2.2" + mkdirp "^1.0.3" + p-map "^4.0.0" + promise-inflight "^1.0.1" + rimraf "^3.0.2" + ssri "^8.0.0" + tar "^6.0.2" + unique-filename "^1.1.1" + +cache-base@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/cache-base/-/cache-base-1.0.1.tgz#0a7f46416831c8b662ee36fe4e7c59d76f666ab2" + dependencies: + collection-visit "^1.0.0" + component-emitter "^1.2.1" + get-value "^2.0.6" + has-value "^1.0.0" + isobject "^3.0.1" + set-value "^2.0.0" + to-object-path "^0.3.0" + union-value "^1.0.0" + unset-value "^1.0.0" + +cached-path-relative@^1.0.0, cached-path-relative@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/cached-path-relative/-/cached-path-relative-1.0.2.tgz#a13df4196d26776220cc3356eb147a52dba2c6db" + +call-bind@^1.0.0: + version "1.0.2" + resolved "https://registry.yarnpkg.com/call-bind/-/call-bind-1.0.2.tgz#b1d4e89e688119c3c9a903ad30abb2f6a919be3c" + integrity sha512-7O+FbCihrB5WGbFYesctwmTKae6rOiIzmz1icreWJ+0aA7LJfuqhEso2T9ncpcFtzMQtzXf2QGGueWJGTYsqrA== + dependencies: + function-bind "^1.1.1" + get-intrinsic "^1.0.2" + +caller-callsite@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/caller-callsite/-/caller-callsite-2.0.0.tgz#847e0fce0a223750a9a027c54b33731ad3154134" + dependencies: + callsites "^2.0.0" + +caller-path@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/caller-path/-/caller-path-2.0.0.tgz#468f83044e369ab2010fac5f06ceee15bb2cb1f4" + dependencies: + caller-callsite "^2.0.0" + +callsites@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/callsites/-/callsites-2.0.0.tgz#06eb84f00eea413da86affefacbffb36093b3c50" + +callsites@^3.0.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/callsites/-/callsites-3.1.0.tgz#b3630abd8943432f54b3f0519238e33cd7df2f73" + integrity sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ== + +camelcase-keys@^2.0.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/camelcase-keys/-/camelcase-keys-2.1.0.tgz#308beeaffdf28119051efa1d932213c91b8f92e7" + dependencies: + camelcase "^2.0.0" + map-obj "^1.0.0" + +camelcase@^2.0.0: + version "2.1.1" + resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-2.1.1.tgz#7c1d16d679a1bbe59ca02cacecfb011e201f5a1f" + +camelcase@^5.0.0, camelcase@^5.3.1: + version "5.3.1" + resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-5.3.1.tgz#e3c9b31569e106811df242f715725a1f4c494320" + integrity sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg== + +caniuse-api@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/caniuse-api/-/caniuse-api-3.0.0.tgz#5e4d90e2274961d46291997df599e3ed008ee4c0" + dependencies: + browserslist "^4.0.0" + caniuse-lite "^1.0.0" + lodash.memoize "^4.1.2" + lodash.uniq "^4.5.0" + +caniuse-lite@^1.0.0, caniuse-lite@^1.0.30000984: + version "1.0.30000989" + resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30000989.tgz#b9193e293ccf7e4426c5245134b8f2a56c0ac4b9" + +caniuse-lite@^1.0.30000981, caniuse-lite@^1.0.30001109, caniuse-lite@^1.0.30001135: + version "1.0.30001144" + resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001144.tgz#bca0fffde12f97e1127a351fec3bfc1971aa3b3d" + integrity sha512-4GQTEWNMnVZVOFG3BK0xvGeaDAtiPAbG2N8yuMXuXzx/c2Vd4XoMPO8+E918zeXn5IF0FRVtGShBfkfQea2wHQ== + +case-sensitive-paths-webpack-plugin@^2.3.0: + version "2.3.0" + resolved "https://registry.yarnpkg.com/case-sensitive-paths-webpack-plugin/-/case-sensitive-paths-webpack-plugin-2.3.0.tgz#23ac613cc9a856e4f88ff8bb73bbb5e989825cf7" + integrity sha512-/4YgnZS8y1UXXmC02xD5rRrBEu6T5ub+mQHLNRj0fzTRbgdBYhsNo2V5EqwgqrExjxsjtF/OpAKAMkKsxbD5XQ== + +caseless@~0.12.0: + version "0.12.0" + resolved "https://registry.yarnpkg.com/caseless/-/caseless-0.12.0.tgz#1b681c21ff84033c826543090689420d187151dc" + +chain-function@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/chain-function/-/chain-function-1.0.1.tgz#c63045e5b4b663fb86f1c6e186adaf1de402a1cc" + +chalk@^1.1.1: + version "1.1.3" + resolved "https://registry.yarnpkg.com/chalk/-/chalk-1.1.3.tgz#a8115c55e4a702fe4d150abd3872822a7e09fc98" + dependencies: + ansi-styles "^2.2.1" + escape-string-regexp "^1.0.2" + has-ansi "^2.0.0" + strip-ansi "^3.0.0" + supports-color "^2.0.0" + +chalk@^2.0, chalk@^2.0.0, chalk@^2.4.1, chalk@^2.4.2: + version "2.4.2" + resolved "https://registry.yarnpkg.com/chalk/-/chalk-2.4.2.tgz#cd42541677a54333cf541a49108c1432b44c9424" + dependencies: + ansi-styles "^3.2.1" + escape-string-regexp "^1.0.5" + supports-color "^5.3.0" + +chalk@^4.0.0, chalk@^4.1.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/chalk/-/chalk-4.1.0.tgz#4e14870a618d9e2edd97dd8345fd9d9dc315646a" + integrity sha512-qwx12AxXe2Q5xQ43Ac//I6v5aXTipYrSESdOgzrN+9XjgEpyjpKuvSGaN4qE93f7TQTlerQQ8S+EQ0EyDoVL1A== + dependencies: + ansi-styles "^4.1.0" + supports-color "^7.1.0" + +chokidar@^2.1.8: + version "2.1.8" + resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-2.1.8.tgz#804b3a7b6a99358c3c5c61e71d8728f041cff917" + integrity sha512-ZmZUazfOzf0Nve7duiCKD23PFSCs4JPoYyccjUFF3aQkQadqBhfzhjkwBH2mNOG9cTBwhamM37EIsIkZw3nRgg== + dependencies: + anymatch "^2.0.0" + async-each "^1.0.1" + braces "^2.3.2" + glob-parent "^3.1.0" + inherits "^2.0.3" + is-binary-path "^1.0.0" + is-glob "^4.0.0" + normalize-path "^3.0.0" + path-is-absolute "^1.0.0" + readdirp "^2.2.1" + upath "^1.1.1" + optionalDependencies: + fsevents "^1.2.7" + +chokidar@^3.4.1: + version "3.4.2" + resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-3.4.2.tgz#38dc8e658dec3809741eb3ef7bb0a47fe424232d" + integrity sha512-IZHaDeBeI+sZJRX7lGcXsdzgvZqKv6sECqsbErJA4mHWfpRrD8B97kSFN4cQz6nGBGiuFia1MKR4d6c1o8Cv7A== + dependencies: + anymatch "~3.1.1" + braces "~3.0.2" + glob-parent "~5.1.0" + is-binary-path "~2.1.0" + is-glob "~4.0.1" + normalize-path "~3.0.0" + readdirp "~3.4.0" + optionalDependencies: + fsevents "~2.1.2" + +chownr@^1.1.1: + version "1.1.2" + resolved "https://registry.yarnpkg.com/chownr/-/chownr-1.1.2.tgz#a18f1e0b269c8a6a5d3c86eb298beb14c3dd7bf6" + +chownr@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/chownr/-/chownr-2.0.0.tgz#15bfbe53d2eab4cf70f18a8cd68ebe5b3cb1dece" + integrity sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ== + +chrome-trace-event@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/chrome-trace-event/-/chrome-trace-event-1.0.2.tgz#234090ee97c7d4ad1a2c4beae27505deffc608a4" + integrity sha512-9e/zx1jw7B4CO+c/RXoCsfg/x1AfUBioy4owYH0bJprEYAx5hRFLRhWBqHAG57D0ZM4H7vxbP7bPe0VwhQRYDQ== + dependencies: + tslib "^1.9.0" + +cipher-base@^1.0.0, cipher-base@^1.0.1, cipher-base@^1.0.3: + version "1.0.4" + resolved "https://registry.yarnpkg.com/cipher-base/-/cipher-base-1.0.4.tgz#8760e4ecc272f4c363532f926d874aae2c1397de" + dependencies: + inherits "^2.0.1" + safe-buffer "^5.0.1" + +class-utils@^0.3.5: + version "0.3.6" + resolved "https://registry.yarnpkg.com/class-utils/-/class-utils-0.3.6.tgz#f93369ae8b9a7ce02fd41faad0ca83033190c463" + dependencies: + arr-union "^3.1.0" + define-property "^0.2.5" + isobject "^3.0.0" + static-extend "^0.1.1" + +classnames@^2.2.5, classnames@^2.2.6: + version "2.2.6" + resolved "https://registry.yarnpkg.com/classnames/-/classnames-2.2.6.tgz#43935bffdd291f326dad0a205309b38d00f650ce" + +clean-stack@^2.0.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/clean-stack/-/clean-stack-2.2.0.tgz#ee8472dbb129e727b31e8a10a427dee9dfe4008b" + integrity sha512-4diC9HaTE+KRAMWhDhrGOECgWZxoevMc5TlkObMqNSsVU62PYzXZ/SMTjzyGAFF1YusgxGcSWTEXBhp0CPwQ1A== + +cliui@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/cliui/-/cliui-5.0.0.tgz#deefcfdb2e800784aa34f46fa08e06851c7bbbc5" + integrity sha512-PYeGSEmmHM6zvoef2w8TPzlrnNpXIjTipYK780YswmIP9vjxmd6Y2a3CB2Ks6/AU8NHjZugXvo8w3oWM2qnwXA== + dependencies: + string-width "^3.1.0" + strip-ansi "^5.2.0" + wrap-ansi "^5.1.0" + +clone-deep@^4.0.1: + version "4.0.1" + resolved "https://registry.yarnpkg.com/clone-deep/-/clone-deep-4.0.1.tgz#c19fd9bdbbf85942b4fd979c84dcf7d5f07c2387" + integrity sha512-neHB9xuzh/wk0dIHweyAXv2aPGZIVk3pLMe+/RNzINf17fe0OG96QroktYAUm7SM1PBnzTabaLboqqxDyMU+SQ== + dependencies: + is-plain-object "^2.0.4" + kind-of "^6.0.2" + shallow-clone "^3.0.0" + +coa@^2.0.2: + version "2.0.2" + resolved "https://registry.yarnpkg.com/coa/-/coa-2.0.2.tgz#43f6c21151b4ef2bf57187db0d73de229e3e7ec3" + dependencies: + "@types/q" "^1.5.1" + chalk "^2.4.1" + q "^1.1.2" + +code-point-at@^1.0.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/code-point-at/-/code-point-at-1.1.0.tgz#0d070b4d043a5bea33a2f1a40e2edb3d9a4ccf77" + +collection-visit@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/collection-visit/-/collection-visit-1.0.0.tgz#4bc0373c164bc3291b4d368c829cf1a80a59dca0" + dependencies: + map-visit "^1.0.0" + object-visit "^1.0.0" + +color-convert@^1.9.0, color-convert@^1.9.1: + version "1.9.3" + resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-1.9.3.tgz#bb71850690e1f136567de629d2d5471deda4c1e8" + dependencies: + color-name "1.1.3" + +color-convert@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-2.0.1.tgz#72d3a68d598c9bdb3af2ad1e84f21d896abd4de3" + integrity sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ== + dependencies: + color-name "~1.1.4" + +color-name@1.1.3: + version "1.1.3" + resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.3.tgz#a7d0558bd89c42f795dd42328f740831ca53bc25" + +color-name@^1.0.0, color-name@~1.1.4: + version "1.1.4" + resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.4.tgz#c2a09a87acbde69543de6f63fa3995c826c536a2" + +color-string@^1.5.2: + version "1.5.3" + resolved "https://registry.yarnpkg.com/color-string/-/color-string-1.5.3.tgz#c9bbc5f01b58b5492f3d6857459cb6590ce204cc" + dependencies: + color-name "^1.0.0" + simple-swizzle "^0.2.2" + +color@^3.0.0: + version "3.1.2" + resolved "https://registry.yarnpkg.com/color/-/color-3.1.2.tgz#68148e7f85d41ad7649c5fa8c8106f098d229e10" + dependencies: + color-convert "^1.9.1" + color-string "^1.5.2" + +colorette@^1.2.1: + version "1.2.1" + resolved "https://registry.yarnpkg.com/colorette/-/colorette-1.2.1.tgz#4d0b921325c14faf92633086a536db6e89564b1b" + integrity sha512-puCDz0CzydiSYOrnXpz/PKd69zRrribezjtE9yd4zvytoRc8+RY/KJPvtPFKZS3E3wP6neGyMe0vOTlHO5L3Pw== + +combine-source-map@^0.8.0, combine-source-map@~0.8.0: + version "0.8.0" + resolved "https://registry.yarnpkg.com/combine-source-map/-/combine-source-map-0.8.0.tgz#a58d0df042c186fcf822a8e8015f5450d2d79a8b" + dependencies: + convert-source-map "~1.1.0" + inline-source-map "~0.6.0" + lodash.memoize "~3.0.3" + source-map "~0.5.3" + +combined-stream@^1.0.6, combined-stream@^1.0.8, combined-stream@~1.0.6: + version "1.0.8" + resolved "https://registry.yarnpkg.com/combined-stream/-/combined-stream-1.0.8.tgz#c3d45a8b34fd730631a110a8a2520682b31d5a7f" + dependencies: + delayed-stream "~1.0.0" + +commander@^2.18.0, commander@^2.5.0: + version "2.20.0" + resolved "https://registry.yarnpkg.com/commander/-/commander-2.20.0.tgz#d58bb2b5c1ee8f87b0d340027e9e94e222c5a422" + +commander@^2.20.0: + version "2.20.3" + resolved "https://registry.yarnpkg.com/commander/-/commander-2.20.3.tgz#fd485e84c03eb4881c20722ba48035e8531aeb33" + integrity sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ== + +commondir@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/commondir/-/commondir-1.0.1.tgz#ddd800da0c66127393cca5950ea968a3aaf1253b" + +commoner@^0.10.0: + version "0.10.8" + resolved "https://registry.yarnpkg.com/commoner/-/commoner-0.10.8.tgz#34fc3672cd24393e8bb47e70caa0293811f4f2c5" + dependencies: + commander "^2.5.0" + detective "^4.3.1" + glob "^5.0.15" + graceful-fs "^4.1.2" + iconv-lite "^0.4.5" + mkdirp "^0.5.0" + private "^0.1.6" + q "^1.1.2" + recast "^0.11.17" + +component-classes@^1.2.5: + version "1.2.6" + resolved "https://registry.yarnpkg.com/component-classes/-/component-classes-1.2.6.tgz#c642394c3618a4d8b0b8919efccbbd930e5cd691" + dependencies: + component-indexof "0.0.3" + +component-emitter@^1.2.1: + version "1.3.0" + resolved "https://registry.yarnpkg.com/component-emitter/-/component-emitter-1.3.0.tgz#16e4070fba8ae29b679f2215853ee181ab2eabc0" + +component-indexof@0.0.3: + version "0.0.3" + resolved "https://registry.yarnpkg.com/component-indexof/-/component-indexof-0.0.3.tgz#11d091312239eb8f32c8f25ae9cb002ffe8d3c24" + +compressible@~2.0.16: + version "2.0.17" + resolved "https://registry.yarnpkg.com/compressible/-/compressible-2.0.17.tgz#6e8c108a16ad58384a977f3a482ca20bff2f38c1" + dependencies: + mime-db ">= 1.40.0 < 2" + +compression-webpack-plugin@^4.0.0: + version "4.0.1" + resolved "https://registry.yarnpkg.com/compression-webpack-plugin/-/compression-webpack-plugin-4.0.1.tgz#33eda97f1170dd38c5556771de10f34245aa0274" + integrity sha512-0mg6PgwTsUe5LEcUrOu3ob32vraDx2VdbMGAT1PARcOV+UJWDYZFdkSo6RbHoGQ061mmmkC7XpRKOlvwm/gzJQ== + dependencies: + cacache "^15.0.5" + find-cache-dir "^3.3.1" + schema-utils "^2.7.0" + serialize-javascript "^4.0.0" + webpack-sources "^1.4.3" + +compression@^1.7.4: + version "1.7.4" + resolved "https://registry.yarnpkg.com/compression/-/compression-1.7.4.tgz#95523eff170ca57c29a0ca41e6fe131f41e5bb8f" + integrity sha512-jaSIDzP9pZVS4ZfQ+TzvtiWhdpFhE2RDHz8QJkpX9SIpLq88VueF5jJw6t+6CUQcAoA6t+x89MLrWAqpfDE8iQ== + dependencies: + accepts "~1.3.5" + bytes "3.0.0" + compressible "~2.0.16" + debug "2.6.9" + on-headers "~1.0.2" + safe-buffer "5.1.2" + vary "~1.1.2" + +concat-map@0.0.1: + version "0.0.1" + resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b" + +concat-stream@^1.5.0, concat-stream@^1.6.0, concat-stream@^1.6.1, concat-stream@~1.6.0: + version "1.6.2" + resolved "https://registry.yarnpkg.com/concat-stream/-/concat-stream-1.6.2.tgz#904bdf194cd3122fc675c77fc4ac3d4ff0fd1a34" + dependencies: + buffer-from "^1.0.0" + inherits "^2.0.3" + readable-stream "^2.2.2" + typedarray "^0.0.6" + +confusing-browser-globals@^1.0.10: + version "1.0.10" + resolved "https://registry.yarnpkg.com/confusing-browser-globals/-/confusing-browser-globals-1.0.10.tgz#30d1e7f3d1b882b25ec4933d1d1adac353d20a59" + integrity sha512-gNld/3lySHwuhaVluJUKLePYirM3QNCKzVxqAdhJII9/WXKVX5PURzMVJspS1jTslSqjeuG4KMVTSouit5YPHA== + +connect-history-api-fallback@^1.6.0: + version "1.6.0" + resolved "https://registry.yarnpkg.com/connect-history-api-fallback/-/connect-history-api-fallback-1.6.0.tgz#8b32089359308d111115d81cad3fceab888f97bc" + integrity sha512-e54B99q/OUoH64zYYRf3HBP5z24G38h5D3qXu23JGRoigpX5Ss4r9ZnDk3g0Z8uQC2x2lPaJ+UlWBc1ZWBWdLg== + +console-browserify@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/console-browserify/-/console-browserify-1.1.0.tgz#f0241c45730a9fc6323b206dbf38edc741d0bb10" + dependencies: + date-now "^0.1.4" + +console-control-strings@^1.0.0, console-control-strings@~1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/console-control-strings/-/console-control-strings-1.1.0.tgz#3d7cf4464db6446ea644bf4b39507f9851008e8e" + +constants-browserify@^1.0.0, constants-browserify@~1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/constants-browserify/-/constants-browserify-1.0.0.tgz#c20b96d8c617748aaf1c16021760cd27fcb8cb75" + +contains-path@^0.1.0: + version "0.1.0" + resolved "https://registry.yarnpkg.com/contains-path/-/contains-path-0.1.0.tgz#fe8cf184ff6670b6baef01a9d4861a5cbec4120a" + +content-disposition@0.5.3: + version "0.5.3" + resolved "https://registry.yarnpkg.com/content-disposition/-/content-disposition-0.5.3.tgz#e130caf7e7279087c5616c2007d0485698984fbd" + dependencies: + safe-buffer "5.1.2" + +content-type@~1.0.4: + version "1.0.4" + resolved "https://registry.yarnpkg.com/content-type/-/content-type-1.0.4.tgz#e138cc75e040c727b1966fe5e5f8c9aee256fe3b" + +convert-source-map@^1.7.0: + version "1.7.0" + resolved "https://registry.yarnpkg.com/convert-source-map/-/convert-source-map-1.7.0.tgz#17a2cb882d7f77d3490585e2ce6c524424a3a442" + integrity sha512-4FJkXzKXEDB1snCFZlLP4gpC3JILicCpGbzG9f9G7tGqGCzETQ2hWPrcinA9oU4wtf2biUaEH5065UnMeR33oA== + dependencies: + safe-buffer "~5.1.1" + +convert-source-map@~1.1.0: + version "1.1.3" + resolved "https://registry.yarnpkg.com/convert-source-map/-/convert-source-map-1.1.3.tgz#4829c877e9fe49b3161f3bf3673888e204699860" + +cookie-signature@1.0.6: + version "1.0.6" + resolved "https://registry.yarnpkg.com/cookie-signature/-/cookie-signature-1.0.6.tgz#e303a882b342cc3ee8ca513a79999734dab3ae2c" + +cookie@0.4.0: + version "0.4.0" + resolved "https://registry.yarnpkg.com/cookie/-/cookie-0.4.0.tgz#beb437e7022b3b6d49019d088665303ebe9c14ba" + +copy-concurrently@^1.0.0: + version "1.0.5" + resolved "https://registry.yarnpkg.com/copy-concurrently/-/copy-concurrently-1.0.5.tgz#92297398cae34937fcafd6ec8139c18051f0b5e0" + dependencies: + aproba "^1.1.1" + fs-write-stream-atomic "^1.0.8" + iferr "^0.1.5" + mkdirp "^0.5.1" + rimraf "^2.5.4" + run-queue "^1.0.0" + +copy-descriptor@^0.1.0: + version "0.1.1" + resolved "https://registry.yarnpkg.com/copy-descriptor/-/copy-descriptor-0.1.1.tgz#676f6eb3c39997c2ee1ac3a924fd6124748f578d" + +copy-to-clipboard@^3: + version "3.2.0" + resolved "https://registry.yarnpkg.com/copy-to-clipboard/-/copy-to-clipboard-3.2.0.tgz#d2724a3ccbfed89706fac8a894872c979ac74467" + dependencies: + toggle-selection "^1.0.6" + +core-js-compat@^3.6.2: + version "3.6.5" + resolved "https://registry.yarnpkg.com/core-js-compat/-/core-js-compat-3.6.5.tgz#2a51d9a4e25dfd6e690251aa81f99e3c05481f1c" + integrity sha512-7ItTKOhOZbznhXAQ2g/slGg1PJV5zDO/WdkTwi7UEOJmkvsE32PWvx6mKtDjiMpjnR2CNf6BAD6sSxIlv7ptng== + dependencies: + browserslist "^4.8.5" + semver "7.0.0" + +core-js-pure@^3.0.0: + version "3.6.5" + resolved "https://registry.yarnpkg.com/core-js-pure/-/core-js-pure-3.6.5.tgz#c79e75f5e38dbc85a662d91eea52b8256d53b813" + integrity sha512-lacdXOimsiD0QyNf9BC/mxivNJ/ybBGJXQFKzRekp1WTHoVUWsUHEn+2T8GJAzzIhyOuXA+gOxCVN3l+5PLPUA== + +core-js@^2.4.0: + version "2.6.9" + resolved "https://registry.yarnpkg.com/core-js/-/core-js-2.6.9.tgz#6b4b214620c834152e179323727fc19741b084f2" + +core-js@^3.6.5: + version "3.6.5" + resolved "https://registry.yarnpkg.com/core-js/-/core-js-3.6.5.tgz#7395dc273af37fb2e50e9bd3d9fe841285231d1a" + integrity sha512-vZVEEwZoIsI+vPEuoF9Iqf5H7/M3eeQqWlQnYa8FSKKePuYTf5MWnxb5SDAzCa60b3JBRS5g9b+Dq7b1y/RCrA== + +core-util-is@1.0.2, core-util-is@~1.0.0: + version "1.0.2" + resolved "https://registry.yarnpkg.com/core-util-is/-/core-util-is-1.0.2.tgz#b5fd54220aa2bc5ab57aab7140c940754503c1a7" + +cosmiconfig@^5.0.0: + version "5.2.1" + resolved "https://registry.yarnpkg.com/cosmiconfig/-/cosmiconfig-5.2.1.tgz#040f726809c591e77a17c0a3626ca45b4f168b1a" + dependencies: + import-fresh "^2.0.0" + is-directory "^0.3.1" + js-yaml "^3.13.1" + parse-json "^4.0.0" + +cosmiconfig@^6.0.0: + version "6.0.0" + resolved "https://registry.yarnpkg.com/cosmiconfig/-/cosmiconfig-6.0.0.tgz#da4fee853c52f6b1e6935f41c1a2fc50bd4a9982" + integrity sha512-xb3ZL6+L8b9JLLCx3ZdoZy4+2ECphCMo2PwqgP1tlfVq6M6YReyzBJtvWWtbDSpNr9hn96pkCiZqUcFEc+54Qg== + dependencies: + "@types/parse-json" "^4.0.0" + import-fresh "^3.1.0" + parse-json "^5.0.0" + path-type "^4.0.0" + yaml "^1.7.2" + +create-ecdh@^4.0.0: + version "4.0.3" + resolved "https://registry.yarnpkg.com/create-ecdh/-/create-ecdh-4.0.3.tgz#c9111b6f33045c4697f144787f9254cdc77c45ff" + dependencies: + bn.js "^4.1.0" + elliptic "^6.0.0" + +create-hash@^1.1.0, create-hash@^1.1.2: + version "1.2.0" + resolved "https://registry.yarnpkg.com/create-hash/-/create-hash-1.2.0.tgz#889078af11a63756bcfb59bd221996be3a9ef196" + dependencies: + cipher-base "^1.0.1" + inherits "^2.0.1" + md5.js "^1.3.4" + ripemd160 "^2.0.1" + sha.js "^2.4.0" + +create-hmac@^1.1.0, create-hmac@^1.1.2, create-hmac@^1.1.4: + version "1.1.7" + resolved "https://registry.yarnpkg.com/create-hmac/-/create-hmac-1.1.7.tgz#69170c78b3ab957147b2b8b04572e47ead2243ff" + dependencies: + cipher-base "^1.0.3" + create-hash "^1.1.0" + inherits "^2.0.1" + ripemd160 "^2.0.0" + safe-buffer "^5.0.1" + sha.js "^2.4.8" + +create-react-context@^0.3.0: + version "0.3.0" + resolved "https://registry.yarnpkg.com/create-react-context/-/create-react-context-0.3.0.tgz#546dede9dc422def0d3fc2fe03afe0bc0f4f7d8c" + dependencies: + gud "^1.0.0" + warning "^4.0.3" + +cross-spawn@^3.0.0: + version "3.0.1" + resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-3.0.1.tgz#1256037ecb9f0c5f79e3d6ef135e30770184b982" + dependencies: + lru-cache "^4.0.1" + which "^1.2.9" + +cross-spawn@^6.0.0, cross-spawn@^6.0.5: + version "6.0.5" + resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-6.0.5.tgz#4a5ec7c64dfae22c3a14124dbacdee846d80cbc4" + integrity sha512-eTVLrBSt7fjbDygz805pMnstIs2VTBNkRm0qxZd+M7A5XDdxVRWO5MxGBXZhjY4cqLYLdtrGqRf8mBPmzwSpWQ== + dependencies: + nice-try "^1.0.4" + path-key "^2.0.1" + semver "^5.5.0" + shebang-command "^1.2.0" + which "^1.2.9" + +cross-spawn@^7.0.2: + version "7.0.3" + resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-7.0.3.tgz#f73a85b9d5d41d045551c177e2882d4ac85728a6" + integrity sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w== + dependencies: + path-key "^3.1.0" + shebang-command "^2.0.0" + which "^2.0.1" + +crypto-browserify@^3.0.0, crypto-browserify@^3.11.0: + version "3.12.0" + resolved "https://registry.yarnpkg.com/crypto-browserify/-/crypto-browserify-3.12.0.tgz#396cf9f3137f03e4b8e532c58f698254e00f80ec" + dependencies: + browserify-cipher "^1.0.0" + browserify-sign "^4.0.0" + create-ecdh "^4.0.0" + create-hash "^1.1.0" + create-hmac "^1.1.0" + diffie-hellman "^5.0.0" + inherits "^2.0.1" + pbkdf2 "^3.0.3" + public-encrypt "^4.0.0" + randombytes "^2.0.0" + randomfill "^1.0.3" + +css-animation@^1.3.2: + version "1.6.1" + resolved "https://registry.yarnpkg.com/css-animation/-/css-animation-1.6.1.tgz#162064a3b0d51f958b7ff37b3d6d4de18e17039e" + dependencies: + babel-runtime "6.x" + component-classes "^1.2.5" + +css-blank-pseudo@^0.1.4: + version "0.1.4" + resolved "https://registry.yarnpkg.com/css-blank-pseudo/-/css-blank-pseudo-0.1.4.tgz#dfdefd3254bf8a82027993674ccf35483bfcb3c5" + integrity sha512-LHz35Hr83dnFeipc7oqFDmsjHdljj3TQtxGGiNWSOsTLIAubSm4TEz8qCaKFpk7idaQ1GfWscF4E6mgpBysA1w== + dependencies: + postcss "^7.0.5" + +css-box-model@^1.2.0: + version "1.2.1" + resolved "https://registry.yarnpkg.com/css-box-model/-/css-box-model-1.2.1.tgz#59951d3b81fd6b2074a62d49444415b0d2b4d7c1" + integrity sha512-a7Vr4Q/kd/aw96bnJG332W9V9LkJO69JRcaCYDUqjp6/z0w6VcZjgAcTbgFxEPfBgdnAwlh3iwu+hLopa+flJw== + dependencies: + tiny-invariant "^1.0.6" + +css-color-names@0.0.4, css-color-names@^0.0.4: + version "0.0.4" + resolved "https://registry.yarnpkg.com/css-color-names/-/css-color-names-0.0.4.tgz#808adc2e79cf84738069b646cb20ec27beb629e0" + +css-declaration-sorter@^4.0.1: + version "4.0.1" + resolved "https://registry.yarnpkg.com/css-declaration-sorter/-/css-declaration-sorter-4.0.1.tgz#c198940f63a76d7e36c1e71018b001721054cb22" + dependencies: + postcss "^7.0.1" + timsort "^0.3.0" + +css-has-pseudo@^0.10.0: + version "0.10.0" + resolved "https://registry.yarnpkg.com/css-has-pseudo/-/css-has-pseudo-0.10.0.tgz#3c642ab34ca242c59c41a125df9105841f6966ee" + integrity sha512-Z8hnfsZu4o/kt+AuFzeGpLVhFOGO9mluyHBaA2bA8aCGTwah5sT3WV/fTHH8UNZUytOIImuGPrl/prlb4oX4qQ== + dependencies: + postcss "^7.0.6" + postcss-selector-parser "^5.0.0-rc.4" + +css-loader@^3.5.3: + version "3.6.0" + resolved "https://registry.yarnpkg.com/css-loader/-/css-loader-3.6.0.tgz#2e4b2c7e6e2d27f8c8f28f61bffcd2e6c91ef645" + integrity sha512-M5lSukoWi1If8dhQAUCvj4H8vUt3vOnwbQBH9DdTm/s4Ym2B/3dPMtYZeJmq7Q3S3Pa+I94DcZ7pc9bP14cWIQ== + dependencies: + camelcase "^5.3.1" + cssesc "^3.0.0" + icss-utils "^4.1.1" + loader-utils "^1.2.3" + normalize-path "^3.0.0" + postcss "^7.0.32" + postcss-modules-extract-imports "^2.0.0" + postcss-modules-local-by-default "^3.0.2" + postcss-modules-scope "^2.2.0" + postcss-modules-values "^3.0.0" + postcss-value-parser "^4.1.0" + schema-utils "^2.7.0" + semver "^6.3.0" + +css-prefers-color-scheme@^3.1.1: + version "3.1.1" + resolved "https://registry.yarnpkg.com/css-prefers-color-scheme/-/css-prefers-color-scheme-3.1.1.tgz#6f830a2714199d4f0d0d0bb8a27916ed65cff1f4" + integrity sha512-MTu6+tMs9S3EUqzmqLXEcgNRbNkkD/TGFvowpeoWJn5Vfq7FMgsmRQs9X5NXAURiOBmOxm/lLjsDNXDE6k9bhg== + dependencies: + postcss "^7.0.5" + +css-select-base-adapter@^0.1.1: + version "0.1.1" + resolved "https://registry.yarnpkg.com/css-select-base-adapter/-/css-select-base-adapter-0.1.1.tgz#3b2ff4972cc362ab88561507a95408a1432135d7" + +css-select@^2.0.0: + version "2.0.2" + resolved "https://registry.yarnpkg.com/css-select/-/css-select-2.0.2.tgz#ab4386cec9e1f668855564b17c3733b43b2a5ede" + dependencies: + boolbase "^1.0.0" + css-what "^2.1.2" + domutils "^1.7.0" + nth-check "^1.0.2" + +css-tree@1.0.0-alpha.29: + version "1.0.0-alpha.29" + resolved "https://registry.yarnpkg.com/css-tree/-/css-tree-1.0.0-alpha.29.tgz#3fa9d4ef3142cbd1c301e7664c1f352bd82f5a39" + dependencies: + mdn-data "~1.1.0" + source-map "^0.5.3" + +css-tree@1.0.0-alpha.33: + version "1.0.0-alpha.33" + resolved "https://registry.yarnpkg.com/css-tree/-/css-tree-1.0.0-alpha.33.tgz#970e20e5a91f7a378ddd0fc58d0b6c8d4f3be93e" + dependencies: + mdn-data "2.0.4" + source-map "^0.5.3" + +css-unit-converter@^1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/css-unit-converter/-/css-unit-converter-1.1.1.tgz#d9b9281adcfd8ced935bdbaba83786897f64e996" + +css-what@^2.1.2: + version "2.1.3" + resolved "https://registry.yarnpkg.com/css-what/-/css-what-2.1.3.tgz#a6d7604573365fe74686c3f311c56513d88285f2" + +cssdb@^4.4.0: + version "4.4.0" + resolved "https://registry.yarnpkg.com/cssdb/-/cssdb-4.4.0.tgz#3bf2f2a68c10f5c6a08abd92378331ee803cddb0" + integrity sha512-LsTAR1JPEM9TpGhl/0p3nQecC2LJ0kD8X5YARu1hk/9I1gril5vDtMZyNxcEpxxDj34YNck/ucjuoUd66K03oQ== + +cssesc@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/cssesc/-/cssesc-2.0.0.tgz#3b13bd1bb1cb36e1bcb5a4dcd27f54c5dcb35703" + +cssesc@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/cssesc/-/cssesc-3.0.0.tgz#37741919903b868565e1c09ea747445cd18983ee" + integrity sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg== + +cssnano-preset-default@^4.0.7: + version "4.0.7" + resolved "https://registry.yarnpkg.com/cssnano-preset-default/-/cssnano-preset-default-4.0.7.tgz#51ec662ccfca0f88b396dcd9679cdb931be17f76" + dependencies: + css-declaration-sorter "^4.0.1" + cssnano-util-raw-cache "^4.0.1" + postcss "^7.0.0" + postcss-calc "^7.0.1" + postcss-colormin "^4.0.3" + postcss-convert-values "^4.0.1" + postcss-discard-comments "^4.0.2" + postcss-discard-duplicates "^4.0.2" + postcss-discard-empty "^4.0.1" + postcss-discard-overridden "^4.0.1" + postcss-merge-longhand "^4.0.11" + postcss-merge-rules "^4.0.3" + postcss-minify-font-values "^4.0.2" + postcss-minify-gradients "^4.0.2" + postcss-minify-params "^4.0.2" + postcss-minify-selectors "^4.0.2" + postcss-normalize-charset "^4.0.1" + postcss-normalize-display-values "^4.0.2" + postcss-normalize-positions "^4.0.2" + postcss-normalize-repeat-style "^4.0.2" + postcss-normalize-string "^4.0.2" + postcss-normalize-timing-functions "^4.0.2" + postcss-normalize-unicode "^4.0.1" + postcss-normalize-url "^4.0.1" + postcss-normalize-whitespace "^4.0.2" + postcss-ordered-values "^4.1.2" + postcss-reduce-initial "^4.0.3" + postcss-reduce-transforms "^4.0.2" + postcss-svgo "^4.0.2" + postcss-unique-selectors "^4.0.1" + +cssnano-util-get-arguments@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/cssnano-util-get-arguments/-/cssnano-util-get-arguments-4.0.0.tgz#ed3a08299f21d75741b20f3b81f194ed49cc150f" + +cssnano-util-get-match@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/cssnano-util-get-match/-/cssnano-util-get-match-4.0.0.tgz#c0e4ca07f5386bb17ec5e52250b4f5961365156d" + +cssnano-util-raw-cache@^4.0.1: + version "4.0.1" + resolved "https://registry.yarnpkg.com/cssnano-util-raw-cache/-/cssnano-util-raw-cache-4.0.1.tgz#b26d5fd5f72a11dfe7a7846fb4c67260f96bf282" + dependencies: + postcss "^7.0.0" + +cssnano-util-same-parent@^4.0.0: + version "4.0.1" + resolved "https://registry.yarnpkg.com/cssnano-util-same-parent/-/cssnano-util-same-parent-4.0.1.tgz#574082fb2859d2db433855835d9a8456ea18bbf3" + +cssnano@^4.1.10: + version "4.1.10" + resolved "https://registry.yarnpkg.com/cssnano/-/cssnano-4.1.10.tgz#0ac41f0b13d13d465487e111b778d42da631b8b2" + dependencies: + cosmiconfig "^5.0.0" + cssnano-preset-default "^4.0.7" + is-resolvable "^1.0.0" + postcss "^7.0.0" + +csso@^3.5.1: + version "3.5.1" + resolved "https://registry.yarnpkg.com/csso/-/csso-3.5.1.tgz#7b9eb8be61628973c1b261e169d2f024008e758b" + dependencies: + css-tree "1.0.0-alpha.29" + +csstype@^2.2.0: + version "2.6.6" + resolved "https://registry.yarnpkg.com/csstype/-/csstype-2.6.6.tgz#c34f8226a94bbb10c32cc0d714afdf942291fc41" + +csstype@^3.0.2: + version "3.0.3" + resolved "https://registry.yarnpkg.com/csstype/-/csstype-3.0.3.tgz#2b410bbeba38ba9633353aff34b05d9755d065f8" + integrity sha512-jPl+wbWPOWJ7SXsWyqGRk3lGecbar0Cb0OvZF/r/ZU011R4YqiRehgkQ9p4eQfo9DSDLqLL3wHwfxeJiuIsNag== + +currently-unhandled@^0.4.1: + version "0.4.1" + resolved "https://registry.yarnpkg.com/currently-unhandled/-/currently-unhandled-0.4.1.tgz#988df33feab191ef799a61369dd76c17adf957ea" + dependencies: + array-find-index "^1.0.1" + +cyclist@~0.2.2: + version "0.2.2" + resolved "https://registry.yarnpkg.com/cyclist/-/cyclist-0.2.2.tgz#1b33792e11e914a2fd6d6ed6447464444e5fa640" + +damerau-levenshtein@^1.0.6: + version "1.0.6" + resolved "https://registry.yarnpkg.com/damerau-levenshtein/-/damerau-levenshtein-1.0.6.tgz#143c1641cb3d85c60c32329e26899adea8701791" + integrity sha512-JVrozIeElnj3QzfUIt8tB8YMluBJom4Vw9qTPpjGYQ9fYlB3D/rb6OordUxf3xeFB35LKWs0xqcO5U6ySvBtug== + +dash-ast@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/dash-ast/-/dash-ast-1.0.0.tgz#12029ba5fb2f8aa6f0a861795b23c1b4b6c27d37" + +dashdash@^1.12.0: + version "1.14.1" + resolved "https://registry.yarnpkg.com/dashdash/-/dashdash-1.14.1.tgz#853cfa0f7cbe2fed5de20326b8dd581035f6e2f0" + dependencies: + assert-plus "^1.0.0" + +date-fns@^2.0.1: + version "2.16.1" + resolved "https://registry.yarnpkg.com/date-fns/-/date-fns-2.16.1.tgz#05775792c3f3331da812af253e1a935851d3834b" + integrity sha512-sAJVKx/FqrLYHAQeN7VpJrPhagZc9R4ImZIWYRFZaaohR3KzmuK88touwsSwSVT8Qcbd4zoDsnGfX4GFB4imyQ== + +date-now@^0.1.4: + version "0.1.4" + resolved "https://registry.yarnpkg.com/date-now/-/date-now-0.1.4.tgz#eaf439fd4d4848ad74e5cc7dbef200672b9e345b" + +debug@2.6.9, debug@^2.2.0, debug@^2.3.3, debug@^2.6.9: + version "2.6.9" + resolved "https://registry.yarnpkg.com/debug/-/debug-2.6.9.tgz#5d128515df134ff327e90a4c93f4e077a536341f" + dependencies: + ms "2.0.0" + +debug@^3.1.1, debug@^3.2.6: + version "3.2.6" + resolved "https://registry.yarnpkg.com/debug/-/debug-3.2.6.tgz#e83d17de16d8a7efb7717edbe5fb10135eee629b" + dependencies: + ms "^2.1.1" + +debug@^4.0.1, debug@^4.1.0, debug@^4.1.1: + version "4.2.0" + resolved "https://registry.yarnpkg.com/debug/-/debug-4.2.0.tgz#7f150f93920e94c58f5574c2fd01a3110effe7f1" + integrity sha512-IX2ncY78vDTjZMFUdmsvIRFY2Cf4FnD0wRs+nQwJU8Lu99/tPFdb0VybiiMTPe3I6rQmwsqQqRBvxU+bZ/I8sg== + dependencies: + ms "2.1.2" + +decamelize@^1.1.2, decamelize@^1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/decamelize/-/decamelize-1.2.0.tgz#f6534d15148269b20352e7bee26f501f9a191290" + +decode-uri-component@^0.2.0: + version "0.2.0" + resolved "https://registry.yarnpkg.com/decode-uri-component/-/decode-uri-component-0.2.0.tgz#eb3913333458775cb84cd1a1fae062106bb87545" + +deep-equal@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/deep-equal/-/deep-equal-1.0.1.tgz#f5d260292b660e084eff4cdbc9f08ad3247448b5" + +deep-equal@^1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/deep-equal/-/deep-equal-1.1.1.tgz#b5c98c942ceffaf7cb051e24e1434a25a2e6076a" + integrity sha512-yd9c5AdiqVcR+JjcwUQb9DkhJc8ngNr0MahEBGvDiJw8puWab2yZlh+nkasOnZP+EGTAP6rRp2JzJhJZzvNF8g== + dependencies: + is-arguments "^1.0.4" + is-date-object "^1.0.1" + is-regex "^1.0.4" + object-is "^1.0.1" + object-keys "^1.1.1" + regexp.prototype.flags "^1.2.0" + +deep-extend@^0.6.0: + version "0.6.0" + resolved "https://registry.yarnpkg.com/deep-extend/-/deep-extend-0.6.0.tgz#c4fa7c95404a17a9c3e8ca7e1537312b736330ac" + +deep-is@^0.1.3: + version "0.1.3" + resolved "https://registry.yarnpkg.com/deep-is/-/deep-is-0.1.3.tgz#b369d6fb5dbc13eecf524f91b070feedc357cf34" + integrity sha1-s2nW+128E+7PUk+RsHD+7cNXzzQ= + +default-gateway@^4.2.0: + version "4.2.0" + resolved "https://registry.yarnpkg.com/default-gateway/-/default-gateway-4.2.0.tgz#167104c7500c2115f6dd69b0a536bb8ed720552b" + integrity sha512-h6sMrVB1VMWVrW13mSc6ia/DwYYw5MN6+exNu1OaJeFac5aSAvwM7lZ0NVfTABuSkQelr4h5oebg3KB1XPdjgA== + dependencies: + execa "^1.0.0" + ip-regex "^2.1.0" + +define-properties@^1.1.2, define-properties@^1.1.3: + version "1.1.3" + resolved "https://registry.yarnpkg.com/define-properties/-/define-properties-1.1.3.tgz#cf88da6cbee26fe6db7094f61d870cbd84cee9f1" + dependencies: + object-keys "^1.0.12" + +define-property@^0.2.5: + version "0.2.5" + resolved "https://registry.yarnpkg.com/define-property/-/define-property-0.2.5.tgz#c35b1ef918ec3c990f9a5bc57be04aacec5c8116" + dependencies: + is-descriptor "^0.1.0" + +define-property@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/define-property/-/define-property-1.0.0.tgz#769ebaaf3f4a63aad3af9e8d304c9bbe79bfb0e6" + dependencies: + is-descriptor "^1.0.0" + +define-property@^2.0.2: + version "2.0.2" + resolved "https://registry.yarnpkg.com/define-property/-/define-property-2.0.2.tgz#d459689e8d654ba77e02a817f8710d702cb16e9d" + dependencies: + is-descriptor "^1.0.2" + isobject "^3.0.1" + +defined@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/defined/-/defined-1.0.0.tgz#c98d9bcef75674188e110969151199e39b1fa693" + +del@^4.1.1: + version "4.1.1" + resolved "https://registry.yarnpkg.com/del/-/del-4.1.1.tgz#9e8f117222ea44a31ff3a156c049b99052a9f0b4" + integrity sha512-QwGuEUouP2kVwQenAsOof5Fv8K9t3D8Ca8NxcXKrIpEHjTXK5J2nXLdP+ALI1cgv8wj7KuwBhTwBkOZSJKM5XQ== + dependencies: + "@types/glob" "^7.1.1" + globby "^6.1.0" + is-path-cwd "^2.0.0" + is-path-in-cwd "^2.0.0" + p-map "^2.0.0" + pify "^4.0.1" + rimraf "^2.6.3" + +delayed-stream@~1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/delayed-stream/-/delayed-stream-1.0.0.tgz#df3ae199acadfb7d440aaae0b29e2272b24ec619" + +delegates@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/delegates/-/delegates-1.0.0.tgz#84c6e159b81904fdca59a0ef44cd870d31250f9a" + +depd@~1.1.2: + version "1.1.2" + resolved "https://registry.yarnpkg.com/depd/-/depd-1.1.2.tgz#9bcd52e14c097763e749b274c4346ed2e560b5a9" + +deps-sort@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/deps-sort/-/deps-sort-2.0.0.tgz#091724902e84658260eb910748cccd1af6e21fb5" + dependencies: + JSONStream "^1.0.3" + shasum "^1.0.0" + subarg "^1.0.0" + through2 "^2.0.0" + +des.js@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/des.js/-/des.js-1.0.0.tgz#c074d2e2aa6a8a9a07dbd61f9a15c2cd83ec8ecc" + dependencies: + inherits "^2.0.1" + minimalistic-assert "^1.0.0" + +destroy@~1.0.4: + version "1.0.4" + resolved "https://registry.yarnpkg.com/destroy/-/destroy-1.0.4.tgz#978857442c44749e4206613e37946205826abd80" + +detect-file@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/detect-file/-/detect-file-1.0.0.tgz#f0d66d03672a825cb1b73bdb3fe62310c8e552b7" + integrity sha1-8NZtA2cqglyxtzvbP+YjEMjlUrc= + +detect-libc@^1.0.2: + version "1.0.3" + resolved "https://registry.yarnpkg.com/detect-libc/-/detect-libc-1.0.3.tgz#fa137c4bd698edf55cd5cd02ac559f91a4c4ba9b" + +detect-node@^2.0.4: + version "2.0.4" + resolved "https://registry.yarnpkg.com/detect-node/-/detect-node-2.0.4.tgz#014ee8f8f669c5c58023da64b8179c083a28c46c" + integrity sha512-ZIzRpLJrOj7jjP2miAtgqIfmzbxa4ZOr5jJc601zklsfEx9oTzmmj2nVpIPRpNlRTIh8lc1kyViIY7BWSGNmKw== + +detective@^4.3.1: + version "4.7.1" + resolved "https://registry.yarnpkg.com/detective/-/detective-4.7.1.tgz#0eca7314338442febb6d65da54c10bb1c82b246e" + dependencies: + acorn "^5.2.1" + defined "^1.0.0" + +detective@^5.2.0: + version "5.2.0" + resolved "https://registry.yarnpkg.com/detective/-/detective-5.2.0.tgz#feb2a77e85b904ecdea459ad897cc90a99bd2a7b" + integrity sha512-6SsIx+nUUbuK0EthKjv0zrdnajCCXVYGmbYYiYjFVpzcjwEs/JMDZ8tPRG29J/HhN56t3GJp2cGSWDRjjot8Pg== + dependencies: + acorn-node "^1.6.1" + defined "^1.0.0" + minimist "^1.1.1" + +diffie-hellman@^5.0.0: + version "5.0.3" + resolved "https://registry.yarnpkg.com/diffie-hellman/-/diffie-hellman-5.0.3.tgz#40e8ee98f55a2149607146921c63e1ae5f3d2875" + dependencies: + bn.js "^4.1.0" + miller-rabin "^4.0.0" + randombytes "^2.0.0" + +dns-equal@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/dns-equal/-/dns-equal-1.0.0.tgz#b39e7f1da6eb0a75ba9c17324b34753c47e0654d" + +dns-packet@^1.3.1: + version "1.3.1" + resolved "https://registry.yarnpkg.com/dns-packet/-/dns-packet-1.3.1.tgz#12aa426981075be500b910eedcd0b47dd7deda5a" + dependencies: + ip "^1.1.0" + safe-buffer "^5.0.1" + +dns-txt@^2.0.2: + version "2.0.2" + resolved "https://registry.yarnpkg.com/dns-txt/-/dns-txt-2.0.2.tgz#b91d806f5d27188e4ab3e7d107d881a1cc4642b6" + dependencies: + buffer-indexof "^1.0.0" + +doctrine@1.5.0: + version "1.5.0" + resolved "https://registry.yarnpkg.com/doctrine/-/doctrine-1.5.0.tgz#379dce730f6166f76cefa4e6707a159b02c5a6fa" + dependencies: + esutils "^2.0.2" + isarray "^1.0.0" + +doctrine@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/doctrine/-/doctrine-2.1.0.tgz#5cd01fc101621b42c4cd7f5d1a66243716d3f39d" + integrity sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw== + dependencies: + esutils "^2.0.2" + +doctrine@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/doctrine/-/doctrine-3.0.0.tgz#addebead72a6574db783639dc87a121773973961" + integrity sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w== + dependencies: + esutils "^2.0.2" + +dom-align@^1.7.0: + version "1.10.2" + resolved "https://registry.yarnpkg.com/dom-align/-/dom-align-1.10.2.tgz#540ea1c9e20462bd11b9fc28c561dc8351ece4c6" + +dom-helpers@^3.2.0: + version "3.4.0" + resolved "https://registry.yarnpkg.com/dom-helpers/-/dom-helpers-3.4.0.tgz#e9b369700f959f62ecde5a6babde4bccd9169af8" + dependencies: + "@babel/runtime" "^7.1.2" + +dom-serializer@0: + version "0.2.1" + resolved "https://registry.yarnpkg.com/dom-serializer/-/dom-serializer-0.2.1.tgz#13650c850daffea35d8b626a4cfc4d3a17643fdb" + dependencies: + domelementtype "^2.0.1" + entities "^2.0.0" + +domain-browser@^1.1.1, domain-browser@^1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/domain-browser/-/domain-browser-1.2.0.tgz#3d31f50191a6749dd1375a7f522e823d42e54eda" + +domelementtype@1: + version "1.3.1" + resolved "https://registry.yarnpkg.com/domelementtype/-/domelementtype-1.3.1.tgz#d048c44b37b0d10a7f2a3d5fee3f4333d790481f" + +domelementtype@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/domelementtype/-/domelementtype-2.0.1.tgz#1f8bdfe91f5a78063274e803b4bdcedf6e94f94d" + +domutils@^1.7.0: + version "1.7.0" + resolved "https://registry.yarnpkg.com/domutils/-/domutils-1.7.0.tgz#56ea341e834e06e6748af7a1cb25da67ea9f8c2a" + dependencies: + dom-serializer "0" + domelementtype "1" + +dot-prop@^4.1.1, dot-prop@^4.2.1: + version "4.2.1" + resolved "https://registry.yarnpkg.com/dot-prop/-/dot-prop-4.2.1.tgz#45884194a71fc2cda71cbb4bceb3a4dd2f433ba4" + integrity sha512-l0p4+mIuJIua0mhxGoh4a+iNL9bmeK5DvnSVQa6T0OhrVmaEa1XScX5Etc673FePCJOArq/4Pa2cLGODUWTPOQ== + dependencies: + is-obj "^1.0.0" + +dropzone@^5.7.2: + version "5.7.2" + resolved "https://registry.yarnpkg.com/dropzone/-/dropzone-5.7.2.tgz#91bee1572dda515d40901da304bc79dddf309b4c" + integrity sha512-m217bJHtf0J1IiKn4Tv6mnu1h5QvQNBnKZ39gma7hzGQhIZMxYq1vYEHs4AVd4ThFwmALys+52NAOD4zdLTG4w== + +duplexer2@^0.1.2, duplexer2@~0.1.0, duplexer2@~0.1.2: + version "0.1.4" + resolved "https://registry.yarnpkg.com/duplexer2/-/duplexer2-0.1.4.tgz#8b12dab878c0d69e3e7891051662a32fc6bddcc1" + dependencies: + readable-stream "^2.0.2" + +duplexify@^3.4.2, duplexify@^3.6.0: + version "3.7.1" + resolved "https://registry.yarnpkg.com/duplexify/-/duplexify-3.7.1.tgz#2a4df5317f6ccfd91f86d6fd25d8d8a103b88309" + dependencies: + end-of-stream "^1.0.0" + inherits "^2.0.1" + readable-stream "^2.0.0" + stream-shift "^1.0.0" + +ecc-jsbn@~0.1.1: + version "0.1.2" + resolved "https://registry.yarnpkg.com/ecc-jsbn/-/ecc-jsbn-0.1.2.tgz#3a83a904e54353287874c564b7549386849a98c9" + dependencies: + jsbn "~0.1.0" + safer-buffer "^2.1.0" + +ee-first@1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/ee-first/-/ee-first-1.1.1.tgz#590c61156b0ae2f4f0255732a158b266bc56b21d" + +electron-to-chromium@^1.3.191: + version "1.3.241" + resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.3.241.tgz#859dc49ab7f90773ed698767372d384190f60cb1" + +electron-to-chromium@^1.3.571: + version "1.3.578" + resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.3.578.tgz#e6671936f4571a874eb26e2e833aa0b2c0b776e0" + integrity sha512-z4gU6dA1CbBJsAErW5swTGAaU2TBzc2mPAonJb00zqW1rOraDo2zfBMDRvaz9cVic+0JEZiYbHWPw/fTaZlG2Q== + +element-resize-detector@^1.2.1: + version "1.2.1" + resolved "https://registry.yarnpkg.com/element-resize-detector/-/element-resize-detector-1.2.1.tgz#b0305194447a4863155e58f13323a0aef30851d1" + integrity sha512-BdFsPepnQr9fznNPF9nF4vQ457U/ZJXQDSNF1zBe7yaga8v9AdZf3/NElYxFdUh7SitSGt040QygiTo6dtatIw== + dependencies: + batch-processor "1.0.0" + +elliptic@^6.0.0, elliptic@^6.5.3: + version "6.5.3" + resolved "https://registry.yarnpkg.com/elliptic/-/elliptic-6.5.3.tgz#cb59eb2efdaf73a0bd78ccd7015a62ad6e0f93d6" + integrity sha512-IMqzv5wNQf+E6aHeIqATs0tOLeOTwj1QKbRcS3jBbYkl5oLAserA8yJTT7/VyHUYG91PRmPyeQDObKLPpeS4dw== + dependencies: + bn.js "^4.4.0" + brorand "^1.0.1" + hash.js "^1.0.0" + hmac-drbg "^1.0.0" + inherits "^2.0.1" + minimalistic-assert "^1.0.0" + minimalistic-crypto-utils "^1.0.0" + +email-addresses@^3.0.1: + version "3.0.3" + resolved "https://registry.yarnpkg.com/email-addresses/-/email-addresses-3.0.3.tgz#fc3c6952f68da24239914e982c8a7783bc2ed96d" + +emoji-regex@^7.0.1: + version "7.0.3" + resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-7.0.3.tgz#933a04052860c85e83c122479c4748a8e4c72156" + integrity sha512-CwBLREIQ7LvYFB0WyRvwhq5N5qPhc6PMjD6bYggFlI5YyDgl+0vxq5VHbMOFqLg7hfWzmu8T5Z1QofhmTIhItA== + +emoji-regex@^8.0.0: + version "8.0.0" + resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-8.0.0.tgz#e818fd69ce5ccfcb404594f842963bf53164cc37" + integrity sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A== + +emoji-regex@^9.0.0: + version "9.0.0" + resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-9.0.0.tgz#48a2309cc8a1d2e9d23bc6a67c39b63032e76ea4" + integrity sha512-6p1NII1Vm62wni/VR/cUMauVQoxmLVb9csqQlvLz+hO2gk8U2UYDfXHQSUYIBKmZwAKz867IDqG7B+u0mj+M6w== + +emojis-list@^2.0.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/emojis-list/-/emojis-list-2.1.0.tgz#4daa4d9db00f9819880c79fa457ae5b09a1fd389" + +emojis-list@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/emojis-list/-/emojis-list-3.0.0.tgz#5570662046ad29e2e916e71aae260abdff4f6a78" + integrity sha512-/kyM18EfinwXZbno9FyUGeFh87KC8HRQBQGildHZbEuRyWFOmv1U10o9BBp8XVZDVNNuQKyIGIu5ZYAAXJ0V2Q== + +encodeurl@~1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/encodeurl/-/encodeurl-1.0.2.tgz#ad3ff4c86ec2d029322f5a02c3a9a606c95b3f59" + +end-of-stream@^1.0.0, end-of-stream@^1.1.0: + version "1.4.1" + resolved "https://registry.yarnpkg.com/end-of-stream/-/end-of-stream-1.4.1.tgz#ed29634d19baba463b6ce6b80a37213eab71ec43" + dependencies: + once "^1.4.0" + +enhanced-resolve@^4.0.0, enhanced-resolve@^4.1.1, enhanced-resolve@^4.3.0: + version "4.3.0" + resolved "https://registry.yarnpkg.com/enhanced-resolve/-/enhanced-resolve-4.3.0.tgz#3b806f3bfafc1ec7de69551ef93cca46c1704126" + integrity sha512-3e87LvavsdxyoCfGusJnrZ5G8SLPOFeHSNpZI/ATL9a5leXo2k0w6MKnbqhdBad9qTobSfB20Ld7UmgoNbAZkQ== + dependencies: + graceful-fs "^4.1.2" + memory-fs "^0.5.0" + tapable "^1.0.0" + +enquirer@^2.3.5: + version "2.3.6" + resolved "https://registry.yarnpkg.com/enquirer/-/enquirer-2.3.6.tgz#2a7fe5dd634a1e4125a975ec994ff5456dc3734d" + integrity sha512-yjNnPr315/FjS4zIsUxYguYUPP2e1NK4d7E7ZOLiyYCcbFBiTMyID+2wvm2w6+pZ/odMA7cRkjhsPbltwBOrLg== + dependencies: + ansi-colors "^4.1.1" + +entities@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/entities/-/entities-2.0.0.tgz#68d6084cab1b079767540d80e56a39b423e4abf4" + +errno@^0.1.3, errno@~0.1.7: + version "0.1.7" + resolved "https://registry.yarnpkg.com/errno/-/errno-0.1.7.tgz#4684d71779ad39af177e3f007996f7c67c852618" + dependencies: + prr "~1.0.1" + +error-ex@^1.2.0, error-ex@^1.3.1: + version "1.3.2" + resolved "https://registry.yarnpkg.com/error-ex/-/error-ex-1.3.2.tgz#b4ac40648107fdcdcfae242f428bea8a14d4f1bf" + dependencies: + is-arrayish "^0.2.1" + +es-abstract@^1.12.0, es-abstract@^1.5.1: + version "1.13.0" + resolved "https://registry.yarnpkg.com/es-abstract/-/es-abstract-1.13.0.tgz#ac86145fdd5099d8dd49558ccba2eaf9b88e24e9" + dependencies: + es-to-primitive "^1.2.0" + function-bind "^1.1.1" + has "^1.0.3" + is-callable "^1.1.4" + is-regex "^1.0.4" + object-keys "^1.0.12" + +es-abstract@^1.17.0, es-abstract@^1.17.0-next.1, es-abstract@^1.17.5: + version "1.17.7" + resolved "https://registry.yarnpkg.com/es-abstract/-/es-abstract-1.17.7.tgz#a4de61b2f66989fc7421676c1cb9787573ace54c" + integrity sha512-VBl/gnfcJ7OercKA9MVaegWsBHFjV492syMudcnQZvt/Dw8ezpcOHYZXa/J96O8vx+g4x65YKhxOwDUh63aS5g== + dependencies: + es-to-primitive "^1.2.1" + function-bind "^1.1.1" + has "^1.0.3" + has-symbols "^1.0.1" + is-callable "^1.2.2" + is-regex "^1.1.1" + object-inspect "^1.8.0" + object-keys "^1.1.1" + object.assign "^4.1.1" + string.prototype.trimend "^1.0.1" + string.prototype.trimstart "^1.0.1" + +es-abstract@^1.18.0-next.0, es-abstract@^1.18.0-next.1: + version "1.18.0-next.1" + resolved "https://registry.yarnpkg.com/es-abstract/-/es-abstract-1.18.0-next.1.tgz#6e3a0a4bda717e5023ab3b8e90bec36108d22c68" + integrity sha512-I4UGspA0wpZXWENrdA0uHbnhte683t3qT/1VFH9aX2dA5PPSf6QW5HHXf5HImaqPmjXaVeVk4RGWnaylmV7uAA== + dependencies: + es-to-primitive "^1.2.1" + function-bind "^1.1.1" + has "^1.0.3" + has-symbols "^1.0.1" + is-callable "^1.2.2" + is-negative-zero "^2.0.0" + is-regex "^1.1.1" + object-inspect "^1.8.0" + object-keys "^1.1.1" + object.assign "^4.1.1" + string.prototype.trimend "^1.0.1" + string.prototype.trimstart "^1.0.1" + +es-to-primitive@^1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/es-to-primitive/-/es-to-primitive-1.2.0.tgz#edf72478033456e8dda8ef09e00ad9650707f377" + dependencies: + is-callable "^1.1.4" + is-date-object "^1.0.1" + is-symbol "^1.0.2" + +es-to-primitive@^1.2.1: + version "1.2.1" + resolved "https://registry.yarnpkg.com/es-to-primitive/-/es-to-primitive-1.2.1.tgz#e55cd4c9cdc188bcefb03b366c736323fc5c898a" + integrity sha512-QCOllgZJtaUo9miYBcLChTUaHNjJF3PYs1VidD7AwiEj1kYxKeQTctLAezAOH5ZKRH0g2IgPn6KwB4IT8iRpvA== + dependencies: + is-callable "^1.1.4" + is-date-object "^1.0.1" + is-symbol "^1.0.2" + +escalade@^3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/escalade/-/escalade-3.1.0.tgz#e8e2d7c7a8b76f6ee64c2181d6b8151441602d4e" + integrity sha512-mAk+hPSO8fLDkhV7V0dXazH5pDc6MrjBTPyD3VeKzxnVFjH1MIxbCdqGZB9O8+EwWakZs3ZCbDS4IpRt79V1ig== + +escape-html@~1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/escape-html/-/escape-html-1.0.3.tgz#0258eae4d3d0c0974de1c169188ef0051d1d1988" + +escape-string-regexp@^1.0.2, escape-string-regexp@^1.0.5: + version "1.0.5" + resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz#1b61c0562190a8dff6ae3bb2cf0200ca130b86d4" + +eslint-config-airbnb-base@^14.2.1: + version "14.2.1" + resolved "https://registry.yarnpkg.com/eslint-config-airbnb-base/-/eslint-config-airbnb-base-14.2.1.tgz#8a2eb38455dc5a312550193b319cdaeef042cd1e" + integrity sha512-GOrQyDtVEc1Xy20U7vsB2yAoB4nBlfH5HZJeatRXHleO+OS5Ot+MWij4Dpltw4/DyIkqUfqz1epfhVR5XWWQPA== + dependencies: + confusing-browser-globals "^1.0.10" + object.assign "^4.1.2" + object.entries "^1.1.2" + +eslint-config-airbnb@^18.2.1: + version "18.2.1" + resolved "https://registry.yarnpkg.com/eslint-config-airbnb/-/eslint-config-airbnb-18.2.1.tgz#b7fe2b42f9f8173e825b73c8014b592e449c98d9" + integrity sha512-glZNDEZ36VdlZWoxn/bUR1r/sdFKPd1mHPbqUtkctgNG4yT2DLLtJ3D+yCV+jzZCc2V1nBVkmdknOJBZ5Hc0fg== + dependencies: + eslint-config-airbnb-base "^14.2.1" + object.assign "^4.1.2" + object.entries "^1.1.2" + +eslint-import-resolver-node@^0.3.4: + version "0.3.4" + resolved "https://registry.yarnpkg.com/eslint-import-resolver-node/-/eslint-import-resolver-node-0.3.4.tgz#85ffa81942c25012d8231096ddf679c03042c717" + integrity sha512-ogtf+5AB/O+nM6DIeBUNr2fuT7ot9Qg/1harBfBtaP13ekEWFQEEMP94BCB7zaNW3gyY+8SHYF00rnqYwXKWOA== + dependencies: + debug "^2.6.9" + resolve "^1.13.1" + +eslint-module-utils@^2.6.0: + version "2.6.0" + resolved "https://registry.yarnpkg.com/eslint-module-utils/-/eslint-module-utils-2.6.0.tgz#579ebd094f56af7797d19c9866c9c9486629bfa6" + integrity sha512-6j9xxegbqe8/kZY8cYpcp0xhbK0EgJlg3g9mib3/miLaExuuwc3n5UEfSnU6hWMbT0FAYVvDbL9RrRgpUeQIvA== + dependencies: + debug "^2.6.9" + pkg-dir "^2.0.0" + +eslint-plugin-import@^2.22.1: + version "2.22.1" + resolved "https://registry.yarnpkg.com/eslint-plugin-import/-/eslint-plugin-import-2.22.1.tgz#0896c7e6a0cf44109a2d97b95903c2bb689d7702" + integrity sha512-8K7JjINHOpH64ozkAhpT3sd+FswIZTfMZTjdx052pnWrgRCVfp8op9tbjpAk3DdUeI/Ba4C8OjdC0r90erHEOw== + dependencies: + array-includes "^3.1.1" + array.prototype.flat "^1.2.3" + contains-path "^0.1.0" + debug "^2.6.9" + doctrine "1.5.0" + eslint-import-resolver-node "^0.3.4" + eslint-module-utils "^2.6.0" + has "^1.0.3" + minimatch "^3.0.4" + object.values "^1.1.1" + read-pkg-up "^2.0.0" + resolve "^1.17.0" + tsconfig-paths "^3.9.0" + +eslint-plugin-jsx-a11y@^6.4.1: + version "6.4.1" + resolved "https://registry.yarnpkg.com/eslint-plugin-jsx-a11y/-/eslint-plugin-jsx-a11y-6.4.1.tgz#a2d84caa49756942f42f1ffab9002436391718fd" + integrity sha512-0rGPJBbwHoGNPU73/QCLP/vveMlM1b1Z9PponxO87jfr6tuH5ligXbDT6nHSSzBC8ovX2Z+BQu7Bk5D/Xgq9zg== + dependencies: + "@babel/runtime" "^7.11.2" + aria-query "^4.2.2" + array-includes "^3.1.1" + ast-types-flow "^0.0.7" + axe-core "^4.0.2" + axobject-query "^2.2.0" + damerau-levenshtein "^1.0.6" + emoji-regex "^9.0.0" + has "^1.0.3" + jsx-ast-utils "^3.1.0" + language-tags "^1.0.5" + +eslint-plugin-promise@^4.2.1: + version "4.2.1" + resolved "https://registry.yarnpkg.com/eslint-plugin-promise/-/eslint-plugin-promise-4.2.1.tgz#845fd8b2260ad8f82564c1222fce44ad71d9418a" + integrity sha512-VoM09vT7bfA7D+upt+FjeBO5eHIJQBUWki1aPvB+vbNiHS3+oGIJGIeyBtKQTME6UPXXy3vV07OL1tHd3ANuDw== + +eslint-plugin-react@^7.22.0: + version "7.22.0" + resolved "https://registry.yarnpkg.com/eslint-plugin-react/-/eslint-plugin-react-7.22.0.tgz#3d1c542d1d3169c45421c1215d9470e341707269" + integrity sha512-p30tuX3VS+NWv9nQot9xIGAHBXR0+xJVaZriEsHoJrASGCJZDJ8JLNM0YqKqI0AKm6Uxaa1VUHoNEibxRCMQHA== + dependencies: + array-includes "^3.1.1" + array.prototype.flatmap "^1.2.3" + doctrine "^2.1.0" + has "^1.0.3" + jsx-ast-utils "^2.4.1 || ^3.0.0" + object.entries "^1.1.2" + object.fromentries "^2.0.2" + object.values "^1.1.1" + prop-types "^15.7.2" + resolve "^1.18.1" + string.prototype.matchall "^4.0.2" + +eslint-plugin-standard@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/eslint-plugin-standard/-/eslint-plugin-standard-5.0.0.tgz#c43f6925d669f177db46f095ea30be95476b1ee4" + integrity sha512-eSIXPc9wBM4BrniMzJRBm2uoVuXz2EPa+NXPk2+itrVt+r5SbKFERx/IgrK/HmfjddyKVz2f+j+7gBRvu19xLg== + +eslint-scope@^4.0.3: + version "4.0.3" + resolved "https://registry.yarnpkg.com/eslint-scope/-/eslint-scope-4.0.3.tgz#ca03833310f6889a3264781aa82e63eb9cfe7848" + integrity sha512-p7VutNr1O/QrxysMo3E45FjYDTeXBy0iTltPFNSqKAIfjDSXC+4dj+qfyuD8bfAXrW/y6lW3O76VaYNPKfpKrg== + dependencies: + esrecurse "^4.1.0" + estraverse "^4.1.1" + +eslint-scope@^5.1.1: + version "5.1.1" + resolved "https://registry.yarnpkg.com/eslint-scope/-/eslint-scope-5.1.1.tgz#e786e59a66cb92b3f6c1fb0d508aab174848f48c" + integrity sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw== + dependencies: + esrecurse "^4.3.0" + estraverse "^4.1.1" + +eslint-utils@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/eslint-utils/-/eslint-utils-2.1.0.tgz#d2de5e03424e707dc10c74068ddedae708741b27" + integrity sha512-w94dQYoauyvlDc43XnGB8lU3Zt713vNChgt4EWwhXAP2XkBvndfxF0AgIqKOOasjPIPzj9JqgwkwbCYD0/V3Zg== + dependencies: + eslint-visitor-keys "^1.1.0" + +eslint-visitor-keys@^1.1.0, eslint-visitor-keys@^1.3.0: + version "1.3.0" + resolved "https://registry.yarnpkg.com/eslint-visitor-keys/-/eslint-visitor-keys-1.3.0.tgz#30ebd1ef7c2fdff01c3a4f151044af25fab0523e" + integrity sha512-6J72N8UNa462wa/KFODt/PJ3IU60SDpC3QXC1Hjc1BXXpfL2C9R5+AU7jhe0F6GREqVMh4Juu+NY7xn+6dipUQ== + +eslint-visitor-keys@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/eslint-visitor-keys/-/eslint-visitor-keys-2.0.0.tgz#21fdc8fbcd9c795cc0321f0563702095751511a8" + integrity sha512-QudtT6av5WXels9WjIM7qz1XD1cWGvX4gGXvp/zBn9nXG02D0utdU3Em2m/QjTnrsk6bBjmCygl3rmj118msQQ== + +eslint@^7.18.0: + version "7.18.0" + resolved "https://registry.yarnpkg.com/eslint/-/eslint-7.18.0.tgz#7fdcd2f3715a41fe6295a16234bd69aed2c75e67" + integrity sha512-fbgTiE8BfUJZuBeq2Yi7J3RB3WGUQ9PNuNbmgi6jt9Iv8qrkxfy19Ds3OpL1Pm7zg3BtTVhvcUZbIRQ0wmSjAQ== + dependencies: + "@babel/code-frame" "^7.0.0" + "@eslint/eslintrc" "^0.3.0" + ajv "^6.10.0" + chalk "^4.0.0" + cross-spawn "^7.0.2" + debug "^4.0.1" + doctrine "^3.0.0" + enquirer "^2.3.5" + eslint-scope "^5.1.1" + eslint-utils "^2.1.0" + eslint-visitor-keys "^2.0.0" + espree "^7.3.1" + esquery "^1.2.0" + esutils "^2.0.2" + file-entry-cache "^6.0.0" + functional-red-black-tree "^1.0.1" + glob-parent "^5.0.0" + globals "^12.1.0" + ignore "^4.0.6" + import-fresh "^3.0.0" + imurmurhash "^0.1.4" + is-glob "^4.0.0" + js-yaml "^3.13.1" + json-stable-stringify-without-jsonify "^1.0.1" + levn "^0.4.1" + lodash "^4.17.20" + minimatch "^3.0.4" + natural-compare "^1.4.0" + optionator "^0.9.1" + progress "^2.0.0" + regexpp "^3.1.0" + semver "^7.2.1" + strip-ansi "^6.0.0" + strip-json-comments "^3.1.0" + table "^6.0.4" + text-table "^0.2.0" + v8-compile-cache "^2.0.3" + +espree@^7.3.0: + version "7.3.0" + resolved "https://registry.yarnpkg.com/espree/-/espree-7.3.0.tgz#dc30437cf67947cf576121ebd780f15eeac72348" + integrity sha512-dksIWsvKCixn1yrEXO8UosNSxaDoSYpq9reEjZSbHLpT5hpaCAKTLBwq0RHtLrIr+c0ByiYzWT8KTMRzoRCNlw== + dependencies: + acorn "^7.4.0" + acorn-jsx "^5.2.0" + eslint-visitor-keys "^1.3.0" + +espree@^7.3.1: + version "7.3.1" + resolved "https://registry.yarnpkg.com/espree/-/espree-7.3.1.tgz#f2df330b752c6f55019f8bd89b7660039c1bbbb6" + integrity sha512-v3JCNCE64umkFpmkFGqzVKsOT0tN1Zr+ueqLZfpV1Ob8e+CEgPWa+OxCoGH3tnhimMKIaBm4m/vaRpJ/krRz2g== + dependencies: + acorn "^7.4.0" + acorn-jsx "^5.3.1" + eslint-visitor-keys "^1.3.0" + +esprima-fb@13001.1001.0-dev-harmony-fb: + version "13001.1001.0-dev-harmony-fb" + resolved "https://registry.yarnpkg.com/esprima-fb/-/esprima-fb-13001.1001.0-dev-harmony-fb.tgz#633acdb40d9bd4db8a1c1d68c06a942959fad2b0" + +esprima@^4.0.0: + version "4.0.1" + resolved "https://registry.yarnpkg.com/esprima/-/esprima-4.0.1.tgz#13b04cdb3e6c5d19df91ab6987a8695619b0aa71" + +esprima@~3.1.0: + version "3.1.3" + resolved "https://registry.yarnpkg.com/esprima/-/esprima-3.1.3.tgz#fdca51cee6133895e3c88d535ce49dbff62a4633" + +esquery@^1.2.0: + version "1.3.1" + resolved "https://registry.yarnpkg.com/esquery/-/esquery-1.3.1.tgz#b78b5828aa8e214e29fb74c4d5b752e1c033da57" + integrity sha512-olpvt9QG0vniUBZspVRN6lwB7hOZoTRtT+jzR+tS4ffYx2mzbw+z0XCOk44aaLYKApNX5nMm+E+P6o25ip/DHQ== + dependencies: + estraverse "^5.1.0" + +esrecurse@^4.1.0: + version "4.2.1" + resolved "https://registry.yarnpkg.com/esrecurse/-/esrecurse-4.2.1.tgz#007a3b9fdbc2b3bb87e4879ea19c92fdbd3942cf" + dependencies: + estraverse "^4.1.0" + +esrecurse@^4.3.0: + version "4.3.0" + resolved "https://registry.yarnpkg.com/esrecurse/-/esrecurse-4.3.0.tgz#7ad7964d679abb28bee72cec63758b1c5d2c9921" + integrity sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag== + dependencies: + estraverse "^5.2.0" + +estraverse@^4.1.0, estraverse@^4.1.1: + version "4.3.0" + resolved "https://registry.yarnpkg.com/estraverse/-/estraverse-4.3.0.tgz#398ad3f3c5a24948be7725e83d11a7de28cdbd1d" + +estraverse@^5.1.0, estraverse@^5.2.0: + version "5.2.0" + resolved "https://registry.yarnpkg.com/estraverse/-/estraverse-5.2.0.tgz#307df42547e6cc7324d3cf03c155d5cdb8c53880" + integrity sha512-BxbNGGNm0RyRYvUdHpIwv9IWzeM9XClbOxwoATuFdOE7ZE6wHL+HQ5T8hoPM+zHvmKzzsEqhgy0GrQ5X13afiQ== + +esutils@^2.0.2: + version "2.0.3" + resolved "https://registry.yarnpkg.com/esutils/-/esutils-2.0.3.tgz#74d2eb4de0b8da1293711910d50775b9b710ef64" + +etag@~1.8.1: + version "1.8.1" + resolved "https://registry.yarnpkg.com/etag/-/etag-1.8.1.tgz#41ae2eeb65efa62268aebfea83ac7d79299b0887" + +eventemitter3@^4.0.0: + version "4.0.7" + resolved "https://registry.yarnpkg.com/eventemitter3/-/eventemitter3-4.0.7.tgz#2de9b68f6528d5644ef5c59526a1b4a07306169f" + integrity sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw== + +events@^2.0.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/events/-/events-2.1.0.tgz#2a9a1e18e6106e0e812aa9ebd4a819b3c29c0ba5" + integrity sha512-3Zmiobend8P9DjmKAty0Era4jV8oJ0yGYe2nJJAxgymF9+N8F2m0hhZiMoWtcfepExzNKZumFU3ksdQbInGWCg== + +events@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/events/-/events-3.0.0.tgz#9a0a0dfaf62893d92b875b8f2698ca4114973e88" + +eventsource@^1.0.7: + version "1.0.7" + resolved "https://registry.yarnpkg.com/eventsource/-/eventsource-1.0.7.tgz#8fbc72c93fcd34088090bc0a4e64f4b5cee6d8d0" + integrity sha512-4Ln17+vVT0k8aWq+t/bF5arcS3EpT9gYtW66EPacdj/mAFevznsnyoHLPy2BA8gbIQeIHoPsvwmfBftfcG//BQ== + dependencies: + original "^1.0.0" + +evp_bytestokey@^1.0.0, evp_bytestokey@^1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/evp_bytestokey/-/evp_bytestokey-1.0.3.tgz#7fcbdb198dc71959432efe13842684e0525acb02" + dependencies: + md5.js "^1.3.4" + safe-buffer "^5.1.1" + +execa@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/execa/-/execa-1.0.0.tgz#c6236a5bb4df6d6f15e88e7f017798216749ddd8" + integrity sha512-adbxcyWV46qiHyvSp50TKt05tB4tK3HcmF7/nxfAdhnox83seTDbwnaqKO4sXRy7roHAIFqJP/Rw/AuEbX61LA== + dependencies: + cross-spawn "^6.0.0" + get-stream "^4.0.0" + is-stream "^1.1.0" + npm-run-path "^2.0.0" + p-finally "^1.0.0" + signal-exit "^3.0.0" + strip-eof "^1.0.0" + +expand-brackets@^2.1.4: + version "2.1.4" + resolved "https://registry.yarnpkg.com/expand-brackets/-/expand-brackets-2.1.4.tgz#b77735e315ce30f6b6eff0f83b04151a22449622" + dependencies: + debug "^2.3.3" + define-property "^0.2.5" + extend-shallow "^2.0.1" + posix-character-classes "^0.1.0" + regex-not "^1.0.0" + snapdragon "^0.8.1" + to-regex "^3.0.1" + +expand-tilde@^2.0.0, expand-tilde@^2.0.2: + version "2.0.2" + resolved "https://registry.yarnpkg.com/expand-tilde/-/expand-tilde-2.0.2.tgz#97e801aa052df02454de46b02bf621642cdc8502" + integrity sha1-l+gBqgUt8CRU3kawK/YhZCzchQI= + dependencies: + homedir-polyfill "^1.0.1" + +express@^4.17.1: + version "4.17.1" + resolved "https://registry.yarnpkg.com/express/-/express-4.17.1.tgz#4491fc38605cf51f8629d39c2b5d026f98a4c134" + integrity sha512-mHJ9O79RqluphRrcw2X/GTh3k9tVv8YcoyY4Kkh4WDMUYKRZUq0h1o0w2rrrxBqM7VoeUVqgb27xlEMXTnYt4g== + dependencies: + accepts "~1.3.7" + array-flatten "1.1.1" + body-parser "1.19.0" + content-disposition "0.5.3" + content-type "~1.0.4" + cookie "0.4.0" + cookie-signature "1.0.6" + debug "2.6.9" + depd "~1.1.2" + encodeurl "~1.0.2" + escape-html "~1.0.3" + etag "~1.8.1" + finalhandler "~1.1.2" + fresh "0.5.2" + merge-descriptors "1.0.1" + methods "~1.1.2" + on-finished "~2.3.0" + parseurl "~1.3.3" + path-to-regexp "0.1.7" + proxy-addr "~2.0.5" + qs "6.7.0" + range-parser "~1.2.1" + safe-buffer "5.1.2" + send "0.17.1" + serve-static "1.14.1" + setprototypeof "1.1.1" + statuses "~1.5.0" + type-is "~1.6.18" + utils-merge "1.0.1" + vary "~1.1.2" + +extend-shallow@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/extend-shallow/-/extend-shallow-2.0.1.tgz#51af7d614ad9a9f610ea1bafbb989d6b1c56890f" + dependencies: + is-extendable "^0.1.0" + +extend-shallow@^3.0.0, extend-shallow@^3.0.2: + version "3.0.2" + resolved "https://registry.yarnpkg.com/extend-shallow/-/extend-shallow-3.0.2.tgz#26a71aaf073b39fb2127172746131c2704028db8" + dependencies: + assign-symbols "^1.0.0" + is-extendable "^1.0.1" + +extend@~3.0.2: + version "3.0.2" + resolved "https://registry.yarnpkg.com/extend/-/extend-3.0.2.tgz#f8b1136b4071fbd8eb140aff858b1019ec2915fa" + +extglob@^2.0.4: + version "2.0.4" + resolved "https://registry.yarnpkg.com/extglob/-/extglob-2.0.4.tgz#ad00fe4dc612a9232e8718711dc5cb5ab0285543" + dependencies: + array-unique "^0.3.2" + define-property "^1.0.0" + expand-brackets "^2.1.4" + extend-shallow "^2.0.1" + fragment-cache "^0.2.1" + regex-not "^1.0.0" + snapdragon "^0.8.1" + to-regex "^3.0.1" + +extsprintf@1.3.0: + version "1.3.0" + resolved "https://registry.yarnpkg.com/extsprintf/-/extsprintf-1.3.0.tgz#96918440e3041a7a414f8c52e3c574eb3c3e1e05" + +extsprintf@^1.2.0: + version "1.4.0" + resolved "https://registry.yarnpkg.com/extsprintf/-/extsprintf-1.4.0.tgz#e2689f8f356fad62cca65a3a91c5df5f9551692f" + +fast-deep-equal@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-2.0.1.tgz#7b05218ddf9667bf7f370bf7fdb2cb15fdd0aa49" + +fast-deep-equal@^3.1.1: + version "3.1.3" + resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz#3a7d56b559d6cbc3eb512325244e619a65c6c525" + integrity sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q== + +fast-json-stable-stringify@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/fast-json-stable-stringify/-/fast-json-stable-stringify-2.0.0.tgz#d5142c0caee6b1189f87d3a76111064f86c8bbf2" + +fast-levenshtein@^2.0.6: + version "2.0.6" + resolved "https://registry.yarnpkg.com/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz#3d8a5c66883a16a30ca8643e851f19baa7797917" + integrity sha1-PYpcZog6FqMMqGQ+hR8Zuqd5eRc= + +faye-websocket@^0.11.3: + version "0.11.3" + resolved "https://registry.yarnpkg.com/faye-websocket/-/faye-websocket-0.11.3.tgz#5c0e9a8968e8912c286639fde977a8b209f2508e" + integrity sha512-D2y4bovYpzziGgbHYtGCMjlJM36vAl/y+xUyn1C+FVx8szd1E+86KwVw6XvYSzOP8iMpm1X0I4xJD+QtUb36OA== + dependencies: + websocket-driver ">=0.5.1" + +figgy-pudding@^3.5.1: + version "3.5.2" + resolved "https://registry.yarnpkg.com/figgy-pudding/-/figgy-pudding-3.5.2.tgz#b4eee8148abb01dcf1d1ac34367d59e12fa61d6e" + integrity sha512-0btnI/H8f2pavGMN8w40mlSKOfTK2SVJmBfBeVIj3kNw0swwgzyRq0d5TJVOwodFmtvpPeWPN/MCcfuWF0Ezbw== + +file-entry-cache@^6.0.0: + version "6.0.0" + resolved "https://registry.yarnpkg.com/file-entry-cache/-/file-entry-cache-6.0.0.tgz#7921a89c391c6d93efec2169ac6bf300c527ea0a" + integrity sha512-fqoO76jZ3ZnYrXLDRxBR1YvOvc0k844kcOg40bgsPrE25LAb/PDqTY+ho64Xh2c8ZXgIKldchCFHczG2UVRcWA== + dependencies: + flat-cache "^3.0.4" + +file-loader@^6.0.0: + version "6.1.0" + resolved "https://registry.yarnpkg.com/file-loader/-/file-loader-6.1.0.tgz#65b9fcfb0ea7f65a234a1f10cdd7f1ab9a33f253" + integrity sha512-26qPdHyTsArQ6gU4P1HJbAbnFTyT2r0pG7czh1GFAd9TZbj0n94wWbupgixZH/ET/meqi2/5+F7DhW4OAXD+Lg== + dependencies: + loader-utils "^2.0.0" + schema-utils "^2.7.1" + +filename-reserved-regex@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/filename-reserved-regex/-/filename-reserved-regex-1.0.0.tgz#e61cf805f0de1c984567d0386dc5df50ee5af7e4" + +filenamify-url@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/filenamify-url/-/filenamify-url-1.0.0.tgz#b32bd81319ef5863b73078bed50f46a4f7975f50" + dependencies: + filenamify "^1.0.0" + humanize-url "^1.0.0" + +filenamify@^1.0.0: + version "1.2.1" + resolved "https://registry.yarnpkg.com/filenamify/-/filenamify-1.2.1.tgz#a9f2ffd11c503bed300015029272378f1f1365a5" + dependencies: + filename-reserved-regex "^1.0.0" + strip-outer "^1.0.0" + trim-repeated "^1.0.0" + +fill-range@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/fill-range/-/fill-range-4.0.0.tgz#d544811d428f98eb06a63dc402d2403c328c38f7" + dependencies: + extend-shallow "^2.0.1" + is-number "^3.0.0" + repeat-string "^1.6.1" + to-regex-range "^2.1.0" + +fill-range@^7.0.1: + version "7.0.1" + resolved "https://registry.yarnpkg.com/fill-range/-/fill-range-7.0.1.tgz#1919a6a7c75fe38b2c7c77e5198535da9acdda40" + integrity sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ== + dependencies: + to-regex-range "^5.0.1" + +finalhandler@~1.1.2: + version "1.1.2" + resolved "https://registry.yarnpkg.com/finalhandler/-/finalhandler-1.1.2.tgz#b7e7d000ffd11938d0fdb053506f6ebabe9f587d" + dependencies: + debug "2.6.9" + encodeurl "~1.0.2" + escape-html "~1.0.3" + on-finished "~2.3.0" + parseurl "~1.3.3" + statuses "~1.5.0" + unpipe "~1.0.0" + +find-cache-dir@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/find-cache-dir/-/find-cache-dir-2.1.0.tgz#8d0f94cd13fe43c6c7c261a0d86115ca918c05f7" + integrity sha512-Tq6PixE0w/VMFfCgbONnkiQIVol/JJL7nRMi20fqzA4NRs9AfeqMGeRdPi3wIhYkxjeBaWh2rxwapn5Tu3IqOQ== + dependencies: + commondir "^1.0.1" + make-dir "^2.0.0" + pkg-dir "^3.0.0" + +find-cache-dir@^3.3.1: + version "3.3.1" + resolved "https://registry.yarnpkg.com/find-cache-dir/-/find-cache-dir-3.3.1.tgz#89b33fad4a4670daa94f855f7fbe31d6d84fe880" + integrity sha512-t2GDMt3oGC/v+BMwzmllWDuJF/xcDtE5j/fCGbqDD7OLuJkj0cfh1YSA5VKPvwMeLFLNDBkwOKZ2X85jGLVftQ== + dependencies: + commondir "^1.0.1" + make-dir "^3.0.2" + pkg-dir "^4.1.0" + +find-up@^1.0.0: + version "1.1.2" + resolved "https://registry.yarnpkg.com/find-up/-/find-up-1.1.2.tgz#6b2e9822b1a2ce0a60ab64d610eccad53cb24d0f" + dependencies: + path-exists "^2.0.0" + pinkie-promise "^2.0.0" + +find-up@^2.0.0, find-up@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/find-up/-/find-up-2.1.0.tgz#45d1b7e506c717ddd482775a2b77920a3c0c57a7" + dependencies: + locate-path "^2.0.0" + +find-up@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/find-up/-/find-up-3.0.0.tgz#49169f1d7993430646da61ecc5ae355c21c97b73" + integrity sha512-1yD6RmLI1XBfxugvORwlck6f75tYL+iR0jqwsOrOxMZyGYqUuDhJ0l4AXdO1iX/FTs9cBAMEk1gWSEx1kSbylg== + dependencies: + locate-path "^3.0.0" + +find-up@^4.0.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/find-up/-/find-up-4.1.0.tgz#97afe7d6cdc0bc5928584b7c8d7b16e8a9aa5d19" + integrity sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw== + dependencies: + locate-path "^5.0.0" + path-exists "^4.0.0" + +findup-sync@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/findup-sync/-/findup-sync-3.0.0.tgz#17b108f9ee512dfb7a5c7f3c8b27ea9e1a9c08d1" + integrity sha512-YbffarhcicEhOrm4CtrwdKBdCuz576RLdhJDsIfvNtxUuhdRet1qZcsMjqbePtAseKdAnDyM/IyXbu7PRPRLYg== + dependencies: + detect-file "^1.0.0" + is-glob "^4.0.0" + micromatch "^3.0.4" + resolve-dir "^1.0.1" + +flat-cache@^3.0.4: + version "3.0.4" + resolved "https://registry.yarnpkg.com/flat-cache/-/flat-cache-3.0.4.tgz#61b0338302b2fe9f957dcc32fc2a87f1c3048b11" + integrity sha512-dm9s5Pw7Jc0GvMYbshN6zchCA9RgQlzzEZX3vylR9IqFfS8XciblUXOKfW6SiuJ0e13eDYZoZV5wdrev7P3Nwg== + dependencies: + flatted "^3.1.0" + rimraf "^3.0.2" + +flatted@^3.0.4: + version "3.1.0" + resolved "https://registry.yarnpkg.com/flatted/-/flatted-3.1.0.tgz#a5d06b4a8b01e3a63771daa5cb7a1903e2e57067" + integrity sha512-tW+UkmtNg/jv9CSofAKvgVcO7c2URjhTdW1ZTkcAritblu8tajiYy7YisnIflEwtKssCtOxpnBRoCB7iap0/TA== + +flatted@^3.1.0: + version "3.1.1" + resolved "https://registry.yarnpkg.com/flatted/-/flatted-3.1.1.tgz#c4b489e80096d9df1dfc97c79871aea7c617c469" + integrity sha512-zAoAQiudy+r5SvnSw3KJy5os/oRJYHzrzja/tBDqrZtNhUw8bt6y8OBzMWcjWr+8liV8Eb6yOhw8WZ7VFZ5ZzA== + +flatten@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/flatten/-/flatten-1.0.2.tgz#dae46a9d78fbe25292258cc1e780a41d95c03782" + +flush-write-stream@^1.0.0: + version "1.1.1" + resolved "https://registry.yarnpkg.com/flush-write-stream/-/flush-write-stream-1.1.1.tgz#8dd7d873a1babc207d94ead0c2e0e44276ebf2e8" + dependencies: + inherits "^2.0.3" + readable-stream "^2.3.6" + +follow-redirects@^1.0.0: + version "1.7.0" + resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.7.0.tgz#489ebc198dc0e7f64167bd23b03c4c19b5784c76" + dependencies: + debug "^3.2.6" + +for-in@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/for-in/-/for-in-1.0.2.tgz#81068d295a8142ec0ac726c6e2200c30fb6d5e80" + +forever-agent@~0.6.1: + version "0.6.1" + resolved "https://registry.yarnpkg.com/forever-agent/-/forever-agent-0.6.1.tgz#fbc71f0c41adeb37f96c577ad1ed42d8fdacca91" + +form-data@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/form-data/-/form-data-3.0.0.tgz#31b7e39c85f1355b7139ee0c647cf0de7f83c682" + integrity sha512-CKMFDglpbMi6PyN+brwB9Q/GOw0eAnsrEZDgcsH5Krhz5Od/haKHAX0NmQfha2zPPz0JpWzA7GJHGSnvCRLWsg== + dependencies: + asynckit "^0.4.0" + combined-stream "^1.0.8" + mime-types "^2.1.12" + +form-data@~2.3.2: + version "2.3.3" + resolved "https://registry.yarnpkg.com/form-data/-/form-data-2.3.3.tgz#dcce52c05f644f298c6a7ab936bd724ceffbf3a6" + dependencies: + asynckit "^0.4.0" + combined-stream "^1.0.6" + mime-types "^2.1.12" + +forwarded@~0.1.2: + version "0.1.2" + resolved "https://registry.yarnpkg.com/forwarded/-/forwarded-0.1.2.tgz#98c23dab1175657b8c0573e8ceccd91b0ff18c84" + +fragment-cache@^0.2.1: + version "0.2.1" + resolved "https://registry.yarnpkg.com/fragment-cache/-/fragment-cache-0.2.1.tgz#4290fad27f13e89be7f33799c6bc5a0abfff0d19" + dependencies: + map-cache "^0.2.2" + +fresh@0.5.2: + version "0.5.2" + resolved "https://registry.yarnpkg.com/fresh/-/fresh-0.5.2.tgz#3d8cadd90d976569fa835ab1f8e4b23a105605a7" + +from2@^2.1.0: + version "2.3.0" + resolved "https://registry.yarnpkg.com/from2/-/from2-2.3.0.tgz#8bfb5502bde4a4d36cfdeea007fcca21d7e382af" + dependencies: + inherits "^2.0.1" + readable-stream "^2.0.0" + +fs-extra@^7.0.0: + version "7.0.1" + resolved "https://registry.yarnpkg.com/fs-extra/-/fs-extra-7.0.1.tgz#4f189c44aa123b895f722804f55ea23eadc348e9" + dependencies: + graceful-fs "^4.1.2" + jsonfile "^4.0.0" + universalify "^0.1.0" + +fs-minipass@^1.2.5: + version "1.2.6" + resolved "https://registry.yarnpkg.com/fs-minipass/-/fs-minipass-1.2.6.tgz#2c5cc30ded81282bfe8a0d7c7c1853ddeb102c07" + dependencies: + minipass "^2.2.1" + +fs-minipass@^2.0.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/fs-minipass/-/fs-minipass-2.1.0.tgz#7f5036fdbf12c63c169190cbe4199c852271f9fb" + integrity sha512-V/JgOLFCS+R6Vcq0slCuaeWEdNC3ouDlJMNIsacH2VtALiu9mV4LPrHc5cDl8k5aw6J8jwgWWpiTo5RYhmIzvg== + dependencies: + minipass "^3.0.0" + +fs-write-stream-atomic@^1.0.8: + version "1.0.10" + resolved "https://registry.yarnpkg.com/fs-write-stream-atomic/-/fs-write-stream-atomic-1.0.10.tgz#b47df53493ef911df75731e70a9ded0189db40c9" + dependencies: + graceful-fs "^4.1.2" + iferr "^0.1.5" + imurmurhash "^0.1.4" + readable-stream "1 || 2" + +fs.realpath@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/fs.realpath/-/fs.realpath-1.0.0.tgz#1504ad2523158caa40db4a2787cb01411994ea4f" + +fsevents@^1.2.7: + version "1.2.9" + resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-1.2.9.tgz#3f5ed66583ccd6f400b5a00db6f7e861363e388f" + dependencies: + nan "^2.12.1" + node-pre-gyp "^0.12.0" + +fsevents@~2.1.2: + version "2.1.3" + resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-2.1.3.tgz#fb738703ae8d2f9fe900c33836ddebee8b97f23e" + integrity sha512-Auw9a4AxqWpa9GUfj370BMPzzyncfBABW8Mab7BGWBYDj4Isgq+cDKtx0i6u9jcX9pQDnswsaaOTgTmA5pEjuQ== + +fstream@^1.0.0, fstream@^1.0.12: + version "1.0.12" + resolved "https://registry.yarnpkg.com/fstream/-/fstream-1.0.12.tgz#4e8ba8ee2d48be4f7d0de505455548eae5932045" + dependencies: + graceful-fs "^4.1.2" + inherits "~2.0.0" + mkdirp ">=0.5 0" + rimraf "2" + +function-bind@^1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/function-bind/-/function-bind-1.1.1.tgz#a56899d3ea3c9bab874bb9773b7c5ede92f4895d" + +functional-red-black-tree@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/functional-red-black-tree/-/functional-red-black-tree-1.0.1.tgz#1b0ab3bd553b2a0d6399d29c0e3ea0b252078327" + integrity sha1-GwqzvVU7Kg1jmdKcDj6gslIHgyc= + +gauge@~2.7.3: + version "2.7.4" + resolved "https://registry.yarnpkg.com/gauge/-/gauge-2.7.4.tgz#2c03405c7538c39d7eb37b317022e325fb018bf7" + dependencies: + aproba "^1.0.3" + console-control-strings "^1.0.0" + has-unicode "^2.0.0" + object-assign "^4.1.0" + signal-exit "^3.0.0" + string-width "^1.0.1" + strip-ansi "^3.0.1" + wide-align "^1.1.0" + +gaze@^1.0.0: + version "1.1.3" + resolved "https://registry.yarnpkg.com/gaze/-/gaze-1.1.3.tgz#c441733e13b927ac8c0ff0b4c3b033f28812924a" + dependencies: + globule "^1.0.0" + +gensync@^1.0.0-beta.1: + version "1.0.0-beta.1" + resolved "https://registry.yarnpkg.com/gensync/-/gensync-1.0.0-beta.1.tgz#58f4361ff987e5ff6e1e7a210827aa371eaac269" + integrity sha512-r8EC6NO1sngH/zdD9fiRDLdcgnbayXah+mLgManTaIZJqEC1MZstmnox8KpnI2/fxQwrp5OpCOYWLp4rBl4Jcg== + +get-assigned-identifiers@^1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/get-assigned-identifiers/-/get-assigned-identifiers-1.2.0.tgz#6dbf411de648cbaf8d9169ebb0d2d576191e2ff1" + +get-caller-file@^2.0.1: + version "2.0.5" + resolved "https://registry.yarnpkg.com/get-caller-file/-/get-caller-file-2.0.5.tgz#4f94412a82db32f36e3b0b9741f8a97feb031f7e" + integrity sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg== + +get-intrinsic@^1.0.1, get-intrinsic@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/get-intrinsic/-/get-intrinsic-1.0.2.tgz#6820da226e50b24894e08859469dc68361545d49" + integrity sha512-aeX0vrFm21ILl3+JpFFRNe9aUvp6VFZb2/CTbgLb8j75kOhvoNYjt9d8KA/tJG4gSo8nzEDedRl0h7vDmBYRVg== + dependencies: + function-bind "^1.1.1" + has "^1.0.3" + has-symbols "^1.0.1" + +get-stdin@^4.0.1: + version "4.0.1" + resolved "https://registry.yarnpkg.com/get-stdin/-/get-stdin-4.0.1.tgz#b968c6b0a04384324902e8bf1a5df32579a450fe" + +get-stream@^4.0.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/get-stream/-/get-stream-4.1.0.tgz#c1b255575f3dc21d59bfc79cd3d2b46b1c3a54b5" + integrity sha512-GMat4EJ5161kIy2HevLlr4luNjBgvmj413KaQA7jt4V8B4RDsfpHk7WQ9GVqfYyyx8OS/L66Kox+rJRNklLK7w== + dependencies: + pump "^3.0.0" + +get-value@^2.0.3, get-value@^2.0.6: + version "2.0.6" + resolved "https://registry.yarnpkg.com/get-value/-/get-value-2.0.6.tgz#dc15ca1c672387ca76bd37ac0a395ba2042a2c28" + +getpass@^0.1.1: + version "0.1.7" + resolved "https://registry.yarnpkg.com/getpass/-/getpass-0.1.7.tgz#5eff8e3e684d569ae4cb2b1282604e8ba62149fa" + dependencies: + assert-plus "^1.0.0" + +gh-pages@^2.0.1: + version "2.1.1" + resolved "https://registry.yarnpkg.com/gh-pages/-/gh-pages-2.1.1.tgz#5be70a92f9cb70404bafabd8bb149c0e9a8c264b" + dependencies: + async "^2.6.1" + commander "^2.18.0" + email-addresses "^3.0.1" + filenamify-url "^1.0.0" + fs-extra "^7.0.0" + globby "^6.1.0" + graceful-fs "^4.1.11" + rimraf "^2.6.2" + +glob-parent@^3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/glob-parent/-/glob-parent-3.1.0.tgz#9e6af6299d8d3bd2bd40430832bd113df906c5ae" + dependencies: + is-glob "^3.1.0" + path-dirname "^1.0.0" + +glob-parent@^5.0.0, glob-parent@~5.1.0: + version "5.1.1" + resolved "https://registry.yarnpkg.com/glob-parent/-/glob-parent-5.1.1.tgz#b6c1ef417c4e5663ea498f1c45afac6916bbc229" + integrity sha512-FnI+VGOpnlGHWZxthPGR+QhR78fuiK0sNLkHQv+bL9fQi57lNNdquIbna/WrfROrolq8GK5Ek6BiMwqL/voRYQ== + dependencies: + is-glob "^4.0.1" + +glob@^5.0.15: + version "5.0.15" + resolved "https://registry.yarnpkg.com/glob/-/glob-5.0.15.tgz#1bc936b9e02f4a603fcc222ecf7633d30b8b93b1" + dependencies: + inflight "^1.0.4" + inherits "2" + minimatch "2 || 3" + once "^1.3.0" + path-is-absolute "^1.0.0" + +glob@^7.0.0, glob@^7.0.3, glob@^7.1.0, glob@^7.1.2, glob@^7.1.3, glob@~7.1.1: + version "7.1.4" + resolved "https://registry.yarnpkg.com/glob/-/glob-7.1.4.tgz#aa608a2f6c577ad357e1ae5a5c26d9a8d1969255" + dependencies: + fs.realpath "^1.0.0" + inflight "^1.0.4" + inherits "2" + minimatch "^3.0.4" + once "^1.3.0" + path-is-absolute "^1.0.0" + +glob@^7.1.4, glob@^7.1.6: + version "7.1.6" + resolved "https://registry.yarnpkg.com/glob/-/glob-7.1.6.tgz#141f33b81a7c2492e125594307480c46679278a6" + integrity sha512-LwaxwyZ72Lk7vZINtNNrywX0ZuLyStrdDtabefZKAY5ZGJhVtgdznluResxNmPitE0SAO+O26sWTHeKSI2wMBA== + dependencies: + fs.realpath "^1.0.0" + inflight "^1.0.4" + inherits "2" + minimatch "^3.0.4" + once "^1.3.0" + path-is-absolute "^1.0.0" + +global-modules@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/global-modules/-/global-modules-1.0.0.tgz#6d770f0eb523ac78164d72b5e71a8877265cc3ea" + integrity sha512-sKzpEkf11GpOFuw0Zzjzmt4B4UZwjOcG757PPvrfhxcLFbq0wpsgpOqxpxtxFiCG4DtG93M6XRVbF2oGdev7bg== + dependencies: + global-prefix "^1.0.1" + is-windows "^1.0.1" + resolve-dir "^1.0.0" + +global-modules@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/global-modules/-/global-modules-2.0.0.tgz#997605ad2345f27f51539bea26574421215c7780" + integrity sha512-NGbfmJBp9x8IxyJSd1P+otYK8vonoJactOogrVfFRIAEY1ukil8RSKDz2Yo7wh1oihl51l/r6W4epkeKJHqL8A== + dependencies: + global-prefix "^3.0.0" + +global-prefix@^1.0.1: + version "1.0.2" + resolved "https://registry.yarnpkg.com/global-prefix/-/global-prefix-1.0.2.tgz#dbf743c6c14992593c655568cb66ed32c0122ebe" + integrity sha1-2/dDxsFJklk8ZVVoy2btMsASLr4= + dependencies: + expand-tilde "^2.0.2" + homedir-polyfill "^1.0.1" + ini "^1.3.4" + is-windows "^1.0.1" + which "^1.2.14" + +global-prefix@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/global-prefix/-/global-prefix-3.0.0.tgz#fc85f73064df69f50421f47f883fe5b913ba9b97" + integrity sha512-awConJSVCHVGND6x3tmMaKcQvwXLhjdkmomy2W+Goaui8YPgYgXJZewhg3fWC+DlfqqQuWg8AwqjGTD2nAPVWg== + dependencies: + ini "^1.3.5" + kind-of "^6.0.2" + which "^1.3.1" + +globals@^11.1.0: + version "11.12.0" + resolved "https://registry.yarnpkg.com/globals/-/globals-11.12.0.tgz#ab8795338868a0babd8525758018c2a7eb95c42e" + integrity sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA== + +globals@^12.1.0: + version "12.4.0" + resolved "https://registry.yarnpkg.com/globals/-/globals-12.4.0.tgz#a18813576a41b00a24a97e7f815918c2e19925f8" + integrity sha512-BWICuzzDvDoH54NHKCseDanAhE3CeDorgDL5MT6LMXXj2WCnd9UC2szdk4AWLfjdgNBCXLUanXYcpBBKOSWGwg== + dependencies: + type-fest "^0.8.1" + +globby@^6.1.0: + version "6.1.0" + resolved "https://registry.yarnpkg.com/globby/-/globby-6.1.0.tgz#f5a6d70e8395e21c858fb0489d64df02424d506c" + dependencies: + array-union "^1.0.1" + glob "^7.0.3" + object-assign "^4.0.1" + pify "^2.0.0" + pinkie-promise "^2.0.0" + +globule@^1.0.0: + version "1.2.1" + resolved "https://registry.yarnpkg.com/globule/-/globule-1.2.1.tgz#5dffb1b191f22d20797a9369b49eab4e9839696d" + dependencies: + glob "~7.1.1" + lodash "~4.17.10" + minimatch "~3.0.2" + +graceful-fs@^4.1.11, graceful-fs@^4.1.2, graceful-fs@^4.1.6: + version "4.2.2" + resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.2.tgz#6f0952605d0140c1cfdb138ed005775b92d67b02" + +graceful-fs@^4.1.15: + version "4.2.4" + resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.4.tgz#2256bde14d3632958c465ebc96dc467ca07a29fb" + integrity sha512-WjKPNJF79dtJAVniUlGGWHYGz2jWxT6VhN/4m1NdkbZ2nOsEF+cI1Edgql5zCRhs/VsQYRvrXctxktVXZUkixw== + +gud@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/gud/-/gud-1.0.0.tgz#a489581b17e6a70beca9abe3ae57de7a499852c0" + +handle-thing@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/handle-thing/-/handle-thing-2.0.1.tgz#857f79ce359580c340d43081cc648970d0bb234e" + integrity sha512-9Qn4yBxelxoh2Ow62nP+Ka/kMnOXRi8BXnRaUwezLNhqelnN49xKz4F/dPP8OYLxLxq6JDtZb2i9XznUQbNPTg== + +har-schema@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/har-schema/-/har-schema-2.0.0.tgz#a94c2224ebcac04782a0d9035521f24735b7ec92" + +har-validator@~5.1.0: + version "5.1.3" + resolved "https://registry.yarnpkg.com/har-validator/-/har-validator-5.1.3.tgz#1ef89ebd3e4996557675eed9893110dc350fa080" + dependencies: + ajv "^6.5.5" + har-schema "^2.0.0" + +has-ansi@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/has-ansi/-/has-ansi-2.0.0.tgz#34f5049ce1ecdf2b0649af3ef24e45ed35416d91" + dependencies: + ansi-regex "^2.0.0" + +has-flag@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-3.0.0.tgz#b5d454dc2199ae225699f3467e5a07f3b955bafd" + +has-flag@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-4.0.0.tgz#944771fd9c81c81265c4d6941860da06bb59479b" + integrity sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ== + +has-symbols@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/has-symbols/-/has-symbols-1.0.0.tgz#ba1a8f1af2a0fc39650f5c850367704122063b44" + +has-symbols@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/has-symbols/-/has-symbols-1.0.1.tgz#9f5214758a44196c406d9bd76cebf81ec2dd31e8" + integrity sha512-PLcsoqu++dmEIZB+6totNFKq/7Do+Z0u4oT0zKOJNl3lYK6vGwwu2hjHs+68OEZbTjiUE9bgOABXbP/GvrS0Kg== + +has-unicode@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/has-unicode/-/has-unicode-2.0.1.tgz#e0e6fe6a28cf51138855e086d1691e771de2a8b9" + +has-value@^0.3.1: + version "0.3.1" + resolved "https://registry.yarnpkg.com/has-value/-/has-value-0.3.1.tgz#7b1f58bada62ca827ec0a2078025654845995e1f" + dependencies: + get-value "^2.0.3" + has-values "^0.1.4" + isobject "^2.0.0" + +has-value@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/has-value/-/has-value-1.0.0.tgz#18b281da585b1c5c51def24c930ed29a0be6b177" + dependencies: + get-value "^2.0.6" + has-values "^1.0.0" + isobject "^3.0.0" + +has-values@^0.1.4: + version "0.1.4" + resolved "https://registry.yarnpkg.com/has-values/-/has-values-0.1.4.tgz#6d61de95d91dfca9b9a02089ad384bff8f62b771" + +has-values@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/has-values/-/has-values-1.0.0.tgz#95b0b63fec2146619a6fe57fe75628d5a39efe4f" + dependencies: + is-number "^3.0.0" + kind-of "^4.0.0" + +has@^1.0.0, has@^1.0.1, has@^1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/has/-/has-1.0.3.tgz#722d7cbfc1f6aa8241f16dd814e011e1f41e8796" + dependencies: + function-bind "^1.1.1" + +hash-base@^3.0.0: + version "3.0.4" + resolved "https://registry.yarnpkg.com/hash-base/-/hash-base-3.0.4.tgz#5fc8686847ecd73499403319a6b0a3f3f6ae4918" + dependencies: + inherits "^2.0.1" + safe-buffer "^5.0.1" + +hash.js@^1.0.0, hash.js@^1.0.3: + version "1.1.7" + resolved "https://registry.yarnpkg.com/hash.js/-/hash.js-1.1.7.tgz#0babca538e8d4ee4a0f8988d68866537a003cf42" + dependencies: + inherits "^2.0.3" + minimalistic-assert "^1.0.1" + +hex-color-regex@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/hex-color-regex/-/hex-color-regex-1.1.0.tgz#4c06fccb4602fe2602b3c93df82d7e7dbf1a8a8e" + +highlightjs@^9.16.2: + version "9.16.2" + resolved "https://registry.yarnpkg.com/highlightjs/-/highlightjs-9.16.2.tgz#07ea6cc7c93340fc440734fb7abf28558f1f0fe1" + integrity sha512-FK1vmMj8BbEipEy8DLIvp71t5UsC7n2D6En/UfM/91PCwmOpj6f2iu0Y0coRC62KSRHHC+dquM2xMULV/X7NFg== + +hmac-drbg@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/hmac-drbg/-/hmac-drbg-1.0.1.tgz#d2745701025a6c775a6c545793ed502fc0c649a1" + dependencies: + hash.js "^1.0.3" + minimalistic-assert "^1.0.0" + minimalistic-crypto-utils "^1.0.1" + +hoist-non-react-statics@^3.3.0: + version "3.3.2" + resolved "https://registry.yarnpkg.com/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz#ece0acaf71d62c2969c2ec59feff42a4b1a85b45" + integrity sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw== + dependencies: + react-is "^16.7.0" + +homedir-polyfill@^1.0.1: + version "1.0.3" + resolved "https://registry.yarnpkg.com/homedir-polyfill/-/homedir-polyfill-1.0.3.tgz#743298cef4e5af3e194161fbadcc2151d3a058e8" + integrity sha512-eSmmWE5bZTK2Nou4g0AI3zZ9rswp7GRKoKXS1BLUkvPviOqs4YTN1djQIqrXy9k5gEtdLPy86JjRwsNM9tnDcA== + dependencies: + parse-passwd "^1.0.0" + +hosted-git-info@^2.1.4: + version "2.8.4" + resolved "https://registry.yarnpkg.com/hosted-git-info/-/hosted-git-info-2.8.4.tgz#44119abaf4bc64692a16ace34700fed9c03e2546" + +hpack.js@^2.1.6: + version "2.1.6" + resolved "https://registry.yarnpkg.com/hpack.js/-/hpack.js-2.1.6.tgz#87774c0949e513f42e84575b3c45681fade2a0b2" + dependencies: + inherits "^2.0.1" + obuf "^1.0.0" + readable-stream "^2.0.1" + wbuf "^1.1.0" + +hsl-regex@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/hsl-regex/-/hsl-regex-1.0.0.tgz#d49330c789ed819e276a4c0d272dffa30b18fe6e" + +hsla-regex@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/hsla-regex/-/hsla-regex-1.0.0.tgz#c1ce7a3168c8c6614033a4b5f7877f3b225f9c38" + +html-comment-regex@^1.1.0: + version "1.1.2" + resolved "https://registry.yarnpkg.com/html-comment-regex/-/html-comment-regex-1.1.2.tgz#97d4688aeb5c81886a364faa0cad1dda14d433a7" + +html-entities@^1.3.1: + version "1.3.1" + resolved "https://registry.yarnpkg.com/html-entities/-/html-entities-1.3.1.tgz#fb9a1a4b5b14c5daba82d3e34c6ae4fe701a0e44" + integrity sha512-rhE/4Z3hIhzHAUKbW8jVcCyuT5oJCXXqhN/6mXXVCpzTmvJnoH2HL/bt3EZ6p55jbFJBeAe1ZNpL5BugLujxNA== + +htmlescape@^1.1.0: + version "1.1.1" + resolved "https://registry.yarnpkg.com/htmlescape/-/htmlescape-1.1.1.tgz#3a03edc2214bca3b66424a3e7959349509cb0351" + +http-deceiver@^1.2.7: + version "1.2.7" + resolved "https://registry.yarnpkg.com/http-deceiver/-/http-deceiver-1.2.7.tgz#fa7168944ab9a519d337cb0bec7284dc3e723d87" + +http-errors@1.7.2: + version "1.7.2" + resolved "https://registry.yarnpkg.com/http-errors/-/http-errors-1.7.2.tgz#4f5029cf13239f31036e5b2e55292bcfbcc85c8f" + dependencies: + depd "~1.1.2" + inherits "2.0.3" + setprototypeof "1.1.1" + statuses ">= 1.5.0 < 2" + toidentifier "1.0.0" + +http-errors@~1.6.2: + version "1.6.3" + resolved "https://registry.yarnpkg.com/http-errors/-/http-errors-1.6.3.tgz#8b55680bb4be283a0b5bf4ea2e38580be1d9320d" + dependencies: + depd "~1.1.2" + inherits "2.0.3" + setprototypeof "1.1.0" + statuses ">= 1.4.0 < 2" + +http-errors@~1.7.2: + version "1.7.3" + resolved "https://registry.yarnpkg.com/http-errors/-/http-errors-1.7.3.tgz#6c619e4f9c60308c38519498c14fbb10aacebb06" + dependencies: + depd "~1.1.2" + inherits "2.0.4" + setprototypeof "1.1.1" + statuses ">= 1.5.0 < 2" + toidentifier "1.0.0" + +"http-parser-js@>=0.4.0 <0.4.11": + version "0.4.10" + resolved "https://registry.yarnpkg.com/http-parser-js/-/http-parser-js-0.4.10.tgz#92c9c1374c35085f75db359ec56cc257cbb93fa4" + +http-parser-js@>=0.5.1: + version "0.5.3" + resolved "https://registry.yarnpkg.com/http-parser-js/-/http-parser-js-0.5.3.tgz#01d2709c79d41698bb01d4decc5e9da4e4a033d9" + integrity sha512-t7hjvef/5HEK7RWTdUzVUhl8zkEu+LlaE0IYzdMuvbSDipxBRpOn4Uhw8ZyECEa808iVT8XCjzo6xmYt4CiLZg== + +http-proxy-middleware@0.19.1: + version "0.19.1" + resolved "https://registry.yarnpkg.com/http-proxy-middleware/-/http-proxy-middleware-0.19.1.tgz#183c7dc4aa1479150306498c210cdaf96080a43a" + integrity sha512-yHYTgWMQO8VvwNS22eLLloAkvungsKdKTLO8AJlftYIKNfJr3GK3zK0ZCfzDDGUBttdGc8xFy1mCitvNKQtC3Q== + dependencies: + http-proxy "^1.17.0" + is-glob "^4.0.0" + lodash "^4.17.11" + micromatch "^3.1.10" + +http-proxy@^1.17.0: + version "1.18.1" + resolved "https://registry.yarnpkg.com/http-proxy/-/http-proxy-1.18.1.tgz#401541f0534884bbf95260334e72f88ee3976549" + integrity sha512-7mz/721AbnJwIVbnaSv1Cz3Am0ZLT/UBwkC92VlxhXv/k/BBQfM2fXElQNC27BVGr0uwUpplYPQM9LnaBMR5NQ== + dependencies: + eventemitter3 "^4.0.0" + follow-redirects "^1.0.0" + requires-port "^1.0.0" + +http-signature@~1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/http-signature/-/http-signature-1.2.0.tgz#9aecd925114772f3d95b65a60abb8f7c18fbace1" + dependencies: + assert-plus "^1.0.0" + jsprim "^1.2.2" + sshpk "^1.7.0" + +https-browserify@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/https-browserify/-/https-browserify-1.0.0.tgz#ec06c10e0a34c0f2faf199f7fd7fc78fffd03c73" + +humanize-url@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/humanize-url/-/humanize-url-1.0.1.tgz#f4ab99e0d288174ca4e1e50407c55fbae464efff" + dependencies: + normalize-url "^1.0.0" + strip-url-auth "^1.0.0" + +iconv-lite@0.4.24, iconv-lite@^0.4.4, iconv-lite@^0.4.5: + version "0.4.24" + resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.24.tgz#2022b4b25fbddc21d2f524974a474aafe733908b" + dependencies: + safer-buffer ">= 2.1.2 < 3" + +icss-utils@^4.0.0, icss-utils@^4.1.1: + version "4.1.1" + resolved "https://registry.yarnpkg.com/icss-utils/-/icss-utils-4.1.1.tgz#21170b53789ee27447c2f47dd683081403f9a467" + integrity sha512-4aFq7wvWyMHKgxsH8QQtGpvbASCf+eM3wPRLI6R+MgAnTCZ6STYsRvttLvRWK0Nfif5piF394St3HeJDaljGPA== + dependencies: + postcss "^7.0.14" + +ieee754@^1.1.4: + version "1.1.13" + resolved "https://registry.yarnpkg.com/ieee754/-/ieee754-1.1.13.tgz#ec168558e95aa181fd87d37f55c32bbcb6708b84" + +iferr@^0.1.5: + version "0.1.5" + resolved "https://registry.yarnpkg.com/iferr/-/iferr-0.1.5.tgz#c60eed69e6d8fdb6b3104a1fcbca1c192dc5b501" + +ignore-walk@^3.0.1: + version "3.0.1" + resolved "https://registry.yarnpkg.com/ignore-walk/-/ignore-walk-3.0.1.tgz#a83e62e7d272ac0e3b551aaa82831a19b69f82f8" + dependencies: + minimatch "^3.0.4" + +ignore@^4.0.6: + version "4.0.6" + resolved "https://registry.yarnpkg.com/ignore/-/ignore-4.0.6.tgz#750e3db5862087b4737ebac8207ffd1ef27b25fc" + integrity sha512-cyFDKrqc/YdcWFniJhzI42+AzS+gNwmUzOSFcRCQYwySuBBBy/KjuxWLZ/FHEH6Moq1NizMOBWyTcv8O4OZIMg== + +immutability-helper@^3.1.1: + version "3.1.1" + resolved "https://registry.yarnpkg.com/immutability-helper/-/immutability-helper-3.1.1.tgz#2b86b2286ed3b1241c9e23b7b21e0444f52f77b7" + integrity sha512-Q0QaXjPjwIju/28TsugCHNEASwoCcJSyJV3uO1sOIQGI0jKgm9f41Lvz0DZj3n46cNCyAZTsEYoY4C2bVRUzyQ== + +import-cwd@^2.0.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/import-cwd/-/import-cwd-2.1.0.tgz#aa6cf36e722761285cb371ec6519f53e2435b0a9" + dependencies: + import-from "^2.1.0" + +import-fresh@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/import-fresh/-/import-fresh-2.0.0.tgz#d81355c15612d386c61f9ddd3922d4304822a546" + dependencies: + caller-path "^2.0.0" + resolve-from "^3.0.0" + +import-fresh@^3.0.0, import-fresh@^3.1.0, import-fresh@^3.2.1: + version "3.2.1" + resolved "https://registry.yarnpkg.com/import-fresh/-/import-fresh-3.2.1.tgz#633ff618506e793af5ac91bf48b72677e15cbe66" + integrity sha512-6e1q1cnWP2RXD9/keSkxHScg508CdXqXWgWBaETNhyuBFz+kUZlKboh+ISK+bU++DmbHimVBrOz/zzPe0sZ3sQ== + dependencies: + parent-module "^1.0.0" + resolve-from "^4.0.0" + +import-from@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/import-from/-/import-from-2.1.0.tgz#335db7f2a7affd53aaa471d4b8021dee36b7f3b1" + dependencies: + resolve-from "^3.0.0" + +import-local@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/import-local/-/import-local-2.0.0.tgz#55070be38a5993cf18ef6db7e961f5bee5c5a09d" + integrity sha512-b6s04m3O+s3CGSbqDIyP4R6aAwAeYlVq9+WUWep6iHa8ETRf9yei1U48C5MmfJmV9AiLYYBKPMq/W+/WRpQmCQ== + dependencies: + pkg-dir "^3.0.0" + resolve-cwd "^2.0.0" + +imurmurhash@^0.1.4: + version "0.1.4" + resolved "https://registry.yarnpkg.com/imurmurhash/-/imurmurhash-0.1.4.tgz#9218b9b2b928a238b13dc4fb6b6d576f231453ea" + +in-publish@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/in-publish/-/in-publish-2.0.0.tgz#e20ff5e3a2afc2690320b6dc552682a9c7fadf51" + +indent-string@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/indent-string/-/indent-string-2.1.0.tgz#8e2d48348742121b4a8218b7a137e9a52049dc80" + dependencies: + repeating "^2.0.0" + +indent-string@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/indent-string/-/indent-string-4.0.0.tgz#624f8f4497d619b2d9768531d58f4122854d7251" + integrity sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg== + +indexes-of@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/indexes-of/-/indexes-of-1.0.1.tgz#f30f716c8e2bd346c7b67d3df3915566a7c05607" + +infer-owner@^1.0.3, infer-owner@^1.0.4: + version "1.0.4" + resolved "https://registry.yarnpkg.com/infer-owner/-/infer-owner-1.0.4.tgz#c4cefcaa8e51051c2a40ba2ce8a3d27295af9467" + integrity sha512-IClj+Xz94+d7irH5qRyfJonOdfTzuDaifE6ZPWfx0N0+/ATZCbuTPq2prFl526urkQd90WyUKIh1DfBQ2hMz9A== + +inflight@^1.0.4: + version "1.0.6" + resolved "https://registry.yarnpkg.com/inflight/-/inflight-1.0.6.tgz#49bd6331d7d02d0c09bc910a1075ba8165b56df9" + dependencies: + once "^1.3.0" + wrappy "1" + +inherits@2, inherits@2.0.4, inherits@^2.0.1, inherits@^2.0.3, inherits@^2.0.4, inherits@~2.0.0, inherits@~2.0.1, inherits@~2.0.3: + version "2.0.4" + resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.4.tgz#0fa2c64f932917c3433a0ded55363aae37416b7c" + +inherits@2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.1.tgz#b17d08d326b4423e568eff719f91b0b1cbdf69f1" + +inherits@2.0.3: + version "2.0.3" + resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.3.tgz#633c2c83e3da42a502f52466022480f4208261de" + +ini@^1.3.4, ini@^1.3.5, ini@^1.3.8, ini@~1.3.0: + version "1.3.8" + resolved "https://registry.yarnpkg.com/ini/-/ini-1.3.8.tgz#a29da425b48806f34767a4efce397269af28432c" + integrity sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew== + +inline-source-map@~0.6.0: + version "0.6.2" + resolved "https://registry.yarnpkg.com/inline-source-map/-/inline-source-map-0.6.2.tgz#f9393471c18a79d1724f863fa38b586370ade2a5" + dependencies: + source-map "~0.5.3" + +insert-module-globals@^7.0.0: + version "7.2.0" + resolved "https://registry.yarnpkg.com/insert-module-globals/-/insert-module-globals-7.2.0.tgz#ec87e5b42728479e327bd5c5c71611ddfb4752ba" + dependencies: + JSONStream "^1.0.3" + acorn-node "^1.5.2" + combine-source-map "^0.8.0" + concat-stream "^1.6.1" + is-buffer "^1.1.0" + path-is-absolute "^1.0.1" + process "~0.11.0" + through2 "^2.0.0" + undeclared-identifiers "^1.1.2" + xtend "^4.0.0" + +internal-ip@^4.3.0: + version "4.3.0" + resolved "https://registry.yarnpkg.com/internal-ip/-/internal-ip-4.3.0.tgz#845452baad9d2ca3b69c635a137acb9a0dad0907" + integrity sha512-S1zBo1D6zcsyuC6PMmY5+55YMILQ9av8lotMx447Bq6SAgo/sDK6y6uUKmuYhW7eacnIhFfsPmCNYdDzsnnDCg== + dependencies: + default-gateway "^4.2.0" + ipaddr.js "^1.9.0" + +internal-slot@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/internal-slot/-/internal-slot-1.0.2.tgz#9c2e9fb3cd8e5e4256c6f45fe310067fcfa378a3" + integrity sha512-2cQNfwhAfJIkU4KZPkDI+Gj5yNNnbqi40W9Gge6dfnk4TocEVm00B3bdiL+JINrbGJil2TeHvM4rETGzk/f/0g== + dependencies: + es-abstract "^1.17.0-next.1" + has "^1.0.3" + side-channel "^1.0.2" + +interpret@^1.4.0: + version "1.4.0" + resolved "https://registry.yarnpkg.com/interpret/-/interpret-1.4.0.tgz#665ab8bc4da27a774a40584e812e3e0fa45b1a1e" + integrity sha512-agE4QfB2Lkp9uICn7BAqoscw4SZP9kTE2hxiFI3jBPmXJfdqiahTbUuKGsMoN2GtqL9AxhYioAcVvgsb1HvRbA== + +invariant@^2.2.2, invariant@^2.2.4: + version "2.2.4" + resolved "https://registry.yarnpkg.com/invariant/-/invariant-2.2.4.tgz#610f3c92c9359ce1db616e538008d23ff35158e6" + dependencies: + loose-envify "^1.0.0" + +ip-regex@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/ip-regex/-/ip-regex-2.1.0.tgz#fa78bf5d2e6913c911ce9f819ee5146bb6d844e9" + integrity sha1-+ni/XS5pE8kRzp+BnuUUa7bYROk= + +ip@^1.1.0, ip@^1.1.5: + version "1.1.5" + resolved "https://registry.yarnpkg.com/ip/-/ip-1.1.5.tgz#bdded70114290828c0a039e72ef25f5aaec4354a" + +ipaddr.js@1.9.0: + version "1.9.0" + resolved "https://registry.yarnpkg.com/ipaddr.js/-/ipaddr.js-1.9.0.tgz#37df74e430a0e47550fe54a2defe30d8acd95f65" + +ipaddr.js@^1.9.0: + version "1.9.1" + resolved "https://registry.yarnpkg.com/ipaddr.js/-/ipaddr.js-1.9.1.tgz#bff38543eeb8984825079ff3a2a8e6cbd46781b3" + integrity sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g== + +is-absolute-url@^2.0.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/is-absolute-url/-/is-absolute-url-2.1.0.tgz#50530dfb84fcc9aa7dbe7852e83a37b93b9f2aa6" + +is-absolute-url@^3.0.3: + version "3.0.3" + resolved "https://registry.yarnpkg.com/is-absolute-url/-/is-absolute-url-3.0.3.tgz#96c6a22b6a23929b11ea0afb1836c36ad4a5d698" + integrity sha512-opmNIX7uFnS96NtPmhWQgQx6/NYFgsUXYMllcfzwWKUMwfo8kku1TvE6hkNcH+Q1ts5cMVrsY7j0bxXQDciu9Q== + +is-accessor-descriptor@^0.1.6: + version "0.1.6" + resolved "https://registry.yarnpkg.com/is-accessor-descriptor/-/is-accessor-descriptor-0.1.6.tgz#a9e12cb3ae8d876727eeef3843f8a0897b5c98d6" + dependencies: + kind-of "^3.0.2" + +is-accessor-descriptor@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/is-accessor-descriptor/-/is-accessor-descriptor-1.0.0.tgz#169c2f6d3df1f992618072365c9b0ea1f6878656" + dependencies: + kind-of "^6.0.0" + +is-arguments@^1.0.4: + version "1.0.4" + resolved "https://registry.yarnpkg.com/is-arguments/-/is-arguments-1.0.4.tgz#3faf966c7cba0ff437fb31f6250082fcf0448cf3" + integrity sha512-xPh0Rmt8NE65sNzvyUmWgI1tz3mKq74lGA0mL8LYZcoIzKOzDh6HmrYm3d18k60nHerC8A9Km8kYu87zfSFnLA== + +is-arrayish@^0.2.1: + version "0.2.1" + resolved "https://registry.yarnpkg.com/is-arrayish/-/is-arrayish-0.2.1.tgz#77c99840527aa8ecb1a8ba697b80645a7a926a9d" + +is-arrayish@^0.3.1: + version "0.3.2" + resolved "https://registry.yarnpkg.com/is-arrayish/-/is-arrayish-0.3.2.tgz#4574a2ae56f7ab206896fb431eaeed066fdf8f03" + +is-binary-path@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/is-binary-path/-/is-binary-path-1.0.1.tgz#75f16642b480f187a711c814161fd3a4a7655898" + dependencies: + binary-extensions "^1.0.0" + +is-binary-path@~2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/is-binary-path/-/is-binary-path-2.1.0.tgz#ea1f7f3b80f064236e83470f86c09c254fb45b09" + integrity sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw== + dependencies: + binary-extensions "^2.0.0" + +is-buffer@^1.1.0: + version "1.1.6" + resolved "https://registry.yarnpkg.com/is-buffer/-/is-buffer-1.1.6.tgz#efaa2ea9daa0d7ab2ea13a97b2b8ad51fefbe8be" + +is-callable@^1.1.4: + version "1.1.4" + resolved "https://registry.yarnpkg.com/is-callable/-/is-callable-1.1.4.tgz#1e1adf219e1eeb684d691f9d6a05ff0d30a24d75" + +is-callable@^1.2.2: + version "1.2.2" + resolved "https://registry.yarnpkg.com/is-callable/-/is-callable-1.2.2.tgz#c7c6715cd22d4ddb48d3e19970223aceabb080d9" + integrity sha512-dnMqspv5nU3LoewK2N/y7KLtxtakvTuaCsU9FU50/QDmdbHNy/4/JuRtMHqRU22o3q+W89YQndQEeCVwK+3qrA== + +is-color-stop@^1.0.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/is-color-stop/-/is-color-stop-1.1.0.tgz#cfff471aee4dd5c9e158598fbe12967b5cdad345" + dependencies: + css-color-names "^0.0.4" + hex-color-regex "^1.1.0" + hsl-regex "^1.0.0" + hsla-regex "^1.0.0" + rgb-regex "^1.0.1" + rgba-regex "^1.0.0" + +is-core-module@^2.1.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/is-core-module/-/is-core-module-2.2.0.tgz#97037ef3d52224d85163f5597b2b63d9afed981a" + integrity sha512-XRAfAdyyY5F5cOXn7hYQDqh2Xmii+DEfIcQGxK/uNwMHhIkPWO0g8msXcbzLe+MpGoR951MlqM/2iIlU4vKDdQ== + dependencies: + has "^1.0.3" + +is-data-descriptor@^0.1.4: + version "0.1.4" + resolved "https://registry.yarnpkg.com/is-data-descriptor/-/is-data-descriptor-0.1.4.tgz#0b5ee648388e2c860282e793f1856fec3f301b56" + dependencies: + kind-of "^3.0.2" + +is-data-descriptor@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/is-data-descriptor/-/is-data-descriptor-1.0.0.tgz#d84876321d0e7add03990406abbbbd36ba9268c7" + dependencies: + kind-of "^6.0.0" + +is-date-object@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/is-date-object/-/is-date-object-1.0.1.tgz#9aa20eb6aeebbff77fbd33e74ca01b33581d3a16" + +is-descriptor@^0.1.0: + version "0.1.6" + resolved "https://registry.yarnpkg.com/is-descriptor/-/is-descriptor-0.1.6.tgz#366d8240dde487ca51823b1ab9f07a10a78251ca" + dependencies: + is-accessor-descriptor "^0.1.6" + is-data-descriptor "^0.1.4" + kind-of "^5.0.0" + +is-descriptor@^1.0.0, is-descriptor@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/is-descriptor/-/is-descriptor-1.0.2.tgz#3b159746a66604b04f8c81524ba365c5f14d86ec" + dependencies: + is-accessor-descriptor "^1.0.0" + is-data-descriptor "^1.0.0" + kind-of "^6.0.2" + +is-directory@^0.3.1: + version "0.3.1" + resolved "https://registry.yarnpkg.com/is-directory/-/is-directory-0.3.1.tgz#61339b6f2475fc772fd9c9d83f5c8575dc154ae1" + +is-extendable@^0.1.0, is-extendable@^0.1.1: + version "0.1.1" + resolved "https://registry.yarnpkg.com/is-extendable/-/is-extendable-0.1.1.tgz#62b110e289a471418e3ec36a617d472e301dfc89" + +is-extendable@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/is-extendable/-/is-extendable-1.0.1.tgz#a7470f9e426733d81bd81e1155264e3a3507cab4" + dependencies: + is-plain-object "^2.0.4" + +is-extglob@^2.1.0, is-extglob@^2.1.1: + version "2.1.1" + resolved "https://registry.yarnpkg.com/is-extglob/-/is-extglob-2.1.1.tgz#a88c02535791f02ed37c76a1b9ea9773c833f8c2" + +is-finite@^1.0.0: + version "1.0.2" + resolved "https://registry.yarnpkg.com/is-finite/-/is-finite-1.0.2.tgz#cc6677695602be550ef11e8b4aa6305342b6d0aa" + dependencies: + number-is-nan "^1.0.0" + +is-fullwidth-code-point@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/is-fullwidth-code-point/-/is-fullwidth-code-point-1.0.0.tgz#ef9e31386f031a7f0d643af82fde50c457ef00cb" + dependencies: + number-is-nan "^1.0.0" + +is-fullwidth-code-point@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz#a3b30a5c4f199183167aaab93beefae3ddfb654f" + +is-fullwidth-code-point@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz#f116f8064fe90b3f7844a38997c0b75051269f1d" + integrity sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg== + +is-glob@^3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/is-glob/-/is-glob-3.1.0.tgz#7ba5ae24217804ac70707b96922567486cc3e84a" + dependencies: + is-extglob "^2.1.0" + +is-glob@^4.0.0, is-glob@^4.0.1, is-glob@~4.0.1: + version "4.0.1" + resolved "https://registry.yarnpkg.com/is-glob/-/is-glob-4.0.1.tgz#7567dbe9f2f5e2467bc77ab83c4a29482407a5dc" + dependencies: + is-extglob "^2.1.1" + +is-negative-zero@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/is-negative-zero/-/is-negative-zero-2.0.0.tgz#9553b121b0fac28869da9ed459e20c7543788461" + integrity sha1-lVOxIbD6wohp2p7UWeIMdUN4hGE= + +is-number@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/is-number/-/is-number-3.0.0.tgz#24fd6201a4782cf50561c810276afc7d12d71195" + dependencies: + kind-of "^3.0.2" + +is-number@^7.0.0: + version "7.0.0" + resolved "https://registry.yarnpkg.com/is-number/-/is-number-7.0.0.tgz#7535345b896734d5f80c4d06c50955527a14f12b" + integrity sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng== + +is-obj@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/is-obj/-/is-obj-1.0.1.tgz#3e4729ac1f5fde025cd7d83a896dab9f4f67db0f" + +is-path-cwd@^2.0.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/is-path-cwd/-/is-path-cwd-2.2.0.tgz#67d43b82664a7b5191fd9119127eb300048a9fdb" + integrity sha512-w942bTcih8fdJPJmQHFzkS76NEP8Kzzvmw92cXsazb8intwLqPibPPdXf4ANdKV3rYMuuQYGIWtvz9JilB3NFQ== + +is-path-in-cwd@^2.0.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/is-path-in-cwd/-/is-path-in-cwd-2.1.0.tgz#bfe2dca26c69f397265a4009963602935a053acb" + integrity sha512-rNocXHgipO+rvnP6dk3zI20RpOtrAM/kzbB258Uw5BWr3TpXi861yzjo16Dn4hUox07iw5AyeMLHWsujkjzvRQ== + dependencies: + is-path-inside "^2.1.0" + +is-path-inside@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/is-path-inside/-/is-path-inside-2.1.0.tgz#7c9810587d659a40d27bcdb4d5616eab059494b2" + integrity sha512-wiyhTzfDWsvwAW53OBWF5zuvaOGlZ6PwYxAbPVDhpm+gM09xKQGjBq/8uYN12aDvMxnAnq3dxTyoSoRNmg5YFg== + dependencies: + path-is-inside "^1.0.2" + +is-plain-obj@^1.0.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/is-plain-obj/-/is-plain-obj-1.1.0.tgz#71a50c8429dfca773c92a390a4a03b39fcd51d3e" + +is-plain-object@^2.0.3, is-plain-object@^2.0.4: + version "2.0.4" + resolved "https://registry.yarnpkg.com/is-plain-object/-/is-plain-object-2.0.4.tgz#2c163b3fafb1b606d9d17928f05c2a1c38e07677" + dependencies: + isobject "^3.0.1" + +is-regex@^1.0.4: + version "1.0.4" + resolved "https://registry.yarnpkg.com/is-regex/-/is-regex-1.0.4.tgz#5517489b547091b0930e095654ced25ee97e9491" + dependencies: + has "^1.0.1" + +is-regex@^1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/is-regex/-/is-regex-1.1.1.tgz#c6f98aacc546f6cec5468a07b7b153ab564a57b9" + integrity sha512-1+QkEcxiLlB7VEyFtyBg94e08OAsvq7FUBgApTq/w2ymCLyKJgDPsybBENVtA7XCQEgEXxKPonG+mvYRxh/LIg== + dependencies: + has-symbols "^1.0.1" + +is-resolvable@^1.0.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/is-resolvable/-/is-resolvable-1.1.0.tgz#fb18f87ce1feb925169c9a407c19318a3206ed88" + +is-stream@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/is-stream/-/is-stream-1.1.0.tgz#12d4a3dd4e68e0b79ceb8dbc84173ae80d91ca44" + +is-string@^1.0.5: + version "1.0.5" + resolved "https://registry.yarnpkg.com/is-string/-/is-string-1.0.5.tgz#40493ed198ef3ff477b8c7f92f644ec82a5cd3a6" + integrity sha512-buY6VNRjhQMiF1qWDouloZlQbRhDPCebwxSjxMjxgemYT46YMd2NR0/H+fBhEfWX4A/w9TBJ+ol+okqJKFE6vQ== + +is-svg@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/is-svg/-/is-svg-3.0.0.tgz#9321dbd29c212e5ca99c4fa9794c714bcafa2f75" + dependencies: + html-comment-regex "^1.1.0" + +is-symbol@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/is-symbol/-/is-symbol-1.0.2.tgz#a055f6ae57192caee329e7a860118b497a950f38" + dependencies: + has-symbols "^1.0.0" + +is-typedarray@~1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/is-typedarray/-/is-typedarray-1.0.0.tgz#e479c80858df0c1b11ddda6940f96011fcda4a9a" + +is-utf8@^0.2.0: + version "0.2.1" + resolved "https://registry.yarnpkg.com/is-utf8/-/is-utf8-0.2.1.tgz#4b0da1442104d1b336340e80797e865cf39f7d72" + +is-windows@^1.0.1, is-windows@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/is-windows/-/is-windows-1.0.2.tgz#d1850eb9791ecd18e6182ce12a30f396634bb19d" + +is-wsl@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/is-wsl/-/is-wsl-1.1.0.tgz#1f16e4aa22b04d1336b66188a66af3c600c3a66d" + +isarray@1.0.0, isarray@^1.0.0, isarray@~1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/isarray/-/isarray-1.0.0.tgz#bb935d48582cba168c06834957a54a3e07124f11" + +isexe@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/isexe/-/isexe-2.0.0.tgz#e8fbf374dc556ff8947a10dcb0572d633f2cfa10" + +isobject@^2.0.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/isobject/-/isobject-2.1.0.tgz#f065561096a3f1da2ef46272f815c840d87e0c89" + dependencies: + isarray "1.0.0" + +isobject@^3.0.0, isobject@^3.0.1: + version "3.0.1" + resolved "https://registry.yarnpkg.com/isobject/-/isobject-3.0.1.tgz#4e431e92b11a9731636aa1f9c8d1ccbcfdab78df" + +isstream@~0.1.2: + version "0.1.2" + resolved "https://registry.yarnpkg.com/isstream/-/isstream-0.1.2.tgz#47e63f7af55afa6f92e1500e690eb8b8529c099a" + +jest-worker@^26.3.0: + version "26.5.0" + resolved "https://registry.yarnpkg.com/jest-worker/-/jest-worker-26.5.0.tgz#87deee86dbbc5f98d9919e0dadf2c40e3152fa30" + integrity sha512-kTw66Dn4ZX7WpjZ7T/SUDgRhapFRKWmisVAF0Rv4Fu8SLFD7eLbqpLvbxVqYhSgaWa7I+bW7pHnbyfNsH6stug== + dependencies: + "@types/node" "*" + merge-stream "^2.0.0" + supports-color "^7.0.0" + +js-base64@^2.1.8: + version "2.5.1" + resolved "https://registry.yarnpkg.com/js-base64/-/js-base64-2.5.1.tgz#1efa39ef2c5f7980bb1784ade4a8af2de3291121" + +"js-tokens@^3.0.0 || ^4.0.0", js-tokens@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-4.0.0.tgz#19203fb59991df98e3a287050d4647cdeaf32499" + +js-yaml@^3.13.1: + version "3.13.1" + resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-3.13.1.tgz#aff151b30bfdfa8e49e05da22e7415e9dfa37847" + dependencies: + argparse "^1.0.7" + esprima "^4.0.0" + +js-yaml@^3.14.0: + version "3.14.0" + resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-3.14.0.tgz#a7a34170f26a21bb162424d8adacb4113a69e482" + integrity sha512-/4IbIeHcD9VMHFqDR/gQ7EdZdLimOvW2DdcxFjdyyZ9NsbS+ccrXqVWDtab/lRl5AlUqmpBx8EhPaWR+OtY17A== + dependencies: + argparse "^1.0.7" + esprima "^4.0.0" + +jsbn@~0.1.0: + version "0.1.1" + resolved "https://registry.yarnpkg.com/jsbn/-/jsbn-0.1.1.tgz#a5e654c2e5a2deb5f201d96cefbca80c0ef2f513" + +jsesc@^2.5.1: + version "2.5.2" + resolved "https://registry.yarnpkg.com/jsesc/-/jsesc-2.5.2.tgz#80564d2e483dacf6e8ef209650a67df3f0c283a4" + integrity sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA== + +jsesc@~0.5.0: + version "0.5.0" + resolved "https://registry.yarnpkg.com/jsesc/-/jsesc-0.5.0.tgz#e7dee66e35d6fc16f710fe91d5cf69f70f08911d" + +json-parse-better-errors@^1.0.1, json-parse-better-errors@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/json-parse-better-errors/-/json-parse-better-errors-1.0.2.tgz#bb867cfb3450e69107c131d1c514bab3dc8bcaa9" + +json-parse-even-better-errors@^2.3.0: + version "2.3.1" + resolved "https://registry.yarnpkg.com/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz#7c47805a94319928e05777405dc12e1f7a4ee02d" + integrity sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w== + +json-schema-traverse@^0.4.1: + version "0.4.1" + resolved "https://registry.yarnpkg.com/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz#69f6a87d9513ab8bb8fe63bdb0979c448e684660" + +json-schema-traverse@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz#ae7bcb3656ab77a73ba5c49bf654f38e6b6860e2" + integrity sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug== + +json-schema@0.2.3: + version "0.2.3" + resolved "https://registry.yarnpkg.com/json-schema/-/json-schema-0.2.3.tgz#b480c892e59a2f05954ce727bd3f2a4e882f9e13" + +json-stable-stringify-without-jsonify@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz#9db7b59496ad3f3cfef30a75142d2d930ad72651" + integrity sha1-nbe1lJatPzz+8wp1FC0tkwrXJlE= + +json-stable-stringify@~0.0.0: + version "0.0.1" + resolved "https://registry.yarnpkg.com/json-stable-stringify/-/json-stable-stringify-0.0.1.tgz#611c23e814db375527df851193db59dd2af27f45" + dependencies: + jsonify "~0.0.0" + +json-stringify-safe@~5.0.1: + version "5.0.1" + resolved "https://registry.yarnpkg.com/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz#1296a2d58fd45f19a0f6ce01d65701e2c735b6eb" + +json3@^3.3.3: + version "3.3.3" + resolved "https://registry.yarnpkg.com/json3/-/json3-3.3.3.tgz#7fc10e375fc5ae42c4705a5cc0aa6f62be305b81" + integrity sha512-c7/8mbUsKigAbLkD5B010BK4D9LZm7A1pNItkEwiUZRpIN66exu/e7YQWysGun+TRKaJp8MhemM+VkfWv42aCA== + +json5@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/json5/-/json5-1.0.1.tgz#779fb0018604fa854eacbf6252180d83543e3dbe" + dependencies: + minimist "^1.2.0" + +json5@^2.1.2: + version "2.1.3" + resolved "https://registry.yarnpkg.com/json5/-/json5-2.1.3.tgz#c9b0f7fa9233bfe5807fe66fcf3a5617ed597d43" + integrity sha512-KXPvOm8K9IJKFM0bmdn8QXh7udDh1g/giieX0NLCaMnb4hEiVFqnop2ImTXCc5e0/oHz3LTqmHGtExn5hfMkOA== + dependencies: + minimist "^1.2.5" + +jsonfile@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/jsonfile/-/jsonfile-4.0.0.tgz#8771aae0799b64076b76640fca058f9c10e33ecb" + optionalDependencies: + graceful-fs "^4.1.6" + +jsonify@~0.0.0: + version "0.0.0" + resolved "https://registry.yarnpkg.com/jsonify/-/jsonify-0.0.0.tgz#2c74b6ee41d93ca51b7b5aaee8f503631d252a73" + +jsonparse@0.0.5: + version "0.0.5" + resolved "https://registry.yarnpkg.com/jsonparse/-/jsonparse-0.0.5.tgz#330542ad3f0a654665b778f3eb2d9a9fa507ac64" + +jsonparse@^1.2.0: + version "1.3.1" + resolved "https://registry.yarnpkg.com/jsonparse/-/jsonparse-1.3.1.tgz#3f4dae4a91fac315f71062f8521cc239f1366280" + +jsprim@^1.2.2: + version "1.4.1" + resolved "https://registry.yarnpkg.com/jsprim/-/jsprim-1.4.1.tgz#313e66bc1e5cc06e438bc1b7499c2e5c56acb6a2" + dependencies: + assert-plus "1.0.0" + extsprintf "1.3.0" + json-schema "0.2.3" + verror "1.10.0" + +jstransform@^10.1.0: + version "10.1.0" + resolved "https://registry.yarnpkg.com/jstransform/-/jstransform-10.1.0.tgz#b4c49bf63f162c108b0348399a8737c713b0a83a" + dependencies: + base62 "0.1.1" + esprima-fb "13001.1001.0-dev-harmony-fb" + source-map "0.1.31" + +"jsx-ast-utils@^2.4.1 || ^3.0.0", jsx-ast-utils@^3.1.0: + version "3.2.0" + resolved "https://registry.yarnpkg.com/jsx-ast-utils/-/jsx-ast-utils-3.2.0.tgz#41108d2cec408c3453c1bbe8a4aae9e1e2bd8f82" + integrity sha512-EIsmt3O3ljsU6sot/J4E1zDRxfBNrhjyf/OKjlydwgEimQuznlM4Wv7U+ueONJMyEn1WRE0K8dhi3dVAXYT24Q== + dependencies: + array-includes "^3.1.2" + object.assign "^4.1.2" + +killable@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/killable/-/killable-1.0.1.tgz#4c8ce441187a061c7474fb87ca08e2a638194892" + integrity sha512-LzqtLKlUwirEUyl/nicirVmNiPvYs7l5n8wOPP7fyJVpUPkvCnW/vuiXGpylGUlnPDnB7311rARzAt3Mhswpjg== + +kind-of@^3.0.2, kind-of@^3.0.3, kind-of@^3.2.0, kind-of@^4.0.0, kind-of@^5.0.0, kind-of@^6.0.0, kind-of@^6.0.2, kind-of@^6.0.3: + version "6.0.3" + resolved "https://registry.yarnpkg.com/kind-of/-/kind-of-6.0.3.tgz#07c05034a6c349fa06e24fa35aa76db4580ce4dd" + integrity sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw== + +labeled-stream-splicer@^2.0.0: + version "2.0.2" + resolved "https://registry.yarnpkg.com/labeled-stream-splicer/-/labeled-stream-splicer-2.0.2.tgz#42a41a16abcd46fd046306cf4f2c3576fffb1c21" + dependencies: + inherits "^2.0.1" + stream-splicer "^2.0.0" + +language-subtag-registry@~0.3.2: + version "0.3.20" + resolved "https://registry.yarnpkg.com/language-subtag-registry/-/language-subtag-registry-0.3.20.tgz#a00a37121894f224f763268e431c55556b0c0755" + integrity sha512-KPMwROklF4tEx283Xw0pNKtfTj1gZ4UByp4EsIFWLgBavJltF4TiYPc39k06zSTsLzxTVXXDSpbwaQXaFB4Qeg== + +language-tags@^1.0.5: + version "1.0.5" + resolved "https://registry.yarnpkg.com/language-tags/-/language-tags-1.0.5.tgz#d321dbc4da30ba8bf3024e040fa5c14661f9193a" + integrity sha1-0yHbxNowuovzAk4ED6XBRmH5GTo= + dependencies: + language-subtag-registry "~0.3.2" + +last-call-webpack-plugin@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/last-call-webpack-plugin/-/last-call-webpack-plugin-3.0.0.tgz#9742df0e10e3cf46e5c0381c2de90d3a7a2d7555" + integrity sha512-7KI2l2GIZa9p2spzPIVZBYyNKkN+e/SQPpnjlTiPhdbDW3F86tdKKELxKpzJ5sgU19wQWsACULZmpTPYHeWO5w== + dependencies: + lodash "^4.17.5" + webpack-sources "^1.1.0" + +leven@^3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/leven/-/leven-3.1.0.tgz#77891de834064cccba82ae7842bb6b14a13ed7f2" + integrity sha512-qsda+H8jTaUaN/x5vzW2rzc+8Rw4TAQ/4KjB46IwK5VH+IlVeeeje/EoZRpiXvIqjFgK84QffqPztGI3VBLG1A== + +levenary@^1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/levenary/-/levenary-1.1.1.tgz#842a9ee98d2075aa7faeedbe32679e9205f46f77" + integrity sha512-mkAdOIt79FD6irqjYSs4rdbnlT5vRonMEvBVPVb3XmevfS8kgRXwfes0dhPdEtzTWD/1eNE/Bm/G1iRt6DcnQQ== + dependencies: + leven "^3.1.0" + +levn@^0.4.1: + version "0.4.1" + resolved "https://registry.yarnpkg.com/levn/-/levn-0.4.1.tgz#ae4562c007473b932a6200d403268dd2fffc6ade" + integrity sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ== + dependencies: + prelude-ls "^1.2.1" + type-check "~0.4.0" + +lines-and-columns@^1.1.6: + version "1.1.6" + resolved "https://registry.yarnpkg.com/lines-and-columns/-/lines-and-columns-1.1.6.tgz#1c00c743b433cd0a4e80758f7b64a57440d9ff00" + integrity sha1-HADHQ7QzzQpOgHWPe2SldEDZ/wA= + +load-json-file@^1.0.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/load-json-file/-/load-json-file-1.1.0.tgz#956905708d58b4bab4c2261b04f59f31c99374c0" + dependencies: + graceful-fs "^4.1.2" + parse-json "^2.2.0" + pify "^2.0.0" + pinkie-promise "^2.0.0" + strip-bom "^2.0.0" + +load-json-file@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/load-json-file/-/load-json-file-2.0.0.tgz#7947e42149af80d696cbf797bcaabcfe1fe29ca8" + dependencies: + graceful-fs "^4.1.2" + parse-json "^2.2.0" + pify "^2.0.0" + strip-bom "^3.0.0" + +loader-runner@^2.4.0: + version "2.4.0" + resolved "https://registry.yarnpkg.com/loader-runner/-/loader-runner-2.4.0.tgz#ed47066bfe534d7e84c4c7b9998c2a75607d9357" + integrity sha512-Jsmr89RcXGIwivFY21FcRrisYZfvLMTWx5kOLc+JTxtpBOG6xML0vzbc6SEQG2FO9/4Fc3wW4LVcB5DmGflaRw== + +loader-utils@^1.0.0, loader-utils@^1.2.3, loader-utils@^1.4.0: + version "1.4.0" + resolved "https://registry.yarnpkg.com/loader-utils/-/loader-utils-1.4.0.tgz#c579b5e34cb34b1a74edc6c1fb36bfa371d5a613" + integrity sha512-qH0WSMBtn/oHuwjy/NucEgbx5dbxxnxup9s4PVXJUDHZBQY+s0NWA9rJf53RBnQZxfch7euUui7hpoAPvALZdA== + dependencies: + big.js "^5.2.2" + emojis-list "^3.0.0" + json5 "^1.0.1" + +loader-utils@^1.1.0: + version "1.2.3" + resolved "https://registry.yarnpkg.com/loader-utils/-/loader-utils-1.2.3.tgz#1ff5dc6911c9f0a062531a4c04b609406108c2c7" + dependencies: + big.js "^5.2.2" + emojis-list "^2.0.0" + json5 "^1.0.1" + +loader-utils@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/loader-utils/-/loader-utils-2.0.0.tgz#e4cace5b816d425a166b5f097e10cd12b36064b0" + integrity sha512-rP4F0h2RaWSvPEkD7BLDFQnvSf+nK+wr3ESUjNTyAGobqrijmW92zc+SO6d4p4B1wh7+B/Jg1mkQe5NYUEHtHQ== + dependencies: + big.js "^5.2.2" + emojis-list "^3.0.0" + json5 "^2.1.2" + +locate-path@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/locate-path/-/locate-path-2.0.0.tgz#2b568b265eec944c6d9c0de9c3dbbbca0354cd8e" + dependencies: + p-locate "^2.0.0" + path-exists "^3.0.0" + +locate-path@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/locate-path/-/locate-path-3.0.0.tgz#dbec3b3ab759758071b58fe59fc41871af21400e" + integrity sha512-7AO748wWnIhNqAuaty2ZWHkQHRSNfPVIsPIfwEOWO22AmaoVrWavlOcMR5nzTLNYvp36X220/maaRsrec1G65A== + dependencies: + p-locate "^3.0.0" + path-exists "^3.0.0" + +locate-path@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/locate-path/-/locate-path-5.0.0.tgz#1afba396afd676a6d42504d0a67a3a7eb9f62aa0" + integrity sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g== + dependencies: + p-locate "^4.1.0" + +lodash-es@^4.17.20: + version "4.17.20" + resolved "https://registry.yarnpkg.com/lodash-es/-/lodash-es-4.17.20.tgz#29f6332eefc60e849f869c264bc71126ad61e8f7" + integrity sha512-JD1COMZsq8maT6mnuz1UMV0jvYD0E0aUsSOdrr1/nAG3dhqQXwRRgeW0cSqH1U43INKcqxaiVIQNOUDld7gRDA== + +lodash._getnative@^3.0.0: + version "3.9.1" + resolved "https://registry.yarnpkg.com/lodash._getnative/-/lodash._getnative-3.9.1.tgz#570bc7dede46d61cdcde687d65d3eecbaa3aaff5" + +lodash._reinterpolate@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/lodash._reinterpolate/-/lodash._reinterpolate-3.0.0.tgz#0ccf2d89166af03b3663c796538b75ac6e114d9d" + +lodash.get@^4.0: + version "4.4.2" + resolved "https://registry.yarnpkg.com/lodash.get/-/lodash.get-4.4.2.tgz#2d177f652fa31e939b4438d5341499dfa3825e99" + integrity sha1-LRd/ZS+jHpObRDjVNBSZ36OCXpk= + +lodash.has@^4.0: + version "4.5.2" + resolved "https://registry.yarnpkg.com/lodash.has/-/lodash.has-4.5.2.tgz#d19f4dc1095058cccbe2b0cdf4ee0fe4aa37c862" + integrity sha1-0Z9NwQlQWMzL4rDN9O4P5Ko3yGI= + +lodash.isarguments@^3.0.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/lodash.isarguments/-/lodash.isarguments-3.1.0.tgz#2f573d85c6a24289ff00663b491c1d338ff3458a" + +lodash.isarray@^3.0.0: + version "3.0.4" + resolved "https://registry.yarnpkg.com/lodash.isarray/-/lodash.isarray-3.0.4.tgz#79e4eb88c36a8122af86f844aa9bcd851b5fbb55" + +lodash.keys@^3.1.2: + version "3.1.2" + resolved "https://registry.yarnpkg.com/lodash.keys/-/lodash.keys-3.1.2.tgz#4dbc0472b156be50a0b286855d1bd0b0c656098a" + dependencies: + lodash._getnative "^3.0.0" + lodash.isarguments "^3.0.0" + lodash.isarray "^3.0.0" + +lodash.memoize@^4.1.2: + version "4.1.2" + resolved "https://registry.yarnpkg.com/lodash.memoize/-/lodash.memoize-4.1.2.tgz#bcc6c49a42a2840ed997f323eada5ecd182e0bfe" + +lodash.memoize@~3.0.3: + version "3.0.4" + resolved "https://registry.yarnpkg.com/lodash.memoize/-/lodash.memoize-3.0.4.tgz#2dcbd2c287cbc0a55cc42328bd0c736150d53e3f" + +lodash.template@^4.5.0: + version "4.5.0" + resolved "https://registry.yarnpkg.com/lodash.template/-/lodash.template-4.5.0.tgz#f976195cf3f347d0d5f52483569fe8031ccce8ab" + integrity sha512-84vYFxIkmidUiFxidA/KjjH9pAycqW+h980j7Fuz5qxRtO9pgB7MDFTdys1N7A5mcucRiDyEq4fusljItR1T/A== + dependencies: + lodash._reinterpolate "^3.0.0" + lodash.templatesettings "^4.0.0" + +lodash.templatesettings@^4.0.0: + version "4.2.0" + resolved "https://registry.yarnpkg.com/lodash.templatesettings/-/lodash.templatesettings-4.2.0.tgz#e481310f049d3cf6d47e912ad09313b154f0fb33" + dependencies: + lodash._reinterpolate "^3.0.0" + +lodash.uniq@^4.5.0: + version "4.5.0" + resolved "https://registry.yarnpkg.com/lodash.uniq/-/lodash.uniq-4.5.0.tgz#d0225373aeb652adc1bc82e4945339a842754773" + +lodash@^4.0.0, lodash@^4.17.11, lodash@^4.17.14, lodash@^4.17.15, lodash@^4.17.19, lodash@^4.17.20, lodash@^4.17.4, lodash@^4.17.5, lodash@~4.17.10: + version "4.17.20" + resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.20.tgz#b44a9b6297bcb698f1c51a3545a2b3b368d59c52" + integrity sha512-PlhdFcillOINfeV7Ni6oF1TAEayyZBoZ8bcshTHqOYJYlrqzRK5hagpagky5o4HfCzzd1TRkXPMFq6cKk9rGmA== + +loglevel@^1.6.8: + version "1.7.0" + resolved "https://registry.yarnpkg.com/loglevel/-/loglevel-1.7.0.tgz#728166855a740d59d38db01cf46f042caa041bb0" + integrity sha512-i2sY04nal5jDcagM3FMfG++T69GEEM8CYuOfeOIvmXzOIcwE9a/CJPR0MFM97pYMj/u10lzz7/zd7+qwhrBTqQ== + +loose-envify@^1.0.0, loose-envify@^1.1.0, loose-envify@^1.3.1, loose-envify@^1.4.0: + version "1.4.0" + resolved "https://registry.yarnpkg.com/loose-envify/-/loose-envify-1.4.0.tgz#71ee51fa7be4caec1a63839f7e682d8132d30caf" + dependencies: + js-tokens "^3.0.0 || ^4.0.0" + +loud-rejection@^1.0.0: + version "1.6.0" + resolved "https://registry.yarnpkg.com/loud-rejection/-/loud-rejection-1.6.0.tgz#5b46f80147edee578870f086d04821cf998e551f" + dependencies: + currently-unhandled "^0.4.1" + signal-exit "^3.0.0" + +lru-cache@^4.0.1: + version "4.1.5" + resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-4.1.5.tgz#8bbe50ea85bed59bc9e33dcab8235ee9bcf443cd" + dependencies: + pseudomap "^1.0.2" + yallist "^2.1.2" + +lru-cache@^5.1.1: + version "5.1.1" + resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-5.1.1.tgz#1da27e6710271947695daf6848e847f01d84b920" + integrity sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w== + dependencies: + yallist "^3.0.2" + +lru-cache@^6.0.0: + version "6.0.0" + resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-6.0.0.tgz#6d6fe6570ebd96aaf90fcad1dafa3b2566db3a94" + integrity sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA== + dependencies: + yallist "^4.0.0" + +make-cancellable-promise@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/make-cancellable-promise/-/make-cancellable-promise-1.0.0.tgz#826214115b0827ca7a45ba204df7c31546243870" + integrity sha512-+YO6Grg2uy/z8Mv3uV90OP6yAUHIF43YGgEFbejmBrK9VWFsVO6DvzFMcopXr9wCNg3/QIltIKiSCROC7zFB2g== + +make-dir@^2.0.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/make-dir/-/make-dir-2.1.0.tgz#5f0310e18b8be898cc07009295a30ae41e91e6f5" + integrity sha512-LS9X+dc8KLxXCb8dni79fLIIUA5VyZoyjSMCwTluaXA0o27cCK0bhXkpgw+sTXVpPy/lSO57ilRixqk0vDmtRA== + dependencies: + pify "^4.0.1" + semver "^5.6.0" + +make-dir@^3.0.2: + version "3.1.0" + resolved "https://registry.yarnpkg.com/make-dir/-/make-dir-3.1.0.tgz#415e967046b3a7f1d185277d84aa58203726a13f" + integrity sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw== + dependencies: + semver "^6.0.0" + +make-event-props@^1.1.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/make-event-props/-/make-event-props-1.2.0.tgz#96b87d88919533b8f8934b58b4c3d5679459a0cf" + +map-cache@^0.2.2: + version "0.2.2" + resolved "https://registry.yarnpkg.com/map-cache/-/map-cache-0.2.2.tgz#c32abd0bd6525d9b051645bb4f26ac5dc98a0dbf" + +map-obj@^1.0.0, map-obj@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/map-obj/-/map-obj-1.0.1.tgz#d933ceb9205d82bdcf4886f6742bdc2b4dea146d" + +map-visit@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/map-visit/-/map-visit-1.0.0.tgz#ecdca8f13144e660f1b5bd41f12f3479d98dfb8f" + dependencies: + object-visit "^1.0.0" + +marked@^1.2.7: + version "1.2.7" + resolved "https://registry.yarnpkg.com/marked/-/marked-1.2.7.tgz#6e14b595581d2319cdcf033a24caaf41455a01fb" + integrity sha512-No11hFYcXr/zkBvL6qFmAp1z6BKY3zqLMHny/JN/ey+al7qwCM2+CMBL9BOgqMxZU36fz4cCWfn2poWIf7QRXA== + +mathjax@^2.7.9: + version "2.7.9" + resolved "https://registry.yarnpkg.com/mathjax/-/mathjax-2.7.9.tgz#d6b67955c173e7d719fcb2fc0288662884eb7d3d" + integrity sha512-NOGEDTIM9+MrsqnjPEjVGNx4q0GQxqm61yQwSK+/5S59i26wId5IC5gNu9/bu8+CCVl5p9G2IHcAl/wJa+5+BQ== + +md5.js@^1.3.4: + version "1.3.5" + resolved "https://registry.yarnpkg.com/md5.js/-/md5.js-1.3.5.tgz#b5d07b8e3216e3e27cd728d72f70d1e6a342005f" + dependencies: + hash-base "^3.0.0" + inherits "^2.0.1" + safe-buffer "^5.1.2" + +mdn-data@2.0.4: + version "2.0.4" + resolved "https://registry.yarnpkg.com/mdn-data/-/mdn-data-2.0.4.tgz#699b3c38ac6f1d728091a64650b65d388502fd5b" + +mdn-data@~1.1.0: + version "1.1.4" + resolved "https://registry.yarnpkg.com/mdn-data/-/mdn-data-1.1.4.tgz#50b5d4ffc4575276573c4eedb8780812a8419f01" + +media-typer@0.3.0: + version "0.3.0" + resolved "https://registry.yarnpkg.com/media-typer/-/media-typer-0.3.0.tgz#8710d7af0aa626f8fffa1ce00168545263255748" + +memoize-one@^5.1.1: + version "5.1.1" + resolved "https://registry.yarnpkg.com/memoize-one/-/memoize-one-5.1.1.tgz#047b6e3199b508eaec03504de71229b8eb1d75c0" + integrity sha512-HKeeBpWvqiVJD57ZUAsJNm71eHTykffzcLZVYWiVfQeI1rJtuEaS7hQiEpWfVVk18donPwJEcFKIkCmPJNOhHA== + +memory-fs@^0.4.1: + version "0.4.1" + resolved "https://registry.yarnpkg.com/memory-fs/-/memory-fs-0.4.1.tgz#3a9a20b8462523e447cfbc7e8bb80ed667bfc552" + integrity sha1-OpoguEYlI+RHz7x+i7gO1me/xVI= + dependencies: + errno "^0.1.3" + readable-stream "^2.0.1" + +memory-fs@^0.5.0: + version "0.5.0" + resolved "https://registry.yarnpkg.com/memory-fs/-/memory-fs-0.5.0.tgz#324c01288b88652966d161db77838720845a8e3c" + integrity sha512-jA0rdU5KoQMC0e6ppoNRtpp6vjFq6+NY7r8hywnC7V+1Xj/MtHwGIbB1QaK/dunyjWteJzmkpd7ooeWg10T7GA== + dependencies: + errno "^0.1.3" + readable-stream "^2.0.1" + +meow@^3.7.0: + version "3.7.0" + resolved "https://registry.yarnpkg.com/meow/-/meow-3.7.0.tgz#72cb668b425228290abbfa856892587308a801fb" + dependencies: + camelcase-keys "^2.0.0" + decamelize "^1.1.2" + loud-rejection "^1.0.0" + map-obj "^1.0.1" + minimist "^1.1.3" + normalize-package-data "^2.3.4" + object-assign "^4.0.1" + read-pkg-up "^1.0.1" + redent "^1.0.0" + trim-newlines "^1.0.0" + +merge-class-names@^1.1.1: + version "1.2.0" + resolved "https://registry.yarnpkg.com/merge-class-names/-/merge-class-names-1.2.0.tgz#cb30ecfc3bdbd96b6f76d0a98777907e5fbb3462" + +merge-descriptors@1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/merge-descriptors/-/merge-descriptors-1.0.1.tgz#b00aaa556dd8b44568150ec9d1b953f3f90cbb61" + +merge-stream@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/merge-stream/-/merge-stream-2.0.0.tgz#52823629a14dd00c9770fb6ad47dc6310f2c1f60" + integrity sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w== + +methods@~1.1.2: + version "1.1.2" + resolved "https://registry.yarnpkg.com/methods/-/methods-1.1.2.tgz#5529a4d67654134edcc5266656835b0f851afcee" + +micromatch@^3.0.4, micromatch@^3.1.10, micromatch@^3.1.4: + version "3.1.10" + resolved "https://registry.yarnpkg.com/micromatch/-/micromatch-3.1.10.tgz#70859bc95c9840952f359a068a3fc49f9ecfac23" + dependencies: + arr-diff "^4.0.0" + array-unique "^0.3.2" + braces "^2.3.1" + define-property "^2.0.2" + extend-shallow "^3.0.2" + extglob "^2.0.4" + fragment-cache "^0.2.1" + kind-of "^6.0.2" + nanomatch "^1.2.9" + object.pick "^1.3.0" + regex-not "^1.0.0" + snapdragon "^0.8.1" + to-regex "^3.0.2" + +micromatch@^4.0.0: + version "4.0.2" + resolved "https://registry.yarnpkg.com/micromatch/-/micromatch-4.0.2.tgz#4fcb0999bf9fbc2fcbdd212f6d629b9a56c39259" + integrity sha512-y7FpHSbMUMoyPbYUSzO6PaZ6FyRnQOpHuKwbo1G+Knck95XVU4QAiKdGEnj5wwoS7PlOgthX/09u5iFJ+aYf5Q== + dependencies: + braces "^3.0.1" + picomatch "^2.0.5" + +miller-rabin@^4.0.0: + version "4.0.1" + resolved "https://registry.yarnpkg.com/miller-rabin/-/miller-rabin-4.0.1.tgz#f080351c865b0dc562a8462966daa53543c78a4d" + dependencies: + bn.js "^4.0.0" + brorand "^1.0.1" + +mime-db@1.40.0, "mime-db@>= 1.40.0 < 2": + version "1.40.0" + resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.40.0.tgz#a65057e998db090f732a68f6c276d387d4126c32" + +mime-types@^2.1.12, mime-types@~2.1.17, mime-types@~2.1.19, mime-types@~2.1.24: + version "2.1.24" + resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.24.tgz#b6f8d0b3e951efb77dedeca194cff6d16f676f81" + dependencies: + mime-db "1.40.0" + +mime@1.6.0: + version "1.6.0" + resolved "https://registry.yarnpkg.com/mime/-/mime-1.6.0.tgz#32cd9e5c64553bd58d19a568af452acff04981b1" + +mime@^2.4.4: + version "2.4.6" + resolved "https://registry.yarnpkg.com/mime/-/mime-2.4.6.tgz#e5b407c90db442f2beb5b162373d07b69affa4d1" + integrity sha512-RZKhC3EmpBchfTGBVb8fb+RL2cWyw/32lshnsETttkBAyAUXSGHxbEJWWRXc751DrIxG1q04b8QwMbAwkRPpUA== + +mini-css-extract-plugin@^0.9.0: + version "0.9.0" + resolved "https://registry.yarnpkg.com/mini-css-extract-plugin/-/mini-css-extract-plugin-0.9.0.tgz#47f2cf07aa165ab35733b1fc97d4c46c0564339e" + integrity sha512-lp3GeY7ygcgAmVIcRPBVhIkf8Us7FZjA+ILpal44qLdSu11wmjKQ3d9k15lfD7pO4esu9eUIAW7qiYIBppv40A== + dependencies: + loader-utils "^1.1.0" + normalize-url "1.9.1" + schema-utils "^1.0.0" + webpack-sources "^1.1.0" + +minimalistic-assert@^1.0.0, minimalistic-assert@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz#2e194de044626d4a10e7f7fbc00ce73e83e4d5c7" + +minimalistic-crypto-utils@^1.0.0, minimalistic-crypto-utils@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/minimalistic-crypto-utils/-/minimalistic-crypto-utils-1.0.1.tgz#f6c00c1c0b082246e5c4d99dfb8c7c083b2b582a" + +"minimatch@2 || 3", minimatch@^3.0.4, minimatch@~3.0.2: + version "3.0.4" + resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.0.4.tgz#5166e286457f03306064be5497e8dbb0c3d32083" + dependencies: + brace-expansion "^1.1.7" + +minimist@0.0.8, minimist@^1.1.0, minimist@^1.1.1, minimist@^1.1.3, minimist@^1.2.0, minimist@^1.2.3, minimist@^1.2.5: + version "1.2.5" + resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.5.tgz#67d66014b66a6a8aaa0c083c5fd58df4e4e97602" + integrity sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw== + +minipass-collect@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/minipass-collect/-/minipass-collect-1.0.2.tgz#22b813bf745dc6edba2576b940022ad6edc8c617" + integrity sha512-6T6lH0H8OG9kITm/Jm6tdooIbogG9e0tLgpY6mphXSm/A9u8Nq1ryBG+Qspiub9LjWlBPsPS3tWQ/Botq4FdxA== + dependencies: + minipass "^3.0.0" + +minipass-flush@^1.0.5: + version "1.0.5" + resolved "https://registry.yarnpkg.com/minipass-flush/-/minipass-flush-1.0.5.tgz#82e7135d7e89a50ffe64610a787953c4c4cbb373" + integrity sha512-JmQSYYpPUqX5Jyn1mXaRwOda1uQ8HP5KAT/oDSLCzt1BYRhQU0/hDtsB1ufZfEEzMZ9aAVmsBw8+FWsIXlClWw== + dependencies: + minipass "^3.0.0" + +minipass-pipeline@^1.2.2: + version "1.2.4" + resolved "https://registry.yarnpkg.com/minipass-pipeline/-/minipass-pipeline-1.2.4.tgz#68472f79711c084657c067c5c6ad93cddea8214c" + integrity sha512-xuIq7cIOt09RPRJ19gdi4b+RiNvDFYe5JH+ggNvBqGqpQXcru3PcRmOZuHBKWK1Txf9+cQ+HMVN4d6z46LZP7A== + dependencies: + minipass "^3.0.0" + +minipass@^2.2.1, minipass@^2.3.5: + version "2.4.0" + resolved "https://registry.yarnpkg.com/minipass/-/minipass-2.4.0.tgz#38f0af94f42fb6f34d3d7d82a90e2c99cd3ff485" + dependencies: + safe-buffer "^5.1.2" + yallist "^3.0.0" + +minipass@^3.0.0, minipass@^3.1.1: + version "3.1.3" + resolved "https://registry.yarnpkg.com/minipass/-/minipass-3.1.3.tgz#7d42ff1f39635482e15f9cdb53184deebd5815fd" + integrity sha512-Mgd2GdMVzY+x3IJ+oHnVM+KG3lA5c8tnabyJKmHSaG2kAGpudxuOf8ToDkhumF7UzME7DecbQE9uOZhNm7PuJg== + dependencies: + yallist "^4.0.0" + +minizlib@^1.2.1: + version "1.2.1" + resolved "https://registry.yarnpkg.com/minizlib/-/minizlib-1.2.1.tgz#dd27ea6136243c7c880684e8672bb3a45fd9b614" + dependencies: + minipass "^2.2.1" + +minizlib@^2.1.1: + version "2.1.2" + resolved "https://registry.yarnpkg.com/minizlib/-/minizlib-2.1.2.tgz#e90d3466ba209b932451508a11ce3d3632145931" + integrity sha512-bAxsR8BVfj60DWXHE3u30oHzfl4G7khkSuPW+qvpd7jFRHm7dLxOjUk1EHACJ/hxLY8phGJ0YhYHZo7jil7Qdg== + dependencies: + minipass "^3.0.0" + yallist "^4.0.0" + +mississippi@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/mississippi/-/mississippi-3.0.0.tgz#ea0a3291f97e0b5e8776b363d5f0a12d94c67022" + integrity sha512-x471SsVjUtBRtcvd4BzKE9kFC+/2TeWgKCgw0bZcw1b9l2X3QX5vCWgF+KaZaYm87Ss//rHnWryupDrgLvmSkA== + dependencies: + concat-stream "^1.5.0" + duplexify "^3.4.2" + end-of-stream "^1.1.0" + flush-write-stream "^1.0.0" + from2 "^2.1.0" + parallel-transform "^1.1.0" + pump "^3.0.0" + pumpify "^1.3.3" + stream-each "^1.1.0" + through2 "^2.0.0" + +mixin-deep@^1.2.0: + version "1.3.2" + resolved "https://registry.yarnpkg.com/mixin-deep/-/mixin-deep-1.3.2.tgz#1120b43dc359a785dce65b55b82e257ccf479566" + dependencies: + for-in "^1.0.2" + is-extendable "^1.0.1" + +mkdirp-classic@^0.5.2: + version "0.5.3" + resolved "https://registry.yarnpkg.com/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz#fa10c9115cc6d8865be221ba47ee9bed78601113" + integrity sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A== + +"mkdirp@>=0.5 0", mkdirp@^0.5.0, mkdirp@^0.5.1, mkdirp@~0.5.1: + version "0.5.1" + resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-0.5.1.tgz#30057438eac6cf7f8c4767f38648d6697d75c903" + dependencies: + minimist "0.0.8" + +mkdirp@^0.5, mkdirp@^0.5.3, mkdirp@^0.5.5: + version "0.5.5" + resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-0.5.5.tgz#d91cefd62d1436ca0f41620e251288d420099def" + integrity sha512-NKmAlESf6jMGym1++R0Ra7wvhV+wFW63FaSOFPwRahvea0gMUcGUhVeAg/0BC0wiv9ih5NYPB1Wn1UEI1/L+xQ== + dependencies: + minimist "^1.2.5" + +mkdirp@^1.0.3, mkdirp@^1.0.4: + version "1.0.4" + resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-1.0.4.tgz#3eb5ed62622756d79a5f0e2a221dfebad75c2f7e" + integrity sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw== + +module-deps@^6.2.3: + version "6.2.3" + resolved "https://registry.yarnpkg.com/module-deps/-/module-deps-6.2.3.tgz#15490bc02af4b56cf62299c7c17cba32d71a96ee" + integrity sha512-fg7OZaQBcL4/L+AK5f4iVqf9OMbCclXfy/znXRxTVhJSeW5AIlS9AwheYwDaXM3lVW7OBeaeUEY3gbaC6cLlSA== + dependencies: + JSONStream "^1.0.3" + browser-resolve "^2.0.0" + cached-path-relative "^1.0.2" + concat-stream "~1.6.0" + defined "^1.0.0" + detective "^5.2.0" + duplexer2 "^0.1.2" + inherits "^2.0.1" + parents "^1.0.0" + readable-stream "^2.0.2" + resolve "^1.4.0" + stream-combiner2 "^1.1.1" + subarg "^1.0.0" + through2 "^2.0.0" + xtend "^4.0.0" + +moment-timezone@^0.5.32: + version "0.5.32" + resolved "https://registry.yarnpkg.com/moment-timezone/-/moment-timezone-0.5.32.tgz#db7677cc3cc680fd30303ebd90b0da1ca0dfecc2" + integrity sha512-Z8QNyuQHQAmWucp8Knmgei8YNo28aLjJq6Ma+jy1ZSpSk5nyfRT8xgUbSQvD2+2UajISfenndwvFuH3NGS+nvA== + dependencies: + moment ">= 2.9.0" + +"moment@>= 2.9.0": + version "2.24.0" + resolved "https://registry.yarnpkg.com/moment/-/moment-2.24.0.tgz#0d055d53f5052aa653c9f6eb68bb5d12bf5c2b5b" + +moment@^2.29.1: + version "2.29.1" + resolved "https://registry.yarnpkg.com/moment/-/moment-2.29.1.tgz#b2be769fa31940be9eeea6469c075e35006fa3d3" + integrity sha512-kHmoybcPV8Sqy59DwNDY3Jefr64lK/by/da0ViFcuA4DH0vQg5Q6Ze5VimxkfQNSC+Mls/Kx53s7TjP1RhFEDQ== + +move-concurrently@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/move-concurrently/-/move-concurrently-1.0.1.tgz#be2c005fda32e0b29af1f05d7c4b33214c701f92" + dependencies: + aproba "^1.1.1" + copy-concurrently "^1.0.0" + fs-write-stream-atomic "^1.0.8" + mkdirp "^0.5.1" + rimraf "^2.5.4" + run-queue "^1.0.3" + +ms@2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/ms/-/ms-2.0.0.tgz#5608aeadfc00be6c2901df5f9861788de0d597c8" + +ms@2.1.1: + version "2.1.1" + resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.1.tgz#30a5864eb3ebb0a66f2ebe6d727af06a09d86e0a" + +ms@2.1.2, ms@^2.1.1: + version "2.1.2" + resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.2.tgz#d09d1f357b443f493382a8eb3ccd183872ae6009" + integrity sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w== + +multicast-dns-service-types@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/multicast-dns-service-types/-/multicast-dns-service-types-1.1.0.tgz#899f11d9686e5e05cb91b35d5f0e63b773cfc901" + +multicast-dns@^6.0.1: + version "6.2.3" + resolved "https://registry.yarnpkg.com/multicast-dns/-/multicast-dns-6.2.3.tgz#a0ec7bd9055c4282f790c3c82f4e28db3b31b229" + dependencies: + dns-packet "^1.3.1" + thunky "^1.0.2" + +nan@^2.12.1, nan@^2.13.2: + version "2.14.0" + resolved "https://registry.yarnpkg.com/nan/-/nan-2.14.0.tgz#7818f722027b2459a86f0295d434d1fc2336c52c" + +nanomatch@^1.2.9: + version "1.2.13" + resolved "https://registry.yarnpkg.com/nanomatch/-/nanomatch-1.2.13.tgz#b87a8aa4fc0de8fe6be88895b38983ff265bd119" + dependencies: + arr-diff "^4.0.0" + array-unique "^0.3.2" + define-property "^2.0.2" + extend-shallow "^3.0.2" + fragment-cache "^0.2.1" + is-windows "^1.0.2" + kind-of "^6.0.2" + object.pick "^1.3.0" + regex-not "^1.0.0" + snapdragon "^0.8.1" + to-regex "^3.0.1" + +native-promise-only@0.8.1: + version "0.8.1" + resolved "https://registry.yarnpkg.com/native-promise-only/-/native-promise-only-0.8.1.tgz#20a318c30cb45f71fe7adfbf7b21c99c1472ef11" + +natural-compare@^1.4.0: + version "1.4.0" + resolved "https://registry.yarnpkg.com/natural-compare/-/natural-compare-1.4.0.tgz#4abebfeed7541f2c27acfb29bdbbd15c8d5ba4f7" + +needle@^2.2.1: + version "2.4.0" + resolved "https://registry.yarnpkg.com/needle/-/needle-2.4.0.tgz#6833e74975c444642590e15a750288c5f939b57c" + dependencies: + debug "^3.2.6" + iconv-lite "^0.4.4" + sax "^1.2.4" + +negotiator@0.6.2: + version "0.6.2" + resolved "https://registry.yarnpkg.com/negotiator/-/negotiator-0.6.2.tgz#feacf7ccf525a77ae9634436a64883ffeca346fb" + +neo-async@^2.5.0: + version "2.6.1" + resolved "https://registry.yarnpkg.com/neo-async/-/neo-async-2.6.1.tgz#ac27ada66167fa8849a6addd837f6b189ad2081c" + +neo-async@^2.6.1: + version "2.6.2" + resolved "https://registry.yarnpkg.com/neo-async/-/neo-async-2.6.2.tgz#b4aafb93e3aeb2d8174ca53cf163ab7d7308305f" + integrity sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw== + +nice-try@^1.0.4: + version "1.0.5" + resolved "https://registry.yarnpkg.com/nice-try/-/nice-try-1.0.5.tgz#a3378a7696ce7d223e88fc9b764bd7ef1089e366" + integrity sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ== + +node-ensure@^0.0.0: + version "0.0.0" + resolved "https://registry.yarnpkg.com/node-ensure/-/node-ensure-0.0.0.tgz#ecae764150de99861ec5c810fd5d096b183932a7" + integrity sha1-7K52QVDemYYexcgQ/V0Jaxg5Mqc= + +node-forge@^0.10.0: + version "0.10.0" + resolved "https://registry.yarnpkg.com/node-forge/-/node-forge-0.10.0.tgz#32dea2afb3e9926f02ee5ce8794902691a676bf3" + integrity sha512-PPmu8eEeG9saEUvI97fm4OYxXVB6bFvyNTyiUOBichBpFG8A1Ljw3bY62+5oOjDEMHRnd0Y7HQ+x7uzxOzC6JA== + +node-gyp@^3.8.0: + version "3.8.0" + resolved "https://registry.yarnpkg.com/node-gyp/-/node-gyp-3.8.0.tgz#540304261c330e80d0d5edce253a68cb3964218c" + dependencies: + fstream "^1.0.0" + glob "^7.0.3" + graceful-fs "^4.1.2" + mkdirp "^0.5.0" + nopt "2 || 3" + npmlog "0 || 1 || 2 || 3 || 4" + osenv "0" + request "^2.87.0" + rimraf "2" + semver "~5.3.0" + tar "^2.0.0" + which "1" + +node-libs-browser@^2.2.1: + version "2.2.1" + resolved "https://registry.yarnpkg.com/node-libs-browser/-/node-libs-browser-2.2.1.tgz#b64f513d18338625f90346d27b0d235e631f6425" + integrity sha512-h/zcD8H9kaDZ9ALUWwlBUDo6TKF8a7qBSCSEGfjTVIYeqsioSKaAX+BN7NgiMGp6iSIXZ3PxgCu8KS3b71YK5Q== + dependencies: + assert "^1.1.1" + browserify-zlib "^0.2.0" + buffer "^4.3.0" + console-browserify "^1.1.0" + constants-browserify "^1.0.0" + crypto-browserify "^3.11.0" + domain-browser "^1.1.1" + events "^3.0.0" + https-browserify "^1.0.0" + os-browserify "^0.3.0" + path-browserify "0.0.1" + process "^0.11.10" + punycode "^1.2.4" + querystring-es3 "^0.2.0" + readable-stream "^2.3.3" + stream-browserify "^2.0.1" + stream-http "^2.7.2" + string_decoder "^1.0.0" + timers-browserify "^2.0.4" + tty-browserify "0.0.0" + url "^0.11.0" + util "^0.11.0" + vm-browserify "^1.0.1" + +node-pre-gyp@^0.12.0: + version "0.12.0" + resolved "https://registry.yarnpkg.com/node-pre-gyp/-/node-pre-gyp-0.12.0.tgz#39ba4bb1439da030295f899e3b520b7785766149" + dependencies: + detect-libc "^1.0.2" + mkdirp "^0.5.1" + needle "^2.2.1" + nopt "^4.0.1" + npm-packlist "^1.1.6" + npmlog "^4.0.2" + rc "^1.2.7" + rimraf "^2.6.1" + semver "^5.3.0" + tar "^4" + +node-releases@^1.1.25: + version "1.1.28" + resolved "https://registry.yarnpkg.com/node-releases/-/node-releases-1.1.28.tgz#503c3c70d0e4732b84e7aaa2925fbdde10482d4a" + dependencies: + semver "^5.3.0" + +node-releases@^1.1.61: + version "1.1.61" + resolved "https://registry.yarnpkg.com/node-releases/-/node-releases-1.1.61.tgz#707b0fca9ce4e11783612ba4a2fcba09047af16e" + integrity sha512-DD5vebQLg8jLCOzwupn954fbIiZht05DAZs0k2u8NStSe6h9XdsuIQL8hSRKYiU8WUQRznmSDrKGbv3ObOmC7g== + +node-sass@^4.14.1: + version "4.14.1" + resolved "https://registry.yarnpkg.com/node-sass/-/node-sass-4.14.1.tgz#99c87ec2efb7047ed638fb4c9db7f3a42e2217b5" + integrity sha512-sjCuOlvGyCJS40R8BscF5vhVlQjNN069NtQ1gSxyK1u9iqvn6tf7O1R4GNowVZfiZUCRt5MmMs1xd+4V/7Yr0g== + dependencies: + async-foreach "^0.1.3" + chalk "^1.1.1" + cross-spawn "^3.0.0" + gaze "^1.0.0" + get-stdin "^4.0.1" + glob "^7.0.3" + in-publish "^2.0.0" + lodash "^4.17.15" + meow "^3.7.0" + mkdirp "^0.5.1" + nan "^2.13.2" + node-gyp "^3.8.0" + npmlog "^4.0.0" + request "^2.88.0" + sass-graph "2.2.5" + stdout-stream "^1.4.0" + "true-case-path" "^1.0.2" + +"nopt@2 || 3": + version "3.0.6" + resolved "https://registry.yarnpkg.com/nopt/-/nopt-3.0.6.tgz#c6465dbf08abcd4db359317f79ac68a646b28ff9" + dependencies: + abbrev "1" + +nopt@^4.0.1: + version "4.0.1" + resolved "https://registry.yarnpkg.com/nopt/-/nopt-4.0.1.tgz#d0d4685afd5415193c8c7505602d0d17cd64474d" + dependencies: + abbrev "1" + osenv "^0.1.4" + +normalize-package-data@^2.3.2, normalize-package-data@^2.3.4: + version "2.5.0" + resolved "https://registry.yarnpkg.com/normalize-package-data/-/normalize-package-data-2.5.0.tgz#e66db1838b200c1dfc233225d12cb36520e234a8" + dependencies: + hosted-git-info "^2.1.4" + resolve "^1.10.0" + semver "2 || 3 || 4 || 5" + validate-npm-package-license "^3.0.1" + +normalize-path@^2.1.1: + version "2.1.1" + resolved "https://registry.yarnpkg.com/normalize-path/-/normalize-path-2.1.1.tgz#1ab28b556e198363a8c1a6f7e6fa20137fe6aed9" + dependencies: + remove-trailing-separator "^1.0.1" + +normalize-path@^3.0.0, normalize-path@~3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/normalize-path/-/normalize-path-3.0.0.tgz#0dcd69ff23a1c9b11fd0978316644a0388216a65" + +normalize-range@^0.1.2: + version "0.1.2" + resolved "https://registry.yarnpkg.com/normalize-range/-/normalize-range-0.1.2.tgz#2d10c06bdfd312ea9777695a4d28439456b75942" + +normalize-url@1.9.1, normalize-url@^1.0.0: + version "1.9.1" + resolved "https://registry.yarnpkg.com/normalize-url/-/normalize-url-1.9.1.tgz#2cc0d66b31ea23036458436e3620d85954c66c3c" + integrity sha1-LMDWazHqIwNkWENuNiDYWVTGbDw= + dependencies: + object-assign "^4.0.1" + prepend-http "^1.0.0" + query-string "^4.1.0" + sort-keys "^1.0.0" + +normalize-url@^3.0.0: + version "3.3.0" + resolved "https://registry.yarnpkg.com/normalize-url/-/normalize-url-3.3.0.tgz#b2e1c4dc4f7c6d57743df733a4f5978d18650559" + +npm-bundled@^1.0.1: + version "1.0.6" + resolved "https://registry.yarnpkg.com/npm-bundled/-/npm-bundled-1.0.6.tgz#e7ba9aadcef962bb61248f91721cd932b3fe6bdd" + +npm-packlist@^1.1.6: + version "1.4.4" + resolved "https://registry.yarnpkg.com/npm-packlist/-/npm-packlist-1.4.4.tgz#866224233850ac534b63d1a6e76050092b5d2f44" + dependencies: + ignore-walk "^3.0.1" + npm-bundled "^1.0.1" + +npm-run-path@^2.0.0: + version "2.0.2" + resolved "https://registry.yarnpkg.com/npm-run-path/-/npm-run-path-2.0.2.tgz#35a9232dfa35d7067b4cb2ddf2357b1871536c5f" + dependencies: + path-key "^2.0.0" + +"npmlog@0 || 1 || 2 || 3 || 4", npmlog@^4.0.0, npmlog@^4.0.2: + version "4.1.2" + resolved "https://registry.yarnpkg.com/npmlog/-/npmlog-4.1.2.tgz#08a7f2a8bf734604779a9efa4ad5cc717abb954b" + dependencies: + are-we-there-yet "~1.1.2" + console-control-strings "~1.1.0" + gauge "~2.7.3" + set-blocking "~2.0.0" + +nth-check@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/nth-check/-/nth-check-1.0.2.tgz#b2bd295c37e3dd58a3bf0700376663ba4d9cf05c" + dependencies: + boolbase "~1.0.0" + +num2fraction@^1.2.2: + version "1.2.2" + resolved "https://registry.yarnpkg.com/num2fraction/-/num2fraction-1.2.2.tgz#6f682b6a027a4e9ddfa4564cd2589d1d4e669ede" + +number-is-nan@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/number-is-nan/-/number-is-nan-1.0.1.tgz#097b602b53422a522c1afb8790318336941a011d" + +oauth-sign@~0.9.0: + version "0.9.0" + resolved "https://registry.yarnpkg.com/oauth-sign/-/oauth-sign-0.9.0.tgz#47a7b016baa68b5fa0ecf3dee08a85c679ac6455" + +object-assign@4.x, object-assign@^4.0.1, object-assign@^4.1.0, object-assign@^4.1.1: + version "4.1.1" + resolved "https://registry.yarnpkg.com/object-assign/-/object-assign-4.1.1.tgz#2109adc7965887cfc05cbbd442cac8bfbb360863" + +object-copy@^0.1.0: + version "0.1.0" + resolved "https://registry.yarnpkg.com/object-copy/-/object-copy-0.1.0.tgz#7e7d858b781bd7c991a41ba975ed3812754e998c" + dependencies: + copy-descriptor "^0.1.0" + define-property "^0.2.5" + kind-of "^3.0.3" + +object-inspect@^1.8.0: + version "1.8.0" + resolved "https://registry.yarnpkg.com/object-inspect/-/object-inspect-1.8.0.tgz#df807e5ecf53a609cc6bfe93eac3cc7be5b3a9d0" + integrity sha512-jLdtEOB112fORuypAyl/50VRVIBIdVQOSUUGQHzJ4xBSbit81zRarz7GThkEFZy1RceYrWYcPcBFPQwHyAc1gA== + +object-is@^1.0.1: + version "1.1.3" + resolved "https://registry.yarnpkg.com/object-is/-/object-is-1.1.3.tgz#2e3b9e65560137455ee3bd62aec4d90a2ea1cc81" + integrity sha512-teyqLvFWzLkq5B9ki8FVWA902UER2qkxmdA4nLf+wjOLAWgxzCWZNCxpDq9MvE8MmhWNr+I8w3BN49Vx36Y6Xg== + dependencies: + define-properties "^1.1.3" + es-abstract "^1.18.0-next.1" + +object-keys@^1.0.12, object-keys@^1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/object-keys/-/object-keys-1.1.1.tgz#1c47f272df277f3b1daf061677d9c82e2322c60e" + +object-visit@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/object-visit/-/object-visit-1.0.1.tgz#f79c4493af0c5377b59fe39d395e41042dd045bb" + dependencies: + isobject "^3.0.0" + +object.assign@^4.1.0, object.assign@^4.1.1: + version "4.1.1" + resolved "https://registry.yarnpkg.com/object.assign/-/object.assign-4.1.1.tgz#303867a666cdd41936ecdedfb1f8f3e32a478cdd" + integrity sha512-VT/cxmx5yaoHSOTSyrCygIDFco+RsibY2NM0a4RdEeY/4KgqezwFtK1yr3U67xYhqJSlASm2pKhLVzPj2lr4bA== + dependencies: + define-properties "^1.1.3" + es-abstract "^1.18.0-next.0" + has-symbols "^1.0.1" + object-keys "^1.1.1" + +object.assign@^4.1.2: + version "4.1.2" + resolved "https://registry.yarnpkg.com/object.assign/-/object.assign-4.1.2.tgz#0ed54a342eceb37b38ff76eb831a0e788cb63940" + integrity sha512-ixT2L5THXsApyiUPYKmW+2EHpXXe5Ii3M+f4e+aJFAHao5amFRW6J0OO6c/LU8Be47utCx2GL89hxGB6XSmKuQ== + dependencies: + call-bind "^1.0.0" + define-properties "^1.1.3" + has-symbols "^1.0.1" + object-keys "^1.1.1" + +object.entries@^1.1.2: + version "1.1.2" + resolved "https://registry.yarnpkg.com/object.entries/-/object.entries-1.1.2.tgz#bc73f00acb6b6bb16c203434b10f9a7e797d3add" + integrity sha512-BQdB9qKmb/HyNdMNWVr7O3+z5MUIx3aiegEIJqjMBbBf0YT9RRxTJSim4mzFqtyr7PDAHigq0N9dO0m0tRakQA== + dependencies: + define-properties "^1.1.3" + es-abstract "^1.17.5" + has "^1.0.3" + +object.fromentries@^2.0.2: + version "2.0.2" + resolved "https://registry.yarnpkg.com/object.fromentries/-/object.fromentries-2.0.2.tgz#4a09c9b9bb3843dd0f89acdb517a794d4f355ac9" + integrity sha512-r3ZiBH7MQppDJVLx6fhD618GKNG40CZYH9wgwdhKxBDDbQgjeWGGd4AtkZad84d291YxvWe7bJGuE65Anh0dxQ== + dependencies: + define-properties "^1.1.3" + es-abstract "^1.17.0-next.1" + function-bind "^1.1.1" + has "^1.0.3" + +object.getownpropertydescriptors@^2.0.3: + version "2.0.3" + resolved "https://registry.yarnpkg.com/object.getownpropertydescriptors/-/object.getownpropertydescriptors-2.0.3.tgz#8758c846f5b407adab0f236e0986f14b051caa16" + dependencies: + define-properties "^1.1.2" + es-abstract "^1.5.1" + +object.pick@^1.3.0: + version "1.3.0" + resolved "https://registry.yarnpkg.com/object.pick/-/object.pick-1.3.0.tgz#87a10ac4c1694bd2e1cbf53591a66141fb5dd747" + dependencies: + isobject "^3.0.1" + +object.values@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/object.values/-/object.values-1.1.0.tgz#bf6810ef5da3e5325790eaaa2be213ea84624da9" + dependencies: + define-properties "^1.1.3" + es-abstract "^1.12.0" + function-bind "^1.1.1" + has "^1.0.3" + +object.values@^1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/object.values/-/object.values-1.1.1.tgz#68a99ecde356b7e9295a3c5e0ce31dc8c953de5e" + integrity sha512-WTa54g2K8iu0kmS/us18jEmdv1a4Wi//BZ/DTVYEcH0XhLM5NYdpDHja3gt57VrZLcNAO2WGA+KpWsDBaHt6eA== + dependencies: + define-properties "^1.1.3" + es-abstract "^1.17.0-next.1" + function-bind "^1.1.1" + has "^1.0.3" + +objectify-array@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/objectify-array/-/objectify-array-2.1.0.tgz#1578ba9e65b5878564d8872f7906de99a18593a5" + +obuf@^1.0.0, obuf@^1.1.2: + version "1.1.2" + resolved "https://registry.yarnpkg.com/obuf/-/obuf-1.1.2.tgz#09bea3343d41859ebd446292d11c9d4db619084e" + +on-finished@~2.3.0: + version "2.3.0" + resolved "https://registry.yarnpkg.com/on-finished/-/on-finished-2.3.0.tgz#20f1336481b083cd75337992a16971aa2d906947" + dependencies: + ee-first "1.1.1" + +on-headers@~1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/on-headers/-/on-headers-1.0.2.tgz#772b0ae6aaa525c399e489adfad90c403eb3c28f" + +once@^1.3.0, once@^1.3.1, once@^1.4.0: + version "1.4.0" + resolved "https://registry.yarnpkg.com/once/-/once-1.4.0.tgz#583b1aa775961d4b113ac17d9c50baef9dd76bd1" + dependencies: + wrappy "1" + +opn@^5.5.0: + version "5.5.0" + resolved "https://registry.yarnpkg.com/opn/-/opn-5.5.0.tgz#fc7164fab56d235904c51c3b27da6758ca3b9bfc" + integrity sha512-PqHpggC9bLV0VeWcdKhkpxY+3JTzetLSqTCWL/z/tFIbI6G8JCjondXklT1JinczLz2Xib62sSp0T/gKT4KksA== + dependencies: + is-wsl "^1.1.0" + +optimize-css-assets-webpack-plugin@^5.0.3: + version "5.0.4" + resolved "https://registry.yarnpkg.com/optimize-css-assets-webpack-plugin/-/optimize-css-assets-webpack-plugin-5.0.4.tgz#85883c6528aaa02e30bbad9908c92926bb52dc90" + integrity sha512-wqd6FdI2a5/FdoiCNNkEvLeA//lHHfG24Ln2Xm2qqdIk4aOlsR18jwpyOihqQ8849W3qu2DX8fOYxpvTMj+93A== + dependencies: + cssnano "^4.1.10" + last-call-webpack-plugin "^3.0.0" + +optionator@^0.9.1: + version "0.9.1" + resolved "https://registry.yarnpkg.com/optionator/-/optionator-0.9.1.tgz#4f236a6373dae0566a6d43e1326674f50c291499" + integrity sha512-74RlY5FCnhq4jRxVUPKDaRwrVNXMqsGsiW6AJw4XK8hmtm10wC0ypZBLw5IIp85NZMr91+qd1RvvENwg7jjRFw== + dependencies: + deep-is "^0.1.3" + fast-levenshtein "^2.0.6" + levn "^0.4.1" + prelude-ls "^1.2.1" + type-check "^0.4.0" + word-wrap "^1.2.3" + +original@^1.0.0: + version "1.0.2" + resolved "https://registry.yarnpkg.com/original/-/original-1.0.2.tgz#e442a61cffe1c5fd20a65f3261c26663b303f25f" + integrity sha512-hyBVl6iqqUOJ8FqRe+l/gS8H+kKYjrEndd5Pm1MfBtsEKA038HkkdbAl/72EAXGyonD/PFsvmVG+EvcIpliMBg== + dependencies: + url-parse "^1.4.3" + +os-browserify@^0.3.0, os-browserify@~0.3.0: + version "0.3.0" + resolved "https://registry.yarnpkg.com/os-browserify/-/os-browserify-0.3.0.tgz#854373c7f5c2315914fc9bfc6bd8238fdda1ec27" + +os-homedir@^1.0.0: + version "1.0.2" + resolved "https://registry.yarnpkg.com/os-homedir/-/os-homedir-1.0.2.tgz#ffbc4988336e0e833de0c168c7ef152121aa7fb3" + +os-tmpdir@^1.0.0: + version "1.0.2" + resolved "https://registry.yarnpkg.com/os-tmpdir/-/os-tmpdir-1.0.2.tgz#bbe67406c79aa85c5cfec766fe5734555dfa1274" + +osenv@0, osenv@^0.1.4: + version "0.1.5" + resolved "https://registry.yarnpkg.com/osenv/-/osenv-0.1.5.tgz#85cdfafaeb28e8677f416e287592b5f3f49ea410" + dependencies: + os-homedir "^1.0.0" + os-tmpdir "^1.0.0" + +p-finally@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/p-finally/-/p-finally-1.0.0.tgz#3fbcfb15b899a44123b34b6dcc18b724336a2cae" + +p-limit@^1.1.0: + version "1.3.0" + resolved "https://registry.yarnpkg.com/p-limit/-/p-limit-1.3.0.tgz#b86bd5f0c25690911c7590fcbfc2010d54b3ccb8" + dependencies: + p-try "^1.0.0" + +p-limit@^2.0.0, p-limit@^2.2.0: + version "2.3.0" + resolved "https://registry.yarnpkg.com/p-limit/-/p-limit-2.3.0.tgz#3dd33c647a214fdfffd835933eb086da0dc21db1" + integrity sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w== + dependencies: + p-try "^2.0.0" + +p-limit@^3.0.2: + version "3.0.2" + resolved "https://registry.yarnpkg.com/p-limit/-/p-limit-3.0.2.tgz#1664e010af3cadc681baafd3e2a437be7b0fb5fe" + integrity sha512-iwqZSOoWIW+Ew4kAGUlN16J4M7OB3ysMLSZtnhmqx7njIHFPlxWBX8xo3lVTyFVq6mI/lL9qt2IsN1sHwaxJkg== + dependencies: + p-try "^2.0.0" + +p-locate@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/p-locate/-/p-locate-2.0.0.tgz#20a0103b222a70c8fd39cc2e580680f3dde5ec43" + dependencies: + p-limit "^1.1.0" + +p-locate@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/p-locate/-/p-locate-3.0.0.tgz#322d69a05c0264b25997d9f40cd8a891ab0064a4" + integrity sha512-x+12w/To+4GFfgJhBEpiDcLozRJGegY+Ei7/z0tSLkMmxGZNybVMSfWj9aJn8Z5Fc7dBUNJOOVgPv2H7IwulSQ== + dependencies: + p-limit "^2.0.0" + +p-locate@^4.1.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/p-locate/-/p-locate-4.1.0.tgz#a3428bb7088b3a60292f66919278b7c297ad4f07" + integrity sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A== + dependencies: + p-limit "^2.2.0" + +p-map@^2.0.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/p-map/-/p-map-2.1.0.tgz#310928feef9c9ecc65b68b17693018a665cea175" + integrity sha512-y3b8Kpd8OAN444hxfBbFfj1FY/RjtTd8tzYwhUqNYXx0fXx2iX4maP4Qr6qhIKbQXI02wTLAda4fYUbDagTUFw== + +p-map@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/p-map/-/p-map-4.0.0.tgz#bb2f95a5eda2ec168ec9274e06a747c3e2904d2b" + integrity sha512-/bjOqmgETBYB5BoEeGVea8dmvHb2m9GLy1E9W43yeyfP6QQCZGFNa+XRceJEuDB6zqr+gKpIAmlLebMpykw/MQ== + dependencies: + aggregate-error "^3.0.0" + +p-retry@^3.0.1: + version "3.0.1" + resolved "https://registry.yarnpkg.com/p-retry/-/p-retry-3.0.1.tgz#316b4c8893e2c8dc1cfa891f406c4b422bebf328" + integrity sha512-XE6G4+YTTkT2a0UWb2kjZe8xNwf8bIbnqpc/IS/idOBVhyves0mK5OJgeocjx7q5pvX/6m23xuzVPYT1uGM73w== + dependencies: + retry "^0.12.0" + +p-try@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/p-try/-/p-try-1.0.0.tgz#cbc79cdbaf8fd4228e13f621f2b1a237c1b207b3" + +p-try@^2.0.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/p-try/-/p-try-2.2.0.tgz#cb2868540e313d61de58fafbe35ce9004d5540e6" + integrity sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ== + +pako@~1.0.5: + version "1.0.10" + resolved "https://registry.yarnpkg.com/pako/-/pako-1.0.10.tgz#4328badb5086a426aa90f541977d4955da5c9732" + +parallel-transform@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/parallel-transform/-/parallel-transform-1.1.0.tgz#d410f065b05da23081fcd10f28854c29bda33b06" + dependencies: + cyclist "~0.2.2" + inherits "^2.0.3" + readable-stream "^2.1.5" + +parent-module@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/parent-module/-/parent-module-1.0.1.tgz#691d2709e78c79fae3a156622452d00762caaaa2" + integrity sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g== + dependencies: + callsites "^3.0.0" + +parents@^1.0.0, parents@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/parents/-/parents-1.0.1.tgz#fedd4d2bf193a77745fe71e371d73c3307d9c751" + dependencies: + path-platform "~0.11.15" + +parse-asn1@^5.0.0: + version "5.1.4" + resolved "https://registry.yarnpkg.com/parse-asn1/-/parse-asn1-5.1.4.tgz#37f6628f823fbdeb2273b4d540434a22f3ef1fcc" + dependencies: + asn1.js "^4.0.0" + browserify-aes "^1.0.0" + create-hash "^1.1.0" + evp_bytestokey "^1.0.0" + pbkdf2 "^3.0.3" + safe-buffer "^5.1.1" + +parse-json@^2.2.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/parse-json/-/parse-json-2.2.0.tgz#f480f40434ef80741f8469099f8dea18f55a4dc9" + dependencies: + error-ex "^1.2.0" + +parse-json@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/parse-json/-/parse-json-4.0.0.tgz#be35f5425be1f7f6c747184f98a788cb99477ee0" + dependencies: + error-ex "^1.3.1" + json-parse-better-errors "^1.0.1" + +parse-json@^5.0.0: + version "5.1.0" + resolved "https://registry.yarnpkg.com/parse-json/-/parse-json-5.1.0.tgz#f96088cdf24a8faa9aea9a009f2d9d942c999646" + integrity sha512-+mi/lmVVNKFNVyLXV31ERiy2CY5E1/F6QtJFEzoChPRwwngMNXRDQ9GJ5WdE2Z2P4AujsOi0/+2qHID68KwfIQ== + dependencies: + "@babel/code-frame" "^7.0.0" + error-ex "^1.3.1" + json-parse-even-better-errors "^2.3.0" + lines-and-columns "^1.1.6" + +parse-passwd@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/parse-passwd/-/parse-passwd-1.0.0.tgz#6d5b934a456993b23d37f40a382d6f1666a8e5c6" + integrity sha1-bVuTSkVpk7I9N/QKOC1vFmao5cY= + +parseurl@~1.3.2, parseurl@~1.3.3: + version "1.3.3" + resolved "https://registry.yarnpkg.com/parseurl/-/parseurl-1.3.3.tgz#9da19e7bee8d12dff0513ed5b76957793bc2e8d4" + +pascalcase@^0.1.1: + version "0.1.1" + resolved "https://registry.yarnpkg.com/pascalcase/-/pascalcase-0.1.1.tgz#b363e55e8006ca6fe21784d2db22bd15d7917f14" + +path-browserify@0.0.1, path-browserify@~0.0.0: + version "0.0.1" + resolved "https://registry.yarnpkg.com/path-browserify/-/path-browserify-0.0.1.tgz#e6c4ddd7ed3aa27c68a20cc4e50e1a4ee83bbc4a" + +path-complete-extname@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/path-complete-extname/-/path-complete-extname-1.0.0.tgz#f889985dc91000c815515c0bfed06c5acda0752b" + +path-dirname@^1.0.0: + version "1.0.2" + resolved "https://registry.yarnpkg.com/path-dirname/-/path-dirname-1.0.2.tgz#cc33d24d525e099a5388c0336c6e32b9160609e0" + +path-exists@^2.0.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/path-exists/-/path-exists-2.1.0.tgz#0feb6c64f0fc518d9a754dd5efb62c7022761f4b" + dependencies: + pinkie-promise "^2.0.0" + +path-exists@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/path-exists/-/path-exists-3.0.0.tgz#ce0ebeaa5f78cb18925ea7d810d7b59b010fd515" + +path-exists@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/path-exists/-/path-exists-4.0.0.tgz#513bdbe2d3b95d7762e8c1137efa195c6c61b5b3" + integrity sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w== + +path-is-absolute@^1.0.0, path-is-absolute@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/path-is-absolute/-/path-is-absolute-1.0.1.tgz#174b9268735534ffbc7ace6bf53a5a9e1b5c5f5f" + +path-is-inside@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/path-is-inside/-/path-is-inside-1.0.2.tgz#365417dede44430d1c11af61027facf074bdfc53" + integrity sha1-NlQX3t5EQw0cEa9hAn+s8HS9/FM= + +path-key@^2.0.0, path-key@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/path-key/-/path-key-2.0.1.tgz#411cadb574c5a140d3a4b1910d40d80cc9f40b40" + integrity sha1-QRyttXTFoUDTpLGRDUDYDMn0C0A= + +path-key@^3.1.0: + version "3.1.1" + resolved "https://registry.yarnpkg.com/path-key/-/path-key-3.1.1.tgz#581f6ade658cbba65a0d3380de7753295054f375" + integrity sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q== + +path-parse@^1.0.6: + version "1.0.6" + resolved "https://registry.yarnpkg.com/path-parse/-/path-parse-1.0.6.tgz#d62dbb5679405d72c4737ec58600e9ddcf06d24c" + +path-platform@~0.11.15: + version "0.11.15" + resolved "https://registry.yarnpkg.com/path-platform/-/path-platform-0.11.15.tgz#e864217f74c36850f0852b78dc7bf7d4a5721bf2" + +path-to-regexp@0.1.7: + version "0.1.7" + resolved "https://registry.yarnpkg.com/path-to-regexp/-/path-to-regexp-0.1.7.tgz#df604178005f522f15eb4490e7247a1bfaa67f8c" + +path-type@^1.0.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/path-type/-/path-type-1.1.0.tgz#59c44f7ee491da704da415da5a4070ba4f8fe441" + dependencies: + graceful-fs "^4.1.2" + pify "^2.0.0" + pinkie-promise "^2.0.0" + +path-type@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/path-type/-/path-type-2.0.0.tgz#f012ccb8415b7096fc2daa1054c3d72389594c73" + dependencies: + pify "^2.0.0" + +path-type@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/path-type/-/path-type-4.0.0.tgz#84ed01c0a7ba380afe09d90a8c180dcd9d03043b" + integrity sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw== + +pbkdf2@^3.0.3: + version "3.0.17" + resolved "https://registry.yarnpkg.com/pbkdf2/-/pbkdf2-3.0.17.tgz#976c206530617b14ebb32114239f7b09336e93a6" + dependencies: + create-hash "^1.1.2" + create-hmac "^1.1.4" + ripemd160 "^2.0.1" + safe-buffer "^5.0.1" + sha.js "^2.4.8" + +pdfjs-dist@2.1.266: + version "2.1.266" + resolved "https://registry.yarnpkg.com/pdfjs-dist/-/pdfjs-dist-2.1.266.tgz#cded02268b389559e807f410d2a729db62160026" + integrity sha512-Jy7o1wE3NezPxozexSbq4ltuLT0Z21ew/qrEiAEeUZzHxMHGk4DUV1D7RuCXg5vJDvHmjX1YssN+we9QfRRgXQ== + dependencies: + node-ensure "^0.0.0" + worker-loader "^2.0.0" + +performance-now@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/performance-now/-/performance-now-2.1.0.tgz#6309f4e0e5fa913ec1c69307ae364b4b377c9e7b" + +picomatch@^2.0.4, picomatch@^2.0.5, picomatch@^2.2.1: + version "2.2.2" + resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-2.2.2.tgz#21f333e9b6b8eaff02468f5146ea406d345f4dad" + integrity sha512-q0M/9eZHzmr0AulXyPwNfZjtwZ/RBZlbN3K3CErVrk50T2ASYI7Bye0EvekFY3IP1Nt2DHu0re+V2ZHIpMkuWg== + +pify@^2.0.0, pify@^2.3.0: + version "2.3.0" + resolved "https://registry.yarnpkg.com/pify/-/pify-2.3.0.tgz#ed141a6ac043a849ea588498e7dca8b15330e90c" + +pify@^4.0.1: + version "4.0.1" + resolved "https://registry.yarnpkg.com/pify/-/pify-4.0.1.tgz#4b2cd25c50d598735c50292224fd8c6df41e3231" + integrity sha512-uB80kBFb/tfd68bVleG9T5GGsGPjJrLAUpR5PZIrhBnIaRTQRjqdJSsIKkOP6OAIFbj7GOrcudc5pNjZ+geV2g== + +pinkie-promise@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/pinkie-promise/-/pinkie-promise-2.0.1.tgz#2135d6dfa7a358c069ac9b178776288228450ffa" + dependencies: + pinkie "^2.0.0" + +pinkie@^2.0.0: + version "2.0.4" + resolved "https://registry.yarnpkg.com/pinkie/-/pinkie-2.0.4.tgz#72556b80cfa0d48a974e80e77248e80ed4f7f870" + +pkg-dir@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/pkg-dir/-/pkg-dir-2.0.0.tgz#f6d5d1109e19d63edf428e0bd57e12777615334b" + dependencies: + find-up "^2.1.0" + +pkg-dir@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/pkg-dir/-/pkg-dir-3.0.0.tgz#2749020f239ed990881b1f71210d51eb6523bea3" + integrity sha512-/E57AYkoeQ25qkxMj5PBOVgF8Kiu/h7cYS30Z5+R7WaiCCBfLq58ZI/dSeaEKb9WVJV5n/03QwrN3IeWIFllvw== + dependencies: + find-up "^3.0.0" + +pkg-dir@^4.1.0: + version "4.2.0" + resolved "https://registry.yarnpkg.com/pkg-dir/-/pkg-dir-4.2.0.tgz#f099133df7ede422e81d1d8448270eeb3e4261f3" + integrity sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ== + dependencies: + find-up "^4.0.0" + +pnp-webpack-plugin@^1.6.4: + version "1.6.4" + resolved "https://registry.yarnpkg.com/pnp-webpack-plugin/-/pnp-webpack-plugin-1.6.4.tgz#c9711ac4dc48a685dabafc86f8b6dd9f8df84149" + integrity sha512-7Wjy+9E3WwLOEL30D+m8TSTF7qJJUJLONBnwQp0518siuMxUQUbgZwssaFX+QKlZkjHZcw/IpZCt/H0srrntSg== + dependencies: + ts-pnp "^1.1.6" + +popper.js@^1.14.1, popper.js@^1.14.4: + version "1.15.0" + resolved "https://registry.yarnpkg.com/popper.js/-/popper.js-1.15.0.tgz#5560b99bbad7647e9faa475c6b8056621f5a4ff2" + +popper.js@^1.16.1: + version "1.16.1" + resolved "https://registry.yarnpkg.com/popper.js/-/popper.js-1.16.1.tgz#2a223cb3dc7b6213d740e40372be40de43e65b1b" + integrity sha512-Wb4p1J4zyFTbM+u6WuO4XstYx4Ky9Cewe4DWrel7B0w6VVICvPwdOpotjzcf6eD8TsckVnIMNONQyPIUFOUbCQ== + +portfinder@^1.0.26: + version "1.0.28" + resolved "https://registry.yarnpkg.com/portfinder/-/portfinder-1.0.28.tgz#67c4622852bd5374dd1dd900f779f53462fac778" + integrity sha512-Se+2isanIcEqf2XMHjyUKskczxbPH7dQnlMjXX6+dybayyHvAf/TCgyMRlzf/B6QDhAEFOGes0pzRo3by4AbMA== + dependencies: + async "^2.6.2" + debug "^3.1.1" + mkdirp "^0.5.5" + +posix-character-classes@^0.1.0: + version "0.1.1" + resolved "https://registry.yarnpkg.com/posix-character-classes/-/posix-character-classes-0.1.1.tgz#01eac0fe3b5af71a2a6c02feabb8c1fef7e00eab" + +postcss-attribute-case-insensitive@^4.0.1: + version "4.0.2" + resolved "https://registry.yarnpkg.com/postcss-attribute-case-insensitive/-/postcss-attribute-case-insensitive-4.0.2.tgz#d93e46b504589e94ac7277b0463226c68041a880" + integrity sha512-clkFxk/9pcdb4Vkn0hAHq3YnxBQ2p0CGD1dy24jN+reBck+EWxMbxSUqN4Yj7t0w8csl87K6p0gxBe1utkJsYA== + dependencies: + postcss "^7.0.2" + postcss-selector-parser "^6.0.2" + +postcss-calc@^7.0.1: + version "7.0.1" + resolved "https://registry.yarnpkg.com/postcss-calc/-/postcss-calc-7.0.1.tgz#36d77bab023b0ecbb9789d84dcb23c4941145436" + dependencies: + css-unit-converter "^1.1.1" + postcss "^7.0.5" + postcss-selector-parser "^5.0.0-rc.4" + postcss-value-parser "^3.3.1" + +postcss-color-functional-notation@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/postcss-color-functional-notation/-/postcss-color-functional-notation-2.0.1.tgz#5efd37a88fbabeb00a2966d1e53d98ced93f74e0" + integrity sha512-ZBARCypjEDofW4P6IdPVTLhDNXPRn8T2s1zHbZidW6rPaaZvcnCS2soYFIQJrMZSxiePJ2XIYTlcb2ztr/eT2g== + dependencies: + postcss "^7.0.2" + postcss-values-parser "^2.0.0" + +postcss-color-gray@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/postcss-color-gray/-/postcss-color-gray-5.0.0.tgz#532a31eb909f8da898ceffe296fdc1f864be8547" + integrity sha512-q6BuRnAGKM/ZRpfDascZlIZPjvwsRye7UDNalqVz3s7GDxMtqPY6+Q871liNxsonUw8oC61OG+PSaysYpl1bnw== + dependencies: + "@csstools/convert-colors" "^1.4.0" + postcss "^7.0.5" + postcss-values-parser "^2.0.0" + +postcss-color-hex-alpha@^5.0.3: + version "5.0.3" + resolved "https://registry.yarnpkg.com/postcss-color-hex-alpha/-/postcss-color-hex-alpha-5.0.3.tgz#a8d9ca4c39d497c9661e374b9c51899ef0f87388" + integrity sha512-PF4GDel8q3kkreVXKLAGNpHKilXsZ6xuu+mOQMHWHLPNyjiUBOr75sp5ZKJfmv1MCus5/DWUGcK9hm6qHEnXYw== + dependencies: + postcss "^7.0.14" + postcss-values-parser "^2.0.1" + +postcss-color-mod-function@^3.0.3: + version "3.0.3" + resolved "https://registry.yarnpkg.com/postcss-color-mod-function/-/postcss-color-mod-function-3.0.3.tgz#816ba145ac11cc3cb6baa905a75a49f903e4d31d" + integrity sha512-YP4VG+xufxaVtzV6ZmhEtc+/aTXH3d0JLpnYfxqTvwZPbJhWqp8bSY3nfNzNRFLgB4XSaBA82OE4VjOOKpCdVQ== + dependencies: + "@csstools/convert-colors" "^1.4.0" + postcss "^7.0.2" + postcss-values-parser "^2.0.0" + +postcss-color-rebeccapurple@^4.0.1: + version "4.0.1" + resolved "https://registry.yarnpkg.com/postcss-color-rebeccapurple/-/postcss-color-rebeccapurple-4.0.1.tgz#c7a89be872bb74e45b1e3022bfe5748823e6de77" + integrity sha512-aAe3OhkS6qJXBbqzvZth2Au4V3KieR5sRQ4ptb2b2O8wgvB3SJBsdG+jsn2BZbbwekDG8nTfcCNKcSfe/lEy8g== + dependencies: + postcss "^7.0.2" + postcss-values-parser "^2.0.0" + +postcss-colormin@^4.0.3: + version "4.0.3" + resolved "https://registry.yarnpkg.com/postcss-colormin/-/postcss-colormin-4.0.3.tgz#ae060bce93ed794ac71264f08132d550956bd381" + dependencies: + browserslist "^4.0.0" + color "^3.0.0" + has "^1.0.0" + postcss "^7.0.0" + postcss-value-parser "^3.0.0" + +postcss-convert-values@^4.0.1: + version "4.0.1" + resolved "https://registry.yarnpkg.com/postcss-convert-values/-/postcss-convert-values-4.0.1.tgz#ca3813ed4da0f812f9d43703584e449ebe189a7f" + dependencies: + postcss "^7.0.0" + postcss-value-parser "^3.0.0" + +postcss-custom-media@^7.0.8: + version "7.0.8" + resolved "https://registry.yarnpkg.com/postcss-custom-media/-/postcss-custom-media-7.0.8.tgz#fffd13ffeffad73621be5f387076a28b00294e0c" + integrity sha512-c9s5iX0Ge15o00HKbuRuTqNndsJUbaXdiNsksnVH8H4gdc+zbLzr/UasOwNG6CTDpLFekVY4672eWdiiWu2GUg== + dependencies: + postcss "^7.0.14" + +postcss-custom-properties@^8.0.11: + version "8.0.11" + resolved "https://registry.yarnpkg.com/postcss-custom-properties/-/postcss-custom-properties-8.0.11.tgz#2d61772d6e92f22f5e0d52602df8fae46fa30d97" + integrity sha512-nm+o0eLdYqdnJ5abAJeXp4CEU1c1k+eB2yMCvhgzsds/e0umabFrN6HoTy/8Q4K5ilxERdl/JD1LO5ANoYBeMA== + dependencies: + postcss "^7.0.17" + postcss-values-parser "^2.0.1" + +postcss-custom-selectors@^5.1.2: + version "5.1.2" + resolved "https://registry.yarnpkg.com/postcss-custom-selectors/-/postcss-custom-selectors-5.1.2.tgz#64858c6eb2ecff2fb41d0b28c9dd7b3db4de7fba" + integrity sha512-DSGDhqinCqXqlS4R7KGxL1OSycd1lydugJ1ky4iRXPHdBRiozyMHrdu0H3o7qNOCiZwySZTUI5MV0T8QhCLu+w== + dependencies: + postcss "^7.0.2" + postcss-selector-parser "^5.0.0-rc.3" + +postcss-dir-pseudo-class@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/postcss-dir-pseudo-class/-/postcss-dir-pseudo-class-5.0.0.tgz#6e3a4177d0edb3abcc85fdb6fbb1c26dabaeaba2" + integrity sha512-3pm4oq8HYWMZePJY+5ANriPs3P07q+LW6FAdTlkFH2XqDdP4HeeJYMOzn0HYLhRSjBO3fhiqSwwU9xEULSrPgw== + dependencies: + postcss "^7.0.2" + postcss-selector-parser "^5.0.0-rc.3" + +postcss-discard-comments@^4.0.2: + version "4.0.2" + resolved "https://registry.yarnpkg.com/postcss-discard-comments/-/postcss-discard-comments-4.0.2.tgz#1fbabd2c246bff6aaad7997b2b0918f4d7af4033" + dependencies: + postcss "^7.0.0" + +postcss-discard-duplicates@^4.0.2: + version "4.0.2" + resolved "https://registry.yarnpkg.com/postcss-discard-duplicates/-/postcss-discard-duplicates-4.0.2.tgz#3fe133cd3c82282e550fc9b239176a9207b784eb" + dependencies: + postcss "^7.0.0" + +postcss-discard-empty@^4.0.1: + version "4.0.1" + resolved "https://registry.yarnpkg.com/postcss-discard-empty/-/postcss-discard-empty-4.0.1.tgz#c8c951e9f73ed9428019458444a02ad90bb9f765" + dependencies: + postcss "^7.0.0" + +postcss-discard-overridden@^4.0.1: + version "4.0.1" + resolved "https://registry.yarnpkg.com/postcss-discard-overridden/-/postcss-discard-overridden-4.0.1.tgz#652aef8a96726f029f5e3e00146ee7a4e755ff57" + dependencies: + postcss "^7.0.0" + +postcss-double-position-gradients@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/postcss-double-position-gradients/-/postcss-double-position-gradients-1.0.0.tgz#fc927d52fddc896cb3a2812ebc5df147e110522e" + integrity sha512-G+nV8EnQq25fOI8CH/B6krEohGWnF5+3A6H/+JEpOncu5dCnkS1QQ6+ct3Jkaepw1NGVqqOZH6lqrm244mCftA== + dependencies: + postcss "^7.0.5" + postcss-values-parser "^2.0.0" + +postcss-env-function@^2.0.2: + version "2.0.2" + resolved "https://registry.yarnpkg.com/postcss-env-function/-/postcss-env-function-2.0.2.tgz#0f3e3d3c57f094a92c2baf4b6241f0b0da5365d7" + integrity sha512-rwac4BuZlITeUbiBq60h/xbLzXY43qOsIErngWa4l7Mt+RaSkT7QBjXVGTcBHupykkblHMDrBFh30zchYPaOUw== + dependencies: + postcss "^7.0.2" + postcss-values-parser "^2.0.0" + +postcss-flexbugs-fixes@^4.2.1: + version "4.2.1" + resolved "https://registry.yarnpkg.com/postcss-flexbugs-fixes/-/postcss-flexbugs-fixes-4.2.1.tgz#9218a65249f30897deab1033aced8578562a6690" + integrity sha512-9SiofaZ9CWpQWxOwRh1b/r85KD5y7GgvsNt1056k6OYLvWUun0czCvogfJgylC22uJTwW1KzY3Gz65NZRlvoiQ== + dependencies: + postcss "^7.0.26" + +postcss-focus-visible@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/postcss-focus-visible/-/postcss-focus-visible-4.0.0.tgz#477d107113ade6024b14128317ade2bd1e17046e" + integrity sha512-Z5CkWBw0+idJHSV6+Bgf2peDOFf/x4o+vX/pwcNYrWpXFrSfTkQ3JQ1ojrq9yS+upnAlNRHeg8uEwFTgorjI8g== + dependencies: + postcss "^7.0.2" + +postcss-focus-within@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/postcss-focus-within/-/postcss-focus-within-3.0.0.tgz#763b8788596cee9b874c999201cdde80659ef680" + integrity sha512-W0APui8jQeBKbCGZudW37EeMCjDeVxKgiYfIIEo8Bdh5SpB9sxds/Iq8SEuzS0Q4YFOlG7EPFulbbxujpkrV2w== + dependencies: + postcss "^7.0.2" + +postcss-font-variant@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/postcss-font-variant/-/postcss-font-variant-4.0.0.tgz#71dd3c6c10a0d846c5eda07803439617bbbabacc" + integrity sha512-M8BFYKOvCrI2aITzDad7kWuXXTm0YhGdP9Q8HanmN4EF1Hmcgs1KK5rSHylt/lUJe8yLxiSwWAHdScoEiIxztg== + dependencies: + postcss "^7.0.2" + +postcss-gap-properties@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/postcss-gap-properties/-/postcss-gap-properties-2.0.0.tgz#431c192ab3ed96a3c3d09f2ff615960f902c1715" + integrity sha512-QZSqDaMgXCHuHTEzMsS2KfVDOq7ZFiknSpkrPJY6jmxbugUPTuSzs/vuE5I3zv0WAS+3vhrlqhijiprnuQfzmg== + dependencies: + postcss "^7.0.2" + +postcss-image-set-function@^3.0.1: + version "3.0.1" + resolved "https://registry.yarnpkg.com/postcss-image-set-function/-/postcss-image-set-function-3.0.1.tgz#28920a2f29945bed4c3198d7df6496d410d3f288" + integrity sha512-oPTcFFip5LZy8Y/whto91L9xdRHCWEMs3e1MdJxhgt4jy2WYXfhkng59fH5qLXSCPN8k4n94p1Czrfe5IOkKUw== + dependencies: + postcss "^7.0.2" + postcss-values-parser "^2.0.0" + +postcss-import@^12.0.1: + version "12.0.1" + resolved "https://registry.yarnpkg.com/postcss-import/-/postcss-import-12.0.1.tgz#cf8c7ab0b5ccab5649024536e565f841928b7153" + integrity sha512-3Gti33dmCjyKBgimqGxL3vcV8w9+bsHwO5UrBawp796+jdardbcFl4RP5w/76BwNL7aGzpKstIfF9I+kdE8pTw== + dependencies: + postcss "^7.0.1" + postcss-value-parser "^3.2.3" + read-cache "^1.0.0" + resolve "^1.1.7" + +postcss-initial@^3.0.0: + version "3.0.2" + resolved "https://registry.yarnpkg.com/postcss-initial/-/postcss-initial-3.0.2.tgz#f018563694b3c16ae8eaabe3c585ac6319637b2d" + integrity sha512-ugA2wKonC0xeNHgirR4D3VWHs2JcU08WAi1KFLVcnb7IN89phID6Qtg2RIctWbnvp1TM2BOmDtX8GGLCKdR8YA== + dependencies: + lodash.template "^4.5.0" + postcss "^7.0.2" + +postcss-lab-function@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/postcss-lab-function/-/postcss-lab-function-2.0.1.tgz#bb51a6856cd12289ab4ae20db1e3821ef13d7d2e" + integrity sha512-whLy1IeZKY+3fYdqQFuDBf8Auw+qFuVnChWjmxm/UhHWqNHZx+B99EwxTvGYmUBqe3Fjxs4L1BoZTJmPu6usVg== + dependencies: + "@csstools/convert-colors" "^1.4.0" + postcss "^7.0.2" + postcss-values-parser "^2.0.0" + +postcss-load-config@^2.0.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/postcss-load-config/-/postcss-load-config-2.1.0.tgz#c84d692b7bb7b41ddced94ee62e8ab31b417b003" + dependencies: + cosmiconfig "^5.0.0" + import-cwd "^2.0.0" + +postcss-loader@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/postcss-loader/-/postcss-loader-3.0.0.tgz#6b97943e47c72d845fa9e03f273773d4e8dd6c2d" + integrity sha512-cLWoDEY5OwHcAjDnkyRQzAXfs2jrKjXpO/HQFcc5b5u/r7aa471wdmChmwfnv7x2u840iat/wi0lQ5nbRgSkUA== + dependencies: + loader-utils "^1.1.0" + postcss "^7.0.0" + postcss-load-config "^2.0.0" + schema-utils "^1.0.0" + +postcss-logical@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/postcss-logical/-/postcss-logical-3.0.0.tgz#2495d0f8b82e9f262725f75f9401b34e7b45d5b5" + integrity sha512-1SUKdJc2vuMOmeItqGuNaC+N8MzBWFWEkAnRnLpFYj1tGGa7NqyVBujfRtgNa2gXR+6RkGUiB2O5Vmh7E2RmiA== + dependencies: + postcss "^7.0.2" + +postcss-media-minmax@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/postcss-media-minmax/-/postcss-media-minmax-4.0.0.tgz#b75bb6cbc217c8ac49433e12f22048814a4f5ed5" + integrity sha512-fo9moya6qyxsjbFAYl97qKO9gyre3qvbMnkOZeZwlsW6XYFsvs2DMGDlchVLfAd8LHPZDxivu/+qW2SMQeTHBw== + dependencies: + postcss "^7.0.2" + +postcss-merge-longhand@^4.0.11: + version "4.0.11" + resolved "https://registry.yarnpkg.com/postcss-merge-longhand/-/postcss-merge-longhand-4.0.11.tgz#62f49a13e4a0ee04e7b98f42bb16062ca2549e24" + dependencies: + css-color-names "0.0.4" + postcss "^7.0.0" + postcss-value-parser "^3.0.0" + stylehacks "^4.0.0" + +postcss-merge-rules@^4.0.3: + version "4.0.3" + resolved "https://registry.yarnpkg.com/postcss-merge-rules/-/postcss-merge-rules-4.0.3.tgz#362bea4ff5a1f98e4075a713c6cb25aefef9a650" + dependencies: + browserslist "^4.0.0" + caniuse-api "^3.0.0" + cssnano-util-same-parent "^4.0.0" + postcss "^7.0.0" + postcss-selector-parser "^3.0.0" + vendors "^1.0.0" + +postcss-minify-font-values@^4.0.2: + version "4.0.2" + resolved "https://registry.yarnpkg.com/postcss-minify-font-values/-/postcss-minify-font-values-4.0.2.tgz#cd4c344cce474343fac5d82206ab2cbcb8afd5a6" + dependencies: + postcss "^7.0.0" + postcss-value-parser "^3.0.0" + +postcss-minify-gradients@^4.0.2: + version "4.0.2" + resolved "https://registry.yarnpkg.com/postcss-minify-gradients/-/postcss-minify-gradients-4.0.2.tgz#93b29c2ff5099c535eecda56c4aa6e665a663471" + dependencies: + cssnano-util-get-arguments "^4.0.0" + is-color-stop "^1.0.0" + postcss "^7.0.0" + postcss-value-parser "^3.0.0" + +postcss-minify-params@^4.0.2: + version "4.0.2" + resolved "https://registry.yarnpkg.com/postcss-minify-params/-/postcss-minify-params-4.0.2.tgz#6b9cef030c11e35261f95f618c90036d680db874" + dependencies: + alphanum-sort "^1.0.0" + browserslist "^4.0.0" + cssnano-util-get-arguments "^4.0.0" + postcss "^7.0.0" + postcss-value-parser "^3.0.0" + uniqs "^2.0.0" + +postcss-minify-selectors@^4.0.2: + version "4.0.2" + resolved "https://registry.yarnpkg.com/postcss-minify-selectors/-/postcss-minify-selectors-4.0.2.tgz#e2e5eb40bfee500d0cd9243500f5f8ea4262fbd8" + dependencies: + alphanum-sort "^1.0.0" + has "^1.0.0" + postcss "^7.0.0" + postcss-selector-parser "^3.0.0" + +postcss-modules-extract-imports@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/postcss-modules-extract-imports/-/postcss-modules-extract-imports-2.0.0.tgz#818719a1ae1da325f9832446b01136eeb493cd7e" + integrity sha512-LaYLDNS4SG8Q5WAWqIJgdHPJrDDr/Lv775rMBFUbgjTz6j34lUznACHcdRWroPvXANP2Vj7yNK57vp9eFqzLWQ== + dependencies: + postcss "^7.0.5" + +postcss-modules-local-by-default@^3.0.2: + version "3.0.3" + resolved "https://registry.yarnpkg.com/postcss-modules-local-by-default/-/postcss-modules-local-by-default-3.0.3.tgz#bb14e0cc78279d504dbdcbfd7e0ca28993ffbbb0" + integrity sha512-e3xDq+LotiGesympRlKNgaJ0PCzoUIdpH0dj47iWAui/kyTgh3CiAr1qP54uodmJhl6p9rN6BoNcdEDVJx9RDw== + dependencies: + icss-utils "^4.1.1" + postcss "^7.0.32" + postcss-selector-parser "^6.0.2" + postcss-value-parser "^4.1.0" + +postcss-modules-scope@^2.2.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/postcss-modules-scope/-/postcss-modules-scope-2.2.0.tgz#385cae013cc7743f5a7d7602d1073a89eaae62ee" + integrity sha512-YyEgsTMRpNd+HmyC7H/mh3y+MeFWevy7V1evVhJWewmMbjDHIbZbOXICC2y+m1xI1UVfIT1HMW/O04Hxyu9oXQ== + dependencies: + postcss "^7.0.6" + postcss-selector-parser "^6.0.0" + +postcss-modules-values@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/postcss-modules-values/-/postcss-modules-values-3.0.0.tgz#5b5000d6ebae29b4255301b4a3a54574423e7f10" + integrity sha512-1//E5jCBrZ9DmRX+zCtmQtRSV6PV42Ix7Bzj9GbwJceduuf7IqP8MgeTXuRDHOWj2m0VzZD5+roFWDuU8RQjcg== + dependencies: + icss-utils "^4.0.0" + postcss "^7.0.6" + +postcss-nesting@^7.0.0: + version "7.0.1" + resolved "https://registry.yarnpkg.com/postcss-nesting/-/postcss-nesting-7.0.1.tgz#b50ad7b7f0173e5b5e3880c3501344703e04c052" + integrity sha512-FrorPb0H3nuVq0Sff7W2rnc3SmIcruVC6YwpcS+k687VxyxO33iE1amna7wHuRVzM8vfiYofXSBHNAZ3QhLvYg== + dependencies: + postcss "^7.0.2" + +postcss-normalize-charset@^4.0.1: + version "4.0.1" + resolved "https://registry.yarnpkg.com/postcss-normalize-charset/-/postcss-normalize-charset-4.0.1.tgz#8b35add3aee83a136b0471e0d59be58a50285dd4" + dependencies: + postcss "^7.0.0" + +postcss-normalize-display-values@^4.0.2: + version "4.0.2" + resolved "https://registry.yarnpkg.com/postcss-normalize-display-values/-/postcss-normalize-display-values-4.0.2.tgz#0dbe04a4ce9063d4667ed2be476bb830c825935a" + dependencies: + cssnano-util-get-match "^4.0.0" + postcss "^7.0.0" + postcss-value-parser "^3.0.0" + +postcss-normalize-positions@^4.0.2: + version "4.0.2" + resolved "https://registry.yarnpkg.com/postcss-normalize-positions/-/postcss-normalize-positions-4.0.2.tgz#05f757f84f260437378368a91f8932d4b102917f" + dependencies: + cssnano-util-get-arguments "^4.0.0" + has "^1.0.0" + postcss "^7.0.0" + postcss-value-parser "^3.0.0" + +postcss-normalize-repeat-style@^4.0.2: + version "4.0.2" + resolved "https://registry.yarnpkg.com/postcss-normalize-repeat-style/-/postcss-normalize-repeat-style-4.0.2.tgz#c4ebbc289f3991a028d44751cbdd11918b17910c" + dependencies: + cssnano-util-get-arguments "^4.0.0" + cssnano-util-get-match "^4.0.0" + postcss "^7.0.0" + postcss-value-parser "^3.0.0" + +postcss-normalize-string@^4.0.2: + version "4.0.2" + resolved "https://registry.yarnpkg.com/postcss-normalize-string/-/postcss-normalize-string-4.0.2.tgz#cd44c40ab07a0c7a36dc5e99aace1eca4ec2690c" + dependencies: + has "^1.0.0" + postcss "^7.0.0" + postcss-value-parser "^3.0.0" + +postcss-normalize-timing-functions@^4.0.2: + version "4.0.2" + resolved "https://registry.yarnpkg.com/postcss-normalize-timing-functions/-/postcss-normalize-timing-functions-4.0.2.tgz#8e009ca2a3949cdaf8ad23e6b6ab99cb5e7d28d9" + dependencies: + cssnano-util-get-match "^4.0.0" + postcss "^7.0.0" + postcss-value-parser "^3.0.0" + +postcss-normalize-unicode@^4.0.1: + version "4.0.1" + resolved "https://registry.yarnpkg.com/postcss-normalize-unicode/-/postcss-normalize-unicode-4.0.1.tgz#841bd48fdcf3019ad4baa7493a3d363b52ae1cfb" + dependencies: + browserslist "^4.0.0" + postcss "^7.0.0" + postcss-value-parser "^3.0.0" + +postcss-normalize-url@^4.0.1: + version "4.0.1" + resolved "https://registry.yarnpkg.com/postcss-normalize-url/-/postcss-normalize-url-4.0.1.tgz#10e437f86bc7c7e58f7b9652ed878daaa95faae1" + dependencies: + is-absolute-url "^2.0.0" + normalize-url "^3.0.0" + postcss "^7.0.0" + postcss-value-parser "^3.0.0" + +postcss-normalize-whitespace@^4.0.2: + version "4.0.2" + resolved "https://registry.yarnpkg.com/postcss-normalize-whitespace/-/postcss-normalize-whitespace-4.0.2.tgz#bf1d4070fe4fcea87d1348e825d8cc0c5faa7d82" + dependencies: + postcss "^7.0.0" + postcss-value-parser "^3.0.0" + +postcss-ordered-values@^4.1.2: + version "4.1.2" + resolved "https://registry.yarnpkg.com/postcss-ordered-values/-/postcss-ordered-values-4.1.2.tgz#0cf75c820ec7d5c4d280189559e0b571ebac0eee" + dependencies: + cssnano-util-get-arguments "^4.0.0" + postcss "^7.0.0" + postcss-value-parser "^3.0.0" + +postcss-overflow-shorthand@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/postcss-overflow-shorthand/-/postcss-overflow-shorthand-2.0.0.tgz#31ecf350e9c6f6ddc250a78f0c3e111f32dd4c30" + integrity sha512-aK0fHc9CBNx8jbzMYhshZcEv8LtYnBIRYQD5i7w/K/wS9c2+0NSR6B3OVMu5y0hBHYLcMGjfU+dmWYNKH0I85g== + dependencies: + postcss "^7.0.2" + +postcss-page-break@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/postcss-page-break/-/postcss-page-break-2.0.0.tgz#add52d0e0a528cabe6afee8b46e2abb277df46bf" + integrity sha512-tkpTSrLpfLfD9HvgOlJuigLuk39wVTbbd8RKcy8/ugV2bNBUW3xU+AIqyxhDrQr1VUj1RmyJrBn1YWrqUm9zAQ== + dependencies: + postcss "^7.0.2" + +postcss-place@^4.0.1: + version "4.0.1" + resolved "https://registry.yarnpkg.com/postcss-place/-/postcss-place-4.0.1.tgz#e9f39d33d2dc584e46ee1db45adb77ca9d1dcc62" + integrity sha512-Zb6byCSLkgRKLODj/5mQugyuj9bvAAw9LqJJjgwz5cYryGeXfFZfSXoP1UfveccFmeq0b/2xxwcTEVScnqGxBg== + dependencies: + postcss "^7.0.2" + postcss-values-parser "^2.0.0" + +postcss-preset-env@^6.7.0: + version "6.7.0" + resolved "https://registry.yarnpkg.com/postcss-preset-env/-/postcss-preset-env-6.7.0.tgz#c34ddacf8f902383b35ad1e030f178f4cdf118a5" + integrity sha512-eU4/K5xzSFwUFJ8hTdTQzo2RBLbDVt83QZrAvI07TULOkmyQlnYlpwep+2yIK+K+0KlZO4BvFcleOCCcUtwchg== + dependencies: + autoprefixer "^9.6.1" + browserslist "^4.6.4" + caniuse-lite "^1.0.30000981" + css-blank-pseudo "^0.1.4" + css-has-pseudo "^0.10.0" + css-prefers-color-scheme "^3.1.1" + cssdb "^4.4.0" + postcss "^7.0.17" + postcss-attribute-case-insensitive "^4.0.1" + postcss-color-functional-notation "^2.0.1" + postcss-color-gray "^5.0.0" + postcss-color-hex-alpha "^5.0.3" + postcss-color-mod-function "^3.0.3" + postcss-color-rebeccapurple "^4.0.1" + postcss-custom-media "^7.0.8" + postcss-custom-properties "^8.0.11" + postcss-custom-selectors "^5.1.2" + postcss-dir-pseudo-class "^5.0.0" + postcss-double-position-gradients "^1.0.0" + postcss-env-function "^2.0.2" + postcss-focus-visible "^4.0.0" + postcss-focus-within "^3.0.0" + postcss-font-variant "^4.0.0" + postcss-gap-properties "^2.0.0" + postcss-image-set-function "^3.0.1" + postcss-initial "^3.0.0" + postcss-lab-function "^2.0.1" + postcss-logical "^3.0.0" + postcss-media-minmax "^4.0.0" + postcss-nesting "^7.0.0" + postcss-overflow-shorthand "^2.0.0" + postcss-page-break "^2.0.0" + postcss-place "^4.0.1" + postcss-pseudo-class-any-link "^6.0.0" + postcss-replace-overflow-wrap "^3.0.0" + postcss-selector-matches "^4.0.0" + postcss-selector-not "^4.0.0" + +postcss-pseudo-class-any-link@^6.0.0: + version "6.0.0" + resolved "https://registry.yarnpkg.com/postcss-pseudo-class-any-link/-/postcss-pseudo-class-any-link-6.0.0.tgz#2ed3eed393b3702879dec4a87032b210daeb04d1" + integrity sha512-lgXW9sYJdLqtmw23otOzrtbDXofUdfYzNm4PIpNE322/swES3VU9XlXHeJS46zT2onFO7V1QFdD4Q9LiZj8mew== + dependencies: + postcss "^7.0.2" + postcss-selector-parser "^5.0.0-rc.3" + +postcss-reduce-initial@^4.0.3: + version "4.0.3" + resolved "https://registry.yarnpkg.com/postcss-reduce-initial/-/postcss-reduce-initial-4.0.3.tgz#7fd42ebea5e9c814609639e2c2e84ae270ba48df" + dependencies: + browserslist "^4.0.0" + caniuse-api "^3.0.0" + has "^1.0.0" + postcss "^7.0.0" + +postcss-reduce-transforms@^4.0.2: + version "4.0.2" + resolved "https://registry.yarnpkg.com/postcss-reduce-transforms/-/postcss-reduce-transforms-4.0.2.tgz#17efa405eacc6e07be3414a5ca2d1074681d4e29" + dependencies: + cssnano-util-get-match "^4.0.0" + has "^1.0.0" + postcss "^7.0.0" + postcss-value-parser "^3.0.0" + +postcss-replace-overflow-wrap@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/postcss-replace-overflow-wrap/-/postcss-replace-overflow-wrap-3.0.0.tgz#61b360ffdaedca84c7c918d2b0f0d0ea559ab01c" + integrity sha512-2T5hcEHArDT6X9+9dVSPQdo7QHzG4XKclFT8rU5TzJPDN7RIRTbO9c4drUISOVemLj03aezStHCR2AIcr8XLpw== + dependencies: + postcss "^7.0.2" + +postcss-safe-parser@^4.0.2: + version "4.0.2" + resolved "https://registry.yarnpkg.com/postcss-safe-parser/-/postcss-safe-parser-4.0.2.tgz#a6d4e48f0f37d9f7c11b2a581bf00f8ba4870b96" + integrity sha512-Uw6ekxSWNLCPesSv/cmqf2bY/77z11O7jZGPax3ycZMFU/oi2DMH9i89AdHc1tRwFg/arFoEwX0IS3LCUxJh1g== + dependencies: + postcss "^7.0.26" + +postcss-selector-matches@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/postcss-selector-matches/-/postcss-selector-matches-4.0.0.tgz#71c8248f917ba2cc93037c9637ee09c64436fcff" + integrity sha512-LgsHwQR/EsRYSqlwdGzeaPKVT0Ml7LAT6E75T8W8xLJY62CE4S/l03BWIt3jT8Taq22kXP08s2SfTSzaraoPww== + dependencies: + balanced-match "^1.0.0" + postcss "^7.0.2" + +postcss-selector-not@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/postcss-selector-not/-/postcss-selector-not-4.0.0.tgz#c68ff7ba96527499e832724a2674d65603b645c0" + integrity sha512-W+bkBZRhqJaYN8XAnbbZPLWMvZD1wKTu0UxtFKdhtGjWYmxhkUneoeOhRJKdAE5V7ZTlnbHfCR+6bNwK9e1dTQ== + dependencies: + balanced-match "^1.0.0" + postcss "^7.0.2" + +postcss-selector-parser@^3.0.0: + version "3.1.1" + resolved "https://registry.yarnpkg.com/postcss-selector-parser/-/postcss-selector-parser-3.1.1.tgz#4f875f4afb0c96573d5cf4d74011aee250a7e865" + dependencies: + dot-prop "^4.1.1" + indexes-of "^1.0.1" + uniq "^1.0.1" + +postcss-selector-parser@^5.0.0-rc.3, postcss-selector-parser@^5.0.0-rc.4: + version "5.0.0" + resolved "https://registry.yarnpkg.com/postcss-selector-parser/-/postcss-selector-parser-5.0.0.tgz#249044356697b33b64f1a8f7c80922dddee7195c" + dependencies: + cssesc "^2.0.0" + indexes-of "^1.0.1" + uniq "^1.0.1" + +postcss-selector-parser@^6.0.0, postcss-selector-parser@^6.0.2: + version "6.0.4" + resolved "https://registry.yarnpkg.com/postcss-selector-parser/-/postcss-selector-parser-6.0.4.tgz#56075a1380a04604c38b063ea7767a129af5c2b3" + integrity sha512-gjMeXBempyInaBqpp8gODmwZ52WaYsVOsfr4L4lDQ7n3ncD6mEyySiDtgzCT+NYC0mmeOLvtsF8iaEf0YT6dBw== + dependencies: + cssesc "^3.0.0" + indexes-of "^1.0.1" + uniq "^1.0.1" + util-deprecate "^1.0.2" + +postcss-svgo@^4.0.2: + version "4.0.2" + resolved "https://registry.yarnpkg.com/postcss-svgo/-/postcss-svgo-4.0.2.tgz#17b997bc711b333bab143aaed3b8d3d6e3d38258" + dependencies: + is-svg "^3.0.0" + postcss "^7.0.0" + postcss-value-parser "^3.0.0" + svgo "^1.0.0" + +postcss-unique-selectors@^4.0.1: + version "4.0.1" + resolved "https://registry.yarnpkg.com/postcss-unique-selectors/-/postcss-unique-selectors-4.0.1.tgz#9446911f3289bfd64c6d680f073c03b1f9ee4bac" + dependencies: + alphanum-sort "^1.0.0" + postcss "^7.0.0" + uniqs "^2.0.0" + +postcss-value-parser@^3.0.0, postcss-value-parser@^3.2.3, postcss-value-parser@^3.3.1: + version "3.3.1" + resolved "https://registry.yarnpkg.com/postcss-value-parser/-/postcss-value-parser-3.3.1.tgz#9ff822547e2893213cf1c30efa51ac5fd1ba8281" + +postcss-value-parser@^4.1.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/postcss-value-parser/-/postcss-value-parser-4.1.0.tgz#443f6a20ced6481a2bda4fa8532a6e55d789a2cb" + integrity sha512-97DXOFbQJhk71ne5/Mt6cOu6yxsSfM0QGQyl0L25Gca4yGWEGJaig7l7gbCX623VqTBNGLRLaVUCnNkcedlRSQ== + +postcss-values-parser@^2.0.0, postcss-values-parser@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/postcss-values-parser/-/postcss-values-parser-2.0.1.tgz#da8b472d901da1e205b47bdc98637b9e9e550e5f" + integrity sha512-2tLuBsA6P4rYTNKCXYG/71C7j1pU6pK503suYOmn4xYrQIzW+opD+7FAFNuGSdZC/3Qfy334QbeMu7MEb8gOxg== + dependencies: + flatten "^1.0.2" + indexes-of "^1.0.1" + uniq "^1.0.1" + +postcss@^7.0.0, postcss@^7.0.1, postcss@^7.0.2, postcss@^7.0.5: + version "7.0.17" + resolved "https://registry.yarnpkg.com/postcss/-/postcss-7.0.17.tgz#4da1bdff5322d4a0acaab4d87f3e782436bad31f" + dependencies: + chalk "^2.4.2" + source-map "^0.6.1" + supports-color "^6.1.0" + +postcss@^7.0.14, postcss@^7.0.17, postcss@^7.0.26, postcss@^7.0.32, postcss@^7.0.6: + version "7.0.35" + resolved "https://registry.yarnpkg.com/postcss/-/postcss-7.0.35.tgz#d2be00b998f7f211d8a276974079f2e92b970e24" + integrity sha512-3QT8bBJeX/S5zKTTjTCIjRF3If4avAT6kqxcASlTWEtAFCb9NH0OUxNDfgZSWdP5fJnBYCMEWkIFfWeugjzYMg== + dependencies: + chalk "^2.4.2" + source-map "^0.6.1" + supports-color "^6.1.0" + +prelude-ls@^1.2.1: + version "1.2.1" + resolved "https://registry.yarnpkg.com/prelude-ls/-/prelude-ls-1.2.1.tgz#debc6489d7a6e6b0e7611888cec880337d316396" + integrity sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g== + +prepend-http@^1.0.0: + version "1.0.4" + resolved "https://registry.yarnpkg.com/prepend-http/-/prepend-http-1.0.4.tgz#d4f4562b0ce3696e41ac52d0e002e57a635dc6dc" + +private@^0.1.6, private@~0.1.5: + version "0.1.8" + resolved "https://registry.yarnpkg.com/private/-/private-0.1.8.tgz#2381edb3689f7a53d653190060fcf822d2f368ff" + +process-nextick-args@~2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/process-nextick-args/-/process-nextick-args-2.0.1.tgz#7820d9b16120cc55ca9ae7792680ae7dba6d7fe2" + +process@^0.11.10, process@~0.11.0: + version "0.11.10" + resolved "https://registry.yarnpkg.com/process/-/process-0.11.10.tgz#7332300e840161bda3e69a1d1d91a7d4bc16f182" + +progress@^2.0.0: + version "2.0.3" + resolved "https://registry.yarnpkg.com/progress/-/progress-2.0.3.tgz#7e8cf8d8f5b8f239c1bc68beb4eb78567d572ef8" + integrity sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA== + +promise-inflight@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/promise-inflight/-/promise-inflight-1.0.1.tgz#98472870bf228132fcbdd868129bad12c3c029e3" + +prop-types@15.x, prop-types@^15.5.10, prop-types@^15.5.4, prop-types@^15.5.6, prop-types@^15.5.8, prop-types@^15.6.1, prop-types@^15.6.2, prop-types@^15.7.2: + version "15.7.2" + resolved "https://registry.yarnpkg.com/prop-types/-/prop-types-15.7.2.tgz#52c41e75b8c87e72b9d9360e0206b99dcbffa6c5" + dependencies: + loose-envify "^1.4.0" + object-assign "^4.1.1" + react-is "^16.8.1" + +proxy-addr@~2.0.5: + version "2.0.5" + resolved "https://registry.yarnpkg.com/proxy-addr/-/proxy-addr-2.0.5.tgz#34cbd64a2d81f4b1fd21e76f9f06c8a45299ee34" + dependencies: + forwarded "~0.1.2" + ipaddr.js "1.9.0" + +prr@~1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/prr/-/prr-1.0.1.tgz#d3fc114ba06995a45ec6893f484ceb1d78f5f476" + +pseudomap@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/pseudomap/-/pseudomap-1.0.2.tgz#f052a28da70e618917ef0a8ac34c1ae5a68286b3" + +psl@^1.1.24: + version "1.3.0" + resolved "https://registry.yarnpkg.com/psl/-/psl-1.3.0.tgz#e1ebf6a3b5564fa8376f3da2275da76d875ca1bd" + +public-encrypt@^4.0.0: + version "4.0.3" + resolved "https://registry.yarnpkg.com/public-encrypt/-/public-encrypt-4.0.3.tgz#4fcc9d77a07e48ba7527e7cbe0de33d0701331e0" + dependencies: + bn.js "^4.1.0" + browserify-rsa "^4.0.0" + create-hash "^1.1.0" + parse-asn1 "^5.0.0" + randombytes "^2.0.1" + safe-buffer "^5.1.2" + +pump@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/pump/-/pump-2.0.1.tgz#12399add6e4cf7526d973cbc8b5ce2e2908b3909" + dependencies: + end-of-stream "^1.1.0" + once "^1.3.1" + +pump@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/pump/-/pump-3.0.0.tgz#b4a2116815bde2f4e1ea602354e8c75565107a64" + integrity sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww== + dependencies: + end-of-stream "^1.1.0" + once "^1.3.1" + +pumpify@^1.3.3: + version "1.5.1" + resolved "https://registry.yarnpkg.com/pumpify/-/pumpify-1.5.1.tgz#36513be246ab27570b1a374a5ce278bfd74370ce" + dependencies: + duplexify "^3.6.0" + inherits "^2.0.3" + pump "^2.0.0" + +punycode@1.3.2: + version "1.3.2" + resolved "https://registry.yarnpkg.com/punycode/-/punycode-1.3.2.tgz#9653a036fb7c1ee42342f2325cceefea3926c48d" + +punycode@^1.2.4, punycode@^1.3.2, punycode@^1.4.1: + version "1.4.1" + resolved "https://registry.yarnpkg.com/punycode/-/punycode-1.4.1.tgz#c0d5a63b2718800ad8e1eb0fa5269c84dd41845e" + +punycode@^2.1.0: + version "2.1.1" + resolved "https://registry.yarnpkg.com/punycode/-/punycode-2.1.1.tgz#b58b010ac40c22c5657616c8d2c2c02c7bf479ec" + +q@^1.1.2: + version "1.5.1" + resolved "https://registry.yarnpkg.com/q/-/q-1.5.1.tgz#7e32f75b41381291d04611f1bf14109ac00651d7" + +qs@6.7.0: + version "6.7.0" + resolved "https://registry.yarnpkg.com/qs/-/qs-6.7.0.tgz#41dc1a015e3d581f1621776be31afb2876a9b1bc" + +qs@~6.5.2: + version "6.5.2" + resolved "https://registry.yarnpkg.com/qs/-/qs-6.5.2.tgz#cb3ae806e8740444584ef154ce8ee98d403f3e36" + +query-string@^4.1.0: + version "4.3.4" + resolved "https://registry.yarnpkg.com/query-string/-/query-string-4.3.4.tgz#bbb693b9ca915c232515b228b1a02b609043dbeb" + dependencies: + object-assign "^4.1.0" + strict-uri-encode "^1.0.0" + +querystring-es3@^0.2.0, querystring-es3@~0.2.0: + version "0.2.1" + resolved "https://registry.yarnpkg.com/querystring-es3/-/querystring-es3-0.2.1.tgz#9ec61f79049875707d69414596fd907a4d711e73" + +querystring@0.2.0: + version "0.2.0" + resolved "https://registry.yarnpkg.com/querystring/-/querystring-0.2.0.tgz#b209849203bb25df820da756e747005878521620" + +querystringify@^2.1.1: + version "2.1.1" + resolved "https://registry.yarnpkg.com/querystringify/-/querystringify-2.1.1.tgz#60e5a5fd64a7f8bfa4d2ab2ed6fdf4c85bad154e" + +raf-schd@^4.0.2: + version "4.0.2" + resolved "https://registry.yarnpkg.com/raf-schd/-/raf-schd-4.0.2.tgz#bd44c708188f2e84c810bf55fcea9231bcaed8a0" + integrity sha512-VhlMZmGy6A6hrkJWHLNTGl5gtgMUm+xfGza6wbwnE914yeQ5Ybm18vgM734RZhMgfw4tacUrWseGZlpUrrakEQ== + +raf@^3.4.0: + version "3.4.1" + resolved "https://registry.yarnpkg.com/raf/-/raf-3.4.1.tgz#0742e99a4a6552f445d73e3ee0328af0ff1ede39" + dependencies: + performance-now "^2.1.0" + +randombytes@^2.0.0, randombytes@^2.0.1, randombytes@^2.0.5, randombytes@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/randombytes/-/randombytes-2.1.0.tgz#df6f84372f0270dc65cdf6291349ab7a473d4f2a" + dependencies: + safe-buffer "^5.1.0" + +randomfill@^1.0.3: + version "1.0.4" + resolved "https://registry.yarnpkg.com/randomfill/-/randomfill-1.0.4.tgz#c92196fc86ab42be983f1bf31778224931d61458" + dependencies: + randombytes "^2.0.5" + safe-buffer "^5.1.0" + +range-parser@^1.2.1, range-parser@~1.2.1: + version "1.2.1" + resolved "https://registry.yarnpkg.com/range-parser/-/range-parser-1.2.1.tgz#3cf37023d199e1c24d1a55b84800c2f3e6468031" + +raw-body@2.4.0: + version "2.4.0" + resolved "https://registry.yarnpkg.com/raw-body/-/raw-body-2.4.0.tgz#a1ce6fb9c9bc356ca52e89256ab59059e13d0332" + dependencies: + bytes "3.1.0" + http-errors "1.7.2" + iconv-lite "0.4.24" + unpipe "1.0.0" + +rc-align@^2.4.0: + version "2.4.5" + resolved "https://registry.yarnpkg.com/rc-align/-/rc-align-2.4.5.tgz#c941a586f59d1017f23a428f0b468663fb7102ab" + dependencies: + babel-runtime "^6.26.0" + dom-align "^1.7.0" + prop-types "^15.5.8" + rc-util "^4.0.4" + +rc-animate@2.x: + version "2.9.2" + resolved "https://registry.yarnpkg.com/rc-animate/-/rc-animate-2.9.2.tgz#5964767805c886f1bdc7563d3935a74912a0b78f" + dependencies: + babel-runtime "6.x" + classnames "^2.2.6" + css-animation "^1.3.2" + prop-types "15.x" + raf "^3.4.0" + rc-util "^4.8.0" + react-lifecycles-compat "^3.0.4" + +rc-slider@Galvanize-IT/slider#c569ca3b11979aced8306e6c02f37466cb7cd365: + version "8.6.13" + resolved "https://codeload.github.com/Galvanize-IT/slider/tar.gz/c569ca3b11979aced8306e6c02f37466cb7cd365" + dependencies: + babel-runtime "6.x" + classnames "^2.2.5" + gh-pages "^2.0.1" + prop-types "^15.5.4" + rc-tooltip "^3.7.0" + rc-util "^4.0.4" + shallowequal "^1.0.1" + warning "^4.0.3" + +rc-tooltip@^3.7.0: + version "3.7.3" + resolved "https://registry.yarnpkg.com/rc-tooltip/-/rc-tooltip-3.7.3.tgz#280aec6afcaa44e8dff0480fbaff9e87fc00aecc" + dependencies: + babel-runtime "6.x" + prop-types "^15.5.8" + rc-trigger "^2.2.2" + +rc-trigger@^2.2.2: + version "2.6.5" + resolved "https://registry.yarnpkg.com/rc-trigger/-/rc-trigger-2.6.5.tgz#140a857cf28bd0fa01b9aecb1e26a50a700e9885" + dependencies: + babel-runtime "6.x" + classnames "^2.2.6" + prop-types "15.x" + rc-align "^2.4.0" + rc-animate "2.x" + rc-util "^4.4.0" + react-lifecycles-compat "^3.0.4" + +rc-util@^4.0.4, rc-util@^4.4.0, rc-util@^4.8.0: + version "4.11.0" + resolved "https://registry.yarnpkg.com/rc-util/-/rc-util-4.11.0.tgz#cf437dcff74ca08a8565ae14f0368acb3a650796" + dependencies: + add-dom-event-listener "^1.1.0" + babel-runtime "6.x" + prop-types "^15.5.10" + react-lifecycles-compat "^3.0.4" + shallowequal "^0.2.2" + +rc@^1.2.7: + version "1.2.8" + resolved "https://registry.yarnpkg.com/rc/-/rc-1.2.8.tgz#cd924bf5200a075b83c188cd6b9e211b7fc0d3ed" + dependencies: + deep-extend "^0.6.0" + ini "~1.3.0" + minimist "^1.2.0" + strip-json-comments "~2.0.1" + +react-addons-css-transition-group@^15.6.2: + version "15.6.2" + resolved "https://registry.yarnpkg.com/react-addons-css-transition-group/-/react-addons-css-transition-group-15.6.2.tgz#9e4376bcf40b5217d14ec68553081cee4b08a6d6" + dependencies: + react-transition-group "^1.2.0" + +react-addons-test-utils@^15.6.2: + version "15.6.2" + resolved "https://registry.yarnpkg.com/react-addons-test-utils/-/react-addons-test-utils-15.6.2.tgz#c12b6efdc2247c10da7b8770d185080a7b047156" + integrity sha1-wStu/cIkfBDae4dw0YUICnsEcVY= + +react-beautiful-dnd@^13.0.0: + version "13.0.0" + resolved "https://registry.yarnpkg.com/react-beautiful-dnd/-/react-beautiful-dnd-13.0.0.tgz#f70cc8ff82b84bc718f8af157c9f95757a6c3b40" + integrity sha512-87It8sN0ineoC3nBW0SbQuTFXM6bUqM62uJGY4BtTf0yzPl8/3+bHMWkgIe0Z6m8e+gJgjWxefGRVfpE3VcdEg== + dependencies: + "@babel/runtime" "^7.8.4" + css-box-model "^1.2.0" + memoize-one "^5.1.1" + raf-schd "^4.0.2" + react-redux "^7.1.1" + redux "^4.0.4" + use-memo-one "^1.1.1" + +react-copy-to-clipboard@^5.0.3: + version "5.0.3" + resolved "https://registry.yarnpkg.com/react-copy-to-clipboard/-/react-copy-to-clipboard-5.0.3.tgz#2a0623b1115a1d8c84144e9434d3342b5af41ab4" + integrity sha512-9S3j+m+UxDZOM0Qb8mhnT/rMR0NGSrj9A/073yz2DSxPMYhmYFBMYIdI2X4o8AjOjyFsSNxDRnCX6s/gRxpriw== + dependencies: + copy-to-clipboard "^3" + prop-types "^15.5.8" + +react-datepicker@^3.4.1: + version "3.4.1" + resolved "https://registry.yarnpkg.com/react-datepicker/-/react-datepicker-3.4.1.tgz#3c8e8989f1ab31a767c17170a2d1318aa3c3b9ef" + integrity sha512-ASyVb7UmVx1vzeITidD7Cr/EXRXhKyjjbSkBndPc1MipYq4rqQ3eMFgvRQzpsXc3JmIMFgICm7nmN6Otc1GE/Q== + dependencies: + classnames "^2.2.6" + date-fns "^2.0.1" + prop-types "^15.7.2" + react-onclickoutside "^6.9.0" + react-popper "^1.3.4" + +react-dom@^17.0.1: + version "17.0.1" + resolved "https://registry.yarnpkg.com/react-dom/-/react-dom-17.0.1.tgz#1de2560474ec9f0e334285662ede52dbc5426fc6" + integrity sha512-6eV150oJZ9U2t9svnsspTMrWNyHc6chX0KzDeAOXftRa8bNeOKTTfCJ7KorIwenkHd2xqVTBTCZd79yk/lx/Ug== + dependencies: + loose-envify "^1.1.0" + object-assign "^4.1.1" + scheduler "^0.20.1" + +react-is@^16.7.0, react-is@^16.8.1: + version "16.9.0" + resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.9.0.tgz#21ca9561399aad0ff1a7701c01683e8ca981edcb" + +react-is@^16.9.0: + version "16.13.1" + resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.13.1.tgz#789729a4dc36de2999dc156dd6c1d9c18cea56a4" + integrity sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ== + +react-json-pretty@^2.2.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/react-json-pretty/-/react-json-pretty-2.2.0.tgz#9ba907d2b08d87e90456d87b6025feeceb8f63cf" + integrity sha512-3UMzlAXkJ4R8S4vmkRKtvJHTewG4/rn1Q18n0zqdu/ipZbUPLVZD+QwC7uVcD/IAY3s8iNVHlgR2dMzIUS0n1A== + dependencies: + prop-types "^15.6.2" + +react-lifecycles-compat@^3.0.4: + version "3.0.4" + resolved "https://registry.yarnpkg.com/react-lifecycles-compat/-/react-lifecycles-compat-3.0.4.tgz#4f1a273afdfc8f3488a8c516bfda78f872352362" + +react-onclickoutside@^6.9.0: + version "6.9.0" + resolved "https://registry.yarnpkg.com/react-onclickoutside/-/react-onclickoutside-6.9.0.tgz#a54bc317ae8cf6131a5d78acea55a11067f37a1f" + integrity sha512-8ltIY3bC7oGhj2nPAvWOGi+xGFybPNhJM0V1H8hY/whNcXgmDeaeoCMPPd8VatrpTsUWjb/vGzrmu6SrXVty3A== + +react-paginate@^7.0.0: + version "7.0.0" + resolved "https://registry.yarnpkg.com/react-paginate/-/react-paginate-7.0.0.tgz#af206ef92a2b6ef87646855eb1612157254ad0c0" + integrity sha512-mzPwHGJfSs79JBGX2V0v/FfQp3yWdz0XRrB9JvsUbJdsxqCt4osk1O669+K8VPQ0Lh9v0lJsnLLoJwnsgdJFng== + dependencies: + prop-types "^15.6.1" + +react-pdf@^4.2.0: + version "4.2.0" + resolved "https://registry.yarnpkg.com/react-pdf/-/react-pdf-4.2.0.tgz#b83a01eb070912522075b7a51aee7d63b2c912ed" + integrity sha512-Ao44mZszfPwtCUsrXHtXnhM+czTvPxfG5wqssbWgj2onL70TKSOKGzQfCH4csCnNDOKji/Pc/s0Og70/VOE+Rg== + dependencies: + "@babel/runtime" "^7.0.0" + make-cancellable-promise "^1.0.0" + make-event-props "^1.1.0" + merge-class-names "^1.1.1" + pdfjs-dist "2.1.266" + prop-types "^15.6.2" + +react-popper@^1.3.4: + version "1.3.7" + resolved "https://registry.yarnpkg.com/react-popper/-/react-popper-1.3.7.tgz#f6a3471362ef1f0d10a4963673789de1baca2324" + integrity sha512-nmqYTx7QVjCm3WUZLeuOomna138R1luC4EqkW3hxJUrAe+3eNz3oFCLYdnPwILfn0mX1Ew2c3wctrjlUMYYUww== + dependencies: + "@babel/runtime" "^7.1.2" + create-react-context "^0.3.0" + deep-equal "^1.1.1" + popper.js "^1.14.4" + prop-types "^15.6.1" + typed-styles "^0.0.7" + warning "^4.0.2" + +react-redux@^7.1.1: + version "7.2.1" + resolved "https://registry.yarnpkg.com/react-redux/-/react-redux-7.2.1.tgz#8dedf784901014db2feca1ab633864dee68ad985" + integrity sha512-T+VfD/bvgGTUA74iW9d2i5THrDQWbweXP0AVNI8tNd1Rk5ch1rnMiJkDD67ejw7YBKM4+REvcvqRuWJb7BLuEg== + dependencies: + "@babel/runtime" "^7.5.5" + hoist-non-react-statics "^3.3.0" + loose-envify "^1.4.0" + prop-types "^15.7.2" + react-is "^16.9.0" + +react-scroll-sync@^0.8.0: + version "0.8.0" + resolved "https://registry.yarnpkg.com/react-scroll-sync/-/react-scroll-sync-0.8.0.tgz#61fabed2afc47d41e6938819d620799da3610548" + integrity sha512-Ms9srm41UM+lWexuqdocXjqaqqt6ZRSFxcudgB0sYhC7Or+m12WemTwY8BaQCRf7hA8zHDk55FHvMkqsH7gF+w== + +react-sizeme@^2.6.12: + version "2.6.12" + resolved "https://registry.yarnpkg.com/react-sizeme/-/react-sizeme-2.6.12.tgz#ed207be5476f4a85bf364e92042520499455453e" + integrity sha512-tL4sCgfmvapYRZ1FO2VmBmjPVzzqgHA7kI8lSJ6JS6L78jXFNRdOZFpXyK6P1NBZvKPPCZxReNgzZNUajAerZw== + dependencies: + element-resize-detector "^1.2.1" + invariant "^2.2.4" + shallowequal "^1.1.0" + throttle-debounce "^2.1.0" + +react-textarea-autosize@^8.3.0: + version "8.3.0" + resolved "https://registry.yarnpkg.com/react-textarea-autosize/-/react-textarea-autosize-8.3.0.tgz#e6e2fd186d9f61bb80ac6e2dcb4c55504f93c2fa" + integrity sha512-3GLWFAan2pbwBeoeNDoqGmSbrShORtgWfaWX0RJDivsUrpShh01saRM5RU/i4Zmf+whpBVEY5cA90Eq8Ub1N3w== + dependencies: + "@babel/runtime" "^7.10.2" + use-composed-ref "^1.0.0" + use-latest "^1.0.0" + +react-tools@~0.13.0: + version "0.13.3" + resolved "https://registry.yarnpkg.com/react-tools/-/react-tools-0.13.3.tgz#da6ac7d4d7777a59a5e951cf46e72fd4b6b40a2c" + dependencies: + commoner "^0.10.0" + jstransform "^10.1.0" + +react-tooltip@^4.2.13: + version "4.2.13" + resolved "https://registry.yarnpkg.com/react-tooltip/-/react-tooltip-4.2.13.tgz#908db8a41dc10ae2ae9cc1864746cde939aaab0f" + integrity sha512-iAZ02wSxChLWb7Vnu0zeQMyAo/jiGHrwFNILWaR3pCKaFVRjKcv/B6TBI4+Xd66xLXVzLngwJ91Tf5D+mqAqVA== + dependencies: + prop-types "^15.7.2" + uuid "^7.0.3" + +react-transition-group@^1.2.0: + version "1.2.1" + resolved "https://registry.yarnpkg.com/react-transition-group/-/react-transition-group-1.2.1.tgz#e11f72b257f921b213229a774df46612346c7ca6" + dependencies: + chain-function "^1.0.0" + dom-helpers "^3.2.0" + loose-envify "^1.3.1" + prop-types "^15.5.6" + warning "^3.0.0" + +react@^17.0.1: + version "17.0.1" + resolved "https://registry.yarnpkg.com/react/-/react-17.0.1.tgz#6e0600416bd57574e3f86d92edba3d9008726127" + integrity sha512-lG9c9UuMHdcAexXtigOZLX8exLWkW0Ku29qPRU8uhF2R9BN96dLCt0psvzPLlHc5OWkgymP3qwTRgbnw5BKx3w== + dependencies: + loose-envify "^1.1.0" + object-assign "^4.1.1" + +react_ujs@^2.6.0: + version "2.6.0" + resolved "https://registry.yarnpkg.com/react_ujs/-/react_ujs-2.6.0.tgz#98c53cc40b2b3b8558300e19394638d45d37a177" + dependencies: + react_ujs "^2.6.0" + +react_ujs@^2.6.1: + version "2.6.1" + resolved "https://registry.yarnpkg.com/react_ujs/-/react_ujs-2.6.1.tgz#a202a33c95c9e2bb18ab56926b7e79f3325ec855" + integrity sha512-9M33/A8cubStkZ2cpJSimcTD0RlCWiqXF6e90IQmMw/Caf/W0dtAzOtHtiQE3JjLbt/nhRR7NLPxMfnlm141ig== + dependencies: + react_ujs "^2.6.0" + +reactify@^1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/reactify/-/reactify-1.1.1.tgz#a8f119596273c0d4bfb1abea0c14c2601ea03bba" + dependencies: + react-tools "~0.13.0" + through "~2.3.4" + +read-cache@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/read-cache/-/read-cache-1.0.0.tgz#e664ef31161166c9751cdbe8dbcf86b5fb58f774" + dependencies: + pify "^2.3.0" + +read-only-stream@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/read-only-stream/-/read-only-stream-2.0.0.tgz#2724fd6a8113d73764ac288d4386270c1dbf17f0" + dependencies: + readable-stream "^2.0.2" + +read-pkg-up@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/read-pkg-up/-/read-pkg-up-1.0.1.tgz#9d63c13276c065918d57f002a57f40a1b643fb02" + dependencies: + find-up "^1.0.0" + read-pkg "^1.0.0" + +read-pkg-up@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/read-pkg-up/-/read-pkg-up-2.0.0.tgz#6b72a8048984e0c41e79510fd5e9fa99b3b549be" + dependencies: + find-up "^2.0.0" + read-pkg "^2.0.0" + +read-pkg@^1.0.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/read-pkg/-/read-pkg-1.1.0.tgz#f5ffaa5ecd29cb31c0474bca7d756b6bb29e3f28" + dependencies: + load-json-file "^1.0.0" + normalize-package-data "^2.3.2" + path-type "^1.0.0" + +read-pkg@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/read-pkg/-/read-pkg-2.0.0.tgz#8ef1c0623c6a6db0dc6713c4bfac46332b2368f8" + dependencies: + load-json-file "^2.0.0" + normalize-package-data "^2.3.2" + path-type "^2.0.0" + +"readable-stream@1 || 2", readable-stream@^2.0.0, readable-stream@^2.0.1, readable-stream@^2.0.2, readable-stream@^2.0.6, readable-stream@^2.1.5, readable-stream@^2.2.2, readable-stream@^2.3.3, readable-stream@^2.3.6, readable-stream@~2.3.6: + version "2.3.6" + resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-2.3.6.tgz#b11c27d88b8ff1fbe070643cf94b0c79ae1b0aaf" + dependencies: + core-util-is "~1.0.0" + inherits "~2.0.3" + isarray "~1.0.0" + process-nextick-args "~2.0.0" + safe-buffer "~5.1.1" + string_decoder "~1.1.1" + util-deprecate "~1.0.1" + +readable-stream@^3.0.6, readable-stream@^3.6.0: + version "3.6.0" + resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-3.6.0.tgz#337bbda3adc0706bd3e024426a286d4b4b2c9198" + integrity sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA== + dependencies: + inherits "^2.0.3" + string_decoder "^1.1.1" + util-deprecate "^1.0.1" + +readdirp@^2.2.1: + version "2.2.1" + resolved "https://registry.yarnpkg.com/readdirp/-/readdirp-2.2.1.tgz#0e87622a3325aa33e892285caf8b4e846529a525" + dependencies: + graceful-fs "^4.1.11" + micromatch "^3.1.10" + readable-stream "^2.0.2" + +readdirp@~3.4.0: + version "3.4.0" + resolved "https://registry.yarnpkg.com/readdirp/-/readdirp-3.4.0.tgz#9fdccdf9e9155805449221ac645e8303ab5b9ada" + integrity sha512-0xe001vZBnJEK+uKcj8qOhyAKPzIT+gStxWr3LCB0DwcXR5NZJ3IaC+yGnHCYzB/S7ov3m3EEbZI2zeNvX+hGQ== + dependencies: + picomatch "^2.2.1" + +recast@^0.11.17: + version "0.11.23" + resolved "https://registry.yarnpkg.com/recast/-/recast-0.11.23.tgz#451fd3004ab1e4df9b4e4b66376b2a21912462d3" + dependencies: + ast-types "0.9.6" + esprima "~3.1.0" + private "~0.1.5" + source-map "~0.5.0" + +redent@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/redent/-/redent-1.0.0.tgz#cf916ab1fd5f1f16dfb20822dd6ec7f730c2afde" + dependencies: + indent-string "^2.1.0" + strip-indent "^1.0.1" + +redux@^4.0.4: + version "4.0.5" + resolved "https://registry.yarnpkg.com/redux/-/redux-4.0.5.tgz#4db5de5816e17891de8a80c424232d06f051d93f" + integrity sha512-VSz1uMAH24DM6MF72vcojpYPtrTUu3ByVWfPL1nPfVRb5mZVTve5GnNCUV53QM/BZ66xfWrm0CTWoM+Xlz8V1w== + dependencies: + loose-envify "^1.4.0" + symbol-observable "^1.2.0" + +regenerate-unicode-properties@^8.2.0: + version "8.2.0" + resolved "https://registry.yarnpkg.com/regenerate-unicode-properties/-/regenerate-unicode-properties-8.2.0.tgz#e5de7111d655e7ba60c057dbe9ff37c87e65cdec" + integrity sha512-F9DjY1vKLo/tPePDycuH3dn9H1OTPIkVD9Kz4LODu+F2C75mgjAJ7x/gwy6ZcSNRAAkhNlJSOHRe8k3p+K9WhA== + dependencies: + regenerate "^1.4.0" + +regenerate@^1.4.0: + version "1.4.1" + resolved "https://registry.yarnpkg.com/regenerate/-/regenerate-1.4.1.tgz#cad92ad8e6b591773485fbe05a485caf4f457e6f" + integrity sha512-j2+C8+NtXQgEKWk49MMP5P/u2GhnahTtVkRIHr5R5lVRlbKvmQ+oS+A5aLKWp2ma5VkT8sh6v+v4hbH0YHR66A== + +regenerator-runtime@^0.11.0: + version "0.11.1" + resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.11.1.tgz#be05ad7f9bf7d22e056f9726cee5017fbf19e2e9" + +regenerator-runtime@^0.13.2: + version "0.13.3" + resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.13.3.tgz#7cf6a77d8f5c6f60eb73c5fc1955b2ceb01e6bf5" + +regenerator-runtime@^0.13.4, regenerator-runtime@^0.13.7: + version "0.13.7" + resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.13.7.tgz#cac2dacc8a1ea675feaabaeb8ae833898ae46f55" + integrity sha512-a54FxoJDIr27pgf7IgeQGxmqUNYrcV338lf/6gH456HZ/PhX+5BcwHXG9ajESmwe6WRO0tAzRUrRmNONWgkrew== + +regenerator-transform@^0.14.2: + version "0.14.5" + resolved "https://registry.yarnpkg.com/regenerator-transform/-/regenerator-transform-0.14.5.tgz#c98da154683671c9c4dcb16ece736517e1b7feb4" + integrity sha512-eOf6vka5IO151Jfsw2NO9WpGX58W6wWmefK3I1zEGr0lOD0u8rwPaNqQL1aRxUaxLeKO3ArNh3VYg1KbaD+FFw== + dependencies: + "@babel/runtime" "^7.8.4" + +regex-not@^1.0.0, regex-not@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/regex-not/-/regex-not-1.0.2.tgz#1f4ece27e00b0b65e0247a6810e6a85d83a5752c" + dependencies: + extend-shallow "^3.0.2" + safe-regex "^1.1.0" + +regexp.prototype.flags@^1.2.0, regexp.prototype.flags@^1.3.0: + version "1.3.0" + resolved "https://registry.yarnpkg.com/regexp.prototype.flags/-/regexp.prototype.flags-1.3.0.tgz#7aba89b3c13a64509dabcf3ca8d9fbb9bdf5cb75" + integrity sha512-2+Q0C5g951OlYlJz6yu5/M33IcsESLlLfsyIaLJaG4FA2r4yP8MvVMJUUP/fVBkSpbbbZlS5gynbEWLipiiXiQ== + dependencies: + define-properties "^1.1.3" + es-abstract "^1.17.0-next.1" + +regexpp@^3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/regexpp/-/regexpp-3.1.0.tgz#206d0ad0a5648cffbdb8ae46438f3dc51c9f78e2" + integrity sha512-ZOIzd8yVsQQA7j8GCSlPGXwg5PfmA1mrq0JP4nGhh54LaKN3xdai/vHUDu74pKwV8OxseMS65u2NImosQcSD0Q== + +regexpu-core@^4.7.0: + version "4.7.1" + resolved "https://registry.yarnpkg.com/regexpu-core/-/regexpu-core-4.7.1.tgz#2dea5a9a07233298fbf0db91fa9abc4c6e0f8ad6" + integrity sha512-ywH2VUraA44DZQuRKzARmw6S66mr48pQVva4LBeRhcOltJ6hExvWly5ZjFLYo67xbIxb6W1q4bAGtgfEl20zfQ== + dependencies: + regenerate "^1.4.0" + regenerate-unicode-properties "^8.2.0" + regjsgen "^0.5.1" + regjsparser "^0.6.4" + unicode-match-property-ecmascript "^1.0.4" + unicode-match-property-value-ecmascript "^1.2.0" + +regjsgen@^0.5.1: + version "0.5.2" + resolved "https://registry.yarnpkg.com/regjsgen/-/regjsgen-0.5.2.tgz#92ff295fb1deecbf6ecdab2543d207e91aa33733" + integrity sha512-OFFT3MfrH90xIW8OOSyUrk6QHD5E9JOTeGodiJeBS3J6IwlgzJMNE/1bZklWz5oTg+9dCMyEetclvCVXOPoN3A== + +regjsparser@^0.6.4: + version "0.6.4" + resolved "https://registry.yarnpkg.com/regjsparser/-/regjsparser-0.6.4.tgz#a769f8684308401a66e9b529d2436ff4d0666272" + integrity sha512-64O87/dPDgfk8/RQqC4gkZoGyyWFIEUTTh80CU6CWuK5vkCGyekIx+oKcEIYtP/RAxSQltCZHCNu/mdd7fqlJw== + dependencies: + jsesc "~0.5.0" + +remove-trailing-separator@^1.0.1: + version "1.1.0" + resolved "https://registry.yarnpkg.com/remove-trailing-separator/-/remove-trailing-separator-1.1.0.tgz#c24bce2a283adad5bc3f58e0d48249b92379d8ef" + +repeat-element@^1.1.2: + version "1.1.3" + resolved "https://registry.yarnpkg.com/repeat-element/-/repeat-element-1.1.3.tgz#782e0d825c0c5a3bb39731f84efee6b742e6b1ce" + +repeat-string@^1.6.1: + version "1.6.1" + resolved "https://registry.yarnpkg.com/repeat-string/-/repeat-string-1.6.1.tgz#8dcae470e1c88abc2d600fff4a776286da75e637" + +repeating@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/repeating/-/repeating-2.0.1.tgz#5214c53a926d3552707527fbab415dbc08d06dda" + dependencies: + is-finite "^1.0.0" + +request@^2.87.0, request@^2.88.0: + version "2.88.0" + resolved "https://registry.yarnpkg.com/request/-/request-2.88.0.tgz#9c2fca4f7d35b592efe57c7f0a55e81052124fef" + dependencies: + aws-sign2 "~0.7.0" + aws4 "^1.8.0" + caseless "~0.12.0" + combined-stream "~1.0.6" + extend "~3.0.2" + forever-agent "~0.6.1" + form-data "~2.3.2" + har-validator "~5.1.0" + http-signature "~1.2.0" + is-typedarray "~1.0.0" + isstream "~0.1.2" + json-stringify-safe "~5.0.1" + mime-types "~2.1.19" + oauth-sign "~0.9.0" + performance-now "^2.1.0" + qs "~6.5.2" + safe-buffer "^5.1.2" + tough-cookie "~2.4.3" + tunnel-agent "^0.6.0" + uuid "^3.3.2" + +require-directory@^2.1.1: + version "2.1.1" + resolved "https://registry.yarnpkg.com/require-directory/-/require-directory-2.1.1.tgz#8c64ad5fd30dab1c976e2344ffe7f792a6a6df42" + +require-from-string@^2.0.2: + version "2.0.2" + resolved "https://registry.yarnpkg.com/require-from-string/-/require-from-string-2.0.2.tgz#89a7fdd938261267318eafe14f9c32e598c36909" + integrity sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw== + +require-main-filename@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/require-main-filename/-/require-main-filename-2.0.0.tgz#d0b329ecc7cc0f61649f62215be69af54aa8989b" + integrity sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg== + +requires-port@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/requires-port/-/requires-port-1.0.0.tgz#925d2601d39ac485e091cf0da5c6e694dc3dcaff" + +resolve-cwd@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/resolve-cwd/-/resolve-cwd-2.0.0.tgz#00a9f7387556e27038eae232caa372a6a59b665a" + dependencies: + resolve-from "^3.0.0" + +resolve-dir@^1.0.0, resolve-dir@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/resolve-dir/-/resolve-dir-1.0.1.tgz#79a40644c362be82f26effe739c9bb5382046f43" + integrity sha1-eaQGRMNivoLybv/nOcm7U4IEb0M= + dependencies: + expand-tilde "^2.0.0" + global-modules "^1.0.0" + +resolve-from@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/resolve-from/-/resolve-from-3.0.0.tgz#b22c7af7d9d6881bc8b6e653335eebcb0a188748" + +resolve-from@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/resolve-from/-/resolve-from-4.0.0.tgz#4abcd852ad32dd7baabfe9b40e00a36db5f392e6" + integrity sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g== + +resolve-url@^0.2.1: + version "0.2.1" + resolved "https://registry.yarnpkg.com/resolve-url/-/resolve-url-0.2.1.tgz#2c637fe77c893afd2a663fe21aa9080068e2052a" + +resolve@^1.1.4, resolve@^1.1.7, resolve@^1.10.0: + version "1.12.0" + resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.12.0.tgz#3fc644a35c84a48554609ff26ec52b66fa577df6" + dependencies: + path-parse "^1.0.6" + +resolve@^1.12.0, resolve@^1.13.1, resolve@^1.17.0, resolve@^1.3.2, resolve@^1.4.0, resolve@^1.8.1: + version "1.17.0" + resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.17.0.tgz#b25941b54968231cc2d1bb76a79cb7f2c0bf8444" + integrity sha512-ic+7JYiV8Vi2yzQGFWOkiZD5Z9z7O2Zhm9XMaTxdJExKasieFCr+yXZ/WmXsckHiKl12ar0y6XiXDx3m4RHn1w== + dependencies: + path-parse "^1.0.6" + +resolve@^1.18.1: + version "1.19.0" + resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.19.0.tgz#1af5bf630409734a067cae29318aac7fa29a267c" + integrity sha512-rArEXAgsBG4UgRGcynxWIWKFvh/XZCcS8UJdHhwy91zwAvCZIbcs+vAbflgBnNjYMs/i/i+/Ux6IZhML1yPvxg== + dependencies: + is-core-module "^2.1.0" + path-parse "^1.0.6" + +ret@~0.1.10: + version "0.1.15" + resolved "https://registry.yarnpkg.com/ret/-/ret-0.1.15.tgz#b8a4825d5bdb1fc3f6f53c2bc33f81388681c7bc" + +retry@^0.12.0: + version "0.12.0" + resolved "https://registry.yarnpkg.com/retry/-/retry-0.12.0.tgz#1b42a6266a21f07421d1b0b54b7dc167b01c013b" + integrity sha1-G0KmJmoh8HQh0bC1S33BZ7AcATs= + +rgb-regex@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/rgb-regex/-/rgb-regex-1.0.1.tgz#c0e0d6882df0e23be254a475e8edd41915feaeb1" + +rgba-regex@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/rgba-regex/-/rgba-regex-1.0.0.tgz#43374e2e2ca0968b0ef1523460b7d730ff22eeb3" + +rimraf@2, rimraf@^2.5.4, rimraf@^2.6.1, rimraf@^2.6.2, rimraf@^2.6.3: + version "2.7.1" + resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-2.7.1.tgz#35797f13a7fdadc566142c29d4f07ccad483e3ec" + dependencies: + glob "^7.1.3" + +rimraf@^3.0.2: + version "3.0.2" + resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-3.0.2.tgz#f1a5402ba6220ad52cc1282bac1ae3aa49fd061a" + integrity sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA== + dependencies: + glob "^7.1.3" + +ripemd160@^2.0.0, ripemd160@^2.0.1: + version "2.0.2" + resolved "https://registry.yarnpkg.com/ripemd160/-/ripemd160-2.0.2.tgz#a1c1a6f624751577ba5d07914cbc92850585890c" + dependencies: + hash-base "^3.0.0" + inherits "^2.0.1" + +run-queue@^1.0.0, run-queue@^1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/run-queue/-/run-queue-1.0.3.tgz#e848396f057d223f24386924618e25694161ec47" + dependencies: + aproba "^1.1.1" + +safe-buffer@5.1.2, safe-buffer@~5.1.0, safe-buffer@~5.1.1: + version "5.1.2" + resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.1.2.tgz#991ec69d296e0313747d59bdfd2b745c35f8828d" + +safe-buffer@>=5.1.0, safe-buffer@^5.0.1, safe-buffer@^5.1.0, safe-buffer@^5.1.1, safe-buffer@^5.1.2, safe-buffer@~5.2.0: + version "5.2.0" + resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.2.0.tgz#b74daec49b1148f88c64b68d49b1e815c1f2f519" + +safe-regex@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/safe-regex/-/safe-regex-1.1.0.tgz#40a3669f3b077d1e943d44629e157dd48023bf2e" + dependencies: + ret "~0.1.10" + +"safer-buffer@>= 2.1.2 < 3", safer-buffer@^2.0.2, safer-buffer@^2.1.0, safer-buffer@~2.1.0: + version "2.1.2" + resolved "https://registry.yarnpkg.com/safer-buffer/-/safer-buffer-2.1.2.tgz#44fa161b0187b9549dd84bb91802f9bd8385cd6a" + +sass-graph@2.2.5: + version "2.2.5" + resolved "https://registry.yarnpkg.com/sass-graph/-/sass-graph-2.2.5.tgz#a981c87446b8319d96dce0671e487879bd24c2e8" + integrity sha512-VFWDAHOe6mRuT4mZRd4eKE+d8Uedrk6Xnh7Sh9b4NGufQLQjOrvf/MQoOdx+0s92L89FeyUUNfU597j/3uNpag== + dependencies: + glob "^7.0.0" + lodash "^4.0.0" + scss-tokenizer "^0.2.3" + yargs "^13.3.2" + +sass-loader@^8.0.2: + version "8.0.2" + resolved "https://registry.yarnpkg.com/sass-loader/-/sass-loader-8.0.2.tgz#debecd8c3ce243c76454f2e8290482150380090d" + integrity sha512-7o4dbSK8/Ol2KflEmSco4jTjQoV988bM82P9CZdmo9hR3RLnvNc0ufMNdMrB0caq38JQ/FgF4/7RcbcfKzxoFQ== + dependencies: + clone-deep "^4.0.1" + loader-utils "^1.2.3" + neo-async "^2.6.1" + schema-utils "^2.6.1" + semver "^6.3.0" + +sax@^1.2.4, sax@~1.2.4: + version "1.2.4" + resolved "https://registry.yarnpkg.com/sax/-/sax-1.2.4.tgz#2816234e2378bddc4e5354fab5caa895df7100d9" + +scheduler@^0.20.1: + version "0.20.1" + resolved "https://registry.yarnpkg.com/scheduler/-/scheduler-0.20.1.tgz#da0b907e24026b01181ecbc75efdc7f27b5a000c" + integrity sha512-LKTe+2xNJBNxu/QhHvDR14wUXHRQbVY5ZOYpOGWRzhydZUqrLb2JBvLPY7cAqFmqrWuDED0Mjk7013SZiOz6Bw== + dependencies: + loose-envify "^1.1.0" + object-assign "^4.1.1" + +schema-utils@^0.4.0: + version "0.4.7" + resolved "https://registry.yarnpkg.com/schema-utils/-/schema-utils-0.4.7.tgz#ba74f597d2be2ea880131746ee17d0a093c68187" + integrity sha512-v/iwU6wvwGK8HbU9yi3/nhGzP0yGSuhQMzL6ySiec1FSrZZDkhm4noOSWzrNFo/jEc+SJY6jRTwuwbSXJPDUnQ== + dependencies: + ajv "^6.1.0" + ajv-keywords "^3.1.0" + +schema-utils@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/schema-utils/-/schema-utils-1.0.0.tgz#0b79a93204d7b600d4b2850d1f66c2a34951c770" + integrity sha512-i27Mic4KovM/lnGsy8whRCHhc7VicJajAjTrYg11K9zfZXnYIt4k5F+kZkwjnrhKzLic/HLU4j11mjsz2G/75g== + dependencies: + ajv "^6.1.0" + ajv-errors "^1.0.0" + ajv-keywords "^3.1.0" + +schema-utils@^2.6.1, schema-utils@^2.6.5, schema-utils@^2.7.0, schema-utils@^2.7.1: + version "2.7.1" + resolved "https://registry.yarnpkg.com/schema-utils/-/schema-utils-2.7.1.tgz#1ca4f32d1b24c590c203b8e7a50bf0ea4cd394d7" + integrity sha512-SHiNtMOUGWBQJwzISiVYKu82GiV4QYGePp3odlY1tuKO7gPtphAT5R/py0fA6xtbgLL/RvtJZnU9b8s0F1q0Xg== + dependencies: + "@types/json-schema" "^7.0.5" + ajv "^6.12.4" + ajv-keywords "^3.5.2" + +scss-tokenizer@^0.2.3: + version "0.2.3" + resolved "https://registry.yarnpkg.com/scss-tokenizer/-/scss-tokenizer-0.2.3.tgz#8eb06db9a9723333824d3f5530641149847ce5d1" + dependencies: + js-base64 "^2.1.8" + source-map "^0.4.2" + +select-hose@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/select-hose/-/select-hose-2.0.0.tgz#625d8658f865af43ec962bfc376a37359a4994ca" + +selfsigned@^1.10.8: + version "1.10.8" + resolved "https://registry.yarnpkg.com/selfsigned/-/selfsigned-1.10.8.tgz#0d17208b7d12c33f8eac85c41835f27fc3d81a30" + integrity sha512-2P4PtieJeEwVgTU9QEcwIRDQ/mXJLX8/+I3ur+Pg16nS8oNbrGxEso9NyYWy8NAmXiNl4dlAp5MwoNeCWzON4w== + dependencies: + node-forge "^0.10.0" + +"semver@2 || 3 || 4 || 5", semver@^5.3.0, semver@^5.4.1, semver@^5.5.0, semver@^5.5.1, semver@^5.6.0: + version "5.7.1" + resolved "https://registry.yarnpkg.com/semver/-/semver-5.7.1.tgz#a954f931aeba508d307bbf069eff0c01c96116f7" + +semver@7.0.0: + version "7.0.0" + resolved "https://registry.yarnpkg.com/semver/-/semver-7.0.0.tgz#5f3ca35761e47e05b206c6daff2cf814f0316b8e" + integrity sha512-+GB6zVA9LWh6zovYQLALHwv5rb2PHGlJi3lfiqIHxR0uuwCgefcOJc59v9fv1w8GbStwxuuqqAjI9NMAOOgq1A== + +semver@^6.0.0, semver@^6.3.0: + version "6.3.0" + resolved "https://registry.yarnpkg.com/semver/-/semver-6.3.0.tgz#ee0a64c8af5e8ceea67687b133761e1becbd1d3d" + integrity sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw== + +semver@^7.2.1: + version "7.3.2" + resolved "https://registry.yarnpkg.com/semver/-/semver-7.3.2.tgz#604962b052b81ed0786aae84389ffba70ffd3938" + integrity sha512-OrOb32TeeambH6UrhtShmF7CRDqhL6/5XpPNp2DuRH6+9QLw/orhp72j87v8Qa1ScDkvrrBNpZcDejAirJmfXQ== + +semver@^7.3.4: + version "7.3.4" + resolved "https://registry.yarnpkg.com/semver/-/semver-7.3.4.tgz#27aaa7d2e4ca76452f98d3add093a72c943edc97" + integrity sha512-tCfb2WLjqFAtXn4KEdxIhalnRtoKFN7nAwj0B3ZXCbQloV2tq5eDbcTmT68JJD3nRJq24/XgxtQKFIpQdtvmVw== + dependencies: + lru-cache "^6.0.0" + +semver@~5.3.0: + version "5.3.0" + resolved "https://registry.yarnpkg.com/semver/-/semver-5.3.0.tgz#9b2ce5d3de02d17c6012ad326aa6b4d0cf54f94f" + +send@0.17.1: + version "0.17.1" + resolved "https://registry.yarnpkg.com/send/-/send-0.17.1.tgz#c1d8b059f7900f7466dd4938bdc44e11ddb376c8" + dependencies: + debug "2.6.9" + depd "~1.1.2" + destroy "~1.0.4" + encodeurl "~1.0.2" + escape-html "~1.0.3" + etag "~1.8.1" + fresh "0.5.2" + http-errors "~1.7.2" + mime "1.6.0" + ms "2.1.1" + on-finished "~2.3.0" + range-parser "~1.2.1" + statuses "~1.5.0" + +serialize-javascript@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/serialize-javascript/-/serialize-javascript-4.0.0.tgz#b525e1238489a5ecfc42afacc3fe99e666f4b1aa" + integrity sha512-GaNA54380uFefWghODBWEGisLZFj00nS5ACs6yHa9nLqlLpVLO8ChDGeKRjZnV4Nh4n0Qi7nhYZD/9fCPzEqkw== + dependencies: + randombytes "^2.1.0" + +serialize-javascript@^5.0.1: + version "5.0.1" + resolved "https://registry.yarnpkg.com/serialize-javascript/-/serialize-javascript-5.0.1.tgz#7886ec848049a462467a97d3d918ebb2aaf934f4" + integrity sha512-SaaNal9imEO737H2c05Og0/8LUXG7EnsZyMa8MzkmuHoELfT6txuj0cMqRj6zfPKnmQ1yasR4PCJc8x+M4JSPA== + dependencies: + randombytes "^2.1.0" + +serve-index@^1.9.1: + version "1.9.1" + resolved "https://registry.yarnpkg.com/serve-index/-/serve-index-1.9.1.tgz#d3768d69b1e7d82e5ce050fff5b453bea12a9239" + integrity sha1-03aNabHn2C5c4FD/9bRTvqEqkjk= + dependencies: + accepts "~1.3.4" + batch "0.6.1" + debug "2.6.9" + escape-html "~1.0.3" + http-errors "~1.6.2" + mime-types "~2.1.17" + parseurl "~1.3.2" + +serve-static@1.14.1: + version "1.14.1" + resolved "https://registry.yarnpkg.com/serve-static/-/serve-static-1.14.1.tgz#666e636dc4f010f7ef29970a88a674320898b2f9" + dependencies: + encodeurl "~1.0.2" + escape-html "~1.0.3" + parseurl "~1.3.3" + send "0.17.1" + +set-blocking@^2.0.0, set-blocking@~2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/set-blocking/-/set-blocking-2.0.0.tgz#045f9782d011ae9a6803ddd382b24392b3d890f7" + +set-value@^2.0.0, set-value@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/set-value/-/set-value-2.0.1.tgz#a18d40530e6f07de4228c7defe4227af8cad005b" + dependencies: + extend-shallow "^2.0.1" + is-extendable "^0.1.1" + is-plain-object "^2.0.3" + split-string "^3.0.1" + +setimmediate@^1.0.4: + version "1.0.5" + resolved "https://registry.yarnpkg.com/setimmediate/-/setimmediate-1.0.5.tgz#290cbb232e306942d7d7ea9b83732ab7856f8285" + +setprototypeof@1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/setprototypeof/-/setprototypeof-1.1.0.tgz#d0bd85536887b6fe7c0d818cb962d9d91c54e656" + +setprototypeof@1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/setprototypeof/-/setprototypeof-1.1.1.tgz#7e95acb24aa92f5885e0abef5ba131330d4ae683" + +sha.js@^2.4.0, sha.js@^2.4.8, sha.js@~2.4.4: + version "2.4.11" + resolved "https://registry.yarnpkg.com/sha.js/-/sha.js-2.4.11.tgz#37a5cf0b81ecbc6943de109ba2960d1b26584ae7" + dependencies: + inherits "^2.0.1" + safe-buffer "^5.0.1" + +shallow-clone@^3.0.0: + version "3.0.1" + resolved "https://registry.yarnpkg.com/shallow-clone/-/shallow-clone-3.0.1.tgz#8f2981ad92531f55035b01fb230769a40e02efa3" + integrity sha512-/6KqX+GVUdqPuPPd2LxDDxzX6CAbjJehAAOKlNpqqUpAqPM6HeL8f+o3a+JsyGjn2lv0WY8UsTgUJjU9Ok55NA== + dependencies: + kind-of "^6.0.2" + +shallowequal@^0.2.2: + version "0.2.2" + resolved "https://registry.yarnpkg.com/shallowequal/-/shallowequal-0.2.2.tgz#1e32fd5bcab6ad688a4812cb0cc04efc75c7014e" + dependencies: + lodash.keys "^3.1.2" + +shallowequal@^1.0.1, shallowequal@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/shallowequal/-/shallowequal-1.1.0.tgz#188d521de95b9087404fd4dcb68b13df0ae4e7f8" + +shasum@^1.0.0: + version "1.0.2" + resolved "https://registry.yarnpkg.com/shasum/-/shasum-1.0.2.tgz#e7012310d8f417f4deb5712150e5678b87ae565f" + dependencies: + json-stable-stringify "~0.0.0" + sha.js "~2.4.4" + +shebang-command@^1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/shebang-command/-/shebang-command-1.2.0.tgz#44aac65b695b03398968c39f363fee5deafdf1ea" + dependencies: + shebang-regex "^1.0.0" + +shebang-command@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/shebang-command/-/shebang-command-2.0.0.tgz#ccd0af4f8835fbdc265b82461aaf0c36663f34ea" + integrity sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA== + dependencies: + shebang-regex "^3.0.0" + +shebang-regex@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/shebang-regex/-/shebang-regex-1.0.0.tgz#da42f49740c0b42db2ca9728571cb190c98efea3" + +shebang-regex@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/shebang-regex/-/shebang-regex-3.0.0.tgz#ae16f1644d873ecad843b0307b143362d4c42172" + integrity sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A== + +shell-quote@^1.6.1: + version "1.7.1" + resolved "https://registry.yarnpkg.com/shell-quote/-/shell-quote-1.7.1.tgz#3161d969886fb14f9140c65245a5dd19b6f0b06b" + +side-channel@^1.0.2: + version "1.0.3" + resolved "https://registry.yarnpkg.com/side-channel/-/side-channel-1.0.3.tgz#cdc46b057550bbab63706210838df5d4c19519c3" + integrity sha512-A6+ByhlLkksFoUepsGxfj5x1gTSrs+OydsRptUxeNCabQpCFUvcwIczgOigI8vhY/OJCnPnyE9rGiwgvr9cS1g== + dependencies: + es-abstract "^1.18.0-next.0" + object-inspect "^1.8.0" + +signal-exit@^3.0.0: + version "3.0.2" + resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-3.0.2.tgz#b5fdc08f1287ea1178628e415e25132b73646c6d" + +simple-concat@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/simple-concat/-/simple-concat-1.0.0.tgz#7344cbb8b6e26fb27d66b2fc86f9f6d5997521c6" + +simple-swizzle@^0.2.2: + version "0.2.2" + resolved "https://registry.yarnpkg.com/simple-swizzle/-/simple-swizzle-0.2.2.tgz#a4da6b635ffcccca33f70d17cb92592de95e557a" + dependencies: + is-arrayish "^0.3.1" + +slice-ansi@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/slice-ansi/-/slice-ansi-4.0.0.tgz#500e8dd0fd55b05815086255b3195adf2a45fe6b" + integrity sha512-qMCMfhY040cVHT43K9BFygqYbUPFZKHOg7K73mtTWJRb8pyP3fzf4Ixd5SzdEJQ6MRUg/WBnOLxghZtKKurENQ== + dependencies: + ansi-styles "^4.0.0" + astral-regex "^2.0.0" + is-fullwidth-code-point "^3.0.0" + +snapdragon-node@^2.0.1: + version "2.1.1" + resolved "https://registry.yarnpkg.com/snapdragon-node/-/snapdragon-node-2.1.1.tgz#6c175f86ff14bdb0724563e8f3c1b021a286853b" + dependencies: + define-property "^1.0.0" + isobject "^3.0.0" + snapdragon-util "^3.0.1" + +snapdragon-util@^3.0.1: + version "3.0.1" + resolved "https://registry.yarnpkg.com/snapdragon-util/-/snapdragon-util-3.0.1.tgz#f956479486f2acd79700693f6f7b805e45ab56e2" + dependencies: + kind-of "^3.2.0" + +snapdragon@^0.8.1: + version "0.8.2" + resolved "https://registry.yarnpkg.com/snapdragon/-/snapdragon-0.8.2.tgz#64922e7c565b0e14204ba1aa7d6964278d25182d" + dependencies: + base "^0.11.1" + debug "^2.2.0" + define-property "^0.2.5" + extend-shallow "^2.0.1" + map-cache "^0.2.2" + source-map "^0.5.6" + source-map-resolve "^0.5.0" + use "^3.1.0" + +sockjs-client@^1.5.0: + version "1.5.0" + resolved "https://registry.yarnpkg.com/sockjs-client/-/sockjs-client-1.5.0.tgz#2f8ff5d4b659e0d092f7aba0b7c386bd2aa20add" + integrity sha512-8Dt3BDi4FYNrCFGTL/HtwVzkARrENdwOUf1ZoW/9p3M8lZdFT35jVdrHza+qgxuG9H3/shR4cuX/X9umUrjP8Q== + dependencies: + debug "^3.2.6" + eventsource "^1.0.7" + faye-websocket "^0.11.3" + inherits "^2.0.4" + json3 "^3.3.3" + url-parse "^1.4.7" + +sockjs@^0.3.21: + version "0.3.21" + resolved "https://registry.yarnpkg.com/sockjs/-/sockjs-0.3.21.tgz#b34ffb98e796930b60a0cfa11904d6a339a7d417" + integrity sha512-DhbPFGpxjc6Z3I+uX07Id5ZO2XwYsWOrYjaSeieES78cq+JaJvVe5q/m1uvjIQhXinhIeCFRH6JgXe+mvVMyXw== + dependencies: + faye-websocket "^0.11.3" + uuid "^3.4.0" + websocket-driver "^0.7.4" + +sort-keys@^1.0.0: + version "1.1.2" + resolved "https://registry.yarnpkg.com/sort-keys/-/sort-keys-1.1.2.tgz#441b6d4d346798f1b4e49e8920adfba0e543f9ad" + dependencies: + is-plain-obj "^1.0.0" + +source-list-map@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/source-list-map/-/source-list-map-2.0.1.tgz#3993bd873bfc48479cca9ea3a547835c7c154b34" + +source-map-resolve@^0.5.0: + version "0.5.2" + resolved "https://registry.yarnpkg.com/source-map-resolve/-/source-map-resolve-0.5.2.tgz#72e2cc34095543e43b2c62b2c4c10d4a9054f259" + dependencies: + atob "^2.1.1" + decode-uri-component "^0.2.0" + resolve-url "^0.2.1" + source-map-url "^0.4.0" + urix "^0.1.0" + +source-map-support@~0.5.12, source-map-support@~0.5.19: + version "0.5.19" + resolved "https://registry.yarnpkg.com/source-map-support/-/source-map-support-0.5.19.tgz#a98b62f86dcaf4f67399648c085291ab9e8fed61" + integrity sha512-Wonm7zOCIJzBGQdB+thsPar0kYuCIzYvxZwlBa87yi/Mdjv7Tip2cyVbLj5o0cFPN4EVkuTwb3GDDyUx2DGnGw== + dependencies: + buffer-from "^1.0.0" + source-map "^0.6.0" + +source-map-url@^0.4.0: + version "0.4.0" + resolved "https://registry.yarnpkg.com/source-map-url/-/source-map-url-0.4.0.tgz#3e935d7ddd73631b97659956d55128e87b5084a3" + +source-map@0.1.31: + version "0.1.31" + resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.1.31.tgz#9f704d0d69d9e138a81badf6ebb4fde33d151c61" + dependencies: + amdefine ">=0.0.4" + +source-map@^0.4.2: + version "0.4.4" + resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.4.4.tgz#eba4f5da9c0dc999de68032d8b4f76173652036b" + dependencies: + amdefine ">=0.0.4" + +source-map@^0.5.0, source-map@^0.5.3, source-map@^0.5.6, source-map@~0.5.0, source-map@~0.5.3: + version "0.5.7" + resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.5.7.tgz#8a039d2d1021d22d1ea14c80d8ea468ba2ef3fcc" + +source-map@^0.6.0, source-map@^0.6.1, source-map@~0.6.1: + version "0.6.1" + resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.6.1.tgz#74722af32e9614e9c287a8d0bbde48b5e2f1a263" + +source-map@~0.7.2: + version "0.7.3" + resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.7.3.tgz#5302f8169031735226544092e64981f751750383" + integrity sha512-CkCj6giN3S+n9qrYiBTX5gystlENnRW5jZeNLHpe6aue+SrHcG5VYwujhW9s4dY31mEGsxBDrHR6oI69fTXsaQ== + +spdx-correct@^3.0.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/spdx-correct/-/spdx-correct-3.1.0.tgz#fb83e504445268f154b074e218c87c003cd31df4" + dependencies: + spdx-expression-parse "^3.0.0" + spdx-license-ids "^3.0.0" + +spdx-exceptions@^2.1.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/spdx-exceptions/-/spdx-exceptions-2.2.0.tgz#2ea450aee74f2a89bfb94519c07fcd6f41322977" + +spdx-expression-parse@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/spdx-expression-parse/-/spdx-expression-parse-3.0.0.tgz#99e119b7a5da00e05491c9fa338b7904823b41d0" + dependencies: + spdx-exceptions "^2.1.0" + spdx-license-ids "^3.0.0" + +spdx-license-ids@^3.0.0: + version "3.0.5" + resolved "https://registry.yarnpkg.com/spdx-license-ids/-/spdx-license-ids-3.0.5.tgz#3694b5804567a458d3c8045842a6358632f62654" + +spdy-transport@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/spdy-transport/-/spdy-transport-3.0.0.tgz#00d4863a6400ad75df93361a1608605e5dcdcf31" + integrity sha512-hsLVFE5SjA6TCisWeJXFKniGGOpBgMLmerfO2aCyCU5s7nJ/rpAepqmFifv/GCbSbueEeAJJnmSQ2rKC/g8Fcw== + dependencies: + debug "^4.1.0" + detect-node "^2.0.4" + hpack.js "^2.1.6" + obuf "^1.1.2" + readable-stream "^3.0.6" + wbuf "^1.7.3" + +spdy@^4.0.2: + version "4.0.2" + resolved "https://registry.yarnpkg.com/spdy/-/spdy-4.0.2.tgz#b74f466203a3eda452c02492b91fb9e84a27677b" + integrity sha512-r46gZQZQV+Kl9oItvl1JZZqJKGr+oEkB08A6BzkiR7593/7IbtuncXHd2YoYeTsG4157ZssMu9KYvUHLcjcDoA== + dependencies: + debug "^4.1.0" + handle-thing "^2.0.0" + http-deceiver "^1.2.7" + select-hose "^2.0.0" + spdy-transport "^3.0.0" + +split-string@^3.0.1, split-string@^3.0.2: + version "3.1.0" + resolved "https://registry.yarnpkg.com/split-string/-/split-string-3.1.0.tgz#7cb09dda3a86585705c64b39a6466038682e8fe2" + dependencies: + extend-shallow "^3.0.0" + +sprintf-js@~1.0.2: + version "1.0.3" + resolved "https://registry.yarnpkg.com/sprintf-js/-/sprintf-js-1.0.3.tgz#04e6926f662895354f3dd015203633b857297e2c" + +sshpk@^1.7.0: + version "1.16.1" + resolved "https://registry.yarnpkg.com/sshpk/-/sshpk-1.16.1.tgz#fb661c0bef29b39db40769ee39fa70093d6f6877" + dependencies: + asn1 "~0.2.3" + assert-plus "^1.0.0" + bcrypt-pbkdf "^1.0.0" + dashdash "^1.12.0" + ecc-jsbn "~0.1.1" + getpass "^0.1.1" + jsbn "~0.1.0" + safer-buffer "^2.0.2" + tweetnacl "~0.14.0" + +ssri@^6.0.1: + version "6.0.1" + resolved "https://registry.yarnpkg.com/ssri/-/ssri-6.0.1.tgz#2a3c41b28dd45b62b63676ecb74001265ae9edd8" + integrity sha512-3Wge10hNcT1Kur4PDFwEieXSCMCJs/7WvSACcrMYrNp+b8kDL1/0wJch5Ni2WrtwEa2IO8OsVfeKIciKCDx/QA== + dependencies: + figgy-pudding "^3.5.1" + +ssri@^8.0.0: + version "8.0.0" + resolved "https://registry.yarnpkg.com/ssri/-/ssri-8.0.0.tgz#79ca74e21f8ceaeddfcb4b90143c458b8d988808" + integrity sha512-aq/pz989nxVYwn16Tsbj1TqFpD5LLrQxHf5zaHuieFV+R0Bbr4y8qUsOA45hXT/N4/9UNXTarBjnjVmjSOVaAA== + dependencies: + minipass "^3.1.1" + +stable@^0.1.8: + version "0.1.8" + resolved "https://registry.yarnpkg.com/stable/-/stable-0.1.8.tgz#836eb3c8382fe2936feaf544631017ce7d47a3cf" + +static-extend@^0.1.1: + version "0.1.2" + resolved "https://registry.yarnpkg.com/static-extend/-/static-extend-0.1.2.tgz#60809c39cbff55337226fd5e0b520f341f1fb5c6" + dependencies: + define-property "^0.2.5" + object-copy "^0.1.0" + +"statuses@>= 1.4.0 < 2", "statuses@>= 1.5.0 < 2", statuses@~1.5.0: + version "1.5.0" + resolved "https://registry.yarnpkg.com/statuses/-/statuses-1.5.0.tgz#161c7dac177659fd9811f43771fa99381478628c" + +stdout-stream@^1.4.0: + version "1.4.1" + resolved "https://registry.yarnpkg.com/stdout-stream/-/stdout-stream-1.4.1.tgz#5ac174cdd5cd726104aa0c0b2bd83815d8d535de" + dependencies: + readable-stream "^2.0.1" + +stream-browserify@^2.0.0, stream-browserify@^2.0.1: + version "2.0.2" + resolved "https://registry.yarnpkg.com/stream-browserify/-/stream-browserify-2.0.2.tgz#87521d38a44aa7ee91ce1cd2a47df0cb49dd660b" + dependencies: + inherits "~2.0.1" + readable-stream "^2.0.2" + +stream-combiner2@^1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/stream-combiner2/-/stream-combiner2-1.1.1.tgz#fb4d8a1420ea362764e21ad4780397bebcb41cbe" + dependencies: + duplexer2 "~0.1.0" + readable-stream "^2.0.2" + +stream-each@^1.1.0: + version "1.2.3" + resolved "https://registry.yarnpkg.com/stream-each/-/stream-each-1.2.3.tgz#ebe27a0c389b04fbcc233642952e10731afa9bae" + dependencies: + end-of-stream "^1.1.0" + stream-shift "^1.0.0" + +stream-http@^2.7.2: + version "2.8.3" + resolved "https://registry.yarnpkg.com/stream-http/-/stream-http-2.8.3.tgz#b2d242469288a5a27ec4fe8933acf623de6514fc" + dependencies: + builtin-status-codes "^3.0.0" + inherits "^2.0.1" + readable-stream "^2.3.6" + to-arraybuffer "^1.0.0" + xtend "^4.0.0" + +stream-http@^3.0.0: + version "3.1.1" + resolved "https://registry.yarnpkg.com/stream-http/-/stream-http-3.1.1.tgz#0370a8017cf8d050b9a8554afe608f043eaff564" + integrity sha512-S7OqaYu0EkFpgeGFb/NPOoPLxFko7TPqtEeFg5DXPB4v/KETHG0Ln6fRFrNezoelpaDKmycEmmZ81cC9DAwgYg== + dependencies: + builtin-status-codes "^3.0.0" + inherits "^2.0.4" + readable-stream "^3.6.0" + xtend "^4.0.2" + +stream-shift@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/stream-shift/-/stream-shift-1.0.0.tgz#d5c752825e5367e786f78e18e445ea223a155952" + +stream-splicer@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/stream-splicer/-/stream-splicer-2.0.1.tgz#0b13b7ee2b5ac7e0609a7463d83899589a363fcd" + dependencies: + inherits "^2.0.1" + readable-stream "^2.0.2" + +strict-uri-encode@^1.0.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/strict-uri-encode/-/strict-uri-encode-1.1.0.tgz#279b225df1d582b1f54e65addd4352e18faa0713" + +string-width@^1.0.1: + version "1.0.2" + resolved "https://registry.yarnpkg.com/string-width/-/string-width-1.0.2.tgz#118bdf5b8cdc51a2a7e70d211e07e2b0b9b107d3" + dependencies: + code-point-at "^1.0.0" + is-fullwidth-code-point "^1.0.0" + strip-ansi "^3.0.0" + +"string-width@^1.0.2 || 2": + version "2.1.1" + resolved "https://registry.yarnpkg.com/string-width/-/string-width-2.1.1.tgz#ab93f27a8dc13d28cac815c462143a6d9012ae9e" + dependencies: + is-fullwidth-code-point "^2.0.0" + strip-ansi "^4.0.0" + +string-width@^3.0.0, string-width@^3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/string-width/-/string-width-3.1.0.tgz#22767be21b62af1081574306f69ac51b62203961" + integrity sha512-vafcv6KjVZKSgz06oM/H6GDBrAtz8vdhQakGjFIvNrHA6y3HCF1CInLy+QLq8dTJPQ1b+KDUqDFctkdRW44e1w== + dependencies: + emoji-regex "^7.0.1" + is-fullwidth-code-point "^2.0.0" + strip-ansi "^5.1.0" + +string-width@^4.2.0: + version "4.2.0" + resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.0.tgz#952182c46cc7b2c313d1596e623992bd163b72b5" + integrity sha512-zUz5JD+tgqtuDjMhwIg5uFVV3dtqZ9yQJlZVfq4I01/K5Paj5UHj7VyrQOJvzawSVlKpObApbfD0Ed6yJc+1eg== + dependencies: + emoji-regex "^8.0.0" + is-fullwidth-code-point "^3.0.0" + strip-ansi "^6.0.0" + +string.prototype.matchall@^4.0.2: + version "4.0.2" + resolved "https://registry.yarnpkg.com/string.prototype.matchall/-/string.prototype.matchall-4.0.2.tgz#48bb510326fb9fdeb6a33ceaa81a6ea04ef7648e" + integrity sha512-N/jp6O5fMf9os0JU3E72Qhf590RSRZU/ungsL/qJUYVTNv7hTG0P/dbPjxINVN9jpscu3nzYwKESU3P3RY5tOg== + dependencies: + define-properties "^1.1.3" + es-abstract "^1.17.0" + has-symbols "^1.0.1" + internal-slot "^1.0.2" + regexp.prototype.flags "^1.3.0" + side-channel "^1.0.2" + +string.prototype.trimend@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/string.prototype.trimend/-/string.prototype.trimend-1.0.1.tgz#85812a6b847ac002270f5808146064c995fb6913" + integrity sha512-LRPxFUaTtpqYsTeNKaFOw3R4bxIzWOnbQ837QfBylo8jIxtcbK/A/sMV7Q+OAV/vWo+7s25pOE10KYSjaSO06g== + dependencies: + define-properties "^1.1.3" + es-abstract "^1.17.5" + +string.prototype.trimstart@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/string.prototype.trimstart/-/string.prototype.trimstart-1.0.1.tgz#14af6d9f34b053f7cfc89b72f8f2ee14b9039a54" + integrity sha512-XxZn+QpvrBI1FOcg6dIpxUPgWCPuNXvMD72aaRaUQv1eD4e/Qy8i/hFTe0BUmD60p/QA6bh1avmuPTfNjqVWRw== + dependencies: + define-properties "^1.1.3" + es-abstract "^1.17.5" + +string_decoder@^1.0.0, string_decoder@^1.1.1: + version "1.3.0" + resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-1.3.0.tgz#42f114594a46cf1a8e30b0a84f56c78c3edac21e" + dependencies: + safe-buffer "~5.2.0" + +string_decoder@~1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-1.1.1.tgz#9cf1611ba62685d7030ae9e4ba34149c3af03fc8" + dependencies: + safe-buffer "~5.1.0" + +strip-ansi@^3.0.0, strip-ansi@^3.0.1: + version "3.0.1" + resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-3.0.1.tgz#6a385fb8853d952d5ff05d0e8aaf94278dc63dcf" + dependencies: + ansi-regex "^2.0.0" + +strip-ansi@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-4.0.0.tgz#a8479022eb1ac368a871389b635262c505ee368f" + dependencies: + ansi-regex "^3.0.0" + +strip-ansi@^5.0.0, strip-ansi@^5.1.0, strip-ansi@^5.2.0: + version "5.2.0" + resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-5.2.0.tgz#8c9a536feb6afc962bdfa5b104a5091c1ad9c0ae" + integrity sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA== + dependencies: + ansi-regex "^4.1.0" + +strip-ansi@^6.0.0: + version "6.0.0" + resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.0.tgz#0b1571dd7669ccd4f3e06e14ef1eed26225ae532" + integrity sha512-AuvKTrTfQNYNIctbR1K/YGTR1756GycPsg7b9bdV9Duqur4gv6aKqHXah67Z8ImS7WEz5QVcOtlfW2rZEugt6w== + dependencies: + ansi-regex "^5.0.0" + +strip-bom@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/strip-bom/-/strip-bom-2.0.0.tgz#6219a85616520491f35788bdbf1447a99c7e6b0e" + dependencies: + is-utf8 "^0.2.0" + +strip-bom@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/strip-bom/-/strip-bom-3.0.0.tgz#2334c18e9c759f7bdd56fdef7e9ae3d588e68ed3" + +strip-eof@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/strip-eof/-/strip-eof-1.0.0.tgz#bb43ff5598a6eb05d89b59fcd129c983313606bf" + +strip-indent@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/strip-indent/-/strip-indent-1.0.1.tgz#0c7962a6adefa7bbd4ac366460a638552ae1a0a2" + dependencies: + get-stdin "^4.0.1" + +strip-json-comments@^3.1.0, strip-json-comments@^3.1.1: + version "3.1.1" + resolved "https://registry.yarnpkg.com/strip-json-comments/-/strip-json-comments-3.1.1.tgz#31f1281b3832630434831c310c01cccda8cbe006" + integrity sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig== + +strip-json-comments@~2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/strip-json-comments/-/strip-json-comments-2.0.1.tgz#3c531942e908c2697c0ec344858c286c7ca0a60a" + +strip-outer@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/strip-outer/-/strip-outer-1.0.1.tgz#b2fd2abf6604b9d1e6013057195df836b8a9d631" + dependencies: + escape-string-regexp "^1.0.2" + +strip-url-auth@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/strip-url-auth/-/strip-url-auth-1.0.1.tgz#22b0fa3a41385b33be3f331551bbb837fa0cd7ae" + +style-loader@^1.2.1: + version "1.3.0" + resolved "https://registry.yarnpkg.com/style-loader/-/style-loader-1.3.0.tgz#828b4a3b3b7e7aa5847ce7bae9e874512114249e" + integrity sha512-V7TCORko8rs9rIqkSrlMfkqA63DfoGBBJmK1kKGCcSi+BWb4cqz0SRsnp4l6rU5iwOEd0/2ePv68SV22VXon4Q== + dependencies: + loader-utils "^2.0.0" + schema-utils "^2.7.0" + +stylehacks@^4.0.0: + version "4.0.3" + resolved "https://registry.yarnpkg.com/stylehacks/-/stylehacks-4.0.3.tgz#6718fcaf4d1e07d8a1318690881e8d96726a71d5" + dependencies: + browserslist "^4.0.0" + postcss "^7.0.0" + postcss-selector-parser "^3.0.0" + +subarg@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/subarg/-/subarg-1.0.0.tgz#f62cf17581e996b48fc965699f54c06ae268b8d2" + dependencies: + minimist "^1.1.0" + +supports-color@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-2.0.0.tgz#535d045ce6b6363fa40117084629995e9df324c7" + +supports-color@^5.3.0: + version "5.5.0" + resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-5.5.0.tgz#e2e69a44ac8772f78a1ec0b35b689df6530efc8f" + dependencies: + has-flag "^3.0.0" + +supports-color@^6.1.0: + version "6.1.0" + resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-6.1.0.tgz#0764abc69c63d5ac842dd4867e8d025e880df8f3" + dependencies: + has-flag "^3.0.0" + +supports-color@^7.0.0, supports-color@^7.1.0: + version "7.2.0" + resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-7.2.0.tgz#1b7dcdcb32b8138801b3e478ba6a51caa89648da" + integrity sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw== + dependencies: + has-flag "^4.0.0" + +svgo@^1.0.0: + version "1.3.0" + resolved "https://registry.yarnpkg.com/svgo/-/svgo-1.3.0.tgz#bae51ba95ded9a33a36b7c46ce9c359ae9154313" + dependencies: + chalk "^2.4.1" + coa "^2.0.2" + css-select "^2.0.0" + css-select-base-adapter "^0.1.1" + css-tree "1.0.0-alpha.33" + csso "^3.5.1" + js-yaml "^3.13.1" + mkdirp "~0.5.1" + object.values "^1.1.0" + sax "~1.2.4" + stable "^0.1.8" + unquote "~1.1.1" + util.promisify "~1.0.0" + +symbol-observable@^1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/symbol-observable/-/symbol-observable-1.2.0.tgz#c22688aed4eab3cdc2dfeacbb561660560a00804" + +syntax-error@^1.1.1: + version "1.4.0" + resolved "https://registry.yarnpkg.com/syntax-error/-/syntax-error-1.4.0.tgz#2d9d4ff5c064acb711594a3e3b95054ad51d907c" + dependencies: + acorn-node "^1.2.0" + +table@^6.0.4: + version "6.0.7" + resolved "https://registry.yarnpkg.com/table/-/table-6.0.7.tgz#e45897ffbcc1bcf9e8a87bf420f2c9e5a7a52a34" + integrity sha512-rxZevLGTUzWna/qBLObOe16kB2RTnnbhciwgPbMMlazz1yZGVEgnZK762xyVdVznhqxrfCeBMmMkgOOaPwjH7g== + dependencies: + ajv "^7.0.2" + lodash "^4.17.20" + slice-ansi "^4.0.0" + string-width "^4.2.0" + +tapable@^1.0.0, tapable@^1.1.3: + version "1.1.3" + resolved "https://registry.yarnpkg.com/tapable/-/tapable-1.1.3.tgz#a1fccc06b58db61fd7a45da2da44f5f3a3e67ba2" + integrity sha512-4WK/bYZmj8xLr+HUCODHGF1ZFzsYffasLUgEiMBY4fgtltdO6B4WJtlSbPaDTLpYTcGVwM2qLnFTICEcNxs3kA== + +tar@^2.0.0: + version "2.2.2" + resolved "https://registry.yarnpkg.com/tar/-/tar-2.2.2.tgz#0ca8848562c7299b8b446ff6a4d60cdbb23edc40" + dependencies: + block-stream "*" + fstream "^1.0.12" + inherits "2" + +tar@^4: + version "4.4.10" + resolved "https://registry.yarnpkg.com/tar/-/tar-4.4.10.tgz#946b2810b9a5e0b26140cf78bea6b0b0d689eba1" + dependencies: + chownr "^1.1.1" + fs-minipass "^1.2.5" + minipass "^2.3.5" + minizlib "^1.2.1" + mkdirp "^0.5.0" + safe-buffer "^5.1.2" + yallist "^3.0.3" + +tar@^6.0.2: + version "6.0.5" + resolved "https://registry.yarnpkg.com/tar/-/tar-6.0.5.tgz#bde815086e10b39f1dcd298e89d596e1535e200f" + integrity sha512-0b4HOimQHj9nXNEAA7zWwMM91Zhhba3pspja6sQbgTpynOJf+bkjBnfybNYzbpLbnwXnbyB4LOREvlyXLkCHSg== + dependencies: + chownr "^2.0.0" + fs-minipass "^2.0.0" + minipass "^3.0.0" + minizlib "^2.1.1" + mkdirp "^1.0.3" + yallist "^4.0.0" + +terser-webpack-plugin@^1.4.3: + version "1.4.5" + resolved "https://registry.yarnpkg.com/terser-webpack-plugin/-/terser-webpack-plugin-1.4.5.tgz#a217aefaea330e734ffacb6120ec1fa312d6040b" + integrity sha512-04Rfe496lN8EYruwi6oPQkG0vo8C+HT49X687FZnpPF0qMAIHONI6HEXYPKDOE8e5HjXTyKfqRd/agHtH0kOtw== + dependencies: + cacache "^12.0.2" + find-cache-dir "^2.1.0" + is-wsl "^1.1.0" + schema-utils "^1.0.0" + serialize-javascript "^4.0.0" + source-map "^0.6.1" + terser "^4.1.2" + webpack-sources "^1.4.0" + worker-farm "^1.7.0" + +terser-webpack-plugin@^4.0.0: + version "4.2.2" + resolved "https://registry.yarnpkg.com/terser-webpack-plugin/-/terser-webpack-plugin-4.2.2.tgz#d86200c700053bba637913fe4310ba1bdeb5568e" + integrity sha512-3qAQpykRTD5DReLu5/cwpsg7EZFzP3Q0Hp2XUWJUw2mpq2jfgOKTZr8IZKKnNieRVVo1UauROTdhbQJZveGKtQ== + dependencies: + cacache "^15.0.5" + find-cache-dir "^3.3.1" + jest-worker "^26.3.0" + p-limit "^3.0.2" + schema-utils "^2.7.1" + serialize-javascript "^5.0.1" + source-map "^0.6.1" + terser "^5.3.2" + webpack-sources "^1.4.3" + +terser@^4.1.2: + version "4.8.0" + resolved "https://registry.yarnpkg.com/terser/-/terser-4.8.0.tgz#63056343d7c70bb29f3af665865a46fe03a0df17" + integrity sha512-EAPipTNeWsb/3wLPeup1tVPaXfIaU68xMnVdPafIL1TV05OhASArYyIfFvnvJCNrR2NIOvDVNNTFRa+Re2MWyw== + dependencies: + commander "^2.20.0" + source-map "~0.6.1" + source-map-support "~0.5.12" + +terser@^5.3.2: + version "5.3.4" + resolved "https://registry.yarnpkg.com/terser/-/terser-5.3.4.tgz#e510e05f86e0bd87f01835c3238839193f77a60c" + integrity sha512-dxuB8KQo8Gt6OVOeLg/rxfcxdNZI/V1G6ze1czFUzPeCFWZRtvZMgSzlZZ5OYBZ4HoG607F6pFPNLekJyV+yVw== + dependencies: + commander "^2.20.0" + source-map "~0.7.2" + source-map-support "~0.5.19" + +text-table@^0.2.0: + version "0.2.0" + resolved "https://registry.yarnpkg.com/text-table/-/text-table-0.2.0.tgz#7f5ee823ae805207c00af2df4a84ec3fcfa570b4" + integrity sha1-f17oI66AUgfACvLfSoTsP8+lcLQ= + +throttle-debounce@^2.1.0: + version "2.3.0" + resolved "https://registry.yarnpkg.com/throttle-debounce/-/throttle-debounce-2.3.0.tgz#fd31865e66502071e411817e241465b3e9c372e2" + integrity sha512-H7oLPV0P7+jgvrk+6mwwwBDmxTaxnu9HMXmloNLXwnNO0ZxZ31Orah2n8lU1eMPvsaowP2CX+USCgyovXfdOFQ== + +through2@^2.0.0: + version "2.0.5" + resolved "https://registry.yarnpkg.com/through2/-/through2-2.0.5.tgz#01c1e39eb31d07cb7d03a96a70823260b23132cd" + dependencies: + readable-stream "~2.3.6" + xtend "~4.0.1" + +"through@>=2.2.7 <3", through@~2.3.4: + version "2.3.8" + resolved "https://registry.yarnpkg.com/through/-/through-2.3.8.tgz#0dd4c9ffaabc357960b1b724115d7e0e86a2e1f5" + +thunky@^1.0.2: + version "1.0.3" + resolved "https://registry.yarnpkg.com/thunky/-/thunky-1.0.3.tgz#f5df732453407b09191dae73e2a8cc73f381a826" + +timers-browserify@^1.0.1: + version "1.4.2" + resolved "https://registry.yarnpkg.com/timers-browserify/-/timers-browserify-1.4.2.tgz#c9c58b575be8407375cb5e2462dacee74359f41d" + dependencies: + process "~0.11.0" + +timers-browserify@^2.0.4: + version "2.0.11" + resolved "https://registry.yarnpkg.com/timers-browserify/-/timers-browserify-2.0.11.tgz#800b1f3eee272e5bc53ee465a04d0e804c31211f" + dependencies: + setimmediate "^1.0.4" + +timsort@^0.3.0: + version "0.3.0" + resolved "https://registry.yarnpkg.com/timsort/-/timsort-0.3.0.tgz#405411a8e7e6339fe64db9a234de11dc31e02bd4" + +tiny-invariant@^1.0.6: + version "1.0.6" + resolved "https://registry.yarnpkg.com/tiny-invariant/-/tiny-invariant-1.0.6.tgz#b3f9b38835e36a41c843a3b0907a5a7b3755de73" + +to-arraybuffer@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/to-arraybuffer/-/to-arraybuffer-1.0.1.tgz#7d229b1fcc637e466ca081180836a7aabff83f43" + +to-fast-properties@^1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/to-fast-properties/-/to-fast-properties-1.0.3.tgz#b83571fa4d8c25b82e231b06e3a3055de4ca1a47" + +to-fast-properties@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/to-fast-properties/-/to-fast-properties-2.0.0.tgz#dc5e698cbd079265bc73e0377681a4e4e83f616e" + integrity sha1-3F5pjL0HkmW8c+A3doGk5Og/YW4= + +to-object-path@^0.3.0: + version "0.3.0" + resolved "https://registry.yarnpkg.com/to-object-path/-/to-object-path-0.3.0.tgz#297588b7b0e7e0ac08e04e672f85c1f4999e17af" + dependencies: + kind-of "^3.0.2" + +to-regex-range@^2.1.0: + version "2.1.1" + resolved "https://registry.yarnpkg.com/to-regex-range/-/to-regex-range-2.1.1.tgz#7c80c17b9dfebe599e27367e0d4dd5590141db38" + dependencies: + is-number "^3.0.0" + repeat-string "^1.6.1" + +to-regex-range@^5.0.1: + version "5.0.1" + resolved "https://registry.yarnpkg.com/to-regex-range/-/to-regex-range-5.0.1.tgz#1648c44aae7c8d988a326018ed72f5b4dd0392e4" + integrity sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ== + dependencies: + is-number "^7.0.0" + +to-regex@^3.0.1, to-regex@^3.0.2: + version "3.0.2" + resolved "https://registry.yarnpkg.com/to-regex/-/to-regex-3.0.2.tgz#13cfdd9b336552f30b51f33a8ae1b42a7a7599ce" + dependencies: + define-property "^2.0.2" + extend-shallow "^3.0.2" + regex-not "^1.0.2" + safe-regex "^1.1.0" + +toggle-selection@^1.0.6: + version "1.0.6" + resolved "https://registry.yarnpkg.com/toggle-selection/-/toggle-selection-1.0.6.tgz#6e45b1263f2017fa0acc7d89d78b15b8bf77da32" + +toidentifier@1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/toidentifier/-/toidentifier-1.0.0.tgz#7e1be3470f1e77948bc43d94a3c8f4d7752ba553" + +tough-cookie@~2.4.3: + version "2.4.3" + resolved "https://registry.yarnpkg.com/tough-cookie/-/tough-cookie-2.4.3.tgz#53f36da3f47783b0925afa06ff9f3b165280f781" + dependencies: + psl "^1.1.24" + punycode "^1.4.1" + +trim-newlines@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/trim-newlines/-/trim-newlines-1.0.0.tgz#5887966bb582a4503a41eb524f7d35011815a613" + +trim-repeated@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/trim-repeated/-/trim-repeated-1.0.0.tgz#e3646a2ea4e891312bf7eace6cfb05380bc01c21" + dependencies: + escape-string-regexp "^1.0.2" + +"true-case-path@^1.0.2": + version "1.0.3" + resolved "https://registry.yarnpkg.com/true-case-path/-/true-case-path-1.0.3.tgz#f813b5a8c86b40da59606722b144e3225799f47d" + dependencies: + glob "^7.1.2" + +ts-essentials@^2.0.3: + version "2.0.12" + resolved "https://registry.yarnpkg.com/ts-essentials/-/ts-essentials-2.0.12.tgz#c9303f3d74f75fa7528c3d49b80e089ab09d8745" + integrity sha512-3IVX4nI6B5cc31/GFFE+i8ey/N2eA0CZDbo6n0yrz0zDX8ZJ8djmU1p+XRz7G3is0F3bB3pu2pAroFdAWQKU3w== + +ts-loader@^8.0.14: + version "8.0.14" + resolved "https://registry.yarnpkg.com/ts-loader/-/ts-loader-8.0.14.tgz#e46ac1f8dcb88808d0b1335d2eae65b74bd78fe8" + integrity sha512-Jt/hHlUnApOZjnSjTmZ+AbD5BGlQFx3f1D0nYuNKwz0JJnuDGHJas6az+FlWKwwRTu+26GXpv249A8UAnYUpqA== + dependencies: + chalk "^4.1.0" + enhanced-resolve "^4.0.0" + loader-utils "^2.0.0" + micromatch "^4.0.0" + semver "^7.3.4" + +ts-pnp@^1.1.6: + version "1.2.0" + resolved "https://registry.yarnpkg.com/ts-pnp/-/ts-pnp-1.2.0.tgz#a500ad084b0798f1c3071af391e65912c86bca92" + integrity sha512-csd+vJOb/gkzvcCHgTGSChYpy5f1/XKNsmvBGO4JXS+z1v2HobugDz4s1IeFXM3wZB44uczs+eazB5Q/ccdhQw== + +tsconfig-paths@^3.9.0: + version "3.9.0" + resolved "https://registry.yarnpkg.com/tsconfig-paths/-/tsconfig-paths-3.9.0.tgz#098547a6c4448807e8fcb8eae081064ee9a3c90b" + integrity sha512-dRcuzokWhajtZWkQsDVKbWyY+jgcLC5sqJhg2PSgf4ZkH2aHPvaOY8YWGhmjb68b5qqTfasSsDO9k7RUiEmZAw== + dependencies: + "@types/json5" "^0.0.29" + json5 "^1.0.1" + minimist "^1.2.0" + strip-bom "^3.0.0" + +tslib@^1.9.0: + version "1.14.0" + resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.14.0.tgz#d624983f3e2c5e0b55307c3dd6c86acd737622c6" + integrity sha512-+Zw5lu0D9tvBMjGP8LpvMb0u2WW2QV3y+D8mO6J+cNzCYIN4sVy43Bf9vl92nqFahutN0I8zHa7cc4vihIshnw== + +tty-browserify@0.0.0: + version "0.0.0" + resolved "https://registry.yarnpkg.com/tty-browserify/-/tty-browserify-0.0.0.tgz#a157ba402da24e9bf957f9aa69d524eed42901a6" + +tty-browserify@0.0.1: + version "0.0.1" + resolved "https://registry.yarnpkg.com/tty-browserify/-/tty-browserify-0.0.1.tgz#3f05251ee17904dfd0677546670db9651682b811" + integrity sha512-C3TaO7K81YvjCgQH9Q1S3R3P3BtN3RIM8n+OvX4il1K1zgE8ZhI0op7kClgkxtutIE8hQrcrHBXvIheqKUUCxw== + +tunnel-agent@^0.6.0: + version "0.6.0" + resolved "https://registry.yarnpkg.com/tunnel-agent/-/tunnel-agent-0.6.0.tgz#27a5dea06b36b04a0a9966774b290868f0fc40fd" + dependencies: + safe-buffer "^5.0.1" + +tweetnacl@^0.14.3, tweetnacl@~0.14.0: + version "0.14.5" + resolved "https://registry.yarnpkg.com/tweetnacl/-/tweetnacl-0.14.5.tgz#5ae68177f192d4456269d108afa93ff8743f4f64" + +type-check@^0.4.0, type-check@~0.4.0: + version "0.4.0" + resolved "https://registry.yarnpkg.com/type-check/-/type-check-0.4.0.tgz#07b8203bfa7056c0657050e3ccd2c37730bab8f1" + integrity sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew== + dependencies: + prelude-ls "^1.2.1" + +type-fest@^0.8.1: + version "0.8.1" + resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.8.1.tgz#09e249ebde851d3b1e48d27c105444667f17b83d" + integrity sha512-4dbzIzqvjtgiM5rw1k5rEHtBANKmdudhGyBEajN01fEyhaAIhsoKNy6y7+IN93IfpFtwY9iqi7kD+xwKhQsNJA== + +type-is@~1.6.17, type-is@~1.6.18: + version "1.6.18" + resolved "https://registry.yarnpkg.com/type-is/-/type-is-1.6.18.tgz#4e552cd05df09467dcbc4ef739de89f2cf37c131" + dependencies: + media-typer "0.3.0" + mime-types "~2.1.24" + +typed-styles@^0.0.7: + version "0.0.7" + resolved "https://registry.yarnpkg.com/typed-styles/-/typed-styles-0.0.7.tgz#93392a008794c4595119ff62dde6809dbc40a3d9" + +typedarray@^0.0.6: + version "0.0.6" + resolved "https://registry.yarnpkg.com/typedarray/-/typedarray-0.0.6.tgz#867ac74e3864187b1d3d47d996a78ec5c8830777" + +typescript@^4.1.3: + version "4.1.3" + resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.1.3.tgz#519d582bd94cba0cf8934c7d8e8467e473f53bb7" + integrity sha512-B3ZIOf1IKeH2ixgHhj6la6xdwR9QrLC5d1VKeCSY4tvkqhF2eqd9O7txNlS0PO3GrBAFIdr3L1ndNwteUbZLYg== + +umd@^3.0.0: + version "3.0.3" + resolved "https://registry.yarnpkg.com/umd/-/umd-3.0.3.tgz#aa9fe653c42b9097678489c01000acb69f0b26cf" + +undeclared-identifiers@^1.1.2: + version "1.1.3" + resolved "https://registry.yarnpkg.com/undeclared-identifiers/-/undeclared-identifiers-1.1.3.tgz#9254c1d37bdac0ac2b52de4b6722792d2a91e30f" + dependencies: + acorn-node "^1.3.0" + dash-ast "^1.0.0" + get-assigned-identifiers "^1.2.0" + simple-concat "^1.0.0" + xtend "^4.0.1" + +unfetch@^4.2.0: + version "4.2.0" + resolved "https://registry.yarnpkg.com/unfetch/-/unfetch-4.2.0.tgz#7e21b0ef7d363d8d9af0fb929a5555f6ef97a3be" + integrity sha512-F9p7yYCn6cIW9El1zi0HI6vqpeIvBsr3dSuRO6Xuppb1u5rXpCPmMvLSyECLhybr9isec8Ohl0hPekMVrEinDA== + +unicode-canonical-property-names-ecmascript@^1.0.4: + version "1.0.4" + resolved "https://registry.yarnpkg.com/unicode-canonical-property-names-ecmascript/-/unicode-canonical-property-names-ecmascript-1.0.4.tgz#2619800c4c825800efdd8343af7dd9933cbe2818" + integrity sha512-jDrNnXWHd4oHiTZnx/ZG7gtUTVp+gCcTTKr8L0HjlwphROEW3+Him+IpvC+xcJEFegapiMZyZe02CyuOnRmbnQ== + +unicode-match-property-ecmascript@^1.0.4: + version "1.0.4" + resolved "https://registry.yarnpkg.com/unicode-match-property-ecmascript/-/unicode-match-property-ecmascript-1.0.4.tgz#8ed2a32569961bce9227d09cd3ffbb8fed5f020c" + integrity sha512-L4Qoh15vTfntsn4P1zqnHulG0LdXgjSO035fEpdtp6YxXhMT51Q6vgM5lYdG/5X3MjS+k/Y9Xw4SFCY9IkR0rg== + dependencies: + unicode-canonical-property-names-ecmascript "^1.0.4" + unicode-property-aliases-ecmascript "^1.0.4" + +unicode-match-property-value-ecmascript@^1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/unicode-match-property-value-ecmascript/-/unicode-match-property-value-ecmascript-1.2.0.tgz#0d91f600eeeb3096aa962b1d6fc88876e64ea531" + integrity sha512-wjuQHGQVofmSJv1uVISKLE5zO2rNGzM/KCYZch/QQvez7C1hUhBIuZ701fYXExuufJFMPhv2SyL8CyoIfMLbIQ== + +unicode-property-aliases-ecmascript@^1.0.4: + version "1.1.0" + resolved "https://registry.yarnpkg.com/unicode-property-aliases-ecmascript/-/unicode-property-aliases-ecmascript-1.1.0.tgz#dd57a99f6207bedff4628abefb94c50db941c8f4" + integrity sha512-PqSoPh/pWetQ2phoj5RLiaqIk4kCNwoV3CI+LfGmWLKI3rE3kl1h59XpX2BjgDrmbxD9ARtQobPGU1SguCYuQg== + +union-value@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/union-value/-/union-value-1.0.1.tgz#0b6fe7b835aecda61c6ea4d4f02c14221e109847" + dependencies: + arr-union "^3.1.0" + get-value "^2.0.6" + is-extendable "^0.1.1" + set-value "^2.0.1" + +uniq@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/uniq/-/uniq-1.0.1.tgz#b31c5ae8254844a3a8281541ce2b04b865a734ff" + +uniqs@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/uniqs/-/uniqs-2.0.0.tgz#ffede4b36b25290696e6e165d4a59edb998e6b02" + +unique-filename@^1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/unique-filename/-/unique-filename-1.1.1.tgz#1d69769369ada0583103a1e6ae87681b56573230" + integrity sha512-Vmp0jIp2ln35UTXuryvjzkjGdRyf9b2lTXuSYUiPmzRcl3FDtYqAwOnTJkAngD9SWhnoJzDbTKwaOrZ+STtxNQ== + dependencies: + unique-slug "^2.0.0" + +unique-slug@^2.0.0: + version "2.0.2" + resolved "https://registry.yarnpkg.com/unique-slug/-/unique-slug-2.0.2.tgz#baabce91083fc64e945b0f3ad613e264f7cd4e6c" + dependencies: + imurmurhash "^0.1.4" + +universalify@^0.1.0: + version "0.1.2" + resolved "https://registry.yarnpkg.com/universalify/-/universalify-0.1.2.tgz#b646f69be3942dabcecc9d6639c80dc105efaa66" + +unpipe@1.0.0, unpipe@~1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/unpipe/-/unpipe-1.0.0.tgz#b2bf4ee8514aae6165b4817829d21b2ef49904ec" + +unquote@~1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/unquote/-/unquote-1.1.1.tgz#8fded7324ec6e88a0ff8b905e7c098cdc086d544" + +unset-value@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/unset-value/-/unset-value-1.0.0.tgz#8376873f7d2335179ffb1e6fc3a8ed0dfc8ab559" + dependencies: + has-value "^0.3.1" + isobject "^3.0.0" + +upath@^1.1.1: + version "1.1.2" + resolved "https://registry.yarnpkg.com/upath/-/upath-1.1.2.tgz#3db658600edaeeccbe6db5e684d67ee8c2acd068" + +uri-js@^4.2.2: + version "4.2.2" + resolved "https://registry.yarnpkg.com/uri-js/-/uri-js-4.2.2.tgz#94c540e1ff772956e2299507c010aea6c8838eb0" + dependencies: + punycode "^2.1.0" + +urix@^0.1.0: + version "0.1.0" + resolved "https://registry.yarnpkg.com/urix/-/urix-0.1.0.tgz#da937f7a62e21fec1fd18d49b35c2935067a6c72" + +url-parse@^1.4.3, url-parse@^1.4.7: + version "1.4.7" + resolved "https://registry.yarnpkg.com/url-parse/-/url-parse-1.4.7.tgz#a8a83535e8c00a316e403a5db4ac1b9b853ae278" + dependencies: + querystringify "^2.1.1" + requires-port "^1.0.0" + +url@^0.11.0, url@~0.11.0: + version "0.11.0" + resolved "https://registry.yarnpkg.com/url/-/url-0.11.0.tgz#3838e97cfc60521eb73c525a8e55bfdd9e2e28f1" + dependencies: + punycode "1.3.2" + querystring "0.2.0" + +use-composed-ref@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/use-composed-ref/-/use-composed-ref-1.0.0.tgz#bb13e8f4a0b873632cde4940abeb88b92d03023a" + integrity sha512-RVqY3NFNjZa0xrmK3bIMWNmQ01QjKPDc7DeWR3xa/N8aliVppuutOE5bZzPkQfvL+5NRWMMp0DJ99Trd974FIw== + dependencies: + ts-essentials "^2.0.3" + +use-isomorphic-layout-effect@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/use-isomorphic-layout-effect/-/use-isomorphic-layout-effect-1.0.0.tgz#f56b4ed633e1c21cd9fc76fe249002a1c28989fb" + integrity sha512-JMwJ7Vd86NwAt1jH7q+OIozZSIxA4ND0fx6AsOe2q1H8ooBUp5aN6DvVCqZiIaYU6JaMRJGyR0FO7EBCIsb/Rg== + +use-latest@^1.0.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/use-latest/-/use-latest-1.1.0.tgz#7bf9684555869c3f5f37e10d0884c8accf4d3aa6" + integrity sha512-gF04d0ZMV3AMB8Q7HtfkAWe+oq1tFXP6dZKwBHQF5nVXtGsh2oAYeeqma5ZzxtlpOcW8Ro/tLcfmEodjDeqtuw== + dependencies: + use-isomorphic-layout-effect "^1.0.0" + +use-memo-one@^1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/use-memo-one/-/use-memo-one-1.1.1.tgz#39e6f08fe27e422a7d7b234b5f9056af313bd22c" + integrity sha512-oFfsyun+bP7RX8X2AskHNTxu+R3QdE/RC5IefMbqptmACAA/gfol1KDD5KRzPsGMa62sWxGZw+Ui43u6x4ddoQ== + +use@^3.1.0: + version "3.1.1" + resolved "https://registry.yarnpkg.com/use/-/use-3.1.1.tgz#d50c8cac79a19fbc20f2911f56eb973f4e10070f" + +util-deprecate@^1.0.1, util-deprecate@^1.0.2, util-deprecate@~1.0.1: + version "1.0.2" + resolved "https://registry.yarnpkg.com/util-deprecate/-/util-deprecate-1.0.2.tgz#450d4dc9fa70de732762fbd2d4a28981419a0ccf" + +util.promisify@~1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/util.promisify/-/util.promisify-1.0.0.tgz#440f7165a459c9a16dc145eb8e72f35687097030" + dependencies: + define-properties "^1.1.2" + object.getownpropertydescriptors "^2.0.3" + +util@0.10.3: + version "0.10.3" + resolved "https://registry.yarnpkg.com/util/-/util-0.10.3.tgz#7afb1afe50805246489e3db7fe0ed379336ac0f9" + dependencies: + inherits "2.0.1" + +util@^0.11.0: + version "0.11.1" + resolved "https://registry.yarnpkg.com/util/-/util-0.11.1.tgz#3236733720ec64bb27f6e26f421aaa2e1b588d61" + dependencies: + inherits "2.0.3" + +util@~0.10.1: + version "0.10.4" + resolved "https://registry.yarnpkg.com/util/-/util-0.10.4.tgz#3aa0125bfe668a4672de58857d3ace27ecb76901" + dependencies: + inherits "2.0.3" + +utils-merge@1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/utils-merge/-/utils-merge-1.0.1.tgz#9f95710f50a267947b2ccc124741c1028427e713" + +uuid@^3.3.2: + version "3.3.3" + resolved "https://registry.yarnpkg.com/uuid/-/uuid-3.3.3.tgz#4568f0216e78760ee1dbf3a4d2cf53e224112866" + +uuid@^3.4.0: + version "3.4.0" + resolved "https://registry.yarnpkg.com/uuid/-/uuid-3.4.0.tgz#b23e4358afa8a202fe7a100af1f5f883f02007ee" + integrity sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A== + +uuid@^7.0.3: + version "7.0.3" + resolved "https://registry.yarnpkg.com/uuid/-/uuid-7.0.3.tgz#c5c9f2c8cf25dc0a372c4df1441c41f5bd0c680b" + integrity sha512-DPSke0pXhTZgoF/d+WSt2QaKMCFSfx7QegxEWT+JOuHF5aWrKEn0G+ztjuJg/gG8/ItK+rbPCD/yNv8yyih6Cg== + +v8-compile-cache@^2.0.3, v8-compile-cache@^2.1.1: + version "2.1.1" + resolved "https://registry.yarnpkg.com/v8-compile-cache/-/v8-compile-cache-2.1.1.tgz#54bc3cdd43317bca91e35dcaf305b1a7237de745" + integrity sha512-8OQ9CL+VWyt3JStj7HX7/ciTL2V3Rl1Wf5OL+SNTm0yK1KvtReVulksyeRnCANHHuUxHlQig+JJDlUhBt1NQDQ== + +validate-npm-package-license@^3.0.1: + version "3.0.4" + resolved "https://registry.yarnpkg.com/validate-npm-package-license/-/validate-npm-package-license-3.0.4.tgz#fc91f6b9c7ba15c857f4cb2c5defeec39d4f410a" + dependencies: + spdx-correct "^3.0.0" + spdx-expression-parse "^3.0.0" + +vary@~1.1.2: + version "1.1.2" + resolved "https://registry.yarnpkg.com/vary/-/vary-1.1.2.tgz#2299f02c6ded30d4a5961b0b9f74524a18f634fc" + +vendors@^1.0.0: + version "1.0.3" + resolved "https://registry.yarnpkg.com/vendors/-/vendors-1.0.3.tgz#a6467781abd366217c050f8202e7e50cc9eef8c0" + +verror@1.10.0: + version "1.10.0" + resolved "https://registry.yarnpkg.com/verror/-/verror-1.10.0.tgz#3a105ca17053af55d6e270c1f8288682e18da400" + dependencies: + assert-plus "^1.0.0" + core-util-is "1.0.2" + extsprintf "^1.2.0" + +vm-browserify@^1.0.0: + version "1.1.2" + resolved "https://registry.yarnpkg.com/vm-browserify/-/vm-browserify-1.1.2.tgz#78641c488b8e6ca91a75f511e7a3b32a86e5dda0" + integrity sha512-2ham8XPWTONajOR0ohOKOHXkm3+gaBmGut3SRuu75xLd/RRaY6vqgh8NBYYk7+RW3u5AtzPQZG8F10LHkl0lAQ== + +vm-browserify@^1.0.1: + version "1.1.0" + resolved "https://registry.yarnpkg.com/vm-browserify/-/vm-browserify-1.1.0.tgz#bd76d6a23323e2ca8ffa12028dc04559c75f9019" + +warning@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/warning/-/warning-3.0.0.tgz#32e5377cb572de4ab04753bdf8821c01ed605b7c" + dependencies: + loose-envify "^1.0.0" + +warning@^4.0.2, warning@^4.0.3: + version "4.0.3" + resolved "https://registry.yarnpkg.com/warning/-/warning-4.0.3.tgz#16e9e077eb8a86d6af7d64aa1e05fd85b4678ca3" + dependencies: + loose-envify "^1.0.0" + +watchpack-chokidar2@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/watchpack-chokidar2/-/watchpack-chokidar2-2.0.0.tgz#9948a1866cbbd6cb824dea13a7ed691f6c8ddff0" + integrity sha512-9TyfOyN/zLUbA288wZ8IsMZ+6cbzvsNyEzSBp6e/zkifi6xxbl8SmQ/CxQq32k8NNqrdVEVUVSEf56L4rQ/ZxA== + dependencies: + chokidar "^2.1.8" + +watchpack@^1.7.4: + version "1.7.4" + resolved "https://registry.yarnpkg.com/watchpack/-/watchpack-1.7.4.tgz#6e9da53b3c80bb2d6508188f5b200410866cd30b" + integrity sha512-aWAgTW4MoSJzZPAicljkO1hsi1oKj/RRq/OJQh2PKI2UKL04c2Bs+MBOB+BBABHTXJpf9mCwHN7ANCvYsvY2sg== + dependencies: + graceful-fs "^4.1.2" + neo-async "^2.5.0" + optionalDependencies: + chokidar "^3.4.1" + watchpack-chokidar2 "^2.0.0" + +wbuf@^1.1.0, wbuf@^1.7.3: + version "1.7.3" + resolved "https://registry.yarnpkg.com/wbuf/-/wbuf-1.7.3.tgz#c1d8d149316d3ea852848895cb6a0bfe887b87df" + dependencies: + minimalistic-assert "^1.0.0" + +weakmap-polyfill@2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/weakmap-polyfill/-/weakmap-polyfill-2.0.1.tgz#ff0278c7f75bb0c492fc3343257f3e87114d709a" + integrity sha512-Jy177Lvb1LCrPQDWJsXyyqf4eOhcdvQHFGoCqSv921kVF5i42MVbr4e2WEwetuTLBn1NX0IhPzTmMu0N3cURqQ== + +webpack-assets-manifest@^3.1.1: + version "3.1.1" + resolved "https://registry.yarnpkg.com/webpack-assets-manifest/-/webpack-assets-manifest-3.1.1.tgz#39bbc3bf2ee57fcd8ba07cda51c9ba4a3c6ae1de" + integrity sha512-JV9V2QKc5wEWQptdIjvXDUL1ucbPLH2f27toAY3SNdGZp+xSaStAgpoMcvMZmqtFrBc9a5pTS1058vxyMPOzRQ== + dependencies: + chalk "^2.0" + lodash.get "^4.0" + lodash.has "^4.0" + mkdirp "^0.5" + schema-utils "^1.0.0" + tapable "^1.0.0" + webpack-sources "^1.0.0" + +webpack-cli@^3.3.12: + version "3.3.12" + resolved "https://registry.yarnpkg.com/webpack-cli/-/webpack-cli-3.3.12.tgz#94e9ada081453cd0aa609c99e500012fd3ad2d4a" + integrity sha512-NVWBaz9k839ZH/sinurM+HcDvJOTXwSjYp1ku+5XKeOC03z8v5QitnK/x+lAxGXFyhdayoIf/GOpv85z3/xPag== + dependencies: + chalk "^2.4.2" + cross-spawn "^6.0.5" + enhanced-resolve "^4.1.1" + findup-sync "^3.0.0" + global-modules "^2.0.0" + import-local "^2.0.0" + interpret "^1.4.0" + loader-utils "^1.4.0" + supports-color "^6.1.0" + v8-compile-cache "^2.1.1" + yargs "^13.3.2" + +webpack-dev-middleware@^3.7.2: + version "3.7.2" + resolved "https://registry.yarnpkg.com/webpack-dev-middleware/-/webpack-dev-middleware-3.7.2.tgz#0019c3db716e3fa5cecbf64f2ab88a74bab331f3" + integrity sha512-1xC42LxbYoqLNAhV6YzTYacicgMZQTqRd27Sim9wn5hJrX3I5nxYy1SxSd4+gjUFsz1dQFj+yEe6zEVmSkeJjw== + dependencies: + memory-fs "^0.4.1" + mime "^2.4.4" + mkdirp "^0.5.1" + range-parser "^1.2.1" + webpack-log "^2.0.0" + +webpack-dev-server@^3.11.2: + version "3.11.2" + resolved "https://registry.yarnpkg.com/webpack-dev-server/-/webpack-dev-server-3.11.2.tgz#695ebced76a4929f0d5de7fd73fafe185fe33708" + integrity sha512-A80BkuHRQfCiNtGBS1EMf2ChTUs0x+B3wGDFmOeT4rmJOHhHTCH2naNxIHhmkr0/UillP4U3yeIyv1pNp+QDLQ== + dependencies: + ansi-html "0.0.7" + bonjour "^3.5.0" + chokidar "^2.1.8" + compression "^1.7.4" + connect-history-api-fallback "^1.6.0" + debug "^4.1.1" + del "^4.1.1" + express "^4.17.1" + html-entities "^1.3.1" + http-proxy-middleware "0.19.1" + import-local "^2.0.0" + internal-ip "^4.3.0" + ip "^1.1.5" + is-absolute-url "^3.0.3" + killable "^1.0.1" + loglevel "^1.6.8" + opn "^5.5.0" + p-retry "^3.0.1" + portfinder "^1.0.26" + schema-utils "^1.0.0" + selfsigned "^1.10.8" + semver "^6.3.0" + serve-index "^1.9.1" + sockjs "^0.3.21" + sockjs-client "^1.5.0" + spdy "^4.0.2" + strip-ansi "^3.0.1" + supports-color "^6.1.0" + url "^0.11.0" + webpack-dev-middleware "^3.7.2" + webpack-log "^2.0.0" + ws "^6.2.1" + yargs "^13.3.2" + +webpack-log@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/webpack-log/-/webpack-log-2.0.0.tgz#5b7928e0637593f119d32f6227c1e0ac31e1b47f" + integrity sha512-cX8G2vR/85UYG59FgkoMamwHUIkSSlV3bBMRsbxVXVUk2j6NleCKjQ/WE9eYg9WY4w25O9w8wKP4rzNZFmUcUg== + dependencies: + ansi-colors "^3.0.0" + uuid "^3.3.2" + +webpack-sources@^1.0.0, webpack-sources@^1.1.0, webpack-sources@^1.4.0, webpack-sources@^1.4.1, webpack-sources@^1.4.3: + version "1.4.3" + resolved "https://registry.yarnpkg.com/webpack-sources/-/webpack-sources-1.4.3.tgz#eedd8ec0b928fbf1cbfe994e22d2d890f330a933" + dependencies: + source-list-map "^2.0.0" + source-map "~0.6.1" + +webpack@^4.44.1: + version "4.44.2" + resolved "https://registry.yarnpkg.com/webpack/-/webpack-4.44.2.tgz#6bfe2b0af055c8b2d1e90ed2cd9363f841266b72" + integrity sha512-6KJVGlCxYdISyurpQ0IPTklv+DULv05rs2hseIXer6D7KrUicRDLFb4IUM1S6LUAKypPM/nSiVSuv8jHu1m3/Q== + dependencies: + "@webassemblyjs/ast" "1.9.0" + "@webassemblyjs/helper-module-context" "1.9.0" + "@webassemblyjs/wasm-edit" "1.9.0" + "@webassemblyjs/wasm-parser" "1.9.0" + acorn "^6.4.1" + ajv "^6.10.2" + ajv-keywords "^3.4.1" + chrome-trace-event "^1.0.2" + enhanced-resolve "^4.3.0" + eslint-scope "^4.0.3" + json-parse-better-errors "^1.0.2" + loader-runner "^2.4.0" + loader-utils "^1.2.3" + memory-fs "^0.4.1" + micromatch "^3.1.10" + mkdirp "^0.5.3" + neo-async "^2.6.1" + node-libs-browser "^2.2.1" + schema-utils "^1.0.0" + tapable "^1.1.3" + terser-webpack-plugin "^1.4.3" + watchpack "^1.7.4" + webpack-sources "^1.4.1" + +websocket-driver@>=0.5.1: + version "0.7.3" + resolved "https://registry.yarnpkg.com/websocket-driver/-/websocket-driver-0.7.3.tgz#a2d4e0d4f4f116f1e6297eba58b05d430100e9f9" + dependencies: + http-parser-js ">=0.4.0 <0.4.11" + safe-buffer ">=5.1.0" + websocket-extensions ">=0.1.1" + +websocket-driver@^0.7.4: + version "0.7.4" + resolved "https://registry.yarnpkg.com/websocket-driver/-/websocket-driver-0.7.4.tgz#89ad5295bbf64b480abcba31e4953aca706f5760" + integrity sha512-b17KeDIQVjvb0ssuSDF2cYXSg2iztliJ4B9WdsuB6J952qCPKmnVq4DyW5motImXHDC1cBT/1UezrJVsKw5zjg== + dependencies: + http-parser-js ">=0.5.1" + safe-buffer ">=5.1.0" + websocket-extensions ">=0.1.1" + +websocket-extensions@>=0.1.1, websocket-extensions@^0.1.4: + version "0.1.4" + resolved "https://registry.yarnpkg.com/websocket-extensions/-/websocket-extensions-0.1.4.tgz#7f8473bc839dfd87608adb95d7eb075211578a42" + integrity sha512-OqedPIGOfsDlo31UNwYbCFMSaO9m9G/0faIHj5/dZFDMFqPTcx6UwqyOy3COEaEOg/9VsGIpdqn62W5KhoKSpg== + +which-module@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/which-module/-/which-module-2.0.0.tgz#d9ef07dce77b9902b8a3a8fa4b31c3e3f7e6e87a" + +which@1, which@^1.2.14, which@^1.2.9, which@^1.3.1: + version "1.3.1" + resolved "https://registry.yarnpkg.com/which/-/which-1.3.1.tgz#a45043d54f5805316da8d62f9f50918d3da70b0a" + dependencies: + isexe "^2.0.0" + +which@^2.0.1: + version "2.0.2" + resolved "https://registry.yarnpkg.com/which/-/which-2.0.2.tgz#7c6a8dd0a636a0327e10b59c9286eee93f3f51b1" + integrity sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA== + dependencies: + isexe "^2.0.0" + +wide-align@^1.1.0: + version "1.1.3" + resolved "https://registry.yarnpkg.com/wide-align/-/wide-align-1.1.3.tgz#ae074e6bdc0c14a431e804e624549c633b000457" + dependencies: + string-width "^1.0.2 || 2" + +word-wrap@^1.2.3: + version "1.2.3" + resolved "https://registry.yarnpkg.com/word-wrap/-/word-wrap-1.2.3.tgz#610636f6b1f703891bd34771ccb17fb93b47079c" + integrity sha512-Hz/mrNwitNRh/HUAtM/VT/5VH+ygD6DV7mYKZAtHOrbs8U7lvPS6xf7EJKMF0uW1KJCl0H701g3ZGus+muE5vQ== + +worker-farm@^1.7.0: + version "1.7.0" + resolved "https://registry.yarnpkg.com/worker-farm/-/worker-farm-1.7.0.tgz#26a94c5391bbca926152002f69b84a4bf772e5a8" + integrity sha512-rvw3QTZc8lAxyVrqcSGVm5yP/IJ2UcB3U0graE3LCFoZ0Yn2x4EoVSqJKdB/T5M+FLcRPjz4TDacRf3OCfNUzw== + dependencies: + errno "~0.1.7" + +worker-loader@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/worker-loader/-/worker-loader-2.0.0.tgz#45fda3ef76aca815771a89107399ee4119b430ac" + integrity sha512-tnvNp4K3KQOpfRnD20m8xltE3eWh89Ye+5oj7wXEEHKac1P4oZ6p9oTj8/8ExqoSBnk9nu5Pr4nKfQ1hn2APJw== + dependencies: + loader-utils "^1.0.0" + schema-utils "^0.4.0" + +wrap-ansi@^5.1.0: + version "5.1.0" + resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-5.1.0.tgz#1fd1f67235d5b6d0fee781056001bfb694c03b09" + integrity sha512-QC1/iN/2/RPVJ5jYK8BGttj5z83LmSKmvbvrXPNCLZSEb32KKVDJDl/MOt2N01qU2H/FkzEa9PKto1BqDjtd7Q== + dependencies: + ansi-styles "^3.2.0" + string-width "^3.0.0" + strip-ansi "^5.0.0" + +wrappy@1: + version "1.0.2" + resolved "https://registry.yarnpkg.com/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f" + +ws@^6.2.1: + version "6.2.1" + resolved "https://registry.yarnpkg.com/ws/-/ws-6.2.1.tgz#442fdf0a47ed64f59b6a5d8ff130f4748ed524fb" + integrity sha512-GIyAXC2cB7LjvpgMt9EKS2ldqr0MTrORaleiOno6TweZ6r3TKtoFQWay/2PceJ3RuBasOHzXNn5Lrw1X0bEjqA== + dependencies: + async-limiter "~1.0.0" + +xtend@^4.0.0, xtend@^4.0.1, xtend@^4.0.2, xtend@~4.0.1: + version "4.0.2" + resolved "https://registry.yarnpkg.com/xtend/-/xtend-4.0.2.tgz#bb72779f5fa465186b1f438f674fa347fdb5db54" + +y18n@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/y18n/-/y18n-4.0.0.tgz#95ef94f85ecc81d007c264e190a120f0a3c8566b" + +yallist@^2.1.2: + version "2.1.2" + resolved "https://registry.yarnpkg.com/yallist/-/yallist-2.1.2.tgz#1c11f9218f076089a47dd512f93c6699a6a81d52" + +yallist@^3.0.0, yallist@^3.0.3: + version "3.0.3" + resolved "https://registry.yarnpkg.com/yallist/-/yallist-3.0.3.tgz#b4b049e314be545e3ce802236d6cd22cd91c3de9" + +yallist@^3.0.2: + version "3.1.1" + resolved "https://registry.yarnpkg.com/yallist/-/yallist-3.1.1.tgz#dbb7daf9bfd8bac9ab45ebf602b8cbad0d5d08fd" + integrity sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g== + +yallist@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/yallist/-/yallist-4.0.0.tgz#9bb92790d9c0effec63be73519e11a35019a3a72" + integrity sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A== + +yaml@^1.7.2: + version "1.10.0" + resolved "https://registry.yarnpkg.com/yaml/-/yaml-1.10.0.tgz#3b593add944876077d4d683fee01081bd9fff31e" + integrity sha512-yr2icI4glYaNG+KWONODapy2/jDdMSDnrONSjblABjD9B4Z5LgiircSt8m8sRZFNi08kG9Sm0uSHtEmP3zaEGg== + +yargs-parser@^13.1.2: + version "13.1.2" + resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-13.1.2.tgz#130f09702ebaeef2650d54ce6e3e5706f7a4fb38" + integrity sha512-3lbsNRf/j+A4QuSZfDRA7HRSfWrzO0YjqTJd5kjAq37Zep1CEgaYmrH9Q3GwPiB9cHyd1Y1UwggGhJGoxipbzg== + dependencies: + camelcase "^5.0.0" + decamelize "^1.2.0" + +yargs@^13.3.2: + version "13.3.2" + resolved "https://registry.yarnpkg.com/yargs/-/yargs-13.3.2.tgz#ad7ffefec1aa59565ac915f82dccb38a9c31a2dd" + integrity sha512-AX3Zw5iPruN5ie6xGRIDgqkT+ZhnRlZMLMHAs8tg7nRruy2Nb+i5o9bwghAogtM08q1dpr2LVoS8KSTMYpWXUw== + dependencies: + cliui "^5.0.0" + find-up "^3.0.0" + get-caller-file "^2.0.1" + require-directory "^2.1.1" + require-main-filename "^2.0.0" + set-blocking "^2.0.0" + string-width "^3.0.0" + which-module "^2.0.0" + y18n "^4.0.0" + yargs-parser "^13.1.2"