diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..6509c91
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,27 @@
+# General
+.DS_Store
+.AppleDouble
+.LSOverride
+
+# Icon must end with two \r
+Icon
+
+
+# Thumbnails
+._*
+
+# Files that might appear in the root of a volume
+.DocumentRevisions-V100
+.fseventsd
+.Spotlight-V100
+.TemporaryItems
+.Trashes
+.VolumeIcon.icns
+.com.apple.timemachine.donotpresent
+
+# Directories potentially created on remote AFP share
+.AppleDB
+.AppleDesktop
+Network Trash Folder
+Temporary Items
+.apdisk
diff --git a/LICENSE b/LICENSE
index e23e97e..4304181 100644
--- a/LICENSE
+++ b/LICENSE
@@ -1,6 +1,6 @@
MIT License
-Copyright (c) 2020 KeepWorks
+Copyright (c) 2020 KeepWorks Technologies Pvt Ltd
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
diff --git a/README.md b/README.md
index 7e92398..330b0e2 100644
--- a/README.md
+++ b/README.md
@@ -1,2 +1,3 @@
-# claycms
-Clay CMS Monorepo
+# Clay CMS
+
+Clay CMS Monorepo.
diff --git a/api/.env.example b/api/.env.example
new file mode 100755
index 0000000..8d41f35
--- /dev/null
+++ b/api/.env.example
@@ -0,0 +1,14 @@
+API_HOST=api.claycms-dev.io
+API_PORT=8080
+ASSET_HOST=http://api.claycms-dev.io:8080
+APP_HOST=claycms-dev.io
+RACK_ENV=development
+RAILS_ENV=development
+FORCE_SSL=
+PORT=8080
+CLOUDAMQP_URL=amqp://guest:guest@localhost:5672
+RABBIT_HOST=
+RABBIT_PORT=
+CLAY_SSO_URL=http://localhost:3000/sso/clay
+CLAY_SSO_SECRET=f1be8d630eb5d477310ed1882c5a04c0f6e7ac8e17e0d138a15fdea6495c6bf0
+HMAC_SECRET=4f78a814febed95543ccf83be99ad7baabd42e1db09ffd934e2a866a3e91b106
diff --git a/api/.gitignore b/api/.gitignore
new file mode 100755
index 0000000..da58fcd
--- /dev/null
+++ b/api/.gitignore
@@ -0,0 +1,136 @@
+## Rails
+
+*.rbc
+*.sassc
+.sass-cache
+capybara-*.html
+.rspec
+/db/*.sqlite3
+/db/*.sqlite3-journal
+/public/system
+/public/assets
+/public/uploads
+/public/exports
+/coverage/
+/spec/reports/
+/spec/tmp
+/spec/examples.txt
+rerun.txt
+pickle-email-*.html
+dump.rdb
+
+/node_modules
+/yarn-error.log
+
+## Environment normalisation:
+/.bundle
+/vendor/bundle
+
+# unless supporting rvm < 1.11.0 or doing something fancy, ignore this:
+.rvmrc
+
+# if using bower-rails ignore default bower_components path bower.json files
+/vendor/assets/bower_components
+*.bowerrc
+bower.json
+
+# Ignore pow environment settings
+.powenv
+
+# Ignore all logfiles and tempfiles.
+/log/*
+/tmp/*
+!/log/.keep
+!/tmp/.keep
+
+# Ignore uploaded files in development
+/storage/*
+
+# Ignore master key for decrypting credentials and more.
+/config/master.key
+
+# Ignore Byebug command history file.
+.byebug_history
+
+## Documentation cache and generated files:
+/.yardoc/
+/_yardoc/
+/doc/
+/rdoc/
+
+# Developer-specific files - These have a corresponding *.example file as a template to quickly copy over
+config/database.yml
+.env
+
+## General
+
+# Git
+**.orig
+
+# OS X
+.DS_Store
+.DS_Store?
+.AppleDouble
+.LSOverride
+
+# Icon must end with two \r
+Icon
+
+
+# Thumbnails
+._*
+
+# Files that might appear on external disk
+.Spotlight-V100
+.Trashes
+
+# Directories potentially created on remote AFP share
+.AppleDB
+.AppleDesktop
+Network Trash Folder
+Temporary Items
+.apdisk
+
+# Windows image file caches
+Thumbs.db
+ehthumbs.db
+
+# Folder config file
+Desktop.ini
+
+# Recycle Bin used on file shares
+$RECYCLE.BIN/
+
+# Windows Installer files
+*.cab
+*.msi
+*.msm
+*.msp
+
+# Windows shortcuts
+*.lnk
+
+# Compiled source
+*.com
+*.class
+*.dll
+*.exe
+*.o
+*.so
+
+# Packages
+# it's better to unpack these files and commit the raw source
+# git has its own built in compression methods
+*.7z
+*.dmg
+*.gz
+*.iso
+*.jar
+*.rar
+*.tar
+*.zip
+
+# Logs and databases
+*.log
+*.sql
+*.sqlite
diff --git a/api/.rubocop.yml b/api/.rubocop.yml
new file mode 100755
index 0000000..2c3d11d
--- /dev/null
+++ b/api/.rubocop.yml
@@ -0,0 +1,97 @@
+require: rubocop-rails
+
+AllCops:
+ Exclude:
+ - 'db/schema.rb'
+ - 'bin/*'
+
+Bundler/OrderedGems:
+ Enabled: false
+
+Layout/FirstArrayElementIndentation:
+ EnforcedStyle: consistent
+
+Layout/FirstHashElementIndentation:
+ EnforcedStyle: consistent
+
+Layout/MultilineMethodCallIndentation:
+ EnforcedStyle: indented
+
+Lint/UnusedBlockArgument:
+ Enabled: false
+
+Metrics/AbcSize:
+ Enabled: false
+
+Metrics/BlockLength:
+ Enabled: false
+
+Layout/LineLength:
+ Enabled: false
+
+Lint/NonDeterministicRequireOrder:
+ Enabled: false
+
+Lint/RaiseException:
+ Enabled: true
+
+Lint/StructNewOverride:
+ Enabled: true
+
+Metrics/MethodLength:
+ Enabled: false
+
+Rails:
+ Enabled: true
+
+Rails/Delegate:
+ Enabled: false
+
+Rails/FilePath:
+ EnforcedStyle: arguments
+
+Rails/HasAndBelongsToMany:
+ Enabled: false
+
+Rails/ReversibleMigration:
+ Enabled: false
+
+Rails/SkipsModelValidations:
+ Enabled: false
+
+Rails/UniqueValidationWithoutIndex:
+ Enabled: false
+
+Rails/UnknownEnv:
+ Environments:
+ - development
+ - test
+ - staging
+ - production
+
+Style/Documentation:
+ Enabled: false
+
+Style/FrozenStringLiteralComment: # This cop is designed to help upgrade to Ruby 3.0
+ Enabled: false
+
+Style/Lambda:
+ EnforcedStyle: literal
+
+Style/NegatedIf:
+ Enabled: false
+
+Style/NumericPredicate:
+ EnforcedStyle: comparison
+
+Style/SymbolArray:
+ EnforcedStyle: brackets
+
+Style/HashEachMethods:
+ Enabled: true
+
+Style/HashTransformKeys:
+ Enabled: true
+
+Style/HashTransformValues:
+ Enabled: true
diff --git a/api/.ruby-gemset b/api/.ruby-gemset
new file mode 100755
index 0000000..27db7eb
--- /dev/null
+++ b/api/.ruby-gemset
@@ -0,0 +1 @@
+claycms-api
diff --git a/api/.ruby-version b/api/.ruby-version
new file mode 100755
index 0000000..cb1d89f
--- /dev/null
+++ b/api/.ruby-version
@@ -0,0 +1 @@
+ruby-2.5.0
\ No newline at end of file
diff --git a/api/Gemfile b/api/Gemfile
new file mode 100755
index 0000000..7fa5770
--- /dev/null
+++ b/api/Gemfile
@@ -0,0 +1,103 @@
+source 'https://rubygems.org'
+git_source(:github) { |repo| "https://github.com/#{repo}.git" }
+
+ruby '2.5.0'
+
+# Stack
+gem 'rails', '~> 5.2.0.rc1'
+gem 'pg'
+gem 'mysql2', '~>0.4.4'
+gem 'puma', '~> 3.11'
+gem 'foreman'
+gem 'rack-cors'
+gem 'rack-canonical-host'
+
+# Auth
+gem 'bcrypt', '~> 3.1.7'
+gem 'pundit'
+gem 'jwt'
+
+# ActiveRecord
+gem 'closure_tree'
+gem 'activerecord-import'
+gem 'amoeba'
+
+# Logging
+gem 'ruby-kafka'
+gem 'rails_semantic_logger'
+
+# Error Logging
+gem 'sentry-raven'
+
+# Performance
+gem 'scout_apm'
+
+# Services
+gem 'interactor-rails', '~> 2.0'
+
+# GraphQL
+gem 'graphql', '~> 1.8.0.pre6'
+gem 'graphql-sugar', github: 'keepworks/graphql-sugar', branch: 'support-1.8-datetime'
+gem 'graphql-guard'
+gem 'apollo_upload_server', '2.0.0.beta.1'
+
+# Uploads
+gem 'shrine', '~> 3.2', '>= 3.2.1'
+gem 'image_processing'
+gem 'aws-sdk-s3', '~> 1'
+
+# Background Jobs
+gem 'bunny'
+gem 'sneakers'
+
+# Misc
+gem 'geocoder'
+gem 'mail'
+gem 'device_detector'
+
+# Reduces boot times through caching; required in config/boot.rb
+gem 'bootsnap', '>= 1.1.0', require: false
+
+group :development do
+ gem 'bullet'
+ gem 'graphiql-rails'
+ gem 'graphql-rails_logger'
+ gem 'listen', '>= 3.0.5', '< 3.2'
+ gem 'spring'
+ gem 'spring-watcher-listen', '~> 2.0.0'
+end
+
+group :test do
+ gem 'simplecov', require: false
+ gem 'shoulda-matchers', github: 'thoughtbot/shoulda-matchers'
+ gem 'pundit-matchers', '~> 1.6.0'
+ gem 'timecop'
+ gem 'webmock'
+ gem 'climate_control'
+end
+
+group :development, :test do
+ gem 'awesome_print'
+ gem 'rspec-rails'
+ gem 'factory_bot_rails'
+
+ gem 'rubocop', '~> 0.81.0', require: false
+ gem 'rubocop-rails'
+
+ gem 'guard'
+ gem 'guard-brakeman', require: false
+ gem 'guard-bundler', require: false
+ gem 'guard-foreman'
+ gem 'guard-rspec'
+ gem 'guard-rubocop'
+ gem 'terminal-notifier-guard', require: false # Shows test alerts in OS X 10.8 Notification Center
+
+ gem 'byebug', platforms: [:mri, :mingw, :x64_mingw]
+end
+
+group :development, :test, :staging do
+ gem 'ffaker'
+end
+
+# Windows does not include zoneinfo files, so bundle the tzinfo-data gem
+gem 'tzinfo-data', platforms: [:mingw, :mswin, :x64_mingw, :jruby]
diff --git a/api/Gemfile.lock b/api/Gemfile.lock
new file mode 100755
index 0000000..b9302a2
--- /dev/null
+++ b/api/Gemfile.lock
@@ -0,0 +1,428 @@
+GIT
+ remote: https://github.com/keepworks/graphql-sugar.git
+ revision: 8d94ce61509457e3a7c574b66dcf692c31394387
+ branch: support-1.8-datetime
+ specs:
+ graphql-sugar (0.1.6)
+
+GIT
+ remote: https://github.com/thoughtbot/shoulda-matchers.git
+ revision: cd96089a56b97cd11f7502826636895253eca27d
+ specs:
+ shoulda-matchers (3.1.2)
+ activesupport (>= 4.2.0)
+
+GEM
+ remote: https://rubygems.org/
+ specs:
+ actioncable (5.2.0.rc1)
+ actionpack (= 5.2.0.rc1)
+ nio4r (~> 2.0)
+ websocket-driver (>= 0.6.1)
+ actionmailer (5.2.0.rc1)
+ actionpack (= 5.2.0.rc1)
+ actionview (= 5.2.0.rc1)
+ activejob (= 5.2.0.rc1)
+ mail (~> 2.5, >= 2.5.4)
+ rails-dom-testing (~> 2.0)
+ actionpack (5.2.0.rc1)
+ actionview (= 5.2.0.rc1)
+ activesupport (= 5.2.0.rc1)
+ rack (~> 2.0)
+ rack-test (>= 0.6.3)
+ rails-dom-testing (~> 2.0)
+ rails-html-sanitizer (~> 1.0, >= 1.0.2)
+ actionview (5.2.0.rc1)
+ activesupport (= 5.2.0.rc1)
+ builder (~> 3.1)
+ erubi (~> 1.4)
+ rails-dom-testing (~> 2.0)
+ rails-html-sanitizer (~> 1.0, >= 1.0.3)
+ activejob (5.2.0.rc1)
+ activesupport (= 5.2.0.rc1)
+ globalid (>= 0.3.6)
+ activemodel (5.2.0.rc1)
+ activesupport (= 5.2.0.rc1)
+ activerecord (5.2.0.rc1)
+ activemodel (= 5.2.0.rc1)
+ activesupport (= 5.2.0.rc1)
+ arel (>= 9.0)
+ activerecord-import (1.0.4)
+ activerecord (>= 3.2)
+ activestorage (5.2.0.rc1)
+ actionpack (= 5.2.0.rc1)
+ activerecord (= 5.2.0.rc1)
+ marcel (~> 0.3.1)
+ activesupport (5.2.0.rc1)
+ concurrent-ruby (~> 1.0, >= 1.0.2)
+ i18n (~> 0.7)
+ minitest (~> 5.1)
+ tzinfo (~> 1.1)
+ addressable (2.7.0)
+ public_suffix (>= 2.0.2, < 5.0)
+ amoeba (3.1.0)
+ activerecord (>= 3.2.6)
+ amq-protocol (2.3.1)
+ apollo_upload_server (2.0.0.beta.1)
+ graphql (~> 1.7)
+ rails (>= 4.2)
+ arel (9.0.0)
+ ast (2.4.0)
+ awesome_print (1.8.0)
+ aws-eventstream (1.0.3)
+ aws-partitions (1.210.0)
+ aws-sdk-core (3.66.0)
+ aws-eventstream (~> 1.0, >= 1.0.2)
+ aws-partitions (~> 1.0)
+ aws-sigv4 (~> 1.1)
+ jmespath (~> 1.0)
+ aws-sdk-kms (1.24.0)
+ aws-sdk-core (~> 3, >= 3.61.1)
+ aws-sigv4 (~> 1.1)
+ aws-sdk-s3 (1.48.0)
+ aws-sdk-core (~> 3, >= 3.61.1)
+ aws-sdk-kms (~> 1)
+ aws-sigv4 (~> 1.1)
+ aws-sigv4 (1.1.0)
+ aws-eventstream (~> 1.0, >= 1.0.2)
+ bcrypt (3.1.11)
+ bootsnap (1.3.0)
+ msgpack (~> 1.0)
+ brakeman (4.1.1)
+ builder (3.2.3)
+ bullet (6.1.0)
+ activesupport (>= 3.0.0)
+ uniform_notifier (~> 1.11)
+ bunny (2.15.0)
+ amq-protocol (~> 2.3, >= 2.3.1)
+ byebug (10.0.0)
+ climate_control (0.2.0)
+ closure_tree (7.0.0)
+ activerecord (>= 4.2.10)
+ with_advisory_lock (>= 4.0.0)
+ coderay (1.1.2)
+ concurrent-ruby (1.0.5)
+ content_disposition (1.0.0)
+ crack (0.4.3)
+ safe_yaml (~> 1.0.0)
+ crass (1.0.3)
+ device_detector (1.0.4)
+ diff-lcs (1.3)
+ digest-crc (0.5.1)
+ docile (1.3.1)
+ down (5.1.1)
+ addressable (~> 2.5)
+ erubi (1.7.0)
+ factory_bot (4.8.2)
+ activesupport (>= 3.0.0)
+ factory_bot_rails (4.8.2)
+ factory_bot (~> 4.8.2)
+ railties (>= 3.0.0)
+ faraday (0.15.0)
+ multipart-post (>= 1.2, < 3)
+ ffaker (2.8.1)
+ ffi (1.12.2)
+ foreman (0.87.1)
+ formatador (0.2.5)
+ geocoder (1.4.7)
+ globalid (0.4.1)
+ activesupport (>= 4.2.0)
+ graphiql-rails (1.7.0)
+ railties
+ sprockets-rails
+ graphql (1.8.0.pre6)
+ graphql-guard (1.0.0)
+ graphql (>= 1.6.0, < 2)
+ graphql-rails_logger (1.1.0)
+ actionpack (~> 5.0)
+ activesupport (~> 5.0)
+ railties (~> 5.0)
+ rouge (~> 3.0)
+ guard (2.16.2)
+ formatador (>= 0.2.4)
+ listen (>= 2.7, < 4.0)
+ lumberjack (>= 1.0.12, < 2.0)
+ nenv (~> 0.1)
+ notiffany (~> 0.0)
+ pry (>= 0.9.12)
+ shellany (~> 0.0)
+ thor (>= 0.18.1)
+ guard-brakeman (0.8.3)
+ brakeman (>= 2.1.1)
+ guard (>= 2.0.0)
+ guard-bundler (2.1.0)
+ bundler (~> 1.0)
+ guard (~> 2.2)
+ guard-compat (~> 1.1)
+ guard-compat (1.2.1)
+ guard-foreman (0.0.4)
+ guard (~> 2.6)
+ spoon (~> 0.0, >= 0.0.4)
+ guard-rspec (4.7.3)
+ guard (~> 2.1)
+ guard-compat (~> 1.1)
+ rspec (>= 2.99.0, < 4.0)
+ guard-rubocop (1.3.0)
+ guard (~> 2.0)
+ rubocop (~> 0.20)
+ hashdiff (0.3.7)
+ i18n (0.9.5)
+ concurrent-ruby (~> 1.0)
+ image_processing (1.9.3)
+ mini_magick (>= 4.9.5, < 5)
+ ruby-vips (>= 2.0.13, < 3)
+ interactor (3.1.0)
+ interactor-rails (2.1.1)
+ interactor (~> 3.0)
+ rails (>= 4.2, < 5.2)
+ jaro_winkler (1.5.4)
+ jmespath (1.4.0)
+ json (1.8.6)
+ jwt (2.2.2)
+ listen (3.1.5)
+ rb-fsevent (~> 0.9, >= 0.9.4)
+ rb-inotify (~> 0.9, >= 0.9.7)
+ ruby_dep (~> 1.2)
+ loofah (2.1.1)
+ crass (~> 1.0.2)
+ nokogiri (>= 1.5.9)
+ lumberjack (1.2.4)
+ mail (2.7.0)
+ mini_mime (>= 0.1.1)
+ marcel (0.3.1)
+ mimemagic (~> 0.3.2)
+ method_source (1.0.0)
+ mimemagic (0.3.2)
+ mini_magick (4.9.5)
+ mini_mime (1.0.0)
+ mini_portile2 (2.3.0)
+ minitest (5.11.3)
+ msgpack (1.2.4)
+ multipart-post (2.0.0)
+ mysql2 (0.4.10)
+ nenv (0.3.0)
+ nio4r (2.2.0)
+ nokogiri (1.8.2)
+ mini_portile2 (~> 2.3.0)
+ notiffany (0.1.3)
+ nenv (~> 0.1)
+ shellany (~> 0.0)
+ parallel (1.19.1)
+ parser (2.7.0.5)
+ ast (~> 2.4.0)
+ pg (1.0.0)
+ pry (0.13.0)
+ coderay (~> 1.1)
+ method_source (~> 1.0)
+ public_suffix (4.0.5)
+ puma (3.11.2)
+ pundit (1.1.0)
+ activesupport (>= 3.0.0)
+ pundit-matchers (1.6.0)
+ rspec-rails (>= 3.0.0)
+ rack (2.2.2)
+ rack-canonical-host (0.2.3)
+ addressable (> 0, < 3)
+ rack (>= 1.0.0, < 3)
+ rack-cors (1.0.2)
+ rack-test (0.8.2)
+ rack (>= 1.0, < 3)
+ rails (5.2.0.rc1)
+ actioncable (= 5.2.0.rc1)
+ actionmailer (= 5.2.0.rc1)
+ actionpack (= 5.2.0.rc1)
+ actionview (= 5.2.0.rc1)
+ activejob (= 5.2.0.rc1)
+ activemodel (= 5.2.0.rc1)
+ activerecord (= 5.2.0.rc1)
+ activestorage (= 5.2.0.rc1)
+ activesupport (= 5.2.0.rc1)
+ bundler (>= 1.3.0)
+ railties (= 5.2.0.rc1)
+ sprockets-rails (>= 2.0.0)
+ rails-dom-testing (2.0.3)
+ activesupport (>= 4.2.0)
+ nokogiri (>= 1.6)
+ rails-html-sanitizer (1.0.3)
+ loofah (~> 2.0)
+ rails_semantic_logger (4.4.4)
+ rack
+ railties (>= 3.2)
+ semantic_logger (~> 4.4)
+ railties (5.2.0.rc1)
+ actionpack (= 5.2.0.rc1)
+ activesupport (= 5.2.0.rc1)
+ method_source
+ rake (>= 0.8.7)
+ thor (>= 0.18.1, < 2.0)
+ rainbow (3.0.0)
+ rake (12.3.0)
+ rb-fsevent (0.10.3)
+ rb-inotify (0.10.1)
+ ffi (~> 1.0)
+ rexml (3.2.4)
+ rouge (3.3.0)
+ rspec (3.7.0)
+ rspec-core (~> 3.7.0)
+ rspec-expectations (~> 3.7.0)
+ rspec-mocks (~> 3.7.0)
+ rspec-core (3.7.1)
+ rspec-support (~> 3.7.0)
+ rspec-expectations (3.7.0)
+ diff-lcs (>= 1.2.0, < 2.0)
+ rspec-support (~> 3.7.0)
+ rspec-mocks (3.7.0)
+ diff-lcs (>= 1.2.0, < 2.0)
+ rspec-support (~> 3.7.0)
+ rspec-rails (3.7.2)
+ actionpack (>= 3.0)
+ activesupport (>= 3.0)
+ railties (>= 3.0)
+ rspec-core (~> 3.7.0)
+ rspec-expectations (~> 3.7.0)
+ rspec-mocks (~> 3.7.0)
+ rspec-support (~> 3.7.0)
+ rspec-support (3.7.1)
+ rubocop (0.81.0)
+ jaro_winkler (~> 1.5.1)
+ parallel (~> 1.10)
+ parser (>= 2.7.0.1)
+ rainbow (>= 2.2.2, < 4.0)
+ rexml
+ ruby-progressbar (~> 1.7)
+ unicode-display_width (>= 1.4.0, < 2.0)
+ rubocop-rails (2.5.1)
+ activesupport
+ rack (>= 1.1)
+ rubocop (>= 0.72.0)
+ ruby-kafka (1.0.0)
+ digest-crc
+ ruby-progressbar (1.10.1)
+ ruby-vips (2.0.14)
+ ffi (~> 1.9)
+ ruby_dep (1.5.0)
+ safe_yaml (1.0.4)
+ scout_apm (2.6.6)
+ parser
+ semantic_logger (4.6.1)
+ concurrent-ruby (~> 1.0)
+ sentry-raven (2.7.3)
+ faraday (>= 0.7.6, < 1.0)
+ serverengine (2.1.1)
+ sigdump (~> 0.2.2)
+ shellany (0.0.1)
+ shrine (3.2.1)
+ content_disposition (~> 1.0)
+ down (~> 5.1)
+ sigdump (0.2.4)
+ simplecov (0.16.1)
+ docile (~> 1.1)
+ json (>= 1.8, < 3)
+ simplecov-html (~> 0.10.0)
+ simplecov-html (0.10.2)
+ sneakers (2.12.0)
+ bunny (~> 2.14)
+ concurrent-ruby (~> 1.0)
+ rake (~> 12.3)
+ serverengine (~> 2.1.0)
+ thor
+ spoon (0.0.6)
+ ffi
+ spring (2.0.2)
+ activesupport (>= 4.2)
+ spring-watcher-listen (2.0.1)
+ listen (>= 2.7, < 4.0)
+ spring (>= 1.2, < 3.0)
+ sprockets (3.7.2)
+ concurrent-ruby (~> 1.0)
+ rack (> 1, < 3)
+ sprockets-rails (3.2.1)
+ actionpack (>= 4.0)
+ activesupport (>= 4.0)
+ sprockets (>= 3.0.0)
+ terminal-notifier-guard (1.7.0)
+ thor (1.0.1)
+ thread_safe (0.3.6)
+ timecop (0.9.1)
+ tzinfo (1.2.5)
+ thread_safe (~> 0.1)
+ unicode-display_width (1.7.0)
+ uniform_notifier (1.13.0)
+ webmock (3.4.1)
+ addressable (>= 2.3.6)
+ crack (>= 0.3.2)
+ hashdiff
+ websocket-driver (0.7.0)
+ websocket-extensions (>= 0.1.0)
+ websocket-extensions (0.1.3)
+ with_advisory_lock (4.6.0)
+ activerecord (>= 4.2)
+
+PLATFORMS
+ ruby
+
+DEPENDENCIES
+ activerecord-import
+ amoeba
+ apollo_upload_server (= 2.0.0.beta.1)
+ awesome_print
+ aws-sdk-s3 (~> 1)
+ bcrypt (~> 3.1.7)
+ bootsnap (>= 1.1.0)
+ bullet
+ bunny
+ byebug
+ climate_control
+ closure_tree
+ device_detector
+ factory_bot_rails
+ ffaker
+ foreman
+ geocoder
+ graphiql-rails
+ graphql (~> 1.8.0.pre6)
+ graphql-guard
+ graphql-rails_logger
+ graphql-sugar!
+ guard
+ guard-brakeman
+ guard-bundler
+ guard-foreman
+ guard-rspec
+ guard-rubocop
+ image_processing
+ interactor-rails (~> 2.0)
+ jwt
+ listen (>= 3.0.5, < 3.2)
+ mail
+ mysql2 (~> 0.4.4)
+ pg
+ puma (~> 3.11)
+ pundit
+ pundit-matchers (~> 1.6.0)
+ rack-canonical-host
+ rack-cors
+ rails (~> 5.2.0.rc1)
+ rails_semantic_logger
+ rspec-rails
+ rubocop (~> 0.81.0)
+ rubocop-rails
+ ruby-kafka
+ scout_apm
+ sentry-raven
+ shoulda-matchers!
+ shrine (~> 3.2, >= 3.2.1)
+ simplecov
+ sneakers
+ spring
+ spring-watcher-listen (~> 2.0.0)
+ terminal-notifier-guard
+ timecop
+ tzinfo-data
+ webmock
+
+RUBY VERSION
+ ruby 2.5.0p0
+
+BUNDLED WITH
+ 1.17.3
diff --git a/api/Guardfile b/api/Guardfile
new file mode 100755
index 0000000..5fbbe59
--- /dev/null
+++ b/api/Guardfile
@@ -0,0 +1,42 @@
+guard :bundler do
+ require 'guard/bundler'
+ require 'guard/bundler/verify'
+ helper = Guard::Bundler::Verify.new
+
+ files = ['Gemfile']
+ files += Dir['*.gemspec'] if files.any? { |f| helper.uses_gemspec?(f) }
+
+ # Assume files are symlinked from somewhere
+ files.each { |file| watch(helper.real_path(file)) }
+end
+
+guard :foreman, concurrency: 'web=1,worker=1,release=0' do
+ watch(%r{^lib\/.+\.rb$})
+ watch(%r{^config\/*})
+end
+
+guard :rubocop, cli: ['--display-cop-names'] do
+ watch(/.+\.rb$/)
+ watch(%r{(?:.+/)?\.rubocop\.yml$}) { |m| File.dirname(m[0]) }
+end
+
+guard :brakeman, quiet: true do
+ watch(%r{^app/.+\.(slim|erb|haml|rhtml|rb)$})
+ watch(%r{^config/.+\.rb$})
+ watch(%r{^lib/.+\.rb$})
+ watch('Gemfile')
+end
+
+guard :rspec, cmd: 'bundle exec rspec', all_on_start: true do
+ watch('spec/spec_helper.rb') { 'spec' }
+ watch('config/routes.rb') { 'spec/routing' }
+ watch('app/controllers/application_controller.rb') { 'spec/controllers' }
+ watch(%r{^spec/.+_spec\.rb$})
+ watch(%r{^app/(.+)\.rb$}) { |m| "spec/#{m[1]}_spec.rb" }
+ watch(%r{^app/(.*)(\.erb|\.haml|\.slim)$}) { |m| "spec/#{m[1]}#{m[2]}_spec.rb" }
+ watch(%r{^lib/(.+)\.rb$}) { |m| "spec/lib/#{m[1]}_spec.rb" }
+ watch(%r{^app/controllers/(.+)_(controller)\.rb$}) { |m| ["spec/routing/#{m[1]}_routing_spec.rb", "spec/#{m[2]}s/#{m[1]}_#{m[2]}_spec.rb", "spec/acceptance/#{m[1]}_spec.rb"] }
+
+ # Watch factories
+ watch(%r{^spec/factories/(.*)s\.rb$}) { |m| "spec/models/#{m[1]}_spec.rb" }
+end
diff --git a/api/Procfile b/api/Procfile
new file mode 100755
index 0000000..8b214ba
--- /dev/null
+++ b/api/Procfile
@@ -0,0 +1,2 @@
+web: bundle exec puma -p $PORT
+worker: bundle exec rake sneakers:run
diff --git a/api/README.md b/api/README.md
new file mode 100755
index 0000000..d1bf37f
--- /dev/null
+++ b/api/README.md
@@ -0,0 +1,51 @@
+# Rails API
+
+## Installation
+
+### Dependencies:
+
+1. Install Terminal Notifier (`brew install terminal-notifier`). If you have already installed Terminal Notifier, ensure it is upgraded to the latest version (`brew upgrade terminal-notifier`).
+2. Install ImageMagick (`brew install imagemagick` or `sudo apt-get install imagemagick`)
+
+### First-Time Setup:
+
+1. Copy and modify the database.yml file: `cp config/database.yml.example config/database.yml`. If you are using PostgreSQL database then change the adapter to `postgresql` else if you are using MySQL / MariaDB change the adapter to `mysql2`.
+2. Copy and modify the .env file: `cp .env.example .env`
+3. Run `bundle install` to install all the gems
+4. Run `rake db:setup` to create and seed the database
+5. Run `rake db:test:load` to load the test database
+6. Run `bundle exec guard` to run the server
+7. Edit your hosts file (`subl /etc/hosts`) and add the following entry:
+
+ ```
+ 127.0.0.1 api.claycms-dev.io
+ ```
+
+9. Now access the app at 'http://api.claycms-dev.io:8080'.
+
+### Install RabbitMQ for Background Jobs:
+
+Install RabbitMQ with Homebrew:
+
+```bash
+brew install rabbitmq
+```
+
+Then, run it (after ensuring that /usr/local/sbin is in your $PATH):
+
+```bash
+rabbitmq-server
+```
+
+### Handling updates:
+
+1. Run `bundle install`
+2. Run `rake db:migrate`
+
+### Issues with Autoloading
+
+If you create new directories under the `app` folder, you might need to run `bin/spring stop` for it to be recognized as all the autoload_paths are computed and cached during initialization.
+
+### Watch for N+1 queries and unused eager loading
+
+In development environment, any N+1 queries or unused eager loading would be reported in `log/bullet.log`.
diff --git a/api/Rakefile b/api/Rakefile
new file mode 100755
index 0000000..393b243
--- /dev/null
+++ b/api/Rakefile
@@ -0,0 +1,7 @@
+# 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'
+require 'sneakers/tasks'
+
+Rails.application.load_tasks
diff --git a/api/app/assets/config/manifest.js b/api/app/assets/config/manifest.js
new file mode 100755
index 0000000..b16e53d
--- /dev/null
+++ b/api/app/assets/config/manifest.js
@@ -0,0 +1,3 @@
+//= link_tree ../images
+//= link_directory ../javascripts .js
+//= link_directory ../stylesheets .css
diff --git a/api/app/assets/images/.keep b/api/app/assets/images/.keep
new file mode 100755
index 0000000..e69de29
diff --git a/api/app/assets/javascripts/application.js b/api/app/assets/javascripts/application.js
new file mode 100755
index 0000000..43ba7e9
--- /dev/null
+++ b/api/app/assets/javascripts/application.js
@@ -0,0 +1,15 @@
+// This is a manifest file that'll be compiled into application.js, which will include all the files
+// listed below.
+//
+// Any JavaScript/Coffee file within this directory, lib/assets/javascripts, or any plugin's
+// vendor/assets/javascripts directory can be referenced here using a relative path.
+//
+// It's not advisable to add code directly here, but if you do, it'll appear at the bottom of the
+// compiled file. JavaScript code in this file should be added after the last require_* statement.
+//
+// Read Sprockets README (https://github.com/rails/sprockets#sprockets-directives) for details
+// about supported directives.
+//
+//= require rails-ujs
+//= require activestorage
+//= require_tree .
diff --git a/api/app/assets/javascripts/cable.js b/api/app/assets/javascripts/cable.js
new file mode 100755
index 0000000..739aa5f
--- /dev/null
+++ b/api/app/assets/javascripts/cable.js
@@ -0,0 +1,13 @@
+// Action Cable provides the framework to deal with WebSockets in Rails.
+// You can generate new channels where WebSocket features live using the `rails generate channel` command.
+//
+//= require action_cable
+//= require_self
+//= require_tree ./channels
+
+(function() {
+ this.App || (this.App = {});
+
+ App.cable = ActionCable.createConsumer();
+
+}).call(this);
diff --git a/api/app/assets/javascripts/channels/.keep b/api/app/assets/javascripts/channels/.keep
new file mode 100755
index 0000000..e69de29
diff --git a/api/app/assets/stylesheets/application.css b/api/app/assets/stylesheets/application.css
new file mode 100755
index 0000000..d05ea0f
--- /dev/null
+++ b/api/app/assets/stylesheets/application.css
@@ -0,0 +1,15 @@
+/*
+ * 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_tree .
+ *= require_self
+ */
diff --git a/api/app/channels/application_cable/channel.rb b/api/app/channels/application_cable/channel.rb
new file mode 100755
index 0000000..d672697
--- /dev/null
+++ b/api/app/channels/application_cable/channel.rb
@@ -0,0 +1,4 @@
+module ApplicationCable
+ class Channel < ActionCable::Channel::Base
+ end
+end
diff --git a/api/app/channels/application_cable/connection.rb b/api/app/channels/application_cable/connection.rb
new file mode 100755
index 0000000..0ff5442
--- /dev/null
+++ b/api/app/channels/application_cable/connection.rb
@@ -0,0 +1,4 @@
+module ApplicationCable
+ class Connection < ActionCable::Connection::Base
+ end
+end
diff --git a/api/app/controllers/application_controller.rb b/api/app/controllers/application_controller.rb
new file mode 100755
index 0000000..eeb9f28
--- /dev/null
+++ b/api/app/controllers/application_controller.rb
@@ -0,0 +1,3 @@
+class ApplicationController < ActionController::API
+ include ApiErrorHandler
+end
diff --git a/api/app/controllers/base_current_project_controller.rb b/api/app/controllers/base_current_project_controller.rb
new file mode 100644
index 0000000..e5ded82
--- /dev/null
+++ b/api/app/controllers/base_current_project_controller.rb
@@ -0,0 +1,27 @@
+class BaseCurrentProjectController < ApplicationController
+ attr_reader :current_project
+
+ before_action :set_current_project
+ before_action :set_error_context
+
+ private
+
+ def set_current_project
+ public_key = request.headers['X-Public-Key']
+ raise Exceptions::Unauthorized, 'Missing header: X-Public-Key' if public_key.blank?
+
+ key_pair = KeyPair.find_by(public_key: public_key)
+ raise Exceptions::Unauthorized, 'Incorrect value passed for header: X-Public-Key' if key_pair.blank?
+
+ @current_project = key_pair.project
+ end
+
+ def set_error_context
+ return if current_project.blank?
+
+ Raven.user_context(
+ id: "project-#{current_project.id}",
+ name: "Project: #{current_project.name}"
+ )
+ end
+end
diff --git a/api/app/controllers/base_current_user_controller.rb b/api/app/controllers/base_current_user_controller.rb
new file mode 100644
index 0000000..e4a43ec
--- /dev/null
+++ b/api/app/controllers/base_current_user_controller.rb
@@ -0,0 +1,29 @@
+class BaseCurrentUserController < ApplicationController
+ attr_reader :current_user
+
+ before_action :set_current_user
+ before_action :set_error_context
+
+ private
+
+ def set_current_user
+ return if request.headers['Authorization'].blank?
+
+ bearer_token = request.headers['Authorization'].split(' ').last
+
+ context = AuthenticateUser.call(bearer_token: bearer_token)
+ raise Exceptions::Unauthorized, context.error if context.failure?
+
+ @current_user = context.user
+ end
+
+ def set_error_context
+ return if current_user.blank?
+
+ Raven.user_context(
+ id: current_user.id,
+ email: current_user.email,
+ name: "#{current_user.first_name} #{current_user.last_name}".strip
+ )
+ end
+end
diff --git a/api/app/controllers/concerns/api_error_handler.rb b/api/app/controllers/concerns/api_error_handler.rb
new file mode 100755
index 0000000..8180e78
--- /dev/null
+++ b/api/app/controllers/concerns/api_error_handler.rb
@@ -0,0 +1,68 @@
+module ApiErrorHandler
+ extend ActiveSupport::Concern
+
+ included do
+ # IMPORTANT: Exceptions are searched bottom to top
+ # http://apidock.com/rails/ActionController/Rescue/ClassMethods/rescue_from
+
+ rescue_from StandardError do |exception|
+ raise exception if Rails.env.development? || Rails.env.test?
+
+ Raven.capture_exception(exception)
+
+ render_errors(Exceptions::InternalServerError.new)
+ end
+
+ rescue_from Exceptions::APIError do |exception|
+ render_errors(exception)
+ end
+
+ rescue_from Interactor::Failure do |exception|
+ render_errors(Exceptions::APIError.new(exception.context.error))
+ end
+
+ rescue_from Exceptions::FormErrors do |exception|
+ render_form_errors(exception.errors)
+ end
+
+ rescue_from GraphQL::Guard::NotAuthorizedError do |exception|
+ render_errors(Exceptions::Forbidden.new)
+ end
+
+ rescue_from ActiveRecord::RecordNotFound do |exception|
+ render_errors(Exceptions::NotFound.new)
+ end
+
+ rescue_from ActiveRecord::RecordInvalid do |exception|
+ render_form_errors(exception.record.errors.to_hash)
+ end
+
+ rescue_from ActiveRecord::RecordNotDestroyed do |exception|
+ errors = exception.record.errors.to_hash
+ if errors.count > 1
+ render_form_errors(errors)
+ else
+ message = errors.values.first&.join(', ')
+ render_errors(Exceptions::APIError.new(message))
+ end
+ end
+ end
+
+ private
+
+ def render_errors(exception)
+ render json: { errors: [{ message: exception.message, type: exception.type }] }, status: exception.status
+ end
+
+ def render_form_errors(form_errors)
+ record_errors = form_errors.deep_transform_keys { |key| key.to_s.camelize(:lower) }
+ errors = record_errors.map do |attribute, attribute_errors|
+ {
+ message: attribute_errors.join(', '),
+ type: Exceptions::APIError::TYPE,
+ path: attribute.split('.')
+ }
+ end
+ render json: { errors: errors }, status: Exceptions::APIError::STATUS
+ end
+end
diff --git a/api/app/controllers/graphql_controller.rb b/api/app/controllers/graphql_controller.rb
new file mode 100755
index 0000000..9dd6b87
--- /dev/null
+++ b/api/app/controllers/graphql_controller.rb
@@ -0,0 +1,62 @@
+class GraphqlController < BaseCurrentUserController
+ before_action :set_context
+ before_action :set_operations
+
+ def execute
+ if @operations.is_a? Array
+ queries = @operations.map(&method(:build_query))
+ result = ClayApiSchema.multiplex(queries)
+ else
+ result = ClayApiSchema.execute(nil, build_query(@operations))
+ end
+ render json: result
+ end
+
+ private
+
+ def set_context
+ @context = {
+ current_user: current_user,
+ current_request: request
+ }
+ end
+
+ def set_operations
+ if request.content_type != 'multipart/form-data'
+ @operations = params[:_json] || params
+ return
+ end
+
+ @operations = ApolloUploadServer::GraphQLDataBuilder.new.call(params)
+ @operations.symbolize_keys!
+ end
+
+ def build_query(document)
+ {
+ query: document[:query],
+ operation_name: document[:operationName],
+ variables: ensure_hash(document[:variables]),
+ context: @context
+ }
+ end
+
+ # Handle form data, JSON body, or a blank value
+ def ensure_hash(ambiguous_param)
+ case ambiguous_param
+ when String
+ if ambiguous_param.present?
+ ensure_hash(JSON.parse(ambiguous_param))
+ else
+ {}
+ end
+ when Hash
+ ambiguous_param
+ when ActionController::Parameters
+ ambiguous_param.permit!.to_h
+ when nil
+ {}
+ else
+ raise ArgumentError, "Unexpected parameter: #{ambiguous_param}"
+ end
+ end
+end
diff --git a/api/app/controllers/v1/content_controller.rb b/api/app/controllers/v1/content_controller.rb
new file mode 100644
index 0000000..783febf
--- /dev/null
+++ b/api/app/controllers/v1/content_controller.rb
@@ -0,0 +1,42 @@
+module V1
+ class ContentController < BaseCurrentProjectController
+ def find
+ entity = current_project.entities.find_by!(name: params[:entity_name])
+
+ record_scope = entity.records
+
+ filters = params[:filters].presence || {}
+
+ filters.each do |field_name, property_value|
+ record_scope = record_scope.joins(properties: :field)
+ record_scope = record_scope.where(fields: { name: field_name })
+ record_scope = record_scope.where(properties: { value: property_value })
+ end
+
+ # sort = params[:sort] || {}
+ # sort.each do |field_name, sort_direction|
+ # record_scope = record_scope.order(field_name, sort_direction)
+ # end
+
+ # if params[:paging].present?
+ # record_scope = record_scope.limit(params[:paging][:limit]) if params[:paging][:limit].present?
+ # record_scope = record_scope.offset(params[:paging][:offset]) if params[:paging][:offset].present?
+ # end
+
+ result_type = params[:result_type].try(:to_sym) || (entity.singleton? ? :one : :many)
+
+ record_mapper = RecordMapper.new
+
+ if result_type == :one
+ record = record_scope.first!
+ data = record_mapper.to_json(record, params[:key_type])
+ else
+ raise Exceptions::APIError, "Cannot find `many` of singleton entity, `#{entity.name}`." if entity.singleton?
+
+ data = record_scope.map { |r| record_mapper.to_json(r, params[:key_type]) }
+ end
+
+ render json: { data: data }
+ end
+ end
+end
diff --git a/api/app/controllers/v1/entities_controller.rb b/api/app/controllers/v1/entities_controller.rb
new file mode 100644
index 0000000..58dec9e
--- /dev/null
+++ b/api/app/controllers/v1/entities_controller.rb
@@ -0,0 +1,15 @@
+module V1
+ class EntitiesController < BaseCurrentProjectController
+ def index
+ render json: { entities: current_project.entities }
+ end
+
+ def show
+ entity = current_project.entities.find(params[:id])
+ json = entity.attributes
+ json[:fields] = entity.fields.to_a
+
+ render json: { entity: json }
+ end
+ end
+end
diff --git a/api/app/graphql/clay_api_schema.rb b/api/app/graphql/clay_api_schema.rb
new file mode 100755
index 0000000..aa06c9d
--- /dev/null
+++ b/api/app/graphql/clay_api_schema.rb
@@ -0,0 +1,6 @@
+class ClayApiSchema < GraphQL::Schema
+ query Roots::QueryType
+ mutation Roots::MutationType
+
+ use GraphQL::Guard.new(policy_object: GraphqlPolicy)
+end
diff --git a/api/app/graphql/functions/application_function.rb b/api/app/graphql/functions/application_function.rb
new file mode 100755
index 0000000..81b0403
--- /dev/null
+++ b/api/app/graphql/functions/application_function.rb
@@ -0,0 +1,12 @@
+class ApplicationFunction < GraphQL::Function
+ include GraphQL::Sugar::Function
+
+ def policy(model_or_record)
+ @policy = Pundit.policy!(context[:current_user], model_or_record)
+ end
+
+ def authorize!(model_or_record, permission)
+ resolved_policy = policy(model_or_record)
+ raise Exceptions::Forbidden if !resolved_policy.respond_to?(permission) || !resolved_policy.send(permission)
+ end
+end
diff --git a/api/app/graphql/graphql_policy.rb b/api/app/graphql/graphql_policy.rb
new file mode 100755
index 0000000..020fd9a
--- /dev/null
+++ b/api/app/graphql/graphql_policy.rb
@@ -0,0 +1,38 @@
+class GraphqlPolicy
+ def self.guard(type, field)
+ rules.dig(type.name, field)
+ end
+
+ def self.authorize_user
+ ->(obj, args, ctx) { ctx[:current_user].present? }
+ end
+
+ def self.authorize_any
+ ->(obj, args, ctx) { true }
+ end
+
+ def self.authorize_none
+ ->(obj, args, ctx) { false }
+ end
+
+ def self.current_user_matches?(attribute)
+ ->(obj, args, ctx) { authorize_user.call(obj, args, ctx) && obj.object.send(attribute) == ctx[:current_user].id }
+ end
+
+ def self.rules
+ @rules ||= {
+ 'Query' => {
+ '*': authorize_user
+ },
+ 'Mutation' => {
+ '*': authorize_user,
+ ssoLogin: authorize_any,
+ ssoCallback: authorize_any
+ },
+
+ 'User' => {
+ sessions: current_user_matches?(:id)
+ }
+ }
+ end
+end
diff --git a/api/app/graphql/mutations/.keep b/api/app/graphql/mutations/.keep
new file mode 100755
index 0000000..e69de29
diff --git a/api/app/graphql/mutators/accept_transfer_request_mutator.rb b/api/app/graphql/mutators/accept_transfer_request_mutator.rb
new file mode 100644
index 0000000..a8c5d5d
--- /dev/null
+++ b/api/app/graphql/mutators/accept_transfer_request_mutator.rb
@@ -0,0 +1,22 @@
+class AcceptTransferRequestMutator < ApplicationMutator
+ AcceptTransferRequestInputType = GraphQL::InputObjectType.define do
+ name 'AcceptTransferRequestInput'
+
+ parameter :token, !types.String
+ end
+
+ parameter :input, !AcceptTransferRequestInputType
+
+ type Types::ResponseType.to_non_null_type
+
+ def mutate
+ team = Team.find_with_transfer_token(permitted_params[:token])
+ raise Exceptions::APIError, 'Your transfer link is invalid.' if team.blank?
+
+ authorize! team, :accept_transfer_request?
+
+ AcceptTransferRequest.call!(team: team)
+
+ { success: true }
+ end
+end
diff --git a/api/app/graphql/mutators/application_mutator.rb b/api/app/graphql/mutators/application_mutator.rb
new file mode 100755
index 0000000..e230ee1
--- /dev/null
+++ b/api/app/graphql/mutators/application_mutator.rb
@@ -0,0 +1,9 @@
+class ApplicationMutator < ApplicationFunction
+ include GraphQL::Sugar::Mutator
+
+ protected
+
+ def permitted_params
+ params[:input]
+ end
+end
diff --git a/api/app/graphql/mutators/cancel_transfer_request_mutator.rb b/api/app/graphql/mutators/cancel_transfer_request_mutator.rb
new file mode 100644
index 0000000..8cd015e
--- /dev/null
+++ b/api/app/graphql/mutators/cancel_transfer_request_mutator.rb
@@ -0,0 +1,14 @@
+class CancelTransferRequestMutator < ApplicationMutator
+ parameter :id, !types.ID
+
+ type Types::TeamType.to_non_null_type
+
+ def mutate
+ team = Team.find(params[:id])
+ authorize! team, :cancel_transfer_request?
+
+ result = CancelTransferRequest.call!(team: team)
+
+ result.team
+ end
+end
diff --git a/api/app/graphql/mutators/clone_record_mutator.rb b/api/app/graphql/mutators/clone_record_mutator.rb
new file mode 100644
index 0000000..19330b9
--- /dev/null
+++ b/api/app/graphql/mutators/clone_record_mutator.rb
@@ -0,0 +1,13 @@
+class CloneRecordMutator < ApplicationMutator
+ parameter :id, !types.ID
+
+ type Types::RecordType.to_non_null_type
+
+ def mutate
+ record = Record.find(params[:id])
+ authorize! record.entity, :clone_record?
+
+ context = CloneRecord.call(record: record)
+ context.record
+ end
+end
diff --git a/api/app/graphql/mutators/create_asset_mutator.rb b/api/app/graphql/mutators/create_asset_mutator.rb
new file mode 100644
index 0000000..8afc0de
--- /dev/null
+++ b/api/app/graphql/mutators/create_asset_mutator.rb
@@ -0,0 +1,21 @@
+class CreateAssetMutator < ApplicationMutator
+ CreateAssetInputType = GraphQL::InputObjectType.define do
+ name 'CreateAssetInput'
+
+ parameter :projectId, !types.ID
+ parameter :name, !types.String
+ parameter :file, Scalars::FileType.to_non_null_type
+ end
+
+ parameter :input, !CreateAssetInputType
+
+ type Types::AssetType.to_non_null_type
+
+ def mutate
+ project = Project.find(permitted_params[:project_id])
+ authorize! project, :create_asset?
+
+ result = CreateAsset.call!(params: permitted_params, project: project)
+ result.asset
+ end
+end
diff --git a/api/app/graphql/mutators/create_entity_mutator.rb b/api/app/graphql/mutators/create_entity_mutator.rb
new file mode 100644
index 0000000..72576a4
--- /dev/null
+++ b/api/app/graphql/mutators/create_entity_mutator.rb
@@ -0,0 +1,28 @@
+class CreateEntityMutator < ApplicationMutator
+ CreateEntityInputType = GraphQL::InputObjectType.define do
+ name 'CreateEntityInput'
+
+ parameter :projectId, !types.ID
+ parameter :parentId, types.ID
+ parameter :name, !types.String
+ parameter :label, !types.String
+ parameter :singleton, types.Boolean
+ end
+
+ parameter :input, !CreateEntityInputType
+
+ type Types::EntityType.to_non_null_type
+
+ def mutate
+ project = Project.find(permitted_params[:project_id])
+ authorize! project, :create_entity?
+
+ if permitted_params[:parent_id].present?
+ entity = Entity.find(permitted_params[:parent_id])
+ authorize! entity, :update?
+ end
+
+ result = CreateEntity.call!(params: permitted_params, project: project)
+ result.entity
+ end
+end
diff --git a/api/app/graphql/mutators/create_field_mutator.rb b/api/app/graphql/mutators/create_field_mutator.rb
new file mode 100644
index 0000000..29813f7
--- /dev/null
+++ b/api/app/graphql/mutators/create_field_mutator.rb
@@ -0,0 +1,30 @@
+class CreateFieldMutator < ApplicationMutator
+ CreateFieldInputType = GraphQL::InputObjectType.define do
+ name 'CreateFieldInput'
+
+ parameter :entityId, !types.ID
+ parameter :name, !types.String
+ parameter :label, !types.String
+ parameter :dataType, !types.String
+ parameter :elementType, types.String
+ parameter :referencedEntityId, types.ID
+ parameter :defaultValue, types.String
+ parameter :hint, types.String
+ parameter :position, types.Int
+ parameter :children, types[Scalars::HashType]
+ parameter :validations, Scalars::HashType
+ parameter :settings, Scalars::HashType
+ end
+
+ parameter :input, !CreateFieldInputType
+
+ type Types::FieldType.to_list_type.to_non_null_type
+
+ def mutate
+ entity = Entity.find(permitted_params[:entity_id])
+ authorize! entity, :create_field?
+
+ result = CreateField.call!(params: permitted_params, entity: entity)
+ result.field.self_and_descendants
+ end
+end
diff --git a/api/app/graphql/mutators/create_key_pair_mutator.rb b/api/app/graphql/mutators/create_key_pair_mutator.rb
new file mode 100644
index 0000000..df8a9f1
--- /dev/null
+++ b/api/app/graphql/mutators/create_key_pair_mutator.rb
@@ -0,0 +1,19 @@
+class CreateKeyPairMutator < ApplicationMutator
+ CreateKeyPairInputType = GraphQL::InputObjectType.define do
+ name 'CreateKeyPairInput'
+
+ parameter :projectId, !types.ID
+ end
+
+ parameter :input, !CreateKeyPairInputType
+
+ type Types::KeyPairType.to_non_null_type
+
+ def mutate
+ project = Project.find(permitted_params[:project_id])
+ authorize! project, :create_key_pair?
+
+ result = CreateKeyPair.call!(project: project)
+ result.key_pair
+ end
+end
diff --git a/api/app/graphql/mutators/create_project_mutator.rb b/api/app/graphql/mutators/create_project_mutator.rb
new file mode 100644
index 0000000..c9aaee4
--- /dev/null
+++ b/api/app/graphql/mutators/create_project_mutator.rb
@@ -0,0 +1,20 @@
+class CreateProjectMutator < ApplicationMutator
+ CreateProjectInputType = GraphQL::InputObjectType.define do
+ name 'CreateProjectInput'
+
+ parameter :teamId, !types.ID
+ parameter :name, !types.String
+ end
+
+ parameter :input, !CreateProjectInputType
+
+ type Types::ProjectType.to_non_null_type
+
+ def mutate
+ team = Team.find(permitted_params[:team_id])
+ authorize! team, :create_project?
+
+ result = CreateProject.call!(params: permitted_params, team: team)
+ result.project
+ end
+end
diff --git a/api/app/graphql/mutators/create_record_mutator.rb b/api/app/graphql/mutators/create_record_mutator.rb
new file mode 100644
index 0000000..3154d35
--- /dev/null
+++ b/api/app/graphql/mutators/create_record_mutator.rb
@@ -0,0 +1,20 @@
+class CreateRecordMutator < ApplicationMutator
+ CreateRecordInputType = GraphQL::InputObjectType.define do
+ name 'CreateRecordInput'
+
+ parameter :entityId, !types.ID
+ parameter :traits, Scalars::HashType
+ end
+
+ parameter :input, !CreateRecordInputType
+
+ type Types::RecordType.to_non_null_type
+
+ def mutate
+ entity = Entity.find(permitted_params[:entity_id])
+ authorize! entity, :create_record?
+
+ result = CreateRecord.call!(params: permitted_params, entity: entity)
+ result.record
+ end
+end
diff --git a/api/app/graphql/mutators/create_resource_mutator.rb b/api/app/graphql/mutators/create_resource_mutator.rb
new file mode 100644
index 0000000..88707bf
--- /dev/null
+++ b/api/app/graphql/mutators/create_resource_mutator.rb
@@ -0,0 +1,21 @@
+class CreateResourceMutator < ApplicationMutator
+ CreateResourceInputType = GraphQL::InputObjectType.define do
+ name 'CreateResourceInput'
+
+ parameter :projectId, !types.ID
+ parameter :name, !types.String
+ parameter :file, Scalars::FileType.to_non_null_type
+ end
+
+ parameter :input, !CreateResourceInputType
+
+ type Types::ResourceType.to_non_null_type
+
+ def mutate
+ project = Project.find(permitted_params[:project_id])
+ authorize! project, :create_resource?
+
+ result = CreateResource.call!(params: permitted_params, project: project)
+ result.resource
+ end
+end
diff --git a/api/app/graphql/mutators/create_team_membership_mutator.rb b/api/app/graphql/mutators/create_team_membership_mutator.rb
new file mode 100644
index 0000000..e6d323b
--- /dev/null
+++ b/api/app/graphql/mutators/create_team_membership_mutator.rb
@@ -0,0 +1,21 @@
+class CreateTeamMembershipMutator < ApplicationMutator
+ CreateTeamMembershipInputType = GraphQL::InputObjectType.define do
+ name 'CreateTeamMembershipInput'
+
+ parameter :teamId, !types.ID
+ parameter :email, !types.String
+ parameter :role, !types.String
+ end
+
+ parameter :input, !CreateTeamMembershipInputType
+
+ type Types::TeamMembershipType.to_non_null_type
+
+ def mutate
+ team = Team.find(permitted_params[:team_id])
+ authorize! team, :create_team_membership?
+
+ result = CreateTeamMembership.call!(params: permitted_params, team: team)
+ result.team_membership
+ end
+end
diff --git a/api/app/graphql/mutators/create_team_mutator.rb b/api/app/graphql/mutators/create_team_mutator.rb
new file mode 100644
index 0000000..fa50027
--- /dev/null
+++ b/api/app/graphql/mutators/create_team_mutator.rb
@@ -0,0 +1,16 @@
+class CreateTeamMutator < ApplicationMutator
+ CreateTeamInputType = GraphQL::InputObjectType.define do
+ name 'CreateTeamInput'
+
+ parameter :name, !types.String
+ end
+
+ parameter :input, !CreateTeamInputType
+
+ type Types::TeamType.to_non_null_type
+
+ def mutate
+ result = CreateTeam.call!(team_params: permitted_params, current_user: context[:current_user])
+ result.team
+ end
+end
diff --git a/api/app/graphql/mutators/create_transfer_request_mutator.rb b/api/app/graphql/mutators/create_transfer_request_mutator.rb
new file mode 100644
index 0000000..742d0f2
--- /dev/null
+++ b/api/app/graphql/mutators/create_transfer_request_mutator.rb
@@ -0,0 +1,21 @@
+class CreateTransferRequestMutator < ApplicationMutator
+ CreateTransferRequestInputType = GraphQL::InputObjectType.define do
+ name 'CreateTransferRequestInput'
+
+ parameter :userId, !types.ID
+ end
+
+ parameter :id, !types.ID
+ parameter :input, !CreateTransferRequestInputType
+
+ type Types::TeamType.to_non_null_type
+
+ def mutate
+ team = Team.find(params[:id])
+ authorize! team, :create_transfer_request?
+
+ CreateTransferRequest.call!(params: permitted_params, team: team)
+
+ team
+ end
+end
diff --git a/api/app/graphql/mutators/destroy_asset_mutator.rb b/api/app/graphql/mutators/destroy_asset_mutator.rb
new file mode 100644
index 0000000..4be6b0b
--- /dev/null
+++ b/api/app/graphql/mutators/destroy_asset_mutator.rb
@@ -0,0 +1,13 @@
+class DestroyAssetMutator < ApplicationMutator
+ parameter :id, !types.ID
+
+ type Types::AssetType.to_non_null_type
+
+ def mutate
+ asset = Asset.find(params[:id])
+ authorize! asset, :destroy?
+
+ result = DestroyAsset.call!(asset: asset)
+ result.asset
+ end
+end
diff --git a/api/app/graphql/mutators/destroy_entity_mutator.rb b/api/app/graphql/mutators/destroy_entity_mutator.rb
new file mode 100644
index 0000000..a1f95aa
--- /dev/null
+++ b/api/app/graphql/mutators/destroy_entity_mutator.rb
@@ -0,0 +1,13 @@
+class DestroyEntityMutator < ApplicationMutator
+ parameter :id, !types.ID
+
+ type Types::EntityType.to_non_null_type
+
+ def mutate
+ entity = Entity.find(params[:id])
+ authorize! entity, :destroy?
+
+ result = DestroyEntity.call!(entity: entity)
+ result.entity
+ end
+end
diff --git a/api/app/graphql/mutators/destroy_field_mutator.rb b/api/app/graphql/mutators/destroy_field_mutator.rb
new file mode 100644
index 0000000..2f9bb0d
--- /dev/null
+++ b/api/app/graphql/mutators/destroy_field_mutator.rb
@@ -0,0 +1,13 @@
+class DestroyFieldMutator < ApplicationMutator
+ parameter :id, !types.ID
+
+ type Types::FieldType.to_non_null_type
+
+ def mutate
+ field = Field.find(params[:id])
+ authorize! field, :destroy?
+
+ result = DestroyField.call!(field: field)
+ result.field
+ end
+end
diff --git a/api/app/graphql/mutators/destroy_record_mutator.rb b/api/app/graphql/mutators/destroy_record_mutator.rb
new file mode 100644
index 0000000..6f5f20f
--- /dev/null
+++ b/api/app/graphql/mutators/destroy_record_mutator.rb
@@ -0,0 +1,13 @@
+class DestroyRecordMutator < ApplicationMutator
+ parameter :id, !types.ID
+
+ type Types::RecordType.to_non_null_type
+
+ def mutate
+ record = Record.find(params[:id])
+ authorize! record, :destroy?
+
+ result = DestroyRecord.call!(record: record)
+ result.record
+ end
+end
diff --git a/api/app/graphql/mutators/destroy_resource_mutator.rb b/api/app/graphql/mutators/destroy_resource_mutator.rb
new file mode 100644
index 0000000..706e3b9
--- /dev/null
+++ b/api/app/graphql/mutators/destroy_resource_mutator.rb
@@ -0,0 +1,13 @@
+class DestroyResourceMutator < ApplicationMutator
+ parameter :id, !types.ID
+
+ type Types::ResourceType.to_non_null_type
+
+ def mutate
+ resource = Resource.find(params[:id])
+ authorize! resource, :destroy?
+
+ result = DestroyResource.call!(resource: resource)
+ result.resource
+ end
+end
diff --git a/api/app/graphql/mutators/destroy_team_membership_mutator.rb b/api/app/graphql/mutators/destroy_team_membership_mutator.rb
new file mode 100644
index 0000000..07c99b2
--- /dev/null
+++ b/api/app/graphql/mutators/destroy_team_membership_mutator.rb
@@ -0,0 +1,13 @@
+class DestroyTeamMembershipMutator < ApplicationMutator
+ parameter :id, !types.ID
+
+ type Types::TeamMembershipType.to_non_null_type
+
+ def mutate
+ team_membership = TeamMembership.find(params[:id])
+ authorize! team_membership, :destroy?
+
+ result = DestroyTeamMembership.call!(team_membership: team_membership)
+ result.team_membership
+ end
+end
diff --git a/api/app/graphql/mutators/export_project_mutator.rb b/api/app/graphql/mutators/export_project_mutator.rb
new file mode 100644
index 0000000..056f969
--- /dev/null
+++ b/api/app/graphql/mutators/export_project_mutator.rb
@@ -0,0 +1,13 @@
+class ExportProjectMutator < ApplicationMutator
+ parameter :id, !types.ID
+
+ type Types::ExportType.to_non_null_type
+
+ def mutate
+ project = Project.find(params[:id])
+ authorize! project, :export_project?
+
+ result = ExportProject.call(project: project)
+ result.export
+ end
+end
diff --git a/api/app/graphql/mutators/import_project_mutator.rb b/api/app/graphql/mutators/import_project_mutator.rb
new file mode 100644
index 0000000..fa2684e
--- /dev/null
+++ b/api/app/graphql/mutators/import_project_mutator.rb
@@ -0,0 +1,20 @@
+class ImportProjectMutator < ApplicationMutator
+ ImportProjectInputType = GraphQL::InputObjectType.define do
+ name 'ImportProjectInput'
+
+ parameter :url, !types.String
+ end
+
+ parameter :id, !types.ID
+ parameter :input, !ImportProjectInputType
+
+ type Types::RestoreType.to_non_null_type
+
+ def mutate
+ project = Project.find(params[:id])
+ authorize! project, :import_project?
+
+ result = ImportProject.call(project: project, params: permitted_params)
+ result.restore
+ end
+end
diff --git a/api/app/graphql/mutators/reject_transfer_request_mutator.rb b/api/app/graphql/mutators/reject_transfer_request_mutator.rb
new file mode 100644
index 0000000..517fe44
--- /dev/null
+++ b/api/app/graphql/mutators/reject_transfer_request_mutator.rb
@@ -0,0 +1,22 @@
+class RejectTransferRequestMutator < ApplicationMutator
+ RejectTransferRequestInputType = GraphQL::InputObjectType.define do
+ name 'RejectTransferRequestInput'
+
+ parameter :token, !types.String
+ end
+
+ parameter :input, !RejectTransferRequestInputType
+
+ type Types::ResponseType.to_non_null_type
+
+ def mutate
+ team = Team.find_with_transfer_token(permitted_params[:token])
+ raise Exceptions::APIError, 'Your transfer link is invalid.' if team.blank?
+
+ authorize! team, :reject_transfer_request?
+
+ RejectTransferRequest.call!(team: team)
+
+ { success: true }
+ end
+end
diff --git a/api/app/graphql/mutators/revoke_key_pair_mutator.rb b/api/app/graphql/mutators/revoke_key_pair_mutator.rb
new file mode 100644
index 0000000..e703680
--- /dev/null
+++ b/api/app/graphql/mutators/revoke_key_pair_mutator.rb
@@ -0,0 +1,13 @@
+class RevokeKeyPairMutator < ApplicationMutator
+ parameter :id, !types.ID
+
+ type Types::KeyPairType.to_non_null_type
+
+ def mutate
+ key_pair = KeyPair.find(params[:id])
+ authorize! key_pair, :revoke?
+
+ result = RevokeKeyPair.call!(key_pair: key_pair)
+ result.key_pair
+ end
+end
diff --git a/api/app/graphql/mutators/sort_fields_mutator.rb b/api/app/graphql/mutators/sort_fields_mutator.rb
new file mode 100644
index 0000000..3eaebe3
--- /dev/null
+++ b/api/app/graphql/mutators/sort_fields_mutator.rb
@@ -0,0 +1,31 @@
+class SortFieldsMutator < ApplicationMutator
+ SortFieldInputType = GraphQL::InputObjectType.define do
+ name 'SortFieldInput'
+
+ parameter :id, !types.ID
+ parameter :position, !types.Int
+ end
+
+ SortFieldsInputType = GraphQL::InputObjectType.define do
+ name 'SortFieldsInput'
+
+ parameter :fields, !types[SortFieldInputType]
+ end
+
+ parameter :input, !SortFieldsInputType
+
+ type Types::FieldType.to_list_type
+
+ def mutate
+ fields = permitted_params[:fields].map do |field_params|
+ field = Field.find(field_params[:id])
+ authorize! field, :update?
+
+ field.position = field_params[:position]
+ field
+ end
+
+ result = SortFields.call!(fields: fields)
+ result.fields
+ end
+end
diff --git a/api/app/graphql/mutators/sso_callback_mutator.rb b/api/app/graphql/mutators/sso_callback_mutator.rb
new file mode 100644
index 0000000..4a63763
--- /dev/null
+++ b/api/app/graphql/mutators/sso_callback_mutator.rb
@@ -0,0 +1,20 @@
+class SsoCallbackMutator < ApplicationMutator
+ SsoCallbackInputType = GraphQL::InputObjectType.define do
+ name 'SsoCallbackInput'
+
+ parameter :sso, !types.String
+ parameter :sig, !types.String
+ end
+
+ parameter :input, !SsoCallbackInputType
+
+ type Scalars::HashType
+
+ def mutate
+ context = SsoCallback.call(
+ params: permitted_params,
+ user_agent: @context[:current_request].user_agent
+ )
+ context.sso_payload
+ end
+end
diff --git a/api/app/graphql/mutators/sso_login_mutator.rb b/api/app/graphql/mutators/sso_login_mutator.rb
new file mode 100644
index 0000000..570f911
--- /dev/null
+++ b/api/app/graphql/mutators/sso_login_mutator.rb
@@ -0,0 +1,8 @@
+class SsoLoginMutator < ApplicationMutator
+ type Scalars::HashType
+
+ def mutate
+ context = SsoLogin.call
+ context.sso_payload
+ end
+end
diff --git a/api/app/graphql/mutators/sso_logout_mutator.rb b/api/app/graphql/mutators/sso_logout_mutator.rb
new file mode 100644
index 0000000..df56342
--- /dev/null
+++ b/api/app/graphql/mutators/sso_logout_mutator.rb
@@ -0,0 +1,15 @@
+class SsoLogoutMutator < ApplicationMutator
+ type Types::ResponseType.to_non_null_type
+
+ def mutate
+ bearer_token = @context.current_request.headers['Authorization'].split(' ').last
+ decoded_token = JsonWebToken.decode(bearer_token)
+
+ context = SsoLogout.call!(
+ current_user: @context[:current_user],
+ decoded_token: decoded_token
+ )
+
+ { success: context.response }
+ end
+end
diff --git a/api/app/graphql/mutators/update_asset_mutator.rb b/api/app/graphql/mutators/update_asset_mutator.rb
new file mode 100644
index 0000000..3a097da
--- /dev/null
+++ b/api/app/graphql/mutators/update_asset_mutator.rb
@@ -0,0 +1,20 @@
+class UpdateAssetMutator < ApplicationMutator
+ UpdateAssetInputType = GraphQL::InputObjectType.define do
+ name 'UpdateAssetInput'
+
+ parameter :name, !types.String
+ end
+
+ parameter :id, !types.ID
+ parameter :input, !UpdateAssetInputType
+
+ type Types::AssetType.to_non_null_type
+
+ def mutate
+ asset = Asset.find(params[:id])
+ authorize! asset, :update?
+
+ result = UpdateAsset.call!(params: permitted_params, asset: asset)
+ result.asset
+ end
+end
diff --git a/api/app/graphql/mutators/update_entity_mutator.rb b/api/app/graphql/mutators/update_entity_mutator.rb
new file mode 100644
index 0000000..37445f2
--- /dev/null
+++ b/api/app/graphql/mutators/update_entity_mutator.rb
@@ -0,0 +1,28 @@
+class UpdateEntityMutator < ApplicationMutator
+ UpdateEntityInputType = GraphQL::InputObjectType.define do
+ name 'UpdateEntityInput'
+
+ parameter :parentId, types.ID
+ parameter :name, !types.String
+ parameter :label, !types.String
+ parameter :singleton, types.Boolean
+ end
+
+ parameter :id, !types.ID
+ parameter :input, !UpdateEntityInputType
+
+ type Types::EntityType.to_non_null_type
+
+ def mutate
+ entity = Entity.find(params[:id])
+ authorize! entity, :update?
+
+ if permitted_params[:parent_id].present?
+ parent = Entity.find(permitted_params[:parent_id])
+ authorize! parent, :update?
+ end
+
+ result = UpdateEntity.call!(params: permitted_params, entity: entity)
+ result.entity
+ end
+end
diff --git a/api/app/graphql/mutators/update_field_mutator.rb b/api/app/graphql/mutators/update_field_mutator.rb
new file mode 100644
index 0000000..328ba03
--- /dev/null
+++ b/api/app/graphql/mutators/update_field_mutator.rb
@@ -0,0 +1,30 @@
+class UpdateFieldMutator < ApplicationMutator
+ UpdateFieldInputType = GraphQL::InputObjectType.define do
+ name 'UpdateFieldInput'
+
+ parameter :name, !types.String
+ parameter :label, !types.String
+ parameter :dataType, !types.String
+ parameter :elementType, types.String
+ parameter :referencedEntityId, types.ID
+ parameter :defaultValue, types.String
+ parameter :hint, types.String
+ parameter :position, types.Int
+ parameter :children, types[Scalars::HashType]
+ parameter :validations, Scalars::HashType
+ parameter :settings, Scalars::HashType
+ end
+
+ parameter :id, !types.ID
+ parameter :input, !UpdateFieldInputType
+
+ type Types::FieldType.to_list_type.to_non_null_type
+
+ def mutate
+ field = Field.find(params[:id])
+ authorize! field, :update?
+
+ result = UpdateField.call!(params: permitted_params, field: field)
+ result.field.self_and_descendants
+ end
+end
diff --git a/api/app/graphql/mutators/update_profile_mutator.rb b/api/app/graphql/mutators/update_profile_mutator.rb
new file mode 100644
index 0000000..b7758bf
--- /dev/null
+++ b/api/app/graphql/mutators/update_profile_mutator.rb
@@ -0,0 +1,19 @@
+class UpdateProfileMutator < ApplicationMutator
+ UpdateProfileInputType = GraphQL::InputObjectType.define do
+ name 'UpdateProfileInput'
+
+ parameter :firstName, types.String
+ parameter :lastName, types.String
+ parameter :profilePicture, Scalars::FileType
+ end
+
+ parameter :input, !UpdateProfileInputType
+
+ type Types::UserType.to_non_null_type
+
+ def mutate
+ result = UpdateUser.call!(params: permitted_params, user: context[:current_user])
+
+ result.user
+ end
+end
diff --git a/api/app/graphql/mutators/update_project_mutator.rb b/api/app/graphql/mutators/update_project_mutator.rb
new file mode 100644
index 0000000..72d0f72
--- /dev/null
+++ b/api/app/graphql/mutators/update_project_mutator.rb
@@ -0,0 +1,20 @@
+class UpdateProjectMutator < ApplicationMutator
+ UpdateProjectInputType = GraphQL::InputObjectType.define do
+ name 'UpdateProjectInput'
+
+ parameter :name, !types.String
+ end
+
+ parameter :id, !types.ID
+ parameter :input, !UpdateProjectInputType
+
+ type Types::ProjectType.to_non_null_type
+
+ def mutate
+ project = Project.find(params[:id])
+ authorize! project, :update?
+
+ result = UpdateProject.call!(params: permitted_params, project: project)
+ result.project
+ end
+end
diff --git a/api/app/graphql/mutators/update_record_mutator.rb b/api/app/graphql/mutators/update_record_mutator.rb
new file mode 100644
index 0000000..62d1417
--- /dev/null
+++ b/api/app/graphql/mutators/update_record_mutator.rb
@@ -0,0 +1,20 @@
+class UpdateRecordMutator < ApplicationMutator
+ UpdateRecordInputType = GraphQL::InputObjectType.define do
+ name 'UpdateRecordInput'
+
+ parameter :traits, Scalars::HashType
+ end
+
+ parameter :id, !types.ID
+ parameter :input, !UpdateRecordInputType
+
+ type Types::RecordType.to_non_null_type
+
+ def mutate
+ record = Record.find(params[:id])
+ authorize! record, :update?
+
+ result = UpdateRecord.call!(params: permitted_params, record: record)
+ result.record
+ end
+end
diff --git a/api/app/graphql/mutators/update_resource_mutator.rb b/api/app/graphql/mutators/update_resource_mutator.rb
new file mode 100644
index 0000000..6e2029b
--- /dev/null
+++ b/api/app/graphql/mutators/update_resource_mutator.rb
@@ -0,0 +1,20 @@
+class UpdateResourceMutator < ApplicationMutator
+ UpdateResourceInputType = GraphQL::InputObjectType.define do
+ name 'UpdateResourceInput'
+
+ parameter :file, Scalars::FileType.to_non_null_type
+ end
+
+ parameter :id, !types.ID
+ parameter :input, !UpdateResourceInputType
+
+ type Types::ResourceType.to_non_null_type
+
+ def mutate
+ resource = Resource.find(params[:id])
+ authorize! resource, :update?
+
+ result = UpdateResource.call!(params: permitted_params, resource: resource)
+ result.resource
+ end
+end
diff --git a/api/app/graphql/mutators/update_team_membership_mutator.rb b/api/app/graphql/mutators/update_team_membership_mutator.rb
new file mode 100644
index 0000000..0fe1c9f
--- /dev/null
+++ b/api/app/graphql/mutators/update_team_membership_mutator.rb
@@ -0,0 +1,20 @@
+class UpdateTeamMembershipMutator < ApplicationMutator
+ UpdateTeamMembershipInputType = GraphQL::InputObjectType.define do
+ name 'UpdateTeamMembershipInput'
+
+ parameter :role, !types.String
+ end
+
+ parameter :id, !types.ID
+ parameter :input, !UpdateTeamMembershipInputType
+
+ type Types::TeamMembershipType.to_non_null_type
+
+ def mutate
+ team_membership = TeamMembership.find(params[:id])
+ authorize! team_membership, :update?
+
+ result = UpdateTeamMembership.call!(params: permitted_params, team_membership: team_membership)
+ result.team_membership
+ end
+end
diff --git a/api/app/graphql/mutators/update_team_mutator.rb b/api/app/graphql/mutators/update_team_mutator.rb
new file mode 100644
index 0000000..10ea4c3
--- /dev/null
+++ b/api/app/graphql/mutators/update_team_mutator.rb
@@ -0,0 +1,20 @@
+class UpdateTeamMutator < ApplicationMutator
+ UpdateTeamInputType = GraphQL::InputObjectType.define do
+ name 'UpdateTeamInput'
+
+ parameter :name, !types.String
+ end
+
+ parameter :id, !types.ID
+ parameter :input, !UpdateTeamInputType
+
+ type Types::TeamType.to_non_null_type
+
+ def mutate
+ team = Team.find(params[:id])
+ authorize! team, :update?
+
+ result = UpdateTeam.call!(params: permitted_params, team: team)
+ result.team
+ end
+end
diff --git a/api/app/graphql/resolvers/application_resolver.rb b/api/app/graphql/resolvers/application_resolver.rb
new file mode 100755
index 0000000..3d88b9c
--- /dev/null
+++ b/api/app/graphql/resolvers/application_resolver.rb
@@ -0,0 +1,39 @@
+class ApplicationResolver < ApplicationFunction
+ include GraphQL::Sugar::Resolver
+
+ def resolved_object(allowed_classes = [])
+ parent = object.respond_to?(:object) ? object.object : object
+
+ if allowed_classes.present?
+ allowed_classes = Array.wrap(allowed_classes)
+ raise Exceptions::Forbidden if allowed_classes.all? { |c| !parent.is_a?(c) }
+ end
+
+ parent
+ end
+
+ def self.sortable
+ parameter :sort, types.String
+ parameter :sortDirection, types.String
+ end
+
+ def self.pageable
+ parameter :first, types.Int
+ parameter :skip, types.Int
+ end
+
+ def sorted_and_paged(records)
+ paged(sorted(records))
+ end
+
+ def sorted(records)
+ records = records.order(params[:sort] => (params[:sort_direction] || 'asc')) if params[:sort].present?
+ records
+ end
+
+ def paged(records)
+ records = records.limit(params[:first]) if params[:first].present?
+ records = records.offset(params[:skip]) if params[:skip].present?
+ records
+ end
+end
diff --git a/api/app/graphql/resolvers/assets_resolver.rb b/api/app/graphql/resolvers/assets_resolver.rb
new file mode 100644
index 0000000..a615bc2
--- /dev/null
+++ b/api/app/graphql/resolvers/assets_resolver.rb
@@ -0,0 +1,10 @@
+class AssetsResolver < ApplicationResolver
+ parameter :projectId, types.ID
+
+ def resolve
+ parent = resolved_object || Project.find(params[:project_id])
+ authorize! parent, :view_assets?
+
+ parent.assets
+ end
+end
diff --git a/api/app/graphql/resolvers/current_user_resolver.rb b/api/app/graphql/resolvers/current_user_resolver.rb
new file mode 100755
index 0000000..1c56ce3
--- /dev/null
+++ b/api/app/graphql/resolvers/current_user_resolver.rb
@@ -0,0 +1,7 @@
+class CurrentUserResolver < ApplicationResolver
+ type Types::UserType.to_non_null_type
+
+ def resolve
+ context[:current_user]
+ end
+end
diff --git a/api/app/graphql/resolvers/entities_resolver.rb b/api/app/graphql/resolvers/entities_resolver.rb
new file mode 100644
index 0000000..3eb6d3f
--- /dev/null
+++ b/api/app/graphql/resolvers/entities_resolver.rb
@@ -0,0 +1,10 @@
+class EntitiesResolver < ApplicationResolver
+ parameter :projectId, types.ID
+
+ def resolve
+ parent = resolved_object || Project.find(params[:project_id])
+ authorize! parent, :view_entities?
+
+ parent.entities.order(:name)
+ end
+end
diff --git a/api/app/graphql/resolvers/entity_resolver.rb b/api/app/graphql/resolvers/entity_resolver.rb
new file mode 100644
index 0000000..ba5b6b6
--- /dev/null
+++ b/api/app/graphql/resolvers/entity_resolver.rb
@@ -0,0 +1,10 @@
+class EntityResolver < ApplicationResolver
+ parameter :id, !types.ID
+
+ def resolve
+ entity = Entity.find(params[:id])
+ authorize! entity, :view?
+
+ entity
+ end
+end
diff --git a/api/app/graphql/resolvers/exports_resolver.rb b/api/app/graphql/resolvers/exports_resolver.rb
new file mode 100644
index 0000000..e90934f
--- /dev/null
+++ b/api/app/graphql/resolvers/exports_resolver.rb
@@ -0,0 +1,10 @@
+class ExportsResolver < ApplicationResolver
+ parameter :projectId, types.ID
+
+ def resolve
+ parent = resolved_object || Project.find(params[:project_id])
+ authorize! parent, :view_exports?
+
+ parent.exports.order(created_at: :desc)
+ end
+end
diff --git a/api/app/graphql/resolvers/fields_resolver.rb b/api/app/graphql/resolvers/fields_resolver.rb
new file mode 100644
index 0000000..42ca086
--- /dev/null
+++ b/api/app/graphql/resolvers/fields_resolver.rb
@@ -0,0 +1,10 @@
+class FieldsResolver < ApplicationResolver
+ parameter :entityId, types.ID
+
+ def resolve
+ entity = resolved_object || Entity.find(params[:entity_id])
+ authorize! entity, :view?
+
+ entity.nested_fields
+ end
+end
diff --git a/api/app/graphql/resolvers/key_pairs_resolver.rb b/api/app/graphql/resolvers/key_pairs_resolver.rb
new file mode 100644
index 0000000..61c06ad
--- /dev/null
+++ b/api/app/graphql/resolvers/key_pairs_resolver.rb
@@ -0,0 +1,10 @@
+class KeyPairsResolver < ApplicationResolver
+ parameter :projectId, types.ID
+
+ def resolve
+ parent = resolved_object || Project.find(params[:project_id])
+ authorize! parent, :view_key_pairs?
+
+ parent.key_pairs
+ end
+end
diff --git a/api/app/graphql/resolvers/project_resolver.rb b/api/app/graphql/resolvers/project_resolver.rb
new file mode 100644
index 0000000..a5c3382
--- /dev/null
+++ b/api/app/graphql/resolvers/project_resolver.rb
@@ -0,0 +1,10 @@
+class ProjectResolver < ApplicationResolver
+ parameter :id, !types.ID
+
+ def resolve
+ project = Project.find(params[:id])
+ authorize! project, :view?
+
+ project
+ end
+end
diff --git a/api/app/graphql/resolvers/projects_resolver.rb b/api/app/graphql/resolvers/projects_resolver.rb
new file mode 100644
index 0000000..6b9f599
--- /dev/null
+++ b/api/app/graphql/resolvers/projects_resolver.rb
@@ -0,0 +1,13 @@
+class ProjectsResolver < ApplicationResolver
+ sortable
+ pageable
+
+ parameter :teamId, !types.ID
+
+ def resolve
+ team = Team.find(params[:team_id])
+ authorize! team, :view_projects?
+
+ sorted_and_paged(team.projects)
+ end
+end
diff --git a/api/app/graphql/resolvers/record_resolver.rb b/api/app/graphql/resolvers/record_resolver.rb
new file mode 100644
index 0000000..2552e73
--- /dev/null
+++ b/api/app/graphql/resolvers/record_resolver.rb
@@ -0,0 +1,10 @@
+class RecordResolver < ApplicationResolver
+ parameter :recordId, types.ID
+
+ def resolve
+ record = Record.find(params[:record_id])
+ authorize! record.entity, :view?
+
+ record
+ end
+end
diff --git a/api/app/graphql/resolvers/records_resolver.rb b/api/app/graphql/resolvers/records_resolver.rb
new file mode 100644
index 0000000..ccab487
--- /dev/null
+++ b/api/app/graphql/resolvers/records_resolver.rb
@@ -0,0 +1,10 @@
+class RecordsResolver < ApplicationResolver
+ parameter :entityId, types.ID
+
+ def resolve
+ entity = resolved_object || Entity.find(params[:entity_id])
+ authorize! entity, :view?
+
+ entity.records
+ end
+end
diff --git a/api/app/graphql/resolvers/referenced_entities_resolver.rb b/api/app/graphql/resolvers/referenced_entities_resolver.rb
new file mode 100644
index 0000000..945df8b
--- /dev/null
+++ b/api/app/graphql/resolvers/referenced_entities_resolver.rb
@@ -0,0 +1,12 @@
+class ReferencedEntitiesResolver < ApplicationResolver
+ parameter :entityId, types.ID
+
+ type Types::EntityType.to_list_type
+
+ def resolve
+ entity = Entity.find(params[:entity_id])
+ authorize! entity.project, :view_entities?
+
+ Entity.where(parent_id: entity.id).or(Entity.where(id: entity.id))
+ end
+end
diff --git a/api/app/graphql/resolvers/resources_resolver.rb b/api/app/graphql/resolvers/resources_resolver.rb
new file mode 100644
index 0000000..e0292fd
--- /dev/null
+++ b/api/app/graphql/resolvers/resources_resolver.rb
@@ -0,0 +1,10 @@
+class ResourcesResolver < ApplicationResolver
+ parameter :projectId, types.ID
+
+ def resolve
+ parent = resolved_object || Project.find(params[:project_id])
+ authorize! parent, :view_resources?
+
+ parent.resources
+ end
+end
diff --git a/api/app/graphql/resolvers/restores_resolver.rb b/api/app/graphql/resolvers/restores_resolver.rb
new file mode 100644
index 0000000..cd4148c
--- /dev/null
+++ b/api/app/graphql/resolvers/restores_resolver.rb
@@ -0,0 +1,10 @@
+class RestoresResolver < ApplicationResolver
+ parameter :projectId, types.ID
+
+ def resolve
+ parent = resolved_object || Project.find(params[:project_id])
+ authorize! parent, :view_imports?
+
+ parent.restores.order(created_at: :desc)
+ end
+end
diff --git a/api/app/graphql/resolvers/team_memberships_resolver.rb b/api/app/graphql/resolvers/team_memberships_resolver.rb
new file mode 100644
index 0000000..76db3ef
--- /dev/null
+++ b/api/app/graphql/resolvers/team_memberships_resolver.rb
@@ -0,0 +1,12 @@
+class TeamMembershipsResolver < ApplicationResolver
+ pageable
+
+ parameter :teamId, types.ID
+
+ def resolve
+ parent_object = resolved_object || context[:current_user].teams.find(params[:team_id])
+ authorize! parent_object, :view_team_memberships?
+
+ paged(parent_object.team_memberships)
+ end
+end
diff --git a/api/app/graphql/resolvers/team_resolver.rb b/api/app/graphql/resolvers/team_resolver.rb
new file mode 100644
index 0000000..9eba774
--- /dev/null
+++ b/api/app/graphql/resolvers/team_resolver.rb
@@ -0,0 +1,7 @@
+class TeamResolver < ApplicationResolver
+ parameter :id, !types.ID
+
+ def resolve
+ context[:current_user].teams.find(params[:id])
+ end
+end
diff --git a/api/app/graphql/resolvers/teams_resolver.rb b/api/app/graphql/resolvers/teams_resolver.rb
new file mode 100644
index 0000000..b730685
--- /dev/null
+++ b/api/app/graphql/resolvers/teams_resolver.rb
@@ -0,0 +1,8 @@
+class TeamsResolver < ApplicationResolver
+ sortable
+ pageable
+
+ def resolve
+ sorted_and_paged(context[:current_user].teams)
+ end
+end
diff --git a/api/app/graphql/roots/mutation_type.rb b/api/app/graphql/roots/mutation_type.rb
new file mode 100755
index 0000000..1d7b386
--- /dev/null
+++ b/api/app/graphql/roots/mutation_type.rb
@@ -0,0 +1,52 @@
+module Roots
+ class MutationType < GraphQL::Schema::Object
+ include GraphQL::Sugar::Mutation
+
+ mutator :updateProfile
+
+ mutator :createTeam
+ mutator :updateTeam
+ mutator :createTransferRequest
+ mutator :cancelTransferRequest
+ mutator :acceptTransferRequest
+ mutator :rejectTransferRequest
+
+ mutator :createTeamMembership
+ mutator :updateTeamMembership
+ mutator :destroyTeamMembership
+
+ mutator :createProject
+ mutator :updateProject
+ mutator :exportProject
+ mutator :importProject
+
+ mutator :createKeyPair
+ mutator :revokeKeyPair
+
+ mutator :createAsset
+ mutator :updateAsset
+ mutator :destroyAsset
+
+ mutator :createEntity
+ mutator :updateEntity
+ mutator :destroyEntity
+
+ mutator :createField
+ mutator :updateField
+ mutator :destroyField
+ mutator :sortFields
+
+ mutator :createRecord
+ mutator :cloneRecord
+ mutator :updateRecord
+ mutator :destroyRecord
+
+ mutator :createResource
+ mutator :updateResource
+ mutator :destroyResource
+
+ mutator :ssoLogin
+ mutator :ssoCallback
+ mutator :ssoLogout
+ end
+end
diff --git a/api/app/graphql/roots/query_type.rb b/api/app/graphql/roots/query_type.rb
new file mode 100755
index 0000000..789357c
--- /dev/null
+++ b/api/app/graphql/roots/query_type.rb
@@ -0,0 +1,39 @@
+module Roots
+ class QueryType < GraphQL::Schema::Object
+ include GraphQL::Sugar::Query
+
+ resolver :currentUser
+
+ resolver :teams
+
+ resolver :team
+
+ resolver :teamMemberships
+
+ resolver :projects
+
+ resolver :project
+
+ resolver :keyPairs
+
+ resolver :assets
+
+ resolver :entities
+
+ resolver :referencedEntities
+
+ resolver :entity
+
+ resolver :fields
+
+ resolver :records
+
+ resolver :record
+
+ resolver :exports
+
+ resolver :restores
+
+ resolver :resources
+ end
+end
diff --git a/api/app/graphql/scalars/base_scalar_type.rb b/api/app/graphql/scalars/base_scalar_type.rb
new file mode 100755
index 0000000..483aad9
--- /dev/null
+++ b/api/app/graphql/scalars/base_scalar_type.rb
@@ -0,0 +1,4 @@
+module Scalars
+ class BaseScalarType < GraphQL::Schema::Scalar
+ end
+end
diff --git a/api/app/graphql/scalars/file_type.rb b/api/app/graphql/scalars/file_type.rb
new file mode 100644
index 0000000..c518a75
--- /dev/null
+++ b/api/app/graphql/scalars/file_type.rb
@@ -0,0 +1,13 @@
+module Scalars
+ class FileType < BaseScalarType
+ graphql_name 'File'
+
+ def self.coerce_input(value, _ctx)
+ value
+ end
+
+ def self.coerce_result(value, _ctx)
+ value
+ end
+ end
+end
diff --git a/api/app/graphql/scalars/hash_type.rb b/api/app/graphql/scalars/hash_type.rb
new file mode 100644
index 0000000..8b34416
--- /dev/null
+++ b/api/app/graphql/scalars/hash_type.rb
@@ -0,0 +1,13 @@
+module Scalars
+ class HashType < BaseScalarType
+ graphql_name 'Hash'
+
+ def self.coerce_input(value, _ctx)
+ value.deep_transform_keys { |key| key.to_s.underscore }
+ end
+
+ def self.coerce_result(value, _ctx)
+ value.deep_transform_keys { |key| key.to_s.camelize(:lower) }
+ end
+ end
+end
diff --git a/api/app/graphql/scalars/json_type.rb b/api/app/graphql/scalars/json_type.rb
new file mode 100644
index 0000000..5d3fa6e
--- /dev/null
+++ b/api/app/graphql/scalars/json_type.rb
@@ -0,0 +1,13 @@
+module Scalars
+ class JsonType < BaseScalarType
+ graphql_name 'Json'
+
+ def self.coerce_input(value, _ctx)
+ value
+ end
+
+ def self.coerce_result(value, _ctx)
+ value
+ end
+ end
+end
diff --git a/api/app/graphql/types/application_type.rb b/api/app/graphql/types/application_type.rb
new file mode 100755
index 0000000..b137c0b
--- /dev/null
+++ b/api/app/graphql/types/application_type.rb
@@ -0,0 +1,19 @@
+module Types
+ class ApplicationType < GraphQL::Schema::Object
+ include GraphQL::Sugar::Object
+
+ def self.file_field(name, version = nil, null: true, **options)
+ field_name = version.present? ? "#{name}_#{version}" : name
+
+ field field_name, String, null: null
+
+ define_method field_name do
+ if version.present? && version != :original
+ object.send("#{name}_url", version, **options)
+ else
+ object.send("#{name}_url", **options)
+ end
+ end
+ end
+ end
+end
diff --git a/api/app/graphql/types/asset_type.rb b/api/app/graphql/types/asset_type.rb
new file mode 100644
index 0000000..472aa25
--- /dev/null
+++ b/api/app/graphql/types/asset_type.rb
@@ -0,0 +1,12 @@
+module Types
+ class AssetType < ApplicationType
+ model_class Asset
+
+ attribute :name
+ file_field :file, :original, public: true
+
+ field :metadata, Scalars::HashType, null: false, method: :metadata
+
+ relationship :project
+ end
+end
diff --git a/api/app/graphql/types/entity_type.rb b/api/app/graphql/types/entity_type.rb
new file mode 100644
index 0000000..526ef43
--- /dev/null
+++ b/api/app/graphql/types/entity_type.rb
@@ -0,0 +1,14 @@
+module Types
+ class EntityType < ApplicationType
+ model_class Entity
+
+ attribute :label
+ attribute :name
+ attribute :singleton
+
+ relationship :fields
+ relationship :parent
+ relationship :project
+ relationship :records
+ end
+end
diff --git a/api/app/graphql/types/export_type.rb b/api/app/graphql/types/export_type.rb
new file mode 100644
index 0000000..7c17677
--- /dev/null
+++ b/api/app/graphql/types/export_type.rb
@@ -0,0 +1,13 @@
+module Types
+ class ExportType < ApplicationType
+ model_class Export
+
+ attribute :status
+
+ file_field :file, public: true
+
+ field :metadata, Scalars::HashType, null: true, resolve: ->(obj, args, ctx) { obj&.file&.metadata }
+
+ relationship :project
+ end
+end
diff --git a/api/app/graphql/types/field_type.rb b/api/app/graphql/types/field_type.rb
new file mode 100644
index 0000000..4ee71d6
--- /dev/null
+++ b/api/app/graphql/types/field_type.rb
@@ -0,0 +1,22 @@
+module Types
+ class FieldType < ApplicationType
+ model_class Field
+
+ attribute :data_type
+ attribute :default_value
+ attribute :editor
+ attribute :element_type
+ attribute :referenced_entity_id
+ attribute :hint
+ attribute :label
+ attribute :name
+ attribute :position
+ attribute :validations, Scalars::JsonType, null: true
+ attribute :settings, Scalars::JsonType, null: true
+
+ field :parentId, ID, null: true
+
+ relationship :entity
+ relationship :children
+ end
+end
diff --git a/api/app/graphql/types/key_pair_type.rb b/api/app/graphql/types/key_pair_type.rb
new file mode 100644
index 0000000..c8e10d7
--- /dev/null
+++ b/api/app/graphql/types/key_pair_type.rb
@@ -0,0 +1,9 @@
+module Types
+ class KeyPairType < ApplicationType
+ model_class KeyPair
+
+ attribute :public_key
+
+ relationship :project
+ end
+end
diff --git a/api/app/graphql/types/locale_type.rb b/api/app/graphql/types/locale_type.rb
new file mode 100644
index 0000000..03f62fe
--- /dev/null
+++ b/api/app/graphql/types/locale_type.rb
@@ -0,0 +1,9 @@
+module Types
+ class LocaleType < ApplicationType
+ model_class Locale
+
+ attribute :language
+
+ relationship :project
+ end
+end
diff --git a/api/app/graphql/types/project_type.rb b/api/app/graphql/types/project_type.rb
new file mode 100644
index 0000000..6260048
--- /dev/null
+++ b/api/app/graphql/types/project_type.rb
@@ -0,0 +1,12 @@
+module Types
+ class ProjectType < ApplicationType
+ model_class Project
+
+ attribute :name
+
+ field :isRestoring, Boolean, null: false, resolve: ->(obj, args, ctx) { obj.restores.pending.present? }
+
+ relationship :team
+ relationship :entities
+ end
+end
diff --git a/api/app/graphql/types/property_type.rb b/api/app/graphql/types/property_type.rb
new file mode 100644
index 0000000..aea2557
--- /dev/null
+++ b/api/app/graphql/types/property_type.rb
@@ -0,0 +1,16 @@
+module Types
+ class PropertyType < ApplicationType
+ model_class Property
+
+ attribute :value
+ attribute :position
+
+ field :parentId, String, null: true
+
+ relationship :asset
+ relationship :children
+ relationship :field
+ relationship :linked_record
+ relationship :record
+ end
+end
diff --git a/api/app/graphql/types/record_type.rb b/api/app/graphql/types/record_type.rb
new file mode 100644
index 0000000..4a6dc84
--- /dev/null
+++ b/api/app/graphql/types/record_type.rb
@@ -0,0 +1,9 @@
+module Types
+ class RecordType < ApplicationType
+ model_class Record
+
+ field :properties, [Types::PropertyType], null: false, method: :nested_properties
+
+ relationship :entity
+ end
+end
diff --git a/api/app/graphql/types/relationship_type.rb b/api/app/graphql/types/relationship_type.rb
new file mode 100644
index 0000000..e5c45ce
--- /dev/null
+++ b/api/app/graphql/types/relationship_type.rb
@@ -0,0 +1,12 @@
+module Types
+ class RelationshipType < ApplicationType
+ model_class Relationship
+
+ relationship :entity
+ relationship :field
+
+ # todos
+ # - add linked entity
+ # - add linked fied
+ end
+end
diff --git a/api/app/graphql/types/resource_type.rb b/api/app/graphql/types/resource_type.rb
new file mode 100644
index 0000000..d27f5c1
--- /dev/null
+++ b/api/app/graphql/types/resource_type.rb
@@ -0,0 +1,12 @@
+module Types
+ class ResourceType < ApplicationType
+ model_class Resource
+
+ attribute :name
+ file_field :file, public: true
+
+ field :metadata, Scalars::HashType, null: false, resolve: ->(obj, args, ctx) { obj.file.metadata }
+
+ relationship :project
+ end
+end
diff --git a/api/app/graphql/types/response_type.rb b/api/app/graphql/types/response_type.rb
new file mode 100755
index 0000000..e1c3ac0
--- /dev/null
+++ b/api/app/graphql/types/response_type.rb
@@ -0,0 +1,5 @@
+Types::ResponseType = GraphQL::ObjectType.define do
+ name 'Response'
+
+ field :success, !types.Boolean, hash_key: :success
+end
diff --git a/api/app/graphql/types/restore_type.rb b/api/app/graphql/types/restore_type.rb
new file mode 100644
index 0000000..8ecb74d
--- /dev/null
+++ b/api/app/graphql/types/restore_type.rb
@@ -0,0 +1,10 @@
+module Types
+ class RestoreType < ApplicationType
+ model_class Restore
+
+ attribute :status
+ attribute :url
+
+ relationship :project
+ end
+end
diff --git a/api/app/graphql/types/team_membership_type.rb b/api/app/graphql/types/team_membership_type.rb
new file mode 100644
index 0000000..b57787f
--- /dev/null
+++ b/api/app/graphql/types/team_membership_type.rb
@@ -0,0 +1,10 @@
+module Types
+ class TeamMembershipType < ApplicationType
+ model_class TeamMembership
+
+ attribute :role
+
+ relationship :team
+ relationship :user
+ end
+end
diff --git a/api/app/graphql/types/team_type.rb b/api/app/graphql/types/team_type.rb
new file mode 100644
index 0000000..7fe87b9
--- /dev/null
+++ b/api/app/graphql/types/team_type.rb
@@ -0,0 +1,11 @@
+module Types
+ class TeamType < ApplicationType
+ model_class Team
+
+ attribute :name
+ field :isTransferRequested, Boolean, null: false, method: :transfer_requested?
+
+ relationship :transfer_owner
+ relationship :team_memberships
+ end
+end
diff --git a/api/app/graphql/types/user_type.rb b/api/app/graphql/types/user_type.rb
new file mode 100755
index 0000000..6cb0f0f
--- /dev/null
+++ b/api/app/graphql/types/user_type.rb
@@ -0,0 +1,14 @@
+module Types
+ class UserType < ApplicationType
+ model_class User
+
+ attribute :first_name
+ attribute :last_name
+ attribute :email
+
+ file_field :profile_picture, :thumbnail
+ file_field :profile_picture, :normal
+
+ relationship :team_memberships
+ end
+end
diff --git a/api/app/helpers/application_helper.rb b/api/app/helpers/application_helper.rb
new file mode 100755
index 0000000..de6be79
--- /dev/null
+++ b/api/app/helpers/application_helper.rb
@@ -0,0 +1,2 @@
+module ApplicationHelper
+end
diff --git a/api/app/interactors/accept_transfer_request.rb b/api/app/interactors/accept_transfer_request.rb
new file mode 100644
index 0000000..33f9222
--- /dev/null
+++ b/api/app/interactors/accept_transfer_request.rb
@@ -0,0 +1,24 @@
+class AcceptTransferRequest
+ include Interactor
+
+ def call
+ ActiveRecord::Base.transaction do
+ verify_transfer_request
+ accept_transfer_request
+ end
+ end
+
+ def verify_transfer_request
+ context.fail!(error: 'Your transfer link has either expired or been canceled.') if !context.team.transfer_requested?
+ end
+
+ def accept_transfer_request
+ old_owner_membership = context.team.team_memberships.find_by(role: :owner)
+ new_owner_membership = context.team.team_memberships.find_by(user_id: context.team.transfer_owner_id)
+
+ old_owner_membership.update!(role: :manager)
+ new_owner_membership.update!(role: :owner)
+
+ context.team.reset_transfer!
+ end
+end
diff --git a/api/app/interactors/authenticate_user.rb b/api/app/interactors/authenticate_user.rb
new file mode 100644
index 0000000..d8fce4e
--- /dev/null
+++ b/api/app/interactors/authenticate_user.rb
@@ -0,0 +1,30 @@
+class AuthenticateUser
+ include Interactor
+
+ def call
+ decode_auth_token
+ find_user
+ validate_token
+ end
+
+ protected
+
+ def decode_auth_token
+ @decoded_token = JsonWebToken.decode(context.bearer_token)
+ rescue JWT::ExpiredSignature
+ context.fail!(error: 'Your session has expired. Please login again to continue.')
+ end
+
+ def find_user
+ context.user = User.find_by(id: @decoded_token['user_id'])
+ context.fail!(error: 'User not found') unless context.user
+ end
+
+ def validate_token
+ revoked = AuthToken.revoked?(
+ user: context.user,
+ jti: @decoded_token['jti']
+ )
+ context.fail!(error: 'Your session has expired. Please login again to continue.') if revoked
+ end
+end
diff --git a/api/app/interactors/cancel_transfer_request.rb b/api/app/interactors/cancel_transfer_request.rb
new file mode 100644
index 0000000..71b6e88
--- /dev/null
+++ b/api/app/interactors/cancel_transfer_request.rb
@@ -0,0 +1,7 @@
+class CancelTransferRequest
+ include Interactor
+
+ def call
+ context.team.reset_transfer!
+ end
+end
diff --git a/api/app/interactors/clone_record.rb b/api/app/interactors/clone_record.rb
new file mode 100644
index 0000000..fd49116
--- /dev/null
+++ b/api/app/interactors/clone_record.rb
@@ -0,0 +1,8 @@
+class CloneRecord
+ include Interactor
+
+ def call
+ context.record = context.record.amoeba_dup
+ context.record.save!(validate: false)
+ end
+end
diff --git a/api/app/interactors/create_asset.rb b/api/app/interactors/create_asset.rb
new file mode 100644
index 0000000..984f381
--- /dev/null
+++ b/api/app/interactors/create_asset.rb
@@ -0,0 +1,7 @@
+class CreateAsset
+ include Interactor
+
+ def call
+ context.asset = context.project.assets.create!(context.params)
+ end
+end
diff --git a/api/app/interactors/create_entity.rb b/api/app/interactors/create_entity.rb
new file mode 100644
index 0000000..592afbf
--- /dev/null
+++ b/api/app/interactors/create_entity.rb
@@ -0,0 +1,7 @@
+class CreateEntity
+ include Interactor
+
+ def call
+ context.entity = context.project.entities.create!(context.params)
+ end
+end
diff --git a/api/app/interactors/create_field.rb b/api/app/interactors/create_field.rb
new file mode 100644
index 0000000..6f0d183
--- /dev/null
+++ b/api/app/interactors/create_field.rb
@@ -0,0 +1,53 @@
+class CreateField
+ include Interactor
+
+ def call
+ ActiveRecord::Base.transaction do
+ process
+ create_field
+ end
+ end
+
+ protected
+
+ def process
+ context.entities = context.entity.project.entities
+ context.field_attributes = process_field(context.params)
+ end
+
+ def create_field
+ context.field = context.entity.fields.create!(context.field_attributes)
+ end
+
+ def process_field(field)
+ field_params = field.except(:children, :referenced_entity_name)
+
+ if context.is_import
+ referenced_entity = context.entities.find_by(name: field[:referenced_entity_name])
+ field_params[:referenced_entity_id] = referenced_entity&.id
+ end
+
+ children_attributes = process_children(field_params, field[:children])
+ field_params[:children_attributes] = children_attributes if children_attributes.present?
+
+ field_params
+ end
+
+ def process_children(parent, children)
+ if parent[:data_type]&.to_sym == :array
+ sub_parent = {
+ name: "#{parent[:name]}_item",
+ label: parent[:label].singularize,
+ position: 0,
+ data_type: parent[:element_type],
+ referenced_entity_id: parent[:referenced_entity_id]
+ }
+
+ sub_parent[:children_attributes] = process_children(sub_parent, children) if children.present?
+
+ [sub_parent]
+ else
+ (children || []).map { |child| process_field(child) }
+ end
+ end
+end
diff --git a/api/app/interactors/create_key_pair.rb b/api/app/interactors/create_key_pair.rb
new file mode 100644
index 0000000..989e2c0
--- /dev/null
+++ b/api/app/interactors/create_key_pair.rb
@@ -0,0 +1,7 @@
+class CreateKeyPair
+ include Interactor
+
+ def call
+ context.key_pair = context.project.key_pairs.create!
+ end
+end
diff --git a/api/app/interactors/create_project.rb b/api/app/interactors/create_project.rb
new file mode 100644
index 0000000..2b9846d
--- /dev/null
+++ b/api/app/interactors/create_project.rb
@@ -0,0 +1,10 @@
+class CreateProject
+ include Interactor
+
+ def call
+ ActiveRecord::Base.transaction do
+ context.project = context.team.projects.create!(context.params.slice(:name))
+ context.project.key_pairs.create!
+ end
+ end
+end
diff --git a/api/app/interactors/create_record.rb b/api/app/interactors/create_record.rb
new file mode 100644
index 0000000..c6a0d0d
--- /dev/null
+++ b/api/app/interactors/create_record.rb
@@ -0,0 +1,110 @@
+class CreateRecord
+ include Interactor
+
+ def call
+ ActiveRecord::Base.transaction do
+ process_traits
+ create_record
+ end
+ end
+
+ protected
+
+ def process_traits
+ context.properties_attributes = (context.params[:traits] || {}).map do |field_name, value|
+ field = context.entity.fields.find_by(name: field_name)
+
+ process_value(field, value)
+ end
+ end
+
+ def create_record
+ context.record = context.entity.records.create!(
+ properties_attributes: context.properties_attributes
+ )
+ end
+
+ def process_value(field, value, position = 0) # rubocop:disable Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
+ property = {}
+
+ if field.array?
+ property = { field: field, children_attributes: process_array(field, value) }
+ elsif field.key_value?
+ property = { field: field, children_attributes: process_hash(field, value) }
+ elsif field.reference?
+ if context.is_import
+ imported_record_id = value&.is_a?(Hash) ? value[:id] : value
+ property = { field: field, value: "____ref_id____#{imported_record_id}" }
+ else
+ property = { field: field, linked_record: process_reference(value) }
+ end
+ elsif field.image? || field.file?
+ property = { field: field, asset: process_asset(value) }
+ else
+ property = { field: field, value: value }
+ end
+
+ property[:position] = position
+
+ property
+ end
+
+ def process_array(field, values)
+ return [] if !values.is_a? Array
+
+ sub_field = field.children.first
+
+ processed_values = values.map do |value|
+ next if value.except(:position).blank?
+
+ input_value = sub_field.reference? ? value : value[:value]
+ process_value(sub_field, input_value, value[:position])
+ end
+
+ processed_values.compact
+ end
+
+ def process_hash(field, values)
+ return [] if !values.is_a? Hash
+
+ values.map do |field_name, value|
+ child_field = field.children.find_by(name: field_name)
+
+ process_value(child_field, value)
+ end
+ end
+
+ def process_reference(value)
+ return nil if context.is_import
+
+ linked_record = Record.find_by(id: value[:id]) if value[:id].present?
+
+ if value[:traits].present?
+ if linked_record.present?
+ linked_record = UpdateRecord.call!(params: value, record: linked_record).record
+ else
+ entity = Entity.find_by(id: value[:entity_id])
+ linked_record = CreateRecord.call!(params: value, entity: entity).record
+ end
+ end
+
+ linked_record
+ end
+
+ def process_asset(value)
+ if context.is_import && value.present? && value[:name].present?
+ begin
+ return context.entity.project.assets.create!(name: value[:name], file_remote_url: value[:url])
+ rescue StandardError => e
+ Rails.logger.debug "Failed to add #{value}"
+ Rails.logger.debug e
+ end
+ end
+
+ return Asset.find(value[:id]) if value.is_a?(Hash) && value[:id].present?
+
+ return if !value.respond_to?(:content_type)
+
+ context.entity.project.assets.create!(name: value.original_filename, file: value)
+ end
+end
diff --git a/api/app/interactors/create_resource.rb b/api/app/interactors/create_resource.rb
new file mode 100644
index 0000000..df45715
--- /dev/null
+++ b/api/app/interactors/create_resource.rb
@@ -0,0 +1,21 @@
+class CreateResource
+ include Interactor
+
+ def call
+ validate
+ create
+ end
+
+ protected
+
+ def validate
+ name = context.params[:name]
+ resource = context.project.resources.find_by(name: name)
+
+ context.fail!(error: "#{name} already exists, please try with different name or update the existing one.") if resource.present?
+ end
+
+ def create
+ context.resource = context.project.resources.create!(context.params)
+ end
+end
diff --git a/api/app/interactors/create_team.rb b/api/app/interactors/create_team.rb
new file mode 100644
index 0000000..d112d37
--- /dev/null
+++ b/api/app/interactors/create_team.rb
@@ -0,0 +1,23 @@
+class CreateTeam
+ include Interactor
+
+ def call
+ ActiveRecord::Base.transaction do
+ create_team
+ create_team_membership
+ end
+ end
+
+ protected
+
+ def create_team
+ context.team = Team.create!(context.team_params)
+ end
+
+ def create_team_membership
+ context.team.team_memberships.create!(
+ user: context.current_user,
+ role: :owner
+ )
+ end
+end
diff --git a/api/app/interactors/create_team_membership.rb b/api/app/interactors/create_team_membership.rb
new file mode 100644
index 0000000..cf51e56
--- /dev/null
+++ b/api/app/interactors/create_team_membership.rb
@@ -0,0 +1,34 @@
+class CreateTeamMembership
+ include Interactor
+
+ def call
+ ActiveRecord::Base.transaction do
+ validate_role
+ find_or_create_user
+ create_team_membership
+ end
+ end
+
+ protected
+
+ def validate_role
+ return if context.params[:role] != 'owner'
+
+ context.fail!(error: 'Role cannot be owner.')
+ end
+
+ def find_or_create_user
+ context.user = User.find_or_create_by!(email: context.params[:email])
+ end
+
+ def create_team_membership
+ team_membership = context.team.team_memberships.find_by(user: context.user)
+
+ context.fail!(error: 'Email is already added to the team.') if team_membership.present?
+
+ context.team_membership = context.team.team_memberships.create!(
+ user: context.user,
+ role: context.params[:role]
+ )
+ end
+end
diff --git a/api/app/interactors/create_transfer_request.rb b/api/app/interactors/create_transfer_request.rb
new file mode 100644
index 0000000..bf59d53
--- /dev/null
+++ b/api/app/interactors/create_transfer_request.rb
@@ -0,0 +1,27 @@
+class CreateTransferRequest
+ include Interactor
+
+ def call
+ ActiveRecord::Base.transaction do
+ find_user
+ validate_user
+ initiate_transfer
+ end
+ end
+
+ def find_user
+ @user = User.find_by(id: context.params[:user_id])
+ end
+
+ def validate_user
+ context.fail!(error: 'The user you have selected does not exist.') if @user.blank?
+
+ team_membership = context.team.team_memberships.find_by(user: @user)
+ context.fail!(error: 'The user you have selected does not belong to this team.') if team_membership.blank?
+ context.fail!(error: 'The user you have selected is already the owner of this team.') if team_membership.owner?
+ end
+
+ def initiate_transfer
+ context.team.request_transfer_to!(@user)
+ end
+end
diff --git a/api/app/interactors/destroy_asset.rb b/api/app/interactors/destroy_asset.rb
new file mode 100644
index 0000000..b79157f
--- /dev/null
+++ b/api/app/interactors/destroy_asset.rb
@@ -0,0 +1,7 @@
+class DestroyAsset
+ include Interactor
+
+ def call
+ context.asset.destroy!
+ end
+end
diff --git a/api/app/interactors/destroy_entity.rb b/api/app/interactors/destroy_entity.rb
new file mode 100644
index 0000000..6aa4a5f
--- /dev/null
+++ b/api/app/interactors/destroy_entity.rb
@@ -0,0 +1,7 @@
+class DestroyEntity
+ include Interactor
+
+ def call
+ context.entity.destroy!
+ end
+end
diff --git a/api/app/interactors/destroy_field.rb b/api/app/interactors/destroy_field.rb
new file mode 100644
index 0000000..d0fb3c4
--- /dev/null
+++ b/api/app/interactors/destroy_field.rb
@@ -0,0 +1,7 @@
+class DestroyField
+ include Interactor
+
+ def call
+ context.field.destroy!
+ end
+end
diff --git a/api/app/interactors/destroy_record.rb b/api/app/interactors/destroy_record.rb
new file mode 100644
index 0000000..9dbeb27
--- /dev/null
+++ b/api/app/interactors/destroy_record.rb
@@ -0,0 +1,7 @@
+class DestroyRecord
+ include Interactor
+
+ def call
+ context.record.destroy!
+ end
+end
diff --git a/api/app/interactors/destroy_resource.rb b/api/app/interactors/destroy_resource.rb
new file mode 100644
index 0000000..3014046
--- /dev/null
+++ b/api/app/interactors/destroy_resource.rb
@@ -0,0 +1,7 @@
+class DestroyResource
+ include Interactor
+
+ def call
+ context.resource.destroy!
+ end
+end
diff --git a/api/app/interactors/destroy_team_membership.rb b/api/app/interactors/destroy_team_membership.rb
new file mode 100644
index 0000000..5306fb6
--- /dev/null
+++ b/api/app/interactors/destroy_team_membership.rb
@@ -0,0 +1,24 @@
+class DestroyTeamMembership
+ include Interactor
+
+ def call
+ ActiveRecord::Base.transaction do
+ verify_team_membership
+ destroy_team_membership
+ clean_up_transfer_requests
+ end
+ end
+
+ def verify_team_membership
+ context.fail!(error: 'Owner cannot be deleted.') if context.team_membership.owner?
+ end
+
+ def destroy_team_membership
+ context.team_membership.destroy!
+ end
+
+ def clean_up_transfer_requests
+ team = context.team_membership.team
+ team.reset_transfer! if team.transfer_owner_id == context.team_membership.user_id
+ end
+end
diff --git a/api/app/interactors/export_project.rb b/api/app/interactors/export_project.rb
new file mode 100644
index 0000000..f6e8cf5
--- /dev/null
+++ b/api/app/interactors/export_project.rb
@@ -0,0 +1,23 @@
+class ExportProject
+ include Interactor
+
+ def call
+ ActiveRecord::Base.transaction do
+ check_pending_export
+ create_export_instance
+ initiate_export
+ end
+ end
+
+ def check_pending_export
+ context.fail!(error: 'You have a pending export.') if context.project.exports.pending.present?
+ end
+
+ def create_export_instance
+ context.export = context.project.exports.create
+ end
+
+ def initiate_export
+ ExportProjectPublisher.publish({ project_id: context.project.id, export_id: context.export.id })
+ end
+end
diff --git a/api/app/interactors/import_project.rb b/api/app/interactors/import_project.rb
new file mode 100644
index 0000000..e45c189
--- /dev/null
+++ b/api/app/interactors/import_project.rb
@@ -0,0 +1,23 @@
+class ImportProject
+ include Interactor
+
+ def call
+ ActiveRecord::Base.transaction do
+ check_pending_restore
+ initialize_restore
+ initiate_restore_worker
+ end
+ end
+
+ def check_pending_restore
+ context.fail!(error: 'Previous import is running. It may take a while to finish.') if context.project.restores.pending.present?
+ end
+
+ def initialize_restore
+ context.restore = context.project.restores.create(url: context.params[:url])
+ end
+
+ def initiate_restore_worker
+ ImportProjectPublisher.publish({ project_id: context.project.id, restore_id: context.restore.id })
+ end
+end
diff --git a/api/app/interactors/perform_export.rb b/api/app/interactors/perform_export.rb
new file mode 100644
index 0000000..64b10c2
--- /dev/null
+++ b/api/app/interactors/perform_export.rb
@@ -0,0 +1,224 @@
+class PerformExport # rubocop:disable Metrics/ClassLength
+ include Interactor
+
+ MAIN_FOLDER_PATH = 'tmp/exports'.freeze
+ FILE_NAME = 'data.json'.freeze
+ MODELS = [Entity, Field, Record, Property].freeze
+
+ def call
+ ActiveRecord::Base.transaction do
+ set_default
+ collect_entities
+ collect_resources
+ collect_fields
+ collect_field_hierarchies
+ collect_records
+ collect_properties
+ collect_property_hierarchies
+ write_to_temp_file
+ upload_temp_file
+ delete_temp_file
+ end
+ end
+
+ def set_default
+ @mapping = MODELS.each_with_object({}) { |model, obj| obj[model.name.downcase.to_sym] = {} }
+ @assets = context.project.assets
+ @data = {}
+ end
+
+ def collect_entities
+ @current_model = Entity
+ @entities = context.project.entities
+ @data[model_name] = segregrate_and_map(@entities)
+ end
+
+ def collect_resources
+ @data[:resources] = context.project.resources.map do |resource|
+ {
+ name: resource.name,
+ url: resource.resolve_file,
+ size: resource.file.size
+ }
+ end
+ end
+
+ def collect_fields
+ @current_model = Field
+
+ sql = %(
+ SELECT fields.* FROM fields
+ INNER JOIN field_hierarchies ON fields.id = field_hierarchies.descendant_id
+ WHERE field_hierarchies.ancestor_id IN (SELECT id from fields where entity_id IN (#{@entities.ids.join(', ')}))
+ ORDER BY created_at;
+ )
+
+ @fields = @current_model.find_by_sql(sql)
+ @data[model_name] = segregrate_and_map(@fields)
+ end
+
+ def collect_field_hierarchies
+ ids = @fields.pluck(:id).join(', ')
+
+ sql = %(
+ SELECT * from field_hierarchies
+ WHERE ancestor_id IN (#{ids})
+ OR descendant_id IN (#{ids});
+ )
+
+ field_hierarchies = ActiveRecord::Base.connection.exec_query(sql).to_a
+
+ @data["#{model_name}_hierarchies"] = field_hierarchies.map { |fh| process_hierarchy_for(fh) }
+ end
+
+ def collect_records
+ @current_model = Record
+ @records = Record.where(entity_id: @entities.ids)
+ @data[model_name] = segregrate_and_map(@records)
+ end
+
+ def collect_properties
+ @current_model = Property
+ sql = %(
+ SELECT properties.* FROM properties
+ INNER JOIN property_hierarchies ON properties.id = property_hierarchies.descendant_id
+ WHERE property_hierarchies.ancestor_id IN (SELECT id from properties where record_id IN (#{@records.ids.join(', ')}))
+ )
+
+ @properties = ActiveRecord::Base.connection.exec_query(sql).to_a
+ @data[model_name] = segregrate_and_map(@properties)
+ end
+
+ def collect_property_hierarchies
+ ids = @properties.map { |p| p['id'] }.join(', ')
+
+ sql = %(
+ SELECT * from property_hierarchies
+ WHERE ancestor_id IN (#{ids})
+ OR descendant_id IN (#{ids})
+ )
+
+ property_hierarchies = ActiveRecord::Base.connection.exec_query(sql).to_a
+
+ @data["#{model_name}_hierarchies"] = property_hierarchies.map { |fh| process_hierarchy_for(fh) }
+ end
+
+ def write_to_temp_file
+ FileUtils.mkdir_p(folder_path) unless Dir.exist?(folder_path)
+ File.open(file_path, 'w') do |f|
+ f.write(@data.to_json)
+ end
+ end
+
+ def upload_temp_file
+ if context.skip_upload
+ context.path = file_path
+ else
+ context.export.file = File.open(file_path, 'rb')
+ context.export.completed!
+ end
+ end
+
+ def delete_temp_file
+ File.delete(file_path) if File.exist?(file_path) && !context.skip_upload
+ end
+
+ # Helper methods
+
+ def file_path
+ "#{folder_path}/#{FILE_NAME}"
+ end
+
+ def folder_path
+ "#{MAIN_FOLDER_PATH}/#{context.project.id}"
+ end
+
+ def process_hierarchy_for(node)
+ {
+ ancestor_id: mapping[node['ancestor_id']],
+ descendant_id: mapping[node['descendant_id']],
+ generations: node['generations']
+ }
+ end
+
+ def segregate(records)
+ dataset = []
+ level = 0
+ cloned_records = records.clone.to_a
+
+ until cloned_records.empty?
+ if level == 0
+ dataset[level] = select_from_array(cloned_records) { |r| (r[:parent_id] || r['parent_id']).nil? }
+ else
+ parent_ids = dataset[level - 1].map { |r| r[:id] || r['id'] }
+ dataset[level] = select_from_array(cloned_records) { |r| parent_ids.include?(r[:parent_id] || r['parent_id']) }
+ end
+
+ level += 1
+ end
+
+ dataset
+ end
+
+ def segregrate_and_map(records)
+ segregate(records).map { |set| process(set) }
+ end
+
+ def process(records) # rubocop:disable Metrics/CyclomaticComplexity,Metrics/PerceivedComplexity
+ records.map do |record|
+ normalized_record = record.respond_to?(:attributes) ? record.attributes : record
+ attributes = normalized_record.except('created_at', 'updated_at')
+ attributes = attributes.except('id') if context.include_id.blank?
+ @mapping[model_name][normalized_record['id']] = normalized_record['uid']
+
+ associations.each do |association_name, association_model_name|
+ association = "#{association_name}_id" # property_id, linked_record_id etc.
+
+ if association_name == :project
+ attributes[association] = nil
+ elsif association_name == :asset && attributes['asset_id'].present?
+ attributes['asset_id'] = extract_asset_for(attributes)
+ elsif attributes[association].present?
+ attributes[association] = resolve_uid(association_model_name, attributes[association])
+ end
+ end
+
+ attributes
+ end
+ end
+
+ def extract_asset_for(record)
+ asset = @assets.find { |a| a.id == record['asset_id'] }
+
+ {
+ name: asset&.name,
+ url: asset&.resolve_original_file
+ }
+ end
+
+ def select_from_array(array, &block)
+ temp = array.select(&block)
+ array.reject!(&block)
+ temp
+ end
+
+ def resolve_uid(name, id)
+ @mapping[name][id] if @mapping[name].present?
+ end
+
+ def associations
+ model.reflect_on_all_associations(:belongs_to).inject({}) { |obj, a| obj[a.name] = a.class_name.downcase.to_sym; obj } # rubocop:disable Style/EachWithObject,Style/Semicolon
+ end
+
+ def model
+ @current_model
+ end
+
+ def mapping
+ @mapping[model_name]
+ end
+
+ def model_name
+ model.name.downcase.to_sym
+ end
+end
diff --git a/api/app/interactors/perform_restore.rb b/api/app/interactors/perform_restore.rb
new file mode 100644
index 0000000..5c08dbf
--- /dev/null
+++ b/api/app/interactors/perform_restore.rb
@@ -0,0 +1,339 @@
+# rubocop:disable Metrics/ClassLength
+require 'open-uri'
+
+class PerformRestore
+ include Interactor
+
+ MODELS = [Entity, Field, Record, Property].freeze
+ MODELS_AS_TREE = [:field, :property].freeze
+ MODEL_PRIMARY_BELONGS_TO = {
+ entity: :project,
+ field: :entity,
+ property: :record,
+ record: :entity
+ }.freeze
+ OPERATIONS = [:create, :update, :destroy].freeze
+
+ def call
+ ActiveRecord::Base.transaction do
+ setup
+ process
+ restore_resources
+ destroy_temp_file
+ update_restore_complete
+ end
+ end
+
+ def setup
+ # Initialize mapping
+ @mapping = MODELS.each_with_object({}) { |m, o| o[m.name.downcase.to_sym] = {} }
+ @local_data = {}
+ @local_export = nil
+
+ open(context.restore.url) { |io| @raw_data = io.read }
+ @remote_data = JSON.parse(@raw_data, symbolize_names: true)
+ end
+
+ def process
+ if project_empty?
+ MODELS.each do |model|
+ set_current_model(model)
+
+ data = @remote_data[current_model_name]
+ create_records(data)
+
+ if MODELS_AS_TREE.include? current_model_name
+ hierarchy_data = @remote_data[current_hierarchy]
+ create_hierarchies(hierarchy_data)
+ end
+ end
+ else
+ @local_export = PerformExport.call!(project: context.project, skip_upload: true, include_id: true)
+ @local_data = JSON.parse(File.read(@local_export.path), symbolize_names: true)
+
+ MODELS.each do |model|
+ set_current_model(model)
+
+ initialize_uid_mapping
+
+ data = compose_diff(
+ @local_data[current_model_name],
+ @remote_data[current_model_name]
+ )
+
+ if MODELS_AS_TREE.include? current_model_name
+ hierarchy_data = compose_hierarchy_diff(
+ @local_data[current_hierarchy],
+ @remote_data[current_hierarchy]
+ )
+ end
+
+ OPERATIONS.each do |operation|
+ public_send("#{operation}_records", data[operation])
+ public_send("#{operation}_hierarchies", hierarchy_data[operation]) if MODELS_AS_TREE.include? current_model_name
+ end
+ end
+ end
+ end
+
+ def restore_resources
+ diff = Diff.perform(@local_data[:resources], @remote_data[:resources], identifier: :name, ignore: [[:url]])
+ resources = context.project.resources
+
+ diff[:create].each do |resource|
+ resources.create!(
+ project: context.project, # if project not passed explicitly, project_id becomes nil in generate_location (ResourceFileUploader)
+ name: resource[:name],
+ file_remote_url: resource[:url]
+ )
+ end
+
+ diff[:destroy].each do |resource|
+ resources.find_by(name: resource[:name])&.destroy
+ end
+
+ diff[:update].each do |resource|
+ resolved_resource = resources.find_by(name: resource[:name])
+ resolved_resource&.update(file_remote_url: resource[:url])
+ end
+ end
+
+ def destroy_temp_file
+ path = @local_export&.path
+
+ return if path.blank?
+
+ File.delete(path) if File.exist?(path)
+ end
+
+ def update_restore_complete
+ context.restore.completed!
+ end
+
+ def create_records(data)
+ data.each do |dataset|
+ next if dataset.empty?
+
+ import_and_sync_records_for(dataset)
+ end
+ end
+
+ def update_records(data)
+ data.each do |dataset|
+ next if dataset.empty?
+
+ sorted_dataset = map_references(dataset).sort_by { |c| c[:uid] }
+ loaded_records = fetch_from_db(sorted_dataset).order(:uid)
+
+ loaded_records.zip(sorted_dataset).each { |r, v| r.update(v) }
+ end
+ end
+
+ def destroy_records(data)
+ data.each do |dataset|
+ next if dataset.empty?
+
+ dataset.each { |r| @mapping[current_model_name][r[:uid]] = r[:id] }
+ ids = dataset.collect { |d| d[:id] }
+
+ current_model.where(id: ids).destroy_all
+ end
+ end
+
+ def create_hierarchies(data)
+ query = hierarchies_insertion_query(data)
+
+ ActiveRecord::Base.connection.execute(query) if query.present?
+ end
+
+ def update_hierarchies(data)
+ # No op
+ end
+
+ def destroy_hierarchies(data)
+ query = hierarchies_deletion_query(data)
+
+ ActiveRecord::Base.connection.execute(query) if query.present?
+ end
+
+ def compose_diff(local_data, remote_data)
+ diff = {
+ create: [],
+ update: [],
+ destroy: []
+ }
+
+ i = 0
+
+ # Refactor using zip?
+ while i < local_data.length && i < remote_data.length
+ chunk_diff = Diff.perform(local_data[i], remote_data[i], ignore: [[:asset_id, :url], [:id], [:position]])
+
+ diff[:create] << chunk_diff[:create]
+ diff[:update] << chunk_diff[:update]
+ diff[:destroy] << chunk_diff[:destroy]
+
+ i += 1
+ end
+
+ diff[:destroy] << local_data[i..local_data.length] if i != local_data.length
+ diff[:create] << remote_data[i..remote_data.length] if i != remote_data.length
+ diff
+ end
+
+ def compose_hierarchy_diff(local_data, remote_data)
+ {
+ create: remote_data - local_data,
+ destroy: local_data - remote_data
+ }
+ end
+
+ def import_and_sync_records_for(dataset)
+ return if dataset.empty?
+
+ records = map_references(dataset)
+
+ current_model.import records
+
+ reload_mapping_for(records)
+ end
+
+ def map_references(records)
+ records.map do |record|
+ cloned_record = record.clone
+ # map all belongs_to relation
+ associations.each do |association_name, association_model_name|
+ association = "#{association_name}_id".to_sym # :property_id, :linked_record_id etc.
+
+ if association_name == :project
+ cloned_record[association] = context.project.id
+ elsif association_name == :asset && record[:asset_id].present?
+ begin
+ cloned_record[:asset_id] = asset_for(record)&.id
+ rescue StandardError => e
+ Rails.logger.debug "Failed to add #{record}"
+ Rails.logger.debug e
+ end
+ elsif record[association].present?
+ cloned_record[association] = resolve_id(association_model_name, record[association])
+ end
+ end
+
+ cloned_record
+ end
+ end
+
+ def asset_for(record)
+ context.project.assets.create!(
+ name: record[:asset_id][:name],
+ file_remote_url: record[:asset_id][:url]
+ )
+ end
+
+ def associations
+ current_model.reflect_on_all_associations(:belongs_to).each_with_object({}) do |association, obj|
+ obj[association.name] = association.class_name.downcase.to_sym
+ end
+ end
+
+ def resolve_id(model_name, uid)
+ @mapping[model_name][uid]
+ end
+
+ # This method is responsible for mapping saved records's uid to it's actual id after create.
+ # It fetches the recently created records(fetch_from_db) by collecting uid from the raw list of records(before saving),
+ # fetches the records from db and maps.
+ def reload_mapping_for(records)
+ (fetch_from_db(records) || []).each do |record|
+ @mapping[current_model_name][record.uid] = record.id
+ end
+ end
+
+ def hierarchies(data)
+ (data || []).map do |hierarchy|
+ {
+ ancestor_id: resolve_id(current_model_name, hierarchy[:ancestor_id]),
+ descendant_id: resolve_id(current_model_name, hierarchy[:descendant_id]),
+ generations: hierarchy[:generations]
+ }
+ end
+ end
+
+ def initialize_uid_mapping
+ @local_data[current_model_name].each do |dataset|
+ (dataset || []).each do |record|
+ @mapping[current_model_name][record[:uid]] = record[:id]
+ end
+ end
+ end
+
+ # This method fetches the recently created records from the db.
+ # If Project B is being cloned from Project A then a particular record in both the project would have
+ # the same UID. So in order to fetch the records using their UID, there needs to another identifier that
+ # can uniquely identify a record (a composite key). For models having nested elements, there are two possibilities
+ # There could be a direct belongs to, like record_id for Property. But for nested elements, this record_id would be nil
+ # and it would be identified by parent_id.
+ def fetch_from_db(records)
+ return [] if records.empty?
+
+ identifier = primary_identifier_of(records.first)
+ identifier_ids = records.collect { |r| r[identifier] }.uniq
+ records_uid = records.collect { |r| r[:uid] }
+
+ current_model.where(uid: records_uid).where(identifier => identifier_ids)
+ end
+
+ def primary_identifier_of(record)
+ is_child_generation = record[:parent_id].present?
+ primary_identifier = "#{MODEL_PRIMARY_BELONGS_TO[current_model_name]}_id".to_sym
+ secondary_identifier = :parent_id
+
+ is_child_generation ? secondary_identifier : primary_identifier
+ end
+
+ def hierarchies_insertion_query(data)
+ return '' if data.empty?
+
+ records = hierarchies(data)
+ values = records.map { |r| "(#{r.values.join(', ')})" }.join(', ')
+
+ %(
+ INSERT INTO #{current_hierarchy} (ancestor_id, descendant_id, generations)
+ VALUES #{values};
+ )
+ end
+
+ def hierarchies_deletion_query(data)
+ return '' if data.empty?
+
+ records = hierarchies(data)
+ values = records.map { |r| "(#{r.values.join(', ')})" }.join(', ')
+
+ %(
+ DELETE FROM #{current_hierarchy}
+ WHERE (ancestor_id, descendant_id, generations)
+ IN (#{values});
+ )
+ end
+
+ def current_model_name
+ current_model.name.downcase.to_sym
+ end
+
+ def current_hierarchy
+ "#{current_model_name}_hierarchies".to_sym
+ end
+
+ def current_model
+ @model
+ end
+
+ def set_current_model(model) # rubocop:disable Naming/AccessorMethodName: Do not prefix writer method names with set_.
+ @model = model
+ end
+
+ def project_empty?
+ context.project.entities.empty?
+ end
+end
+# rubocop:enable Metrics/ClassLength
diff --git a/api/app/interactors/reject_transfer_request.rb b/api/app/interactors/reject_transfer_request.rb
new file mode 100644
index 0000000..c08deac
--- /dev/null
+++ b/api/app/interactors/reject_transfer_request.rb
@@ -0,0 +1,18 @@
+class RejectTransferRequest
+ include Interactor
+
+ def call
+ ActiveRecord::Base.transaction do
+ verify_transfer_request
+ reject_transfer_request
+ end
+ end
+
+ def verify_transfer_request
+ context.fail!(error: 'Your transfer link has either expired or been canceled.') if !context.team.transfer_requested?
+ end
+
+ def reject_transfer_request
+ context.team.reset_transfer!
+ end
+end
diff --git a/api/app/interactors/revoke_key_pair.rb b/api/app/interactors/revoke_key_pair.rb
new file mode 100644
index 0000000..eadf04d
--- /dev/null
+++ b/api/app/interactors/revoke_key_pair.rb
@@ -0,0 +1,7 @@
+class RevokeKeyPair
+ include Interactor
+
+ def call
+ context.key_pair.revoke!
+ end
+end
diff --git a/api/app/interactors/sort_fields.rb b/api/app/interactors/sort_fields.rb
new file mode 100644
index 0000000..74333df
--- /dev/null
+++ b/api/app/interactors/sort_fields.rb
@@ -0,0 +1,15 @@
+class SortFields
+ include Interactor
+
+ def call
+ ActiveRecord::Base.transaction do
+ update_fields
+ end
+ end
+
+ protected
+
+ def update_fields
+ context.fields.each(&:save!)
+ end
+end
diff --git a/api/app/interactors/sso_callback.rb b/api/app/interactors/sso_callback.rb
new file mode 100644
index 0000000..a8d3b94
--- /dev/null
+++ b/api/app/interactors/sso_callback.rb
@@ -0,0 +1,73 @@
+class SsoCallback
+ include Interactor
+
+ def call
+ verify_payload
+ find_user
+ build_payload
+ end
+
+ private
+
+ def verify_payload
+ url_decoded_sso_payload = CGI.unescape context.params[:sso]
+ raise Exceptions::Unauthorized, 'Invalid Signature' unless valid_signature?(url_decoded_sso_payload)
+
+ sso_payload = Base64.decode64(url_decoded_sso_payload)
+ context.query_params = extract_query_params(sso_payload)
+
+ raise Exceptions::Unauthorized, 'Missing required params' unless required_params_present?
+ raise Exceptions::Unauthorized, 'Request Expired' if nonce_expired?(context.query_params[:nonce])
+ end
+
+ def valid_signature?(sso_payload)
+ ActiveSupport::SecurityUtils.secure_compare(
+ OpenSSL::HMAC.hexdigest(
+ 'SHA256',
+ ENV['CLAY_SSO_SECRET'],
+ sso_payload
+ ),
+ context.params[:sig]
+ )
+ end
+
+ def extract_query_params(sso_payload)
+ query_hash = Rack::Utils.parse_nested_query sso_payload
+ query_hash.transform_keys(&:to_sym)
+ end
+
+ def nonce_expired?(nonce)
+ nonce = AuthNonce.find_by(nonce: nonce)
+ return true if nonce.blank?
+
+ nonce.expired?
+ end
+
+ def required_params_present?
+ context.query_params[:nonce].present? && context.query_params[:email].present?
+ end
+
+ def find_user
+ user = User.find_by(email: context.query_params[:email])
+ raise Exceptions::Unauthorized, 'User not found' unless user
+
+ user.update external_uid: context.query_params[:external_uid].presence
+ context.user = user
+ end
+
+ def build_payload
+ jti = AuthToken.generate_uniq_jti
+ aud = DeviceDetector.new(context.user_agent)&.device_type || 'desktop'
+
+ context.sso_payload = {
+ token: JsonWebToken.encode(
+ user_id: context.user.id,
+ exp: (Time.current + 1.month).to_i,
+ jti: jti,
+ aud: aud
+ )
+ }
+
+ AuthToken.create!(jti: jti, aud: aud, user: context.user)
+ end
+end
diff --git a/api/app/interactors/sso_login.rb b/api/app/interactors/sso_login.rb
new file mode 100644
index 0000000..e06320c
--- /dev/null
+++ b/api/app/interactors/sso_login.rb
@@ -0,0 +1,34 @@
+class SsoLogin
+ include Interactor
+
+ def call
+ generate_nonce
+ generate_sso
+ generate_sig
+ build_payload
+ end
+
+ private
+
+ def generate_nonce
+ nonce = AuthNonce.generate_uniq_nonce
+ context.nonce = AuthNonce.create!(
+ nonce: nonce,
+ expires_at: Time.current + AuthNonce::NONCE_EXPIRY_PERIOD
+ ).nonce
+ end
+
+ def generate_sso
+ context.base64_encoded_sso = Base64.encode64("nonce=#{context.nonce}")
+ context.url_encoded_sso = CGI.escape context.base64_encoded_sso
+ end
+
+ def generate_sig
+ context.sig = OpenSSL::HMAC.hexdigest('SHA256', ENV['CLAY_SSO_SECRET'], context.base64_encoded_sso)
+ end
+
+ def build_payload
+ sso_url = "#{ENV['CLAY_SSO_URL']}?sso=#{context.url_encoded_sso}&sig=#{context.sig}"
+ context.sso_payload = { sso_url: sso_url }
+ end
+end
diff --git a/api/app/interactors/sso_logout.rb b/api/app/interactors/sso_logout.rb
new file mode 100644
index 0000000..dca00ce
--- /dev/null
+++ b/api/app/interactors/sso_logout.rb
@@ -0,0 +1,8 @@
+class SsoLogout
+ include Interactor
+
+ def call
+ jti = context.decoded_token['jti']
+ context.response = context.current_user.auth_tokens.where(jti: jti).delete_all != 0
+ end
+end
diff --git a/api/app/interactors/update_asset.rb b/api/app/interactors/update_asset.rb
new file mode 100644
index 0000000..2cf8068
--- /dev/null
+++ b/api/app/interactors/update_asset.rb
@@ -0,0 +1,7 @@
+class UpdateAsset
+ include Interactor
+
+ def call
+ context.asset.update!(context.params)
+ end
+end
diff --git a/api/app/interactors/update_entity.rb b/api/app/interactors/update_entity.rb
new file mode 100644
index 0000000..e32c81e
--- /dev/null
+++ b/api/app/interactors/update_entity.rb
@@ -0,0 +1,7 @@
+class UpdateEntity
+ include Interactor
+
+ def call
+ context.entity.update!(context.params)
+ end
+end
diff --git a/api/app/interactors/update_field.rb b/api/app/interactors/update_field.rb
new file mode 100644
index 0000000..66d45a5
--- /dev/null
+++ b/api/app/interactors/update_field.rb
@@ -0,0 +1,56 @@
+class UpdateField
+ include Interactor
+
+ def call
+ ActiveRecord::Base.transaction do
+ process
+ update_field
+ end
+ end
+
+ protected
+
+ def process
+ context.field_attributes = process_field(context.field, context.params)
+ end
+
+ def update_field
+ context.field.update!(context.field_attributes)
+ end
+
+ def process_field(field, params)
+ field_params = params.except(:children)
+
+ children_attributes = process_children(field, params)
+ field_params[:id] = field.id if field.present?
+ field_params[:children_attributes] = children_attributes if children_attributes.present?
+
+ field_params
+ end
+
+ def process_children(parent, params)
+ children = params[:children]
+
+ if parent&.array?
+ sub_parent = parent.children.first
+ sub_parent_attributes = {
+ name: "#{params[:name]}_item",
+ label: params[:label].singularize,
+ data_type: params[:element_type],
+ referenced_entity_id: params[:referenced_entity_id]
+ }
+
+ sub_parent_attributes[:id] = sub_parent.id if sub_parent.present?
+ sub_parent_attributes[:children_attributes] = process_children(sub_parent, params) if children.present?
+
+ [sub_parent_attributes]
+ else
+ (children || []).map do |child|
+ field = parent&.children&.find_by(id: child[:id])
+ field = Field.new(child.except(:children)) if field.blank?
+
+ process_field(field, child)
+ end
+ end
+ end
+end
diff --git a/api/app/interactors/update_project.rb b/api/app/interactors/update_project.rb
new file mode 100644
index 0000000..9a4617b
--- /dev/null
+++ b/api/app/interactors/update_project.rb
@@ -0,0 +1,7 @@
+class UpdateProject
+ include Interactor
+
+ def call
+ context.project.update!(context.params)
+ end
+end
diff --git a/api/app/interactors/update_record.rb b/api/app/interactors/update_record.rb
new file mode 100644
index 0000000..4d6512b
--- /dev/null
+++ b/api/app/interactors/update_record.rb
@@ -0,0 +1,109 @@
+class UpdateRecord
+ include Interactor
+
+ def call
+ ActiveRecord::Base.transaction do
+ process
+ update_record
+ end
+ end
+
+ protected
+
+ def process
+ context.properties_attributes = (context.params[:traits] || {}).map do |field_name, value|
+ field = context.record.entity.fields.find_by(name: field_name)
+ property = (context.record.properties || []).find { |p| p.field_id == field.id }
+
+ process_value(field, property, value, context.params[:position])
+ end
+ end
+
+ def update_record
+ context.record.update!(
+ properties_attributes: context.properties_attributes
+ )
+ end
+
+ def process_value(field, property, input_value, position = 0) # rubocop:disable Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
+ attribute = {}
+
+ if field.array?
+ attribute = { children_attributes: process_array(field, property, input_value) }
+ elsif field.key_value?
+ attribute = { children_attributes: process_hash(field, property, input_value) }
+ elsif field.reference?
+ attribute = { linked_record: process_reference(input_value) }
+ elsif field.image? || field.file?
+ attribute = { asset: process_asset(input_value) } if !URL.valid?(input_value)
+ else
+ attribute = { value: input_value }
+ end
+
+ if property.present?
+ attribute[:id] = property.id
+ else
+ attribute[:field] = field
+ end
+
+ attribute[:position] = position if position.present?
+
+ attribute
+ end
+
+ def process_array(field, property, input_values) # rubocop:disable Metrics/CyclomaticComplexity
+ if property.present?
+ removed_records = property.children.ids - input_values.pluck(:id).map(&:to_i)
+ Property.destroy(removed_records) if removed_records.present?
+ end
+
+ return [] if !input_values.is_a? Array
+
+ sub_field = field.children.first
+
+ processed_values = input_values.map do |input_value|
+ next if input_value.except(:position).blank?
+
+ child_property = property.children.find_by(id: input_value[:id]) if property.present?
+
+ value = sub_field.reference? ? input_value : input_value[:value]
+ process_value(sub_field, child_property, value, input_value[:position])
+ end
+
+ processed_values.compact
+ end
+
+ def process_hash(field, property, input_values)
+ return [] if !input_values.is_a? Hash
+
+ input_values.map do |field_name, value|
+ child_field = field.children.find_by(name: field_name)
+ child_property = (property&.children || []).find { |p| p.field_id == child_field.id }
+
+ process_value(child_field, child_property, value)
+ end
+ end
+
+ def process_reference(input_value)
+ linked_record = Record.find_by(id: input_value[:id]) if input_value[:id].present?
+
+ if input_value&.is_a?(Hash) && input_value[:traits].present?
+ if linked_record.present?
+ linked_record = UpdateRecord.call!(params: input_value, record: linked_record).record
+ else
+ entity = Entity.find_by(id: input_value[:entity_id])
+ linked_record = CreateRecord.call!(params: input_value, entity: entity).record
+ end
+ end
+
+ linked_record
+ end
+
+ def process_asset(value)
+ return Asset.find(value[:id]) if value.is_a?(Hash) && value[:id].present?
+
+ return if !value.respond_to?(:content_type)
+
+ context.record.entity.project.assets.create!(name: value.original_filename, file: value)
+ end
+end
diff --git a/api/app/interactors/update_resource.rb b/api/app/interactors/update_resource.rb
new file mode 100644
index 0000000..3db7928
--- /dev/null
+++ b/api/app/interactors/update_resource.rb
@@ -0,0 +1,7 @@
+class UpdateResource
+ include Interactor
+
+ def call
+ context.resource.update!(context.params)
+ end
+end
diff --git a/api/app/interactors/update_team.rb b/api/app/interactors/update_team.rb
new file mode 100644
index 0000000..a338d3f
--- /dev/null
+++ b/api/app/interactors/update_team.rb
@@ -0,0 +1,7 @@
+class UpdateTeam
+ include Interactor
+
+ def call
+ context.team.update!(context.params)
+ end
+end
diff --git a/api/app/interactors/update_team_membership.rb b/api/app/interactors/update_team_membership.rb
new file mode 100644
index 0000000..690a415
--- /dev/null
+++ b/api/app/interactors/update_team_membership.rb
@@ -0,0 +1,9 @@
+class UpdateTeamMembership
+ include Interactor
+
+ def call
+ context.fail!(error: 'Role cannot be owner.') if context.params[:role] == 'owner'
+
+ context.team_membership.update!(context.params)
+ end
+end
diff --git a/api/app/interactors/update_user.rb b/api/app/interactors/update_user.rb
new file mode 100644
index 0000000..687fbc3
--- /dev/null
+++ b/api/app/interactors/update_user.rb
@@ -0,0 +1,7 @@
+class UpdateUser
+ include Interactor
+
+ def call
+ context.user.update!(context.params.slice(:first_name, :last_name, :profile_picture))
+ end
+end
diff --git a/api/app/jobs/application_job.rb b/api/app/jobs/application_job.rb
new file mode 100755
index 0000000..a009ace
--- /dev/null
+++ b/api/app/jobs/application_job.rb
@@ -0,0 +1,2 @@
+class ApplicationJob < ActiveJob::Base
+end
diff --git a/api/app/mailers/application_mailer.rb b/api/app/mailers/application_mailer.rb
new file mode 100755
index 0000000..286b223
--- /dev/null
+++ b/api/app/mailers/application_mailer.rb
@@ -0,0 +1,4 @@
+class ApplicationMailer < ActionMailer::Base
+ default from: 'from@example.com'
+ layout 'mailer'
+end
diff --git a/api/app/models/application_record.rb b/api/app/models/application_record.rb
new file mode 100755
index 0000000..10a4cba
--- /dev/null
+++ b/api/app/models/application_record.rb
@@ -0,0 +1,3 @@
+class ApplicationRecord < ActiveRecord::Base
+ self.abstract_class = true
+end
diff --git a/api/app/models/asset.rb b/api/app/models/asset.rb
new file mode 100644
index 0000000..349c852
--- /dev/null
+++ b/api/app/models/asset.rb
@@ -0,0 +1,35 @@
+class Asset < ApplicationRecord
+ serialize :file_data, JSON
+
+ include AssetFileUploader::Attachment.new(:file)
+
+ belongs_to :project
+
+ has_many :properties, dependent: :nullify
+
+ validates :name, presence: true
+ validates :file, presence: true
+
+ def metadata
+ file&.metadata
+ end
+
+ def resolve_url_for(field)
+ return resolve_original_file if (field.settings && field.settings['versions']).blank?
+
+ versions = { original: file_url(options) }
+ field.settings['versions'].each do |version|
+ versions[version['name']] = file_url(version['name'], options)
+ end
+
+ versions
+ end
+
+ def resolve_original_file
+ file_url(options)
+ end
+
+ def options
+ { public: true }
+ end
+end
diff --git a/api/app/models/auth_nonce.rb b/api/app/models/auth_nonce.rb
new file mode 100644
index 0000000..b6b9720
--- /dev/null
+++ b/api/app/models/auth_nonce.rb
@@ -0,0 +1,18 @@
+class AuthNonce < ApplicationRecord
+ validates :nonce, presence: true, uniqueness: true
+ validates :expires_at, presence: true
+
+ NONCE_EXPIRY_PERIOD = 5.minutes
+
+ def expired?
+ expires_at < Time.current
+ end
+
+ def self.generate_uniq_nonce
+ loop do
+ key = SecureRandom.hex(32)
+ nonce = ActionController::HttpAuthentication::Digest.nonce(key)
+ break nonce unless exists?(nonce: nonce)
+ end
+ end
+end
diff --git a/api/app/models/auth_token.rb b/api/app/models/auth_token.rb
new file mode 100644
index 0000000..0a076e0
--- /dev/null
+++ b/api/app/models/auth_token.rb
@@ -0,0 +1,18 @@
+class AuthToken < ApplicationRecord
+ validates :jti, presence: true, uniqueness: true
+ validates :aud, presence: true
+
+ belongs_to :user
+
+ def self.revoked?(user: nil, jti: nil)
+ !user.auth_tokens.exists?(jti: jti)
+ end
+
+ def self.generate_uniq_jti
+ loop do
+ jti_raw = [ENV['HMAC_SECRET'], Time.current.to_i].join(':').to_s
+ jti = Digest::MD5.hexdigest(jti_raw)
+ break jti unless exists?(jti: jti)
+ end
+ end
+end
diff --git a/api/app/models/concerns/nested_fetchable.rb b/api/app/models/concerns/nested_fetchable.rb
new file mode 100644
index 0000000..7b1f3b6
--- /dev/null
+++ b/api/app/models/concerns/nested_fetchable.rb
@@ -0,0 +1,23 @@
+module NestedFetchable
+ extend ActiveSupport::Concern
+
+ class_methods do
+ def has_nested(table) # rubocop:disable Naming/PredicateName: Rename has_nested to nested?.
+ method_name = "nested_#{table}"
+ model = table.to_s.classify.constantize
+ hierarchy_table = "#{table.to_s.singularize}_hierarchies"
+ parent_foreign_key = "#{table_name.singularize}_id"
+
+ define_method(method_name) do
+ query = <<-SQL.strip_heredoc
+ SELECT #{table}.* FROM #{table}
+ INNER JOIN #{hierarchy_table} ON #{table}.id = #{hierarchy_table}.descendant_id
+ WHERE #{hierarchy_table}.ancestor_id IN (SELECT id FROM #{table} WHERE #{parent_foreign_key} = #{id})
+ ORDER BY created_at;
+ SQL
+
+ model.find_by_sql(query)
+ end
+ end
+ end
+end
diff --git a/api/app/models/concerns/tokenable.rb b/api/app/models/concerns/tokenable.rb
new file mode 100755
index 0000000..1163b8b
--- /dev/null
+++ b/api/app/models/concerns/tokenable.rb
@@ -0,0 +1,39 @@
+module Tokenable
+ extend ActiveSupport::Concern
+
+ class_methods do
+ def has_token(prefix = nil, digest_attribute: nil, token_attribute: nil) # rubocop:disable Naming/PredicateName, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
+ prefix = "#{prefix}_" if prefix
+ digest_attribute ||= "#{prefix}digest".to_sym
+ token_attribute ||= "#{prefix}token".to_sym
+
+ define_singleton_method("generate_#{digest_attribute}") do
+ loop do
+ # We use base58 instead of hex, keeping the behavior similar to has_secure_token.
+ # However, this could be an issue if we ever shift from PostgreSQL to MySQL:
+ # https://github.com/rails/rails/issues/20133
+ digest = SecureRandom.base58(24)
+ break digest unless exists?(digest_attribute => digest)
+ end
+ end
+
+ define_singleton_method("find_with_#{token_attribute}") do |token|
+ id, digest = Token.decode(token)
+ return if !id || !digest
+
+ record = find_by(id: id)
+ return if !record
+
+ record_digest = record.send(digest_attribute)
+ return if !record_digest || !ActiveSupport::SecurityUtils.secure_compare(record_digest, digest)
+
+ record
+ end
+
+ define_method(token_attribute) do
+ digest = send(digest_attribute)
+ Token.encode(id, digest) if digest.present?
+ end
+ end
+ end
+end
diff --git a/api/app/models/concerns/transferable.rb b/api/app/models/concerns/transferable.rb
new file mode 100644
index 0000000..41dd221
--- /dev/null
+++ b/api/app/models/concerns/transferable.rb
@@ -0,0 +1,35 @@
+module Transferable
+ extend ActiveSupport::Concern
+
+ include Tokenable
+
+ TRANSFER_EXPIRY_PERIOD = 3.days
+
+ included do
+ has_token :transfer
+ end
+
+ def request_transfer_to!(user)
+ self.transfer_digest = self.class.generate_transfer_digest
+ self.transfer_generated_at = Time.zone.now
+ self.transfer_owner = user
+
+ save!
+ end
+
+ def reset_transfer!
+ self.transfer_digest = nil
+ self.transfer_generated_at = nil
+ self.transfer_owner = nil
+
+ save!
+ end
+
+ def transfer_expired?
+ transfer_generated_at.present? && (Time.zone.now - transfer_generated_at > TRANSFER_EXPIRY_PERIOD)
+ end
+
+ def transfer_requested?
+ transfer_owner.present? && !transfer_expired?
+ end
+end
diff --git a/api/app/models/concerns/uid.rb b/api/app/models/concerns/uid.rb
new file mode 100644
index 0000000..2502dde
--- /dev/null
+++ b/api/app/models/concerns/uid.rb
@@ -0,0 +1,15 @@
+module Uid
+ extend ActiveSupport::Concern
+
+ included do
+ validates :uid, presence: true
+
+ after_initialize :set_uid, if: :new_record?
+ end
+
+ protected
+
+ def set_uid
+ self.uid ||= SecureRandom.uuid
+ end
+end
diff --git a/api/app/models/entity.rb b/api/app/models/entity.rb
new file mode 100644
index 0000000..de5a974
--- /dev/null
+++ b/api/app/models/entity.rb
@@ -0,0 +1,17 @@
+class Entity < ApplicationRecord
+ include NestedFetchable
+ include Uid
+
+ belongs_to :parent, class_name: 'Entity', optional: true
+ belongs_to :project
+
+ has_many :fields, dependent: :destroy
+ has_many :relationships, dependent: :destroy
+ has_many :records, dependent: :destroy
+ has_many :referenced_fields, class_name: 'Field', foreign_key: :referenced_entity_id, inverse_of: :referenced_entity, dependent: :nullify
+
+ validates :label, presence: true
+ validates :name, presence: true
+
+ has_nested :fields
+end
diff --git a/api/app/models/export.rb b/api/app/models/export.rb
new file mode 100644
index 0000000..e1d6bb0
--- /dev/null
+++ b/api/app/models/export.rb
@@ -0,0 +1,11 @@
+class Export < ApplicationRecord
+ serialize :file_data, JSON
+
+ include ExportFileUploader::Attachment.new(:file)
+
+ enum status: { pending: 0, completed: 1, failed: 2 }
+
+ belongs_to :project
+
+ validates :status, presence: true
+end
diff --git a/api/app/models/field.rb b/api/app/models/field.rb
new file mode 100644
index 0000000..b49c869
--- /dev/null
+++ b/api/app/models/field.rb
@@ -0,0 +1,35 @@
+class Field < ApplicationRecord
+ include Uid
+
+ serialize :validations, JSON
+ serialize :settings, JSON
+
+ has_closure_tree order: 'position', numeric_order: true, dependent: :destroy
+
+ enum data_type: { single_line_text: 0, multiple_line_text: 1, number: 2, decimal: 3, boolean: 4, image: 5, key_value: 6, reference: 7, array: 8, color: 9, file: 10 }
+ enum element_type: Field.data_types.except(:array), _prefix: :element
+
+ belongs_to :entity, optional: true
+ belongs_to :referenced_entity, class_name: 'Entity', optional: true
+
+ has_many :properties, dependent: :destroy
+ has_many :relationships, dependent: :destroy
+
+ validates :data_type, presence: true
+ validates :label, presence: true
+ validates :name, presence: true
+ validates :position, presence: true
+
+ after_initialize :set_defaults
+
+ accepts_nested_attributes_for :children
+
+ protected
+
+ def set_defaults
+ return unless new_record?
+
+ self.validations ||= {}
+ self.settings ||= {}
+ end
+end
diff --git a/api/app/models/key_pair.rb b/api/app/models/key_pair.rb
new file mode 100644
index 0000000..66bf413
--- /dev/null
+++ b/api/app/models/key_pair.rb
@@ -0,0 +1,31 @@
+class KeyPair < ApplicationRecord
+ PUBLIC_KEY_LENGTH = 24
+
+ belongs_to :project
+
+ validates :public_key, presence: true, uniqueness: true
+
+ before_validation :set_keys, on: :create
+
+ def self.generate_public_key
+ generate_key(:public_key, PUBLIC_KEY_LENGTH)
+ end
+
+ private_class_method def self.generate_key(key_attribute, key_length)
+ loop do
+ digest = SecureRandom.base58(key_length)
+ break digest unless exists?(key_attribute => digest)
+ end
+ end
+
+ def revoke!
+ self.expires_at ||= Time.current
+ save!
+ end
+
+ protected
+
+ def set_keys
+ self.public_key ||= self.class.generate_public_key
+ end
+end
diff --git a/api/app/models/locale.rb b/api/app/models/locale.rb
new file mode 100644
index 0000000..d73b970
--- /dev/null
+++ b/api/app/models/locale.rb
@@ -0,0 +1,5 @@
+class Locale < ApplicationRecord
+ validates :language, presence: true
+
+ belongs_to :project
+end
diff --git a/api/app/models/project.rb b/api/app/models/project.rb
new file mode 100644
index 0000000..bf96b1d
--- /dev/null
+++ b/api/app/models/project.rb
@@ -0,0 +1,16 @@
+class Project < ApplicationRecord
+ include Uid
+
+ belongs_to :team
+
+ has_many :assets, dependent: :destroy
+ has_many :entities, dependent: :destroy
+ has_many :exports, dependent: :destroy
+ has_many :key_pairs, dependent: :destroy
+ has_many :locales, dependent: :destroy
+ has_many :restores, dependent: :destroy
+ has_many :resources, dependent: :destroy
+
+ validates :name, presence: true
+ validates :uid, uniqueness: true
+end
diff --git a/api/app/models/property.rb b/api/app/models/property.rb
new file mode 100644
index 0000000..b68f225
--- /dev/null
+++ b/api/app/models/property.rb
@@ -0,0 +1,19 @@
+class Property < ApplicationRecord
+ include Uid
+
+ has_closure_tree order: 'position', numeric_order: true, dependent: :destroy
+
+ belongs_to :record, optional: true
+ belongs_to :field
+ belongs_to :asset, optional: true
+ belongs_to :linked_record, class_name: 'Record', optional: true
+
+ accepts_nested_attributes_for :children
+
+ # To clone record and its properties
+ amoeba do
+ enable
+ set uid: SecureRandom.uuid
+ clone [:parent]
+ end
+end
diff --git a/api/app/models/record.rb b/api/app/models/record.rb
new file mode 100644
index 0000000..11d81fb
--- /dev/null
+++ b/api/app/models/record.rb
@@ -0,0 +1,40 @@
+class Record < ApplicationRecord
+ include NestedFetchable
+ include Uid
+
+ belongs_to :entity
+
+ has_many :properties, dependent: :destroy
+ has_many :linked_properties, class_name: 'Property', foreign_key: :linked_record_id, inverse_of: :linked_record, dependent: :destroy
+
+ has_nested :properties
+
+ accepts_nested_attributes_for :properties
+
+ after_destroy -> { Property.rebuild! }
+
+ # To clone record and its properties
+ amoeba do
+ enable
+ set uid: SecureRandom.uuid
+ end
+
+ def convert_to_json(key_type = nil)
+ key_type ||= :camelize
+
+ json = slice(:id, :created_at, :updated_at)
+ json = json.merge(entity_name: entity.name)
+
+ entity_fields = entity.fields
+ record_properties = properties.includes(:field).where(field: entity_fields.pluck(:id))
+
+ entity_fields.each do |field|
+ property = record_properties.find { |p| p.field_id == field.id }
+
+ json[field.name] = property&.data
+ end
+
+ json.deep_transform_keys! { |k| k.camelize(:lower) } if key_type.try(:to_sym) == :camelize
+ json
+ end
+end
diff --git a/api/app/models/relationship.rb b/api/app/models/relationship.rb
new file mode 100644
index 0000000..02f031c
--- /dev/null
+++ b/api/app/models/relationship.rb
@@ -0,0 +1,7 @@
+class Relationship < ApplicationRecord
+ belongs_to :entity
+ belongs_to :field
+
+ belongs_to :linked_entity, class_name: 'Entity', foreign_key: :linked_entity_id, inverse_of: :relationships
+ belongs_to :linked_field, class_name: 'Field', foreign_key: :linked_field_id, inverse_of: :relationships
+end
diff --git a/api/app/models/resource.rb b/api/app/models/resource.rb
new file mode 100644
index 0000000..fa5ab4e
--- /dev/null
+++ b/api/app/models/resource.rb
@@ -0,0 +1,18 @@
+class Resource < ApplicationRecord
+ serialize :file_data, JSON
+
+ include ResourceFileUploader::Attachment.new(:file)
+
+ belongs_to :project
+
+ validates :name, presence: true, uniqueness: { scope: :project }
+ validates :file, presence: true
+
+ def resolve_file
+ file_url(options)
+ end
+
+ def options
+ { public: true }
+ end
+end
diff --git a/api/app/models/restore.rb b/api/app/models/restore.rb
new file mode 100644
index 0000000..129d471
--- /dev/null
+++ b/api/app/models/restore.rb
@@ -0,0 +1,8 @@
+class Restore < ApplicationRecord
+ enum status: { pending: 0, completed: 1, failed: 2 }
+
+ belongs_to :project
+
+ validates :status, presence: true
+ validates :url, presence: true
+end
diff --git a/api/app/models/team.rb b/api/app/models/team.rb
new file mode 100644
index 0000000..f2ea381
--- /dev/null
+++ b/api/app/models/team.rb
@@ -0,0 +1,12 @@
+class Team < ApplicationRecord
+ include Transferable
+
+ validates :name, presence: true
+
+ belongs_to :transfer_owner, class_name: 'User', inverse_of: :transferable_teams, optional: true
+
+ has_many :projects, dependent: :destroy
+ has_many :team_memberships, dependent: :destroy
+
+ has_many :users, through: :team_memberships
+end
diff --git a/api/app/models/team_membership.rb b/api/app/models/team_membership.rb
new file mode 100644
index 0000000..66a7339
--- /dev/null
+++ b/api/app/models/team_membership.rb
@@ -0,0 +1,29 @@
+class TeamMembership < ApplicationRecord
+ enum role: { editor: 0, developer: 1, manager: 2, owner: 3 }
+
+ belongs_to :team
+ belongs_to :user
+
+ validates :role, presence: true
+ validates :user, uniqueness: { scope: :team_id }
+
+ validate :only_one_owner_role_per_team
+
+ def atleast?(allowed_role)
+ allowed_role_level = self.class.roles[allowed_role.to_s]
+ raise "Unknown role: #{allowed_role}" if allowed_role_level.blank?
+
+ current_role_level = self.class.roles[role.to_s]
+
+ current_role_level >= allowed_role_level
+ end
+
+ protected
+
+ def only_one_owner_role_per_team
+ return if !role_changed?(to: 'owner') || team.blank? || user.blank?
+
+ owner_membership = self.class.find_by(team: team, role: :owner)
+ errors.add(:role, 'cannot be owner as the team already has one') if owner_membership.present? && owner_membership.user != user
+ end
+end
diff --git a/api/app/models/user.rb b/api/app/models/user.rb
new file mode 100644
index 0000000..ee5c37c
--- /dev/null
+++ b/api/app/models/user.rb
@@ -0,0 +1,15 @@
+class User < ApplicationRecord
+ serialize :profile_picture_data, JSON
+
+ include ProfilePictureUploader::Attachment.new(:profile_picture)
+
+ validates :email, presence: true, email_format: true, uniqueness: { case_sensitive: false }
+ validates :first_name, presence: true
+ validates :last_name, presence: true
+
+ has_many :auth_tokens, dependent: :destroy
+ has_many :team_memberships, dependent: :destroy
+ has_many :transferable_teams, class_name: 'Team', foreign_key: :transfer_owner, inverse_of: :transfer_owner, dependent: :nullify
+
+ has_many :teams, through: :team_memberships
+end
diff --git a/api/app/policies/application_policy.rb b/api/app/policies/application_policy.rb
new file mode 100755
index 0000000..b91c7f5
--- /dev/null
+++ b/api/app/policies/application_policy.rb
@@ -0,0 +1,53 @@
+class ApplicationPolicy
+ attr_reader :user, :record
+
+ def initialize(user, record)
+ @user = user
+ @record = record
+ end
+
+ def index?
+ false
+ end
+
+ def show?
+ scope.where(id: record.id).exists?
+ end
+
+ def create?
+ false
+ end
+
+ def new?
+ create?
+ end
+
+ def update?
+ false
+ end
+
+ def edit?
+ update?
+ end
+
+ def destroy?
+ false
+ end
+
+ def scope
+ Pundit.policy_scope!(user, record.class)
+ end
+
+ class Scope
+ attr_reader :user, :scope
+
+ def initialize(user, scope)
+ @user = user
+ @scope = scope
+ end
+
+ def resolve
+ scope
+ end
+ end
+end
diff --git a/api/app/policies/asset_policy.rb b/api/app/policies/asset_policy.rb
new file mode 100644
index 0000000..edf0bb9
--- /dev/null
+++ b/api/app/policies/asset_policy.rb
@@ -0,0 +1,15 @@
+class AssetPolicy < BaseMemberPolicy
+ def update?
+ team_membership&.atleast?(:manager)
+ end
+
+ def destroy?
+ team_membership&.atleast?(:manager)
+ end
+
+ protected
+
+ def team
+ @team ||= record.project.team
+ end
+end
diff --git a/api/app/policies/base_member_policy.rb b/api/app/policies/base_member_policy.rb
new file mode 100644
index 0000000..2b355f2
--- /dev/null
+++ b/api/app/policies/base_member_policy.rb
@@ -0,0 +1,13 @@
+class BaseMemberPolicy < ApplicationPolicy
+ protected
+
+ def team
+ raise 'must be overridden'
+ end
+
+ def team_membership
+ return if user.blank? || team.blank?
+
+ @team_membership ||= TeamMembership.find_by(user: user, team: team)
+ end
+end
diff --git a/api/app/policies/entity_policy.rb b/api/app/policies/entity_policy.rb
new file mode 100644
index 0000000..fa490a0
--- /dev/null
+++ b/api/app/policies/entity_policy.rb
@@ -0,0 +1,29 @@
+class EntityPolicy < BaseMemberPolicy
+ def view?
+ team_membership&.atleast?(:developer)
+ end
+
+ def update?
+ team_membership&.atleast?(:developer)
+ end
+
+ def destroy?
+ team_membership&.atleast?(:developer)
+ end
+
+ def create_field?
+ team_membership&.atleast?(:developer)
+ end
+
+ def create_record?
+ team_membership&.atleast?(:editor)
+ end
+
+ alias clone_record? create_record?
+
+ protected
+
+ def team
+ @team ||= record.project.team
+ end
+end
diff --git a/api/app/policies/field_policy.rb b/api/app/policies/field_policy.rb
new file mode 100644
index 0000000..fc4635e
--- /dev/null
+++ b/api/app/policies/field_policy.rb
@@ -0,0 +1,19 @@
+class FieldPolicy < BaseMemberPolicy
+ def view?
+ team_membership&.atleast?(:developer)
+ end
+
+ def update?
+ team_membership&.atleast?(:developer)
+ end
+
+ def destroy?
+ team_membership&.atleast?(:developer)
+ end
+
+ protected
+
+ def team
+ @team ||= record.entity.project.team
+ end
+end
diff --git a/api/app/policies/key_pair_policy.rb b/api/app/policies/key_pair_policy.rb
new file mode 100644
index 0000000..23a43f4
--- /dev/null
+++ b/api/app/policies/key_pair_policy.rb
@@ -0,0 +1,11 @@
+class KeyPairPolicy < BaseMemberPolicy
+ def revoke?
+ team_membership&.atleast?(:developer)
+ end
+
+ protected
+
+ def team
+ @team ||= record.project.team
+ end
+end
diff --git a/api/app/policies/project_policy.rb b/api/app/policies/project_policy.rb
new file mode 100644
index 0000000..ec31169
--- /dev/null
+++ b/api/app/policies/project_policy.rb
@@ -0,0 +1,63 @@
+class ProjectPolicy < BaseMemberPolicy
+ def view?
+ team_membership.present?
+ end
+
+ def view_assets?
+ view?
+ end
+
+ def view_resources?
+ view?
+ end
+
+ def view_exports?
+ view?
+ end
+
+ def view_imports?
+ view?
+ end
+
+ def export_project?
+ team_membership&.atleast?(:manager)
+ end
+
+ def import_project?
+ team_membership&.atleast?(:manager)
+ end
+
+ def view_key_pairs?
+ team_membership&.atleast?(:developer)
+ end
+
+ def view_entities?
+ team_membership&.atleast?(:developer)
+ end
+
+ def create_asset?
+ team_membership&.atleast?(:editor)
+ end
+
+ def create_resource?
+ team_membership&.atleast?(:editor)
+ end
+
+ def create_key_pair?
+ team_membership&.atleast?(:developer)
+ end
+
+ def create_entity?
+ team_membership&.atleast?(:developer)
+ end
+
+ def update?
+ team_membership&.atleast?(:developer)
+ end
+
+ protected
+
+ def team
+ @team ||= record.team
+ end
+end
diff --git a/api/app/policies/record_policy.rb b/api/app/policies/record_policy.rb
new file mode 100644
index 0000000..49b949b
--- /dev/null
+++ b/api/app/policies/record_policy.rb
@@ -0,0 +1,19 @@
+class RecordPolicy < BaseMemberPolicy
+ def view?
+ team_membership&.atleast?(:editor)
+ end
+
+ def update?
+ team_membership&.atleast?(:editor)
+ end
+
+ def destroy?
+ team_membership&.atleast?(:editor)
+ end
+
+ protected
+
+ def team
+ @team ||= record.entity.project.team
+ end
+end
diff --git a/api/app/policies/resource_policy.rb b/api/app/policies/resource_policy.rb
new file mode 100644
index 0000000..61ca1d3
--- /dev/null
+++ b/api/app/policies/resource_policy.rb
@@ -0,0 +1,15 @@
+class ResourcePolicy < BaseMemberPolicy
+ def update?
+ team_membership&.atleast?(:manager)
+ end
+
+ def destroy?
+ team_membership&.atleast?(:manager)
+ end
+
+ protected
+
+ def team
+ @team ||= record.project.team
+ end
+end
diff --git a/api/app/policies/team_membership_policy.rb b/api/app/policies/team_membership_policy.rb
new file mode 100644
index 0000000..743bc6c
--- /dev/null
+++ b/api/app/policies/team_membership_policy.rb
@@ -0,0 +1,17 @@
+class TeamMembershipPolicy < BaseMemberPolicy
+ def update?
+ return false if record.owner?
+
+ team_membership&.atleast?(:manager)
+ end
+
+ def destroy?
+ update?
+ end
+
+ protected
+
+ def team
+ @team ||= record.team
+ end
+end
diff --git a/api/app/policies/team_policy.rb b/api/app/policies/team_policy.rb
new file mode 100644
index 0000000..60b0fef
--- /dev/null
+++ b/api/app/policies/team_policy.rb
@@ -0,0 +1,51 @@
+class TeamPolicy < BaseMemberPolicy
+ def view?
+ team_membership.present?
+ end
+
+ def view_team_memberships?
+ view?
+ end
+
+ def view_projects?
+ view?
+ end
+
+ def update?
+ team_membership&.atleast?(:owner)
+ end
+
+ def destroy?
+ update?
+ end
+
+ def create_team_membership?
+ team_membership&.atleast?(:manager)
+ end
+
+ def create_project?
+ team_membership&.atleast?(:manager)
+ end
+
+ def create_transfer_request?
+ team_membership&.atleast?(:owner)
+ end
+
+ def cancel_transfer_request?
+ team_membership&.atleast?(:owner)
+ end
+
+ def accept_transfer_request?
+ team_membership.present? && user.id == team.transfer_owner_id
+ end
+
+ def reject_transfer_request?
+ accept_transfer_request?
+ end
+
+ protected
+
+ def team
+ @team ||= record
+ end
+end
diff --git a/api/app/publishers/base_publisher.rb b/api/app/publishers/base_publisher.rb
new file mode 100644
index 0000000..17eee31
--- /dev/null
+++ b/api/app/publishers/base_publisher.rb
@@ -0,0 +1,21 @@
+class BasePublisher
+ def self.publish(message = {})
+ exchange.publish(message.to_json, routing_key: @queue)
+ end
+
+ def self.channel
+ @channel ||= connection.create_channel
+ end
+
+ def self.connection
+ @connection ||= Bunny.new(RabbitMq.connection_url).tap(&:start)
+ end
+
+ def self.exchange
+ @exchange ||= channel.default_exchange
+ end
+
+ def self.to_queue(name)
+ @queue = name
+ end
+end
diff --git a/api/app/publishers/export_project_publisher.rb b/api/app/publishers/export_project_publisher.rb
new file mode 100644
index 0000000..3e17332
--- /dev/null
+++ b/api/app/publishers/export_project_publisher.rb
@@ -0,0 +1,3 @@
+class ExportProjectPublisher < BasePublisher
+ to_queue 'export_project'
+end
diff --git a/api/app/publishers/import_project_publisher.rb b/api/app/publishers/import_project_publisher.rb
new file mode 100644
index 0000000..c11b6bc
--- /dev/null
+++ b/api/app/publishers/import_project_publisher.rb
@@ -0,0 +1,3 @@
+class ImportProjectPublisher < BasePublisher
+ to_queue 'import_project'
+end
diff --git a/api/app/uploaders/asset_file_uploader.rb b/api/app/uploaders/asset_file_uploader.rb
new file mode 100644
index 0000000..0f9ca47
--- /dev/null
+++ b/api/app/uploaders/asset_file_uploader.rb
@@ -0,0 +1,25 @@
+class AssetFileUploader < BaseUploader
+ plugin :add_metadata
+ plugin :remote_url, max_size: 20 * 1024 * 1024
+ plugin :upload_options,
+ cache: ->(io, **) { { cache_control: "max-age=#{1.year}, s-maxage=#{1.year}" } },
+ store: ->(io, **) { { acl: 'public-read' } }
+
+ add_metadata do |io, context|
+ metadata = {}
+ mime_type = Shrine.determine_mime_type(io)
+ metadata[:extension] = Shrine.infer_extension(mime_type)
+ metadata
+ end
+
+ Attacher.derivatives_processor do |original|
+ versions = {}
+ pipeline = ImageProcessing::MiniMagick.source(original)
+ settings = context[:record].properties.first&.field&.settings || {}
+ (settings['versions'] || []).each do |version|
+ versions[version['name'].to_sym] = pipeline.resize_to_fill!(version['width'], version['height'], gravity: 'Center')
+ end
+
+ versions
+ end
+end
diff --git a/api/app/uploaders/base_uploader.rb b/api/app/uploaders/base_uploader.rb
new file mode 100644
index 0000000..8f62d4d
--- /dev/null
+++ b/api/app/uploaders/base_uploader.rb
@@ -0,0 +1,3 @@
+class BaseUploader < Shrine
+ plugin :pretty_location, namespace: '_'
+end
diff --git a/api/app/uploaders/export_file_uploader.rb b/api/app/uploaders/export_file_uploader.rb
new file mode 100644
index 0000000..b744b35
--- /dev/null
+++ b/api/app/uploaders/export_file_uploader.rb
@@ -0,0 +1,4 @@
+class ExportFileUploader < BaseUploader
+ plugin :remote_url, max_size: 20 * 1024 * 1024
+ plugin :upload_options, store: ->(io, **) { { acl: 'public-read' } }
+end
diff --git a/api/app/uploaders/image_uploader.rb b/api/app/uploaders/image_uploader.rb
new file mode 100644
index 0000000..60358ad
--- /dev/null
+++ b/api/app/uploaders/image_uploader.rb
@@ -0,0 +1,2 @@
+class ImageUploader < BaseUploader
+end
diff --git a/api/app/uploaders/profile_picture_uploader.rb b/api/app/uploaders/profile_picture_uploader.rb
new file mode 100644
index 0000000..ac07bab
--- /dev/null
+++ b/api/app/uploaders/profile_picture_uploader.rb
@@ -0,0 +1,16 @@
+class ProfilePictureUploader < ImageUploader
+ plugin :validation_helpers
+
+ Attacher.validate do
+ validate_mime_type_inclusion %w[image/jpeg image/png]
+ end
+
+ Attacher.derivatives_processor do |original|
+ versions = {}
+ pipeline = ImageProcessing::MiniMagick.source(original)
+ versions[:normal] = pipeline.resize_to_fill!(200, 200, gravity: 'Center')
+ versions[:thumbnail] = pipeline.resize_to_fill!(100, 100, gravity: 'Center')
+
+ versions
+ end
+end
diff --git a/api/app/uploaders/resource_file_uploader.rb b/api/app/uploaders/resource_file_uploader.rb
new file mode 100644
index 0000000..1830178
--- /dev/null
+++ b/api/app/uploaders/resource_file_uploader.rb
@@ -0,0 +1,21 @@
+class ResourceFileUploader < BaseUploader
+ BASE_FOLDER = 'resources'.freeze
+
+ plugin :add_metadata
+ plugin :remote_url, max_size: 20 * 1024 * 1024
+ plugin :upload_options,
+ cache: ->(io, **) { { cache_control: "max-age=#{1.year}, s-maxage=#{1.year}" } },
+ store: ->(io, context) { { acl: 'public-read' } }
+
+ add_metadata do |io, context|
+ metadata = {}
+ mime_type = Shrine.determine_mime_type(io)
+ metadata[:extension] = Shrine.infer_extension(mime_type)
+
+ metadata
+ end
+
+ def generate_location(io, record: nil, **context) # rubocop:disable Lint/UnusedMethodArgument
+ "#{BASE_FOLDER}/#{record.project.uid}/file/#{record.name}"
+ end
+end
diff --git a/api/app/validators/email_format_validator.rb b/api/app/validators/email_format_validator.rb
new file mode 100755
index 0000000..2e86b44
--- /dev/null
+++ b/api/app/validators/email_format_validator.rb
@@ -0,0 +1,7 @@
+class EmailFormatValidator < ActiveModel::EachValidator
+ def validate_each(record, attribute, value)
+ return if value.blank? || MailAddress.valid_email?(value)
+
+ record.errors[attribute] << (options[:message] || 'is not a valid email')
+ end
+end
diff --git a/api/app/views/layouts/application.html.erb b/api/app/views/layouts/application.html.erb
new file mode 100755
index 0000000..cfc4c82
--- /dev/null
+++ b/api/app/views/layouts/application.html.erb
@@ -0,0 +1,15 @@
+
+
+
+ Rails API
+ <%= csrf_meta_tags %>
+ <%= csp_meta_tag %>
+
+ <%= stylesheet_link_tag 'application', media: 'all', 'data-turbolinks-track': 'reload' %>
+ <%= javascript_include_tag 'application', 'data-turbolinks-track': 'reload' %>
+
+
+
+ <%= yield %>
+
+
diff --git a/api/app/views/layouts/mailer.html.erb b/api/app/views/layouts/mailer.html.erb
new file mode 100755
index 0000000..cbd34d2
--- /dev/null
+++ b/api/app/views/layouts/mailer.html.erb
@@ -0,0 +1,13 @@
+
+
+
+
+
+
+
+
+ <%= yield %>
+
+
diff --git a/api/app/views/layouts/mailer.text.erb b/api/app/views/layouts/mailer.text.erb
new file mode 100755
index 0000000..37f0bdd
--- /dev/null
+++ b/api/app/views/layouts/mailer.text.erb
@@ -0,0 +1 @@
+<%= yield %>
diff --git a/api/app/workers/export_project_worker.rb b/api/app/workers/export_project_worker.rb
new file mode 100644
index 0000000..b791daa
--- /dev/null
+++ b/api/app/workers/export_project_worker.rb
@@ -0,0 +1,26 @@
+class ExportProjectWorker
+ include Sneakers::Worker
+ from_queue 'export_project'
+
+ def work(msg)
+ obj = JSON.parse(msg, symbolize_names: true)
+
+ project = Project.find(obj[:project_id])
+ export = Export.find(obj[:export_id])
+
+ return ack! if export.completed?
+
+ begin
+ PerformExport.call!(project: project, export: export)
+ rescue StandardError => e
+ export.failed!
+
+ logger.info "Failed to export #{msg}"
+ logger.info e
+
+ return reject!
+ end
+
+ ack!
+ end
+end
diff --git a/api/app/workers/import_project_worker.rb b/api/app/workers/import_project_worker.rb
new file mode 100644
index 0000000..a190960
--- /dev/null
+++ b/api/app/workers/import_project_worker.rb
@@ -0,0 +1,26 @@
+class ImportProjectWorker
+ include Sneakers::Worker
+ from_queue 'import_project'
+
+ def work(msg)
+ obj = JSON.parse(msg, symbolize_names: true)
+
+ project = Project.find(obj[:project_id])
+ restore = Restore.find(obj[:restore_id])
+
+ return ack! if restore.completed?
+
+ begin
+ PerformRestore.call!(project: project, restore: restore)
+ rescue StandardError => e
+ restore.failed!
+
+ logger.info "Failed to import #{msg}"
+ logger.info e
+
+ return reject!
+ end
+
+ ack!
+ end
+end
diff --git a/api/bin/bundle b/api/bin/bundle
new file mode 100755
index 0000000..f19acf5
--- /dev/null
+++ b/api/bin/bundle
@@ -0,0 +1,3 @@
+#!/usr/bin/env ruby
+ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../Gemfile', __dir__)
+load Gem.bin_path('bundler', 'bundle')
diff --git a/api/bin/rails b/api/bin/rails
new file mode 100755
index 0000000..5badb2f
--- /dev/null
+++ b/api/bin/rails
@@ -0,0 +1,9 @@
+#!/usr/bin/env ruby
+begin
+ load File.expand_path('../spring', __FILE__)
+rescue LoadError => e
+ raise unless e.message.include?('spring')
+end
+APP_PATH = File.expand_path('../config/application', __dir__)
+require_relative '../config/boot'
+require 'rails/commands'
diff --git a/api/bin/rake b/api/bin/rake
new file mode 100755
index 0000000..d87d5f5
--- /dev/null
+++ b/api/bin/rake
@@ -0,0 +1,9 @@
+#!/usr/bin/env ruby
+begin
+ load File.expand_path('../spring', __FILE__)
+rescue LoadError => e
+ raise unless e.message.include?('spring')
+end
+require_relative '../config/boot'
+require 'rake'
+Rake.application.run
diff --git a/api/bin/scripts/clay b/api/bin/scripts/clay
new file mode 100755
index 0000000..edfc5b3
--- /dev/null
+++ b/api/bin/scripts/clay
@@ -0,0 +1,3 @@
+#!/usr/bin/env bash
+
+docker-compose exec clay bash
\ No newline at end of file
diff --git a/api/bin/scripts/setup-db-dev b/api/bin/scripts/setup-db-dev
new file mode 100755
index 0000000..13e84e6
--- /dev/null
+++ b/api/bin/scripts/setup-db-dev
@@ -0,0 +1,12 @@
+#!/usr/bin/env bash
+
+docker-compose exec pgdb psql -U postgres -c 'CREATE DATABASE clay_cms_development'
+docker-compose exec pgdb psql -U postgres -c 'CREATE DATABASE clay_cms_test'
+docker-compose exec pgdb psql -U postgres -c 'CREATE DATABASE clay_cms_staging'
+docker-compose exec pgdb psql -U postgres -c 'CREATE DATABASE clay_cms_production'
+
+docker-compose exec pgdb psql -U postgres -c "CREATE USER claycool with ENCRYPTED PASSWORD 'ABC12abc'"
+docker-compose exec pgdb psql -U postgres -c 'GRANT ALL PRIVILEGES ON DATABASE clay_cms_development to claycool'
+docker-compose exec pgdb psql -U postgres -c 'GRANT ALL PRIVILEGES ON DATABASE clay_cms_test to claycool'
+docker-compose exec pgdb psql -U postgres -c 'GRANT ALL PRIVILEGES ON DATABASE clay_cms_staging to claycool'
+docker-compose exec pgdb psql -U postgres -c 'GRANT ALL PRIVILEGES ON DATABASE clay_cms_production to claycool'
\ No newline at end of file
diff --git a/api/bin/scripts/start-app b/api/bin/scripts/start-app
new file mode 100755
index 0000000..8edfd85
--- /dev/null
+++ b/api/bin/scripts/start-app
@@ -0,0 +1,10 @@
+#!/usr/bin/env bash
+
+echo "removing old pid's .."
+rm -f tmp/pids/server.pid
+
+echo "checking bundle dependencies .."
+bundle check || bundle install
+
+echo "boooting up .."
+bundle exec rails s -p 3000 -b 0.0.0.0 -e development
\ No newline at end of file
diff --git a/api/bin/setup b/api/bin/setup
new file mode 100755
index 0000000..94fd4d7
--- /dev/null
+++ b/api/bin/setup
@@ -0,0 +1,36 @@
+#!/usr/bin/env ruby
+require 'fileutils'
+include FileUtils
+
+# path to your application root.
+APP_ROOT = File.expand_path('..', __dir__)
+
+def system!(*args)
+ system(*args) || abort("\n== Command #{args} failed ==")
+end
+
+chdir APP_ROOT do
+ # This script is a starting point to setup your application.
+ # Add necessary setup steps to this file.
+
+ puts '== Installing dependencies =='
+ system! 'gem install bundler --conservative'
+ system('bundle check') || system!('bundle install')
+
+ # Install JavaScript dependencies if using Yarn
+ # system('bin/yarn')
+
+ # puts "\n== Copying sample files =="
+ # unless File.exist?('config/database.yml')
+ # cp 'config/database.yml.sample', 'config/database.yml'
+ # end
+
+ puts "\n== Preparing database =="
+ system! 'bin/rails db:setup'
+
+ puts "\n== Removing old logs and tempfiles =="
+ system! 'bin/rails log:clear tmp:clear'
+
+ puts "\n== Restarting application server =="
+ system! 'bin/rails restart'
+end
diff --git a/api/bin/spring b/api/bin/spring
new file mode 100755
index 0000000..d89ee49
--- /dev/null
+++ b/api/bin/spring
@@ -0,0 +1,17 @@
+#!/usr/bin/env ruby
+
+# This file loads Spring without using Bundler, in order to be fast.
+# It gets overwritten when you run the `spring binstub` command.
+
+unless defined?(Spring)
+ require 'rubygems'
+ require 'bundler'
+
+ lockfile = Bundler::LockfileParser.new(Bundler.default_lockfile.read)
+ spring = lockfile.specs.detect { |spec| spec.name == 'spring' }
+ if spring
+ Gem.use_paths Gem.dir, Bundler.bundle_path.to_s, *Gem.path
+ gem 'spring', spring.version
+ require 'spring/binstub'
+ end
+end
diff --git a/api/bin/update b/api/bin/update
new file mode 100755
index 0000000..58bfaed
--- /dev/null
+++ b/api/bin/update
@@ -0,0 +1,31 @@
+#!/usr/bin/env ruby
+require 'fileutils'
+include FileUtils
+
+# path to your application root.
+APP_ROOT = File.expand_path('..', __dir__)
+
+def system!(*args)
+ system(*args) || abort("\n== Command #{args} failed ==")
+end
+
+chdir APP_ROOT do
+ # This script is a way to update your development environment automatically.
+ # Add necessary update steps to this file.
+
+ puts '== Installing dependencies =='
+ system! 'gem install bundler --conservative'
+ system('bundle check') || system!('bundle install')
+
+ # Install JavaScript dependencies if using Yarn
+ # system('bin/yarn')
+
+ puts "\n== Updating database =="
+ system! 'bin/rails db:migrate'
+
+ puts "\n== Removing old logs and tempfiles =="
+ system! 'bin/rails log:clear tmp:clear'
+
+ puts "\n== Restarting application server =="
+ system! 'bin/rails restart'
+end
diff --git a/api/bin/yarn b/api/bin/yarn
new file mode 100755
index 0000000..460dd56
--- /dev/null
+++ b/api/bin/yarn
@@ -0,0 +1,11 @@
+#!/usr/bin/env ruby
+APP_ROOT = File.expand_path('..', __dir__)
+Dir.chdir(APP_ROOT) do
+ begin
+ exec "yarnpkg", *ARGV
+ rescue Errno::ENOENT
+ $stderr.puts "Yarn executable was not detected in the system."
+ $stderr.puts "Download Yarn at https://yarnpkg.com/en/docs/install"
+ exit 1
+ end
+end
diff --git a/api/config.ru b/api/config.ru
new file mode 100755
index 0000000..62f79ab
--- /dev/null
+++ b/api/config.ru
@@ -0,0 +1,18 @@
+# This file is used by Rack-based servers to start the application.
+
+require_relative 'config/environment'
+
+require 'rack/cors'
+
+use Rack::Deflater
+
+use Rack::Cors do
+ allow do
+ origins '*'
+ resource '*', headers: :any, methods: [:get, :post, :put, :patch, :delete, :options, :head]
+ end
+end
+
+use Rack::CanonicalHost, ENV['API_HOST'], force_ssl: ENV['FORCE_SSL'].present?
+
+run Rails.application
diff --git a/api/config/application.rb b/api/config/application.rb
new file mode 100755
index 0000000..1d28cf2
--- /dev/null
+++ b/api/config/application.rb
@@ -0,0 +1,41 @@
+require_relative 'boot'
+
+require 'rails/all'
+# # Pick the frameworks you want:
+# require 'active_model/railtie'
+# require 'active_job/railtie'
+# require 'active_record/railtie'
+# require 'active_storage/engine'
+# require 'action_controller/railtie'
+# require 'action_mailer/railtie'
+# require 'action_view/railtie'
+# require 'action_cable/engine'
+require 'rails_semantic_logger'
+# # require 'sprockets/railtie'
+
+# Require the gems listed in Gemfile, including any gems
+# you've limited to :test, :development, or :production.
+Bundler.require(*Rails.groups)
+
+module ClayCMS
+ class Application < Rails::Application
+ # Initialize configuration defaults for originally generated Rails version.
+ config.load_defaults 5.2
+
+ # Settings in config/environments/* take precedence over those specified here.
+ # Application configuration can go into files in config/initializers
+ # -- all .rb files in that directory are automatically loaded after loading
+ # the framework and any gems in your application.
+
+ # Only loads a smaller set of middleware suitable for API only apps.
+ # Middleware like session, flash, cookies can be added back manually.
+ # Skip views, helpers and assets when generating a new resource.
+ # config.api_only = true
+
+ config.eager_load_paths += Dir["#{config.root}/app/graphql/resolvers/**/"]
+ config.eager_load_paths += Dir["#{config.root}/app/graphql/mutators/**/"]
+ config.eager_load_paths += Dir["#{config.root}/app/graphql/functions/**/"]
+
+ config.middleware.delete ApolloUploadServer::Middleware
+ end
+end
diff --git a/api/config/boot.rb b/api/config/boot.rb
new file mode 100755
index 0000000..b9e460c
--- /dev/null
+++ b/api/config/boot.rb
@@ -0,0 +1,4 @@
+ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../Gemfile', __dir__)
+
+require 'bundler/setup' # Set up gems listed in the Gemfile.
+require 'bootsnap/setup' # Speed up boot time by caching expensive operations.
diff --git a/api/config/cable.yml b/api/config/cable.yml
new file mode 100755
index 0000000..6f41f6b
--- /dev/null
+++ b/api/config/cable.yml
@@ -0,0 +1,10 @@
+development:
+ adapter: async
+
+test:
+ adapter: async
+
+production:
+ adapter: redis
+ url: <%= ENV.fetch("REDIS_URL") { "redis://localhost:6379/1" } %>
+ channel_prefix: claycms_api_production
diff --git a/api/config/credentials.yml.enc b/api/config/credentials.yml.enc
new file mode 100644
index 0000000..e81bf12
--- /dev/null
+++ b/api/config/credentials.yml.enc
@@ -0,0 +1 @@
+Dqm6IU8g63ID29DjktlDK4xPgoLRHEbEIMnDiqo3EJG8R6a3W93u0eglCXXLf+DDZ93rD1Wzwi99nwdKf43QPzBBb4PY0qbKZLiqOBkFPtOQFhT2uwXI6OJKP6IfnB8hb+rx7T0GkGalTh5wwD3KpyAthpgdSo/P2EuWnlUg7f2182ulVus9IuG/RjPesH5XdpMTJ3aezutpDv+gAjG4IXPx7W6sScjI3fcrfCDBBfUBHzQkF9janT/UjLFXzJIK3wbJ2G1ccFUMV/rqfoh8SU1zH7zcREuYbO0Kya7QWpsVzS3GDFQ3Dr2jpBq9857CTiEQgYmX32npdKheC6MZSRbS6Q0Uil04q1eEohmRUnjFjV+lPI1wmVe4oAltdcAO0wac3OFNuVkZqpV8gZ/esp0z9Fym9pA7ysLwVsuaGviAC7Ap2VA3lO8aMb0aCpaL4g4DnZaEfD2+StCTHcEhGq2CAegKeZjsvISlB/pV4KVYy6aELwVs5jT2UbfKUywJ6gMPZ43cS/09ztZ5PRYNRTyJbt/upoNit6K4mISihLlKwUGyt8ohiFKJJXnnaMhDf6+jNcB8/Y8LSgp9f1yN6vBULncvkRMBM9T4LXb2YsfQZtqOKu9gm7MGUaglqrkwpekFsd08tKt2GCgE7OfICYdoWFVXAP9TFmLdP0RMaoWCBvFeFKGjl3F3u+i8ErUDqa19HdvqyRLBmaP3yE7YSapsvC9GW/GaPD6LQbiRQ1vq2/kqPdNMnBaNWR/CnzCSYhdjIkQipbxOeNGHMz5jtnRbOjWxfmM/9YhFS4lvmbToIAiY/Webt6cHJe6WHpPZdNZbX+AkYF1XN8t9bke1vdx2PXQZ5yM40zdUkGVnKEFkrrYyqK6C4/FzlJVbz2LTL83VrJZ4qqS8Vm+ZC0I9R0fKPOYyWBh0GjRAo0oAy8Rjteuu7tHIDcG1dk1WrDM7w09H/Oc7SMwDlPuwhKD1twD68NSU2JWfylhBn21p4tGYpQ57zEYLbuPwzQS/66EK+mLXFUqXfrpppMBGQ8H1OBLipx1nDYBpwOA9vN6mFx+EAj/88uS5rW8Oponkaf5uX8mtYtIsDRZRyFEn14GacuoYjTdjlV2Hmr7SoZyOJ3A4+rmEGknTlTRrqmz3v7FrFUunaO0o7zq/QAwr9M9FKCT5DXzO2NMXv3kqdTMyEFvhJkdbeosBaepS5jDWv6rc7U4PkyFTQVBY9eWSelxRMLamypFQymLxkAzy6JkWL9/iy1OLFqs33ySO7KaxS+bJOTxbw1aJB0BQp+CvCo7YZrHnxLmE/OSv/W4bH4fFfdPhAY8oEuDWxowQaogAnb9EKI36dW7FOu16DswTp+4ZtYSaX0jTDWgoqEdiJpjAhI6S2lZCKPLuoCj8OJGC0qiDy2xMLVH9Duim44dOR62o10pRJE2KEs0vxIjdqsNI4+N2+MyEhs85dfM658D32fTQAmDmBXt3bdcNkYjlN0eoUAiARqVxtzNUsAymKOrwOj1EnEZFVJ1jyxcMuIgsZ1u71kr928S/--948l/4DijcbXWS2/--CK9U9u5Gdi+n67//Y66chA==
\ No newline at end of file
diff --git a/api/config/database.yml.example b/api/config/database.yml.example
new file mode 100755
index 0000000..1f86a04
--- /dev/null
+++ b/api/config/database.yml.example
@@ -0,0 +1,23 @@
+base: &base
+ adapter: postgresql
+ encoding: utf8
+ username:
+ password:
+ host: localhost
+ pool: <%= ENV.fetch('RAILS_MAX_THREADS') { 5 } %>
+
+development:
+ <<: *base
+ database: clay_cms_development
+
+test:
+ <<: *base
+ database: clay_cms_test
+
+staging:
+ <<: *base
+ database: clay_cms_staging
+
+production:
+ <<: *base
+ database: clay_cms_production
diff --git a/api/config/environment.rb b/api/config/environment.rb
new file mode 100755
index 0000000..426333b
--- /dev/null
+++ b/api/config/environment.rb
@@ -0,0 +1,5 @@
+# Load the Rails application.
+require_relative 'application'
+
+# Initialize the Rails application.
+Rails.application.initialize!
diff --git a/api/config/environments/development.rb b/api/config/environments/development.rb
new file mode 100755
index 0000000..46d4e19
--- /dev/null
+++ b/api/config/environments/development.rb
@@ -0,0 +1,57 @@
+Rails.application.configure do
+ # Settings specified here will take precedence over those in config/application.rb.
+
+ # In the development environment your application's code is reloaded on
+ # every request. This slows down response time but is perfect for development
+ # since you don't have to restart the web server when you make code changes.
+ config.cache_classes = false
+
+ # Do not eager load code on boot.
+ config.eager_load = false
+
+ # Show full error reports.
+ config.consider_all_requests_local = true
+
+ # Enable/disable caching. By default caching is disabled.
+ # Run rails dev:cache to toggle caching.
+ if Rails.root.join('tmp', 'caching-dev.txt').exist?
+ config.action_controller.perform_caching = true
+
+ config.cache_store = :memory_store
+ config.public_file_server.headers = {
+ 'Cache-Control' => "public, max-age=#{2.days.to_i}"
+ }
+ else
+ config.action_controller.perform_caching = false
+
+ config.cache_store = :null_store
+ end
+
+ # Store uploaded files on the local file system (see config/storage.yml for options)
+ config.active_storage.service = :local
+
+ # Don't care if the mailer can't send.
+ config.action_mailer.raise_delivery_errors = false
+
+ config.action_mailer.perform_caching = false
+
+ # Print deprecation notices to the Rails logger.
+ config.active_support.deprecation = :log
+
+ # Raise an error on page load if there are pending migrations.
+ config.active_record.migration_error = :page_load
+
+ # Highlight code that triggered database queries in logs.
+ config.active_record.verbose_query_logs = true
+
+ # Raises error for missing translations
+ # config.action_view.raise_on_missing_translations = true
+
+ # Use an evented file watcher to asynchronously detect changes in source code,
+ # routes, locales, etc. This feature depends on the listen gem.
+ # ActiveSupport::EventedFileUpdateChecker || ActiveSupport::FileUpdateChecker
+ config.file_watcher = ActiveSupport::FileUpdateChecker
+
+ # Enable stdout logger
+ config.logger = Logger.new(STDOUT)
+end
diff --git a/api/config/environments/production.rb b/api/config/environments/production.rb
new file mode 100755
index 0000000..047d7b6
--- /dev/null
+++ b/api/config/environments/production.rb
@@ -0,0 +1,116 @@
+Rails.application.configure do
+ # Settings specified here will take precedence over those in config/application.rb.
+
+ # Code is not reloaded between requests.
+ config.cache_classes = true
+
+ # Eager load code on boot. This eager loads most of Rails and
+ # your application in memory, allowing both threaded web servers
+ # and those relying on copy on write to perform better.
+ # Rake tasks automatically ignore this option for performance.
+ config.eager_load = true
+
+ # Full error reports are disabled and caching is turned on.
+ config.consider_all_requests_local = false
+ config.action_controller.perform_caching = true
+
+ # Ensures that a master key has been made available in either ENV["RAILS_MASTER_KEY"]
+ # or in config/master.key. This key is used to decrypt credentials (and other encrypted files).
+ # config.require_master_key = true
+
+ # Disable serving static files from the `/public` folder by default since
+ # Apache or NGINX already handles this.
+ config.public_file_server.enabled = ENV['RAILS_SERVE_STATIC_FILES'].present?
+
+ # Compress JavaScripts and CSS.
+ # config.assets.js_compressor = :uglifier
+ # config.assets.css_compressor = :sass
+
+ # Do not fallback to assets pipeline if a precompiled asset is missed.
+ config.assets.compile = false
+
+ # `config.assets.precompile` and `config.assets.version` have moved to config/initializers/assets.rb
+
+ # Enable serving of images, stylesheets, and JavaScripts from an asset server.
+ # config.action_controller.asset_host = 'http://assets.example.com'
+
+ # Specifies the header that your server uses for sending files.
+ # config.action_dispatch.x_sendfile_header = 'X-Sendfile' # for Apache
+ # config.action_dispatch.x_sendfile_header = 'X-Accel-Redirect' # for NGINX
+
+ # Store uploaded files on the local file system (see config/storage.yml for options)
+ config.active_storage.service = :local
+
+ # Mount Action Cable outside main process or domain
+ # config.action_cable.mount_path = nil
+ # config.action_cable.url = 'wss://example.com/cable'
+ # config.action_cable.allowed_request_origins = [ 'http://example.com', /http:\/\/example.*/ ]
+
+ # Force all access to the app over SSL, use Strict-Transport-Security, and use secure cookies.
+ # config.force_ssl = true
+
+ # Use the lowest log level to ensure availability of diagnostic information
+ # when problems arise.
+ config.log_level = :info
+
+ # Prepend all log lines with the following tags.
+ config.log_tags = [:request_id]
+
+ # Use a different cache store in production.
+ # config.cache_store = :mem_cache_store
+
+ # Use a real queuing backend for Active Job (and separate queues per environment)
+ # config.active_job.queue_adapter = :resque
+ # config.active_job.queue_name_prefix = "claycms_api_#{Rails.env}"
+
+ config.action_mailer.perform_caching = false
+
+ # Ignore bad email addresses and do not raise email delivery errors.
+ # Set this to true and configure the email server for immediate delivery to raise delivery errors.
+ # config.action_mailer.raise_delivery_errors = false
+
+ # Enable locale fallbacks for I18n (makes lookups for any locale fall back to
+ # the I18n.default_locale when a translation cannot be found).
+ config.i18n.fallbacks = true
+
+ # Send deprecation notices to registered listeners.
+ config.active_support.deprecation = :notify
+
+ # Use default logging formatter so that PID and timestamp are not suppressed.
+ config.log_formatter = ::Logger::Formatter.new
+
+ # Use a different logger for distributed setups.
+ # require 'syslog/logger'
+ # config.logger = ActiveSupport::TaggedLogging.new(Syslog::Logger.new 'app-name')
+
+ if ENV['RAILS_LOG_TO_STDOUT'].present?
+ logger = ActiveSupport::Logger.new(STDOUT)
+ logger.formatter = config.log_formatter
+ config.logger = ActiveSupport::TaggedLogging.new(logger)
+ end
+
+ config.log_level = ENV['LOG_LEVEL'].upcase if ENV['LOG_LEVEL'].present?
+
+ if ENV['KAFKA_ENABLE'].present?
+ max_log_level = :trace
+ config.rails_semantic_logger.format = GbLogger::Formatter.new
+ config.rails_semantic_logger.add_file_appender = false
+ config.semantic_logger.backtrace_level = nil
+ config.semantic_logger.add_appender(
+ io: STDOUT,
+ level: max_log_level,
+ formatter: config.rails_semantic_logger.format
+ )
+ config.semantic_logger.add_appender(
+ appender: :kafka,
+ seed_brokers: [ENV['KAFKA_DNS']],
+ connect_timeout: ENV['KAFKA_TIMEOUT_MS'].to_i,
+ topic: 'log',
+ level: max_log_level,
+ formatter: config.rails_semantic_logger.format
+ )
+ end
+
+ # Do not dump schema after migrations.
+ config.active_record.dump_schema_after_migration = false
+end
diff --git a/api/config/environments/staging.rb b/api/config/environments/staging.rb
new file mode 100644
index 0000000..047d7b6
--- /dev/null
+++ b/api/config/environments/staging.rb
@@ -0,0 +1,116 @@
+Rails.application.configure do
+ # Settings specified here will take precedence over those in config/application.rb.
+
+ # Code is not reloaded between requests.
+ config.cache_classes = true
+
+ # Eager load code on boot. This eager loads most of Rails and
+ # your application in memory, allowing both threaded web servers
+ # and those relying on copy on write to perform better.
+ # Rake tasks automatically ignore this option for performance.
+ config.eager_load = true
+
+ # Full error reports are disabled and caching is turned on.
+ config.consider_all_requests_local = false
+ config.action_controller.perform_caching = true
+
+ # Ensures that a master key has been made available in either ENV["RAILS_MASTER_KEY"]
+ # or in config/master.key. This key is used to decrypt credentials (and other encrypted files).
+ # config.require_master_key = true
+
+ # Disable serving static files from the `/public` folder by default since
+ # Apache or NGINX already handles this.
+ config.public_file_server.enabled = ENV['RAILS_SERVE_STATIC_FILES'].present?
+
+ # Compress JavaScripts and CSS.
+ # config.assets.js_compressor = :uglifier
+ # config.assets.css_compressor = :sass
+
+ # Do not fallback to assets pipeline if a precompiled asset is missed.
+ config.assets.compile = false
+
+ # `config.assets.precompile` and `config.assets.version` have moved to config/initializers/assets.rb
+
+ # Enable serving of images, stylesheets, and JavaScripts from an asset server.
+ # config.action_controller.asset_host = 'http://assets.example.com'
+
+ # Specifies the header that your server uses for sending files.
+ # config.action_dispatch.x_sendfile_header = 'X-Sendfile' # for Apache
+ # config.action_dispatch.x_sendfile_header = 'X-Accel-Redirect' # for NGINX
+
+ # Store uploaded files on the local file system (see config/storage.yml for options)
+ config.active_storage.service = :local
+
+ # Mount Action Cable outside main process or domain
+ # config.action_cable.mount_path = nil
+ # config.action_cable.url = 'wss://example.com/cable'
+ # config.action_cable.allowed_request_origins = [ 'http://example.com', /http:\/\/example.*/ ]
+
+ # Force all access to the app over SSL, use Strict-Transport-Security, and use secure cookies.
+ # config.force_ssl = true
+
+ # Use the lowest log level to ensure availability of diagnostic information
+ # when problems arise.
+ config.log_level = :info
+
+ # Prepend all log lines with the following tags.
+ config.log_tags = [:request_id]
+
+ # Use a different cache store in production.
+ # config.cache_store = :mem_cache_store
+
+ # Use a real queuing backend for Active Job (and separate queues per environment)
+ # config.active_job.queue_adapter = :resque
+ # config.active_job.queue_name_prefix = "claycms_api_#{Rails.env}"
+
+ config.action_mailer.perform_caching = false
+
+ # Ignore bad email addresses and do not raise email delivery errors.
+ # Set this to true and configure the email server for immediate delivery to raise delivery errors.
+ # config.action_mailer.raise_delivery_errors = false
+
+ # Enable locale fallbacks for I18n (makes lookups for any locale fall back to
+ # the I18n.default_locale when a translation cannot be found).
+ config.i18n.fallbacks = true
+
+ # Send deprecation notices to registered listeners.
+ config.active_support.deprecation = :notify
+
+ # Use default logging formatter so that PID and timestamp are not suppressed.
+ config.log_formatter = ::Logger::Formatter.new
+
+ # Use a different logger for distributed setups.
+ # require 'syslog/logger'
+ # config.logger = ActiveSupport::TaggedLogging.new(Syslog::Logger.new 'app-name')
+
+ if ENV['RAILS_LOG_TO_STDOUT'].present?
+ logger = ActiveSupport::Logger.new(STDOUT)
+ logger.formatter = config.log_formatter
+ config.logger = ActiveSupport::TaggedLogging.new(logger)
+ end
+
+ config.log_level = ENV['LOG_LEVEL'].upcase if ENV['LOG_LEVEL'].present?
+
+ if ENV['KAFKA_ENABLE'].present?
+ max_log_level = :trace
+ config.rails_semantic_logger.format = GbLogger::Formatter.new
+ config.rails_semantic_logger.add_file_appender = false
+ config.semantic_logger.backtrace_level = nil
+ config.semantic_logger.add_appender(
+ io: STDOUT,
+ level: max_log_level,
+ formatter: config.rails_semantic_logger.format
+ )
+ config.semantic_logger.add_appender(
+ appender: :kafka,
+ seed_brokers: [ENV['KAFKA_DNS']],
+ connect_timeout: ENV['KAFKA_TIMEOUT_MS'].to_i,
+ topic: 'log',
+ level: max_log_level,
+ formatter: config.rails_semantic_logger.format
+ )
+ end
+
+ # Do not dump schema after migrations.
+ config.active_record.dump_schema_after_migration = false
+end
diff --git a/api/config/environments/test.rb b/api/config/environments/test.rb
new file mode 100755
index 0000000..0a38fd3
--- /dev/null
+++ b/api/config/environments/test.rb
@@ -0,0 +1,46 @@
+Rails.application.configure do
+ # Settings specified here will take precedence over those in config/application.rb.
+
+ # The test environment is used exclusively to run your application's
+ # test suite. You never need to work with it otherwise. Remember that
+ # your test database is "scratch space" for the test suite and is wiped
+ # and recreated between test runs. Don't rely on the data there!
+ config.cache_classes = true
+
+ # Do not eager load code on boot. This avoids loading your whole application
+ # just for the purpose of running a single test. If you are using a tool that
+ # preloads Rails for running tests, you may have to set it to true.
+ config.eager_load = false
+
+ # Configure public file server for tests with Cache-Control for performance.
+ config.public_file_server.enabled = true
+ config.public_file_server.headers = {
+ 'Cache-Control' => "public, max-age=#{1.hour.to_i}"
+ }
+
+ # Show full error reports and disable caching.
+ config.consider_all_requests_local = true
+ config.action_controller.perform_caching = false
+
+ # Raise exceptions instead of rendering exception templates.
+ config.action_dispatch.show_exceptions = false
+
+ # Disable request forgery protection in test environment.
+ config.action_controller.allow_forgery_protection = false
+
+ # Store uploaded files on the local file system in a temporary directory
+ config.active_storage.service = :test
+
+ config.action_mailer.perform_caching = false
+
+ # Tell Action Mailer not to deliver emails to the real world.
+ # The :test delivery method accumulates sent emails in the
+ # ActionMailer::Base.deliveries array.
+ config.action_mailer.delivery_method = :test
+
+ # Print deprecation notices to the stderr.
+ config.active_support.deprecation = :stderr
+
+ # Raises error for missing translations
+ # config.action_view.raise_on_missing_translations = true
+end
diff --git a/api/config/initializers/00_require.rb b/api/config/initializers/00_require.rb
new file mode 100755
index 0000000..1d61cb9
--- /dev/null
+++ b/api/config/initializers/00_require.rb
@@ -0,0 +1,11 @@
+require Rails.root.join('lib', 'app_url')
+require Rails.root.join('lib', 'credentials')
+require Rails.root.join('lib', 'exceptions')
+require Rails.root.join('lib', 'mail_address')
+require Rails.root.join('lib', 'token')
+require Rails.root.join('lib', 'url')
+require Rails.root.join('lib', 'uid_generator')
+require Rails.root.join('lib', 'rabbit_mq')
+require Rails.root.join('lib', 'diff')
+require Rails.root.join('lib', 'record_mapper')
+require Rails.root.join('lib', 'json_web_token')
diff --git a/api/config/initializers/application_controller_renderer.rb b/api/config/initializers/application_controller_renderer.rb
new file mode 100755
index 0000000..89d2efa
--- /dev/null
+++ b/api/config/initializers/application_controller_renderer.rb
@@ -0,0 +1,8 @@
+# Be sure to restart your server when you modify this file.
+
+# ActiveSupport::Reloader.to_prepare do
+# ApplicationController.renderer.defaults.merge!(
+# http_host: 'example.org',
+# https: false
+# )
+# end
diff --git a/api/config/initializers/backtrace_silencers.rb b/api/config/initializers/backtrace_silencers.rb
new file mode 100755
index 0000000..59385cd
--- /dev/null
+++ b/api/config/initializers/backtrace_silencers.rb
@@ -0,0 +1,7 @@
+# Be sure to restart your server when you modify this file.
+
+# You can add backtrace silencers for libraries that you're using but don't wish to see in your backtraces.
+# Rails.backtrace_cleaner.add_silencer { |line| line =~ /my_noisy_library/ }
+
+# You can also remove all the silencers if you're trying to debug a problem that might stem from framework code.
+# Rails.backtrace_cleaner.remove_silencers!
diff --git a/api/config/initializers/bullet.rb b/api/config/initializers/bullet.rb
new file mode 100644
index 0000000..f2db991
--- /dev/null
+++ b/api/config/initializers/bullet.rb
@@ -0,0 +1,4 @@
+if Rails.env.development?
+ Bullet.enable = true
+ Bullet.bullet_logger = true
+end
diff --git a/api/config/initializers/content_security_policy.rb b/api/config/initializers/content_security_policy.rb
new file mode 100755
index 0000000..d3bcaa5
--- /dev/null
+++ b/api/config/initializers/content_security_policy.rb
@@ -0,0 +1,25 @@
+# Be sure to restart your server when you modify this file.
+
+# Define an application-wide content security policy
+# For further information see the following documentation
+# https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy
+
+# Rails.application.config.content_security_policy do |policy|
+# policy.default_src :self, :https
+# policy.font_src :self, :https, :data
+# policy.img_src :self, :https, :data
+# policy.object_src :none
+# policy.script_src :self, :https
+# policy.style_src :self, :https
+
+# # Specify URI for violation reports
+# # policy.report_uri "/csp-violation-report-endpoint"
+# end
+
+# If you are using UJS then enable automatic nonce generation
+# Rails.application.config.content_security_policy_nonce_generator = -> request { SecureRandom.base64(16) }
+
+# Report CSP violations to a specified URI
+# For further information see the following documentation:
+# https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy-Report-Only
+# Rails.application.config.content_security_policy_report_only = true
diff --git a/api/config/initializers/cookies_serializer.rb b/api/config/initializers/cookies_serializer.rb
new file mode 100755
index 0000000..5a6a32d
--- /dev/null
+++ b/api/config/initializers/cookies_serializer.rb
@@ -0,0 +1,5 @@
+# Be sure to restart your server when you modify this file.
+
+# Specify a serializer for the signed and encrypted cookie jars.
+# Valid options are :json, :marshal, and :hybrid.
+Rails.application.config.action_dispatch.cookies_serializer = :json
diff --git a/api/config/initializers/filter_parameter_logging.rb b/api/config/initializers/filter_parameter_logging.rb
new file mode 100755
index 0000000..4a994e1
--- /dev/null
+++ b/api/config/initializers/filter_parameter_logging.rb
@@ -0,0 +1,4 @@
+# Be sure to restart your server when you modify this file.
+
+# Configure sensitive parameters which will be filtered from the log file.
+Rails.application.config.filter_parameters += [:password]
diff --git a/api/config/initializers/graphql_rails_logger.rb b/api/config/initializers/graphql_rails_logger.rb
new file mode 100755
index 0000000..4a9681b
--- /dev/null
+++ b/api/config/initializers/graphql_rails_logger.rb
@@ -0,0 +1,5 @@
+if Rails.env.development?
+ GraphQL::RailsLogger.configure do |config|
+ config.skip_introspection_query = true
+ end
+end
diff --git a/api/config/initializers/inflections.rb b/api/config/initializers/inflections.rb
new file mode 100755
index 0000000..ac033bf
--- /dev/null
+++ b/api/config/initializers/inflections.rb
@@ -0,0 +1,16 @@
+# Be sure to restart your server when you modify this file.
+
+# Add new inflection rules using the following format. Inflections
+# are locale specific, and you may define rules for as many different
+# locales as you wish. All of these examples are active by default:
+# ActiveSupport::Inflector.inflections(:en) do |inflect|
+# inflect.plural /^(ox)$/i, '\1en'
+# inflect.singular /^(ox)en/i, '\1'
+# inflect.irregular 'person', 'people'
+# inflect.uncountable %w( fish sheep )
+# end
+
+# These inflection rules are supported but not enabled by default:
+# ActiveSupport::Inflector.inflections(:en) do |inflect|
+# inflect.acronym 'RESTful'
+# end
diff --git a/api/config/initializers/mime_types.rb b/api/config/initializers/mime_types.rb
new file mode 100755
index 0000000..dc18996
--- /dev/null
+++ b/api/config/initializers/mime_types.rb
@@ -0,0 +1,4 @@
+# Be sure to restart your server when you modify this file.
+
+# Add new mime types for use in respond_to blocks:
+# Mime::Type.register "text/richtext", :rtf
diff --git a/api/config/initializers/raven.rb b/api/config/initializers/raven.rb
new file mode 100644
index 0000000..808bc50
--- /dev/null
+++ b/api/config/initializers/raven.rb
@@ -0,0 +1,8 @@
+Raven.configure do |config|
+ config.silence_ready = true
+ config.dsn = Credentials.get(:sentry_dsn)
+ config.environments = %w[staging production]
+ config.excluded_exceptions = [
+ Interactor::Failure
+ ]
+end
diff --git a/api/config/initializers/shrine.rb b/api/config/initializers/shrine.rb
new file mode 100644
index 0000000..37cbff5
--- /dev/null
+++ b/api/config/initializers/shrine.rb
@@ -0,0 +1,49 @@
+url_options = {
+ host: ENV['ASSET_HOST']
+}
+
+if Rails.env.production? || Rails.env.staging?
+ require 'shrine/storage/s3'
+
+ s3_options = {
+ access_key_id: Credentials.get(:aws_access_key_id),
+ secret_access_key: Credentials.get(:aws_secret_access_key),
+ region: Credentials.get(:s3_region),
+ bucket: Credentials.get(:s3_bucket)
+ }
+
+ Shrine.storages = {
+ cache: Shrine::Storage::S3.new(prefix: 'cache', **s3_options),
+ store: Shrine::Storage::S3.new(**s3_options)
+ }
+elsif Rails.env.test?
+ require 'shrine/storage/memory'
+
+ Shrine.storages = {
+ cache: Shrine::Storage::Memory.new,
+ store: Shrine::Storage::Memory.new
+ }
+else
+ require 'shrine/storage/file_system'
+
+ Shrine.storages = {
+ cache: Shrine::Storage::FileSystem.new('public', prefix: 'uploads/cache'),
+ store: Shrine::Storage::FileSystem.new('public', prefix: 'uploads')
+ }
+
+ Shrine.logger = Rails.logger
+ Shrine.plugin :instrumentation
+end
+
+Shrine.plugin :activerecord
+Shrine.plugin :determine_mime_type
+Shrine.plugin :infer_extension, force: true
+Shrine.plugin :derivatives, versions_compatibility: true
+Shrine.plugin :url_options, cache: url_options, store: url_options
+
+class Shrine::Attacher
+ def promote(*)
+ create_derivatives
+ super
+ end
+end
diff --git a/api/config/initializers/sneakers.rb b/api/config/initializers/sneakers.rb
new file mode 100644
index 0000000..3f08b3b
--- /dev/null
+++ b/api/config/initializers/sneakers.rb
@@ -0,0 +1,11 @@
+require 'sneakers/metrics/logging_metrics'
+
+Sneakers.configure({
+ amqp: RabbitMq.connection_url,
+ metrics: Sneakers::Metrics::LoggingMetrics.new,
+ env: ENV['RAILS_ENV'],
+ threads: 1,
+ worker: 1
+})
+
+Sneakers.logger.level = Logger::INFO # the default DEBUG is too noisy
diff --git a/api/config/initializers/wrap_parameters.rb b/api/config/initializers/wrap_parameters.rb
new file mode 100755
index 0000000..bbfc396
--- /dev/null
+++ b/api/config/initializers/wrap_parameters.rb
@@ -0,0 +1,14 @@
+# Be sure to restart your server when you modify this file.
+
+# This file contains settings for ActionController::ParamsWrapper which
+# is enabled by default.
+
+# Enable parameter wrapping for JSON. You can disable this by setting :format to an empty array.
+ActiveSupport.on_load(:action_controller) do
+ wrap_parameters format: [:json]
+end
+
+# To enable root element in JSON for ActiveRecord objects.
+# ActiveSupport.on_load(:active_record) do
+# self.include_root_in_json = true
+# end
diff --git a/api/config/locales/en.yml b/api/config/locales/en.yml
new file mode 100755
index 0000000..decc5a8
--- /dev/null
+++ b/api/config/locales/en.yml
@@ -0,0 +1,33 @@
+# Files in the config/locales directory are used for internationalization
+# and are automatically loaded by Rails. If you want to use locales other
+# than English, add the necessary files in this directory.
+#
+# To use the locales, use `I18n.t`:
+#
+# I18n.t 'hello'
+#
+# In views, this is aliased to just `t`:
+#
+# <%= t('hello') %>
+#
+# To use a different locale, set it with `I18n.locale`:
+#
+# I18n.locale = :es
+#
+# This would use the information in config/locales/es.yml.
+#
+# The following keys must be escaped otherwise they will not be retrieved by
+# the default I18n backend:
+#
+# true, false, on, off, yes, no
+#
+# Instead, surround them with single quotes.
+#
+# en:
+# 'true': 'foo'
+#
+# To learn more, please read the Rails Internationalization guide
+# available at http://guides.rubyonrails.org/i18n.html.
+
+en:
+ hello: "Hello world"
diff --git a/api/config/puma.rb b/api/config/puma.rb
new file mode 100755
index 0000000..1e84d96
--- /dev/null
+++ b/api/config/puma.rb
@@ -0,0 +1,34 @@
+# Puma can serve each request in a thread from an internal thread pool.
+# The `threads` method setting takes two numbers: a minimum and maximum.
+# Any libraries that use thread pools should be configured to match
+# the maximum value specified for Puma. Default is set to 5 threads for minimum
+# and maximum; this matches the default thread size of Active Record.
+#
+threads_count = ENV.fetch('RAILS_MAX_THREADS') { 5 }
+threads threads_count, threads_count
+
+# Specifies the `port` that Puma will listen on to receive requests; default is 3000.
+#
+port ENV.fetch('PORT') { 3000 }
+
+# Specifies the `environment` that Puma will run in.
+#
+environment ENV.fetch('RAILS_ENV') { 'development' }
+
+# Specifies the number of `workers` to boot in clustered mode.
+# Workers are forked webserver processes. If using threads and workers together
+# the concurrency of the application would be max `threads` * `workers`.
+# Workers do not work on JRuby or Windows (both of which do not support
+# processes).
+#
+# workers ENV.fetch('WEB_CONCURRENCY') { 2 }
+
+# Use the `preload_app!` method when specifying a `workers` number.
+# This directive tells Puma to first boot the application and load code
+# before forking the application. This takes advantage of Copy On Write
+# process behavior so workers use less memory.
+#
+# preload_app!
+
+# Allow puma to be restarted by `rails restart` command.
+plugin :tmp_restart
diff --git a/api/config/routes.rb b/api/config/routes.rb
new file mode 100755
index 0000000..aef1fca
--- /dev/null
+++ b/api/config/routes.rb
@@ -0,0 +1,12 @@
+Rails.application.routes.draw do
+ namespace :v1 do
+ resources :entities, only: [:index, :show]
+ get '/content', to: 'content#find'
+ end
+
+ mount GraphiQL::Rails::Engine, at: '/graphiql', graphql_path: '/graphql' if Rails.env.development?
+
+ post '/graphql', to: 'graphql#execute'
+
+ post '/rest/version/1/logLevel/:level', to: 'control_interface#configure_logging'
+end
diff --git a/api/config/spring.rb b/api/config/spring.rb
new file mode 100755
index 0000000..9fa7863
--- /dev/null
+++ b/api/config/spring.rb
@@ -0,0 +1,6 @@
+%w[
+ .ruby-version
+ .rbenv-vars
+ tmp/restart.txt
+ tmp/caching-dev.txt
+].each { |path| Spring.watch(path) }
diff --git a/api/config/storage.yml b/api/config/storage.yml
new file mode 100755
index 0000000..d32f76e
--- /dev/null
+++ b/api/config/storage.yml
@@ -0,0 +1,34 @@
+test:
+ service: Disk
+ root: <%= Rails.root.join("tmp/storage") %>
+
+local:
+ service: Disk
+ root: <%= Rails.root.join("storage") %>
+
+# Use rails credentials:edit to set the AWS secrets (as aws:access_key_id|secret_access_key)
+# amazon:
+# service: S3
+# access_key_id: <%= Rails.application.credentials.dig(:aws, :access_key_id) %>
+# secret_access_key: <%= Rails.application.credentials.dig(:aws, :secret_access_key) %>
+# region: us-east-1
+# bucket: your_own_bucket
+
+# Remember not to checkin your GCS keyfile to a repository
+# google:
+# service: GCS
+# project: your_project
+# credentials: <%= Rails.root.join("path/to/gcs.keyfile") %>
+# bucket: your_own_bucket
+
+# Use rails credentials:edit to set the Azure Storage secret (as azure_storage:storage_access_key)
+# microsoft:
+# service: AzureStorage
+# storage_account_name: your_account_name
+# storage_access_key: <%= Rails.application.credentials.dig(:azure_storage, :storage_access_key) %>
+# container: your_container_name
+
+# mirror:
+# service: Mirror
+# primary: local
+# mirrors: [ amazon, google, microsoft ]
diff --git a/api/db/schema.rb b/api/db/schema.rb
new file mode 100755
index 0000000..ca5b42e
--- /dev/null
+++ b/api/db/schema.rb
@@ -0,0 +1,244 @@
+# This file is auto-generated from the current state of the database. Instead
+# of editing this file, please use the migrations feature of Active Record to
+# incrementally modify your database, and then regenerate this schema definition.
+#
+# Note that this schema.rb definition is the authoritative source for your
+# database schema. If you need to create the application database on another
+# system, you should be using db:schema:load, not running all the migrations
+# from scratch. The latter is a flawed and unsustainable approach (the more migrations
+# you'll amass, the slower it'll run and the greater likelihood for issues).
+#
+# It's strongly recommended that you check this file into your version control system.
+
+ActiveRecord::Schema.define(version: 2020_09_09_072630) do
+
+ # These are extensions that must be enabled in order to support this database
+ enable_extension "plpgsql"
+ enable_extension "uuid-ossp"
+
+ create_table "assets", force: :cascade do |t|
+ t.bigint "project_id"
+ t.string "name"
+ t.text "file_data"
+ t.datetime "created_at", null: false
+ t.datetime "updated_at", null: false
+ t.index ["project_id"], name: "index_assets_on_project_id"
+ end
+
+ create_table "auth_nonces", force: :cascade do |t|
+ t.string "nonce"
+ t.datetime "expires_at"
+ t.datetime "created_at", null: false
+ t.datetime "updated_at", null: false
+ t.index ["nonce"], name: "index_auth_nonces_on_nonce", unique: true
+ end
+
+ create_table "auth_tokens", force: :cascade do |t|
+ t.bigint "user_id"
+ t.string "jti", null: false
+ t.string "aud", null: false
+ t.datetime "created_at", null: false
+ t.datetime "updated_at", null: false
+ t.index ["aud"], name: "index_auth_tokens_on_aud"
+ t.index ["jti"], name: "index_auth_tokens_on_jti", unique: true
+ t.index ["user_id"], name: "index_auth_tokens_on_user_id"
+ end
+
+ create_table "entities", force: :cascade do |t|
+ t.bigint "project_id"
+ t.string "name"
+ t.string "label"
+ t.boolean "singleton", default: false
+ t.datetime "created_at", null: false
+ t.datetime "updated_at", null: false
+ t.bigint "parent_id"
+ t.string "uid"
+ t.index ["name"], name: "index_entities_on_name"
+ t.index ["parent_id"], name: "index_entities_on_parent_id"
+ t.index ["project_id"], name: "index_entities_on_project_id"
+ end
+
+ create_table "exports", force: :cascade do |t|
+ t.bigint "project_id"
+ t.text "file_data"
+ t.integer "status", default: 0, null: false
+ t.string "name"
+ t.datetime "created_at", null: false
+ t.datetime "updated_at", null: false
+ t.index ["project_id"], name: "index_exports_on_project_id"
+ end
+
+ create_table "field_hierarchies", id: false, force: :cascade do |t|
+ t.bigint "ancestor_id", null: false
+ t.bigint "descendant_id", null: false
+ t.integer "generations", null: false
+ t.index ["ancestor_id", "descendant_id", "generations"], name: "field_anc_desc_idx", unique: true
+ t.index ["descendant_id"], name: "field_desc_idx"
+ end
+
+ create_table "fields", force: :cascade do |t|
+ t.bigint "entity_id"
+ t.string "label"
+ t.string "name"
+ t.integer "data_type"
+ t.text "default_value"
+ t.text "validations", default: "{}"
+ t.string "hint"
+ t.integer "position", default: 0
+ t.string "editor"
+ t.datetime "created_at", null: false
+ t.datetime "updated_at", null: false
+ t.bigint "parent_id"
+ t.integer "element_type"
+ t.bigint "referenced_entity_id"
+ t.text "settings", default: "{}"
+ t.string "uid"
+ t.index ["entity_id"], name: "index_fields_on_entity_id"
+ t.index ["name"], name: "index_fields_on_name"
+ t.index ["referenced_entity_id"], name: "index_fields_on_referenced_entity_id"
+ end
+
+ create_table "key_pairs", force: :cascade do |t|
+ t.bigint "project_id"
+ t.string "public_key"
+ t.datetime "expires_at"
+ t.datetime "created_at", null: false
+ t.datetime "updated_at", null: false
+ t.index ["project_id"], name: "index_key_pairs_on_project_id"
+ t.index ["public_key"], name: "index_key_pairs_on_public_key"
+ end
+
+ create_table "locales", force: :cascade do |t|
+ t.bigint "project_id"
+ t.string "language"
+ t.datetime "created_at", null: false
+ t.datetime "updated_at", null: false
+ t.index ["project_id"], name: "index_locales_on_project_id"
+ end
+
+ create_table "projects", force: :cascade do |t|
+ t.string "name"
+ t.bigint "team_id"
+ t.datetime "created_at", null: false
+ t.datetime "updated_at", null: false
+ t.string "uid"
+ t.index ["team_id"], name: "index_projects_on_team_id"
+ end
+
+ create_table "properties", force: :cascade do |t|
+ t.bigint "record_id"
+ t.bigint "field_id"
+ t.text "value"
+ t.datetime "created_at", null: false
+ t.datetime "updated_at", null: false
+ t.bigint "asset_id"
+ t.bigint "parent_id"
+ t.bigint "linked_record_id"
+ t.integer "position", default: 0
+ t.string "uid"
+ t.index ["asset_id"], name: "index_properties_on_asset_id"
+ t.index ["field_id"], name: "index_properties_on_field_id"
+ t.index ["linked_record_id"], name: "index_properties_on_linked_record_id"
+ t.index ["record_id"], name: "index_properties_on_record_id"
+ end
+
+ create_table "property_hierarchies", id: false, force: :cascade do |t|
+ t.bigint "ancestor_id", null: false
+ t.bigint "descendant_id", null: false
+ t.integer "generations", null: false
+ t.index ["ancestor_id", "descendant_id", "generations"], name: "property_anc_desc_idx", unique: true
+ t.index ["descendant_id"], name: "property_desc_idx"
+ end
+
+ create_table "records", force: :cascade do |t|
+ t.bigint "entity_id"
+ t.datetime "created_at", null: false
+ t.datetime "updated_at", null: false
+ t.string "uid"
+ t.index ["entity_id"], name: "index_records_on_entity_id"
+ end
+
+ create_table "relationships", force: :cascade do |t|
+ t.bigint "entity_id"
+ t.bigint "field_id"
+ t.integer "linked_entity_id"
+ t.integer "linked_field_id"
+ t.datetime "created_at", null: false
+ t.datetime "updated_at", null: false
+ t.index ["entity_id"], name: "index_relationships_on_entity_id"
+ t.index ["field_id"], name: "index_relationships_on_field_id"
+ t.index ["linked_entity_id"], name: "index_relationships_on_linked_entity_id"
+ t.index ["linked_field_id"], name: "index_relationships_on_linked_field_id"
+ end
+
+ create_table "resources", force: :cascade do |t|
+ t.bigint "project_id"
+ t.string "name"
+ t.text "file_data"
+ t.datetime "created_at", null: false
+ t.datetime "updated_at", null: false
+ t.index ["project_id"], name: "index_resources_on_project_id"
+ end
+
+ create_table "restores", force: :cascade do |t|
+ t.bigint "project_id"
+ t.string "url"
+ t.integer "status", default: 0, null: false
+ t.datetime "created_at", null: false
+ t.datetime "updated_at", null: false
+ t.index ["project_id"], name: "index_restores_on_project_id"
+ end
+
+ create_table "team_memberships", force: :cascade do |t|
+ t.bigint "team_id"
+ t.bigint "user_id"
+ t.integer "role", default: 0, null: false
+ t.datetime "created_at", null: false
+ t.datetime "updated_at", null: false
+ t.index ["team_id"], name: "index_team_memberships_on_team_id"
+ t.index ["user_id"], name: "index_team_memberships_on_user_id"
+ end
+
+ create_table "teams", force: :cascade do |t|
+ t.string "name"
+ t.datetime "created_at", null: false
+ t.datetime "updated_at", null: false
+ t.bigint "transfer_owner_id"
+ t.string "transfer_digest"
+ t.datetime "transfer_generated_at"
+ t.index ["transfer_owner_id"], name: "index_teams_on_transfer_owner_id"
+ end
+
+ create_table "users", force: :cascade do |t|
+ t.string "first_name", limit: 50
+ t.string "last_name", limit: 50
+ t.string "email"
+ t.text "profile_picture_data"
+ t.datetime "created_at", null: false
+ t.datetime "updated_at", null: false
+ t.bigint "external_uid"
+ t.index ["email"], name: "index_users_on_email"
+ end
+
+ add_foreign_key "assets", "projects"
+ add_foreign_key "auth_tokens", "users"
+ add_foreign_key "entities", "projects"
+ add_foreign_key "exports", "projects"
+ add_foreign_key "fields", "entities"
+ add_foreign_key "fields", "entities", column: "referenced_entity_id"
+ add_foreign_key "key_pairs", "projects"
+ add_foreign_key "locales", "projects"
+ add_foreign_key "projects", "teams"
+ add_foreign_key "properties", "assets"
+ add_foreign_key "properties", "fields"
+ add_foreign_key "properties", "records"
+ add_foreign_key "properties", "records", column: "linked_record_id"
+ add_foreign_key "records", "entities"
+ add_foreign_key "relationships", "entities"
+ add_foreign_key "relationships", "fields"
+ add_foreign_key "resources", "projects"
+ add_foreign_key "restores", "projects"
+ add_foreign_key "team_memberships", "teams"
+ add_foreign_key "team_memberships", "users"
+ add_foreign_key "teams", "users", column: "transfer_owner_id"
+end
diff --git a/api/db/seeds.rb b/api/db/seeds.rb
new file mode 100755
index 0000000..da30f19
--- /dev/null
+++ b/api/db/seeds.rb
@@ -0,0 +1,52 @@
+user = User.create!(
+ email: 'test@keepworks.com',
+ first_name: 'Test',
+ last_name: 'User',
+)
+
+team = Team.create!(
+ name: 'Clay'
+)
+
+team.team_memberships.create!(
+ user: user,
+ role: :owner
+)
+
+project = team.projects.create!(
+ name: 'Test Project'
+)
+
+project.key_pairs.create!
+
+project.assets.create!(
+ name: 'Logo',
+ file: File.open(Rails.root.join('fixtures', 'assets', 'logo.png'))
+)
+
+movie_entity = project.entities.create!(label: 'Movie', name: 'movie')
+actor_entity = project.entities.create!(label: 'Actor', name: 'actor')
+
+movie_name = movie_entity.fields.create!(label: 'Name', name: 'name', data_type: :single_line_text)
+movie_genre = movie_entity.fields.create!(label: 'Genre', name: 'genre', data_type: :single_line_text)
+movie_year = movie_entity.fields.create!(label: 'Year of Release', name: 'year', data_type: :number)
+
+actor_first_name = actor_entity.fields.create!(label: 'First Name', name: 'first_name', data_type: :single_line_text)
+actor_last_name = actor_entity.fields.create!(label: 'Last Name', name: 'last_name', data_type: :single_line_text)
+
+movie_entity.records.create!(properties_attributes: [
+ { field: movie_name, value: 'Back to the Future' },
+ { field: movie_genre, value: 'Adventure' },
+ { field: movie_year, value: 1985 }
+])
+
+movie_entity.records.create!(properties_attributes: [
+ { field: movie_name, value: 'Back to the Future II' },
+ { field: movie_genre, value: 'Adventure' },
+ { field: movie_year, value: 1989 }
+])
+
+actor_entity.records.create!(properties_attributes: [
+ { field: actor_first_name, value: 'Michael' },
+ { field: actor_last_name, value: 'J. Fox' }
+])
diff --git a/api/fixtures/assets/logo.png b/api/fixtures/assets/logo.png
new file mode 100644
index 0000000..7951d1a
Binary files /dev/null and b/api/fixtures/assets/logo.png differ
diff --git a/api/lib/app_url.rb b/api/lib/app_url.rb
new file mode 100644
index 0000000..b038d24
--- /dev/null
+++ b/api/lib/app_url.rb
@@ -0,0 +1,7 @@
+class AppUrl
+ def self.build(path = '', query = {})
+ options = { host: ENV['APP_HOST'], port: ENV['APP_PORT'], path: path, query: query.presence && query.to_query }
+ builder_class = options[:port] == '443' ? URI::HTTPS : URI::HTTP
+ builder_class.build(options).to_s
+ end
+end
diff --git a/api/lib/credentials.rb b/api/lib/credentials.rb
new file mode 100644
index 0000000..63045a4
--- /dev/null
+++ b/api/lib/credentials.rb
@@ -0,0 +1,5 @@
+class Credentials < Rails::Railtie
+ def self.get(*args)
+ Rails.application.credentials.dig(Rails.env.to_sym, *args)
+ end
+end
diff --git a/api/lib/diff.rb b/api/lib/diff.rb
new file mode 100644
index 0000000..bae2796
--- /dev/null
+++ b/api/lib/diff.rb
@@ -0,0 +1,75 @@
+class Diff
+ # This sorts both remote data and local data by their uids and compares them.
+ # Let's assume below are the uids of records.
+ # local remote
+ # 1 2
+ # 4 3
+ # 6 4
+ # 7 5
+ # 6
+ # 7
+ # 8
+ # 9
+ #
+ # 1st pass 1 < 2 - delete 1 (present in local but not in remote)
+ # 2nd pass 4 > 2 - create 2
+ # 3rd pass 4 > 3 - create 3 (present in remote but not in local)
+ # 4th pass 4 == 4 - update if different
+ # 5th pass 6 > 5 - create 5
+ # 6th pass 6 == 6 - update if different
+ # 7th pass 7 == 7 - update if different
+ # 8th pass - local empty
+ #
+ # add remaining 8, 9 to create list
+ #
+ def self.perform(local, remote, identifier: :uid, ignore: []) # rubocop:disable Metrics/PerceivedComplexity,Metrics/CyclomaticComplexity
+ sorted_local = local.sort_by { |c| c[identifier] }
+ sorted_remote = remote.sort_by { |c| c[identifier] }
+
+ diff = {
+ create: [],
+ update: [],
+ destroy: []
+ }
+
+ i = 0 # local iterator
+ j = 0 # remote iterator
+
+ while i < local.length && j < remote.length
+ local_object = clean(sorted_local[i], ignore)
+ remote_object = clean(sorted_remote[j], ignore)
+
+ if local_object[identifier] == remote_object[identifier]
+ is_updated = local_object != remote_object
+
+ diff[:update] << sorted_remote[j] if is_updated
+
+ j += 1
+ i += 1
+ elsif local_object[identifier] < remote_object[identifier]
+ diff[:destroy] << sorted_local[i]
+ i += 1
+ else
+ diff[:create] << sorted_remote[j]
+ j += 1
+ end
+ end
+
+ diff[:destroy] += sorted_local[i..local.length] if i != local.length
+ diff[:create] += sorted_remote[j..remote.length] if j != remote.length
+ diff
+ end
+
+ def self.clean(hash, ignored_paths)
+ cloned_hash = hash.deep_dup
+
+ ignored_paths.each do |ignored_path|
+ *path, final_key = ignored_path
+ to_delete = path.empty? ? cloned_hash : cloned_hash.dig(*path)
+
+ to_delete.except! final_key if to_delete.present?
+ end
+
+ cloned_hash
+ end
+end
diff --git a/api/lib/exceptions.rb b/api/lib/exceptions.rb
new file mode 100755
index 0000000..45e7afd
--- /dev/null
+++ b/api/lib/exceptions.rb
@@ -0,0 +1,52 @@
+module Exceptions
+ class APIError < StandardError
+ MESSAGE = nil
+ TYPE = 'UNPROCESSABLE_ENTITY'.freeze
+ STATUS = 422
+
+ def initialize(message = nil)
+ super(message || self.class::MESSAGE)
+ end
+
+ def type
+ self.class::TYPE
+ end
+
+ def status
+ self.class::STATUS
+ end
+ end
+
+ class FormErrors < APIError
+ attr_reader :errors
+
+ def initialize(errors)
+ @errors = errors
+ super(nil)
+ end
+ end
+
+ class Unauthorized < APIError
+ MESSAGE = 'You must be logged in to continue.'.freeze
+ TYPE = 'UNAUTHORIZED'.freeze
+ STATUS = 401
+ end
+
+ class Forbidden < APIError
+ MESSAGE = 'You are not allowed to access this resource.'.freeze
+ TYPE = 'FORBIDDEN'.freeze
+ STATUS = 403
+ end
+
+ class NotFound < APIError
+ MESSAGE = 'The resource you are looking for does not exist.'.freeze
+ TYPE = 'NOT_FOUND'.freeze
+ STATUS = 404
+ end
+
+ class InternalServerError < APIError
+ MESSAGE = "We're sorry, but something went wrong! Please try again after some time.".freeze
+ TYPE = 'INTERNAL_SERVER_ERROR'.freeze
+ STATUS = 500
+ end
+end
diff --git a/api/lib/gb_logger.rb b/api/lib/gb_logger.rb
new file mode 100644
index 0000000..e92db67
--- /dev/null
+++ b/api/lib/gb_logger.rb
@@ -0,0 +1,63 @@
+module GbLogger
+ class Formatter < SemanticLogger::Formatters::Default
+ def initialize(time_format: :iso_8601, log_host: true, log_application: true, precision: PRECISION)
+ super(time_format: time_format, log_host: log_host, log_application: log_application, precision: precision)
+ end
+
+ def status_code
+ # Payload Example: '{ :path=>"/graphql" :status=>200, :status_message=>"OK" }'
+ status_code_match = payload.match(/:status=>([0-9]*),/) if payload
+ return '' unless status_code_match
+
+ status_code_match.captures[0].to_s
+ end
+
+ def method
+ # Message Example: '-- Completed #configure_logging'
+ # Where configure_logging is the desired method
+ method_match = message.match(/^--\sCompleted\s#(\w+)$/) if message
+ return '' unless method_match
+
+ method_match.captures[0].to_s
+ end
+
+ def service_name
+ ENV['SERVICE_NAME'] || ''
+ end
+
+ def pod_name
+ ENV['POD_NAME'] || ''
+ end
+
+ def subscriber
+ ''
+ end
+
+ def device
+ ''
+ end
+
+ def level
+ log.level.upcase || ''
+ end
+
+ def thread
+ "[#{log.thread_name}]" || ''
+ end
+
+ def file
+ name
+ end
+
+ def line
+ '0'
+ end
+
+ def call(log, logger)
+ self.log = log
+ self.logger = logger
+
+ [time, service_name, pod_name, subscriber, device, level, file, thread, method, line, status_code, message, payload, exception].compact.join(' ')
+ end
+ end
+end
diff --git a/api/lib/json_web_token.rb b/api/lib/json_web_token.rb
new file mode 100644
index 0000000..0b6921f
--- /dev/null
+++ b/api/lib/json_web_token.rb
@@ -0,0 +1,9 @@
+class JsonWebToken
+ def self.encode(payload = {})
+ JWT.encode(payload, ENV['HMAC_SECRET'], 'HS256')
+ end
+
+ def self.decode(token)
+ JWT.decode(token, ENV['HMAC_SECRET'], true, { algorithm: 'HS256' })[0]
+ end
+end
diff --git a/api/lib/mail_address.rb b/api/lib/mail_address.rb
new file mode 100755
index 0000000..64bc6f1
--- /dev/null
+++ b/api/lib/mail_address.rb
@@ -0,0 +1,8 @@
+# Safer subclass of Mail::Address with convenience methods
+class MailAddress < Mail::Address
+ EMAIL_REGEX = /\A[^@\s]+@[^@\s]+\z/.freeze
+
+ def self.valid_email?(value)
+ (value =~ EMAIL_REGEX) != nil
+ end
+end
diff --git a/api/lib/rabbit_mq.rb b/api/lib/rabbit_mq.rb
new file mode 100644
index 0000000..30bc390
--- /dev/null
+++ b/api/lib/rabbit_mq.rb
@@ -0,0 +1,5 @@
+module RabbitMq
+ def self.connection_url
+ ENV['CLOUDAMQP_URL'] || "amqp://#{ENV['RABBIT_HOST']}:#{ENV['RABBIT_PORT']}"
+ end
+end
diff --git a/api/lib/record_mapper.rb b/api/lib/record_mapper.rb
new file mode 100644
index 0000000..f1614f7
--- /dev/null
+++ b/api/lib/record_mapper.rb
@@ -0,0 +1,92 @@
+class RecordMapper
+ def initialize
+ @memoized_entities = {}
+ @fields_mapping = []
+ end
+
+ def to_json(record, key_type = nil)
+ return if record.blank?
+
+ key_type ||= :camelize
+
+ resolved_values = resolve_values(record)
+
+ json = record.slice(:id, :created_at, :updated_at)
+ .merge(entity_name: record.entity.name)
+ .merge(resolved_values)
+
+ json.deep_transform_keys! { |k| k.camelize(:lower) } if key_type.try(:to_sym) == :camelize
+ json
+ end
+
+ private
+
+ def resolve_values(record)
+ root_fields = root_fields_of(record.entity)
+ properties = record.nested_properties
+
+ root_fields.each_with_object({}) do |field, obj|
+ property = properties.find { |p| p.field_id == field.id }
+
+ obj[field.name] = property_value(property, properties)
+ end
+ end
+
+ def property_value(property, properties) # rubocop:disable Metrics/PerceivedComplexity,Metrics/CyclomaticComplexity
+ return if property.blank?
+
+ field = field_of(property)
+ child_properties = child_properties_of(property, properties)
+
+ if field.array?
+ child_properties.map { |cp| property_value(cp, properties) }.compact
+ elsif field.key_value?
+ child_properties.each_with_object({}) do |child_property, obj|
+ child_field = field_of(child_property)
+
+ obj[child_field.name] = property_value(child_property, properties)
+ end
+ elsif field.reference?
+ to_json(property.linked_record)
+ elsif field.image? || field.file?
+ property.asset&.resolve_url_for(field)
+ elsif field.boolean?
+ property.value == 't' || property.value.downcase == 'true' || property.value == '1'
+ else
+ property.value
+ end
+ end
+
+ def root_fields_of(entity)
+ memoize(entity) if !memoized? entity
+
+ memoized(entity)[:root_fields]
+ end
+
+ def memoize(entity)
+ fields = entity.nested_fields
+ @memoized_entities[entity.id] = { root_fields: fields.select { |f| f.parent_id.blank? } }
+
+ map_field_id_to_field_object(fields)
+ end
+
+ def memoized?(entity)
+ memoized(entity).present?
+ end
+
+ def memoized(entity)
+ @memoized_entities[entity.id]
+ end
+
+ def map_field_id_to_field_object(fields)
+ (fields || []).each { |f| @fields_mapping[f.id] = f }
+ end
+
+ def field_of(property)
+ @fields_mapping[property.field_id]
+ end
+
+ def child_properties_of(property, properties)
+ properties.select { |p| p.parent_id == property.id }.sort_by(&:position)
+ end
+end
diff --git a/api/lib/tasks/project.rake b/api/lib/tasks/project.rake
new file mode 100644
index 0000000..67c7188
--- /dev/null
+++ b/api/lib/tasks/project.rake
@@ -0,0 +1,155 @@
+require 'csv'
+require 'json'
+
+FOLDER_PATH = 'public/exports'.freeze
+FILE_NAME = 'data.json'.freeze
+ASSET_PATH = "#{FOLDER_PATH}/assets".freeze
+FILE_PATH = "#{FOLDER_PATH}/#{FILE_NAME}".freeze
+
+namespace :project do
+ desc 'Export Project data to a JSON file'
+ task export: :environment do
+ current_project = Project.find_by(id: ENV['PROJECT_ID'])
+
+ raise 'Project not found!' if current_project.blank?
+
+ entities = current_project.entities.includes(:fields).order(created_at: :asc)
+ data = { project: current_project.slice(:name), entities: [], records: [] }
+
+ def process_properties_for(record)
+ record_json = {}
+ record.properties.each do |property|
+ record_json[property.field.name] = property_value(property)
+ end
+
+ record_json
+ end
+
+ def property_value(property) # rubocop:disable Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
+ if property.field.array?
+ property.children.sort_by(&:position).map do |child|
+ child_value = { position: child.position }
+ value = property_value(child)
+ if child.field.reference?
+ child_value[:id] = value
+ else
+ child_value[:value] = value
+ end
+
+ child_value
+ end
+ .compact
+ elsif property.field.key_value?
+ property.children.each_with_object({}) { |child, obj| obj[child.field.name] = property_value(child); }
+ elsif property.field.reference?
+ property.linked_record_id
+ elsif property.field.image? || property.field.file?
+ {
+ name: property.asset&.name,
+ url: property.asset&.resolve_url_for(property.field)
+ }
+ else
+ property.value
+ end
+ end
+
+ def process_fields(fields_all, root_fields)
+ root_fields.map do |field|
+ parsed_field = field.slice(:label, :name, :data_type, :default_value, :validations, :hint, :position, :editor, :element_type, :settings)
+ child_fields = fields_all.select { |f| f.parent == field }
+ is_sub_parent = child_fields.length == 1 && field.array?
+
+ parsed_field[:referenced_entity_name] = field.referenced_entity&.name || ''
+
+ if is_sub_parent
+ sub_parent = child_fields.first
+ sub_child_fields = fields_all.select { |f| f.parent == sub_parent }
+
+ parsed_field[:children] = process_fields(fields_all, sub_child_fields)
+ elsif child_fields.present?
+ parsed_field[:children] = process_fields(fields_all, child_fields)
+ end
+
+ parsed_field
+ end
+ end
+
+ entities.each do |entity|
+ parsed_entity = entity.slice(:name, :label, :singleton)
+ parsed_entity[:parent_name] = entity.parent&.name || ''
+
+ parsed_entity[:fields] = entity.fields.map do |field|
+ flattened_fields = field.self_and_descendants
+ root_fields = flattened_fields.reject(&:parent)
+
+ process_fields(flattened_fields, root_fields)
+ end
+ .flatten
+ .compact
+
+ data[:entities] << parsed_entity
+ end
+
+ data[:records] = current_project.entities
+ .map(&:records)
+ .flatten
+ .sort_by(&:created_at)
+ .map do |record|
+ parsed_record = { ref_id: record.id, entity_name: record.entity.name }
+
+ parsed_record[:traits] = process_properties_for(record)
+
+ parsed_record
+ end
+
+ Dir.mkdir(FOLDER_PATH) unless Dir.exist?(FOLDER_PATH)
+ File.open(FILE_PATH, 'w') do |f|
+ f.write(data.to_json)
+ end
+ end
+
+ desc 'Import Project data from a JSON File'
+ task import: :environment do
+ team = Team.find_by(id: ENV['TEAM_ID'])
+
+ raise "Team id: #{ENV['TEAM_ID']}, not found!" if team.blank?
+
+ data = JSON.parse(File.read(FILE_PATH), symbolize_names: true)
+
+ # Step 1 - Create project
+ project = CreateProject.call!(params: data[:project], team: team).project
+
+ # Step 2 - Create all entities
+ data[:entities].each do |entity|
+ parent_entity = Entity.find_by(name: entity[:parent_name])
+
+ e = project.entities.new(entity.except(:parent_name, :fields))
+ e.parent_id = parent_entity&.id
+
+ e.save!
+ end
+
+ # Step 3 - Add fields to entities (Need all entity for referenced_entities)
+ data[:entities].each do |entity|
+ loaded_entity = Entity.find_by(name: entity[:name])
+
+ entity[:fields].map do |field|
+ CreateField.call!(params: field, entity: loaded_entity, is_import: true)
+ end
+ end
+
+ # Step 4 - Create records without the reference records.
+ record_index = {}
+ data[:records].each do |record|
+ entity = Entity.find_by(name: record[:entity_name])
+ result = CreateRecord.call!(params: record, entity: entity, is_import: true)
+ record_index[record[:ref_id]] = result.record.id
+ end
+
+ # Step 5 - Update records to map reference records
+ data[:records].each do |record|
+ resolved_record = Record.find(record_index[record[:ref_id]])
+ UpdateRecord.call!(params: record, record: resolved_record, is_import: true, record_index: record_index)
+ end
+ end
+end
diff --git a/api/lib/token.rb b/api/lib/token.rb
new file mode 100755
index 0000000..aac9863
--- /dev/null
+++ b/api/lib/token.rb
@@ -0,0 +1,15 @@
+module Token
+ def self.encode(id, digest)
+ Base64.urlsafe_encode64("#{id}:#{digest}")
+ end
+
+ def self.decode(token)
+ begin
+ decoded_token = Base64.urlsafe_decode64(token)
+ rescue ArgumentError
+ return
+ end
+
+ (decoded_token || '').split(':')
+ end
+end
diff --git a/api/lib/uid_generator.rb b/api/lib/uid_generator.rb
new file mode 100644
index 0000000..3516f44
--- /dev/null
+++ b/api/lib/uid_generator.rb
@@ -0,0 +1,21 @@
+module UidGenerator
+ def self.populate(table, field = 'uid')
+ if adapter == 'postgresql'
+ load_extension = 'CREATE EXTENSION IF NOT EXISTS "uuid-ossp";'
+ sql = "UPDATE #{table} SET #{field}=uuid_generate_v4();"
+
+ ActiveRecord::Base.connection.execute(load_extension)
+ ActiveRecord::Base.connection.execute(sql)
+ elsif adapter == 'mysql2'
+ sql = "UPDATE #{table} SET #{field}=(SELECT uuid());"
+
+ ActiveRecord::Base.connection.execute(sql)
+ else
+ raise 'Unknown database adapter'
+ end
+ end
+
+ def self.adapter
+ ActiveRecord::Base.connection.instance_values['config'][:adapter]
+ end
+end
diff --git a/api/lib/url.rb b/api/lib/url.rb
new file mode 100644
index 0000000..af3bc73
--- /dev/null
+++ b/api/lib/url.rb
@@ -0,0 +1,10 @@
+module URL
+ def self.valid?(value)
+ url = begin
+ URI.parse(value)
+ rescue StandardError
+ false
+ end
+ url.is_a?(URI::HTTP) || url.is_a?(URI::HTTPS)
+ end
+end
diff --git a/api/log/.keep b/api/log/.keep
new file mode 100755
index 0000000..e69de29
diff --git a/api/package.json b/api/package.json
new file mode 100755
index 0000000..4501bc1
--- /dev/null
+++ b/api/package.json
@@ -0,0 +1,5 @@
+{
+ "name": "claycms-api",
+ "private": true,
+ "dependencies": {}
+}
diff --git a/api/public/404.html b/api/public/404.html
new file mode 100755
index 0000000..2be3af2
--- /dev/null
+++ b/api/public/404.html
@@ -0,0 +1,67 @@
+
+
+
+ The page you were looking for doesn't exist (404)
+
+
+
+
+
+
+
+
+
The page you were looking for doesn't exist.
+
You may have mistyped the address or the page may have moved.
+
+
If you are the application owner check the logs for more information.
+
+
+
diff --git a/api/public/422.html b/api/public/422.html
new file mode 100755
index 0000000..c08eac0
--- /dev/null
+++ b/api/public/422.html
@@ -0,0 +1,67 @@
+
+
+
+ The change you wanted was rejected (422)
+
+
+
+
+
+
+
+
+
The change you wanted was rejected.
+
Maybe you tried to change something you didn't have access to.
+
+
If you are the application owner check the logs for more information.
+
+
+
diff --git a/api/public/500.html b/api/public/500.html
new file mode 100755
index 0000000..78a030a
--- /dev/null
+++ b/api/public/500.html
@@ -0,0 +1,66 @@
+
+
+
+ We're sorry, but something went wrong (500)
+
+
+
+
+
+
+
+
+
We're sorry, but something went wrong.
+
+
If you are the application owner check the logs for more information.
+
+
+
diff --git a/api/public/apple-touch-icon-precomposed.png b/api/public/apple-touch-icon-precomposed.png
new file mode 100755
index 0000000..e69de29
diff --git a/api/public/apple-touch-icon.png b/api/public/apple-touch-icon.png
new file mode 100755
index 0000000..e69de29
diff --git a/api/public/favicon.ico b/api/public/favicon.ico
new file mode 100755
index 0000000..e69de29
diff --git a/api/public/robots.txt b/api/public/robots.txt
new file mode 100755
index 0000000..78a0cca
--- /dev/null
+++ b/api/public/robots.txt
@@ -0,0 +1,4 @@
+# See http://www.robotstxt.org/robotstxt.html for documentation on how to use the robots.txt file
+
+User-agent: *
+Disallow: /
diff --git a/api/spec/factories/assets.rb b/api/spec/factories/assets.rb
new file mode 100644
index 0000000..4905cd7
--- /dev/null
+++ b/api/spec/factories/assets.rb
@@ -0,0 +1,8 @@
+FactoryBot.define do
+ factory :asset do
+ project
+
+ sequence(:name) { |n| "Asset #{n}" }
+ file { File.open(Rails.root.join('fixtures', 'assets', 'logo.png')) }
+ end
+end
diff --git a/api/spec/factories/auth_nonces.rb b/api/spec/factories/auth_nonces.rb
new file mode 100644
index 0000000..2936f7c
--- /dev/null
+++ b/api/spec/factories/auth_nonces.rb
@@ -0,0 +1,6 @@
+FactoryBot.define do
+ factory :auth_nonce do
+ nonce "MyString"
+ expires_at "2020-09-03 14:48:52"
+ end
+end
diff --git a/api/spec/factories/auth_tokens.rb b/api/spec/factories/auth_tokens.rb
new file mode 100644
index 0000000..6dd7396
--- /dev/null
+++ b/api/spec/factories/auth_tokens.rb
@@ -0,0 +1,7 @@
+FactoryBot.define do
+ factory :auth_token do
+ jti "MyString"
+ aud "MyString"
+ user nil
+ end
+end
diff --git a/api/spec/factories/entities.rb b/api/spec/factories/entities.rb
new file mode 100644
index 0000000..6573459
--- /dev/null
+++ b/api/spec/factories/entities.rb
@@ -0,0 +1,8 @@
+FactoryBot.define do
+ factory :entity do
+ project
+
+ sequence(:name) { |n| "Entity Name #{n}" }
+ sequence(:label) { |n| "Entity Label #{n}" }
+ end
+end
diff --git a/api/spec/factories/exports.rb b/api/spec/factories/exports.rb
new file mode 100644
index 0000000..e406151
--- /dev/null
+++ b/api/spec/factories/exports.rb
@@ -0,0 +1,4 @@
+FactoryBot.define do
+ factory :export do
+ end
+end
diff --git a/api/spec/factories/fields.rb b/api/spec/factories/fields.rb
new file mode 100644
index 0000000..3b10063
--- /dev/null
+++ b/api/spec/factories/fields.rb
@@ -0,0 +1,9 @@
+FactoryBot.define do
+ factory :field do
+ entity
+
+ sequence(:name) { |n| "Field Name #{n}" }
+ sequence(:label) { |n| "Field Label #{n}" }
+ data_type :single_line_text
+ end
+end
diff --git a/api/spec/factories/key_pairs.rb b/api/spec/factories/key_pairs.rb
new file mode 100644
index 0000000..77edeac
--- /dev/null
+++ b/api/spec/factories/key_pairs.rb
@@ -0,0 +1,5 @@
+FactoryBot.define do
+ factory :key_pair do
+ project
+ end
+end
diff --git a/api/spec/factories/projects.rb b/api/spec/factories/projects.rb
new file mode 100644
index 0000000..55b2b92
--- /dev/null
+++ b/api/spec/factories/projects.rb
@@ -0,0 +1,7 @@
+FactoryBot.define do
+ factory :project do
+ sequence(:name) { |n| "Project #{n}" }
+
+ team
+ end
+end
diff --git a/api/spec/factories/records.rb b/api/spec/factories/records.rb
new file mode 100644
index 0000000..bfda71d
--- /dev/null
+++ b/api/spec/factories/records.rb
@@ -0,0 +1,5 @@
+FactoryBot.define do
+ factory :record do
+ entity
+ end
+end
diff --git a/api/spec/factories/resources.rb b/api/spec/factories/resources.rb
new file mode 100644
index 0000000..d8f1eb0
--- /dev/null
+++ b/api/spec/factories/resources.rb
@@ -0,0 +1,4 @@
+FactoryBot.define do
+ factory :resource do
+ end
+end
diff --git a/api/spec/factories/restores.rb b/api/spec/factories/restores.rb
new file mode 100644
index 0000000..b49e0d5
--- /dev/null
+++ b/api/spec/factories/restores.rb
@@ -0,0 +1,4 @@
+FactoryBot.define do
+ factory :restore do
+ end
+end
diff --git a/api/spec/factories/team_memberships.rb b/api/spec/factories/team_memberships.rb
new file mode 100644
index 0000000..5b25129
--- /dev/null
+++ b/api/spec/factories/team_memberships.rb
@@ -0,0 +1,7 @@
+FactoryBot.define do
+ factory :team_membership do
+ team
+ user
+ role :editor
+ end
+end
diff --git a/api/spec/factories/teams.rb b/api/spec/factories/teams.rb
new file mode 100644
index 0000000..c3412de
--- /dev/null
+++ b/api/spec/factories/teams.rb
@@ -0,0 +1,5 @@
+FactoryBot.define do
+ factory :team do
+ name { FFaker::Company.name }
+ end
+end
diff --git a/api/spec/factories/users.rb b/api/spec/factories/users.rb
new file mode 100755
index 0000000..e33092d
--- /dev/null
+++ b/api/spec/factories/users.rb
@@ -0,0 +1,9 @@
+FactoryBot.define do
+ sequence(:email) { |n| "user#{n}@example.com" }
+
+ factory :user do
+ email
+ first_name { FFaker::Name.first_name }
+ last_name { FFaker::Name.last_name }
+ end
+end
diff --git a/api/spec/interactors/accept_transfer_request_spec.rb b/api/spec/interactors/accept_transfer_request_spec.rb
new file mode 100644
index 0000000..9de5446
--- /dev/null
+++ b/api/spec/interactors/accept_transfer_request_spec.rb
@@ -0,0 +1,72 @@
+require 'rails_helper'
+
+RSpec.describe AcceptTransferRequest, type: :interactor do
+ describe '.call' do
+ before do
+ @owner = create(:user)
+ @user = create(:user)
+ @team = create(:team)
+ @owner_team_membership = create(:team_membership, team: @team, user: @owner, role: :owner)
+ @user_team_membership = create(:team_membership, team: @team, user: @user)
+ end
+
+ context 'with transfer request' do
+ before do
+ @transfer_time = Time.zone.today
+
+ @team.request_transfer_to!(@user)
+ end
+
+ context 'which has not expired' do
+ before do
+ @result = AcceptTransferRequest.call(team: @team)
+ end
+
+ it 'is successful' do
+ expect(@result).to be_a_success
+ end
+
+ it 'transfers ownership to new user and sets old owner as manager' do
+ expect(@user_team_membership.reload.role).to eq('owner')
+ expect(@owner_team_membership.reload.role).to eq('manager')
+ end
+
+ it 'resets transfer' do
+ expect(@result.team.transfer_owner).to be_nil
+ expect(@result.team.transfer_digest).to be_nil
+ expect(@result.team.transfer_generated_at).to be_nil
+ end
+ end
+
+ context 'which has expired' do
+ before do
+ Timecop.freeze(@transfer_time + Team::TRANSFER_EXPIRY_PERIOD + 1.day)
+ @result = AcceptTransferRequest.call(team: @team)
+ Timecop.return
+ end
+
+ it 'is failure' do
+ expect(@result).to be_a_failure
+ expect(@result.error).to eq('Your transfer link has either expired or been canceled.')
+ end
+
+ it 'does not reset transfer' do
+ expect(@result.team.transfer_owner).not_to be_nil
+ expect(@result.team.transfer_digest).not_to be_nil
+ expect(@result.team.transfer_generated_at).not_to be_nil
+ end
+ end
+ end
+
+ context 'without transfer request' do
+ before do
+ @result = AcceptTransferRequest.call(team: @team)
+ end
+
+ it 'is failure' do
+ expect(@result).to be_a_failure
+ expect(@result.error).to eq('Your transfer link has either expired or been canceled.')
+ end
+ end
+ end
+end
diff --git a/api/spec/interactors/authenticate_user_spec.rb b/api/spec/interactors/authenticate_user_spec.rb
new file mode 100644
index 0000000..fc7b865
--- /dev/null
+++ b/api/spec/interactors/authenticate_user_spec.rb
@@ -0,0 +1,59 @@
+require 'rails_helper'
+
+RSpec.describe AuthenticateUser, type: :interactor do
+ describe '.call' do
+ before do
+ stub_const('ENV', { 'HMAC_SECRET' => '123' })
+ @user = create(:user, email: 'test@keepworks.com')
+ @aud = 'desktop'
+ @jti = AuthToken.generate_uniq_jti
+ end
+
+ def generate_token(user_id: nil, exp: nil)
+ JsonWebToken.encode(user_id: user_id, exp: exp, jti: @jti, aud: @aud)
+ end
+
+ context 'valid token' do
+ it 'should suceed and return user' do
+ AuthToken.create!(jti: @jti, aud: @aud, user: @user)
+ bearer_token = generate_token(user_id: @user.id, exp: (Time.current + 1.month).to_i)
+
+ context = AuthenticateUser.call(bearer_token: bearer_token)
+
+ expect(context).to be_a_success
+ expect(context.user).to be_present
+ end
+ end
+
+ context 'invalid token' do
+ it 'should fail and return expired session when token has expired' do
+ AuthToken.create!(jti: @jti, aud: @aud, user: @user)
+ bearer_token = generate_token(user_id: @user.id, exp: (Time.current - 1.month).to_i)
+
+ context = AuthenticateUser.call(bearer_token: bearer_token)
+
+ expect(context).to be_a_failure
+ expect(context.error).to eq 'Your session has expired. Please login again to continue.'
+ end
+
+ it 'should fail and return expired session when token has been revoked' do
+ bearer_token = generate_token(user_id: @user.id, exp: (Time.current + 1.month).to_i)
+
+ context = AuthenticateUser.call(bearer_token: bearer_token)
+
+ expect(context).to be_a_failure
+ expect(context.error).to eq 'Your session has expired. Please login again to continue.'
+ end
+
+ it 'should fail and return user not found when user is not present' do
+ AuthToken.create!(jti: @jti, aud: @aud, user: @user)
+ bearer_token = generate_token(user_id: 'random-id', exp: (Time.current + 1.month).to_i)
+
+ context = AuthenticateUser.call(bearer_token: bearer_token)
+
+ expect(context).to be_a_failure
+ expect(context.error).to eq 'User not found'
+ end
+ end
+ end
+end
diff --git a/api/spec/interactors/cancel_transfer_request_spec.rb b/api/spec/interactors/cancel_transfer_request_spec.rb
new file mode 100644
index 0000000..d596bf9
--- /dev/null
+++ b/api/spec/interactors/cancel_transfer_request_spec.rb
@@ -0,0 +1,21 @@
+require 'rails_helper'
+
+RSpec.describe CancelTransferRequest, type: :interactor do
+ describe '.call' do
+ context 'cancel team ownership transfer request' do
+ it 'is successful' do
+ team = create(:team)
+ user = create(:user)
+
+ team.request_transfer_to!(user)
+
+ result = CancelTransferRequest.call!(team: team)
+
+ expect(result).to be_a_success
+ expect(team.transfer_digest).to be_nil
+ expect(team.transfer_owner).to be_nil
+ expect(team.transfer_generated_at).to be_nil
+ end
+ end
+ end
+end
diff --git a/api/spec/interactors/create_asset_spec.rb b/api/spec/interactors/create_asset_spec.rb
new file mode 100644
index 0000000..edd2adb
--- /dev/null
+++ b/api/spec/interactors/create_asset_spec.rb
@@ -0,0 +1,34 @@
+require 'rails_helper'
+
+RSpec.describe CreateAsset, type: :interactor do
+ describe '.call' do
+ let(:project) { create(:project) }
+
+ context 'when given valid params' do
+ before do
+ valid_params = {
+ name: "Asset #{project.id}-1",
+ file: File.open(Rails.root.join('fixtures', 'assets', 'logo.png'))
+ }
+
+ @result = CreateAsset.call(params: valid_params, project: project)
+ end
+
+ it 'is successful' do
+ expect(@result).to be_a_success
+ expect(@result.asset.name).to eq("Asset #{project.id}-1")
+ expect(@result.asset.file.storage_key).to eq(:store)
+ end
+ end
+
+ context 'when given invalid params' do
+ it 'is a failure' do
+ invalid_params = {
+ name: ''
+ }
+
+ expect { CreateAsset.call(params: invalid_params, project: project) }.to raise_exception(ActiveRecord::RecordInvalid)
+ end
+ end
+ end
+end
diff --git a/api/spec/interactors/create_entity_spec.rb b/api/spec/interactors/create_entity_spec.rb
new file mode 100644
index 0000000..5bf5884
--- /dev/null
+++ b/api/spec/interactors/create_entity_spec.rb
@@ -0,0 +1,36 @@
+require 'rails_helper'
+
+RSpec.describe CreateEntity, type: :interactor do
+ describe '.call' do
+ let(:project) { create(:project) }
+
+ context 'when given valid params' do
+ before do
+ valid_params = {
+ name: "Entity Name #{project.id}-1",
+ label: "Entity Label #{project.id}-1",
+ singleton: true
+ }
+
+ @result = CreateEntity.call(params: valid_params, project: project)
+ end
+
+ it 'is successful' do
+ expect(@result).to be_a_success
+ expect(@result.entity.name).to eq("Entity Name #{project.id}-1")
+ expect(@result.entity.label).to eq("Entity Label #{project.id}-1")
+ expect(@result.entity.singleton).to eq(true)
+ end
+ end
+
+ context 'when given invalid params' do
+ it 'is a failure' do
+ invalid_params = {
+ name: ''
+ }
+
+ expect { CreateEntity.call(params: invalid_params, project: project) }.to raise_exception(ActiveRecord::RecordInvalid)
+ end
+ end
+ end
+end
diff --git a/api/spec/interactors/create_key_pair_spec.rb b/api/spec/interactors/create_key_pair_spec.rb
new file mode 100644
index 0000000..17488c7
--- /dev/null
+++ b/api/spec/interactors/create_key_pair_spec.rb
@@ -0,0 +1,15 @@
+require 'rails_helper'
+
+RSpec.describe CreateKeyPair, type: :interactor do
+ describe '.call' do
+ let(:project) { create(:project) }
+
+ it 'is successful when given a project' do
+ result = CreateKeyPair.call(project: project)
+
+ expect(result).to be_a_success
+ expect(result.key_pair.project_id).to eq(project.id)
+ expect(result.key_pair.public_key).to be_present
+ end
+ end
+end
diff --git a/api/spec/interactors/create_project_spec.rb b/api/spec/interactors/create_project_spec.rb
new file mode 100644
index 0000000..a795aad
--- /dev/null
+++ b/api/spec/interactors/create_project_spec.rb
@@ -0,0 +1,41 @@
+require 'rails_helper'
+
+RSpec.describe CreateProject, type: :interactor do
+ describe '.call' do
+ let(:team) { create(:team) }
+
+ context 'when given valid params' do
+ before do
+ @valid_params = {
+ team_id: team.id,
+ name: "Project #{team.id}-1"
+ }
+ end
+
+ it 'is successful' do
+ result = CreateProject.call(params: @valid_params, team: team)
+
+ expect(result).to be_a_success
+ expect(result.project.name).to eq("Project #{team.id}-1")
+ end
+
+ it 'creates a key pair for the project' do
+ result = CreateProject.call(params: @valid_params, team: team)
+
+ expect(result).to be_a_success
+ expect(result.project.key_pairs.length).to eq(1)
+ end
+ end
+
+ context 'when given invalid params' do
+ it 'is a failure' do
+ invalid_params = {
+ team_id: team.id,
+ name: ''
+ }
+
+ expect { CreateProject.call(params: invalid_params, team: team) }.to raise_exception(ActiveRecord::RecordInvalid)
+ end
+ end
+ end
+end
diff --git a/api/spec/interactors/create_team_membership_spec.rb b/api/spec/interactors/create_team_membership_spec.rb
new file mode 100644
index 0000000..613a768
--- /dev/null
+++ b/api/spec/interactors/create_team_membership_spec.rb
@@ -0,0 +1,73 @@
+require 'rails_helper'
+
+RSpec.describe CreateTeamMembership, type: :interactor do
+ describe '.call' do
+ before(:each) do
+ @team = create(:team)
+ end
+
+ context 'when invited user already exist' do
+ before do
+ @email = generate(:email)
+ end
+
+ context 'when invited user is confirmed' do
+ before do
+ @invited_user = create(:user, email: @email)
+ end
+
+ context 'when invited user is not part of the team' do
+ before do
+ @result = CreateTeamMembership.call(team: @team, params: {
+ email: @email,
+ role: 'editor'
+ })
+ end
+
+ it 'is successful' do
+ expect(@result).to be_a_success
+ end
+
+ it 'must create a team membership with the given role' do
+ expect(@result.team_membership).to be_present
+ expect(@result.team_membership.role).to eq('editor')
+ end
+
+ it 'must invite the already existing user' do
+ expect(@result.user.email).to eq(@email)
+ end
+ end
+
+ context 'when invited user is already part of the team' do
+ before do
+ @invited_user.teams << @team
+
+ @result = CreateTeamMembership.call(team: @team, params: {
+ email: @email,
+ role: 'editor'
+ })
+ end
+
+ it 'is failure' do
+ expect(@result).to be_a_failure
+ expect(@result.error).to eq('Email is already added to the team.')
+ end
+ end
+ end
+ end
+
+ context 'when a user is invited with owner role' do
+ before do
+ @result = CreateTeamMembership.call(team: @team, params: {
+ email: generate(:email),
+ role: 'owner'
+ })
+ end
+
+ it 'is failure' do
+ expect(@result).to be_a_failure
+ expect(@result.error).to eq('Role cannot be owner.')
+ end
+ end
+ end
+end
diff --git a/api/spec/interactors/create_team_spec.rb b/api/spec/interactors/create_team_spec.rb
new file mode 100644
index 0000000..7daab34
--- /dev/null
+++ b/api/spec/interactors/create_team_spec.rb
@@ -0,0 +1,35 @@
+require 'rails_helper'
+
+RSpec.describe CreateTeam, type: :interactor do
+ describe '.call' do
+ context 'when given valid params' do
+ before do
+ @current_user = create(:user)
+
+ valid_params = {
+ team_params: { name: 'Valid Company Name' },
+ current_user: @current_user
+ }
+
+ @result = CreateTeam.call(valid_params)
+ end
+
+ it 'is successful' do
+ expect(@result).to be_a_success
+ end
+
+ it 'must create a team' do
+ expect(@result.team).to be_present
+ expect(@result.team.name).to eq('Valid Company Name')
+ end
+
+ it 'must create a team membership with owner role and normal status' do
+ team_memberships = @result.team.team_memberships
+
+ expect(team_memberships.count).to eq(1)
+ expect(team_memberships.first.user_id).to eq(@current_user.id)
+ expect(team_memberships.first.role).to eq('owner')
+ end
+ end
+ end
+end
diff --git a/api/spec/interactors/create_transfer_request_spec.rb b/api/spec/interactors/create_transfer_request_spec.rb
new file mode 100644
index 0000000..b6cecc1
--- /dev/null
+++ b/api/spec/interactors/create_transfer_request_spec.rb
@@ -0,0 +1,125 @@
+require 'rails_helper'
+
+RSpec.describe CreateTransferRequest, type: :interactor do
+ describe '.call' do
+ let(:user) { create(:user) }
+ let(:team) { create(:team) }
+
+ context 'when given valid params' do
+ before do
+ @valid_user = create(:user)
+
+ valid_params = {
+ user_id: @valid_user.id
+ }
+
+ team.team_memberships.create!(
+ user: @valid_user,
+ role: 'manager'
+ )
+
+ @result = CreateTransferRequest.call(params: valid_params, team: team)
+ end
+
+ it 'is successful' do
+ expect(@result).to be_a_success
+ end
+
+ it 'generates transfer request' do
+ expect(@result.team.transfer_owner).to eq(@valid_user)
+ expect(@result.team.transfer_token).not_to be_nil
+ expect(@result.team.transfer_generated_at).not_to be_nil
+ end
+ end
+
+ context 'when invalid user is given' do
+ before do
+ invalid_params = {
+ user_id: -1
+ }
+
+ @result = CreateTransferRequest.call(params: invalid_params, team: team)
+ end
+
+ it 'is a failure' do
+ expect(@result).to be_a_failure
+ expect(@result.error).to eq('The user you have selected does not exist.')
+ end
+
+ it 'does not generate transfer request' do
+ expect(@result.team.transfer_owner).to be_nil
+ expect(@result.team.transfer_token).to be_nil
+ expect(@result.team.transfer_generated_at).to be_nil
+ end
+ end
+
+ context 'when onboarding pending user is given' do
+ before do
+ invalid_params = {
+ user_id: user.id
+ }
+
+ @result = CreateTransferRequest.call(params: invalid_params, team: team)
+ end
+
+ it 'is a failure' do
+ expect(@result).to be_a_failure
+ end
+
+ it 'does not generate transfer request' do
+ expect(@result.team.transfer_owner).to be_nil
+ expect(@result.team.transfer_token).to be_nil
+ expect(@result.team.transfer_generated_at).to be_nil
+ end
+ end
+
+ context 'when user not belonging to the team is given' do
+ before do
+ invalid_user = create(:user)
+ invalid_params = {
+ user_id: invalid_user.id
+ }
+
+ @result = CreateTransferRequest.call(params: invalid_params, team: team)
+ end
+
+ it 'is a failure' do
+ expect(@result).to be_a_failure
+ expect(@result.error).to eq('The user you have selected does not belong to this team.')
+ end
+
+ it 'does not generate transfer request' do
+ expect(@result.team.transfer_owner).to be_nil
+ expect(@result.team.transfer_token).to be_nil
+ expect(@result.team.transfer_generated_at).to be_nil
+ end
+ end
+
+ context 'when user is the owner of the team' do
+ before do
+ invalid_user = create(:user)
+ invalid_params = {
+ user_id: invalid_user.id
+ }
+
+ team.team_memberships.create!(
+ user: invalid_user,
+ role: 'owner'
+ )
+
+ @result = CreateTransferRequest.call(params: invalid_params, team: team)
+ end
+
+ it 'is a failure' do
+ expect(@result).to be_a_failure
+ expect(@result.error).to eq('The user you have selected is already the owner of this team.')
+ end
+
+ it 'does not generate transfer request' do
+ expect(@result.team.transfer_owner).to be_nil
+ expect(@result.team.transfer_token).to be_nil
+ expect(@result.team.transfer_generated_at).to be_nil
+ end
+ end
+ end
+end
diff --git a/api/spec/interactors/destroy_asset_spec.rb b/api/spec/interactors/destroy_asset_spec.rb
new file mode 100644
index 0000000..f4ffe08
--- /dev/null
+++ b/api/spec/interactors/destroy_asset_spec.rb
@@ -0,0 +1,12 @@
+require 'rails_helper'
+
+RSpec.describe DestroyAsset, type: :interactor do
+ describe '.call' do
+ it 'must destroy the asset' do
+ @result = DestroyAsset.call(asset: create(:asset))
+
+ expect(@result).to be_a_success
+ expect(@result.asset).to be_destroyed
+ end
+ end
+end
diff --git a/api/spec/interactors/destroy_entity_spec.rb b/api/spec/interactors/destroy_entity_spec.rb
new file mode 100644
index 0000000..419eb4a
--- /dev/null
+++ b/api/spec/interactors/destroy_entity_spec.rb
@@ -0,0 +1,12 @@
+require 'rails_helper'
+
+RSpec.describe DestroyEntity, type: :interactor do
+ describe '.call' do
+ it 'must destroy the entity' do
+ @result = DestroyEntity.call(entity: create(:entity))
+
+ expect(@result).to be_a_success
+ expect(@result.entity).to be_destroyed
+ end
+ end
+end
diff --git a/api/spec/interactors/destroy_team_membership_spec.rb b/api/spec/interactors/destroy_team_membership_spec.rb
new file mode 100644
index 0000000..42787a3
--- /dev/null
+++ b/api/spec/interactors/destroy_team_membership_spec.rb
@@ -0,0 +1,32 @@
+require 'rails_helper'
+
+RSpec.describe DestroyTeamMembership, type: :interactor do
+ describe '.call' do
+ it 'must fail if team membership role is owner' do
+ result = DestroyTeamMembership.call(team_membership: create(:team_membership, role: :owner))
+
+ expect(result).to be_a_failure
+ expect(result.error).to eq('Owner cannot be deleted.')
+ end
+
+ it 'must destroy the team membership' do
+ result = DestroyTeamMembership.call(team_membership: create(:team_membership))
+
+ expect(result).to be_a_success
+ expect(result.team_membership).to be_destroyed
+ end
+
+ it 'must clean up pending transfer requests' do
+ team_membership = create(:team_membership)
+ team_membership.team.request_transfer_to!(team_membership.user)
+
+ result = DestroyTeamMembership.call(team_membership: team_membership)
+
+ expect(result).to be_a_success
+ expect(result.team_membership).to be_destroyed
+ expect(result.team_membership.team.transfer_owner).to be_nil
+ expect(result.team_membership.team.transfer_digest).to be_nil
+ expect(result.team_membership.team.transfer_generated_at).to be_nil
+ end
+ end
+end
diff --git a/api/spec/interactors/reject_transfer_request_spec.rb b/api/spec/interactors/reject_transfer_request_spec.rb
new file mode 100644
index 0000000..1166f59
--- /dev/null
+++ b/api/spec/interactors/reject_transfer_request_spec.rb
@@ -0,0 +1,72 @@
+require 'rails_helper'
+
+RSpec.describe RejectTransferRequest, type: :interactor do
+ describe '.call' do
+ before do
+ @owner = create(:user)
+ @user = create(:user)
+ @team = create(:team)
+ @owner_team_membership = create(:team_membership, team: @team, user: @owner, role: :owner)
+ @user_team_membership = create(:team_membership, team: @team, user: @user, role: :editor)
+ end
+
+ context 'with transfer request' do
+ before do
+ @transfer_time = Time.zone.today
+
+ @team.request_transfer_to!(@user)
+ end
+
+ context 'which has not expired' do
+ before do
+ @result = RejectTransferRequest.call(team: @team)
+ end
+
+ it 'is successful' do
+ expect(@result).to be_a_success
+ end
+
+ it 'does not transfer ownership and roles stay the same' do
+ expect(@owner_team_membership.reload.role).to eq('owner')
+ expect(@user_team_membership.reload.role).to eq('editor')
+ end
+
+ it 'resets transfer' do
+ expect(@result.team.transfer_owner).to be_nil
+ expect(@result.team.transfer_digest).to be_nil
+ expect(@result.team.transfer_generated_at).to be_nil
+ end
+ end
+
+ context 'which has expired' do
+ before do
+ Timecop.freeze(@transfer_time + Team::TRANSFER_EXPIRY_PERIOD + 1.day)
+ @result = RejectTransferRequest.call(team: @team)
+ Timecop.return
+ end
+
+ it 'is failure' do
+ expect(@result).to be_a_failure
+ expect(@result.error).to eq('Your transfer link has either expired or been canceled.')
+ end
+
+ it 'does not reset transfer' do
+ expect(@result.team.transfer_owner).not_to be_nil
+ expect(@result.team.transfer_digest).not_to be_nil
+ expect(@result.team.transfer_generated_at).not_to be_nil
+ end
+ end
+ end
+
+ context 'without transfer request' do
+ before do
+ @result = RejectTransferRequest.call(team: @team)
+ end
+
+ it 'is failure' do
+ expect(@result).to be_a_failure
+ expect(@result.error).to eq('Your transfer link has either expired or been canceled.')
+ end
+ end
+ end
+end
diff --git a/api/spec/interactors/revoke_key_pair_spec.rb b/api/spec/interactors/revoke_key_pair_spec.rb
new file mode 100644
index 0000000..60d3821
--- /dev/null
+++ b/api/spec/interactors/revoke_key_pair_spec.rb
@@ -0,0 +1,19 @@
+require 'rails_helper'
+
+RSpec.describe RevokeKeyPair, type: :interactor do
+ describe '.call' do
+ it 'must expire the key_pair' do
+ @result = RevokeKeyPair.call(key_pair: create(:key_pair))
+
+ expect(@result).to be_a_success
+ expect(@result.key_pair.expires_at).to be_present
+ end
+
+ it 'triggers #revoke! on the key_pair' do
+ key_pair = double(key_pair)
+
+ expect(key_pair).to receive(:revoke!)
+ RevokeKeyPair.call(key_pair: key_pair)
+ end
+ end
+end
diff --git a/api/spec/interactors/sso_callback_spec.rb b/api/spec/interactors/sso_callback_spec.rb
new file mode 100644
index 0000000..3cbbed3
--- /dev/null
+++ b/api/spec/interactors/sso_callback_spec.rb
@@ -0,0 +1,66 @@
+require 'rails_helper'
+
+RSpec.describe SsoCallback, type: :interactor do
+ describe '.call' do
+ before do
+ stub_const('ENV', { 'CLAY_SSO_SECRET' => '123', 'CLAY_SSO_URL' => 'http://localhost:3000'})
+ @user = create(:user, email: 'test@keepworks.com')
+ @user_agent = 'Mozilla/5.0 (Windows NT 6.2; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/30.0.1599.17 Safari/537.36'
+ end
+
+ def generate_nonce(time = Time.current)
+ nonce = AuthNonce.generate_uniq_nonce
+ @nonce = AuthNonce.create!(
+ nonce: nonce,
+ expires_at: time + AuthNonce::NONCE_EXPIRY_PERIOD
+ ).nonce
+ end
+
+ def build_payload
+ sso_raw = "nonce=#{@nonce}&email=test@keepworks.com&external_id=1"
+ b64sso = Base64.encode64(sso_raw)
+ @sso = CGI.escape(b64sso)
+ @sig = OpenSSL::HMAC.hexdigest('SHA256', ENV['CLAY_SSO_SECRET'], b64sso)
+ end
+
+ it 'should suceed and return token' do
+ generate_nonce
+ build_payload
+
+ params = { sso: @sso, sig: @sig }
+ context = SsoCallback.call(
+ params: params,
+ user_agent: @user_agent
+ )
+
+ expect(context).to be_a_success
+ expect(context.sso_payload[:token]).to be_present
+ end
+
+ it 'should throw an error if nonce has expired' do
+ generate_nonce(Time.current - 10.minutes)
+ build_payload
+
+ params = { sso: @sso, sig: @sig }
+
+ expect { SsoCallback.call(params: params, user_agent: @user_agent) }.to raise_error(Exceptions::Unauthorized, 'Request Expired')
+ end
+
+ it 'should throw an error if signature mismatches' do
+ generate_nonce
+ build_payload
+ @sig = OpenSSL::HMAC.hexdigest('SHA256', 'random secret', 'random payload')
+ params = { sso: @sso, sig: @sig }
+
+ expect { SsoCallback.call(params: params, user_agent: @user_agent) }.to raise_error(Exceptions::Unauthorized, 'Invalid Signature')
+ end
+
+ it 'should create a record in AuthToken table' do
+ generate_nonce
+ build_payload
+ params = { sso: @sso, sig: @sig }
+
+ expect { SsoCallback.call(params: params, user_agent: @user_agent) }.to change { AuthToken.count }.by(1)
+ end
+ end
+end
diff --git a/api/spec/interactors/sso_login_spec.rb b/api/spec/interactors/sso_login_spec.rb
new file mode 100644
index 0000000..32fd01a
--- /dev/null
+++ b/api/spec/interactors/sso_login_spec.rb
@@ -0,0 +1,24 @@
+require 'rails_helper'
+
+RSpec.describe SsoLogin, type: :interactor do
+ describe '.call' do
+ before do
+ stub_const('ENV', { 'CLAY_SSO_SECRET' => '123', 'CLAY_SSO_URL' => 'http://localhost:3000' })
+ @nonce_expire_in = 5
+ end
+
+ it 'should succeed and return the sso url' do
+ context = SsoLogin.call
+
+ expect(context).to be_a_success
+ expect(context.sso_payload[:sso_url]).to be_present
+ end
+
+ it 'should create a valid nonce record in AuthNonce table' do
+ context = SsoLogin.call
+ nonce = AuthNonce.find_by!(nonce: context.nonce)
+ expect(nonce.expired?).to be_falsey
+ expect { SsoLogin.call }.to change { AuthNonce.count }.by(1)
+ end
+ end
+end
diff --git a/api/spec/interactors/update_asset_spec.rb b/api/spec/interactors/update_asset_spec.rb
new file mode 100644
index 0000000..e4b51f2
--- /dev/null
+++ b/api/spec/interactors/update_asset_spec.rb
@@ -0,0 +1,32 @@
+require 'rails_helper'
+
+RSpec.describe UpdateAsset, type: :interactor do
+ describe '.call' do
+ let(:asset) { create(:asset, name: 'Test Asset 1') }
+
+ context 'when given valid params' do
+ it 'is successful' do
+ valid_params = {
+ name: 'Test Asset 2',
+ file: File.open(Rails.root.join('fixtures', 'assets', 'logo.png'))
+ }
+
+ result = UpdateAsset.call(params: valid_params, asset: asset)
+
+ expect(result).to be_a_success
+ expect(result.asset.name).to eq('Test Asset 2')
+ expect(result.asset.file.storage_key).to eq(:store)
+ end
+ end
+
+ context 'when given invalid params' do
+ it 'is a failure' do
+ invalid_params = {
+ name: ''
+ }
+
+ expect { UpdateAsset.call(params: invalid_params, asset: asset) }.to raise_exception(ActiveRecord::RecordInvalid)
+ end
+ end
+ end
+end
diff --git a/api/spec/interactors/update_entity_spec.rb b/api/spec/interactors/update_entity_spec.rb
new file mode 100644
index 0000000..ccc32e3
--- /dev/null
+++ b/api/spec/interactors/update_entity_spec.rb
@@ -0,0 +1,31 @@
+require 'rails_helper'
+
+RSpec.describe UpdateEntity, type: :interactor do
+ describe '.call' do
+ let(:entity) { create(:entity) }
+
+ context 'when given valid params' do
+ it 'is successful' do
+ valid_params = {
+ name: 'Test Entity 2',
+ label: 'Test Entity 2'
+ }
+
+ result = UpdateEntity.call(params: valid_params, entity: entity)
+
+ expect(result).to be_a_success
+ expect(result.entity.name).to eq('Test Entity 2')
+ end
+ end
+
+ context 'when given invalid params' do
+ it 'is a failure' do
+ invalid_params = {
+ name: ''
+ }
+
+ expect { UpdateEntity.call(params: invalid_params, entity: entity) }.to raise_exception(ActiveRecord::RecordInvalid)
+ end
+ end
+ end
+end
diff --git a/api/spec/interactors/update_project_spec.rb b/api/spec/interactors/update_project_spec.rb
new file mode 100644
index 0000000..2e85c12
--- /dev/null
+++ b/api/spec/interactors/update_project_spec.rb
@@ -0,0 +1,31 @@
+require 'rails_helper'
+
+RSpec.describe UpdateProject, type: :interactor do
+ describe '.call' do
+ let(:project) { create(:project, name: 'Test Project 1') }
+
+ context 'when given valid params' do
+ it 'is successful' do
+ valid_params = {
+ name: 'Test Project 2'
+ }
+
+ result = UpdateProject.call(params: valid_params, project: project)
+
+ expect(result).to be_a_success
+ expect(result.project.name).to eq('Test Project 2')
+ expect(result.project.team).to eq(project.team)
+ end
+ end
+
+ context 'when given invalid params' do
+ it 'is a failure' do
+ invalid_params = {
+ name: ''
+ }
+
+ expect { UpdateProject.call(params: invalid_params, project: project) }.to raise_exception(ActiveRecord::RecordInvalid)
+ end
+ end
+ end
+end
diff --git a/api/spec/interactors/update_team_membership_spec.rb b/api/spec/interactors/update_team_membership_spec.rb
new file mode 100644
index 0000000..3e3fba7
--- /dev/null
+++ b/api/spec/interactors/update_team_membership_spec.rb
@@ -0,0 +1,42 @@
+require 'rails_helper'
+
+RSpec.describe UpdateTeamMembership, type: :interactor do
+ describe '.call' do
+ before(:each) do
+ @team_membership = create(:team_membership, role: :editor)
+ end
+
+ context 'when the new role is owner' do
+ before do
+ @params = {
+ role: 'owner'
+ }
+ @result = UpdateTeamMembership.call(team_membership: @team_membership, params: @params)
+ end
+
+ it 'is failure' do
+ expect(@result).to be_a_failure
+ expect(@result.error).to eq('Role cannot be owner.')
+ end
+ end
+
+ context 'when the new role is not owner' do
+ before do
+ @params = {
+ role: 'editor'
+ }
+
+ @result = UpdateTeamMembership.call(team_membership: @team_membership, params: @params)
+ end
+
+ it 'is success' do
+ expect(@result).to be_a_success
+ end
+
+ it 'must update the team membership with the give role' do
+ expect(@result.team_membership).to be_present
+ expect(@result.team_membership.role).to eq('editor')
+ end
+ end
+ end
+end
diff --git a/api/spec/interactors/update_team_spec.rb b/api/spec/interactors/update_team_spec.rb
new file mode 100644
index 0000000..abc4241
--- /dev/null
+++ b/api/spec/interactors/update_team_spec.rb
@@ -0,0 +1,30 @@
+require 'rails_helper'
+
+RSpec.describe UpdateTeam, type: :interactor do
+ describe '.call' do
+ let(:team) { create(:team, name: 'Team 1') }
+
+ context 'when given valid params' do
+ it 'is successful' do
+ valid_params = {
+ name: 'Team 2'
+ }
+
+ result = UpdateTeam.call(params: valid_params, team: team)
+
+ expect(result).to be_a_success
+ expect(result.team.name).to eq('Team 2')
+ end
+ end
+
+ context 'when given invalid params' do
+ it 'is a failure' do
+ invalid_params = {
+ name: ''
+ }
+
+ expect { UpdateTeam.call(params: invalid_params, team: team) }.to raise_exception(ActiveRecord::RecordInvalid)
+ end
+ end
+ end
+end
diff --git a/api/spec/interactors/update_user_spec.rb b/api/spec/interactors/update_user_spec.rb
new file mode 100644
index 0000000..b0572b5
--- /dev/null
+++ b/api/spec/interactors/update_user_spec.rb
@@ -0,0 +1,51 @@
+require 'rails_helper'
+
+RSpec.describe UpdateUser, type: :interactor do
+ describe '.call' do
+ context 'when valid params are given' do
+ it 'is successful' do
+ user = create(:user, first_name: 'first name', last_name: 'last name')
+
+ valid_params = {
+ first_name: 'new first name',
+ last_name: 'new last name',
+ profile_picture: File.open(Rails.root.join('fixtures', 'assets', 'logo.png'))
+ }
+
+ result = UpdateUser.call(params: valid_params, user: user)
+
+ expect(result).to be_a_success
+ expect(result.user.first_name).to eq('new first name')
+ expect(result.user.last_name).to eq('new last name')
+ expect(result.user.profile_picture.storage_key).to eq(:store)
+ end
+ end
+
+ context 'when no params are given' do
+ it 'does not change value' do
+ user = create(:user, first_name: 'first name', last_name: 'last name')
+
+ params = {}
+
+ result = UpdateUser.call(params: params, user: user)
+
+ expect(result.user.first_name).to eq('first name')
+ expect(result.user.last_name).to eq('last name')
+ expect(result.user.profile_picture).to eq(nil)
+ end
+ end
+
+ context 'when first name and last name are empty' do
+ it 'is a failure' do
+ user = create(:user, first_name: 'first name', last_name: 'last name')
+
+ invalid_params = {
+ first_name: '',
+ last_name: ''
+ }
+
+ expect { UpdateUser.call(params: invalid_params, user: user) }.to raise_exception(ActiveRecord::RecordInvalid)
+ end
+ end
+ end
+end
diff --git a/api/spec/lib/app_url_spec.rb b/api/spec/lib/app_url_spec.rb
new file mode 100644
index 0000000..0ff99b6
--- /dev/null
+++ b/api/spec/lib/app_url_spec.rb
@@ -0,0 +1,57 @@
+require 'rails_helper'
+
+RSpec.describe AppUrl do
+ describe '.build' do
+ context 'in development' do
+ around do |example|
+ ClimateControl.modify APP_HOST: 'pigeonapp-dev.io', APP_PORT: '8080' do
+ example.run
+ end
+ end
+
+ it 'returns URL with http scheme and port' do
+ expect(AppUrl.build).to eq('http://pigeonapp-dev.io:8080')
+ end
+
+ it 'appends path' do
+ expect(AppUrl.build('/test/123')).to eq('http://pigeonapp-dev.io:8080/test/123')
+ end
+
+ it 'appends query' do
+ expect(AppUrl.build('', foo: 'bar')).to eq('http://pigeonapp-dev.io:8080?foo=bar')
+ end
+
+ it 'appends both path and query' do
+ expect(AppUrl.build('/test/123', foo: 'bar')).to eq('http://pigeonapp-dev.io:8080/test/123?foo=bar')
+ end
+
+ it 'fails when given a path without preceding slash' do
+ expect { AppUrl.build('test/123') }.to raise_exception(URI::InvalidComponentError)
+ end
+ end
+
+ context 'in production without https' do
+ around do |example|
+ ClimateControl.modify APP_HOST: 'pigeonapp.io', APP_PORT: '80' do
+ example.run
+ end
+ end
+
+ it 'returns URL with http scheme and no port' do
+ expect(AppUrl.build).to eq('http://pigeonapp.io')
+ end
+ end
+
+ context 'in production with https' do
+ around do |example|
+ ClimateControl.modify APP_HOST: 'pigeonapp.io', APP_PORT: '443' do
+ example.run
+ end
+ end
+
+ it 'returns URL with https scheme and no port' do
+ expect(AppUrl.build).to eq('https://pigeonapp.io')
+ end
+ end
+ end
+end
diff --git a/api/spec/models/asset_spec.rb b/api/spec/models/asset_spec.rb
new file mode 100644
index 0000000..4bb50d8
--- /dev/null
+++ b/api/spec/models/asset_spec.rb
@@ -0,0 +1,12 @@
+require 'rails_helper'
+
+RSpec.describe Asset, type: :model do
+ describe 'relationships' do
+ it { is_expected.to belong_to(:project) }
+ end
+
+ describe 'validations' do
+ it { is_expected.to validate_presence_of(:name) }
+ it { is_expected.to validate_presence_of(:file) }
+ end
+end
diff --git a/api/spec/models/auth_nonce_spec.rb b/api/spec/models/auth_nonce_spec.rb
new file mode 100644
index 0000000..a1ac9aa
--- /dev/null
+++ b/api/spec/models/auth_nonce_spec.rb
@@ -0,0 +1,5 @@
+require 'rails_helper'
+
+RSpec.describe AuthNonce, type: :model do
+ pending "add some examples to (or delete) #{__FILE__}"
+end
diff --git a/api/spec/models/auth_token_spec.rb b/api/spec/models/auth_token_spec.rb
new file mode 100644
index 0000000..d795613
--- /dev/null
+++ b/api/spec/models/auth_token_spec.rb
@@ -0,0 +1,5 @@
+require 'rails_helper'
+
+RSpec.describe AuthToken, type: :model do
+ pending "add some examples to (or delete) #{__FILE__}"
+end
diff --git a/api/spec/models/entity_spec.rb b/api/spec/models/entity_spec.rb
new file mode 100644
index 0000000..2ea93f4
--- /dev/null
+++ b/api/spec/models/entity_spec.rb
@@ -0,0 +1,15 @@
+require 'rails_helper'
+
+RSpec.describe Entity, type: :model do
+ describe 'associations' do
+ it { is_expected.to belong_to(:project) }
+ it { is_expected.to have_many(:fields) }
+ it { is_expected.to have_many(:relationships) }
+ it { is_expected.to have_many(:records) }
+ end
+
+ describe 'validations' do
+ it { is_expected.to validate_presence_of(:label) }
+ it { is_expected.to validate_presence_of(:name) }
+ end
+end
diff --git a/api/spec/models/export_spec.rb b/api/spec/models/export_spec.rb
new file mode 100644
index 0000000..432e1ab
--- /dev/null
+++ b/api/spec/models/export_spec.rb
@@ -0,0 +1,5 @@
+require 'rails_helper'
+
+RSpec.describe Export, type: :model do
+ pending "add some examples to (or delete) #{__FILE__}"
+end
diff --git a/api/spec/models/field_spec.rb b/api/spec/models/field_spec.rb
new file mode 100644
index 0000000..0d1f257
--- /dev/null
+++ b/api/spec/models/field_spec.rb
@@ -0,0 +1,16 @@
+require 'rails_helper'
+
+RSpec.describe Field, type: :model do
+ describe 'associations' do
+ it { is_expected.to belong_to(:entity).optional }
+ it { is_expected.to have_many(:relationships) }
+ it { is_expected.to have_many(:properties) }
+ end
+
+ describe 'validations' do
+ it { is_expected.to validate_presence_of(:label) }
+ it { is_expected.to validate_presence_of(:name) }
+ it { is_expected.to validate_presence_of(:data_type) }
+ it { is_expected.to validate_presence_of(:position) }
+ end
+end
diff --git a/api/spec/models/key_pair_spec.rb b/api/spec/models/key_pair_spec.rb
new file mode 100644
index 0000000..943d294
--- /dev/null
+++ b/api/spec/models/key_pair_spec.rb
@@ -0,0 +1,54 @@
+require 'rails_helper'
+
+RSpec.describe KeyPair, type: :model do
+ describe 'validations' do
+ it { is_expected.to validate_uniqueness_of(:public_key) }
+ end
+
+ describe 'callbacks' do
+ it 'sets public_key on creation' do
+ key_pair = create(:key_pair, public_key: nil)
+
+ expect(key_pair.public_key).to be_present
+ end
+ end
+
+ describe '.generate_public_key' do
+ it 'returns a key 24 characters long' do
+ key = KeyPair.generate_public_key
+
+ expect(key.length).to eq(24)
+ end
+ end
+
+ describe '#revoke!' do
+ it 'sets expires_at to now' do
+ key_pair = create(:key_pair, expires_at: nil)
+
+ revoke_time = Time.current + 5.days
+
+ Timecop.freeze(revoke_time)
+
+ key_pair.revoke!
+
+ expect(key_pair.expires_at).to eq(revoke_time)
+
+ Timecop.return
+ end
+
+ it 'does not overwrite expires_at if already set' do
+ previous_revoke_time = Time.current
+ revoke_time = Time.current + 5.days
+
+ key_pair = create(:key_pair, expires_at: previous_revoke_time)
+
+ Timecop.freeze(revoke_time)
+
+ key_pair.revoke!
+
+ expect(key_pair.expires_at).to eq(previous_revoke_time)
+
+ Timecop.return
+ end
+ end
+end
diff --git a/api/spec/models/locale_spec.rb b/api/spec/models/locale_spec.rb
new file mode 100644
index 0000000..a8f4da1
--- /dev/null
+++ b/api/spec/models/locale_spec.rb
@@ -0,0 +1,11 @@
+require 'rails_helper'
+
+RSpec.describe Locale, type: :model do
+ describe 'associations' do
+ it { is_expected.to belong_to(:project) }
+ end
+
+ describe 'validations' do
+ it { is_expected.to validate_presence_of(:language) }
+ end
+end
diff --git a/api/spec/models/project_spec.rb b/api/spec/models/project_spec.rb
new file mode 100644
index 0000000..77eee26
--- /dev/null
+++ b/api/spec/models/project_spec.rb
@@ -0,0 +1,18 @@
+require 'rails_helper'
+
+RSpec.describe Project, type: :model do
+ describe 'associations' do
+ it { is_expected.to belong_to(:team) }
+ it { is_expected.to have_many(:assets).dependent(:destroy) }
+ it { is_expected.to have_many(:key_pairs).dependent(:destroy) }
+ it { is_expected.to have_many(:entities).dependent(:destroy) }
+ it { is_expected.to have_many(:locales).dependent(:destroy) }
+ it { is_expected.to have_many(:resources).dependent(:destroy) }
+ end
+
+ describe 'validations' do
+ it { is_expected.to validate_presence_of(:name) }
+ it { is_expected.to validate_presence_of(:uid) }
+ it { is_expected.to validate_uniqueness_of(:uid) }
+ end
+end
diff --git a/api/spec/models/property_spec.rb b/api/spec/models/property_spec.rb
new file mode 100644
index 0000000..b3762c5
--- /dev/null
+++ b/api/spec/models/property_spec.rb
@@ -0,0 +1,8 @@
+require 'rails_helper'
+
+RSpec.describe Property, type: :model do
+ describe 'associations' do
+ it { is_expected.to belong_to(:record).optional }
+ it { is_expected.to belong_to(:field) }
+ end
+end
diff --git a/api/spec/models/record_spec.rb b/api/spec/models/record_spec.rb
new file mode 100644
index 0000000..4ad0ed1
--- /dev/null
+++ b/api/spec/models/record_spec.rb
@@ -0,0 +1,13 @@
+require 'rails_helper'
+
+RSpec.describe Record, type: :model do
+ describe 'associations' do
+ it { is_expected.to belong_to(:entity) }
+
+ it { is_expected.to have_many(:properties) }
+ end
+
+ describe 'macros' do
+ it { is_expected.to accept_nested_attributes_for(:properties) }
+ end
+end
diff --git a/api/spec/models/relationship_spec.rb b/api/spec/models/relationship_spec.rb
new file mode 100644
index 0000000..d1afae0
--- /dev/null
+++ b/api/spec/models/relationship_spec.rb
@@ -0,0 +1,10 @@
+require 'rails_helper'
+
+RSpec.describe Relationship, type: :model do
+ describe 'associations' do
+ it { is_expected.to belong_to(:entity) }
+ it { is_expected.to belong_to(:field) }
+ it { is_expected.to belong_to(:linked_entity).class_name('Entity').with_foreign_key(:linked_entity_id) }
+ it { is_expected.to belong_to(:linked_field).class_name('Field').with_foreign_key(:linked_field_id) }
+ end
+end
diff --git a/api/spec/models/resource_spec.rb b/api/spec/models/resource_spec.rb
new file mode 100644
index 0000000..3aae9c4
--- /dev/null
+++ b/api/spec/models/resource_spec.rb
@@ -0,0 +1,12 @@
+require 'rails_helper'
+
+RSpec.describe Resource, type: :model do
+ describe 'relationships' do
+ it { is_expected.to belong_to(:project) }
+ end
+
+ describe 'validations' do
+ it { is_expected.to validate_presence_of(:name) }
+ it { is_expected.to validate_presence_of(:file) }
+ end
+end
diff --git a/api/spec/models/restore_spec.rb b/api/spec/models/restore_spec.rb
new file mode 100644
index 0000000..45ffbb0
--- /dev/null
+++ b/api/spec/models/restore_spec.rb
@@ -0,0 +1,5 @@
+require 'rails_helper'
+
+RSpec.describe Restore, type: :model do
+ pending "add some examples to (or delete) #{__FILE__}"
+end
diff --git a/api/spec/models/team_membership_spec.rb b/api/spec/models/team_membership_spec.rb
new file mode 100644
index 0000000..1e9b6b0
--- /dev/null
+++ b/api/spec/models/team_membership_spec.rb
@@ -0,0 +1,12 @@
+require 'rails_helper'
+
+RSpec.describe TeamMembership, type: :model do
+ describe 'associations' do
+ it { is_expected.to belong_to(:user) }
+ it { is_expected.to belong_to(:team) }
+ end
+
+ describe 'validations' do
+ it { is_expected.to validate_presence_of(:role) }
+ end
+end
diff --git a/api/spec/models/team_spec.rb b/api/spec/models/team_spec.rb
new file mode 100644
index 0000000..cee0f61
--- /dev/null
+++ b/api/spec/models/team_spec.rb
@@ -0,0 +1,15 @@
+require 'rails_helper'
+
+RSpec.describe Team, type: :model do
+ it_behaves_like 'transferable'
+
+ describe 'associations' do
+ it { is_expected.to have_many(:team_memberships) }
+ it { is_expected.to have_many(:users).through(:team_memberships) }
+ it { is_expected.to have_many(:projects) }
+ end
+
+ describe 'validations' do
+ it { is_expected.to validate_presence_of(:name) }
+ end
+end
diff --git a/api/spec/models/user_spec.rb b/api/spec/models/user_spec.rb
new file mode 100644
index 0000000..0cfe1f4
--- /dev/null
+++ b/api/spec/models/user_spec.rb
@@ -0,0 +1,17 @@
+require 'rails_helper'
+
+RSpec.describe User, type: :model do
+ describe 'associations' do
+ it { is_expected.to have_many(:team_memberships) }
+ it { is_expected.to have_many(:transferable_teams).class_name('Team').dependent(:nullify) }
+
+ it { is_expected.to have_many(:teams).through(:team_memberships) }
+ end
+
+ describe 'validations' do
+ it { is_expected.to validate_presence_of(:email) }
+ it { is_expected.to validate_uniqueness_of(:email).case_insensitive }
+ it { is_expected.to validate_presence_of(:first_name) }
+ it { is_expected.to validate_presence_of(:last_name) }
+ end
+end
diff --git a/api/spec/policies/asset_policy_spec.rb b/api/spec/policies/asset_policy_spec.rb
new file mode 100644
index 0000000..3515041
--- /dev/null
+++ b/api/spec/policies/asset_policy_spec.rb
@@ -0,0 +1,14 @@
+require 'rails_helper'
+
+RSpec.describe AssetPolicy do
+ include_context 'policy'
+
+ before do
+ @project = create(:project, team: @team)
+ @asset = create(:asset, project: @project)
+ end
+
+ subject { described_class.new(@user, @asset) }
+
+ it_permits :manager, [:update, :destroy]
+end
diff --git a/api/spec/policies/entity_policy_spec.rb b/api/spec/policies/entity_policy_spec.rb
new file mode 100644
index 0000000..823f71d
--- /dev/null
+++ b/api/spec/policies/entity_policy_spec.rb
@@ -0,0 +1,14 @@
+require 'rails_helper'
+
+RSpec.describe EntityPolicy do
+ include_context 'policy'
+
+ before do
+ @project = create(:project, team: @team)
+ @entity = create(:entity, project: @project)
+ end
+
+ subject { described_class.new(@user, @entity) }
+
+ it_permits :developer, [:view, :update, :destroy, :create_field]
+end
diff --git a/api/spec/policies/field_policy_spec.rb b/api/spec/policies/field_policy_spec.rb
new file mode 100644
index 0000000..1e65c73
--- /dev/null
+++ b/api/spec/policies/field_policy_spec.rb
@@ -0,0 +1,15 @@
+require 'rails_helper'
+
+RSpec.describe FieldPolicy do
+ include_context 'policy'
+
+ before do
+ @project = create(:project, team: @team)
+ @entity = create(:entity, project: @project)
+ @field = create(:field, entity: @entity)
+ end
+
+ subject { described_class.new(@user, @field) }
+
+ it_permits :developer, [:view, :update, :destroy]
+end
diff --git a/api/spec/policies/key_pair_policy_spec.rb b/api/spec/policies/key_pair_policy_spec.rb
new file mode 100644
index 0000000..f953b82
--- /dev/null
+++ b/api/spec/policies/key_pair_policy_spec.rb
@@ -0,0 +1,14 @@
+require 'rails_helper'
+
+RSpec.describe KeyPairPolicy do
+ include_context 'policy'
+
+ before do
+ @project = create(:project, team: @team)
+ @key_pair = create(:key_pair, project: @project)
+ end
+
+ subject { described_class.new(@user, @key_pair) }
+
+ it_permits :developer, [:revoke]
+end
diff --git a/api/spec/policies/project_policy_spec.rb b/api/spec/policies/project_policy_spec.rb
new file mode 100644
index 0000000..a8f4e78
--- /dev/null
+++ b/api/spec/policies/project_policy_spec.rb
@@ -0,0 +1,14 @@
+require 'rails_helper'
+
+RSpec.describe ProjectPolicy do
+ include_context 'policy'
+
+ before do
+ @project = create(:project, team: @team)
+ end
+
+ subject { described_class.new(@user, @project) }
+
+ it_permits :editor, [:view, :view_assets, :view_exports, :view_imports, :view_key_pairs, :view_entities, :create_asset, :create_key_pair, :create_entity]
+ it_permits :manager, [:update]
+end
diff --git a/api/spec/policies/record_policy_spec.rb b/api/spec/policies/record_policy_spec.rb
new file mode 100644
index 0000000..67c18a5
--- /dev/null
+++ b/api/spec/policies/record_policy_spec.rb
@@ -0,0 +1,15 @@
+require 'rails_helper'
+
+RSpec.describe RecordPolicy do
+ include_context 'policy'
+
+ before do
+ @project = create(:project, team: @team)
+ @entity = create(:entity, project: @project)
+ @record = create(:record, entity: @entity)
+ end
+
+ subject { described_class.new(@user, @field) }
+
+ it_permits :editor, [:view, :update, :destroy]
+end
diff --git a/api/spec/policies/team_membership_policy_spec.rb b/api/spec/policies/team_membership_policy_spec.rb
new file mode 100644
index 0000000..0fba47d
--- /dev/null
+++ b/api/spec/policies/team_membership_policy_spec.rb
@@ -0,0 +1,9 @@
+require 'rails_helper'
+
+RSpec.describe TeamMembershipPolicy, type: :policy do
+ include_context 'policy'
+
+ subject { described_class.new(@user) }
+
+ it_permits :manager, [:update, :destroy]
+end
diff --git a/api/spec/policies/team_policy_spec.rb b/api/spec/policies/team_policy_spec.rb
new file mode 100644
index 0000000..4f68f73
--- /dev/null
+++ b/api/spec/policies/team_policy_spec.rb
@@ -0,0 +1,11 @@
+require 'rails_helper'
+
+RSpec.describe TeamPolicy, type: :policy do
+ include_context 'policy'
+
+ subject { described_class.new(@user, @team) }
+
+ it_permits :editor, [:view, :view_team_memberships, :view_projects, :accept_transfer_request, :reject_transfer_request]
+ it_permits :manager, [:create_team_membership, :create_project, :export_project, :import_project]
+ it_permits :owner, [:update, :destroy, :create_transfer_request, :cancel_transfer_request]
+end
diff --git a/api/spec/rails_helper.rb b/api/spec/rails_helper.rb
new file mode 100755
index 0000000..ed95d82
--- /dev/null
+++ b/api/spec/rails_helper.rb
@@ -0,0 +1,66 @@
+# This file is copied to spec/ when you run 'rails generate rspec:install'
+require 'spec_helper'
+ENV['RAILS_ENV'] ||= 'test'
+require File.expand_path('../config/environment', __dir__)
+# Prevent database truncation if the environment is production
+abort('The Rails environment is running in production mode!') if Rails.env.production?
+require 'rspec/rails'
+# Add additional requires below this line. Rails is not loaded until this point!
+
+# Requires supporting ruby files with custom matchers and macros, etc, in
+# spec/support/ and its subdirectories. Files matching `spec/**/*_spec.rb` are
+# run as spec files by default. This means that files in spec/support that end
+# in _spec.rb will both be required and run as specs, causing the specs to be
+# run twice. It is recommended that you do not name files matching this glob to
+# end with _spec.rb. You can configure this pattern with the --pattern
+# option on the command line or in ~/.rspec, .rspec or `.rspec-local`.
+#
+# The following line is provided for convenience purposes. It has the downside
+# of increasing the boot-up time by auto-requiring all files in the support
+# directory. Alternatively, in the individual `*_spec.rb` files, manually
+# require only the support files necessary.
+#
+Dir[Rails.root.join('spec', 'support', '**', '*.rb')].each { |f| require f }
+
+# Checks for pending migrations and applies them before tests are run.
+# If you are not using ActiveRecord, you can remove this line.
+ActiveRecord::Migration.maintain_test_schema!
+
+RSpec.configure do |config|
+ # Remove this line if you're not using ActiveRecord or ActiveRecord fixtures
+ config.fixture_path = "#{::Rails.root}/spec/fixtures"
+
+ # If you're not using ActiveRecord, or you'd prefer not to run each of your
+ # examples within a transaction, remove the following line or assign false
+ # instead of true.
+ config.use_transactional_fixtures = true
+
+ # RSpec Rails can automatically mix in different behaviours to your tests
+ # based on their file location, for example enabling you to call `get` and
+ # `post` in specs under `spec/controllers`.
+ #
+ # You can disable this behaviour by removing the line below, and instead
+ # explicitly tag your specs with their type, e.g.:
+ #
+ # RSpec.describe UsersController, :type => :controller do
+ # # ...
+ # end
+ #
+ # The different available types are documented in the features, such as in
+ # https://relishapp.com/rspec/rspec-rails/docs
+ config.infer_spec_type_from_file_location!
+
+ # Filter lines from Rails gems in backtraces.
+ config.filter_rails_from_backtrace!
+ # arbitrary gems may also be filtered via:
+ # config.filter_gems_from_backtrace("gem name")
+
+ config.include FactoryBot::Syntax::Methods
+end
+
+Shoulda::Matchers.configure do |config|
+ config.integrate do |with|
+ with.test_framework :rspec
+ with.library :rails
+ end
+end
diff --git a/api/spec/spec_helper.rb b/api/spec/spec_helper.rb
new file mode 100755
index 0000000..bf19028
--- /dev/null
+++ b/api/spec/spec_helper.rb
@@ -0,0 +1,109 @@
+# This file was generated by the `rails generate rspec:install` command. Conventionally, all
+# specs live under a `spec` directory, which RSpec adds to the `$LOAD_PATH`.
+# The generated `.rspec` file contains `--require spec_helper` which will cause
+# this file to always be loaded, without a need to explicitly require it in any
+# files.
+#
+# Given that it is always loaded, you are encouraged to keep this file as
+# light-weight as possible. Requiring heavyweight dependencies from this file
+# will add to the boot time of your test suite on EVERY test run, even for an
+# individual file that may not need all of that loaded. Instead, consider making
+# a separate helper file that requires the additional dependencies and performs
+# the additional setup, and require it from the spec files that actually need
+# it.
+#
+# See http://rubydoc.info/gems/rspec-core/RSpec/Core/Configuration
+
+require 'pundit/rspec'
+require 'webmock/rspec'
+require 'simplecov'
+
+WebMock.disable_net_connect!(allow_localhost: true)
+
+SimpleCov.start 'rails' do
+ add_group 'Interactors', 'app/interactors'
+ add_group 'Policies', 'app/policies'
+ add_group 'Notifiers', 'app/notifiers'
+ add_group 'Uploaders', 'app/uploaders'
+ add_group 'Resolvers', 'app/graphql/resolvers'
+ add_group 'Mutators', 'app/graphql/mutators'
+end
+
+RSpec.configure do |config|
+ # rspec-expectations config goes here. You can use an alternate
+ # assertion/expectation library such as wrong or the stdlib/minitest
+ # assertions if you prefer.
+ config.expect_with :rspec do |expectations|
+ expectations.syntax = :expect
+ # This option will default to `true` in RSpec 4. It makes the `description`
+ # and `failure_message` of custom matchers include text for helper methods
+ # defined using `chain`, e.g.:
+ # be_bigger_than(2).and_smaller_than(4).description
+ # # => "be bigger than 2 and smaller than 4"
+ # ...rather than:
+ # # => "be bigger than 2"
+ expectations.include_chain_clauses_in_custom_matcher_descriptions = true
+ end
+
+ # rspec-mocks config goes here. You can use an alternate test double
+ # library (such as bogus or mocha) by changing the `mock_with` option here.
+ config.mock_with :rspec do |mocks|
+ # Prevents you from mocking or stubbing a method that does not exist on
+ # a real object. This is generally recommended, and will default to
+ # `true` in RSpec 4.
+ mocks.verify_partial_doubles = true
+ end
+
+ # This option will default to `:apply_to_host_groups` in RSpec 4 (and will
+ # have no way to turn it off -- the option exists only for backwards
+ # compatibility in RSpec 3). It causes shared context metadata to be
+ # inherited by the metadata hash of host groups and examples, rather than
+ # triggering implicit auto-inclusion in groups with matching metadata.
+ config.shared_context_metadata_behavior = :apply_to_host_groups
+
+ # This allows you to limit a spec run to individual examples or groups
+ # you care about by tagging them with `:focus` metadata. When nothing
+ # is tagged with `:focus`, all examples get run. RSpec also provides
+ # aliases for `it`, `describe`, and `context` that include `:focus`
+ # metadata: `fit`, `fdescribe` and `fcontext`, respectively.
+ config.filter_run_when_matching :focus
+
+ # Allows RSpec to persist some state between runs in order to support
+ # the `--only-failures` and `--next-failure` CLI options. We recommend
+ # you configure your source control system to ignore this file.
+ config.example_status_persistence_file_path = 'spec/examples.txt'
+
+ # Limits the available syntax to the non-monkey patched syntax that is
+ # recommended. For more details, see:
+ # - http://rspec.info/blog/2012/06/rspecs-new-expectation-syntax/
+ # - http://www.teaisaweso.me/blog/2013/05/27/rspecs-new-message-expectation-syntax/
+ # - http://rspec.info/blog/2014/05/notable-changes-in-rspec-3/#zero-monkey-patching-mode
+ config.disable_monkey_patching!
+
+ # Many RSpec users commonly either run the entire suite or an individual
+ # file, and it's useful to allow more verbose output when running an
+ # individual spec file.
+ if config.files_to_run.one?
+ # Use the documentation formatter for detailed output,
+ # unless a formatter has already been configured
+ # (e.g. via a command-line flag).
+ config.default_formatter = 'doc'
+ end
+
+ # Print the 10 slowest examples and example groups at the
+ # end of the spec run, to help surface which specs are running
+ # particularly slow.
+ config.profile_examples = 10
+
+ # Run specs in random order to surface order dependencies. If you find an
+ # order dependency and want to debug it, you can fix the order by providing
+ # the seed, which is printed after each run.
+ # --seed 1234
+ config.order = :random
+
+ # Seed global randomization in this process using the `--seed` CLI option.
+ # Setting this allows you to use `--seed` to deterministically reproduce
+ # test failures related to randomization by passing the same `--seed` value
+ # as the one that triggered the failure.
+ Kernel.srand config.seed
+end
diff --git a/api/spec/support/concerns/transferable.rb b/api/spec/support/concerns/transferable.rb
new file mode 100644
index 0000000..9310342
--- /dev/null
+++ b/api/spec/support/concerns/transferable.rb
@@ -0,0 +1,130 @@
+require 'rails_helper'
+
+RSpec.shared_examples_for 'transferable' do
+ let(:model) { described_class }
+
+ before do
+ @record = create(model.to_s.underscore.to_sym)
+ @user = create(:user)
+ end
+
+ describe '.request_transfer_to!' do
+ context 'when user is given' do
+ it 'generates transfer request' do
+ @record.request_transfer_to!(@user)
+
+ expect(@record.transfer_digest).not_to be_nil
+ expect(@record.transfer_generated_at).not_to be_nil
+ expect(@record.transfer_owner).to eq(@user)
+ end
+ end
+
+ context 'when new user is given in an active transfer request' do
+ it 'generates new transfer request' do
+ @record.request_transfer_to!(@user)
+
+ old_record = @record.clone
+ old_record.freeze
+
+ user1 = create(:user)
+
+ @record.request_transfer_to!(user1)
+
+ expect(@record.transfer_digest).not_to be_nil
+ expect(@record.transfer_generated_at).not_to be_nil
+ expect(@record.transfer_owner).to eq(user1)
+ expect(@record.transfer_digest).not_to eq(old_record.transfer_digest)
+ expect(@record.transfer_generated_at).not_to eq(old_record.transfer_generated_at)
+ end
+ end
+ end
+
+ describe '.reset_transfer!' do
+ context 'when active transfer request is present' do
+ it 'resets transfer request' do
+ @record.transfer_digest = @record.class.generate_transfer_digest
+ @record.transfer_generated_at = Time.zone.now
+ @record.transfer_owner = @user
+
+ @record.reset_transfer!
+
+ expect(@record.transfer_digest).to be_nil
+ expect(@record.transfer_generated_at).to be_nil
+ expect(@record.transfer_owner).to be_nil
+ end
+ end
+ end
+
+ describe '.transfer_expired?' do
+ context 'when no active transfer request is present' do
+ it 'returns false' do
+ expect(@record.transfer_expired?).to eq(false)
+ end
+ end
+
+ context 'when active transfer request is present' do
+ before do
+ @record.transfer_generated_at = Time.zone.now
+ @record.transfer_owner = @user
+ @creation_time = Time.zone.now
+ end
+
+ it 'returns false when transfer period is live' do
+ expect(@record.transfer_expired?).to eq(false)
+ end
+
+ it 'returns true when transfer period is equal to transfer expiry period ' do
+ Timecop.freeze(@creation_time + model::TRANSFER_EXPIRY_PERIOD)
+
+ is_transfer_expired = @record.transfer_expired?
+
+ Timecop.return
+
+ expect(is_transfer_expired).to be(true)
+ end
+
+ it 'returns false when transfer period is less than transfer expiry period' do
+ Timecop.freeze(@creation_time + model::TRANSFER_EXPIRY_PERIOD - 1.second)
+
+ is_transfer_expired = @record.transfer_expired?
+
+ Timecop.return
+
+ expect(is_transfer_expired).to be(false)
+ end
+ end
+ end
+
+ describe '.transfer_requested?' do
+ context 'when transfer request is valid' do
+ it 'returns true' do
+ @record.transfer_generated_at = Time.zone.now
+ @record.transfer_owner = @user
+
+ expect(@record.transfer_requested?).to eq(true)
+ end
+ end
+
+ context 'when transfer request is not present' do
+ it 'returns false' do
+ expect(@record.transfer_requested?).to eq(false)
+ end
+ end
+
+ context 'when transfer request is expired' do
+ it 'returns false' do
+ @record.transfer_generated_at = Time.zone.now
+ @record.transfer_owner = @user
+ creation_time = Time.zone.now
+
+ Timecop.freeze(creation_time + model::TRANSFER_EXPIRY_PERIOD)
+
+ transfer_requested = @record.transfer_requested?
+
+ Timecop.return
+
+ expect(transfer_requested).to be(false)
+ end
+ end
+ end
+end
diff --git a/api/spec/support/geocoder_helper.rb b/api/spec/support/geocoder_helper.rb
new file mode 100755
index 0000000..5ef345a
--- /dev/null
+++ b/api/spec/support/geocoder_helper.rb
@@ -0,0 +1,22 @@
+require 'geocoder/results/freegeoip'
+
+RSpec.shared_context 'Geocoder' do
+ let(:office_ip_address) { '106.51.100.36' }
+
+ def stub_geocoder_search
+ geocoder_response = Geocoder::Result::Freegeoip.new(
+ 'ip' => '106.51.100.36',
+ 'country_code' => 'IN',
+ 'country_name' => 'India',
+ 'region_code' => 'KA',
+ 'region_name' => 'Karnataka',
+ 'city' => 'Bengaluru',
+ 'zip_code' => '',
+ 'time_zone' => 'Asia/Kolkata',
+ 'latitude' => 2.9833,
+ 'longitude' => 7.5833
+ )
+
+ allow(Geocoder).to receive(:search).and_return([geocoder_response])
+ end
+end
diff --git a/api/spec/support/policy.rb b/api/spec/support/policy.rb
new file mode 100644
index 0000000..4ed03ca
--- /dev/null
+++ b/api/spec/support/policy.rb
@@ -0,0 +1,22 @@
+ROLES = [:guest, :editor, :developer, :manager, :owner].freeze
+
+RSpec.shared_context 'policy' do
+ before do
+ @user = create(:user)
+ @team = create(:team)
+ end
+
+ def self.it_permits(role, actions)
+ ROLES.each_with_index do |current_role, index|
+ role_index = ROLES.find_index(role)
+
+ is_granted = index >= role_index if role_index.present?
+
+ it "#{is_granted ? 'grants' : 'denies'} #{actions} for #{current_role}" do
+ create(:team_membership, user: @user, team: @team, role: current_role) if current_role != :guest
+
+ is_granted ? permit_actions(actions) : forbid_actions(actions)
+ end
+ end
+ end
+end
diff --git a/api/tmp/.keep b/api/tmp/.keep
new file mode 100755
index 0000000..e69de29
diff --git a/api/vendor/.keep b/api/vendor/.keep
new file mode 100755
index 0000000..e69de29
diff --git a/ui/.babelrc b/ui/.babelrc
new file mode 100644
index 0000000..4f47cac
--- /dev/null
+++ b/ui/.babelrc
@@ -0,0 +1,21 @@
+{
+ "plugins": [
+ "react-hot-loader/babel",
+
+ // autobinding functions in ES6 classes
+ "babel-plugin-transform-class-properties",
+
+ // support rest/spread properties for objects
+ "transform-object-rest-spread",
+
+ // optimize lodash size by cherry-picking only used modules
+ "lodash",
+
+ // allow dynamic importing of large external libraries
+ "syntax-dynamic-import"
+ ],
+ "presets": [
+ ["es2015", { "modules": false }],
+ "react"
+ ]
+}
diff --git a/ui/.env.example b/ui/.env.example
new file mode 100644
index 0000000..65b323b
--- /dev/null
+++ b/ui/.env.example
@@ -0,0 +1,9 @@
+API_BASE_URL=http://api.claycms-dev.io:8080
+HOST=claycms-dev.io
+NODE_ENV=development
+PORT=3000
+PUBLIC_PATH=/
+SENTRY_AUTH_TOKEN=
+SENTRY_DSN=
+SENTRY_ORG=keepworks
+SENTRY_PROJECT=claycms-development
diff --git a/ui/.env.heroku b/ui/.env.heroku
new file mode 100644
index 0000000..5168908
--- /dev/null
+++ b/ui/.env.heroku
@@ -0,0 +1 @@
+# This file is a workaround to make dotenv work on Heroku
diff --git a/ui/.eslintignore b/ui/.eslintignore
new file mode 100644
index 0000000..5fc937c
--- /dev/null
+++ b/ui/.eslintignore
@@ -0,0 +1,2 @@
+**/*.js
+!src/**/*.js
diff --git a/ui/.eslintrc b/ui/.eslintrc
new file mode 100644
index 0000000..ebf2fce
--- /dev/null
+++ b/ui/.eslintrc
@@ -0,0 +1,36 @@
+{
+ "extends": "airbnb",
+ "env": {
+ "browser": true
+ },
+ "parser": "babel-eslint",
+ "plugins": ["react-hooks"],
+ "rules": {
+ "array-bracket-spacing": ["error", "always"],
+ "comma-dangle": ["error", "never"],
+ "jsx-a11y/anchor-is-valid": ["error", {
+ "components": ["FooterLink", "Link", "MenuLink", "NavLink", "PageLink", "TextLink"],
+ "specialLink": ["to"],
+ "aspects": ["noHref", "invalidHref", "preferButton"]
+ }],
+ "jsx-a11y/label-has-for": ["off"],
+ "no-class-assign": ["off"],
+ "no-func-assign": ["off"],
+ "no-multiple-empty-lines": ["error", { "max": 1, "maxBOF": 0, "maxEOF": 0 }],
+ "no-param-reassign": ["error", { "props": false }],
+ "no-plusplus": ["error", { "allowForLoopAfterthoughts": true }],
+ "no-unused-vars": ["error", { "ignoreRestSiblings": true, "argsIgnorePattern": "^_" }],
+ "object-curly-newline": ["error", { "consistent": true }],
+ "react/button-has-type":["off"],
+ "react/forbid-prop-types": ["off"],
+ "react/jsx-filename-extension": ["error", { "extensions": [".js", ".jsx"] }],
+ "react/no-unused-prop-types": ["off"],
+ "react/prop-types": ["off"],
+ "react-hooks/rules-of-hooks": "error",
+ "react-hooks/exhaustive-deps": "warn",
+ "semi": ["error", "never"]
+ },
+ "settings": {
+ "import/resolver": "webpack"
+ }
+}
diff --git a/ui/.gitignore b/ui/.gitignore
new file mode 100644
index 0000000..0592763
--- /dev/null
+++ b/ui/.gitignore
@@ -0,0 +1,85 @@
+## Node specific changes
+node_modules
+npm-debug.log
+build
+
+# Git
+**.orig
+
+# OS X
+.DS_Store
+.DS_Store?
+.AppleDouble
+.LSOverride
+
+# Icon must end with two \r
+Icon
+
+# Environment files
+.env
+.env.staging
+.env.production
+
+# Thumbnails
+._*
+
+# Files that might appear on external disk
+.Spotlight-V100
+.Trashes
+
+# Directories potentially created on remote AFP share
+.AppleDB
+.AppleDesktop
+Network Trash Folder
+Temporary Items
+.apdisk
+
+# Windows image file caches
+Thumbs.db
+ehthumbs.db
+
+# Folder config file
+Desktop.ini
+
+# Recycle Bin used on file shares
+$RECYCLE.BIN/
+
+# Windows Installer files
+*.cab
+*.msi
+*.msm
+*.msp
+
+# Windows shortcuts
+*.lnk
+
+# Compiled source
+*.com
+*.class
+*.dll
+*.exe
+*.o
+*.so
+
+# Packages
+# it's better to unpack these files and commit the raw source
+# git has its own built in compression methods
+*.7z
+*.dmg
+*.gz
+*.iso
+*.jar
+*.rar
+*.tar
+*.zip
+
+# Logs and databases
+*.log
+*.sql
+*.sqlite
+
+# Bundle Files
+dist/*
+
+# VSCode settings
+.vscode/*
diff --git a/ui/LICENSE.md b/ui/LICENSE.md
new file mode 100644
index 0000000..e44070b
--- /dev/null
+++ b/ui/LICENSE.md
@@ -0,0 +1 @@
+© Copyright 2018 KeepWorks Technologies Pvt. Ltd. All Rights Reserved.
diff --git a/ui/README.md b/ui/README.md
new file mode 100644
index 0000000..cf14c56
--- /dev/null
+++ b/ui/README.md
@@ -0,0 +1,38 @@
+Clay CMS
+=====================
+
+### Dependencies
+
+- [Yarn](https://yarnpkg.com/en/docs/install)
+- [ImageOptim](https://imageoptim.com/mac)
+
+### Installation
+
+1. Copy and modify the .env file: `cp .env.example .env`
+2. Run `yarn install` to install npm dependencies.
+3. Edit your hosts file (`subl /etc/hosts`) and add: `127.0.0.1 claycms-dev.io`
+4. Run `yarn start` to start the development web server.
+
+### Profiling
+
+We use the [Webpack Bundle Analyzer](https://github.com/th0r/webpack-bundle-analyzer) plugin to identify bundle size issues. To use, either:
+
+1. Use the `PROFILE` environment variable
+
+ ```
+ PROFILE=true yarn start
+ ```
+
+2. Use the `profile` npm script
+
+ ```
+ yarn run profile
+ ```
+
+Note that this plugin needs to run against the production build to determine 'parsed' and 'gzipped' sizes.
+
+### List of Screen Resolutions (width x height)
+
+ * 24 inch Desktop - 1920 x 1050
+ * 15 inch Laptop - 1400 x 710
+ * 13 inch Laptop - 1280 x 650
diff --git a/ui/netlify.toml b/ui/netlify.toml
new file mode 100644
index 0000000..0eee231
--- /dev/null
+++ b/ui/netlify.toml
@@ -0,0 +1,4 @@
+[build.environment]
+ NODE_VERSION = "10.15.0"
+ YARN_VERSION = "1.12.3"
+ NPM_VERSION = "6.4.1"
diff --git a/ui/package.json b/ui/package.json
new file mode 100644
index 0000000..98019dd
--- /dev/null
+++ b/ui/package.json
@@ -0,0 +1,128 @@
+{
+ "name": "claycms",
+ "version": "1.0.0",
+ "license": "SEE LICENSE IN LICENSE.md",
+ "private": true,
+ "description": "Clay CMS",
+ "scripts": {
+ "clean": "rimraf dist/",
+ "lint": "eslint src",
+ "start": "webpack-dev-server --disable-host-check",
+ "build": "npm run clean && webpack --progress",
+ "build:staging": "NODE_ENV=staging npm run build",
+ "build:production": "NODE_ENV=production npm run build",
+ "profile": "PROFILE=true npm run build:production",
+ "postinstall": "if [ $HEROKU ]; then npm run build; fi",
+ "optimize-images": "images=$(git diff --exit-code --cached --name-only --diff-filter=ACM -- '*.png' '*.jpg' '*.jpeg' '*.gif' '*.svg') ; $(exit $?) || (echo \"$images\" | xargs imageoptim && git add $images)"
+ },
+ "pre-commit": [
+ "optimize-images"
+ ],
+ "repository": {},
+ "main": "server.js",
+ "dependencies": {
+ "@sentry/browser": "^4.6.3",
+ "@sentry/webpack-plugin": "^1.6.2",
+ "@uppy/core": "^0.30.2",
+ "@uppy/dashboard": "^0.30.2",
+ "@uppy/react": "^0.30.2",
+ "apollo-cache-inmemory": "1.4.3",
+ "apollo-cache-persist": "^0.1.1",
+ "apollo-client": "^2.4.13",
+ "apollo-link": "^1.2.8",
+ "apollo-link-debounce": "^2.1.0",
+ "apollo-link-error": "1.1.7",
+ "apollo-link-state": "^0.4.1",
+ "apollo-upload-client": "^10.0.0",
+ "babel-core": "^6.26.0",
+ "babel-eslint": "^8.2.6",
+ "babel-loader": "^7.1.5",
+ "babel-plugin-lodash": "3.3.4",
+ "babel-plugin-syntax-dynamic-import": "^6.18.0",
+ "babel-plugin-transform-class-properties": "^6.24.1",
+ "babel-plugin-transform-object-rest-spread": "^6.26.0",
+ "babel-preset-es2015": "^6.24.1",
+ "babel-preset-react": "^6.24.1",
+ "classnames": "^2.2.6",
+ "codemirror": "^5.45.0",
+ "compression-webpack-plugin": "^2.0.0",
+ "copy-to-clipboard": "^3.0.8",
+ "css-loader": "^1.0.1",
+ "draft-js": "^0.10.5",
+ "draftjs-to-html": "^0.8.4",
+ "draftjs-utils": "^0.9.4",
+ "email-addresses": "^3.0.3",
+ "eslint": "^5.14.1",
+ "eslint-config-airbnb": "17.1.0",
+ "eslint-import-resolver-webpack": "0.11.0",
+ "eslint-loader": "^2.1.2",
+ "eslint-plugin-import": "2.16.0",
+ "eslint-plugin-jsx-a11y": "6.2.1",
+ "eslint-plugin-react": "7.12.4",
+ "eslint-plugin-react-hooks": "^1.7.0",
+ "favicons-webpack-plugin": "^0.0.9",
+ "file-loader": "^3.0.1",
+ "filesize": "^4.1.2",
+ "final-form": "^4.11.1",
+ "final-form-arrays": "^3.0.2",
+ "final-form-calculate": "^1.3.1",
+ "graphql": "^14.1.1",
+ "graphql-anywhere": "^4.1.28",
+ "graphql-tag": "^2.10.1",
+ "hoist-non-react-statics": "^3.3.0",
+ "html-to-draftjs": "^1.4.0",
+ "html-webpack-plugin": "^3.2.0",
+ "iframe-resizer": "^3.6.5",
+ "imageoptim-cli": "2.3.5",
+ "jump.js": "^1.0.2",
+ "lodash": "^4.17.11",
+ "lodash-webpack-plugin": "^0.11.5",
+ "mini-css-extract-plugin": "^0.5.0",
+ "moment": "^2.24.0",
+ "normalize.css": "^8.0.1",
+ "numbro": "^2.1.2",
+ "optimize-css-assets-webpack-plugin": "5.0.1",
+ "path": "^0.12.7",
+ "pluralize": "^7.0.0",
+ "pre-commit": "^1.2.2",
+ "prop-types": "^15.7.2",
+ "qs": "^6.6.0",
+ "rc-slider": "^8.6.6",
+ "react": "16.8.3",
+ "react-apollo": "^2.4.1",
+ "react-avatar-editor": "^11.0.6",
+ "react-click-outside": "https://github.com/tj/react-click-outside#master",
+ "react-codemirror2": "^5.1.0",
+ "react-color": "^2.18.0",
+ "react-dates": "^20.2.5",
+ "react-dom": "16.8.3",
+ "react-dropzone": "^10.0.6",
+ "react-final-form": "^4.0.2",
+ "react-final-form-arrays": "^2.0.1",
+ "react-helmet-async": "^0.2.0",
+ "react-hot-loader": "4.7.1",
+ "react-iframe-resizer-super": "^0.2.2",
+ "react-json-view": "^1.19.1",
+ "react-jss": "8.6.1",
+ "react-modal": "^3.8.1",
+ "react-moment-proptypes": "^1.6.0",
+ "react-popper": "^1.3.3",
+ "react-resize-aware": "^2.7.2",
+ "react-router-dom": "4.3.1",
+ "react-sortablejs": "^2.0.7",
+ "react-tabs": "^3.0.0",
+ "react-virtualized-auto-sizer": "^1.0.2",
+ "recharts": "^1.6.2",
+ "rimraf": "^2.6.3",
+ "style-loader": "0.23.1",
+ "terser-webpack-plugin": "^1.2.3",
+ "webpack": "4.29.5",
+ "webpack-bundle-analyzer": "3.0.4",
+ "webpack-cli": "3.2.3",
+ "webpack-dev-server": "^3.2.1",
+ "webpack-dotenv-plugin": "^2.1.0",
+ "webpack-merge": "^4.2.1",
+ "webpack-notifier": "^1.7.0",
+ "yup": "^0.26.10"
+ }
+}
diff --git a/ui/src/Root.js b/ui/src/Root.js
new file mode 100644
index 0000000..7eda2c2
--- /dev/null
+++ b/ui/src/Root.js
@@ -0,0 +1,34 @@
+import React from 'react'
+import { ApolloProvider } from 'react-apollo'
+import { BrowserRouter } from 'react-router-dom'
+import { hot } from 'react-hot-loader'
+import { ThemeProvider } from 'react-jss'
+
+import App from 'components/App'
+import AppLoader from 'components/AppLoader'
+import ClientProvider from 'components/ClientProvider'
+import theme from 'styles/theme'
+
+function Root() {
+ return (
+
+
+ {({ apolloClient }) => {
+ if (!apolloClient) {
+ return
+ }
+
+ return (
+
+
+
+
+
+ )
+ }}
+
+
+ )
+}
+
+export default hot(module)(Root)
diff --git a/ui/src/assets/fonts/claycms-icons.woff b/ui/src/assets/fonts/claycms-icons.woff
new file mode 100755
index 0000000..e62d103
Binary files /dev/null and b/ui/src/assets/fonts/claycms-icons.woff differ
diff --git a/ui/src/assets/fonts/claycms-icons.woff2 b/ui/src/assets/fonts/claycms-icons.woff2
new file mode 100755
index 0000000..62e8cd0
Binary files /dev/null and b/ui/src/assets/fonts/claycms-icons.woff2 differ
diff --git a/ui/src/assets/images/external/footer-background-mobile.svg b/ui/src/assets/images/external/footer-background-mobile.svg
new file mode 100644
index 0000000..e28b989
--- /dev/null
+++ b/ui/src/assets/images/external/footer-background-mobile.svg
@@ -0,0 +1,13 @@
+
+
+
+
+
+
+
+
diff --git a/ui/src/assets/images/external/footer-background.svg b/ui/src/assets/images/external/footer-background.svg
new file mode 100644
index 0000000..18cb476
--- /dev/null
+++ b/ui/src/assets/images/external/footer-background.svg
@@ -0,0 +1,13 @@
+
+
+
+
+
+
+
+
diff --git a/ui/src/assets/images/external/page-circle.png b/ui/src/assets/images/external/page-circle.png
new file mode 100644
index 0000000..7d76385
Binary files /dev/null and b/ui/src/assets/images/external/page-circle.png differ
diff --git a/ui/src/assets/images/external/page-circle@2x.png b/ui/src/assets/images/external/page-circle@2x.png
new file mode 100644
index 0000000..383d452
Binary files /dev/null and b/ui/src/assets/images/external/page-circle@2x.png differ
diff --git a/ui/src/assets/images/favicon.png b/ui/src/assets/images/favicon.png
new file mode 100644
index 0000000..105f360
Binary files /dev/null and b/ui/src/assets/images/favicon.png differ
diff --git a/ui/src/assets/images/loader.gif b/ui/src/assets/images/loader.gif
new file mode 100644
index 0000000..6960011
Binary files /dev/null and b/ui/src/assets/images/loader.gif differ
diff --git a/ui/src/assets/images/logo-symbol-color.png b/ui/src/assets/images/logo-symbol-color.png
new file mode 100644
index 0000000..75488ab
Binary files /dev/null and b/ui/src/assets/images/logo-symbol-color.png differ
diff --git a/ui/src/assets/images/logo-symbol-color@2x.png b/ui/src/assets/images/logo-symbol-color@2x.png
new file mode 100644
index 0000000..b9ed2a0
Binary files /dev/null and b/ui/src/assets/images/logo-symbol-color@2x.png differ
diff --git a/ui/src/assets/images/logo-text-color.png b/ui/src/assets/images/logo-text-color.png
new file mode 100644
index 0000000..433ba17
Binary files /dev/null and b/ui/src/assets/images/logo-text-color.png differ
diff --git a/ui/src/assets/images/logo-text-color@2x.png b/ui/src/assets/images/logo-text-color@2x.png
new file mode 100644
index 0000000..7951d1a
Binary files /dev/null and b/ui/src/assets/images/logo-text-color@2x.png differ
diff --git a/ui/src/assets/images/logo-text-white.png b/ui/src/assets/images/logo-text-white.png
new file mode 100644
index 0000000..7153ec8
Binary files /dev/null and b/ui/src/assets/images/logo-text-white.png differ
diff --git a/ui/src/assets/images/logo-text-white@2x.png b/ui/src/assets/images/logo-text-white@2x.png
new file mode 100644
index 0000000..8bd5314
Binary files /dev/null and b/ui/src/assets/images/logo-text-white@2x.png differ
diff --git a/ui/src/assets/stylesheets/fonts.css b/ui/src/assets/stylesheets/fonts.css
new file mode 100644
index 0000000..ce7db65
--- /dev/null
+++ b/ui/src/assets/stylesheets/fonts.css
@@ -0,0 +1,5 @@
+@font-face {
+ font-family: 'claycms-icons';
+ src: url('~fonts/claycms-icons.woff2') format('woff2'),
+ url('~fonts/claycms-icons.woff') format('woff');
+}
diff --git a/ui/src/assets/stylesheets/globals.css b/ui/src/assets/stylesheets/globals.css
new file mode 100644
index 0000000..4f40d8f
--- /dev/null
+++ b/ui/src/assets/stylesheets/globals.css
@@ -0,0 +1,44 @@
+* {
+ -webkit-font-smoothing: antialiased;
+ -moz-osx-font-smoothing: grayscale;
+ box-sizing: border-box;
+}
+
+html,
+body {
+ margin: 0;
+}
+
+html,
+body,
+#root,
+[data-reactroot] {
+ height: 100%;
+}
+
+body {
+ background-color: #fff;
+}
+
+p,
+h1,
+h2,
+h3,
+h4,
+h5,
+h6,
+ul {
+ margin: 0;
+}
+
+a,
+button,
+div,
+input,
+textarea {
+ outline: none;
+}
+
+a {
+ text-decoration: none;
+}
diff --git a/ui/src/assets/stylesheets/icons.css b/ui/src/assets/stylesheets/icons.css
new file mode 100644
index 0000000..202464c
--- /dev/null
+++ b/ui/src/assets/stylesheets/icons.css
@@ -0,0 +1,431 @@
+.icon,
+[class^="icon-"]:before,
+[class*=" icon-"]:before {
+ font-family: "claycms-icons" !important;
+ font-style: normal !important;
+ font-weight: normal !important;
+ font-variant: normal !important;
+ text-transform: none !important;
+ speak: none;
+ line-height: 1;
+ -webkit-font-smoothing: antialiased;
+ -moz-osx-font-smoothing: grayscale;
+}
+
+.icon-email:before {
+ content: "\e000";
+}
+.icon-first-name:before {
+ content: "\e001";
+}
+.icon-last-name:before {
+ content: "\e002";
+}
+.icon-password:before {
+ content: "\e003";
+}
+.icon-alert-success:before {
+ content: "\e004";
+}
+.icon-alert-failure:before {
+ content: "\e005";
+}
+.icon-cross:before {
+ content: "\e006";
+}
+.icon-round-alert:before {
+ content: "\e007";
+}
+.icon-billing:before {
+ content: "\e00a";
+}
+.icon-blimp:before {
+ content: "\e00b";
+}
+.icon-camera:before {
+ content: "\e00c";
+}
+.icon-code:before {
+ content: "\e00d";
+}
+.icon-connect:before {
+ content: "\e00e";
+}
+.icon-credit:before {
+ content: "\e00f";
+}
+.icon-credit-card:before {
+ content: "\e010";
+}
+.icon-invoice:before {
+ content: "\e011";
+}
+.icon-invite-user:before {
+ content: "\e012";
+}
+.icon-route:before {
+ content: "\e013";
+}
+.icon-hot-air-balloon:before {
+ content: "\e014";
+}
+.icon-gift:before {
+ content: "\e015";
+}
+.icon-filter:before {
+ content: "\e016";
+}
+.icon-event:before {
+ content: "\e017";
+}
+.icon-edit:before {
+ content: "\e018";
+}
+.icon-delivery:before {
+ content: "\e019";
+}
+.icon-dashboard:before {
+ content: "\e01a";
+}
+.icon-customer:before {
+ content: "\e01b";
+}
+.icon-layout:before {
+ content: "\e01c";
+}
+.icon-list:before {
+ content: "\e01d";
+}
+.icon-lock:before {
+ content: "\e01e";
+}
+.icon-notification-bell:before {
+ content: "\e01f";
+}
+.icon-notification-push:before {
+ content: "\e020";
+}
+.icon-notification-rules:before {
+ content: "\e021";
+}
+.icon-notification-sms:before {
+ content: "\e022";
+}
+.icon-person:before {
+ content: "\e023";
+}
+.icon-plan:before {
+ content: "\e024";
+}
+.icon-plus:before {
+ content: "\e025";
+}
+.icon-project:before {
+ content: "\e026";
+}
+.icon-trash:before {
+ content: "\e027";
+}
+.icon-team:before {
+ content: "\e029";
+}
+.icon-snippet:before {
+ content: "\e02a";
+}
+.icon-setting:before {
+ content: "\e02b";
+}
+.icon-round-remove:before {
+ content: "\e02c";
+}
+.icon-round-plus:before {
+ content: "\e02d";
+}
+.icon-search:before {
+ content: "\e02e";
+}
+.icon-arrow-left-thin:before {
+ content: "\e02f";
+}
+.icon-tick:before {
+ content: "\e030";
+}
+.icon-round-tick:before {
+ content: "\e031";
+}
+.icon-round-info:before {
+ content: "\e032";
+}
+.icon-slide-right:before {
+ content: "\e033";
+}
+.icon-notification-rules-thin:before {
+ content: "\e034";
+}
+.icon-email-thin:before {
+ content: "\e035";
+}
+.icon-chat:before {
+ content: "\e036";
+}
+.icon-phone:before {
+ content: "\e037";
+}
+.icon-arrow-right-small:before {
+ content: "\e038";
+}
+.icon-arrow-left-small:before {
+ content: "\e039";
+}
+.icon-arrow-down-small:before {
+ content: "\e03a";
+}
+.icon-arrow-up-small:before {
+ content: "\e03b";
+}
+.icon-tag:before {
+ content: "\e03c";
+}
+.icon-checkbox:before {
+ content: "\e03d";
+}
+.icon-attachment:before {
+ content: "\e03e";
+}
+.icon-reply:before {
+ content: "\e03f";
+}
+.icon-email-subject:before {
+ content: "\e040";
+}
+.icon-from:before {
+ content: "\e041";
+}
+.icon-email-preview:before {
+ content: "\e042";
+}
+.icon-round-pause:before {
+ content: "\e043";
+}
+.icon-email-rounded:before {
+ content: "\e046";
+}
+.icon-newsletter:before {
+ content: "\e047";
+}
+.icon-send:before {
+ content: "\e048";
+}
+.icon-download:before {
+ content: "\e049";
+}
+.icon-round-send:before {
+ content: "\e04c";
+}
+.icon-template-unlink:before {
+ content: "\e04d";
+}
+.icon-template-underline:before {
+ content: "\e04e";
+}
+.icon-template-text-color:before {
+ content: "\e04f";
+}
+.icon-template-text:before {
+ content: "\e050";
+}
+.icon-template-superscript:before {
+ content: "\e051";
+}
+.icon-template-subscript:before {
+ content: "\e052";
+}
+.icon-template-background-color:before {
+ content: "\e053";
+}
+.icon-template-bold:before {
+ content: "\e054";
+}
+.icon-template-button:before {
+ content: "\e055";
+}
+.icon-template-code:before {
+ content: "\e056";
+}
+.icon-template-cross:before {
+ content: "\e057";
+}
+.icon-template-hyperlink:before {
+ content: "\e059";
+}
+.icon-template-image:before {
+ content: "\e05a";
+}
+.icon-template-italic:before {
+ content: "\e05b";
+}
+.icon-template-link:before {
+ content: "\e05c";
+}
+.icon-template-list-bullet:before {
+ content: "\e05d";
+}
+.icon-template-align-center:before {
+ content: "\e05e";
+}
+.icon-template-align-justify:before {
+ content: "\e05f";
+}
+.icon-template-align-left:before {
+ content: "\e060";
+}
+.icon-template-align-right:before {
+ content: "\e061";
+}
+.icon-template-strikethrough:before {
+ content: "\e062";
+}
+.icon-template-list-numbers:before {
+ content: "\e063";
+}
+.icon-template-grid:before {
+ content: "\e058";
+}
+.icon-template-square:before {
+ content: "\e064";
+}
+.icon-template-display-normal:before {
+ content: "\e065";
+}
+.icon-template-display-preview:before {
+ content: "\e066";
+}
+.icon-template-variable:before {
+ content: "\e067";
+}
+.icon-template-table:before {
+ content: "\e068";
+}
+.icon-template-test:before {
+ content: "\e04a";
+}
+.icon-template-build:before {
+ content: "\e04b";
+}
+.icon-balloon:before {
+ content: "\e009";
+}
+.icon-template-file:before {
+ content: "\e044";
+}
+.icon-template-eraser:before {
+ content: "\e069";
+}
+.icon-android:before {
+ content: "\e06a";
+}
+.icon-apple:before {
+ content: "\e06b";
+}
+.icon-body:before {
+ content: "\e06c";
+}
+.icon-generic:before {
+ content: "\e06d";
+}
+.icon-upload:before {
+ content: "\e06e";
+}
+.icon-stopwatch:before {
+ content: "\e06f";
+}
+.icon-warning:before {
+ content: "\e070";
+}
+.icon-widget:before {
+ content: "\e071";
+}
+.icon-title:before {
+ content: "\e072";
+}
+.icon-image:before {
+ content: "\e073";
+}
+.icon-image-thin:before {
+ content: "\e074";
+}
+.icon-wifi:before {
+ content: "\e075";
+}
+.icon-network:before {
+ content: "\e076";
+}
+.icon-battery:before {
+ content: "\e077";
+}
+.icon-drag-and-drop:before {
+ content: "\e078";
+}
+.icon-code-thin:before {
+ content: "\e079";
+}
+.icon-star:before {
+ content: "\e045";
+}
+.icon-template:before {
+ content: "\e008";
+}
+.icon-arrow-down:before {
+ content: "\e028";
+}
+.icon-template-spacer:before {
+ content: "\e07a";
+}
+.icon-template-move-thin:before {
+ content: "\e07b";
+}
+.icon-template-move-thick:before {
+ content: "\e07c";
+}
+.icon-template-divider:before {
+ content: "\e07d";
+}
+.icon-template-shift-layer:before {
+ content: "\e07e";
+}
+.icon-campaign:before {
+ content: "\e07f";
+}
+.icon-flow:before {
+ content: "\e080";
+}
+.icon-segment:before {
+ content: "\e081";
+}
+.icon-report:before {
+ content: "\e082";
+}
+.icon-log:before {
+ content: "\e083";
+}
+.icon-archive:before {
+ content: "\e084";
+}
+.icon-calendar:before {
+ content: "\e085";
+}
+.icon-comprising:before {
+ content: "\e086";
+}
+.icon-notification-status:before {
+ content: "\e087";
+}
+.icon-notification-channel:before {
+ content: "\e088";
+}
+.icon-sent:before {
+ content: "\e089";
+}
+.icon-received:before {
+ content: "\e08a";
+}
diff --git a/ui/src/assets/stylesheets/main.css b/ui/src/assets/stylesheets/main.css
new file mode 100644
index 0000000..e5ba773
--- /dev/null
+++ b/ui/src/assets/stylesheets/main.css
@@ -0,0 +1,11 @@
+@import url('https://fonts.googleapis.com/css?family=Poppins:200,300,500,400,600,700');
+@import url('https://fonts.googleapis.com/css?family=Roboto:700,900');
+@import '~node_modules/@uppy/core/dist/style.css';
+@import '~node_modules/@uppy/dashboard/dist/style.css';
+@import '~node_modules/normalize.css/normalize.css';
+
+@import 'react_sortablejs.css';
+
+@import 'fonts.css';
+@import 'icons.css';
+@import 'globals.css';
diff --git a/ui/src/assets/stylesheets/react_sortablejs.css b/ui/src/assets/stylesheets/react_sortablejs.css
new file mode 100644
index 0000000..2693d0f
--- /dev/null
+++ b/ui/src/assets/stylesheets/react_sortablejs.css
@@ -0,0 +1,11 @@
+.sortable-ghost {
+ opacity: 0.5;
+}
+
+.drag-handle {
+ cursor: grab;
+}
+
+.drag-handle:active {
+ cursor: grabbing;
+}
diff --git a/ui/src/client/authLink.js b/ui/src/client/authLink.js
new file mode 100644
index 0000000..22f1f1b
--- /dev/null
+++ b/ui/src/client/authLink.js
@@ -0,0 +1,20 @@
+import { ApolloLink } from 'apollo-link'
+
+import cache from 'client/cache'
+
+import GET_TOKEN from 'queries/session'
+
+const authLink = new ApolloLink((operation, forward) => {
+ const { session: { token } = {} } = cache.readQuery({ query: GET_TOKEN })
+ const headers = {}
+
+ if (token) {
+ headers['X-Token'] = token
+ }
+
+ operation.setContext({ headers })
+
+ return forward(operation)
+})
+
+export default authLink
diff --git a/ui/src/client/cache.js b/ui/src/client/cache.js
new file mode 100644
index 0000000..722673f
--- /dev/null
+++ b/ui/src/client/cache.js
@@ -0,0 +1,5 @@
+import { InMemoryCache } from 'apollo-cache-inmemory'
+
+const cache = new InMemoryCache()
+
+export default cache
diff --git a/ui/src/client/debounceLink.js b/ui/src/client/debounceLink.js
new file mode 100644
index 0000000..a54930c
--- /dev/null
+++ b/ui/src/client/debounceLink.js
@@ -0,0 +1,5 @@
+import DebounceLink from 'apollo-link-debounce'
+
+const DEBOUNCE_TIMEOUT = 250
+
+export default new DebounceLink(DEBOUNCE_TIMEOUT)
diff --git a/ui/src/client/errorLink.js b/ui/src/client/errorLink.js
new file mode 100644
index 0000000..19ea8e6
--- /dev/null
+++ b/ui/src/client/errorLink.js
@@ -0,0 +1,11 @@
+import { onError } from 'apollo-link-error'
+
+import { logout } from 'client/methods'
+
+const errorLink = onError(({ networkError }) => {
+ if (networkError && networkError.statusCode === 401) {
+ logout()
+ }
+})
+
+export default errorLink
diff --git a/ui/src/client/httpLink.js b/ui/src/client/httpLink.js
new file mode 100644
index 0000000..9498f57
--- /dev/null
+++ b/ui/src/client/httpLink.js
@@ -0,0 +1,7 @@
+import { createUploadLink } from 'apollo-upload-client'
+
+const httpLink = createUploadLink({
+ uri: `${process.env.API_BASE_URL}/graphql`
+})
+
+export default httpLink
diff --git a/ui/src/client/index.js b/ui/src/client/index.js
new file mode 100644
index 0000000..90bc77c
--- /dev/null
+++ b/ui/src/client/index.js
@@ -0,0 +1,31 @@
+import ApolloClient from 'apollo-client'
+import { from } from 'apollo-link'
+
+import authLink from './authLink'
+import cache from './cache'
+import debounceLink from './debounceLink'
+import errorLink from './errorLink'
+import httpLink from './httpLink'
+import stateLink from './stateLink'
+
+const client = new ApolloClient({
+ link: from([
+ authLink,
+ errorLink,
+ stateLink,
+ debounceLink,
+ httpLink
+ ]),
+ cache
+})
+
+/*
+ https://www.apollographql.com/docs/link/links/state.html#defaults
+ https://www.apollographql.com/docs/react/advanced/caching.html#reset-store
+
+ The cache is not reset back to defaults set in `stateLink` on client.resetStore().
+ Therefore, we need to rehydrate cache with defaults using `onResetStore()`.
+*/
+client.onResetStore(stateLink.writeDefaults)
+
+export default client
diff --git a/ui/src/client/methods.js b/ui/src/client/methods.js
new file mode 100644
index 0000000..887ed28
--- /dev/null
+++ b/ui/src/client/methods.js
@@ -0,0 +1,16 @@
+import client from 'client'
+
+import SET_TOKEN from 'mutations/session'
+import { ALERT_FAILURE, ALERT_SUCCESS } from 'mutations/alert'
+
+const logout = () => (
+ client.mutate({ mutation: SET_TOKEN, variables: { token: null } }).then(() => (
+ client.resetStore()
+ ))
+)
+
+const showAlertFailure = alert => client.mutate({ mutation: ALERT_FAILURE, variables: { alert } })
+
+const showAlertSuccess = alert => client.mutate({ mutation: ALERT_SUCCESS, variables: { alert } })
+
+export { logout, showAlertFailure, showAlertSuccess }
diff --git a/ui/src/client/stateLink.js b/ui/src/client/stateLink.js
new file mode 100644
index 0000000..7f91614
--- /dev/null
+++ b/ui/src/client/stateLink.js
@@ -0,0 +1,11 @@
+import { withClientState } from 'apollo-link-state'
+
+import cache from './cache'
+import resolvers from '../resolvers'
+
+const stateLink = withClientState({
+ cache,
+ ...resolvers
+})
+
+export default stateLink
diff --git a/ui/src/components/ActionList.js b/ui/src/components/ActionList.js
new file mode 100644
index 0000000..670a261
--- /dev/null
+++ b/ui/src/components/ActionList.js
@@ -0,0 +1,63 @@
+import injectSheet from 'react-jss'
+import PropTypes from 'prop-types'
+import React from 'react'
+
+import * as mixins from 'styles/mixins'
+import FontIcon from 'components/FontIcon'
+import ItemBar from 'components/ItemBar'
+
+function ActionList({ actions, record, classes }) {
+ return (
+
+ {actions.map(({ icon, onClick }) => (
+ {
+ e.stopPropagation()
+ onClick(record, e)
+ }}
+ onKeyPress={(e) => {
+ e.stopPropagation()
+ onClick(record, e)
+ }}
+ >
+
+
+ ))}
+
+ )
+}
+
+ActionList.propTypes = {
+ actions: PropTypes.arrayOf(PropTypes.shape({
+ icon: PropTypes.string.isRequired,
+ onClick: PropTypes.func
+ })),
+ record: PropTypes.object
+}
+
+ActionList.defaultProps = {
+ actions: [],
+ record: null
+}
+
+export default injectSheet(({ colors, units }) => ({
+ action: {
+ ...mixins.transitionSimple(),
+
+ display: 'flex',
+ color: colors.text_pale,
+ cursor: 'pointer',
+
+ '&:hover': {
+ color: colors.text_dark
+ },
+
+ '&:not(:last-child)': {
+ marginRight: units.assetBoxActionMarginRight
+ }
+ }
+}))(ActionList)
diff --git a/ui/src/components/AlertBox.js b/ui/src/components/AlertBox.js
new file mode 100644
index 0000000..a4204ff
--- /dev/null
+++ b/ui/src/components/AlertBox.js
@@ -0,0 +1,173 @@
+import classNames from 'classnames'
+import injectSheet from 'react-jss'
+import PropTypes from 'prop-types'
+import React from 'react'
+
+import * as mixins from 'styles/mixins'
+import CloseButton from 'components/buttons/CloseButton'
+import FontIcon from 'components/FontIcon'
+import { withClientMutation, withClientQuery } from 'lib/data'
+
+import GET_ALERT from 'queries/alert'
+import { CLOSE_ALERT } from 'mutations/alert'
+
+const defaults = {
+ success: {
+ icon: 'alert-success',
+ title: 'Well done!'
+ },
+ failure: {
+ icon: 'alert-failure',
+ title: 'Yikes!'
+ }
+}
+
+const alertIconSize = 'medium'
+const closeTimeoutInMS = 5000
+
+let closeTimer = null
+
+function AlertBox({
+ alert: {
+ isOpen, icon, title, message, variant
+ },
+ closeAlert,
+ classes
+}) {
+ if (isOpen) {
+ if (closeTimer) {
+ clearTimeout(closeTimer)
+ }
+
+ closeTimer = setTimeout(closeAlert, closeTimeoutInMS)
+ }
+
+ const handleClose = () => {
+ if (closeTimer) {
+ clearTimeout(closeTimer)
+ }
+
+ closeAlert()
+ }
+
+ return (
+
+
+
+
+
+
+
+
+ {title || defaults[variant].title}
+
+
+
+ {message && (
+
+ {message || defaults[variant].message}
+
+ )}
+
+
+ )
+}
+
+AlertBox.propTypes = {
+ alert: PropTypes.shape({
+ isOpen: PropTypes.bool,
+ variant: PropTypes.oneOf(Object.keys(defaults)).isRequired
+ })
+}
+
+AlertBox.defaultProps = {
+ alert: {
+ isOpen: false
+ }
+}
+
+AlertBox = injectSheet(({
+ colors, shadows, typography, units, zIndexes
+}) => ({
+ container: {
+ position: 'fixed',
+ right: 0,
+ bottom: 0,
+ left: 0,
+ zIndex: zIndexes.alert
+ },
+ alert: {
+ ...mixins.responsiveProperty('width', units.alertWidth),
+ ...mixins.transitionSimple(),
+
+ backgroundColor: colors.alertBackground,
+ borderRadius: units.alertBorderRadius,
+ boxShadow: shadows.alert,
+ opacity: 0,
+ paddingTop: units.alertVerticalPadding,
+ paddingRight: units.alertHorizontalPadding,
+ paddingBottom: units.alertVerticalPadding,
+ paddingLeft: units.alertHorizontalPadding,
+ pointerEvents: 'none',
+ position: 'absolute',
+ right: units.alertPositionRight,
+ bottom: 0
+ },
+ alert_open: {
+ bottom: units.alertPositionBottom,
+ opacity: 1,
+ pointerEvents: 'auto'
+ },
+ dismiss: {
+ cursor: 'pointer',
+ position: 'absolute',
+ top: 0,
+ right: 0,
+ bottom: 0,
+ left: 0
+ },
+ header: {
+ display: 'flex',
+ alignItems: 'center',
+ paddingBottom: units.alertHeaderPaddingBottom
+ },
+ icon: {
+ fontSize: 0, /* clear line-height that causes extra spacing */
+ marginRight: units.alertIconMargin
+ },
+ icon_success: {
+ color: colors.alert_success
+ },
+ icon_failure: {
+ color: colors.alert_failure
+ },
+ title: {
+ ...typography.semiboldMedium,
+
+ flex: '1 0 auto'
+ },
+ title_success: {
+ color: colors.alert_success
+ },
+ title_failure: {
+ color: colors.alert_failure
+ },
+ message: {
+ ...typography.regularSquished,
+
+ color: colors.alertText
+ }
+}))(AlertBox)
+
+AlertBox = withClientMutation(CLOSE_ALERT)(AlertBox)
+
+AlertBox = withClientQuery(GET_ALERT)(AlertBox)
+
+export default AlertBox
diff --git a/ui/src/components/App.js b/ui/src/components/App.js
new file mode 100644
index 0000000..a8c353f
--- /dev/null
+++ b/ui/src/components/App.js
@@ -0,0 +1,106 @@
+import gql from 'graphql-tag'
+import Helmet, { HelmetProvider } from 'react-helmet-async'
+import injectSheet from 'react-jss'
+import React, { Component } from 'react'
+import { withRouter } from 'react-router-dom'
+
+import * as mixins from 'styles/mixins'
+import * as Sentry from '@sentry/browser'
+import AppContext from 'components/AppContext'
+import AppLoader from 'components/AppLoader'
+import ExternalRouter from 'components/routers/ExternalRouter'
+import User from 'models/User'
+import { withClientQuery, withQuery } from 'lib/data'
+
+import GET_REFERRER from 'queries/referrer'
+import GET_TOKEN from 'queries/session'
+
+const isUserLoggedIn = session => Boolean(session && session.token)
+
+class App extends Component {
+ configureSentry = () => {
+ const { currentUser } = this.props
+
+ let user = {}
+
+ if (currentUser) {
+ user = {
+ id: user.id,
+ email: user.email,
+ name: User.fullName(user)
+ }
+ }
+
+ Sentry.configureScope((scope) => {
+ scope.setUser(user)
+ })
+ }
+
+ render() {
+ const { loading, currentUser, session, referrer } = this.props
+
+ // Logout issue: After session changes, 'loading' continues to be set to true
+ // even though the query is skipped. The issue was raised, but the fix seems to
+ // be only for the component.
+ // https://github.com/apollographql/react-apollo/issues/1869
+ if (isUserLoggedIn(session) && loading) {
+ return
+ }
+
+ this.configureSentry()
+
+ return (
+
+
+
+
+
+
+
+
+
+ )
+ }
+}
+
+App = injectSheet(() => {
+ const globalStyles = Object.keys(mixins.breakpoints).reduce((styles, breakpoint) => {
+ styles[`.hidden-${breakpoint}`] = mixins.responsiveProperties({
+ display: { [breakpoint]: 'none' }
+ })
+
+ styles[`.visible-${breakpoint}`] = mixins.responsiveProperties({
+ display: { [breakpoint]: 'block' }
+ })
+
+ return styles
+ }, {})
+
+ return {
+ '@global': globalStyles
+ }
+})(App)
+
+App = withQuery(gql`
+ query AppQuery {
+ currentUser {
+ id
+ email
+ firstName
+ lastName
+ profilePictureThumbnail
+ profilePictureNormal
+ }
+ }
+`, {
+ skip: ({ session }) => !isUserLoggedIn(session)
+})(App)
+
+App = withClientQuery(GET_TOKEN)(App)
+
+App = withClientQuery(GET_REFERRER)(App)
+
+export default withRouter(App)
diff --git a/ui/src/components/AppContext.js b/ui/src/components/AppContext.js
new file mode 100644
index 0000000..f54c8c4
--- /dev/null
+++ b/ui/src/components/AppContext.js
@@ -0,0 +1,3 @@
+import React from 'react'
+
+export default React.createContext()
diff --git a/ui/src/components/AppLoader.js b/ui/src/components/AppLoader.js
new file mode 100644
index 0000000..580cbd2
--- /dev/null
+++ b/ui/src/components/AppLoader.js
@@ -0,0 +1,27 @@
+import injectSheet from 'react-jss'
+import React from 'react'
+
+import loaderImage from 'images/loader.gif'
+
+function AppLoader({ classes }) {
+ return (
+
+
+
+ )
+}
+
+export default injectSheet(({ colors, zIndexes }) => ({
+ appLoader: {
+ backgroundColor: colors.emptyWrapperBackground,
+ display: 'flex',
+ alignItems: 'center',
+ justifyContent: 'center',
+ position: 'fixed',
+ top: 0,
+ right: 0,
+ bottom: 0,
+ left: 0,
+ zIndex: zIndexes.appLoader
+ }
+}))(AppLoader)
diff --git a/ui/src/components/BaseModal.js b/ui/src/components/BaseModal.js
new file mode 100644
index 0000000..43642f4
--- /dev/null
+++ b/ui/src/components/BaseModal.js
@@ -0,0 +1,78 @@
+import classNames from 'classnames'
+import injectSheet from 'react-jss'
+import Modal from 'react-modal'
+import PropTypes from 'prop-types'
+import React from 'react'
+
+import * as mixins from 'styles/mixins'
+
+const closeTransitionDuration = 300
+
+function BaseModal({
+ overlayBaseClassName,
+ overlayAfterOpenClassName,
+ overlayBeforeCloseClassName,
+ contentBaseClassName,
+ contentAfterOpenClassName,
+ contentBeforeCloseClassName,
+ classes,
+ children,
+ ...other
+}) {
+ return (
+
+ {children}
+
+ )
+}
+
+BaseModal.propTypes = {
+ contentLabel: PropTypes.string.isRequired, // For screenreaders
+ onRequestClose: PropTypes.func.isRequired
+}
+
+export default injectSheet(({ colors, zIndexes }) => ({
+ body: {
+ overflow: 'hidden'
+ },
+ overlay: {
+ ...mixins.transitionFluid(),
+
+ backgroundColor: colors.baseModalOverlayBackground,
+ opacity: 0,
+ position: 'fixed',
+ top: 0,
+ right: 0,
+ bottom: 0,
+ left: 0,
+ zIndex: zIndexes.modal
+ },
+ overlayAfterOpen: {
+ backdropFilter: 'blur(5px)',
+ opacity: 1
+ },
+ overlayBeforeClose: {
+ backdropFilter: 'blur(0)',
+ opacity: 0
+ },
+ content: {
+ ...mixins.transitionFluid(),
+
+ backgroundColor: colors.baseModalBackground
+ }
+}))(BaseModal)
diff --git a/ui/src/components/BaseSlider.js b/ui/src/components/BaseSlider.js
new file mode 100644
index 0000000..396de21
--- /dev/null
+++ b/ui/src/components/BaseSlider.js
@@ -0,0 +1,67 @@
+import injectSheet from 'react-jss'
+import PropTypes from 'prop-types'
+import React from 'react'
+import Slider from 'rc-slider'
+
+import * as mixins from 'styles/mixins'
+
+const barHeight = 2
+const knobSize = 20
+
+function BaseSlider({ handleStyle, trackStyle, railStyle, theme, classes, ...other }) {
+ const baseHandleStyle = {
+ ...mixins.size(knobSize),
+
+ backgroundColor: theme.colors.baseSliderHandleBackground,
+ borderColor: theme.colors.baseSliderHandleBorder,
+ borderStyle: 'solid',
+ borderWidth: 1,
+ boxShadow: theme.shadows.baseSliderHandle,
+ marginLeft: 0,
+ marginTop: 0,
+ transform: 'translateX(-50%)'
+ }
+
+ const baseTrackStyle = {
+ backgroundColor: theme.colors.baseSliderTrackBackground,
+ borderRadius: 0,
+ height: barHeight
+ }
+
+ const baseRailStyle = {
+ backgroundColor: theme.colors.baseSliderRailBackground,
+ borderRadius: 0,
+ height: barHeight
+ }
+
+ return (
+
+ )
+}
+
+BaseSlider.propTypes = {
+ handleStyle: PropTypes.object,
+ trackStyle: PropTypes.object,
+ railStyle: PropTypes.object
+}
+
+BaseSlider.defaultProps = {
+ handleStyle: null,
+ trackStyle: null,
+ railStyle: null
+}
+
+export default injectSheet(() => ({
+ slider: {
+ alignItems: 'center',
+ cursor: 'pointer',
+ display: 'flex'
+ }
+}))(BaseSlider)
diff --git a/ui/src/components/ClientProvider.js b/ui/src/components/ClientProvider.js
new file mode 100644
index 0000000..f12da14
--- /dev/null
+++ b/ui/src/components/ClientProvider.js
@@ -0,0 +1,52 @@
+import { Component } from 'react'
+import { CachePersistor } from 'apollo-cache-persist'
+
+import cache from 'client/cache'
+import client from 'client'
+
+const SCHEMA_VERSION = '1'
+const SCHEMA_VERSION_KEY = 'apollo-schema-version'
+
+const storage = window.localStorage
+
+class ClientProvider extends Component {
+ constructor() {
+ super()
+
+ this.state = {
+ apolloClient: null
+ }
+ }
+
+ componentDidMount() {
+ const persistor = new CachePersistor({
+ cache,
+ storage
+ })
+
+ const currentSchemaVersion = this.getSchemaVersion()
+
+ if (currentSchemaVersion === SCHEMA_VERSION) {
+ persistor.restore().then(this.setClient)
+ } else {
+ persistor.purge().then(() => {
+ this.setSchemaVersion()
+ this.setClient()
+ })
+ }
+ }
+
+ setSchemaVersion = () => storage.setItem(SCHEMA_VERSION_KEY, SCHEMA_VERSION)
+
+ getSchemaVersion = () => storage.getItem(SCHEMA_VERSION_KEY)
+
+ setClient = () => this.setState({ apolloClient: client })
+
+ render() {
+ const { children } = this.props
+
+ return children(this.state)
+ }
+}
+
+export default ClientProvider
diff --git a/ui/src/components/Container.js b/ui/src/components/Container.js
new file mode 100644
index 0000000..d234bc2
--- /dev/null
+++ b/ui/src/components/Container.js
@@ -0,0 +1,20 @@
+import injectSheet from 'react-jss'
+import React from 'react'
+
+function Container({ classes, children }) {
+ return (
+
+ {children}
+
+ )
+}
+
+export default injectSheet(({ units }) => ({
+ container: {
+ marginLeft: 'auto',
+ marginRight: 'auto',
+ paddingLeft: units.containerHorizontalPadding,
+ paddingRight: units.containerHorizontalPadding,
+ width: units.containerWidth + (2 * units.containerHorizontalPadding)
+ }
+}))(Container)
diff --git a/ui/src/components/FieldError.js b/ui/src/components/FieldError.js
new file mode 100644
index 0000000..941ea16
--- /dev/null
+++ b/ui/src/components/FieldError.js
@@ -0,0 +1,63 @@
+import injectSheet from 'react-jss'
+import PropTypes from 'prop-types'
+import React from 'react'
+
+import * as mixins from 'styles/mixins'
+import { Paragraph } from 'components/typography'
+
+function FieldError({ error, classes }) {
+ if (!error) {
+ return null
+ }
+
+ return (
+
+ )
+}
+
+FieldError.propTypes = {
+ active: PropTypes.bool,
+ error: PropTypes.oneOfType([ PropTypes.string, PropTypes.bool ]),
+ stretched: PropTypes.bool
+}
+
+FieldError.defaultProps = {
+ active: false,
+ error: null,
+ stretched: false
+}
+
+export default injectSheet(({ colors, units }) => ({
+ fieldError: {
+ paddingTop: units.fieldErrorVerticalPadding,
+ paddingRight: units.fieldErrorHorizontalPadding,
+ paddingBottom: units.fieldErrorVerticalPadding,
+ paddingLeft: units.fieldErrorHorizontalPadding,
+ position: 'absolute',
+ top: '100%',
+ right: 0,
+ left: 0,
+
+ '&::before': {
+ ...mixins.transitionSimple(),
+
+ backgroundColor: colors.fieldErrorBackground,
+ content: "' '",
+ marginRight: ({ active, stretched }) => (
+ (active && stretched) ? units.inputBorderMarginHorizontal_focus : 0
+ ),
+ marginLeft: ({ active, stretched }) => (
+ (active && stretched) ? units.inputBorderMarginHorizontal_focus : 0
+ ),
+ position: 'absolute',
+ top: 0,
+ right: 0,
+ bottom: 0,
+ left: 0
+ }
+ }
+}))(FieldError)
diff --git a/ui/src/components/FieldHint.js b/ui/src/components/FieldHint.js
new file mode 100644
index 0000000..5b9d768
--- /dev/null
+++ b/ui/src/components/FieldHint.js
@@ -0,0 +1,45 @@
+import injectSheet from 'react-jss'
+import PropTypes from 'prop-types'
+import React from 'react'
+
+import FontIcon from 'components/FontIcon'
+
+function FieldHint({ active, hint, classes }) {
+ if (!hint || !active) {
+ return null
+ }
+
+ return (
+
+
+ {hint}
+
+ )
+}
+
+FieldHint.propTypes = {
+ active: PropTypes.bool,
+ hint: PropTypes.string
+}
+
+FieldHint.defaultProps = {
+ active: false,
+ hint: null
+}
+
+export default injectSheet(({ colors, typography, units }) => ({
+ fieldHint: {
+ ...typography.lightSmall,
+
+ color: colors.text_pale,
+ paddingLeft: units.fieldHintPaddingLeft,
+ position: 'absolute',
+ top: `calc(100% + ${units.fieldHintTop}px)`,
+
+ '& .icon': {
+ lineHeight: `${typography.lightSmall.lineHeight * typography.lightSmall.fontSize}px`,
+ position: 'absolute',
+ left: 0
+ }
+ }
+}))(FieldHint)
diff --git a/ui/src/components/FontIcon.js b/ui/src/components/FontIcon.js
new file mode 100644
index 0000000..e403f69
--- /dev/null
+++ b/ui/src/components/FontIcon.js
@@ -0,0 +1,33 @@
+import PropTypes from 'prop-types'
+import React from 'react'
+
+const FontIcon = React.forwardRef(({ name, size, ...other }, ref) => (
+
+))
+
+FontIcon.sizes = {
+ nano: 6,
+ micro: 8,
+ milli: 10,
+ tiny: 12,
+ small: 16,
+ medium: 24,
+ large: 32,
+ extraLarge: 40
+}
+
+FontIcon.propTypes = {
+ name: PropTypes.string.isRequired,
+ size: PropTypes.oneOf(Object.keys(FontIcon.sizes))
+}
+
+FontIcon.defaultProps = {
+ size: 'medium'
+}
+
+export default FontIcon
diff --git a/ui/src/components/ItemBar.js b/ui/src/components/ItemBar.js
new file mode 100644
index 0000000..1f4dfc8
--- /dev/null
+++ b/ui/src/components/ItemBar.js
@@ -0,0 +1,48 @@
+import injectSheet from 'react-jss'
+import PropTypes from 'prop-types'
+import React from 'react'
+
+const gutterSizes = {
+ tiny: 5,
+ small: 10,
+ medium: 15,
+ large: 20
+}
+
+function ItemBar({ classes, children }) {
+ return (
+
+ {children}
+
+ )
+}
+
+ItemBar.propTypes = {
+ children: PropTypes.node.isRequired,
+ gutter: PropTypes.oneOf(Object.keys(gutterSizes)),
+ justifyContent: PropTypes.string,
+ reversed: PropTypes.bool
+}
+
+ItemBar.defaultProps = {
+ gutter: 'tiny',
+ justifyContent: 'space-between',
+ reversed: false
+}
+
+export default injectSheet(() => ({
+ itemBar: {
+ alignItems: 'center',
+ display: 'flex',
+ flexDirection: ({ reversed }) => (reversed ? 'row-reverse' : 'row'),
+ justifyContent: ({ justifyContent }) => justifyContent,
+
+ '& > *': {
+ marginRight: ({ gutter }) => gutterSizes[gutter]
+ },
+
+ '& > *:last-child': {
+ marginRight: 0
+ }
+ }
+}))(ItemBar)
diff --git a/ui/src/components/LoaderView.js b/ui/src/components/LoaderView.js
new file mode 100644
index 0000000..7a4d3c0
--- /dev/null
+++ b/ui/src/components/LoaderView.js
@@ -0,0 +1,40 @@
+import classNames from 'classnames'
+import injectSheet from 'react-jss'
+import PropTypes from 'prop-types'
+import React from 'react'
+
+import loaderImage from 'images/loader.gif'
+
+function LoaderView({ overlay, classes }) {
+ return (
+
+
+
+ )
+}
+
+LoaderView.propTypes = {
+ overlay: PropTypes.bool
+}
+
+LoaderView.defaultProps = {
+ overlay: false
+}
+
+export default injectSheet(() => ({
+ loaderView: {
+ display: 'flex',
+ alignItems: 'center',
+ justifyContent: 'center',
+ height: 150
+ },
+ loaderView_overlay: {
+ background: 'rgba(255, 255, 255, 0.8)',
+ position: 'absolute',
+ top: 0,
+ left: 0,
+ width: '100%',
+ height: '100%',
+ zIndex: 1
+ }
+}))(LoaderView)
diff --git a/ui/src/components/Logo.js b/ui/src/components/Logo.js
new file mode 100644
index 0000000..c21d259
--- /dev/null
+++ b/ui/src/components/Logo.js
@@ -0,0 +1,34 @@
+import injectSheet from 'react-jss'
+import PropTypes from 'prop-types'
+import React from 'react'
+
+import * as mixins from 'styles/mixins'
+import resolveImage from 'lib/resolveImage'
+
+const sizes = {
+ symbol_normal: [ 35, 54 ],
+ text_normal: [ 89, 40 ],
+ text_large: [ 134, 60 ]
+}
+
+function Logo({ type, variant, classes }) {
+ return
+}
+
+Logo.propTypes = {
+ size: PropTypes.oneOf([ 'normal', 'large' ]),
+ type: PropTypes.oneOf([ 'symbol', 'text' ]),
+ variant: PropTypes.oneOf([ 'color', 'white' ])
+}
+
+Logo.defaultProps = {
+ size: 'normal',
+ type: 'text',
+ variant: 'white'
+}
+
+export default injectSheet(() => ({
+ logo: ({ size, type }) => ({
+ ...mixins.size(...sizes[`${type}_${size}`])
+ })
+}))(Logo)
diff --git a/ui/src/components/ScrollToTop.js b/ui/src/components/ScrollToTop.js
new file mode 100644
index 0000000..daa03fb
--- /dev/null
+++ b/ui/src/components/ScrollToTop.js
@@ -0,0 +1,26 @@
+import { Component } from 'react'
+import { withRouter } from 'react-router-dom'
+
+class ScrollToTop extends Component {
+ // For initial page load
+ componentDidMount() {
+ window.scrollTo(0, 0)
+ }
+
+ // For route transitions
+ componentDidUpdate({ location: prevLocation }) {
+ const { location } = this.props
+
+ if (location !== prevLocation) {
+ window.scrollTo(0, 0)
+ }
+ }
+
+ render() {
+ const { children } = this.props
+
+ return children
+ }
+}
+
+export default withRouter(ScrollToTop)
diff --git a/ui/src/components/Spacer.js b/ui/src/components/Spacer.js
new file mode 100644
index 0000000..7c4b0a5
--- /dev/null
+++ b/ui/src/components/Spacer.js
@@ -0,0 +1,18 @@
+import PropTypes from 'prop-types'
+import React from 'react'
+
+function Spacer({ height, width }) {
+ return
+}
+
+Spacer.propTypes = {
+ height: PropTypes.oneOfType([ PropTypes.string, PropTypes.number ]),
+ width: PropTypes.oneOfType([ PropTypes.string, PropTypes.number ])
+}
+
+Spacer.defaultProps = {
+ height: 0,
+ width: 0
+}
+
+export default Spacer
diff --git a/ui/src/components/buttons/CloseButton.js b/ui/src/components/buttons/CloseButton.js
new file mode 100644
index 0000000..256a7c3
--- /dev/null
+++ b/ui/src/components/buttons/CloseButton.js
@@ -0,0 +1,25 @@
+import injectSheet from 'react-jss'
+import PropTypes from 'prop-types'
+import React from 'react'
+
+import FontIcon from 'components/FontIcon'
+
+function CloseButton({ onClick, classes }) {
+ return (
+
+
+
+ )
+}
+
+CloseButton.propTypes = {
+ onClick: PropTypes.func.isRequired
+}
+
+export default injectSheet(({ colors }) => ({
+ close: {
+ color: colors.closeIcon,
+ cursor: 'pointer',
+ fontSize: 0 /* clear line-height that causes extra spacing */
+ }
+}))(CloseButton)
diff --git a/ui/src/components/buttons/DragButton.js b/ui/src/components/buttons/DragButton.js
new file mode 100644
index 0000000..e25d9ff
--- /dev/null
+++ b/ui/src/components/buttons/DragButton.js
@@ -0,0 +1,20 @@
+import classNames from 'classnames'
+import injectSheet from 'react-jss'
+import React from 'react'
+
+import FontIcon from 'components/FontIcon'
+
+function DragButton({ classes }) {
+ return (
+
+
+
+ )
+}
+
+export default injectSheet(({ colors }) => ({
+ dragHandle: {
+ color: colors.text_pale,
+ fontSize: 0 /* clear line-height that causes extra spacing */
+ }
+}))(DragButton)
diff --git a/ui/src/components/buttons/FilledButton.js b/ui/src/components/buttons/FilledButton.js
new file mode 100644
index 0000000..7fff4c9
--- /dev/null
+++ b/ui/src/components/buttons/FilledButton.js
@@ -0,0 +1,161 @@
+import classNames from 'classnames'
+import injectSheet from 'react-jss'
+import PropTypes from 'prop-types'
+import React from 'react'
+
+import * as mixins from 'styles/mixins'
+import cleanProps from 'lib/cleanProps'
+
+function FilledButton({ isActive, disabled, label, size, variant, classes, ...other }) {
+ return (
+
+ {label}
+
+ )
+}
+
+FilledButton.propTypes = {
+ isActive: PropTypes.bool,
+ disabled: PropTypes.bool,
+ label: PropTypes.string.isRequired,
+ size: PropTypes.oneOf([ 'tiny', 'small', 'normal', 'large' ]),
+ type: PropTypes.oneOf([ 'submit', 'button' ]),
+ variant: PropTypes.oneOf([ 'clear', 'color', 'flat' ])
+}
+
+FilledButton.defaultProps = {
+ isActive: false,
+ disabled: false,
+ size: 'normal',
+ type: 'submit',
+ variant: 'color'
+}
+
+export default injectSheet(({ colors, gradients, shadows, typography, units }) => ({
+ button: {
+ ...mixins.transitionSimple(),
+
+ borderRadius: units.buttonBorderRadius,
+ cursor: 'pointer',
+ paddingTop: 0,
+ paddingBottom: 0,
+ whiteSpace: 'nowrap',
+
+ '&[disabled]': {
+ pointerEvents: 'none'
+ },
+
+ '&:not(:last-child)': {
+ marginRight: units.buttonMarginRight
+ }
+ },
+ button_tiny: {
+ ...typography.regularSmallSpaced,
+
+ height: units.buttonHeight_tiny,
+ minWidth: units.buttonMinWidth_tiny,
+ paddingRight: units.buttonHorizontalPadding_tiny,
+ paddingLeft: units.buttonHorizontalPadding_tiny
+ },
+ button_small: {
+ ...typography.bold,
+
+ height: units.buttonHeight_small,
+ minWidth: units.buttonMinWidth_small,
+ paddingRight: units.buttonHorizontalPadding_normal,
+ paddingLeft: units.buttonHorizontalPadding_normal
+ },
+ button_normal: {
+ ...typography.bold,
+
+ height: units.buttonHeight_normal,
+ minWidth: units.buttonMinWidth_normal,
+ paddingRight: units.buttonHorizontalPadding_normal,
+ paddingLeft: units.buttonHorizontalPadding_normal
+ },
+ button_large: {
+ ...typography.bold,
+
+ height: units.buttonHeight_large,
+ minWidth: units.buttonMinWidth_large,
+ paddingRight: units.buttonHorizontalPadding_normal,
+ paddingLeft: units.buttonHorizontalPadding_normal
+ },
+ button_clear: {
+ ...typography.regularSmallSpaced,
+
+ background: 'none',
+ border: 'none',
+ color: colors.text_pale
+ },
+ button_color: {
+ backgroundImage: gradients.button,
+ borderWidth: 0,
+ boxShadow: ({ size }) => size !== 'tiny' && shadows.button_color,
+ color: colors.text_light,
+ position: 'relative',
+ textTransform: 'uppercase',
+ zIndex: 0,
+
+ '&[disabled]': {
+ boxShadow: 'none',
+ opacity: 0.3
+ },
+
+ '&::after': {
+ ...mixins.transitionSimple(),
+
+ backgroundImage: gradients.button_colorHover,
+ borderRadius: units.buttonBorderRadius,
+ content: '" "',
+ opacity: 0,
+ position: 'absolute',
+ top: 0,
+ right: 0,
+ bottom: 0,
+ left: 0,
+ zIndex: -1
+ },
+
+ '&:hover, &:focus': {
+ boxShadow: shadows.button_hover,
+
+ '&::after': {
+ opacity: 1
+ }
+ }
+ },
+ button_flat: {
+ ...typography.mediumSquished,
+
+ backgroundColor: colors.buttonBackground_flat,
+ borderColor: colors.buttonBorder_flat,
+ borderStyle: 'solid',
+ borderWidth: 1,
+ color: colors.text_dark,
+
+ '&[disabled]': {
+ color: colors.text_darkDisabled
+ },
+
+ '&:hover, &:focus': {
+ backgroundColor: colors.button_flatHover,
+ borderColor: 'transparent',
+ boxShadow: shadows.button_flatHover
+ }
+ },
+ button_flatActive: {
+ backgroundColor: colors.button_flatHover,
+ borderColor: 'transparent',
+ boxShadow: shadows.button_flatHover
+ }
+}))(FilledButton)
diff --git a/ui/src/components/decorators/withUniqueId.js b/ui/src/components/decorators/withUniqueId.js
new file mode 100644
index 0000000..ee22327
--- /dev/null
+++ b/ui/src/components/decorators/withUniqueId.js
@@ -0,0 +1,38 @@
+import hoistNonReactStatics from 'hoist-non-react-statics'
+import React, { Component } from 'react'
+
+let nextId = 0
+
+function withUniqueId() {
+ return (WrappedComponent) => {
+ class EnhancedComponent extends Component {
+ constructor(props) {
+ super(props)
+
+ this.cache = {}
+ }
+
+ uniqueId = (keyStr) => {
+ if (!Object.prototype.hasOwnProperty.call(this.cache, keyStr)) {
+ nextId += 1
+ this.cache[keyStr] = `uid-${keyStr}-${nextId}`
+ }
+ return this.cache[keyStr]
+ }
+
+ render() {
+ const wrappedComponentProps = Object.assign({}, this.props, {
+ uniqueId: this.uniqueId
+ })
+
+ return
+ }
+ }
+
+ hoistNonReactStatics(EnhancedComponent, WrappedComponent)
+
+ return EnhancedComponent
+ }
+}
+
+export default withUniqueId
diff --git a/ui/src/components/external/FieldError.js b/ui/src/components/external/FieldError.js
new file mode 100644
index 0000000..de8a8b8
--- /dev/null
+++ b/ui/src/components/external/FieldError.js
@@ -0,0 +1,61 @@
+import injectSheet from 'react-jss'
+import PropTypes from 'prop-types'
+import React from 'react'
+
+import * as mixins from 'styles/mixins'
+import { FieldErrorText } from 'components/external/typography'
+
+function FieldError({ error, classes }) {
+ if (!error) {
+ return null
+ }
+
+ return (
+
+ )
+}
+
+FieldError.propTypes = {
+ error: PropTypes.oneOfType([ PropTypes.string, PropTypes.bool ])
+}
+
+FieldError.defaultProps = {
+ error: null
+}
+
+export default injectSheet(({ colors, units }) => ({
+ fieldError: {
+ backgroundColor: colors.externalFieldErrorBackground,
+ color: colors.text_primary,
+ marginLeft: units.externalFieldErrorShiftLeft,
+ marginTop: units.externalFieldErrorShiftTop,
+ paddingTop: units.externalFieldErrorVerticalPadding,
+ paddingRight: units.externalFieldErrorHorizontalPadding,
+ paddingBottom: units.externalFieldErrorVerticalPadding,
+ paddingLeft: units.externalFieldErrorHorizontalPadding,
+ position: 'absolute',
+ top: '100%'
+ },
+ arrow: {
+ ...mixins.size(units.externalFieldErrorArrowSize),
+
+ position: 'absolute',
+ top: -0.5 * units.externalFieldErrorArrowSize,
+ left: units.externalFieldErrorArrowShiftHorizontal,
+
+ '&::before': {
+ ...mixins.size('100%'),
+
+ backgroundColor: colors.externalFieldErrorBackground,
+ borderRadius: 1,
+ content: '" "',
+ display: 'block',
+ transform: `translateY(${units.externalFieldErrorArrowShiftVertical}px) rotate(45deg)`
+ }
+ }
+}))(FieldError)
diff --git a/ui/src/components/external/Footer.js b/ui/src/components/external/Footer.js
new file mode 100644
index 0000000..59d0424
--- /dev/null
+++ b/ui/src/components/external/Footer.js
@@ -0,0 +1,71 @@
+import injectSheet from 'react-jss'
+import React from 'react'
+
+import * as mixins from 'styles/mixins'
+import GridContainer from 'components/external/GridContainer'
+import GridItem from 'components/external/GridItem'
+import { FooterLink, FooterText } from 'components/external/typography'
+
+function Footer({ classes }) {
+ return (
+
+
+
+
+
+ Terms of Service
+
+
+
+ Privacy Policy
+
+
+
+
+ ©
+ {' '}
+ {new Date().getFullYear()}
+ {' '}
+ KeepWorks Technologies Pvt. Ltd.
+
+
+
+
+ )
+}
+
+export default injectSheet(({ colors, units }) => ({
+ footerTextWrapper: {
+ ...mixins.responsiveProperties({
+ alignItems: { xs: 'center' },
+ flexDirection: { xs: 'column', xl: 'row' },
+ justifyContent: { xs: 'center', xl: 'space-between' },
+ paddingTop: units.externalFooterPaddingTopResponsive,
+ paddingBottom: units.externalFooterPaddingBottomResponsive
+ }),
+
+ display: 'flex',
+
+ '& a': {
+ ...mixins.responsiveProperty('marginRight', units.externalFooterLinksMarginRightResponsive),
+
+ '&:last-of-type': {
+ marginRight: 0
+ }
+ }
+ },
+ footerLinkWrapper: {
+ ...mixins.responsiveProperty('marginBottom', units.externalFooterLinkWrapperMarginBottomResponsive),
+
+ alignItems: 'center',
+ display: 'flex'
+ },
+ verticalDivider: {
+ ...mixins.responsiveProperty('display', { xs: 'block', xl: 'none' }),
+ ...mixins.size(1, units.externalVerticalDividerHeight),
+
+ backgroundColor: colors.externalVerticalDividerBackground,
+ marginRight: units.externalVerticalDividerHorizontalMargin,
+ marginLeft: units.externalVerticalDividerHorizontalMargin
+ }
+}))(Footer)
diff --git a/ui/src/components/external/GridContainer.js b/ui/src/components/external/GridContainer.js
new file mode 100644
index 0000000..54b3795
--- /dev/null
+++ b/ui/src/components/external/GridContainer.js
@@ -0,0 +1,31 @@
+import injectSheet from 'react-jss'
+import PropTypes from 'prop-types'
+import React from 'react'
+
+import * as mixins from 'styles/mixins'
+
+function GridContainer({ classes, children }) {
+ return (
+
+ {children}
+
+ )
+}
+
+GridContainer.propTypes = {
+ children: PropTypes.node.isRequired
+}
+
+export default injectSheet(({ units }) => ({
+ gridContainer: {
+ ...mixins.responsiveProperties({
+ alignItems: { sm: 'center' },
+ display: { xs: 'block', md: 'grid' },
+ gridColumnGap: { sm: '20px' },
+ marginRight: { xs: 20, md: 'auto' },
+ marginLeft: { xs: 20, md: 'auto' },
+ gridTemplateColumns: { md: 'repeat(15, 1fr)' },
+ maxWidth: units.externalGridContainerMaxWidthResponsive
+ })
+ }
+}))(GridContainer)
diff --git a/ui/src/components/external/GridItem.js b/ui/src/components/external/GridItem.js
new file mode 100644
index 0000000..311475d
--- /dev/null
+++ b/ui/src/components/external/GridItem.js
@@ -0,0 +1,29 @@
+import injectSheet from 'react-jss'
+import PropTypes from 'prop-types'
+import React from 'react'
+
+function GridItem({ classes, children }) {
+ return (
+
+ {children}
+
+ )
+}
+
+GridItem.propTypes = {
+ start: PropTypes.oneOfType([ PropTypes.number, PropTypes.string ]),
+ end: PropTypes.oneOfType([ PropTypes.number, PropTypes.string ]),
+ children: PropTypes.node.isRequired
+}
+
+GridItem.defaultProps = {
+ start: 1,
+ end: 16
+}
+
+export default injectSheet(() => ({
+ gridItem: ({ start, end }) => ({
+ gridColumnStart: start,
+ gridColumnEnd: end
+ })
+}))(GridItem)
diff --git a/ui/src/components/external/Header.js b/ui/src/components/external/Header.js
new file mode 100644
index 0000000..8748678
--- /dev/null
+++ b/ui/src/components/external/Header.js
@@ -0,0 +1,72 @@
+import injectSheet from 'react-jss'
+import React, { Fragment } from 'react'
+import { Link, matchPath } from 'react-router-dom'
+
+import * as mixins from 'styles/mixins'
+import GridContainer from 'components/external/GridContainer'
+import GridItem from 'components/external/GridItem'
+import Logo from 'components/Logo'
+import { NavLink } from 'components/external/typography'
+
+function Header({ classes }) {
+ const isHome = matchPath(document.location.pathname, { path: '/', exact: true })
+
+ const navLinks = (
+
+
+ Log in
+
+
+ Sign up
+
+
+ )
+
+ const homeLink = (
+
+
+
+
+
+
+
+
+ )
+
+ return (
+
+
+
+ {!isHome && homeLink}
+ {navLinks}
+
+
+
+ )
+}
+
+export default injectSheet(({ units }) => ({
+ header: {
+ ...mixins.responsiveProperties({
+ paddingTop: units.externalHeaderPaddingTopResponsive,
+ paddingBottom: units.externalHeaderPaddingBottomResponsive
+ }),
+
+ alignItems: 'center',
+ display: 'flex',
+ justifyContent: 'flex-end',
+
+ '& > :not(:last-child)': {
+ marginRight: units.externalHeaderLinksMarginRight
+ },
+
+ '& $homeLink': { // To override :not(last-child) selector priority
+ marginRight: 'auto'
+ }
+ },
+ homeLink: {
+ },
+ homeLink_small: {
+ ...mixins.responsiveProperty('display', { xs: 'block', xl: 'none' })
+ }
+}))(Header)
diff --git a/ui/src/components/external/buttons/SimpleButton.js b/ui/src/components/external/buttons/SimpleButton.js
new file mode 100644
index 0000000..57c5411
--- /dev/null
+++ b/ui/src/components/external/buttons/SimpleButton.js
@@ -0,0 +1,56 @@
+import injectSheet from 'react-jss'
+import PropTypes from 'prop-types'
+import React from 'react'
+
+import * as mixins from 'styles/mixins'
+
+function SimpleButton({ disabled, label, classes }) {
+ return (
+
+ {label}
+
+ )
+}
+
+SimpleButton.propTypes = {
+ disabled: PropTypes.bool,
+ label: PropTypes.string.isRequired
+}
+
+SimpleButton.defaultProps = {
+ disabled: false
+}
+
+export default injectSheet(({
+ colors, shadows, typography, units
+}) => ({
+ button: {
+ ...mixins.transitionSimple(),
+ ...typography.bold,
+
+ backgroundColor: colors.externalButtonBackground,
+ borderRadius: units.externalButtonBorderRadius,
+ borderWidth: 0,
+ boxShadow: shadows.externalButton,
+ color: colors.text_darker,
+ cursor: 'pointer',
+ height: units.externalButtonHeight,
+ minWidth: units.externalButtonWidth,
+ textTransform: 'uppercase',
+ paddingTop: 0,
+ paddingRight: units.externalButtonHorizontalPadding,
+ paddingBottom: 0,
+ paddingLeft: units.externalButtonHorizontalPadding,
+
+ '&[disabled]': {
+ backgroundColor: colors.externalButtonBackground_disabled,
+ boxShadow: 'none',
+ color: colors.text_primary,
+ pointerEvents: 'none'
+ },
+
+ '&:hover, &:focus': {
+ color: colors.text_primary
+ }
+ }
+}))(SimpleButton)
diff --git a/ui/src/components/external/inputs/TextInput.js b/ui/src/components/external/inputs/TextInput.js
new file mode 100644
index 0000000..75b919b
--- /dev/null
+++ b/ui/src/components/external/inputs/TextInput.js
@@ -0,0 +1,69 @@
+import injectSheet from 'react-jss'
+import PropTypes from 'prop-types'
+import React from 'react'
+
+import * as mixins from 'styles/mixins'
+import cleanProps from 'lib/cleanProps'
+import FieldError from 'components/external/FieldError'
+import Input from 'components/inputs/TextInput'
+
+function TextInput({
+ input,
+ meta,
+ type,
+ classes,
+ ...other
+}) {
+ return (
+
+
+
+
+
+ )
+}
+
+TextInput.propTypes = {
+ type: PropTypes.string
+}
+
+TextInput.defaultProps = {
+ type: 'text'
+}
+
+export default injectSheet(({
+ colors, typography, units
+}) => ({
+ wrapper: {
+ position: 'relative',
+ width: '100%'
+ },
+ input: {
+ ...typography.regularSquishedResponsive,
+
+ ...mixins.placeholder({
+ ...mixins.transitionSimple(),
+
+ color: colors.externalInputPlaceholder
+ }),
+ ...mixins.size('100%', units.externalInputHeight),
+ ...mixins.transitionSimple(),
+
+ backgroundColor: colors.externalInputBackground,
+ borderColor: ({ meta }) => (
+ Input.fieldError(meta) ? colors.externalFieldErrorBackground : 'transparent'
+ ),
+ borderRadius: units.externalInputBorderRadius,
+ borderStyle: 'solid',
+ borderWidth: 1,
+ color: colors.externalInputText,
+ paddingRight: units.externalInputPaddingRight,
+ paddingLeft: units.externalInputPaddingLeft,
+
+ '&:focus': {
+ ...mixins.placeholder({
+ opacity: 0.2
+ })
+ }
+ }
+}))(TextInput)
diff --git a/ui/src/components/external/typography/FieldErrorText.js b/ui/src/components/external/typography/FieldErrorText.js
new file mode 100644
index 0000000..b01aa3a
--- /dev/null
+++ b/ui/src/components/external/typography/FieldErrorText.js
@@ -0,0 +1,18 @@
+import React from 'react'
+
+import BaseText from 'components/typography/BaseText'
+
+function FieldErrorText({ children, ...other }) {
+ return (
+
+ {children}
+
+ )
+}
+
+export default FieldErrorText
diff --git a/ui/src/components/external/typography/FooterLink.js b/ui/src/components/external/typography/FooterLink.js
new file mode 100644
index 0000000..f0c8b1d
--- /dev/null
+++ b/ui/src/components/external/typography/FooterLink.js
@@ -0,0 +1,43 @@
+import injectSheet from 'react-jss'
+import PropTypes from 'prop-types'
+import React from 'react'
+import { NavLink } from 'react-router-dom'
+
+import * as mixins from 'styles/mixins'
+
+function FooterLink({ to, children, classes }) {
+ return (
+
+ {children}
+
+ )
+}
+
+FooterLink.propTypes = {
+ to: PropTypes.oneOfType([ PropTypes.string, PropTypes.object ]),
+ children: PropTypes.node.isRequired
+}
+
+FooterLink.defaultProps = {
+ to: null
+}
+
+export default injectSheet(({ colors, typography }) => ({
+ footerLink: {
+ ...mixins.transitionSimple(),
+ ...typography.regularSmallSquishedResponsive,
+
+ color: colors.text_pale,
+
+ '&:hover': {
+ color: colors.text_darker
+ }
+ },
+ footerLink_active: {
+ color: colors.text_primary,
+
+ '&:hover': {
+ color: colors.text_primary
+ }
+ }
+}))(FooterLink)
diff --git a/ui/src/components/external/typography/FooterText.js b/ui/src/components/external/typography/FooterText.js
new file mode 100644
index 0000000..1f0e4d4
--- /dev/null
+++ b/ui/src/components/external/typography/FooterText.js
@@ -0,0 +1,18 @@
+import React from 'react'
+
+import BaseText from 'components/typography/BaseText'
+
+function FooterText({ children, ...other }) {
+ return (
+
+ {children}
+
+ )
+}
+
+export default FooterText
diff --git a/ui/src/components/external/typography/Heading.js b/ui/src/components/external/typography/Heading.js
new file mode 100644
index 0000000..dee2e25
--- /dev/null
+++ b/ui/src/components/external/typography/Heading.js
@@ -0,0 +1,28 @@
+import injectSheet from 'react-jss'
+import React from 'react'
+
+import * as mixins from 'styles/mixins'
+import BaseText from 'components/typography/BaseText'
+
+function Heading({ children, classes, ...other }) {
+ return (
+
+ {children}
+
+ )
+}
+
+export default injectSheet(({ units }) => ({
+ heading: {
+ ...mixins.responsiveProperties({
+ marginTop: units.externalHeadingMarginTopResponsive,
+ width: units.externalHeadingWidthResponsive
+ })
+ }
+}))(Heading)
diff --git a/ui/src/components/external/typography/NavLink.js b/ui/src/components/external/typography/NavLink.js
new file mode 100644
index 0000000..4ad4979
--- /dev/null
+++ b/ui/src/components/external/typography/NavLink.js
@@ -0,0 +1,66 @@
+import classNames from 'classnames'
+import injectSheet from 'react-jss'
+import PropTypes from 'prop-types'
+import React from 'react'
+import { Link } from 'react-router-dom'
+
+import * as mixins from 'styles/mixins'
+
+function NavLink({
+ className, isButton, to, children, classes
+}) {
+ return (
+
+ {children}
+
+ )
+}
+
+NavLink.propTypes = {
+ className: PropTypes.string,
+ isButton: PropTypes.bool,
+ to: PropTypes.oneOfType([ PropTypes.string, PropTypes.object ]),
+ children: PropTypes.node.isRequired
+}
+
+NavLink.defaultProps = {
+ className: null,
+ isButton: false,
+ to: null
+}
+
+export default injectSheet(({
+ colors, shadows, typography, units
+}) => ({
+ navLink: {
+ ...mixins.animateUnderline({ bottom: -3 }),
+ ...typography.semiboldSmallResponsive,
+
+ color: colors.text_light,
+ textTransform: 'uppercase'
+ },
+ navLink_button: {
+ ...mixins.transitionSimple(),
+ ...typography.semiboldSmallResponsive,
+
+ alignItems: 'center',
+ backgroundColor: '#fff',
+ borderRadius: units.externalButtonBorderRadius,
+ boxShadow: shadows.externalButton,
+ color: colors.text_darker,
+ cursor: 'pointer',
+ display: 'flex',
+ height: units.externalButtonHeight,
+ justifyContent: 'center',
+ minWidth: units.externalButtonWidth,
+ paddingTop: 0,
+ paddingRight: units.externalNavLinkHorizontalPadding_button,
+ paddingBottom: 0,
+ paddingLeft: units.externalNavLinkHorizontalPadding_button,
+ textTransform: 'uppercase',
+
+ '&:hover, &:focus': {
+ color: colors.text_primary
+ }
+ }
+}))(NavLink)
diff --git a/ui/src/components/external/typography/PageHeading.js b/ui/src/components/external/typography/PageHeading.js
new file mode 100644
index 0000000..de33189
--- /dev/null
+++ b/ui/src/components/external/typography/PageHeading.js
@@ -0,0 +1,28 @@
+import injectSheet from 'react-jss'
+import React from 'react'
+
+import * as mixins from 'styles/mixins'
+import BaseText from 'components/typography/BaseText'
+
+function PageHeading({ children, classes, ...other }) {
+ return (
+
+ {children}
+
+ )
+}
+
+export default injectSheet(({ units }) => ({
+ pageHeading: {
+ ...mixins.responsiveProperties({
+ marginTop: units.externalPageHeadingMarginTopResponsive,
+ marginBottom: units.externalPageHeadingMarginBottomResponsive
+ })
+ }
+}))(PageHeading)
diff --git a/ui/src/components/external/typography/PageLink.js b/ui/src/components/external/typography/PageLink.js
new file mode 100644
index 0000000..1109130
--- /dev/null
+++ b/ui/src/components/external/typography/PageLink.js
@@ -0,0 +1,21 @@
+import injectSheet from 'react-jss'
+import React from 'react'
+
+import * as mixins from 'styles/mixins'
+import { TextLink as BaseTextLink } from 'components/typography'
+
+function PageLink({ classes, ...other }) {
+ return (
+
+ )
+}
+
+export default injectSheet(({ colors, typography }) => ({
+ pageLink: {
+ ...mixins.animateUnderline({ color: colors.text_primary, bottom: -2 }),
+ ...typography.semiboldSquishedResponsive,
+
+ color: colors.text_primary,
+ display: 'inline-block'
+ }
+}))(PageLink)
diff --git a/ui/src/components/external/typography/PageList.js b/ui/src/components/external/typography/PageList.js
new file mode 100644
index 0000000..0374fdb
--- /dev/null
+++ b/ui/src/components/external/typography/PageList.js
@@ -0,0 +1,22 @@
+import injectSheet from 'react-jss'
+import React from 'react'
+
+function PageList({ children, classes }) {
+ return (
+
+ )
+}
+
+export default injectSheet(({ units }) => ({
+ list: {
+ listStyle: 'none',
+ margin: 0,
+ padding: 0,
+
+ '& + *': {
+ marginTop: units.externalParagraphMarginTop
+ }
+ }
+}))(PageList)
diff --git a/ui/src/components/external/typography/PageListItem.js b/ui/src/components/external/typography/PageListItem.js
new file mode 100644
index 0000000..878d600
--- /dev/null
+++ b/ui/src/components/external/typography/PageListItem.js
@@ -0,0 +1,36 @@
+import injectSheet from 'react-jss'
+import React from 'react'
+
+import * as mixins from 'styles/mixins'
+import { PageListText } from 'components/external/typography'
+
+function PageListItem({ children, classes }) {
+ return (
+
+
+ {children}
+
+
+ )
+}
+
+export default injectSheet(({ colors, typography, units }) => ({
+ listItem: {
+ position: 'relative',
+ paddingLeft: units.externalListItemBulletSize + units.externalListItemPaddingLeft,
+
+ '&::before': {
+ ...mixins.size(units.externalListItemBulletSize),
+
+ backgroundColor: colors.externalListItemBulletColor,
+ borderRadius: '50%',
+ content: '" "',
+ position: 'absolute',
+ top: (
+ (typography.medium.lineHeight * typography.medium.fontSize)
+ - units.externalListItemBulletSize
+ ) / 2,
+ left: 0
+ }
+ }
+}))(PageListItem)
diff --git a/ui/src/components/external/typography/PageListText.js b/ui/src/components/external/typography/PageListText.js
new file mode 100644
index 0000000..accbdc9
--- /dev/null
+++ b/ui/src/components/external/typography/PageListText.js
@@ -0,0 +1,18 @@
+import React from 'react'
+
+import BaseText from 'components/typography/BaseText'
+
+function PageListText({ children, ...other }) {
+ return (
+
+ {children}
+
+ )
+}
+
+export default PageListText
diff --git a/ui/src/components/external/typography/PageSubHeading.js b/ui/src/components/external/typography/PageSubHeading.js
new file mode 100644
index 0000000..adaab76
--- /dev/null
+++ b/ui/src/components/external/typography/PageSubHeading.js
@@ -0,0 +1,26 @@
+import injectSheet from 'react-jss'
+import React from 'react'
+
+import BaseText from 'components/typography/BaseText'
+
+function PageSubHeading({ classes, children, ...other }) {
+ return (
+
+ {children}
+
+ )
+}
+
+export default injectSheet(({ units }) => ({
+ pageText: {
+ '& + *': {
+ marginTop: units.externalParagraphMarginTop
+ }
+ }
+}))(PageSubHeading)
diff --git a/ui/src/components/external/typography/PageText.js b/ui/src/components/external/typography/PageText.js
new file mode 100644
index 0000000..70f94c9
--- /dev/null
+++ b/ui/src/components/external/typography/PageText.js
@@ -0,0 +1,26 @@
+import injectSheet from 'react-jss'
+import React from 'react'
+
+import BaseText from 'components/typography/BaseText'
+
+function PageText({ classes, children, ...other }) {
+ return (
+
+ {children}
+
+ )
+}
+
+export default injectSheet(({ units }) => ({
+ pageText: {
+ '& + *': {
+ marginTop: units.externalParagraphMarginTop
+ }
+ }
+}))(PageText)
diff --git a/ui/src/components/external/typography/Text.js b/ui/src/components/external/typography/Text.js
new file mode 100644
index 0000000..3d641e8
--- /dev/null
+++ b/ui/src/components/external/typography/Text.js
@@ -0,0 +1,24 @@
+import injectSheet from 'react-jss'
+import React from 'react'
+
+import BaseText from 'components/typography/BaseText'
+
+function Text({ classes, children, ...other }) {
+ return (
+
+ {children}
+
+ )
+}
+
+export default injectSheet(({ units }) => ({
+ text: {
+ marginTop: units.externalTextMarginTop
+ }
+}))(Text)
diff --git a/ui/src/components/external/typography/TextLink.js b/ui/src/components/external/typography/TextLink.js
new file mode 100644
index 0000000..89aff0b
--- /dev/null
+++ b/ui/src/components/external/typography/TextLink.js
@@ -0,0 +1,22 @@
+import injectSheet from 'react-jss'
+import React from 'react'
+
+import * as mixins from 'styles/mixins'
+import { TextLink as BaseTextLink } from 'components/typography'
+
+function TextLink({ classes, ...other }) {
+ return (
+
+ )
+}
+
+export default injectSheet(({ colors, typography, units }) => ({
+ textLink: {
+ ...mixins.animateUnderline({ color: colors.text_primary, bottom: -3 }),
+ ...typography.semiboldSmallSquished,
+
+ color: colors.text_primary,
+ display: 'inline-block',
+ marginTop: units.externalTextLinkMarginTop
+ }
+}))(TextLink)
diff --git a/ui/src/components/external/typography/Title.js b/ui/src/components/external/typography/Title.js
new file mode 100644
index 0000000..d470171
--- /dev/null
+++ b/ui/src/components/external/typography/Title.js
@@ -0,0 +1,18 @@
+import React from 'react'
+
+import BaseText from 'components/typography/BaseText'
+
+function Title({ children, ...other }) {
+ return (
+
+ {children}
+
+ )
+}
+
+export default Title
diff --git a/ui/src/components/external/typography/index.js b/ui/src/components/external/typography/index.js
new file mode 100644
index 0000000..48a043a
--- /dev/null
+++ b/ui/src/components/external/typography/index.js
@@ -0,0 +1,15 @@
+export { default as FieldErrorText } from './FieldErrorText'
+export { default as FooterLink } from './FooterLink'
+export { default as FooterText } from './FooterText'
+export { default as Heading } from './Heading'
+export { default as NavLink } from './NavLink'
+export { default as PageHeading } from './PageHeading'
+export { default as PageLink } from './PageLink'
+export { default as PageList } from './PageList'
+export { default as PageListItem } from './PageListItem'
+export { default as PageListText } from './PageListText'
+export { default as PageSubHeading } from './PageSubHeading'
+export { default as PageText } from './PageText'
+export { default as Text } from './Text'
+export { default as TextLink } from './TextLink'
+export { default as Title } from './Title'
diff --git a/ui/src/components/inputs/TextInput.js b/ui/src/components/inputs/TextInput.js
new file mode 100644
index 0000000..5ae3d50
--- /dev/null
+++ b/ui/src/components/inputs/TextInput.js
@@ -0,0 +1,262 @@
+import classNames from 'classnames'
+import injectSheet from 'react-jss'
+import PropTypes from 'prop-types'
+import React from 'react'
+
+import * as mixins from 'styles/mixins'
+import Badge from 'components/internal/Badge'
+import cleanProps from 'lib/cleanProps'
+import FieldError from 'components/FieldError'
+import FieldHint from 'components/FieldHint'
+import FontIcon from 'components/FontIcon'
+
+function TextInput({
+ badge,
+ disabled,
+ hint,
+ icon,
+ isMultiline,
+ input,
+ label,
+ meta,
+ spaced,
+ stretched,
+ type,
+ classes,
+ ...other
+}) {
+ if (label && icon) {
+ throw new Error('You can pass either `icon` or `label` to TextInput')
+ }
+
+ const error = TextInput.fieldError(meta)
+
+ const renderIcon = () => icon &&
+
+ const renderAlertIcon = () => error &&
+
+ const renderBadge = () => badge && {badge}
+
+ const renderLabel = () => label && (
+
+ {label}
+
+ )
+
+ return (
+
+
+ {renderIcon()}
+ {renderAlertIcon() || renderBadge()}
+ {renderLabel()}
+
+ {isMultiline ? (
+
+ ) : (
+
+ )}
+
+
+
+
+
+
+ )
+}
+
+TextInput.propTypes = {
+ activeIcon: PropTypes.bool,
+ autoComplete: PropTypes.oneOf([ 'off', 'on' ]),
+ badge: PropTypes.oneOfType([ PropTypes.number, PropTypes.bool ]),
+ disabled: PropTypes.bool,
+ hint: PropTypes.string,
+ icon: PropTypes.string,
+ isMultiline: PropTypes.bool,
+ label: PropTypes.string,
+ meta: PropTypes.object,
+ spaced: PropTypes.bool,
+ stretched: PropTypes.bool,
+ type: PropTypes.string,
+ variant: PropTypes.oneOf([ 'default', 'expandable', 'simple' ])
+}
+
+TextInput.defaultProps = {
+ activeIcon: true,
+ autoComplete: 'off',
+ badge: null,
+ disabled: false,
+ hint: null,
+ icon: null,
+ isMultiline: false,
+ label: null,
+ meta: {},
+ spaced: false,
+ stretched: true,
+ type: 'text',
+ variant: 'default'
+}
+
+TextInput.fieldError = ({
+ dirtySinceLastSubmit, error, pristine, submitError, touched, submitFailed
+}) => touched && (!pristine || submitFailed) && (error || (!dirtySinceLastSubmit && submitError))
+
+export default injectSheet(({
+ colors, typography, units
+}) => ({
+ textInput: ({ variant, spaced }) => {
+ const commonStyles = {
+ position: 'relative' // For FieldError and FieldHint
+ }
+
+ if (variant === 'expandable') {
+ return {
+ ...commonStyles,
+
+ display: 'flex',
+ flexDirection: 'row-reverse',
+ marginRight: units.textInputHorizontalMargin_expandable,
+ marginLeft: units.textInputHorizontalMargin_expandable
+ }
+ }
+
+ if (variant === 'simple') {
+ return {
+ ...commonStyles,
+
+ display: 'flex'
+ }
+ }
+
+ return {
+ ...commonStyles,
+
+ marginBottom: (spaced ? units.inputMargin_spaced : units.inputMargin)
+ }
+ },
+ inputWrapper: {
+ ...mixins.transitionFluid(),
+
+ display: 'flex',
+ position: 'relative',
+ minWidth: ({ variant }) => (variant === 'expandable' ? '30%' : 'auto'),
+
+ '& .icon': {
+ ...mixins.transitionSimple(),
+
+ color: ({ activeIcon }) => (activeIcon ? colors.inputIcon_active : colors.inputIcon),
+ paddingTop: ({ variant }) => (variant === 'expandable' || variant === 'simple' ? 0 : units.inputPaddingTop),
+ position: 'absolute',
+ left: 0
+ },
+
+ '& .icon-round-alert': {
+ right: 0,
+ left: 'auto'
+ },
+
+ '& [data-name=badge]': {
+ ...typography.robotoBold,
+
+ position: 'absolute',
+ right: 0,
+ left: 'auto'
+ },
+
+ '&::after': {
+ ...mixins.transitionSimple(),
+
+ backgroundColor: ({ meta }) => (
+ TextInput.fieldError(meta)
+ ? colors.inputBorder_hover : colors.inputBorder
+ ),
+ bottom: 0,
+ content: '" "',
+ height: 1,
+ position: 'absolute',
+ right: 0,
+ left: 0
+ },
+
+ '&:focus-within': {
+ minWidth: ({ variant }) => (variant === 'expandable' ? '40%' : 'auto'),
+
+ '&::after': {
+ backgroundColor: colors.inputBorder_hover,
+ marginRight: ({ stretched }) => (stretched ? units.inputBorderMarginHorizontal_focus : 0),
+ marginLeft: ({ stretched }) => (stretched ? units.inputBorderMarginHorizontal_focus : 0)
+ }
+ },
+
+ '&:hover': {
+ '&::after': {
+ backgroundColor: colors.inputBorder_hover
+ }
+ },
+
+ '&[disabled]': {
+ pointerEvents: 'none',
+
+ '& .icon, & $input': {
+ color: colors.text_pale
+ }
+ }
+ },
+ input: {
+ ...mixins.transitionSimple(),
+ ...typography.regularSquished,
+
+ ...mixins.placeholder({
+ ...mixins.transitionSimple(),
+
+ color: colors.inputPlaceholder
+ }),
+
+ backgroundColor: 'transparent',
+ border: 'none',
+ color: colors.inputText,
+ marginTop: ({ variant }) => (variant === 'expandable' || variant === 'simple' ? 0 : units.inputPaddingTop),
+ paddingRight: units.inputHorizontalPadding,
+ paddingBottom: units.inputPaddingBottom,
+ paddingLeft: ({ icon }) => (icon ? units.inputHorizontalPadding : 0),
+ width: '100%',
+
+ '&:focus': {
+ ...mixins.placeholder({
+ opacity: 0.5
+ })
+ }
+ },
+ label: {
+ ...mixins.transitionSimple(),
+ ...typography.regularSquished,
+
+ color: colors.text_pale,
+ pointerEvents: 'none',
+ position: 'absolute',
+ top: units.inputPaddingTop
+ },
+ label_shrink: {
+ ...typography.regularSmallSpaced,
+
+ pointerEvents: 'auto',
+ top: 0
+ }
+}))(TextInput)
diff --git a/ui/src/components/inputs/UploadInput.js b/ui/src/components/inputs/UploadInput.js
new file mode 100644
index 0000000..bb98db5
--- /dev/null
+++ b/ui/src/components/inputs/UploadInput.js
@@ -0,0 +1,146 @@
+import _ from 'lodash'
+import Dropzone from 'react-dropzone'
+import injectSheet from 'react-jss'
+import PropTypes from 'prop-types'
+import React, { createRef, Fragment, useEffect, useState } from 'react'
+
+import FieldError from 'components/FieldError'
+import FilledButton from 'components/buttons/FilledButton'
+import Hint from 'components/internal/typography/Hint'
+import Input from 'components/inputs/TextInput'
+import ItemBar from 'components/ItemBar'
+import Spacer from 'components/Spacer'
+import { Text } from 'components/internal/typography'
+
+function fileUrl(value) {
+ return _.isString(value) && value
+}
+
+function imageUrl(value) {
+ if (!value) {
+ return null
+ }
+
+ return fileUrl(value) || URL.createObjectURL(value)
+}
+
+function UploadInput({ classes, input, meta, note, isImage, previewUrl }) {
+ const dropzoneRef = createRef()
+ const [ droppedFiles, setDroppedFiles ] = useState([ input.value ])
+
+ const image = isImage && imageUrl(input.value)
+
+ useEffect(() => {
+ input.onChange(droppedFiles[0])
+ }, [ input, droppedFiles ])
+
+ const openDialog = () => {
+ // Note that the ref is set async,
+ // so it might be null at some point
+ if (dropzoneRef.current) {
+ dropzoneRef.current.open()
+ }
+ }
+
+ const update = (files) => {
+ setDroppedFiles([ ...files ])
+ input.onChange(droppedFiles[0])
+ }
+
+ const remove = (files) => {
+ const newFiles = [ ...droppedFiles ]
+ newFiles.splice(newFiles.indexOf(files), 1)
+ setDroppedFiles([ ' ' ])
+ }
+
+ const isFilePresent = droppedFiles[0].name || droppedFiles[0].trim()
+
+ return (
+
+ {({ getRootProps, getInputProps, acceptedFiles }) => (
+
+
+ {isFilePresent && image &&
}
+ {isFilePresent && previewUrl && fileUrl(input.value) && (
+
+ Uploaded File:
+ {fileUrl(input.value)}
+
+ )}
+
+ {isFilePresent && acceptedFiles.length > 0 && (
+
+ {droppedFiles.map(({ path, size }) => (
+
+ {`${path} - ${size} bytes`}
+
+ ))}
+
+ )}
+ {(!isFilePresent || acceptedFiles.length === 0) && (
+ Drag 'n' drop a file here
+ )}
+
+
+
+ {isFilePresent && remove(acceptedFiles[0])} />}
+
+ {note && (
+
+
+ {note}
+
+ )}
+
+
+
+ )}
+
+ )
+}
+
+UploadInput.propTypes = {
+ isImage: PropTypes.bool,
+ previewUrl: PropTypes.bool,
+ note: PropTypes.string
+}
+
+UploadInput.defaultProps = {
+ isImage: false,
+ previewUrl: false,
+ note: null
+}
+
+export default injectSheet(({ colors, units }) => ({
+ dropzone: {
+ backgroundColor: colors.uploadInputBackground,
+ borderColor: colors.uploadInputBorder,
+ borderRadius: units.uploadInputBorderRadius,
+ borderStyle: 'dashed',
+ borderWidth: units.uploadInputBorderWidth,
+ display: 'flex',
+ justifyContent: 'space-around',
+ outline: 'none',
+ padding: units.uploadInputPadding,
+ position: 'relative'
+ },
+ browseSection: {
+ alignItems: 'center',
+ alignSelf: 'center',
+ display: 'flex',
+ flexDirection: 'column'
+ },
+ previewSection: {
+ backgroundPosition: 'center',
+ backgroundRepeat: 'no-repeat',
+ backgroundSize: 'contain',
+ height: units.uploadInputPreviewHeight,
+ width: units.uploadInputPreviewWidth
+ }
+}))(UploadInput)
diff --git a/ui/src/components/internal/AssetBox.js b/ui/src/components/internal/AssetBox.js
new file mode 100644
index 0000000..c1808a6
--- /dev/null
+++ b/ui/src/components/internal/AssetBox.js
@@ -0,0 +1,135 @@
+import classNames from 'classnames'
+import injectSheet from 'react-jss'
+import PropTypes from 'prop-types'
+import React, { useState } from 'react'
+
+import * as mixins from 'styles/mixins'
+import FontIcon from 'components/FontIcon'
+import ItemBar from 'components/ItemBar'
+import Tag from 'components/internal/Tag'
+import { Asset } from 'models'
+
+function AssetBox({ actions, asset, classes }) {
+ const [ isHovered, setIsHovered ] = useState(false)
+
+ const handleMouseEnter = () => setIsHovered(true)
+
+ const handleMouseLeave = () => setIsHovered(false)
+
+ return (
+
+
+ {Asset.getExtension(asset)}
+
+ {actions.map(({ icon, onClick }) => (
+
{
+ e.stopPropagation()
+ onClick(asset, e)
+ }}
+ onKeyPress={(e) => {
+ e.stopPropagation()
+ onClick(asset, e)
+ }}
+ >
+
+
+ ))}
+
+
+
+
+ {Asset.getFullName(asset)}
+
+
+ {Asset.getFormattedSize(asset)}
+
+
+ )
+}
+
+AssetBox.propTypes = {
+ actions: PropTypes.arrayOf(PropTypes.shape({
+ icon: PropTypes.string.isRequired,
+ onClick: PropTypes.func
+ }))
+}
+
+AssetBox.defaultProps = {
+ actions: []
+}
+
+export default injectSheet(({ colors, shadows, typography, units }) => ({
+ assetBox: {
+ ...mixins.size(240, 170),
+ ...mixins.transitionSimple(),
+
+ boxShadow: shadows.assetBox,
+ backgroundColor: colors.assetBoxBackground,
+ cursor: 'pointer',
+ marginRight: units.assetBoxMarginRight,
+ marginBottom: units.assetBoxMarginBottom,
+ padding: units.assetBoxPadding,
+ position: 'relative'
+ },
+ assetBox_hover: {
+ boxShadow: shadows.assetBox_hover,
+ cursor: 'pointer',
+
+ '& $assetName, $assetSize': {
+ color: colors.text_primary
+ },
+
+ '& $assetActions': {
+ opacity: 1,
+ pointerEvents: 'auto'
+ }
+ },
+ assetName: {
+ ...mixins.transitionSimple(),
+ ...typography.semiboldSmallSquished,
+
+ color: colors.text_dark,
+ paddingTop: units.assetBoxNamePaddingTop,
+ paddingBottom: units.assetBoxNamePaddingBottom
+ },
+ assetSize: {
+ ...mixins.transitionSimple(),
+ ...typography.regularSmallSpacedSquished,
+
+ color: colors.text_pale
+ },
+ assetActions: {
+ ...mixins.transitionSimple(),
+
+ display: 'flex',
+ opacity: 0,
+ pointerEvents: 'none'
+ },
+ assetAction: {
+ ...mixins.transitionSimple(),
+
+ display: 'flex',
+ color: colors.text_pale,
+ cursor: 'pointer',
+
+ '&:hover': {
+ color: colors.text_dark
+ },
+
+ '&:not(:last-child)': {
+ marginRight: units.assetBoxActionMarginRight
+ }
+ }
+}))(AssetBox)
diff --git a/ui/src/components/internal/AssetList.js b/ui/src/components/internal/AssetList.js
new file mode 100644
index 0000000..3b82752
--- /dev/null
+++ b/ui/src/components/internal/AssetList.js
@@ -0,0 +1,32 @@
+import injectSheet from 'react-jss'
+import PropTypes from 'prop-types'
+import React from 'react'
+
+import AssetBox from 'components/internal/AssetBox'
+
+function AssetList({ loading, records, classes, ...other }) {
+ if (loading || !records || records.length === 0) {
+ return null
+ }
+
+ return (
+
+ {records.map(record =>
)}
+
+ )
+}
+
+AssetList.propTypes = {
+ records: PropTypes.array
+}
+
+AssetList.defaultProps = {
+ records: null
+}
+
+export default injectSheet(() => ({
+ wrapper: {
+ display: 'flex',
+ flexWrap: 'wrap'
+ }
+}))(AssetList)
diff --git a/ui/src/components/internal/Badge.js b/ui/src/components/internal/Badge.js
new file mode 100644
index 0000000..a00a83a
--- /dev/null
+++ b/ui/src/components/internal/Badge.js
@@ -0,0 +1,46 @@
+import classNames from 'classnames'
+import injectSheet from 'react-jss'
+import PropTypes from 'prop-types'
+import React from 'react'
+
+import * as mixins from 'styles/mixins'
+import cleanProps from 'lib/cleanProps'
+
+let Badge = ({ _ref, size, variant, classes, children, ...other }) => (
+
+ {children}
+
+)
+
+Badge.sizes = {
+ small: 12,
+ medium: 16,
+ large: 24
+}
+
+Badge.propTypes = {
+ size: PropTypes.oneOf([ 'small', 'medium', 'large' ]),
+ variant: PropTypes.oneOf([ 'primary' ])
+}
+
+Badge.defaultProps = {
+ size: 'medium',
+ variant: 'primary'
+}
+
+Badge = injectSheet(({ colors }) => ({
+ badge: ({ size }) => ({
+ ...mixins.size(Badge.sizes[size]),
+
+ alignItems: 'center',
+ borderRadius: '50%',
+ display: 'flex',
+ justifyContent: 'center'
+ }),
+ badge_primary: {
+ backgroundColor: colors.badgeBackground_primary,
+ color: colors.badgeText_primary
+ }
+}))(Badge)
+
+export default React.forwardRef((props, ref) => )
diff --git a/ui/src/components/internal/Box.js b/ui/src/components/internal/Box.js
new file mode 100644
index 0000000..d4e628b
--- /dev/null
+++ b/ui/src/components/internal/Box.js
@@ -0,0 +1,25 @@
+import injectSheet from 'react-jss'
+import React from 'react'
+
+function Box({ children, classes }) {
+ return (
+
+ {children}
+
+ )
+}
+
+export default injectSheet(({ colors, units }) => ({
+ box: {
+ alignItems: 'center',
+ backgroundColor: colors.internalBoxBackground,
+ borderColor: colors.internalBoxBorder,
+ borderRadius: units.internalBoxBorderRadius,
+ borderStyle: 'solid',
+ borderWidth: 1,
+ display: 'flex',
+ flexDirection: 'column',
+ paddingTop: units.internalBoxPaddingVertical,
+ paddingBottom: units.internalBoxPaddingVertical
+ }
+}))(Box)
diff --git a/ui/src/components/internal/Card.js b/ui/src/components/internal/Card.js
new file mode 100644
index 0000000..a7d9853
--- /dev/null
+++ b/ui/src/components/internal/Card.js
@@ -0,0 +1,23 @@
+import injectSheet from 'react-jss'
+import React from 'react'
+
+function Card({ classes, children }) {
+ return (
+
+ {children}
+
+ )
+}
+
+export default injectSheet(({ colors, shadows, units }) => ({
+ card: {
+ backgroundColor: colors.internalCardBackground,
+ boxShadow: shadows.internalCard,
+ display: 'flex',
+ flexDirection: 'column',
+ height: '100%',
+ justifyContent: 'center',
+ paddingRight: units.internalCardHorizontalPadding,
+ paddingLeft: units.internalCardHorizontalPadding
+ }
+}))(Card)
diff --git a/ui/src/components/internal/Code.js b/ui/src/components/internal/Code.js
new file mode 100644
index 0000000..79aadc0
--- /dev/null
+++ b/ui/src/components/internal/Code.js
@@ -0,0 +1,22 @@
+import injectSheet from 'react-jss'
+import React from 'react'
+
+function Code({ classes, children }) {
+ return (
+
+ {children}
+
+ )
+}
+
+export default injectSheet(({ colors, units }) => ({
+ code: {
+ display: 'block',
+ background: colors.codeBackground,
+ borderRadius: units.codeBorderRadius,
+ paddingTop: units.codeVerticalPadding,
+ paddingRight: units.codeHorizontalPadding,
+ paddingBottom: units.codeVerticalPadding,
+ paddingLeft: units.codeHorizontalPadding
+ }
+}))(Code)
diff --git a/ui/src/components/internal/ColorTile.js b/ui/src/components/internal/ColorTile.js
new file mode 100644
index 0000000..889df74
--- /dev/null
+++ b/ui/src/components/internal/ColorTile.js
@@ -0,0 +1,56 @@
+import injectSheet from 'react-jss'
+import PropTypes from 'prop-types'
+import React from 'react'
+
+import CopyToClipboard from 'components/internal/CopyToClipboard'
+
+function ColorTile({ classes, color, onClick }) {
+ const renderTile = onTileClick => (
+
+ )
+
+ if (onClick) {
+ return renderTile(onClick)
+ }
+
+ return (
+
+ )
+}
+
+ColorTile.propTypes = {
+ color: PropTypes.string,
+ onClick: PropTypes.func,
+ spaced: PropTypes.bool
+}
+
+ColorTile.defaultProps = {
+ color: '',
+ onClick: null,
+ spaced: false
+}
+
+export default injectSheet(({ colors, units }) => ({
+ base: {
+ background: colors.colorTileDefaultBackground,
+ borderRadius: units.colorTileBorderRadius,
+ boxShadow: colors.colorTileBoxShadow,
+ cursor: 'pointer',
+ display: 'inline-block',
+ height: units.colorTileColorHeight,
+ marginLeft: ({ spaced }) => spaced && units.colorTileMargin,
+ marginRight: ({ spaced }) => spaced && units.colorTileMargin,
+ width: units.colorTileColorWidth
+ }
+}))(ColorTile)
diff --git a/ui/src/components/internal/Column.js b/ui/src/components/internal/Column.js
new file mode 100644
index 0000000..4d88e2f
--- /dev/null
+++ b/ui/src/components/internal/Column.js
@@ -0,0 +1,55 @@
+import classNames from 'classnames'
+import injectSheet from 'react-jss'
+import PropTypes from 'prop-types'
+import React from 'react'
+
+function Column({ bordered, extraPadded, classes, children }) {
+ return (
+
+ {children}
+
+ )
+}
+
+Column.propTypes = {
+ bordered: PropTypes.bool,
+ children: PropTypes.node.isRequired,
+ extraPadded: PropTypes.bool,
+ width: PropTypes.oneOfType([ PropTypes.string, PropTypes.number ])
+}
+
+Column.defaultProps = {
+ bordered: false,
+ extraPadded: false,
+ width: null
+}
+
+export default injectSheet(({ colors, units }) => ({
+ column: {
+ flexBasis: ({ width }) => width,
+ flexGrow: ({ width }) => (width ? 0 : 1), // 0 is falsey
+ paddingRight: units.columnPaddingHorizontal,
+ paddingLeft: units.columnPaddingHorizontal
+ },
+ bordered: {
+ '& + &': {
+ borderLeft: `solid 1px ${colors.columnBorder}`
+ }
+ },
+ extraPadded: {
+ '&:not(:first-child)': {
+ paddingLeft: units.columnPaddingHorizontal * 4
+ },
+
+ '&:not(:last-child)': {
+ paddingRight: units.columnPaddingHorizontal * 4
+ }
+ }
+}))(Column)
diff --git a/ui/src/components/internal/Container.js b/ui/src/components/internal/Container.js
new file mode 100644
index 0000000..f8e99a4
--- /dev/null
+++ b/ui/src/components/internal/Container.js
@@ -0,0 +1,18 @@
+import injectSheet from 'react-jss'
+import React from 'react'
+
+function Container({ classes, children }) {
+ return (
+
+ {children}
+
+ )
+}
+
+export default injectSheet(({ units }) => ({
+ container: {
+ paddingRight: units.internalContainerHorizontalPadding,
+ paddingLeft: units.internalContainerHorizontalPadding,
+ width: '100%'
+ }
+}))(Container)
diff --git a/ui/src/components/internal/Content.js b/ui/src/components/internal/Content.js
new file mode 100644
index 0000000..8684ca2
--- /dev/null
+++ b/ui/src/components/internal/Content.js
@@ -0,0 +1,36 @@
+import classNames from 'classnames'
+import injectSheet from 'react-jss'
+import PropTypes from 'prop-types'
+import React from 'react'
+
+function Content({ fluid, classes, children }) {
+ return (
+
+ {children}
+
+ )
+}
+
+Content.propTypes = {
+ fluid: PropTypes.bool,
+ children: PropTypes.node.isRequired
+}
+
+Content.defaultProps = {
+ fluid: false
+}
+
+export default injectSheet(({ units }) => ({
+ content: {
+ paddingTop: units.internalContentPaddingTop,
+ paddingRight: units.internalContentPaddingRight,
+ paddingBottom: units.internalContentPaddingBottom,
+ paddingLeft: units.sidebarWidth + units.internalContentPaddingLeft
+ },
+ content_fluid: {
+ paddingTop: 0,
+ paddingRight: units.internalContentPaddingHorizontal_fluid,
+ paddingBottom: units.internalContentPaddingBottom_fluid,
+ paddingLeft: units.internalContentPaddingHorizontal_fluid
+ }
+}))(Content)
diff --git a/ui/src/components/internal/CopyToClipboard.js b/ui/src/components/internal/CopyToClipboard.js
new file mode 100644
index 0000000..bf48ea9
--- /dev/null
+++ b/ui/src/components/internal/CopyToClipboard.js
@@ -0,0 +1,74 @@
+import copy from 'copy-to-clipboard'
+import PropTypes from 'prop-types'
+import React, { PureComponent } from 'react'
+
+import FontIcon from 'components/FontIcon'
+import Tooltip from 'components/internal/Tooltip'
+
+class CopyToClipboard extends PureComponent {
+ constructor() {
+ super()
+
+ this.state = {
+ isCopied: false
+ }
+ }
+
+ handleClick = () => {
+ const { text } = this.props
+
+ copy(text)
+
+ this.setState({ isCopied: true })
+ }
+
+ handleMouseLeave = () => {
+ this.setState({ isCopied: false })
+ }
+
+ renderComponent = () => {
+ const { icon, iconSize, render } = this.props
+
+ if (render) {
+ return render(this.handleClick)
+ }
+
+ return (
+
+ )
+ }
+
+ render() {
+ const { description } = this.props
+ const { isCopied } = this.state
+
+ const tooltip = isCopied ? 'Copied!' : description
+
+ return (
+
+ {this.renderComponent()}
+
+ )
+ }
+}
+
+CopyToClipboard.propTypes = {
+ description: PropTypes.string,
+ icon: PropTypes.string,
+ iconSize: PropTypes.string,
+ render: PropTypes.func,
+ text: PropTypes.string.isRequired
+}
+
+CopyToClipboard.defaultProps = {
+ description: 'Copy to clipboard',
+ icon: 'code',
+ iconSize: 'tiny',
+ render: null
+}
+
+export default CopyToClipboard
diff --git a/ui/src/components/internal/DataTiles.js b/ui/src/components/internal/DataTiles.js
new file mode 100644
index 0000000..acd5b6a
--- /dev/null
+++ b/ui/src/components/internal/DataTiles.js
@@ -0,0 +1,96 @@
+import injectSheet from 'react-jss'
+import PropTypes from 'prop-types'
+import React from 'react'
+import { Link } from 'react-router-dom'
+
+import * as mixins from 'styles/mixins'
+
+const circleColors = [
+ '#58efb1',
+ '#99b9f6',
+ '#fde198',
+ '#ff7a87',
+ '#9c6ce3'
+]
+
+function DataTiles({
+ loading, records, tileLink, classes
+}) {
+ if (loading || !records || records.length === 0) {
+ return null
+ }
+
+ return (
+
+ {records.map((record, index) => (
+
+
+
+ ))}
+
+ )
+}
+
+DataTiles.propTypes = {
+ loading: PropTypes.bool,
+ records: PropTypes.array,
+ tileLink: PropTypes.func.isRequired
+}
+
+DataTiles.defaultProps = {
+ loading: false,
+ records: null
+}
+
+export default injectSheet(({
+ colors, shadows, typography, units
+}) => ({
+ dataTiles: {
+ display: 'grid',
+ gridColumnGap: units.dataTilesColumnGap,
+ gridRowGap: units.dataTilesRowGap,
+ gridTemplateColumns: '1fr 1fr 1fr'
+ },
+ dataTile: {
+ ...mixins.size('100%', units.dataTileHeight),
+ ...mixins.transitionSimple(),
+ ...typography.semibold,
+
+ alignItems: 'center',
+ backgroundColor: colors.dataTileBackground,
+ borderRadius: units.dataTileBorderRadius,
+ boxShadow: shadows.dataTile,
+ color: colors.text_dark,
+ display: 'flex',
+ overflow: 'hidden',
+ paddingRight: units.dataTileHorizontalPadding,
+ paddingLeft: units.dataTileHorizontalPadding,
+ position: 'relative',
+
+ '&:hover': {
+ boxShadow: shadows.dataTitle_hover,
+
+ '& $circle': {
+ ...mixins.size(units.dataTileCircleSize_large),
+
+ top: units.dataTileCircleShiftTop_large,
+ left: units.dataTileCircleShiftLeft_large
+ }
+ }
+ },
+ circle: {
+ ...mixins.size(units.dataTileCircleSize),
+ ...mixins.transitionSimple(),
+
+ borderRadius: '50%',
+ position: 'absolute',
+ top: (units.dataTileHeight - units.dataTileCircleSize) / 2,
+ left: units.dataTileCircleShiftLeft
+ }
+}))(DataTiles)
diff --git a/ui/src/components/internal/Dialog.js b/ui/src/components/internal/Dialog.js
new file mode 100644
index 0000000..cad2d0d
--- /dev/null
+++ b/ui/src/components/internal/Dialog.js
@@ -0,0 +1,93 @@
+import injectSheet from 'react-jss'
+import PropTypes from 'prop-types'
+import React from 'react'
+
+import * as mixins from 'styles/mixins'
+import BaseModal from 'components/BaseModal'
+import CloseButton from 'components/buttons/CloseButton'
+import { DialogTitle } from 'components/internal/typography'
+
+function Dialog({
+ title,
+ onRequestClose,
+ classes,
+ children,
+ ...other
+}) {
+ return (
+
+
+
+ {title}
+
+
+
+ {children}
+
+ )
+}
+
+Dialog.propTypes = {
+ title: PropTypes.string.isRequired
+}
+
+export default injectSheet(({ colors, shadows, units }) => ({
+ overlayBase: {
+ alignItems: 'center',
+ display: 'flex',
+ justifyContent: 'center'
+ },
+ contentBase: {
+ borderRadius: units.dialogBorderRadius,
+ boxShadow: shadows.dialog,
+ marginTop: units.dialogMarginTop,
+ padding: units.dialogPadding,
+ width: units.dialogWidth,
+ overflow: 'hidden',
+ position: 'relative',
+ zIndex: 0,
+
+ '&::before': {
+ ...mixins.size(units.dialogCircleSize_small),
+
+ backgroundColor: colors.dialogCircleBackground,
+ borderRadius: '50%',
+ content: '" "',
+ position: 'absolute',
+ top: units.dialogCircleShiftTop_small,
+ left: units.dialogCircleShiftLeft_small,
+ zIndex: -1
+ },
+
+ '&::after': {
+ ...mixins.size(units.dialogCircleSize_large),
+
+ backgroundColor: colors.dialogCircleBackground,
+ borderRadius: '50%',
+ content: '" "',
+ position: 'absolute',
+ right: units.dialogCircleShiftRight_small,
+ bottom: units.dialogCircleShiftBottom_small,
+ zIndex: -1
+ }
+ },
+ contentAfterOpen: {
+ marginTop: 0
+ },
+ contentBeforeClose: {
+ marginTop: units.dialogMarginTop
+ },
+ header: {
+ display: 'flex',
+ justifyContent: 'space-between',
+ paddingBottom: units.dialogHeaderPaddingBottom
+ }
+}))(Dialog)
diff --git a/ui/src/components/internal/DialogFormFooter.js b/ui/src/components/internal/DialogFormFooter.js
new file mode 100644
index 0000000..2a183d4
--- /dev/null
+++ b/ui/src/components/internal/DialogFormFooter.js
@@ -0,0 +1,24 @@
+import PropTypes from 'prop-types'
+import React, { Fragment } from 'react'
+import { withTheme } from 'react-jss'
+
+import ItemBar from 'components/ItemBar'
+import Spacer from 'components/Spacer'
+
+function DialogFormFooter({ children }) {
+ return (
+
+
+
+
+ {children}
+
+
+ )
+}
+
+DialogFormFooter.propTypes = {
+ children: PropTypes.node.isRequired
+}
+
+export default withTheme(DialogFormFooter)
diff --git a/ui/src/components/internal/Divider.js b/ui/src/components/internal/Divider.js
new file mode 100644
index 0000000..3822884
--- /dev/null
+++ b/ui/src/components/internal/Divider.js
@@ -0,0 +1,26 @@
+import injectSheet from 'react-jss'
+import PropTypes from 'prop-types'
+import React from 'react'
+
+import * as mixins from 'styles/mixins'
+
+function Divider({ classes }) {
+ return
+}
+
+Divider.propTypes = {
+ isVertical: PropTypes.bool
+}
+
+Divider.defaultProps = {
+ isVertical: false
+}
+
+export default injectSheet(({ colors }) => ({
+ divider: ({ isVertical }) => ({
+ alignSelf: 'center',
+ backgroundColor: colors.internalDividerBackground,
+
+ ...(isVertical ? mixins.size(1, '100%') : mixins.size('100%', 1))
+ })
+}))(Divider)
diff --git a/ui/src/components/internal/FieldGroup.js b/ui/src/components/internal/FieldGroup.js
new file mode 100644
index 0000000..1058a92
--- /dev/null
+++ b/ui/src/components/internal/FieldGroup.js
@@ -0,0 +1,38 @@
+import injectSheet from 'react-jss'
+import React from 'react'
+
+function FieldGroup({ classes, children }) {
+ return (
+ {children}
+ )
+}
+
+FieldGroup.Header = ({ classes, children }) => (
+
+ {children}
+
+)
+
+FieldGroup.Header = injectSheet({
+ childFieldHeader: {
+ display: 'flex',
+ position: 'absolute',
+ top: 10,
+ right: 10,
+
+ '& > *': {
+ marginLeft: 10
+ }
+ }
+})(FieldGroup.Header)
+
+export default injectSheet(({ colors }) => ({
+ childFieldWrapper: {
+ backgroundColor: colors.uploadInputBackground,
+ border: `1px solid ${colors.uploadInputBorder}`,
+ borderLeftWidth: 5,
+ marginBottom: 20,
+ padding: [ 30, 30, 20, 20 ],
+ position: 'relative'
+ }
+}))(FieldGroup)
diff --git a/ui/src/components/internal/FieldPrefix.js b/ui/src/components/internal/FieldPrefix.js
new file mode 100644
index 0000000..cebc2b9
--- /dev/null
+++ b/ui/src/components/internal/FieldPrefix.js
@@ -0,0 +1,18 @@
+import React from 'react'
+import { Field } from 'react-final-form'
+
+const FieldPrefixContext = React.createContext()
+
+const FieldPrefix = ({ prefix, children }) => (
+
+ {children}
+
+)
+
+const PrefixedField = ({ name, ...props }) => (
+
+ {prefix => }
+
+)
+
+export { FieldPrefix, PrefixedField }
diff --git a/ui/src/components/internal/FormFooter.js b/ui/src/components/internal/FormFooter.js
new file mode 100644
index 0000000..c07e534
--- /dev/null
+++ b/ui/src/components/internal/FormFooter.js
@@ -0,0 +1,23 @@
+import PropTypes from 'prop-types'
+import React, { Fragment } from 'react'
+
+import ItemBar from 'components/ItemBar'
+import Spacer from 'components/Spacer'
+
+function FormFooter({ children }) {
+ return (
+
+
+
+
+ {children}
+
+
+ )
+}
+
+FormFooter.propTypes = {
+ children: PropTypes.node.isRequired
+}
+
+export default FormFooter
diff --git a/ui/src/components/internal/Header.js b/ui/src/components/internal/Header.js
new file mode 100644
index 0000000..6f9708a
--- /dev/null
+++ b/ui/src/components/internal/Header.js
@@ -0,0 +1,235 @@
+import gql from 'graphql-tag'
+import injectSheet from 'react-jss'
+import React, { Fragment, PureComponent } from 'react'
+import { withRouter } from 'react-router-dom'
+
+import MenuContainer from 'components/internal/menu/MenuContainer'
+import ProjectDialog from 'components/internal/dialogs/ProjectDialog'
+import ProjectHeaderItem from 'components/internal/headerItems/ProjectHeaderItem'
+import ProjectMenu from 'components/internal/menus/ProjectMenu'
+import TeamDialog from 'components/internal/dialogs/TeamDialog'
+import TeamHeaderItem from 'components/internal/headerItems/TeamHeaderItem'
+import TeamMenu from 'components/internal/menus/TeamMenu'
+import UserHeaderItem from 'components/internal/headerItems/UserHeaderItem'
+import UserMenu from 'components/internal/menus/UserMenu'
+import { MutationResponseModes, OptimisticResponseModes, withMutation, withQueries } from 'lib/data'
+
+const headerScrollThreshold = 20
+
+class Header extends PureComponent {
+ constructor(props) {
+ super(props)
+
+ this.state = {
+ isHovered: false,
+ isProjectDialogOpen: false,
+ isTeamDialogOpen: false,
+ lastScrollPosition: window.pageYOffset
+ }
+ }
+
+ componentDidMount() {
+ window.addEventListener('scroll', this.handleScroll)
+ }
+
+ componentWillUnmount() {
+ window.removeEventListener('scroll', this.handleScroll)
+ }
+
+ openProjectDialog = () => this.setState({ isProjectDialogOpen: true })
+
+ closeProjectDialog = () => this.setState({ isProjectDialogOpen: false })
+
+ openTeamDialog = () => this.setState({ isTeamDialogOpen: true })
+
+ closeTeamDialog = () => this.setState({ isTeamDialogOpen: false })
+
+ handleProjectFormSubmit = (values) => {
+ const { createProject, history } = this.props
+
+ return createProject(values, {
+ onSuccess: ({ data: { project: { id, teamId } } }) => {
+ this.closeProjectDialog()
+ history.push(`/teams/${teamId}/projects/${id}`)
+ }
+ })
+ }
+
+ handleTeamFormSubmit = (values) => {
+ const { createTeam, history } = this.props
+
+ return createTeam(values, {
+ onSuccess: ({ data: { team: { id } } }) => {
+ this.closeTeamDialog()
+ history.push(`/teams/${id}`)
+ }
+ })
+ }
+
+ handleMouseEnter = () => this.setState({ isHovered: true })
+
+ handleMouseLeave = () => this.setState({ isHovered: false })
+
+ handleScroll = () => this.setState({ lastScrollPosition: window.pageYOffset })
+
+ render() {
+ const {
+ match, projects, teams, classes, theme
+ } = this.props
+ const {
+ isHovered, isProjectDialogOpen, isTeamDialogOpen, lastScrollPosition
+ } = this.state
+
+ const hideHeaderItem = lastScrollPosition > headerScrollThreshold && !isHovered
+
+ return (
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ )
+ }
+}
+
+Header.fragments = {
+ teams: gql`
+ fragment Header_teams on Team {
+ id
+ name
+ }
+ `,
+ projects: gql`
+ fragment Header_projects on Project {
+ id
+ name
+ teamId
+ }
+ `
+}
+
+const HeaderTeamsQuery = gql`
+ query HeaderTeamsQuery {
+ teams {
+ ...Header_teams
+ }
+ }
+
+ ${Header.fragments.teams}
+`
+
+const HeaderProjectsQuery = gql`
+ query HeaderProjectsQuery($teamId: ID!) {
+ projects(teamId: $teamId) {
+ ...Header_projects
+ }
+ }
+
+ ${Header.fragments.projects}
+`
+
+Header = injectSheet(({
+ gradients, shadows, units, zIndexes
+}) => ({
+ header: {
+ backgroundImage: gradients.internalHeader,
+ borderRadius: units.internalHeaderBorderRadius,
+ boxShadow: shadows.internalHeader,
+ display: 'flex',
+ flex: '0 1 auto',
+ height: units.internalHeaderHeight,
+ position: 'fixed',
+ top: units.internalHeaderTop,
+ right: units.internalHeaderRight,
+ zIndex: zIndexes.internalHeader
+ }
+}))(Header)
+
+Header = withMutation(gql`
+ mutation CreateTeamMutation($input: CreateTeamInput!) {
+ team: createTeam(input: $input) {
+ ...Header_teams
+ }
+ }
+
+ ${Header.fragments.teams}
+`, {
+ mode: MutationResponseModes.APPEND,
+ optimistic: { mode: OptimisticResponseModes.CREATE, response: { __typename: 'Team' } },
+ query: HeaderTeamsQuery
+})(Header)
+
+Header = withMutation(gql`
+ mutation CreateProjectMutation($input: CreateProjectInput!) {
+ project: createProject(input: $input) {
+ ...Header_projects
+ }
+ }
+
+ ${Header.fragments.projects}
+`, {
+ mode: MutationResponseModes.APPEND,
+ optimistic: { mode: OptimisticResponseModes.CREATE, response: { __typename: 'Project' } },
+ query: HeaderProjectsQuery
+})(Header)
+
+Header = withQueries([
+ { query: HeaderTeamsQuery },
+ {
+ query: HeaderProjectsQuery,
+ config: {
+ options: ({ match }) => ({
+ variables: { teamId: match.params.teamId }
+ }),
+ skip: ({ match }) => !match.params.teamId
+ }
+ }
+])(Header)
+
+export default withRouter(Header)
diff --git a/ui/src/components/internal/HeaderItem.js b/ui/src/components/internal/HeaderItem.js
new file mode 100644
index 0000000..23c895a
--- /dev/null
+++ b/ui/src/components/internal/HeaderItem.js
@@ -0,0 +1,88 @@
+import classNames from 'classnames'
+import injectSheet from 'react-jss'
+import PropTypes from 'prop-types'
+import React from 'react'
+
+import * as mixins from 'styles/mixins'
+
+function HeaderItem({
+ isActive, onClick, onItemClick, classes, children
+}) {
+ const handleClick = () => {
+ if (onItemClick) {
+ onItemClick()
+ }
+
+ /*
+ `onClick` is passed from MenuContainer to toggle the menu.
+
+ In MenuContainer, React.cloneElement merges props shallowly.
+ Therefore, if `onItemClick` is passed as `onClick`, it would be overwritten in MenuContainer.
+ */
+ onClick()
+ }
+
+ return (
+
+ {children}
+
+ )
+}
+
+HeaderItem.propTypes = {
+ isActive: PropTypes.bool,
+ isHidden: PropTypes.bool,
+ isWide: PropTypes.bool,
+ onClick: PropTypes.func,
+ onItemClick: PropTypes.func
+}
+
+HeaderItem.defaultProps = {
+ isActive: false,
+ isHidden: false,
+ isWide: false,
+ onClick: null,
+ onItemClick: null
+}
+
+export default injectSheet(({ colors, units }) => ({
+ headerItem: {
+ ...mixins.transitionSimple(),
+
+ borderRightColor: colors.headerItemBorder,
+ borderRightStyle: 'solid',
+ borderWidth: ({ isHidden }) => (isHidden ? 0 : 1),
+ cursor: 'pointer',
+ display: 'flex',
+ flexDirection: 'column',
+ height: '100%',
+ justifyContent: 'center',
+ opacity: ({ isHidden }) => (isHidden ? 0 : 1),
+ paddingRight: ({ isHidden }) => (isHidden ? 0 : units.headerItemHorizonalPadding),
+ paddingLeft: ({ isHidden }) => (isHidden ? 0 : units.headerItemHorizonalPadding),
+ pointerEvents: ({ isHidden }) => (isHidden ? 'none' : 'all'),
+ width: ({ isHidden, isWide }) => {
+ if (isHidden) {
+ return 0
+ }
+
+ return isWide ? units.headerItemWidth_wide : units.headerItemWidth
+ },
+
+ '&:hover': {
+ backgroundColor: colors.headerItemBackground_hover
+ }
+ },
+ headerItem_active: {
+ backgroundColor: colors.headerItemBackground_hover
+ }
+}))(HeaderItem)
diff --git a/ui/src/components/internal/HeaderItemContent.js b/ui/src/components/internal/HeaderItemContent.js
new file mode 100644
index 0000000..67b967d
--- /dev/null
+++ b/ui/src/components/internal/HeaderItemContent.js
@@ -0,0 +1,31 @@
+import injectSheet from 'react-jss'
+import PropTypes from 'prop-types'
+import React from 'react'
+
+import FontIcon from 'components/FontIcon'
+
+function HeaderItemContent({ icon, classes, children }) {
+ return (
+
+ {children}
+
+
+ )
+}
+
+HeaderItemContent.propTypes = {
+ children: PropTypes.node.isRequired
+}
+
+export default injectSheet(({ colors, units }) => ({
+ headerItemContent: {
+ alignItems: 'center',
+ display: 'flex',
+ justifyContent: 'space-between',
+ marginTop: units.headerItemContentMarginTop,
+
+ '& .icon': {
+ color: colors.text_light
+ }
+ }
+}))(HeaderItemContent)
diff --git a/ui/src/components/internal/HintBox.js b/ui/src/components/internal/HintBox.js
new file mode 100644
index 0000000..d3e6324
--- /dev/null
+++ b/ui/src/components/internal/HintBox.js
@@ -0,0 +1,43 @@
+import classNames from 'classnames'
+import injectSheet from 'react-jss'
+import PropTypes from 'prop-types'
+import React from 'react'
+
+function HintBox({ variant, classes, children }) {
+ return (
+
+ {children}
+
+ )
+}
+
+HintBox.propTypes = {
+ children: PropTypes.node.isRequired,
+ variant: PropTypes.oneOf([ 'light', 'dark', 'failure' ])
+}
+
+HintBox.defaultProps = {
+ variant: 'light'
+}
+
+export default injectSheet(({ colors, units }) => ({
+ hintBox: {
+ borderColor: colors.hintBoxBorder,
+ borderRadius: units.hintBoxBorderRadius,
+ borderStyle: 'solid',
+ borderWidth: 1,
+ paddingTop: units.hintBoxVerticalPadding,
+ paddingRight: units.hintBoxPaddingRight,
+ paddingBottom: units.hintBoxVerticalPadding,
+ paddingLeft: units.hintBoxPaddingLeft
+ },
+ hintBox_light: {
+ backgroundColor: colors.hintBoxBackground_light
+ },
+ hintBox_dark: {
+ backgroundColor: colors.hintBoxBackground_dark
+ },
+ hintBox_failure: {
+ borderColor: colors.hintBoxBorder_alert
+ }
+}))(HintBox)
diff --git a/ui/src/components/internal/Loader.js b/ui/src/components/internal/Loader.js
new file mode 100644
index 0000000..508b4c9
--- /dev/null
+++ b/ui/src/components/internal/Loader.js
@@ -0,0 +1,77 @@
+import injectSheet from 'react-jss'
+import PropTypes from 'prop-types'
+import React from 'react'
+
+import EmptyWrapper from 'components/internal/views/EmptyWrapper'
+import FilledButton from 'components/buttons/FilledButton'
+import { LoaderText, LoaderTitle } from 'components/internal/typography'
+
+import loaderGif from 'images/loader.gif'
+
+function Loader({
+ emptyView,
+ record: { loading, value },
+ classes
+}) {
+ const hasData = (Array.isArray(value) && value.length > 0)
+ || (!Array.isArray(value) && Boolean(value))
+
+ if (!loading && hasData) {
+ return null
+ }
+
+ if (loading) {
+ return (
+
+
+
+ Sit tight...
+
+
+ )
+ }
+
+ if (emptyView) {
+ const { buttonLabel, title, onButtonClick } = emptyView
+
+ return (
+
+
+ {`No ${title} found.`}
+
+
+ Feel free to contact us for any assistance.
+
+ {buttonLabel && }
+
+ )
+ }
+
+ return null
+}
+
+Loader.propTypes = {
+ emptyView: PropTypes.shape({
+ buttonLabel: PropTypes.string,
+ title: PropTypes.string,
+ onButtonClick: PropTypes.func
+ }),
+ record: PropTypes.shape({
+ loading: PropTypes.bool,
+ values: PropTypes.oneOfType([ PropTypes.object, PropTypes.array ])
+ })
+}
+
+Loader.defaultProps = {
+ emptyView: null,
+ record: {
+ loading: false,
+ value: null
+ }
+}
+
+export default injectSheet(({ units }) => ({
+ loaderImage: {
+ marginBottom: units.loaderImageMarginBottom
+ }
+}))(Loader)
diff --git a/ui/src/components/internal/Modal.js b/ui/src/components/internal/Modal.js
new file mode 100644
index 0000000..0468ee0
--- /dev/null
+++ b/ui/src/components/internal/Modal.js
@@ -0,0 +1,41 @@
+import injectSheet from 'react-jss'
+import React from 'react'
+
+import BaseModal from 'components/BaseModal'
+import CloseButton from 'components/buttons/CloseButton'
+
+function Modal({ onRequestClose, classes, children, ...other }) {
+ return (
+
+
+
+
+ {children}
+
+ )
+}
+
+export default injectSheet(({ colors, units }) => ({
+ contentBase: {
+ backgroundColor: colors.modalBackground,
+ overflowY: 'auto',
+ paddingTop: units.modalVerticalPadding,
+ paddingRight: units.modalHorizontalPadding,
+ paddingBottom: units.modalVerticalPadding,
+ paddingLeft: units.modalHorizontalPadding,
+ position: 'absolute',
+ top: 0,
+ right: 0,
+ bottom: 0,
+ left: 0
+ },
+ closeButton: {
+ position: 'fixed',
+ top: units.modalCloseButtonPositionTop,
+ right: units.modalCloseButtonPositionRight
+ }
+}))(Modal)
diff --git a/ui/src/components/internal/ProfilePicture.js b/ui/src/components/internal/ProfilePicture.js
new file mode 100644
index 0000000..e1644c8
--- /dev/null
+++ b/ui/src/components/internal/ProfilePicture.js
@@ -0,0 +1,117 @@
+import classNames from 'classnames'
+import injectSheet from 'react-jss'
+import PropTypes from 'prop-types'
+import React from 'react'
+
+import * as mixins from 'styles/mixins'
+import FontIcon from 'components/FontIcon'
+import { User } from 'models'
+
+const sizes = Object.freeze({
+ TINY: 'tiny',
+ SMALL: 'small',
+ LARGE: 'large'
+})
+
+function ProfilePicture({
+ inverted,
+ isActive,
+ size,
+ user: { firstName, lastName, profilePictureThumbnail, profilePictureNormal },
+ classes
+}) {
+ const profilePicture = (size === sizes.SMALL) ? profilePictureThumbnail : profilePictureNormal
+ const userInitials = User.initials({ firstName, lastName })
+
+ let placeholder = null
+ let isEmpty = false
+ if (!profilePicture) {
+ if (size === sizes.TINY || size === sizes.SMALL) {
+ placeholder = userInitials ||
+ isEmpty = !userInitials
+ } else if (size === sizes.LARGE) {
+ placeholder =
+ isEmpty = true
+ }
+ }
+
+ return (
+
+ {placeholder}
+
+ )
+}
+
+ProfilePicture.sizes = sizes
+
+ProfilePicture.propTypes = {
+ centered: PropTypes.bool,
+ inverted: PropTypes.bool,
+ isActive: PropTypes.bool,
+ size: PropTypes.oneOf(Object.values(sizes)),
+ user: PropTypes.object
+}
+
+ProfilePicture.defaultProps = {
+ centered: false,
+ inverted: false,
+ isActive: false,
+ size: ProfilePicture.sizes.SMALL,
+ user: {}
+}
+
+export default injectSheet(({
+ colors, shadows, typography, units
+}) => ({
+ base: ({ size }) => ({ // To handle specificity issues with functional styles
+ ...mixins.size(units[`profilePictureSize_${size}`]),
+
+ lineHeight: `${units[`profilePictureSize_${size}`] - 2 * units.profilePictureBorderWidth}px`
+ }),
+ profilePicture: {
+ ...mixins.backgroundCover(),
+ ...mixins.transitionSimple(),
+ ...typography.semiboldSmall,
+
+ backgroundClip: ({ user }) => (user.profilePictureThumbnail || user.profilePictureNormal) && 'padding-box',
+ backgroundColor: colors.profilePictureBackground,
+ borderColor: 'transparent',
+ borderRadius: '50%',
+ borderStyle: 'solid',
+ borderWidth: units.profilePictureBorderWidth,
+ color: colors.text_dark,
+ marginLeft: props => props.centered && 'auto',
+ marginRight: props => props.centered && 'auto',
+ textAlign: 'center',
+
+ '& .icon': {
+ color: colors.text_light,
+ lineHeight: props => `${units[`profilePictureSize_${props.size}`] - 2 * units.profilePictureBorderWidth}px`
+ }
+ },
+ profilePicture_empty: {
+ backgroundColor: colors.profilePictureBackground_empty
+ },
+ profilePicture_active: {
+ borderColor: colors.profilePictureBorder
+ },
+ profilePicture_inverted: {
+ backgroundColor: colors.profilePictureBackground_inverted,
+ color: colors.text_light
+ },
+ [`profilePicture_${sizes.LARGE}`]: {
+ boxShadow: shadows.profilePicture
+ }
+}))(ProfilePicture)
diff --git a/ui/src/components/internal/Row.js b/ui/src/components/internal/Row.js
new file mode 100644
index 0000000..64ca9bf
--- /dev/null
+++ b/ui/src/components/internal/Row.js
@@ -0,0 +1,24 @@
+import injectSheet from 'react-jss'
+import PropTypes from 'prop-types'
+import React from 'react'
+
+function Row({ classes, children }) {
+ return (
+
+ {children}
+
+ )
+}
+
+Row.propTypes = {
+ children: PropTypes.node.isRequired
+}
+
+export default injectSheet(({ units }) => ({
+ row: {
+ display: 'flex',
+ flexWrap: 'wrap',
+ marginRight: units.rowMarginHorizontal,
+ marginLeft: units.rowMarginHorizontal
+ }
+}))(Row)
diff --git a/ui/src/components/internal/Tag.js b/ui/src/components/internal/Tag.js
new file mode 100644
index 0000000..30c776c
--- /dev/null
+++ b/ui/src/components/internal/Tag.js
@@ -0,0 +1,111 @@
+import classNames from 'classnames'
+import gql from 'graphql-tag'
+import injectSheet from 'react-jss'
+import PropTypes from 'prop-types'
+import React from 'react'
+
+import * as mixins from 'styles/mixins'
+import FontIcon from 'components/FontIcon'
+
+function Tag({ ariaLabel, isFocused, isRemovable, onRemove, size, classes, children }) {
+ return (
+
+ {children}
+
+ {isRemovable && (
+
+
+
+ )}
+
+ )
+}
+
+Tag.propTypes = {
+ ariaLabel: PropTypes.string,
+ isFocused: PropTypes.bool,
+ isRemovable: PropTypes.bool,
+ size: PropTypes.oneOf([ 'small', 'medium' ]),
+ onRemove: PropTypes.func,
+ children: PropTypes.node.isRequired
+}
+
+Tag.defaultProps = {
+ ariaLabel: null,
+ isRemovable: false,
+ isFocused: false,
+ size: 'small',
+ onRemove: () => null
+}
+
+Tag.fragments = {
+ tag: gql`
+ fragment Tag_tag on Tag {
+ id
+ name
+ }
+ `
+}
+
+export default injectSheet(({ colors, typography, units }) => ({
+ tag: {
+ alignItems: 'center',
+ backgroundColor: colors.tagBackground,
+ borderRadius: units.tagBorderRadius,
+ color: colors.tag,
+ display: 'inline-flex',
+ height: units.tagHeight,
+ paddingRight: units.tagPaddingHorizontal,
+ paddingLeft: units.tagPaddingHorizontal,
+ textTransform: 'uppercase',
+ whiteSpace: 'nowrap',
+
+ '&:not(:last-child)': {
+ marginRight: units.tagMarginRight
+ }
+ },
+ tag_small: {
+ ...typography.regularSmall
+ },
+ tag_medium: {
+ ...typography.regular
+ },
+ tag_isRemovable: {
+ paddingRight: 0
+ },
+ removeTag: {
+ ...mixins.transitionSimple(),
+
+ alignItems: 'center',
+ borderBottomRightRadius: units.tagBorderRadius,
+ borderTopRightRadius: units.tagBorderRadius,
+ cursor: 'pointer',
+ display: 'flex',
+ height: '100%',
+ marginLeft: units.tagPaddingHorizontal / 2,
+ paddingRight: units.tagPaddingHorizontal,
+ paddingLeft: units.tagPaddingHorizontal / 2,
+ opacity: 0.6,
+
+ '&:hover': {
+ backgroundColor: colors.tagRemoveBackground,
+ color: colors.text_light,
+ opacity: 1
+ }
+ },
+ removeTag_focused: {
+ backgroundColor: colors.tagRemoveBackground,
+ color: colors.text_light,
+ opacity: 1
+ }
+}))(Tag)
diff --git a/ui/src/components/internal/Tooltip.js b/ui/src/components/internal/Tooltip.js
new file mode 100644
index 0000000..af716ab
--- /dev/null
+++ b/ui/src/components/internal/Tooltip.js
@@ -0,0 +1,141 @@
+import injectSheet from 'react-jss'
+import PropTypes from 'prop-types'
+import React, { PureComponent } from 'react'
+import { Manager, Reference, Popper } from 'react-popper'
+
+import * as mixins from 'styles/mixins'
+
+class Tooltip extends PureComponent {
+ constructor() {
+ super()
+
+ this.state = {
+ isActive: false
+ }
+ }
+
+ showTooltip = () => {
+ const { onMouseEnter } = this.props
+
+ if (onMouseEnter) {
+ onMouseEnter()
+ }
+
+ this.setState({ isActive: true })
+ }
+
+ hideTooltip = () => {
+ const { onMouseLeave } = this.props
+
+ if (onMouseLeave) {
+ onMouseLeave()
+ }
+
+ this.setState({ isActive: false })
+ }
+
+ render() {
+ const {
+ description, placement, horizontalOffset, verticalOffset, classes, children
+ } = this.props
+ const { isActive } = this.state
+
+ const child = React.Children.only(children)
+
+ return (
+
+
+ {({ ref }) => (
+ React.cloneElement(child, {
+ ref,
+ role: 'tooltip',
+ onMouseEnter: this.showTooltip,
+ onMouseLeave: this.hideTooltip
+ })
+ )}
+
+ {isActive && (
+
+ {({ ref, style, placement: popperPlacement, arrowProps }) => (
+
+ )}
+
+ )}
+
+ )
+ }
+}
+
+Tooltip.propTypes = {
+ description: PropTypes.string.isRequired,
+ placement: PropTypes.oneOf([ 'top', 'bottom' ]),
+ horizontalOffset: PropTypes.oneOfType([ PropTypes.string, PropTypes.number ]),
+ verticalOffset: PropTypes.oneOfType([ PropTypes.string, PropTypes.number ]),
+ onMouseEnter: PropTypes.func,
+ onMouseLeave: PropTypes.func,
+ children: PropTypes.node.isRequired
+}
+
+Tooltip.defaultProps = {
+ placement: 'top',
+ horizontalOffset: -15,
+ verticalOffset: 10,
+ onMouseEnter: null,
+ onMouseLeave: null
+}
+
+export default injectSheet(({ colors, shadows, typography, units, zIndexes }) => ({
+ tooltip: {
+ ...typography.regularSmallSquished,
+
+ backgroundColor: colors.tooltipBackground,
+ boxShadow: shadows.tooltip,
+ color: colors.text_light,
+ padding: units.tooltipPadding,
+ position: 'relative',
+ zIndex: zIndexes.tooltip
+ },
+ arrow: {
+ ...mixins.size(units.tooltipArrowSize),
+
+ position: 'absolute',
+
+ '&::before': {
+ ...mixins.size('100%'),
+
+ backgroundColor: colors.tooltipBackground,
+ borderRadius: 1,
+ content: '" "',
+ display: 'block'
+ },
+
+ '&[data-placement^="top"]': {
+ bottom: units.tooltipArrowVerticalShift,
+
+ '&::before': {
+ transform: 'rotate(45deg)'
+ }
+ },
+
+ '&[data-placement^="bottom"]': {
+ top: units.tooltipArrowVerticalShift,
+
+ '&::before': {
+ transform: 'rotate(45deg)'
+ }
+ }
+ }
+}))(Tooltip)
diff --git a/ui/src/components/internal/buttons/IconButton.js b/ui/src/components/internal/buttons/IconButton.js
new file mode 100644
index 0000000..4b24017
--- /dev/null
+++ b/ui/src/components/internal/buttons/IconButton.js
@@ -0,0 +1,74 @@
+import classNames from 'classnames'
+import injectSheet from 'react-jss'
+import PropTypes from 'prop-types'
+import React from 'react'
+
+import * as mixins from 'styles/mixins'
+import FontIcon from 'components/FontIcon'
+
+function IconButton({ icon, onClick, size, variant, classes }) {
+ return (
+
+
+
+ )
+}
+
+IconButton.propTypes = {
+ heavy: PropTypes.bool,
+ icon: PropTypes.string.isRequired,
+ onClick: PropTypes.func,
+ size: PropTypes.oneOf(Object.keys(FontIcon.sizes)),
+ variant: PropTypes.oneOf([ 'color', 'flat' ])
+}
+
+IconButton.defaultProps = {
+ heavy: false,
+ onClick: null,
+ size: 'tiny',
+ variant: 'flat'
+}
+
+export default injectSheet(({ colors, gradients, shadows, units }) => ({
+ iconButton: {
+ ...mixins.transitionSimple(),
+
+ alignItems: 'center',
+ borderRadius: '50%',
+ borderWidth: 0,
+ boxShadow: props => (props.heavy ? shadows.iconButton_heavy : shadows.iconButton),
+ cursor: 'pointer',
+ display: 'inline-flex',
+ justifyContent: 'center',
+ padding: 0,
+ verticalAlign: 'top',
+
+ '&:hover, &:focus': {
+ boxShadow: shadows.iconButton_hover
+ },
+
+ '& + &': {
+ marginRight: units.iconButtonHorizontalMargin,
+ marginLeft: units.iconButtonHorizontalMargin
+ }
+ },
+ iconButton_flat: {
+ backgroundColor: colors.iconButtonBackground,
+ color: colors.text_pale
+ },
+ iconButton_color: {
+ backgroundImage: gradients.button,
+ color: colors.text_light
+ },
+ iconButtonSize: ({ size }) => ({
+ ...mixins.size(units[`iconButtonSize_${size}`])
+ })
+}))(IconButton)
diff --git a/ui/src/components/internal/dataTable/BoxCell.js b/ui/src/components/internal/dataTable/BoxCell.js
new file mode 100644
index 0000000..bdde56d
--- /dev/null
+++ b/ui/src/components/internal/dataTable/BoxCell.js
@@ -0,0 +1,34 @@
+import injectSheet from 'react-jss'
+import PropTypes from 'prop-types'
+import React from 'react'
+
+import * as mixins from 'styles/mixins'
+
+function BoxCell({ children, classes }) {
+ return (
+
+ {children}
+
+ )
+}
+
+BoxCell.propTypes = {
+ children: PropTypes.node.isRequired
+}
+
+export default injectSheet(({ colors, units }) => ({
+ boxCell: {
+ ...mixins.size(units.dataTableRowHeight),
+
+ alignItems: 'center',
+ backgroundColor: colors.dataTableBoxCellBackground,
+ borderTopLeftRadius: units.dataTableRowBorderRadius,
+ borderBottomLeftRadius: units.dataTableRowBorderRadius,
+ borderRightColor: colors.dataTableBoxCellBorderRight,
+ borderRightStyle: 'solid',
+ borderRightWidth: 1,
+ boxSizing: 'content-box',
+ display: 'flex',
+ justifyContent: 'center'
+ }
+}))(BoxCell)
diff --git a/ui/src/components/internal/dataTable/Column.js b/ui/src/components/internal/dataTable/Column.js
new file mode 100644
index 0000000..6f72478
--- /dev/null
+++ b/ui/src/components/internal/dataTable/Column.js
@@ -0,0 +1,47 @@
+import _ from 'lodash'
+import PropTypes from 'prop-types'
+import React from 'react'
+
+import DefaultCellWrapper from 'components/internal/dataTable/DefaultCellWrapper'
+
+const defaultCellRenderer = ({ record, dataKey }) => {
+ const value = _.get(record, dataKey)
+
+ if (value === undefined) {
+ throw new Error(`${dataKey} is not a valid value for dataKey.`)
+ }
+
+ return value
+}
+
+function Column({
+ cellRenderer,
+ cellWrapper: Wrapper,
+ dataKey,
+ record,
+ ...other
+}) {
+ return (
+
+ {cellRenderer({ record, dataKey })}
+
+ )
+}
+
+Column.propTypes = {
+ cellRenderer: PropTypes.func,
+ cellWrapper: PropTypes.oneOfType([ PropTypes.node, PropTypes.func ]),
+ dataKey: PropTypes.string.isRequired,
+ flexGrow: PropTypes.oneOfType([ PropTypes.string, PropTypes.number ]),
+ record: PropTypes.object.isRequired,
+ width: PropTypes.oneOfType([ PropTypes.string, PropTypes.number ])
+}
+
+Column.defaultProps = {
+ cellRenderer: defaultCellRenderer,
+ cellWrapper: DefaultCellWrapper,
+ flexGrow: 0,
+ width: 100
+}
+
+export default Column
diff --git a/ui/src/components/internal/dataTable/DefaultCellWrapper.js b/ui/src/components/internal/dataTable/DefaultCellWrapper.js
new file mode 100644
index 0000000..dd20083
--- /dev/null
+++ b/ui/src/components/internal/dataTable/DefaultCellWrapper.js
@@ -0,0 +1,70 @@
+import classNames from 'classnames'
+import injectSheet from 'react-jss'
+import PropTypes from 'prop-types'
+import React from 'react'
+
+import * as mixins from 'styles/mixins'
+
+function DefaultCellWrapper({ header, children, classes }) {
+ return (
+
+ {children}
+
+ )
+}
+
+DefaultCellWrapper.propTypes = {
+ bordered: PropTypes.bool,
+ header: PropTypes.bool,
+ uppercase: PropTypes.bool,
+ children: PropTypes.node.isRequired
+}
+
+DefaultCellWrapper.defaultProps = {
+ bordered: true,
+ header: false,
+ uppercase: false
+}
+
+export default injectSheet(({ colors, typography, units }) => ({
+ defaultCellWrapper: {
+ ...typography.regularSmallSpaced,
+
+ flexBasis: ({ width }) => width,
+ flexGrow: ({ flexGrow }) => flexGrow,
+ flexShrink: 1,
+ overflow: 'hidden',
+ padding: units.dataTableCellPadding,
+ position: 'relative',
+ textTransform: ({ uppercase }) => (uppercase ? 'uppercase' : 'none'),
+
+ '&::before': {
+ ...mixins.size(1, units.dataTableCellBorderHeight),
+
+ backgroundColor: colors.dataTableBorderedCellBorderBackground,
+ content: ({ bordered }) => (bordered ? '" "' : ''),
+ marginTop: -units.dataTableCellBorderHeight / 2,
+ position: 'absolute',
+ top: '50%',
+ left: 0
+ },
+
+ '& > p': {
+ overflow: 'hidden',
+ textOverflow: 'ellipsis',
+ whiteSpace: 'nowrap'
+ }
+ },
+ defaultCellWrapper_header: {
+ ...typography.regularSmallCompact,
+
+ color: colors.text_pale
+ }
+}))(DefaultCellWrapper)
diff --git a/ui/src/components/internal/dataTable/Row.js b/ui/src/components/internal/dataTable/Row.js
new file mode 100644
index 0000000..87da2f3
--- /dev/null
+++ b/ui/src/components/internal/dataTable/Row.js
@@ -0,0 +1,334 @@
+import _ from 'lodash'
+import classNames from 'classnames'
+import injectSheet from 'react-jss'
+import PropTypes from 'prop-types'
+import React from 'react'
+
+import * as mixins from 'styles/mixins'
+import FontIcon from 'components/FontIcon'
+
+const rowActionIconSize = 'small'
+
+function Row({
+ actions,
+ compact,
+ header,
+ record,
+ rowIds,
+ rowProps,
+ selectable,
+ selectedIds,
+ sortable,
+ onRowClick,
+ updateSelection,
+ classes,
+ children
+}) {
+ const { isPale = false, showActions = true } = rowProps && rowProps({ record })
+
+ const handleRowClick = () => {
+ if (onRowClick) {
+ onRowClick(record)
+ }
+ }
+
+ const renderDragHandleColumn = () => (
+
+
+
+ )
+
+ const renderCheckboxColumn = () => {
+ const checked = header
+ ? _.isEqual(rowIds.sort(), selectedIds.sort())
+ : record && selectedIds.includes(record.id)
+
+ const handleChange = (e) => {
+ e.stopPropagation()
+
+ if (header) {
+ if (checked) {
+ updateSelection([])
+ } else {
+ updateSelection(rowIds)
+ }
+ } else if (record) {
+ if (checked) {
+ updateSelection(selectedIds.filter(selectedId => selectedId !== record.id))
+ } else {
+ updateSelection([ record.id, ...selectedIds ])
+ }
+ }
+ }
+
+ return (
+
+
+
+ )
+ }
+
+ const renderRowActions = () => {
+ if (!showActions || actions.length === 0) {
+ return null
+ }
+
+ return (
+
+ {actions.map(({ icon, onClick }) => (
+
{
+ e.stopPropagation()
+ onClick(record, e)
+ }}
+ onKeyPress={(e) => {
+ e.stopPropagation()
+ onClick(record, e)
+ }}
+ >
+
+
+ ))}
+
+ )
+ }
+
+ return (
+ handleRowClick(record)}
+ onKeyPress={() => handleRowClick(record)}
+ >
+ {sortable && renderDragHandleColumn()}
+
+ {selectable && renderCheckboxColumn()}
+
+ {children}
+
+ {renderRowActions()}
+
+ )
+}
+
+Row = injectSheet(({
+ colors, shadows, typography, units
+}) => ({
+ dataTableRow: {
+ ...mixins.size('100%', units.dataTableRowHeight),
+ ...mixins.transitionSimple(),
+ ...typography.regularSmallSpaced,
+
+ alignItems: 'center',
+ color: colors.text_dark,
+ cursor: ({ onRowClick }) => (onRowClick ? 'pointer' : 'auto'),
+ display: 'flex',
+ position: 'relative',
+ zIndex: 0,
+
+ '&::before': {
+ ...mixins.transitionSimple(),
+
+ backgroundColor: colors.dataTableRowBackground,
+ borderColor: 'transparent',
+ borderRadius: units.dataTableRowBorderRadius,
+ borderStyle: 'solid',
+ borderWidth: 1,
+ boxShadow: shadows.dataTableRow,
+ content: '" "',
+ position: 'absolute',
+ top: 0,
+ right: 0,
+ bottom: 0,
+ left: 0,
+ zIndex: -1
+ },
+
+ '&:hover': {
+ '&::before': {
+ boxShadow: ({ header }) => (header ? 'none' : shadows.dataTableRow_hover)
+ },
+
+ '& $dataTableRowActions': {
+ opacity: 1,
+ pointerEvents: 'auto'
+ }
+ },
+
+ '&:not(:last-child)': {
+ marginBottom: ({ compact }) => (compact
+ ? units.dataTableRowMarginBottom_compact : units.dataTableRowMarginBottom)
+ }
+ },
+ dataTableRow_compact: {
+ ...mixins.size('100%', units.dataTableRowHeight_compact)
+ },
+ dataTableRow_header: {
+ ...mixins.size('100%', units.dataTableHeaderHeight),
+
+ '&::before': {
+ backgroundColor: colors.dataTableHeaderBackground
+ }
+ },
+ dataTableRow_showActions: {
+ '&:hover': {
+ '&::before': {
+ right: -1 * (units.dataTableRowActionPaddingRight + FontIcon.sizes[rowActionIconSize])
+ }
+ }
+ },
+ dataTableRow_selected: {
+ '&::before': {
+ borderStyle: 'solid',
+ borderWidth: 1,
+ borderColor: colors.dataTableRow_selected
+ }
+ },
+ dataTableRow_pale: {
+ color: colors.text_pale
+ },
+ dataTableDragHandle: {
+ alignItems: 'center',
+ cursor: 'grab',
+ display: 'flex',
+ height: '100%',
+ paddingLeft: units.dataTableDragHandleHorizontalPadding,
+ paddingRight: units.dataTableDragHandleHorizontalPadding
+ },
+ dataTableCheckboxWrapper: {
+ alignItems: 'center',
+ borderRight: ({ compact }) => (compact ? `solid 1px ${colors.dataTableBorderedCellBorderBackground}` : 'none'),
+ cursor: 'pointer',
+ display: 'flex',
+ height: '100%',
+ paddingLeft: units.dataTableCheckboxWrapperHorizontalPadding,
+ paddingRight: ({ compact }) => (compact ? units.dataTableCheckboxWrapperHorizontalPadding : 0)
+ },
+ dataTableCheckbox: {
+ height: ({ compact }) => (
+ compact
+ ? (units.dataTableCheckboxSize_compact + 2 * units.dataTableCheckboxBorderWidth)
+ : units.dataTableCheckboxSize + 2 * units.dataTableCheckboxBorderWidth),
+ width: ({ compact }) => (
+ compact
+ ? (units.dataTableCheckboxSize_compact + 2 * units.dataTableCheckboxBorderWidth)
+ : units.dataTableCheckboxSize + 2 * units.dataTableCheckboxBorderWidth),
+
+ position: 'relative',
+
+ '&::before': {
+ height: ({ compact }) => (
+ compact ? units.dataTableCheckboxSize_compact : units.dataTableCheckboxSize),
+ width: ({ compact }) => (
+ compact ? units.dataTableCheckboxSize_compact : units.dataTableCheckboxSize),
+
+ backgroundColor: colors.checkboxBackground,
+ borderColor: colors.checkboxBorder,
+ borderRadius: units.dataTableCheckboxBorderRadius,
+ borderStyle: 'solid',
+ borderWidth: units.dataTableCheckboxBorderWidth,
+ content: '" "',
+ cursor: 'pointer',
+ position: 'absolute'
+ },
+
+ '&:checked::before': {
+ backgroundColor: colors.checkboxBackground_checked,
+ borderColor: colors.checkboxBackground_checked,
+ color: colors.checkboxTick,
+ content: '"\ue030"', // From fontastic - `icon-tick`
+ fontFamily: 'claycms-icons',
+ fontSize: units.checkboxTickFontSize,
+ lineHeight: ({ compact }) => `${compact ? units.dataTableCheckboxSize_compact : units.dataTableCheckboxSize}px`,
+ textAlign: 'center'
+ }
+ },
+ dataTableRowActions: {
+ ...mixins.transitionSimple(),
+
+ cursor: 'pointer',
+ display: 'flex',
+ flexDirection: 'column',
+ flexWrap: 'wrap',
+ height: '100%',
+ opacity: 0,
+ pointerEvents: 'none',
+ position: 'absolute',
+ left: '100%'
+ },
+ dataTableRowAction: {
+ ...mixins.transitionSimple(),
+
+ alignItems: 'center',
+ color: colors.text_pale,
+ display: 'flex',
+ flexBasis: '50%',
+ paddingRight: units.dataTableRowActionPaddingRight,
+
+ '&:hover': {
+ color: colors.text_dark
+ },
+
+ '&:nth-child(2n + 1)': {
+ paddingTop: units.dataTableRowActionPaddingTop
+ },
+
+ '&:nth-child(2n)': {
+ paddingBottom: units.dataTableRowActionPaddingBottom
+ }
+ }
+}))(Row)
+
+Row.propTypes = {
+ actions: PropTypes.arrayOf(PropTypes.shape({
+ icon: PropTypes.string.isRequired,
+ onClick: PropTypes.func
+ })),
+ children: PropTypes.node.isRequired,
+ compact: PropTypes.bool,
+ header: PropTypes.bool,
+ rowProps: PropTypes.func,
+ onRowClick: PropTypes.func,
+ record: PropTypes.object,
+ rowIds: PropTypes.arrayOf(PropTypes.string),
+ selectable: PropTypes.bool,
+ selectedIds: PropTypes.arrayOf(PropTypes.string),
+ sortable: PropTypes.bool,
+ updateSelection: PropTypes.func
+}
+
+Row.defaultProps = {
+ actions: [],
+ compact: false,
+ header: false,
+ rowProps: () => ({}),
+ onRowClick: null,
+ record: null,
+ rowIds: [],
+ selectable: true,
+ selectedIds: [],
+ sortable: false,
+ updateSelection: () => null
+}
+
+export default Row
diff --git a/ui/src/components/internal/dataTable/Table.js b/ui/src/components/internal/dataTable/Table.js
new file mode 100644
index 0000000..92da9e5
--- /dev/null
+++ b/ui/src/components/internal/dataTable/Table.js
@@ -0,0 +1,122 @@
+import PropTypes from 'prop-types'
+import React, { useState, useEffect } from 'react'
+import { ReactSortable } from 'react-sortablejs'
+
+import Column from 'components/internal/dataTable/Column'
+import Row from 'components/internal/dataTable/Row'
+
+function DataTable({
+ columns,
+ compact,
+ loading,
+ onSelectionChange,
+ onSortingChange,
+ sortable,
+ records,
+ ...rest
+}) {
+ const [ selectedIds, setSelectedIds ] = useState([])
+ const [ sortedRecords, setSortedRecords ] = useState([])
+
+ useEffect(() => {
+ setSortedRecords(records || [])
+ }, [ records ])
+
+ const updateSelection = (ids) => {
+ setSelectedIds(ids)
+ onSelectionChange(ids)
+ }
+
+ if (loading || !records || records.length === 0) {
+ return null
+ }
+
+ const rowIds = records.map(record => record.id)
+
+ const rows = sortedRecords.map(record => (
+
+ {columns.map(({ dataKey, ...other }) => (
+
+ ))}
+
+ ))
+
+ return (
+
+ {compact && (
+
+ {columns.map(({
+ dataKey,
+ label,
+ cellRenderer, /* avoid passing to use defaultCellRenderer for header */
+ ...other
+ }) => (
+
+ ))}
+
+ )}
+ {sortable ? (
+ setSortedRecords(newSortedRecords)}
+ onEnd={() => onSortingChange(sortedRecords)}
+ >
+ {rows}
+
+ ) : rows}
+
+ )
+}
+
+DataTable.propTypes = {
+ actions: Row.propTypes.actions,
+ columns: PropTypes.array,
+ compact: PropTypes.bool,
+ loading: PropTypes.bool,
+ onRowClick: Row.propTypes.onRowClick,
+ onSelectionChange: PropTypes.func,
+ onSortingChange: PropTypes.func,
+ records: PropTypes.array,
+ rowProps: Row.propTypes.rowProps,
+ selectable: Row.propTypes.selectable,
+ sortable: Row.propTypes.sortable
+}
+
+DataTable.defaultProps = {
+ actions: Row.defaultProps.actions,
+ columns: [],
+ compact: false,
+ loading: false,
+ onRowClick: Row.defaultProps.onRowClick,
+ onSelectionChange: () => null,
+ onSortingChange: () => null,
+ records: null,
+ rowProps: Row.defaultProps.rowProps,
+ selectable: Row.defaultProps.selectable,
+ sortable: Row.defaultProps.sortable
+}
+
+export default DataTable
diff --git a/ui/src/components/internal/decorators/withConfirmation.js b/ui/src/components/internal/decorators/withConfirmation.js
new file mode 100644
index 0000000..d42de03
--- /dev/null
+++ b/ui/src/components/internal/decorators/withConfirmation.js
@@ -0,0 +1,37 @@
+import hoistNonReactStatics from 'hoist-non-react-statics'
+import React, { Fragment, useState } from 'react'
+
+import ConfirmationDialog from 'components/internal/dialogs/ConfirmationDialog'
+
+function withConfirmation() {
+ return (WrappedComponent) => {
+ function EnhancedComponent(props) {
+ const [ options, setOptions ] = useState({})
+ const [ isConfirmationDialogOpen, setIsConfirmationDialogOpen ] = useState(false)
+
+ const openConfirmationDialog = (values) => {
+ setOptions(values)
+ setIsConfirmationDialogOpen(true)
+ }
+
+ const closeConfirmationDialog = () => setIsConfirmationDialogOpen(false)
+
+ return (
+
+
+
+
+ )
+ }
+
+ hoistNonReactStatics(EnhancedComponent, WrappedComponent)
+
+ return EnhancedComponent
+ }
+}
+
+export default withConfirmation
diff --git a/ui/src/components/internal/dialogs/AssetDialog.js b/ui/src/components/internal/dialogs/AssetDialog.js
new file mode 100644
index 0000000..44369af
--- /dev/null
+++ b/ui/src/components/internal/dialogs/AssetDialog.js
@@ -0,0 +1,17 @@
+import React from 'react'
+
+import AssetForm from 'components/internal/forms/AssetForm'
+import Dialog from 'components/internal/Dialog'
+
+function AssetDialog({ formValues, onFormSubmit, ...other }) {
+ const action = formValues.id ? 'Edit' : 'Add'
+ const title = `${action} Asset`
+
+ return (
+
+
+
+ )
+}
+
+export default AssetDialog
diff --git a/ui/src/components/internal/dialogs/ConfirmationDialog.js b/ui/src/components/internal/dialogs/ConfirmationDialog.js
new file mode 100644
index 0000000..0911c55
--- /dev/null
+++ b/ui/src/components/internal/dialogs/ConfirmationDialog.js
@@ -0,0 +1,49 @@
+import PropTypes from 'prop-types'
+import React from 'react'
+
+import Dialog from 'components/internal/Dialog'
+import DialogFormFooter from 'components/internal/DialogFormFooter'
+import FilledButton from 'components/buttons/FilledButton'
+
+import { Text } from 'components/internal/typography'
+
+function ConfirmationDialog({ description, onConfirmClick, onRequestClose, ...other }) {
+ const handleConfirmClick = () => {
+ if (onConfirmClick) {
+ onConfirmClick()
+ }
+
+ if (onRequestClose) {
+ onRequestClose()
+ }
+ }
+
+ return (
+
+ {description && (
+
+ {description}
+
+ )}
+
+
+
+
+
+
+ )
+}
+
+ConfirmationDialog.propTypes = {
+ description: PropTypes.string,
+ onConfirmClick: PropTypes.func,
+ onRequestClose: PropTypes.func
+}
+
+ConfirmationDialog.defaultProps = {
+ description: null,
+ onConfirmClick: null,
+ onRequestClose: null
+}
+
+export default ConfirmationDialog
diff --git a/ui/src/components/internal/dialogs/ImportProjectDialog.js b/ui/src/components/internal/dialogs/ImportProjectDialog.js
new file mode 100644
index 0000000..77f7805
--- /dev/null
+++ b/ui/src/components/internal/dialogs/ImportProjectDialog.js
@@ -0,0 +1,16 @@
+import React from 'react'
+
+import Dialog from 'components/internal/Dialog'
+import ImportProjectForm from 'components/internal/forms/ImportProjectForm'
+
+function ImportDialog({ formValues, onFormSubmit, ...other }) {
+ const title = 'Import Project'
+
+ return (
+
+
+
+ )
+}
+
+export default ImportDialog
diff --git a/ui/src/components/internal/dialogs/ProjectDialog.js b/ui/src/components/internal/dialogs/ProjectDialog.js
new file mode 100644
index 0000000..1574872
--- /dev/null
+++ b/ui/src/components/internal/dialogs/ProjectDialog.js
@@ -0,0 +1,16 @@
+import React from 'react'
+
+import Dialog from 'components/internal/Dialog'
+import ProjectForm from 'components/internal/forms/ProjectForm'
+
+function ProjectDialog({ formValues, onFormSubmit, ...other }) {
+ const title = 'Create a Project'
+
+ return (
+
+
+
+ )
+}
+
+export default ProjectDialog
diff --git a/ui/src/components/internal/dialogs/TeamDialog.js b/ui/src/components/internal/dialogs/TeamDialog.js
new file mode 100644
index 0000000..2ea6459
--- /dev/null
+++ b/ui/src/components/internal/dialogs/TeamDialog.js
@@ -0,0 +1,16 @@
+import React from 'react'
+
+import Dialog from 'components/internal/Dialog'
+import TeamForm from 'components/internal/forms/TeamForm'
+
+function TeamDialog({ onFormSubmit, ...other }) {
+ const title = 'Create a Team'
+
+ return (
+
+
+
+ )
+}
+
+export default TeamDialog
diff --git a/ui/src/components/internal/dialogs/TeamMemberDialog.js b/ui/src/components/internal/dialogs/TeamMemberDialog.js
new file mode 100644
index 0000000..c242e9b
--- /dev/null
+++ b/ui/src/components/internal/dialogs/TeamMemberDialog.js
@@ -0,0 +1,21 @@
+import React from 'react'
+
+import Dialog from 'components/internal/Dialog'
+import TeamMemberForm from 'components/internal/forms/TeamMemberForm'
+import { TeamMembership } from 'models'
+
+function TeamMemberDialog({ formValues, onFormSubmit, ...other }) {
+ const action = formValues.id ? 'Edit' : 'Invite New'
+ const title = `${action} Member`
+
+ const { id, role = TeamMembership.defaultRole, user: { email } = {}, ...rest } = formValues
+ const initialValues = { id, role, email, ...rest }
+
+ return (
+
+
+
+ )
+}
+
+export default TeamMemberDialog
diff --git a/ui/src/components/internal/fields/ConditionalField.js b/ui/src/components/internal/fields/ConditionalField.js
new file mode 100644
index 0000000..057d288
--- /dev/null
+++ b/ui/src/components/internal/fields/ConditionalField.js
@@ -0,0 +1,42 @@
+import PropTypes from 'prop-types'
+import React from 'react'
+import { Field } from 'react-final-form'
+
+function ConditionalField({ is, isOneOf, when, children }) {
+ const renderMatch = (value) => {
+ if (isOneOf) {
+ return isOneOf.includes(value) ? children : null
+ }
+
+ return value === is ? children : null
+ }
+
+ return (
+
+ {({ input: { value } }) => renderMatch(value)}
+
+ )
+}
+
+ConditionalField.propTypes = {
+ children: PropTypes.node.isRequired,
+ when: PropTypes.string.isRequired,
+ isOrIsOneOf: ({ is, isOneOf }, propName, componentName) => {
+ if (!is && !isOneOf) {
+ return new Error(`One of props 'is' or 'isOneOf' was not specified in '${componentName}'.`)
+ }
+
+ PropTypes.checkPropTypes({
+ is: PropTypes.any,
+ isOneOf: PropTypes.arrayOf(PropTypes.any)
+ }, { is, isOneOf }, 'prop', componentName)
+
+ return null
+ }
+}
+
+ConditionalField.defaultProps = {
+ isOrIsOneOf: null
+}
+
+export default ConditionalField
diff --git a/ui/src/components/internal/forms/AssetForm.js b/ui/src/components/internal/forms/AssetForm.js
new file mode 100644
index 0000000..9c0dd2a
--- /dev/null
+++ b/ui/src/components/internal/forms/AssetForm.js
@@ -0,0 +1,25 @@
+import React from 'react'
+import { Field, Form } from 'react-final-form'
+
+import DialogFormFooter from 'components/internal/DialogFormFooter'
+import FilledButton from 'components/buttons/FilledButton'
+import TextInput from 'components/inputs/TextInput'
+
+function AssetForm(props) {
+ return (
+
+ )}
+ {...props}
+ />
+ )
+}
+
+export default AssetForm
diff --git a/ui/src/components/internal/forms/ChangePasswordForm.js b/ui/src/components/internal/forms/ChangePasswordForm.js
new file mode 100644
index 0000000..eee93f3
--- /dev/null
+++ b/ui/src/components/internal/forms/ChangePasswordForm.js
@@ -0,0 +1,60 @@
+import React from 'react'
+import { Form, Field } from 'react-final-form'
+
+import Column from 'components/internal/Column'
+import FilledButton from 'components/buttons/FilledButton'
+import ItemBar from 'components/ItemBar'
+import Row from 'components/internal/Row'
+import Spacer from 'components/Spacer'
+import TextInput from 'components/inputs/TextInput'
+import { User } from 'models'
+
+function ChangePasswordForm(props) {
+ return (
+
+ )}
+ {...props}
+ />
+ )
+}
+
+export default ChangePasswordForm
diff --git a/ui/src/components/internal/forms/ChangeProjectNameForm.js b/ui/src/components/internal/forms/ChangeProjectNameForm.js
new file mode 100644
index 0000000..bf89c99
--- /dev/null
+++ b/ui/src/components/internal/forms/ChangeProjectNameForm.js
@@ -0,0 +1,26 @@
+import React from 'react'
+import { Field, Form } from 'react-final-form'
+
+import FilledButton from 'components/buttons/FilledButton'
+import FormFooter from 'components/internal/FormFooter'
+import TextInput from 'components/inputs/TextInput'
+import { Project } from 'models'
+
+function ChangeProjectNameForm(props) {
+ return (
+
+ )}
+ {...props}
+ />
+ )
+}
+
+export default ChangeProjectNameForm
diff --git a/ui/src/components/internal/forms/ChangeTeamNameForm.js b/ui/src/components/internal/forms/ChangeTeamNameForm.js
new file mode 100644
index 0000000..c948012
--- /dev/null
+++ b/ui/src/components/internal/forms/ChangeTeamNameForm.js
@@ -0,0 +1,28 @@
+import React from 'react'
+import { Field, Form } from 'react-final-form'
+
+import FilledButton from 'components/buttons/FilledButton'
+import ItemBar from 'components/ItemBar'
+import Spacer from 'components/Spacer'
+import TextInput from 'components/inputs/TextInput'
+import { Team } from 'models'
+
+function ChangeTeamNameForm(props) {
+ return (
+
+ )}
+ {...props}
+ />
+ )
+}
+
+export default ChangeTeamNameForm
diff --git a/ui/src/components/internal/forms/CreateTransferRequestForm.js b/ui/src/components/internal/forms/CreateTransferRequestForm.js
new file mode 100644
index 0000000..15b456d
--- /dev/null
+++ b/ui/src/components/internal/forms/CreateTransferRequestForm.js
@@ -0,0 +1,43 @@
+import PropTypes from 'prop-types'
+import React from 'react'
+import { Form, Field } from 'react-final-form'
+
+import FilledButton from 'components/buttons/FilledButton'
+import ItemBar from 'components/ItemBar'
+import SingleSelectInput from 'components/internal/inputs/SingleSelectInput'
+import Spacer from 'components/Spacer'
+import { Team } from 'models'
+
+function CreateTransferRequestForm({ options, ...other }) {
+ return (
+
+ )}
+ {...other}
+ />
+ )
+}
+
+CreateTransferRequestForm.propTypes = {
+ options: PropTypes.array
+}
+
+CreateTransferRequestForm.defaultProps = {
+ options: []
+}
+
+export default CreateTransferRequestForm
diff --git a/ui/src/components/internal/forms/EntityForm.js b/ui/src/components/internal/forms/EntityForm.js
new file mode 100644
index 0000000..95627c8
--- /dev/null
+++ b/ui/src/components/internal/forms/EntityForm.js
@@ -0,0 +1,58 @@
+import _ from 'lodash'
+import createDecorator from 'final-form-calculate'
+import React from 'react'
+import { Field, Form } from 'react-final-form'
+
+import FilledButton from 'components/buttons/FilledButton'
+import SingleSelectInput from 'components/internal/inputs/SingleSelectInput'
+import SwitchInput from 'components/internal/inputs/SwitchInput'
+import TextInput from 'components/inputs/TextInput'
+import { Entity } from 'models'
+import { SidePaneFormFooter } from 'components/internal/sidePane'
+
+const decorator = createDecorator({
+ field: 'label',
+ updates: {
+ name: value => value && _.snakeCase(value)
+ }
+})
+
+function EntityForm({ entities, initialValues, ...other }) {
+ const decorators = initialValues.id ? [] : [ decorator ]
+
+ const hiddenField = initialValues.id
+ ?
+ :
+
+ const validateFunction = initialValues.id
+ ? Entity.validateUpdate
+ : Entity.validateCreate
+
+ const entityOptions = entities.map(entity => ({ label: entity.label, value: entity.id }))
+ entityOptions.unshift({ label: 'None', value: null })
+
+ return (
+
+ )}
+ {...other}
+ />
+ )
+}
+
+export default EntityForm
diff --git a/ui/src/components/internal/forms/FieldForm.js b/ui/src/components/internal/forms/FieldForm.js
new file mode 100644
index 0000000..96747cf
--- /dev/null
+++ b/ui/src/components/internal/forms/FieldForm.js
@@ -0,0 +1,277 @@
+/* eslint-disable no-use-before-define */
+import _ from 'lodash'
+import arrayMutators from 'final-form-arrays'
+import createDecorator from 'final-form-calculate'
+import React, { Fragment } from 'react'
+import Spacer from 'components/Spacer'
+import Text from 'components/internal/typography/Text'
+import { Field, Form } from 'react-final-form'
+import { FieldArray } from 'react-final-form-arrays'
+
+import CloseButton from 'components/buttons/CloseButton'
+import FieldGroup from 'components/internal/FieldGroup'
+import FilledButton from 'components/buttons/FilledButton'
+import FontIcon from 'components/FontIcon'
+import SingleSelectInput from 'components/internal/inputs/SingleSelectInput'
+import SwitchInput from 'components/internal/inputs/SwitchInput'
+import TextInput from 'components/inputs/TextInput'
+import { Field as FieldModel } from 'models'
+import { FieldPrefix, PrefixedField } from 'components/internal/FieldPrefix'
+import { SidePaneFormFooter } from 'components/internal/sidePane'
+import { Tab, Tabs, TabPanel, TabList } from 'components/internal/tab'
+
+const decorator = createDecorator(
+ {
+ field: /\b(\w*label\w*)\b/,
+ updates: (value, name) => {
+ const fieldName = name.replace('label', 'name')
+
+ return { [fieldName]: value && _.snakeCase(value) }
+ }
+ },
+ {
+ field: /settings\.options\[\d+\]\.label/,
+ updates: (value, name) => {
+ const fieldName = name.replace('.label', '.key')
+
+ return {
+ [fieldName]: value && _.snakeCase(value)
+ }
+ }
+ }
+)
+
+const isPrimitiveDataType = type => !(type === 'array' || type === 'key_value' || type === 'reference')
+
+function FieldForm({ classes, entities, initialValues, ...other }) {
+ const decorators = initialValues.id ? [] : [ decorator ]
+ const entityOptions = entities.map(entity => ({ label: entity.label, value: entity.id }))
+
+ const hiddenField = initialValues.id
+ ?
+ :
+
+ const isExistingField = Boolean(initialValues.id)
+ const validateFunction = isExistingField
+ ? FieldModel.validateUpdate
+ : FieldModel.validateCreate
+
+ const renderArray = (values, parentName = null) => {
+ const prefix = parentName ? `${parentName}.` : ''
+
+ return (
+
+
+
+ {values.elementType === 'key_value' && renderKeyPair(values, parentName)}
+ {values.elementType === 'reference' && renderReference(parentName)}
+
+ )
+ }
+
+ const renderKeyPair = (values, parentName = null) => {
+ const prefix = parentName ? `${parentName}.` : ''
+
+ return (
+
+ Nested Fields
+
+
+
+ {({ fields }) => (
+
+ {fields.map((name, index) => (
+
+
+ fields.remove(index)} />
+
+ {renderForm({ ...values.children[index], position: index }, name)}
+
+ ))}
+ fields.push({ dataType: 'single_line_text' })} />
+
+ )}
+
+
+
+ )
+ }
+
+ const renderReference = (parentName = null) => {
+ const prefix = parentName ? `${parentName}.` : ''
+
+ return (
+
+ )
+ }
+
+ const renderVersionOptions = prefix => (
+
+ Versions
+
+
+ {({ fields }) => (
+
+ {fields.map((name, index) => (
+
+
+ fields.remove(index)} />
+
+
+
+
+
+ ))}
+ fields.push(undefined)} />
+
+ )}
+
+
+ )
+
+ const renderOptions = prefix => (
+
+ Restrict to the following
+
+
+ {({ fields }) => (
+
+ {fields.map((name, index) => (
+
+
+ fields.remove(index)} />
+
+
+
+
+ ))}
+ fields.push(undefined)} />
+
+ )}
+
+
+ )
+
+ const renderForm = (values, parentName = null) => {
+ const prefix = parentName ? `${parentName}.` : ''
+ const isNonPrimitiveDataType = values
+ && values.dataType
+ && !isPrimitiveDataType(values.dataType)
+
+ return (
+
+
+
+
+ Config
+
+
+
+ Validations
+
+
+
+
+
+
+
+ {isNonPrimitiveDataType && (
+
+ {values.dataType === 'array' && renderArray(values, parentName)}
+ {values.dataType === 'key_value' && renderKeyPair(values, parentName)}
+ {values.dataType === 'reference' && renderReference(parentName)}
+
+ )}
+
+ {values && values.dataType && isPrimitiveDataType(values.dataType) && (
+
+ )}
+
+ +value}
+ />
+
+
+
+ {!parentName && (
+
+ )}
+
+
+
+
+
+
+ {values.dataType === 'single_line_text' && renderOptions(prefix)}
+ {values.dataType === 'image' && renderVersionOptions(prefix)}
+
+
+ )
+ }
+
+ return (
+
+ )}
+ {...other}
+ />
+ )
+}
+
+export default FieldForm
diff --git a/ui/src/components/internal/forms/ImportProjectForm.js b/ui/src/components/internal/forms/ImportProjectForm.js
new file mode 100644
index 0000000..5f55609
--- /dev/null
+++ b/ui/src/components/internal/forms/ImportProjectForm.js
@@ -0,0 +1,26 @@
+import React from 'react'
+import { Field, Form } from 'react-final-form'
+
+import FilledButton from 'components/buttons/FilledButton'
+import FormFooter from 'components/internal/FormFooter'
+import TextInput from 'components/inputs/TextInput'
+import { Restore } from 'models'
+
+function ImportProjectForm(props) {
+ return (
+
+ )}
+ {...props}
+ />
+ )
+}
+
+export default ImportProjectForm
diff --git a/ui/src/components/internal/forms/ProjectForm.js b/ui/src/components/internal/forms/ProjectForm.js
new file mode 100644
index 0000000..c8b46e8
--- /dev/null
+++ b/ui/src/components/internal/forms/ProjectForm.js
@@ -0,0 +1,44 @@
+import React from 'react'
+import { Field, Form } from 'react-final-form'
+
+import DialogFormFooter from 'components/internal/DialogFormFooter'
+import FilledButton from 'components/buttons/FilledButton'
+import HintBox from 'components/internal/HintBox'
+import TextInput from 'components/inputs/TextInput'
+import { DialogDescription, Hint } from 'components/internal/typography'
+import { Project } from 'models'
+
+function ProjectForm(props) {
+ return (
+
+ )}
+ {...props}
+ />
+ )
+}
+
+export default ProjectForm
diff --git a/ui/src/components/internal/forms/RecordForm.js b/ui/src/components/internal/forms/RecordForm.js
new file mode 100644
index 0000000..d00b0dd
--- /dev/null
+++ b/ui/src/components/internal/forms/RecordForm.js
@@ -0,0 +1,522 @@
+/* eslint-disable max-len */
+/* eslint-disable no-use-before-define */
+import _ from 'lodash'
+import arrayMutators from 'final-form-arrays'
+import FieldGroup from 'components/internal/FieldGroup'
+import injectSheet from 'react-jss'
+import React, { Fragment, useState } from 'react'
+import { Field, Form } from 'react-final-form'
+import { FieldArray } from 'react-final-form-arrays'
+import { ReactSortable } from 'react-sortablejs'
+
+import * as mixins from 'styles/mixins'
+import ButtonGroupInput from 'components/internal/inputs/ButtonGroupInput'
+import CloseButton from 'components/buttons/CloseButton'
+import ColorPickerInput from 'components/internal/inputs/ColorPickerInput'
+import DragButton from 'components/buttons/DragButton'
+import FieldModel from 'models/Field'
+import FilledButton from 'components/buttons/FilledButton'
+import Hint from 'components/internal/typography/Hint'
+import ItemBar from 'components/ItemBar'
+import Record from 'models/Record'
+import SingleSelectInput from 'components/internal/inputs/SingleSelectInput'
+import Spacer from 'components/Spacer'
+import SwitchInput from 'components/internal/inputs/SwitchInput'
+import Text from 'components/internal/typography/Text'
+import TextInput from 'components/inputs/TextInput'
+import UploadInput from 'components/inputs/UploadInput'
+import { LoaderText } from 'components/internal/typography'
+import { SidePaneFormFooter } from 'components/internal/sidePane'
+
+const REFERENCE_OPTIONS = [
+ { value: 'new', label: 'New Record' },
+ { value: 'edit', label: 'Existing Record' }
+]
+
+const KeyValueField = ({ childFields, fieldLabel, initialValues, name, parentField, values, ...props }) => {
+ const isParentArray = parentField && parentField.dataType === 'array'
+
+ const [ isHidden, setIsHidden ] = useState(!(isParentArray && !(values && values.value)))
+
+ const Wrapper = isParentArray ? Fragment : FieldGroup
+ const subValues = isParentArray && values ? values.value : values
+ const subInitialValues = initialValues && initialValues.value
+ const isNestedField = parentField && parentField.parentId
+
+ return (
+
+
+ {fieldLabel}
+ {isNestedField && (
+ setIsHidden(setIsHiddenPrev => !setIsHiddenPrev)}
+ />
+ )}
+
+ {isNestedField && isHidden && (
+
+
+
+ {Record.summarizeKeyValuePair(childFields, subValues, isHidden)}
+
+
+
+ )}
+
+ {(!isNestedField || !isHidden) && (
+
+ {childFields.map(childField => fieldRenderer({
+ ...props,
+ field: childField,
+ name: `${name}.${childField.name}`,
+ values: subValues && subValues[childField.name],
+ initialValues: subInitialValues && subInitialValues[childField.name]
+ }))}
+
+ )}
+
+ )
+}
+
+const fieldRenderer = (props) => {
+ const { classes, fields, field, values, initialValues, name } = props
+ const childFields = fields.filter(df => df.parentId === field.id)
+ const key = `${name}-${field.id}`
+ const isRequired = field.validations && field.validations.presence
+ const isRequiredAsterix = isRequired ? '*' : ''
+ const fieldLabel = `${field.label}${isRequiredAsterix}`
+
+ if (field.dataType === 'image') {
+ const { versions = [] } = field.settings
+ let minVersion = versions[0]
+ let note = ''
+
+ versions.forEach((version) => {
+ if (version.width < minVersion.width) {
+ minVersion = version
+ }
+ })
+
+ if (minVersion) {
+ note = (
+
+ Note: Minimum dimensions -
+ {minVersion.width}
+ x
+ {minVersion.height}
+
+ )
+ }
+
+ return (
+
+
+ {fieldLabel}
+
+
+
+ )
+ }
+
+ if (field.dataType === 'file') {
+ return (
+
+
+ {fieldLabel}
+
+
+
+ )
+ }
+
+ if (field.dataType === 'boolean') {
+ return (
+
+
+
+ )
+ }
+
+ if (field.dataType === 'color') {
+ return (
+
+ )
+ }
+
+ if (field.dataType === 'key_value') {
+ return (
+
+ )
+ }
+
+ if (field.dataType === 'array') {
+ return (
+
+ {fieldLabel}
+
+
+
+ {({ fields: fieldsArray }) => {
+ const list = fieldsArray.map((fieldArrayName, index) => ({ id: fieldArrayName, index, fieldArrayName }))
+
+ return (
+
+ {
+ currentList.forEach((a, index) => {
+ fieldsArray.update(index, {
+ ...values[a.index],
+ position: index
+ })
+ })
+ }}
+ >
+ {list.map(({ fieldArrayName, index }) => (
+
+
+
+ fieldsArray.remove(index)} />
+
+
+ {childFields.map((childField) => {
+ const childFieldName = childField.dataType === 'reference' ? fieldArrayName : `${fieldArrayName}.value`
+ const newProps = {
+ ...props,
+ field: childField,
+ parentField: field,
+ name: childFieldName,
+ values: values && values[index],
+ initialValues: initialValues && initialValues[index]
+ }
+
+ return fieldRenderer(newProps)
+ })}
+
+ ))}
+
+ fieldsArray.push({ position: list.length })} />
+
+
+ )
+ }}
+
+
+ )
+ }
+
+ if (field.dataType === 'reference') {
+ if (!field.referencedEntityId) {
+ return {`Reference entity not defined for field - ${field.label}.`}
+ }
+
+ const { mutators, getEntities, entitiesLoading, recordLoading, getRecord } = props
+ const parentEntity = props.entities.find(e => e.id === field.referencedEntityId)
+ const loading = Loading References...
+
+ if (entitiesLoading) {
+ return loading
+ }
+
+ if (!parentEntity) {
+ getEntities(field.referencedEntityId)
+
+ return loading
+ }
+
+ const groupedEntities = props.entities.filter(e => e.parentId === parentEntity.id)
+ const entities = (groupedEntities.length && groupedEntities) || [ parentEntity ]
+
+ const entitiesOptions = entities.map(e => ({ label: e.label, value: e.id, entity: e }))
+ const existingRecordsOptions = entities && entities.reduce((options, entity) => ([
+ ...options,
+ ...entity.records.map(record => ({ value: record.id, label: new Record(entity.fields).summarize(record, entity), entity })) || []
+ ]), [])
+
+ const currentOption = (values && values.currentOption) || 'edit'
+ const isformHidden = values && (values.hidden || values.hidden === undefined)
+ const selectedEntityId = values && values.entityId
+ const selectedRecord = values && values.id
+
+ let selectedEntity = {}
+ let record = null
+ if (currentOption === 'new' && selectedEntityId) {
+ selectedEntity = entities.find(e => e.id === selectedEntityId)
+ } else if (currentOption === 'edit' && selectedRecord) {
+ const recordOption = existingRecordsOptions.find(o => o.value === selectedRecord)
+
+ if (!recordOption) {
+ if (!recordLoading) {
+ getRecord(selectedRecord)
+ }
+
+ return loading
+ }
+
+ selectedEntity = existingRecordsOptions.find(o => o.value === selectedRecord).entity
+ record = selectedEntity && selectedEntity.records.find(r => r.id === selectedRecord)
+ }
+
+ const newProps = {
+ ...props,
+ fields: selectedEntity.fields || [],
+ parentName: name
+ }
+
+ return (
+
+ {fieldLabel}
+
+
+ {currentOption && (
+
+
+
+ {currentOption === 'new' && (
+ mutators.newReference(name, option.entity)}
+ />
+ )}
+ {currentOption === 'edit' && (
+ mutators.editReference(name, record, selectedEntity.fields)}
+ truncateText
+ />
+ )}
+ {((selectedRecord && currentOption === 'edit') || (selectedEntityId && currentOption === 'new')) && (
+
+
+ mutators.toggleFormVisibility(name, record, selectedEntity.fields)}
+ />
+ mutators.clearReference(name)} />
+
+ )}
+
+
+
+ )}
+ {!isformHidden && renderForm(newProps)}
+
+ )
+ }
+
+ const fieldOptions = {}
+
+ if (field.dataType === 'number') {
+ fieldOptions.type = 'number'
+ fieldOptions.autoComplete = 'off'
+ fieldOptions.parse = value => +value
+ } else if (field.dataType === 'multiple_line_text') {
+ fieldOptions.isMultiline = true
+ }
+
+ if (field.dataType === 'single_line_text' && field.settings && field.settings.options) {
+ const options = field.settings.options.map(option => ({ label: option.label, value: option.key }))
+
+ return (
+
+ )
+ }
+
+ return (
+ v}
+ stretched={false}
+ {...fieldOptions}
+ />
+ )
+}
+
+const renderForm = ({ fields, parentName, initialValues, values, ...other }) => {
+ const hiddenFieldName = parentName ? `${parentName}.id` : 'id'
+ const hiddenField = initialValues && initialValues.id
+ ?
+ :
+ let rootFields = fields.filter(f => !f.parentId)
+ rootFields = _.sortBy(rootFields, [ 'position' ])
+
+ return (
+
+ {hiddenField}
+ {rootFields.map((field) => {
+ const name = parentName ? `${parentName}.traits.${field.name}` : `traits.${field.name}`
+ const subValues = values && values.traits && values.traits[field.name]
+ const subInitialValues = initialValues && initialValues.traits && initialValues.traits[field.name]
+
+ return fieldRenderer({
+ ...other,
+ values: subValues,
+ field,
+ fields,
+ name,
+ initialValues: subInitialValues
+ })
+ })}
+
+ )
+}
+
+const newReferenceMutator = ([ name, entity ], state, { changeValue }) => {
+ const traits = {}
+ const parentFields = entity.fields.filter(FieldModel.isRoot)
+ parentFields.forEach((field) => {
+ traits[field.name] = ''
+ })
+
+ changeValue(state, name, () => ({ entityId: entity.id, traits, hidden: false, currentOption: 'new' }))
+}
+
+const editReferenceMutator = ([ name, selectedRecord, fields ], state, { changeValue }) => {
+ const record = selectedRecord && new Record(fields || []).process([ selectedRecord ])
+
+ changeValue(state, name, () => ({ ...record, hidden: true, currentOption: 'edit' }))
+}
+
+const clearReferenceMutator = ([ name ], state, { changeValue }) => (
+ changeValue(state, name, () => ({ hidden: true, currentOption: 'new' }))
+)
+
+const toggleFormVisibilityMutator = ([ name, selectedRecord, fields ], state, { changeValue }) => {
+ changeValue(state, `${name}.hidden`, (prevValue) => {
+ if (prevValue === undefined) {
+ return false
+ }
+
+ return !prevValue
+ })
+ changeValue(state, name, (prevValue) => {
+ if (selectedRecord && !prevValue.traits) {
+ const record = selectedRecord && new Record(fields).process([ selectedRecord ])[0]
+
+ return {
+ ...prevValue,
+ ...record
+ }
+ }
+ return prevValue
+ })
+}
+
+function RecordForm({ initialValues, entities, fields, ...other }) {
+ const newRecord = !initialValues.id
+
+ return (
+
+ )}
+ {...other}
+ />
+ )
+}
+
+export default injectSheet(({ colors, typography }) => ({
+ inputWrapper: {
+ marginBottom: 50
+ },
+ input: {
+ ...mixins.transitionSimple(),
+ ...typography.regularSquished
+ },
+ label: {
+ ...mixins.transitionSimple(),
+ ...typography.regularSmallSpaced,
+
+ color: colors.text_pale,
+ display: 'block',
+ marginBottom: 15
+ }
+}))(RecordForm)
diff --git a/ui/src/components/internal/forms/TeamForm.js b/ui/src/components/internal/forms/TeamForm.js
new file mode 100644
index 0000000..f528831
--- /dev/null
+++ b/ui/src/components/internal/forms/TeamForm.js
@@ -0,0 +1,50 @@
+import React from 'react'
+import { Field, Form } from 'react-final-form'
+
+import AppContext from 'components/AppContext'
+import DialogFormFooter from 'components/internal/DialogFormFooter'
+import FilledButton from 'components/buttons/FilledButton'
+import HintBox from 'components/internal/HintBox'
+import TextInput from 'components/inputs/TextInput'
+import { DialogDescription, Hint } from 'components/internal/typography'
+import { Team } from 'models'
+
+function TeamForm({ onSubmit }) {
+ return (
+
+ {({ currentUser }) => (
+
+ )}
+ />
+ )}
+
+ )
+}
+
+export default TeamForm
diff --git a/ui/src/components/internal/forms/TeamMemberForm.js b/ui/src/components/internal/forms/TeamMemberForm.js
new file mode 100644
index 0000000..7e1cc87
--- /dev/null
+++ b/ui/src/components/internal/forms/TeamMemberForm.js
@@ -0,0 +1,148 @@
+import classNames from 'classnames'
+import injectSheet from 'react-jss'
+import React, { Component, Fragment } from 'react'
+import { Field, Form } from 'react-final-form'
+
+import ButtonGroupInput from 'components/internal/inputs/ButtonGroupInput'
+import DialogFormFooter from 'components/internal/DialogFormFooter'
+import FilledButton from 'components/buttons/FilledButton'
+import FontIcon from 'components/FontIcon'
+import TextInput from 'components/inputs/TextInput'
+import { TeamMembership } from 'models'
+
+class TeamMemberForm extends Component {
+ constructor() {
+ super()
+
+ this.state = {
+ hoveredRole: null
+ }
+ }
+
+ handleMouseOver = role => this.setState({ hoveredRole: role })
+
+ handleMouseOut = () => this.setState({ hoveredRole: null })
+
+ renderRoleHints = ({ role }) => {
+ const { classes } = this.props
+ const { hoveredRole } = this.state
+
+ const options = TeamMembership.roleList
+
+ return (
+
+ {options.map(({ hint, value }) => {
+ if (!hint) {
+ return null
+ }
+
+ const isHovered = value === hoveredRole
+ const isSelected = value === role
+
+ return (
+
+
+ {hint}
+
+ )
+ })}
+
+ )
+ }
+
+ renderFormFields = () => {
+ const {
+ initialValues: { id }
+ } = this.props
+
+ const options = TeamMembership.roleList
+
+ if (id) {
+ return (
+
+
+
+
+
+ )
+ }
+
+ return (
+
+
+
+
+
+ )
+ }
+
+ render() {
+ const { initialValues, ...other } = this.props
+
+ const validateFunction = initialValues.id
+ ? TeamMembership.validateUpdate
+ : TeamMembership.validateCreate
+
+ return (
+
+ )}
+ {...other}
+ />
+ )
+ }
+}
+
+export default injectSheet(({ colors, typography, units }) => ({
+ roleHints: {
+ marginTop: units.roleHintsMarginTop,
+ marginBottom: units.roleHintsMarginBottom
+ },
+ roleHint: {
+ ...typography.lightSmall,
+
+ color: colors.text_pale,
+ display: 'none',
+ minHeight: units.roleHintMinHeight,
+ paddingLeft: units.roleHintPaddingLeft,
+ position: 'relative',
+
+ '& .icon': {
+ lineHeight: `${typography.lightSmall.lineHeight * typography.lightSmall.fontSize}px`,
+ position: 'absolute',
+ left: 0
+ }
+ },
+ roleHint_active: {
+ display: 'block'
+ }
+}))(TeamMemberForm)
diff --git a/ui/src/components/internal/forms/UpdateProfileForm.js b/ui/src/components/internal/forms/UpdateProfileForm.js
new file mode 100644
index 0000000..64709e4
--- /dev/null
+++ b/ui/src/components/internal/forms/UpdateProfileForm.js
@@ -0,0 +1,40 @@
+import React from 'react'
+import { Form, Field } from 'react-final-form'
+
+import FilledButton from 'components/buttons/FilledButton'
+import ItemBar from 'components/ItemBar'
+import Spacer from 'components/Spacer'
+import TextInput from 'components/inputs/TextInput'
+import { User } from 'models'
+
+function UpdateProfileForm(props) {
+ return (
+
+ )}
+ {...props}
+ />
+ )
+}
+
+export default UpdateProfileForm
diff --git a/ui/src/components/internal/forms/UpdateProfilePictureForm.js b/ui/src/components/internal/forms/UpdateProfilePictureForm.js
new file mode 100644
index 0000000..012fe4f
--- /dev/null
+++ b/ui/src/components/internal/forms/UpdateProfilePictureForm.js
@@ -0,0 +1,63 @@
+import { withTheme } from 'react-jss'
+import PropTypes from 'prop-types'
+import React from 'react'
+import { Field, Form } from 'react-final-form'
+
+import Column from 'components/internal/Column'
+import FilledButton from 'components/buttons/FilledButton'
+import ImageDropInput from 'components/internal/inputs/ImageDropInput'
+import ItemBar from 'components/ItemBar'
+import ProfilePicture from 'components/internal/ProfilePicture'
+import Row from 'components/internal/Row'
+import Spacer from 'components/Spacer'
+import Text from 'components/internal/typography/Text'
+
+function UpdateProfilePictureForm(props) {
+ const { user, theme } = props
+ const { profilePictureSize_large: profilePictureSize } = theme.units
+
+ return (
+
+ )}
+ {...props}
+ />
+ )
+}
+
+UpdateProfilePictureForm.propTypes = {
+ user: PropTypes.object
+}
+
+UpdateProfilePictureForm.defaultProps = {
+ user: {}
+}
+
+export default withTheme(UpdateProfilePictureForm)
diff --git a/ui/src/components/internal/headerItems/ProjectHeaderItem.js b/ui/src/components/internal/headerItems/ProjectHeaderItem.js
new file mode 100644
index 0000000..c5cfb83
--- /dev/null
+++ b/ui/src/components/internal/headerItems/ProjectHeaderItem.js
@@ -0,0 +1,56 @@
+import PropTypes from 'prop-types'
+import React from 'react'
+
+import HeaderItem from 'components/internal/HeaderItem'
+import HeaderItemContent from 'components/internal/HeaderItemContent'
+import { HeaderItemText, HeaderItemTitle } from 'components/internal/typography'
+import { matchPath } from 'react-router-dom'
+
+function ProjectHeaderItem({ projects, onAddClick, ...other }) {
+ if (!projects) {
+ return null
+ }
+
+ const isProjectPage = matchPath(document.location.pathname, { path: '/teams/:teamId/projects/:projectId' })
+ const isProjectsPage = matchPath(document.location.pathname, { path: '/teams/:teamId/projects', exact: true })
+
+ let text = null
+ let icon = 'arrow-down'
+ let onItemClick = null
+
+ if (projects.length === 0) {
+ text = 'New project'
+ icon = 'round-plus'
+ onItemClick = onAddClick
+ } else if (isProjectsPage) {
+ text = 'All projects'
+ } else if (isProjectPage) {
+ const currentProject = projects.find(project => project.id === isProjectPage.params.projectId)
+ text = currentProject && currentProject.name
+ } else {
+ text = 'Select a project'
+ }
+
+ return (
+
+
+ PROJECT
+
+
+
+ {text}
+
+
+
+ )
+}
+
+ProjectHeaderItem.propTypes = {
+ projects: PropTypes.array
+}
+
+ProjectHeaderItem.defaultProps = {
+ projects: null
+}
+
+export default ProjectHeaderItem
diff --git a/ui/src/components/internal/headerItems/TeamHeaderItem.js b/ui/src/components/internal/headerItems/TeamHeaderItem.js
new file mode 100644
index 0000000..7c70ab9
--- /dev/null
+++ b/ui/src/components/internal/headerItems/TeamHeaderItem.js
@@ -0,0 +1,55 @@
+import PropTypes from 'prop-types'
+import React from 'react'
+
+import HeaderItem from 'components/internal/HeaderItem'
+import HeaderItemContent from 'components/internal/HeaderItemContent'
+import { HeaderItemText, HeaderItemTitle } from 'components/internal/typography'
+import { matchPath } from 'react-router-dom'
+
+function TeamHeaderItem({ teams, onAddClick, ...other }) {
+ if (!teams) {
+ return null
+ }
+
+ const matchTeamPage = matchPath(document.location.pathname, { path: '/teams/:teamId' })
+ const matchTeamsPage = matchPath(document.location.pathname, { path: '/user/teams', exact: true })
+
+ let text = null
+ let icon = 'arrow-down'
+ let onItemClick = null
+
+ if (teams.length === 0) {
+ text = 'New team'
+ icon = 'round-plus'
+ onItemClick = onAddClick
+ } else if (matchTeamsPage) {
+ text = 'All teams'
+ } else if (matchTeamPage) {
+ text = teams.find(team => team.id === matchTeamPage.params.teamId).name
+ } else {
+ text = 'Select a team'
+ }
+
+ return (
+
+
+ TEAM
+
+
+
+ {text}
+
+
+
+ )
+}
+
+TeamHeaderItem.propTypes = {
+ teams: PropTypes.array
+}
+
+TeamHeaderItem.defaultProps = {
+ teams: null
+}
+
+export default TeamHeaderItem
diff --git a/ui/src/components/internal/headerItems/UserHeaderItem.js b/ui/src/components/internal/headerItems/UserHeaderItem.js
new file mode 100644
index 0000000..ddcdc34
--- /dev/null
+++ b/ui/src/components/internal/headerItems/UserHeaderItem.js
@@ -0,0 +1,56 @@
+import classNames from 'classnames'
+import injectSheet from 'react-jss'
+import PropTypes from 'prop-types'
+import React from 'react'
+import { Route } from 'react-router-dom'
+
+import * as mixins from 'styles/mixins'
+import AppContext from 'components/AppContext'
+import cleanProps from 'lib/cleanProps'
+import ProfilePicture from 'components/internal/ProfilePicture'
+
+function UserHeaderItem({ isActive, classes, ...other }) {
+ return (
+
+ {({ match }) => (
+
+ {({ currentUser }) => (
+
+ )}
+
+ )}
+
+ )
+}
+
+UserHeaderItem.propTypes = {
+ isActive: PropTypes.bool
+}
+
+UserHeaderItem.defaultProps = {
+ isActive: false
+}
+
+export default injectSheet(({ colors, units }) => ({
+ userHeaderItem: {
+ ...mixins.transitionSimple(),
+
+ cursor: 'pointer',
+ padding: (units.internalHeaderHeight - units.profilePictureSize_small) / 2,
+
+ '&:hover': {
+ backgroundColor: colors.headerItemBackground_hover
+ }
+ },
+ userHeaderItem_active: {
+ backgroundColor: colors.headerItemBackground_hover
+ }
+}))(UserHeaderItem)
diff --git a/ui/src/components/internal/imageTile/ImageTile.js b/ui/src/components/internal/imageTile/ImageTile.js
new file mode 100644
index 0000000..d7ab5c5
--- /dev/null
+++ b/ui/src/components/internal/imageTile/ImageTile.js
@@ -0,0 +1,68 @@
+import injectSheet from 'react-jss'
+import PropTypes from 'prop-types'
+import React from 'react'
+
+import * as mixins from 'styles/mixins'
+
+function ImageTile({ src, alt, onClick, classes }) {
+ const clickProps = {}
+
+ if (onClick) {
+ clickProps.onClick = onClick
+ clickProps.role = 'button'
+ clickProps.tabIndex = 0
+ }
+
+ return (
+
+
+
+
+
+ )
+}
+
+ImageTile.propTypes = {
+ src: PropTypes.string.isRequired,
+ alt: PropTypes.string.isRequired,
+ onClick: PropTypes.func
+}
+
+ImageTile.defaultProps = {
+ onClick: null
+}
+
+export default injectSheet(({ colors, units, shadows }) => ({
+ imageTile: {
+ ...mixins.transitionSimple(),
+
+ backgroundColor: colors.imageTileBackground,
+ borderRadius: units.imageTileBorderRadius,
+ boxShadow: shadows.imageTile,
+ cursor: props => props.onClick && 'pointer',
+ overflow: 'hidden',
+ paddingTop: '100%', // Trick to maintain aspect ratio
+ position: 'relative',
+ width: '100%',
+
+ '&:hover': {
+ boxShadow: props => props.onClick && shadows.imageTile_hover
+ }
+ },
+ imageHolder: {
+ alignItems: 'center',
+ display: 'flex',
+ justifyContent: 'center',
+ padding: units.imageTileInnerPadding,
+ position: 'absolute',
+ top: 0,
+ right: 0,
+ bottom: 0,
+ left: 0,
+
+ '& img': {
+ maxHeight: '100%',
+ maxWidth: '100%'
+ }
+ }
+}))(ImageTile)
diff --git a/ui/src/components/internal/imageTile/ImageTiles.js b/ui/src/components/internal/imageTile/ImageTiles.js
new file mode 100644
index 0000000..7eb6062
--- /dev/null
+++ b/ui/src/components/internal/imageTile/ImageTiles.js
@@ -0,0 +1,23 @@
+import injectSheet from 'react-jss'
+import React from 'react'
+
+const spacing = 30
+
+function ImageTiles({ children, classes }) {
+ return (
+
+ {children}
+
+ )
+}
+
+export default injectSheet(() => ({
+ imageTiles: {
+ display: 'grid',
+ gridTemplateColumns: '1fr 1fr',
+ gridTemplateRows: 'auto',
+ gridGap: `${spacing}px ${spacing}px`,
+ marginBottom: spacing,
+ marginTop: spacing
+ }
+}))(ImageTiles)
diff --git a/ui/src/components/internal/imageTile/index.js b/ui/src/components/internal/imageTile/index.js
new file mode 100644
index 0000000..c824276
--- /dev/null
+++ b/ui/src/components/internal/imageTile/index.js
@@ -0,0 +1,2 @@
+export { default as ImageTile } from './ImageTile'
+export { default as ImageTiles } from './ImageTiles'
diff --git a/ui/src/components/internal/inputs/BaseSelectInput.js b/ui/src/components/internal/inputs/BaseSelectInput.js
new file mode 100644
index 0000000..d2fdb04
--- /dev/null
+++ b/ui/src/components/internal/inputs/BaseSelectInput.js
@@ -0,0 +1,821 @@
+import _ from 'lodash'
+import classNames from 'classnames'
+import ClickOutside from 'react-click-outside'
+import injectSheet from 'react-jss'
+import PropTypes from 'prop-types'
+import React, { Component } from 'react'
+import ResizeAware from 'react-resize-aware'
+import { Manager, Reference, Popper } from 'react-popper'
+
+import * as mixins from 'styles/mixins'
+import toString from 'lib/toString'
+
+const defaultInitialValue = ''
+const comparisionRules = Object.freeze({
+ PARTIAL: 'PARTIAL',
+ FULL: 'FULL'
+})
+
+const toArray = (value) => {
+ if (Array.isArray(value)) {
+ return value.filter(Boolean)
+ }
+
+ if (typeof value === 'object' && value !== null) {
+ return [ value ]
+ }
+
+ return []
+}
+
+/**
+ * Aria attributes based on https://www.w3.org/TR/wai-aria-practices/examples/listbox/listbox-collapsible.html
+ */
+class BaseSelectInput extends Component {
+ constructor(props) {
+ super(props)
+
+ this.optionElMapping = {}
+
+ this.inputWrapperNode = null
+ this.inputNode = null
+
+ /**
+ * Instance property used to allow different scroll behaviors.
+ *
+ * -> `ArrowUp` and `ArrowDown`: Should trigger `scrollIntoView`
+ * -> `onMouseMove`: Should not trigger `scrollIntoView`
+ */
+ this.scrollToFocusedOptionOnUpdate = false
+
+ const { input: { value } } = props
+
+ const options = toArray(props.options)
+ const selectedOptions = options.filter(option => toString(value).includes(option.value))
+
+ this.state = {
+ options,
+ selectedOptions,
+ /**
+ * `focusedValue` is value of the option focused in the dropdown
+ */
+ focusedValue: defaultInitialValue,
+ /**
+ * `focusedSelectedValue` is the value of the option focused in the input
+ */
+ focusedSelectedValue: defaultInitialValue,
+ isDropdownOpen: false,
+ /**
+ * `isFocused` determines if the inputWrapper is in focus
+ */
+ isFocused: false,
+ typedValue: defaultInitialValue
+ }
+ }
+
+ /**
+ * `getDerivedStateFromProps` is used to handle cases where options are loaded via network calls.
+ */
+ static getDerivedStateFromProps(props, state) {
+ const { options, ...other } = state
+
+ const propOptions = toArray(props.options)
+
+ if (_.difference(options, propOptions).length >= 0) {
+ return {
+ options: propOptions,
+ ...other
+ }
+ }
+
+ // Return null to indicate no change to state.
+ return null
+ }
+
+ componentDidMount() {
+ const { autoFocus } = this.props
+
+ if (autoFocus) {
+ this.focusInputWrapper()
+ }
+ }
+
+ componentDidUpdate() {
+ if (this.hasFilteredOptions() && this.scrollToFocusedOptionOnUpdate) {
+ this.scrollIntoView(this.getFocusedOption())
+ }
+
+ this.scrollToFocusedOptionOnUpdate = false
+ }
+
+ compareOption = (typedValue, option, rule = comparisionRules.PARTIAL) => {
+ const stringToMatch = typedValue.toLowerCase()
+
+ if (!comparisionRules[rule]) {
+ throw new Error(`${rule} is not a valid rule for compareOption`)
+ }
+
+ if (rule === comparisionRules.PARTIAL) {
+ return toString(option.value).toLowerCase().includes(stringToMatch)
+ || toString(option.label).toLowerCase().includes(stringToMatch)
+ }
+
+ return toString(option.value).toLowerCase() === stringToMatch
+ || toString(option.label).toLowerCase() === stringToMatch
+ }
+
+ getNewOptionData = (typedValue, optionLabel) => ({
+ label: optionLabel,
+ value: typedValue,
+ isNew: true
+ })
+
+ isValidNewOption = (typedValue) => {
+ const { options, selectedOptions } = this.state
+
+ return !(
+ !typedValue
+ || selectedOptions.some(o => this.compareOption(typedValue, o, comparisionRules.FULL))
+ || options.some(option => this.compareOption(typedValue, option, comparisionRules.FULL))
+ )
+ }
+
+ cleanOptions = options => options.filter(option => !option.isNew)
+
+ handleInputBlur = () => {
+ this.closeDropdown()
+ this.setState({ isFocused: false })
+ }
+
+ handleInputChange = (e) => {
+ const { creatable, formatCreateLabel } = this.props
+ const { options } = this.state
+
+ const typedValue = e.target.value
+
+ let newOption
+ let newOptions = options
+
+ if (creatable) {
+ newOptions = this.cleanOptions(options)
+
+ if (typedValue) {
+ if (this.isValidNewOption(typedValue)) {
+ newOption = this.getNewOptionData(typedValue, formatCreateLabel(typedValue))
+ newOptions = [ ...newOptions, newOption ]
+ }
+ }
+ }
+
+ this.setState({
+ options: newOptions, typedValue
+ }, () => this.openDropdown('first'))
+ }
+
+ handleInputFocus = () => {
+ const { input, openMenuOnFocus } = this.props
+
+ if (openMenuOnFocus) {
+ this.openDropdown('first')
+ }
+
+ if (input.onFocus) { // Call to Final Form
+ input.onFocus()
+ }
+
+ this.setState({ isFocused: true })
+ }
+
+ handleInputKeyDown = (e) => {
+ const { isMultiple } = this.props
+ const { focusedSelectedValue, isDropdownOpen, typedValue } = this.state
+
+ const focusedOption = this.getFocusedOption()
+ const focusedSelectedOption = this.getFocusedSelectedOption()
+
+ switch (e.key) {
+ case 'Escape':
+ e.preventDefault()
+ e.stopPropagation()
+ if (isDropdownOpen) {
+ this.closeDropdown()
+ }
+ break
+ case ' ': // space
+ if (typedValue) {
+ return
+ }
+ // falls through
+ case 'Enter':
+ e.preventDefault()
+ if (isDropdownOpen) {
+ this.selectOption(focusedOption)
+ } else {
+ this.openDropdown('first')
+ }
+ break
+ case 'ArrowLeft':
+ if (!isMultiple || typedValue) {
+ return
+ }
+ this.focusValue('previous')
+ break
+ case 'ArrowRight':
+ if (!isMultiple || typedValue) {
+ return
+ }
+ this.focusValue('next')
+ break
+ case 'Delete':
+ case 'Backspace':
+ if (typedValue) {
+ return
+ }
+ if (focusedSelectedValue) {
+ this.removeValue(focusedSelectedOption)
+ } else {
+ this.popValue()
+ }
+ break
+ case 'ArrowUp':
+ e.preventDefault()
+ if (isDropdownOpen) {
+ this.focusOption('up')
+ } else {
+ this.openDropdown('last')
+ }
+ break
+ case 'ArrowDown':
+ e.preventDefault()
+ if (isDropdownOpen) {
+ this.focusOption('down')
+ } else {
+ this.openDropdown('first')
+ }
+ break
+ default:
+ }
+ }
+
+ handleOptionHover = (option) => {
+ if (!option) {
+ return
+ }
+
+ this.setState({ focusedValue: option.value })
+ }
+
+ popValue = () => {
+ const { input: { value, onChange } } = this.props
+ const { selectedOptions } = this.state
+
+ const inputValue = toArray(value)
+ if (inputValue.length === 0) {
+ return
+ }
+
+ onChange(inputValue.slice(0, inputValue.length - 1))
+ this.setState({ selectedOptions: selectedOptions.slice(0, selectedOptions.length - 1) })
+ }
+
+ selectOption = (option) => {
+ if (!option) {
+ return
+ }
+
+ const { input: { value, onChange }, isMultiple, onSelectOption } = this.props
+ const { selectedOptions } = this.state
+
+ let selectedOption = option
+ if (option.isNew) {
+ // clean selectedOption's value before passing it to input
+ selectedOption = this.getNewOptionData(option.value, option.value)
+ }
+
+ onSelectOption(selectedOption)
+
+ let inputValue
+ if (isMultiple) {
+ inputValue = toArray(value)
+ inputValue.push(selectedOption.value)
+ selectedOptions.push(selectedOption)
+ } else {
+ inputValue = selectedOption.value
+ }
+
+ onChange(inputValue)
+
+ this.setState({ selectedOptions })
+
+ this.focusInputWrapper()
+ this.closeDropdown()
+ }
+
+ openDropdown = (focusOption) => {
+ const { isMultiple } = this.props
+ const { options, selectedOptions } = this.state
+
+ const filteredOptions = this.getFilteredOptions(options, selectedOptions)
+ let openAtIndex = focusOption === 'first' ? 0 : filteredOptions.length - 1
+ let selectedIndex
+
+ if (isMultiple) {
+ selectedIndex = filteredOptions.indexOf(selectedOptions[0])
+ } else {
+ const selectedOption = this.getSelectedOption(options)
+ selectedIndex = filteredOptions.indexOf(selectedOption)
+ }
+
+ if (selectedIndex > -1) {
+ openAtIndex = selectedIndex
+ }
+
+ const focusedValue = this.hasFilteredOptions()
+ ? filteredOptions[openAtIndex].value
+ : defaultInitialValue
+
+ this.scrollToFocusedOptionOnUpdate = true
+
+ this.setState({
+ isDropdownOpen: true,
+ focusedValue
+ })
+ }
+
+ closeDropdown = () => {
+ const { options } = this.state
+
+ this.setState({
+ options: this.cleanOptions(options),
+ focusedValue: defaultInitialValue,
+ isDropdownOpen: false,
+ typedValue: defaultInitialValue
+ })
+ }
+
+ toggleDropdown = (e) => {
+ e.preventDefault()
+ e.stopPropagation()
+
+ const { isDropdownOpen, isFocused } = this.state
+
+ if (!isFocused) {
+ this.focusInputWrapper()
+ }
+
+ if (isDropdownOpen) {
+ this.closeDropdown()
+ } else {
+ this.openDropdown('first')
+ }
+ }
+
+ getOptionsId = () => {
+ const { input: { name } } = this.props
+
+ return `${name}-popup`
+ }
+
+ getOptionId = (option) => {
+ const { input: { name } } = this.props
+
+ return `${name}-option-${option.value}`
+ }
+
+ getFilteredOptions = (options, selectedOptions) => {
+ const { isMultiple } = this.props
+ const { typedValue } = this.state
+
+ let filteredOptions
+ if (isMultiple) {
+ // filteredOptions is a diff of `options` and `selectedOptions`
+ const selectedOptionsValues = selectedOptions.map(option => option.value)
+ filteredOptions = options.filter(option => (
+ !selectedOptionsValues.includes(toString(option.value))
+ ))
+ } else {
+ filteredOptions = options
+ }
+
+ return filteredOptions.filter(option => this.compareOption(typedValue, option))
+ }
+
+ getFocusedOption = () => {
+ const { options, focusedValue, selectedOptions } = this.state
+
+ const filteredOptions = this.getFilteredOptions(options, selectedOptions)
+ return filteredOptions.find(option => option.value === focusedValue)
+ }
+
+ getFocusedSelectedOption = () => {
+ const { focusedSelectedValue, selectedOptions } = this.state
+
+ return selectedOptions.find(option => option.value === focusedSelectedValue)
+ }
+
+ getSelectedOption = (options) => {
+ const { input: { value } } = this.props
+
+ return options.find(option => toString(option.value) === toString(value)) || {}
+ }
+
+ hasFilteredOptions = () => {
+ const { options, selectedOptions } = this.state
+
+ const filteredOptions = this.getFilteredOptions(options, selectedOptions)
+ return filteredOptions.length !== 0
+ }
+
+ scrollIntoView = (option) => {
+ const focusedEl = this.optionElMapping[option.value]
+ const dropdownEl = focusedEl.parentElement
+
+ const dropdownRect = dropdownEl.getBoundingClientRect()
+ const focusedRect = focusedEl.getBoundingClientRect()
+ const extraScroll = focusedEl.offsetHeight / 3
+
+ if (focusedRect.bottom + extraScroll > dropdownRect.bottom) {
+ dropdownEl.scrollTop = Math.min(
+ focusedEl.offsetTop + focusedEl.clientHeight + extraScroll - dropdownEl.offsetHeight,
+ dropdownEl.scrollHeight
+ )
+ } else if (focusedRect.top - extraScroll < dropdownRect.top) {
+ dropdownEl.scrollTop = Math.max(focusedEl.offsetTop - extraScroll, 0)
+ }
+ }
+
+ removeValue = (option) => {
+ const { input: { value, onChange }, isMultiple } = this.props
+ const { selectedOptions } = this.state
+
+ // Only MultiSelect supports removing value
+ if (!isMultiple || selectedOptions.length === 0) {
+ return
+ }
+
+ const newSelectedOptions = selectedOptions.filter(o => o.value !== option.value)
+ let inputValue = toArray(value)
+ inputValue = inputValue.filter(v => v !== option.value)
+
+ onChange(inputValue)
+
+ this.setState({
+ focusedSelectedValue: defaultInitialValue,
+ selectedOptions: newSelectedOptions
+ })
+ }
+
+ focusInputWrapper() {
+ if (!this.inputWrapperNode || !this.inputNode) {
+ return
+ }
+
+ this.inputWrapperNode.focus()
+ this.inputNode.focus()
+ this.setState({ isFocused: true })
+ }
+
+ focusOption(direction = 'first') {
+ if (!this.hasFilteredOptions()) {
+ return
+ }
+
+ const { options, selectedOptions } = this.state
+ const focusedOption = this.getFocusedOption()
+ const filteredOptions = this.getFilteredOptions(options, selectedOptions)
+
+ let nextFocusIndex = 0 // handles 'first'
+ nextFocusIndex = filteredOptions.indexOf(focusedOption)
+ if (!focusedOption) {
+ nextFocusIndex = -1
+ }
+
+ if (direction === 'up') {
+ nextFocusIndex = Math.max(nextFocusIndex - 1, 0)
+ } else if (direction === 'down') {
+ nextFocusIndex = Math.min(nextFocusIndex + 1, filteredOptions.length - 1)
+ } else if (direction === 'last') {
+ nextFocusIndex = filteredOptions.length - 1
+ }
+
+ this.scrollToFocusedOptionOnUpdate = true
+
+ this.setState({ focusedValue: filteredOptions[nextFocusIndex].value })
+ }
+
+ focusValue(direction = 'previous') {
+ const { isMultiple } = this.props
+ const { focusedSelectedValue, selectedOptions } = this.state
+
+ // Only MultiSelect supports value focusing
+ if (!isMultiple || selectedOptions.length === 0) {
+ return
+ }
+
+ // Reset focused value in dropdown
+ this.setState({ focusedValue: defaultInitialValue })
+
+ const selectedOptionsValues = selectedOptions.map(option => option.value)
+
+ const lastIndex = selectedOptionsValues.length - 1
+ let currentFocusIndex
+ let nextFocusIndex
+
+ if (!focusedSelectedValue) {
+ currentFocusIndex = -1
+ } else {
+ currentFocusIndex = selectedOptionsValues.indexOf(focusedSelectedValue)
+ }
+
+ if (direction === 'previous') {
+ // if nothing is focused, focus the last value first
+ if (currentFocusIndex === -1) {
+ nextFocusIndex = lastIndex
+ } else {
+ nextFocusIndex = Math.max(currentFocusIndex - 1, 0)
+ }
+ } else if (direction === 'next') {
+ nextFocusIndex = Math.min(currentFocusIndex + 1, selectedOptions.length - 1)
+ }
+
+ this.setState({ focusedSelectedValue: selectedOptions[nextFocusIndex].value })
+ }
+
+ renderInput = ({ ref }) => {
+ const {
+ disabled,
+ input: { name, value },
+ inputRenderer,
+ isMultiple,
+ placeholder,
+ searchable
+ } = this.props
+
+ const {
+ focusedSelectedValue,
+ isDropdownOpen,
+ isFocused,
+ options,
+ selectedOptions,
+ typedValue
+ } = this.state
+
+ const focusedOption = this.getFocusedOption()
+
+ let displayValue = defaultInitialValue
+ let inputValue = defaultInitialValue
+
+ if (typedValue && isDropdownOpen) {
+ inputValue = typedValue
+ }
+
+ if (isMultiple) {
+ if (selectedOptions.length > 0) {
+ displayValue = selectedOptions.map((selectedOption) => {
+ const option = Object.assign({}, selectedOption)
+
+ option.isFocused = option.value === focusedSelectedValue
+ option.onClick = (e) => {
+ e.preventDefault()
+ e.stopPropagation()
+
+ this.removeValue(option)
+ }
+
+ return option
+ })
+ } else if (!inputValue) {
+ displayValue = placeholder
+ }
+ } else {
+ const selectedOption = this.getSelectedOption(options)
+
+ if (!inputValue) {
+ if (selectedOption && selectedOption.value) {
+ displayValue = selectedOption.label
+ } else {
+ displayValue = placeholder
+ }
+ }
+ }
+
+ const inputWrapperProps = disabled ? {} : {
+ ref,
+ onMouseDown: this.toggleDropdown
+ }
+
+ // Props for tag
+ const inputProps = disabled ? {} : {
+ ref: (node) => { this.inputNode = node },
+ name,
+ readOnly: !searchable,
+ type: 'text',
+ autoCapitalize: 'none',
+ autoComplete: 'off',
+ 'aria-haspopup': 'listbox',
+ 'aria-controls': this.getOptionsId(),
+ 'aria-expanded': isDropdownOpen,
+ 'aria-activedescendant': focusedOption && this.getOptionId(focusedOption),
+ onBlur: this.handleInputBlur,
+ onChange: this.handleInputChange,
+ onFocus: this.handleInputFocus,
+ onKeyDown: this.handleInputKeyDown,
+ value: inputValue
+ }
+
+ // Props for inputRenderer
+ const displayProps = {
+ displayValue,
+ isDropdownOpen,
+ isFocused // To be used instead of `meta.active`
+ }
+
+ return inputRenderer({ value, inputProps, inputWrapperProps, displayProps })
+ }
+
+ renderOptions = () => {
+ const { classes, disabled } = this.props
+ const { options, selectedOptions } = this.state
+
+ const filteredOptions = this.getFilteredOptions(options, selectedOptions)
+ const optionsProps = disabled ? {} : {
+ id: this.getOptionsId(),
+ role: 'listbox',
+ tabIndex: -1,
+ style: { width: this.inputWrapperNode.offsetWidth }
+ }
+
+ return (
+
+ {filteredOptions.map(option => this.renderOption(option))}
+ {this.renderNoOptionsMessage()}
+
+ )
+ }
+
+ renderOption = (option) => {
+ const { input: { value }, optionRenderer } = this.props
+ const { focusedValue } = this.state
+
+ const isFocused = option.value === focusedValue
+ const isSelected = value === option.value
+
+ const optionProps = {
+ // Props for tag
+ id: this.getOptionId(option),
+ key: option.value,
+ ref: (el) => { this.optionElMapping[option.value] = el },
+ role: 'option',
+ 'aria-selected': isSelected,
+ onMouseDown: () => this.selectOption(option),
+ onMouseMove: () => this.handleOptionHover(option),
+
+ // Additional props to be consumed by optionRenderer
+ option,
+ isFocused,
+ isSelected
+ }
+
+ return optionRenderer(optionProps)
+ }
+
+ renderNoOptionsMessage = () => {
+ const { noOptionMessage, classes } = this.props
+
+ if (this.hasFilteredOptions()) {
+ return null
+ }
+
+ return (
+
+ {noOptionMessage}
+
+ )
+ }
+
+ render() {
+ const { input, wrapperClassName, wrapperOpenClassName, theme } = this.props
+ const { isDropdownOpen } = this.state
+
+ return (
+
+
+ { this.inputWrapperNode = node }}>
+ {referenceProps => this.renderInput(referenceProps)}
+
+
+ {isDropdownOpen && (
+
+ {({ ref, style, scheduleUpdate }) => (
+ /**
+ * Solution: `options` is wrapped by `` which consumes `ref` and `style`.
+ *
+ * Explanation:
+ * Popper.js uses GPU Accelaration for placement and generates the following CSS -
+ * ```
+ * will-change: transform;
+ * transform: translate3D(x, y, z);
+ * ```
+ *
+ * However, if `ref` and `style` is passed to `
`, the text becomes
+ * blurry. This effect can be seen here - http://jsfiddle.net/SfKKv/
+ *
+ * Alternate Solution:
+ * Disable GPU Acceleration using `computeStyle: { gpuAcceleration: false }`
+ * and `top` and `left` position. This also adds `will-change: top, left`
+ *
+ * Why alternate solution does not work:
+ * `will-change` results in the creation of a new compositor layer which is
+ * to be handled by the GPU . However, GPU does not support subpixel antialiasing
+ * as done by the CPU in most browsers, resulting again in blurry text.
+ * https://dev.opera.com/articles/css-will-change-property/
+ *
+ */
+
+
+ {this.renderOptions()}
+
+
+ )}
+
+ )}
+
+
+
+
+ )
+ }
+}
+
+BaseSelectInput.propTypes = {
+ autoFocus: PropTypes.bool,
+ creatable: PropTypes.bool,
+ disabled: PropTypes.bool,
+ formatCreateLabel: PropTypes.func,
+ inputRenderer: PropTypes.func.isRequired,
+ noOptionMessage: PropTypes.string,
+ onSelectOption: PropTypes.func,
+ openMenuOnFocus: PropTypes.bool,
+ options: PropTypes.arrayOf(PropTypes.shape({
+ label: PropTypes.any.isRequired,
+ value: PropTypes.any
+ })),
+ optionRenderer: PropTypes.func.isRequired,
+ placeholder: PropTypes.string,
+ searchable: PropTypes.bool,
+ wrapperClassName: PropTypes.string,
+ wrapperOpenClassName: PropTypes.string
+}
+
+BaseSelectInput.defaultProps = {
+ autoFocus: false,
+ creatable: false,
+ disabled: false,
+ formatCreateLabel: value => `Create "${value}"`,
+ noOptionMessage: 'No options',
+ onSelectOption: () => null,
+ openMenuOnFocus: false,
+ options: null,
+ placeholder: null,
+ searchable: true,
+ wrapperClassName: '',
+ wrapperOpenClassName: ''
+}
+
+export default injectSheet(({ colors, typography }) => ({
+ options: {
+ ...mixins.listless,
+
+ backgroundColor: '#fff',
+ borderRadius: 4,
+ boxShadow: '0px 13px 29px 0px rgba(10, 25, 39, 0.13)',
+ maxHeight: 210,
+ outline: 'none',
+ overflowY: 'auto',
+ paddingTop: 10,
+ paddingBottom: 10
+ },
+ option: {
+ ...mixins.transitionSimple(),
+ ...typography.regularSquished,
+
+ alignItems: 'center',
+ color: colors.text_dark,
+ cursor: 'default',
+ display: 'flex',
+ paddingTop: 10,
+ paddingRight: 30,
+ paddingBottom: 10,
+ paddingLeft: 30
+ }
+}))(BaseSelectInput)
diff --git a/ui/src/components/internal/inputs/ButtonGroupInput.js b/ui/src/components/internal/inputs/ButtonGroupInput.js
new file mode 100644
index 0000000..04ed964
--- /dev/null
+++ b/ui/src/components/internal/inputs/ButtonGroupInput.js
@@ -0,0 +1,156 @@
+import classNames from 'classnames'
+import injectSheet from 'react-jss'
+import PropTypes from 'prop-types'
+import React from 'react'
+
+import * as mixins from 'styles/mixins'
+
+function ButtonGroupInput({
+ className, input, inputLabel, options, handleMouseOut, handleMouseOver, size, classes
+}) {
+ if (!options || options.length === 0) {
+ return null
+ }
+
+ const renderInputLabel = () => {
+ if (!inputLabel) {
+ return null
+ }
+
+ return (
+
+ {inputLabel}
+
+ )
+ }
+
+ return (
+
+ {renderInputLabel()}
+
+ {options.map(({ label, value }) => (
+
input.onChange(value)}
+ onKeyPress={() => input.onChange(value)}
+ onMouseOut={handleMouseOut}
+ onBlur={handleMouseOut}
+ onMouseOver={() => handleMouseOver(value)}
+ onFocus={() => handleMouseOver(value)}
+ >
+ {label}
+
+ ))}
+
+
+
+ )
+}
+
+ButtonGroupInput.propTypes = {
+ className: PropTypes.string,
+ inputLabel: PropTypes.string,
+ options: PropTypes.arrayOf(PropTypes.shape({
+ label: PropTypes.oneOfType([ PropTypes.string, PropTypes.node ]).isRequired,
+ value: PropTypes.string.isRequired
+ })),
+ handleMouseOut: PropTypes.func,
+ handleMouseOver: PropTypes.func,
+ size: PropTypes.oneOf([ 'small', 'normal', 'large' ])
+}
+
+ButtonGroupInput.defaultProps = {
+ className: null,
+ inputLabel: null,
+ options: null,
+ handleMouseOut: () => null,
+ handleMouseOver: () => null,
+ size: 'normal'
+}
+
+export default injectSheet(({
+ colors, shadows, typography, units
+}) => ({
+ wrapper: {
+ alignItems: 'center',
+ display: 'inline-flex'
+ },
+ options: {
+ display: 'flex',
+ flex: '1 0 auto'
+ },
+ inputLabel: {
+ ...typography.regularSmallCompact,
+
+ color: colors.text_pale,
+ marginRight: units.buttonInputGroupLabelMarginRight,
+ textTransform: 'uppercase'
+ },
+ option: {
+ ...mixins.transitionSimple('border-color, box-shadow'),
+ ...typography.regularSmallSquished,
+
+ alignItems: 'center',
+ backgroundColor: colors.buttonGroupInputBackground,
+ borderColor: colors.buttonGroupInputBorder,
+ borderRightColor: colors.buttonGroupInputBorderRight,
+ borderStyle: 'solid',
+ borderWidth: units.buttonGroupInputBorderWidth,
+ color: colors.text_pale,
+ cursor: 'pointer',
+ display: 'flex',
+ flex: '1 1 0',
+ justifyContent: 'center',
+ whiteSpace: 'nowrap',
+
+ '&:first-of-type': {
+ borderTopLeftRadius: units.buttonGroupInputBorderRadius,
+ borderBottomLeftRadius: units.buttonGroupInputBorderRadius
+ },
+
+ '&:last-of-type': {
+ borderRightColor: colors.buttonGroupInputBorder,
+ borderTopRightRadius: units.buttonGroupInputBorderRadius,
+ borderBottomRightRadius: units.buttonGroupInputBorderRadius
+ },
+
+ '&:not(:first-of-type)': {
+ marginLeft: -1 * units.buttonGroupInputBorderWidth // Prevent borders from overlapping
+ },
+
+ '& .icon:not(:only-child)': {
+ paddingRight: units.buttonInputGroupIconMarginRight
+ }
+ },
+ option_small: {
+ height: units.buttonGroupInputHeight_small,
+ minWidth: units.buttonGroupInputMinWidth_small,
+ padding: units.buttonGroupInputPadding_small
+ },
+ option_normal: {
+ height: units.buttonGroupInputHeight_normal,
+ minWidth: units.buttonGroupInputMinWidth_normal,
+ padding: [
+ units.buttonGroupInputPaddingVertical_normal,
+ units.buttonGroupInputPaddingHorizontal_normal
+ ]
+ },
+ option_large: {
+ height: units.buttonGroupInputHeight_large,
+ minWidth: units.buttonGroupInputMinWidth_large,
+ padding: units.buttonGroupInputPadding_large
+ },
+ option_active: {
+ borderColor: `${colors.buttonGroupInputBorder_active} !important`, // `!important` overrides borderRightColor set for `option`
+ boxShadow: shadows.buttonGroupInput_active,
+ color: colors.text_dark,
+ zIndex: 1
+ }
+}))(ButtonGroupInput)
diff --git a/ui/src/components/internal/inputs/CheckboxInput.js b/ui/src/components/internal/inputs/CheckboxInput.js
new file mode 100644
index 0000000..a87cffa
--- /dev/null
+++ b/ui/src/components/internal/inputs/CheckboxInput.js
@@ -0,0 +1,86 @@
+import injectSheet from 'react-jss'
+import PropTypes from 'prop-types'
+import React from 'react'
+
+import withUniqueId from 'components/decorators/withUniqueId'
+
+function CheckboxInput({ input, label, uniqueId, classes }) {
+ return (
+
+
+ {label}
+
+ )
+}
+
+CheckboxInput.propTypes = {
+ label: PropTypes.string
+}
+
+CheckboxInput.defaultProps = {
+ label: ''
+}
+
+const CheckboxInputWithUniqueId = withUniqueId()(CheckboxInput)
+
+export default injectSheet(({ colors, units, typography }) => ({
+ checkboxWrapper: {
+ padding: [ units.checkboxVerticalPadding, units.checkboxHorizontalPadding ]
+ },
+ checkbox: {
+ opacity: 0,
+ position: 'absolute',
+
+ '& + label': {
+ ...typography.regularSquished,
+
+ cursor: 'pointer',
+ padding: 0,
+ position: 'relative'
+ },
+
+ // Box.
+ '& + label::before': {
+ background: 'white',
+ backgroundColor: colors.checkboxBackground,
+ borderColor: colors.checkboxBorder,
+ borderRadius: units.checkboxBorderRadius,
+ borderStyle: 'solid',
+ borderWidth: units.checkboxBorderWidth,
+ content: '" "',
+ display: 'inline-block',
+ height: units.checkboxSize,
+ marginRight: units.checkboxHorizontalMargin,
+ verticalAlign: 'text-top',
+ width: units.checkboxSize
+ },
+
+ // Box checked
+ '&:checked + label::before': {
+ backgroundColor: colors.checkboxBackground_checked,
+ borderColor: colors.checkboxBackground_checked,
+ color: colors.checkboxTick,
+ content: '"\ue030"', // From fontastic - `icon-tick`
+ fontFamily: 'claycms-icons',
+ fontSize: units.checkboxTickFontSize,
+ lineHeight: `${units.checkboxSize}px`,
+ textAlign: 'center'
+ },
+
+ // Disabled state label.
+ '&:disabled + label': {
+ color: colors.text_pale,
+ cursor: 'auto'
+ },
+
+ // Disabled box.
+ '&:disabled + label::before': {
+ boxShadow: 'none',
+ background: colors.text_pale
+ },
+
+ '&:checked + label': {
+ color: colors.checkboxBackground_checked
+ }
+ }
+}))(CheckboxInputWithUniqueId)
diff --git a/ui/src/components/internal/inputs/ColorPickerInput.js b/ui/src/components/internal/inputs/ColorPickerInput.js
new file mode 100644
index 0000000..46a7d40
--- /dev/null
+++ b/ui/src/components/internal/inputs/ColorPickerInput.js
@@ -0,0 +1,72 @@
+import injectSheet from 'react-jss'
+import React, { useState } from 'react'
+import { ChromePicker } from 'react-color'
+
+import ColorTile from 'components/internal/ColorTile'
+
+function ColorPickerInput({ classes = {}, input, label }) {
+ const [ showPicker, updateShowPicker ] = useState(false)
+ const name = (input && input.name) || 'color-picker'
+
+ const onTileClick = () => {
+ updateShowPicker(prevState => !prevState)
+ }
+
+ const onColorChange = (color) => {
+ input.onChange(color.hex)
+ }
+
+ return (
+
+
+ {label}
+
+
+ {input.name &&
}
+ {showPicker && (
+
+
updateShowPicker(false)}
+ onClick={() => updateShowPicker(false)}
+ className={classes.cover}
+ />
+
+
+ )}
+
+ )
+}
+
+export default injectSheet(({ colors, typography, units }) => ({
+ base: {
+ alignItems: 'center',
+ display: 'flex',
+ position: 'relative',
+ marginBottom: ({ spaced }) => spaced && 35
+ },
+ label: {
+ ...typography.regularSquished,
+
+ color: colors.text_pale,
+ top: units.inputPaddingTop
+ },
+ cover: {
+ position: 'fixed',
+ top: 0,
+ right: 0,
+ bottom: 0,
+ left: 0
+ },
+ popover: {
+ position: 'absolute',
+ zIndex: units.colorPickerInputPopoverZindex,
+ top: units.colorPickerInputPopoverTop,
+ left: units.colorPickerInputPopoverLeft
+ }
+}))(ColorPickerInput)
diff --git a/ui/src/components/internal/inputs/ImageDropInput.js b/ui/src/components/internal/inputs/ImageDropInput.js
new file mode 100644
index 0000000..e4e2bfa
--- /dev/null
+++ b/ui/src/components/internal/inputs/ImageDropInput.js
@@ -0,0 +1,264 @@
+import AvatarEditor from 'react-avatar-editor'
+import classNames from 'classnames'
+import Dropzone from 'react-dropzone'
+import injectSheet from 'react-jss'
+import PropTypes from 'prop-types'
+import React, { Component, createRef, Fragment } from 'react'
+
+import * as mixins from 'styles/mixins'
+import AutoSizer from 'react-virtualized-auto-sizer'
+import BaseSlider from 'components/BaseSlider'
+import FilledButton from 'components/buttons/FilledButton'
+import FontIcon from 'components/FontIcon'
+import ItemBar from 'components/ItemBar'
+import Spacer from 'components/Spacer'
+import Text from 'components/internal/typography/Text'
+import TextLink from 'components/internal/typography/TextLink'
+import { showAlertFailure } from 'client/methods'
+
+const sliderStep = 0.01
+const buttonStep = 0.1
+
+class ImageDropInput extends Component {
+ constructor(props) {
+ super(props)
+
+ const { defaultScale } = props
+
+ this.dropzone = createRef()
+ this.avatarEditor = null
+
+ this.state = {
+ image: null,
+ scale: defaultScale
+ }
+ }
+
+ setScale = (scale) => {
+ this.setState({ scale })
+ this.updateImage()
+ }
+
+ handleDrop = (acceptedFiles, rejectedFiles) => {
+ if (acceptedFiles.length > 0) {
+ this.setState({ image: acceptedFiles[0] })
+ return
+ }
+
+ if (rejectedFiles.length > 0) {
+ this.showFailureAlert()
+ }
+ }
+
+ showFailureAlert = () => {
+ showAlertFailure({
+ title: 'Yikes! Format not supported',
+ message: 'Please check the file that you are trying to upload.'
+ })
+ }
+
+ updateImage = () => {
+ const { input: { onChange } } = this.props
+
+ if (this.avatarEditor) {
+ const canvas = this.avatarEditor.getImage()
+ canvas.toBlob((blob) => {
+ onChange(new File([ blob ], 'image', { type: blob.type }))
+ })
+ }
+ }
+
+ increaseImageSize = () => {
+ const { scale } = this.state
+ const { maxScale } = this.props
+
+ this.setScale(Math.min(scale + buttonStep, maxScale))
+ }
+
+ decreaseImageSize = () => {
+ const { scale } = this.state
+ const { minScale } = this.props
+
+ this.setScale(Math.max(scale - buttonStep, minScale))
+ }
+
+ changeImage = () => this.dropzone.current.open()
+
+ renderSlider() {
+ const { defaultScale, maxScale, minScale, theme } = this.props
+ const { scale } = this.state
+
+ const sliderKnobStyle = {
+ backgroundColor: '#fff',
+ borderColor: theme.colors.neutral_light,
+ boxShadow: theme.shadows.sliderKnob
+ }
+
+ const sliderBarStyle = {
+ backgroundColor: theme.colors.neutral_light
+ }
+
+ return (
+
+ )
+ }
+
+ render() {
+ const { height, imageSize, classes } = this.props
+
+ const { image, scale } = this.state
+
+ return (
+
+
+ {({ getRootProps, getInputProps }) => (
+
+
+ {!image && (
+
+ Drag and drop picture here
+
+
+ or
+ {' '}
+ {/* eslint-disable-next-line */}
+
+ upload
+
+ {' '}
+ from disk.
+
+
+ )}
+
+ )}
+
+ {image && (
+
+
+ {({ width }) => {
+ const borderHorizontalWidth = (width - imageSize) / 2
+ const borderVerticalWidth = (height - imageSize) / 2
+ const editorWidth = width - (2 * borderHorizontalWidth)
+ const editorHeight = height - (2 * borderVerticalWidth)
+
+ return (
+ { this.avatarEditor = el }}
+ image={image}
+ width={editorWidth}
+ height={editorHeight}
+ border={[ borderHorizontalWidth, borderVerticalWidth ]}
+ borderRadius={100}
+ scale={scale}
+ onLoadSuccess={this.updateImage}
+ onLoadFailure={this.showFailureAlert}
+ onMouseUp={this.updateImage}
+ />
+ )
+ }}
+
+
+ )}
+
+ {image && (
+
+
+
+
+ {this.renderSlider()}
+
+
+
+
+
+
+ )}
+
+ )
+ }
+}
+
+ImageDropInput.propTypes = {
+ defaultScale: PropTypes.number,
+ height: PropTypes.number,
+ imageSize: PropTypes.number.isRequired,
+ maxScale: PropTypes.number,
+ minScale: PropTypes.number
+}
+
+ImageDropInput.defaultProps = {
+ defaultScale: 1.5,
+ height: 375,
+ maxScale: 5,
+ minScale: 1
+}
+
+export default injectSheet(({ colors }) => ({
+ avatarWrapper: {
+ borderRadius: 6,
+ cursor: 'move',
+ height: props => props.height,
+ overflow: 'hidden'
+ },
+ imageModifiers: {
+ alignItems: 'center',
+ color: colors.imageDropInputModifierIconColor,
+ display: 'flex',
+ flex: 1,
+
+ '& .icon': {
+ cursor: 'pointer'
+ }
+ },
+ dropzoneWrapper: {
+ ...mixins.transitionSimple(),
+
+ alignItems: 'center',
+ borderColor: colors.inputBorder,
+ borderStyle: 'solid',
+ borderWidth: 1,
+ cursor: 'pointer',
+ display: 'flex',
+ flexDirection: 'column',
+ height: props => props.height,
+ justifyContent: 'center',
+ textAlign: 'center',
+
+ ':hover': {
+ borderColor: colors.inputBorder_hover
+ }
+ },
+ dropzoneWrapper_empty: {
+ display: 'none'
+ },
+ uploadInstructions: {
+ textAlign: 'center'
+ }
+}))(ImageDropInput)
diff --git a/ui/src/components/internal/inputs/MultiSelectInput.js b/ui/src/components/internal/inputs/MultiSelectInput.js
new file mode 100644
index 0000000..5d4aa5d
--- /dev/null
+++ b/ui/src/components/internal/inputs/MultiSelectInput.js
@@ -0,0 +1,173 @@
+import classNames from 'classnames'
+import injectSheet from 'react-jss'
+import PropTypes from 'prop-types'
+import React from 'react'
+
+import * as mixins from 'styles/mixins'
+import BaseSelectInput from 'components/internal/inputs/BaseSelectInput'
+import FieldHint from 'components/FieldHint'
+import Tag from 'components/internal/Tag'
+
+function MultiSelectInput({ input, hint, label, classes, ...other }) {
+ const inputRenderer = ({
+ inputWrapperProps,
+ inputProps,
+ displayProps: { displayValue, isFocused }
+ }) => (
+
+ {label && (
+
0) || isFocused
+ }
+ )}
+ >
+ {label}
+
+ )}
+
+ {Array.isArray(displayValue)
+ ? displayValue.map(tag => (
+
+ {tag.label}
+
+ ))
+ :
{displayValue}
+ }
+
+
+
+
+ )
+
+ const optionRenderer = ({ isFocused, isSelected, option, ...optionProps }) => (
+
+ {option.label}
+
+ )
+
+ return (
+
+ )
+}
+
+MultiSelectInput.propTypes = {
+ hint: PropTypes.string,
+ label: PropTypes.string,
+ spaced: PropTypes.bool
+}
+
+MultiSelectInput.defaultProps = {
+ hint: null,
+ label: null,
+ spaced: false
+}
+
+export default injectSheet(({ colors, typography, units }) => ({
+ wrapper: {
+ marginBottom: ({ spaced }) => (spaced ? units.inputMargin_spaced : units.inputMargin)
+ },
+ inputWrapper: {
+ ...typography.regularSquished,
+
+ alignItems: 'center',
+ color: colors.text_pale,
+ display: 'flex',
+ flexWrap: 'wrap',
+ paddingTop: units.inputPaddingTop,
+ paddingBottom: units.inputPaddingBottom,
+ position: 'relative',
+
+ '&::after': {
+ ...mixins.transitionSimple(),
+
+ backgroundColor: colors.inputBorder,
+ content: '" "',
+ height: 1,
+ position: 'absolute',
+ right: 0,
+ bottom: 0,
+ left: 0
+ },
+
+ '&:focus-within': {
+ '&::after': {
+ backgroundColor: colors.inputBorder_hover
+ }
+ }
+ },
+ input: {
+ display: 'inline-block',
+ backgroundColor: 'transparent',
+ border: 'none',
+ color: colors.inputText,
+ paddingTop: 5,
+ paddingBottom: 5
+ },
+ label: {
+ ...mixins.transitionSimple(),
+ ...typography.regularSquished,
+
+ color: colors.text_pale,
+ pointerEvents: 'none',
+ position: 'absolute',
+ top: units.inputPaddingTop,
+ width: '100%'
+ },
+ label_shrink: {
+ ...typography.regularSmallSpaced,
+
+ pointerEvents: 'auto',
+ top: 0
+ },
+ displayValue: { // In case of placeholder being shown
+ position: 'absolute'
+ },
+ option: {
+ ...mixins.transitionSimple(),
+ ...typography.regularSquished,
+
+ alignItems: 'center',
+ color: colors.text_dark,
+ cursor: 'default',
+ display: 'flex',
+ paddingTop: 10,
+ paddingRight: 30,
+ paddingBottom: 10,
+ paddingLeft: 30
+ },
+ option_selected: {
+ color: colors.text_primary
+ },
+ option_focused: {
+ backgroundColor: '#fcfcff',
+ color: colors.text_primary
+ }
+}))(MultiSelectInput)
diff --git a/ui/src/components/internal/inputs/RadioInput.js b/ui/src/components/internal/inputs/RadioInput.js
new file mode 100644
index 0000000..c0d3304
--- /dev/null
+++ b/ui/src/components/internal/inputs/RadioInput.js
@@ -0,0 +1,89 @@
+import injectSheet from 'react-jss'
+import PropTypes from 'prop-types'
+import React from 'react'
+
+import RadioInputOption from 'components/internal/typography/RadioInputOption'
+
+function RadioInput({
+ input, inputLabel, options, onChange, size, classes
+}) {
+ if (!options || options.length === 0) {
+ return null
+ }
+
+ const handleChange = (value) => {
+ if (onChange) {
+ onChange(value)
+ }
+
+ input.onChange(value)
+ }
+
+ const renderInputLabel = () => {
+ if (!inputLabel) {
+ return null
+ }
+
+ return (
+
+ {inputLabel}
+
+ )
+ }
+
+ return (
+
+ {renderInputLabel()}
+
+ {options.map(option => (
+
+ ))}
+
+
+
+ )
+}
+
+RadioInput.propTypes = {
+ inputLabel: PropTypes.string,
+ options: PropTypes.arrayOf(PropTypes.shape({
+ name: PropTypes.string,
+ description: PropTypes.string.isRequired,
+ value: PropTypes.oneOfType([ PropTypes.string, PropTypes.number ]).isRequired,
+ additionalFields: PropTypes.arrayOf(PropTypes.node)
+ })),
+ size: PropTypes.oneOf([ 'small', 'regular' ]),
+ spaced: PropTypes.bool
+}
+
+RadioInput.defaultProps = {
+ inputLabel: null,
+ options: null,
+ size: 'regular',
+ spaced: false
+}
+
+export default injectSheet(({ colors, typography, units }) => ({
+ wrapper: {
+ display: 'flex',
+ flexDirection: 'column',
+ marginBottom: ({ size, spaced }) => size !== 'small' && (spaced ? units.inputMargin_spaced : units.inputMargin),
+ paddingTop: ({ size }) => size !== 'small' && units.inputPaddingTop
+ },
+ options: {
+ display: 'flex',
+ flexDirection: 'column'
+ },
+ inputLabel: {
+ ...typography.regularSmallSpaced,
+
+ color: colors.text_pale,
+ marginBottom: units.radioInputLabelMarginBottom
+ }
+}))(RadioInput)
diff --git a/ui/src/components/internal/inputs/SingleSelectInput.js b/ui/src/components/internal/inputs/SingleSelectInput.js
new file mode 100644
index 0000000..1b53fbd
--- /dev/null
+++ b/ui/src/components/internal/inputs/SingleSelectInput.js
@@ -0,0 +1,203 @@
+import classNames from 'classnames'
+import injectSheet from 'react-jss'
+import PropTypes from 'prop-types'
+import React from 'react'
+
+import * as mixins from 'styles/mixins'
+import BaseSelectInput from 'components/internal/inputs/BaseSelectInput'
+import FontIcon from 'components/FontIcon'
+import FieldError from 'components/FieldError'
+import FieldHint from 'components/FieldHint'
+import TextInput from 'components/inputs/TextInput'
+
+function SingleSelectInput({
+ hint, disabled, input, label, classes, meta = {}, stretched, truncateText, ...other
+}) {
+ const inputRenderer = ({
+ inputWrapperProps,
+ inputProps,
+ displayProps: { displayValue, isFocused, isDropdownOpen }
+ }) => {
+ const arrowDirection = isDropdownOpen ? 'up' : 'down'
+ const error = TextInput.fieldError(meta)
+
+ return (
+
+
+
+ {label && (
+
+ {label}
+
+ )}
+
+ {displayValue && (
+
+ {displayValue}
+
+ )}
+
+
+
+
+
+
+ )
+ }
+
+ const optionRenderer = ({ isFocused, isSelected, option, ...optionProps }) => (
+
+ {option.label}
+
+ )
+
+ return (
+
+ )
+}
+
+SingleSelectInput.propTypes = {
+ disabled: PropTypes.bool,
+ label: PropTypes.string,
+ searchable: PropTypes.bool,
+ stretched: PropTypes.bool,
+ truncateText: PropTypes.bool
+}
+
+SingleSelectInput.defaultProps = {
+ disabled: false,
+ label: null,
+ searchable: false,
+ stretched: true,
+ truncateText: false
+}
+
+export default injectSheet(({ colors, typography, units }) => ({
+ wrapper: {
+ flex: ({ stretched }) => (stretched && 1),
+ marginBottom: ({ spaced }) => (spaced ? units.inputMargin_spaced : units.inputMargin)
+ },
+ inputWrapper: {
+ ...typography.regularSquished,
+
+ alignItems: 'center',
+ color: ({ disabled }) => (disabled ? colors.text_pale : colors.inputText),
+ cursor: ({ disabled }) => (disabled ? 'not-allowed' : 'default'),
+ display: 'flex',
+ flexWrap: 'wrap',
+ paddingTop: units.inputPaddingTop,
+ paddingBottom: units.inputPaddingBottom,
+ position: 'relative',
+
+ '&::after': {
+ ...mixins.transitionSimple(),
+
+ backgroundColor: colors.inputBorder,
+ content: '" "',
+ height: 1,
+ position: 'absolute',
+ right: 0,
+ bottom: 0,
+ left: 0
+ },
+
+ '&:focus-within': {
+ '&::after': {
+ backgroundColor: colors.inputBorder_hover,
+ marginRight: ({ stretched }) => (stretched ? units.inputBorderMarginHorizontal_focus : 0),
+ marginLeft: ({ stretched }) => (stretched ? units.inputBorderMarginHorizontal_focus : 0)
+ }
+ },
+
+ '& .icon': {
+ position: 'absolute',
+ right: 0
+ }
+ },
+ input: {
+ cursor: ({ disabled }) => (disabled ? 'not-allowed' : 'default'),
+ display: 'inline-block',
+ backgroundColor: 'transparent',
+ border: 'none'
+ },
+ label: {
+ ...mixins.transitionSimple(),
+ ...typography.regularSquished,
+
+ color: colors.text_pale,
+ pointerEvents: 'none',
+ position: 'absolute',
+ top: units.inputPaddingTop,
+ width: '100%'
+ },
+ label_shrink: {
+ ...typography.regularSmallSpaced,
+
+ pointerEvents: 'auto',
+ top: 0
+ },
+ displayValue: {
+ cursor: ({ disabled }) => (disabled ? 'not-allowed' : 'default'),
+ position: 'absolute'
+ },
+ option: {
+ ...mixins.transitionSimple(),
+ ...typography.regularSquished,
+
+ alignItems: 'center',
+ color: colors.text_dark,
+ cursor: 'default',
+ display: ({ truncateText }) => (truncateText ? 'block ' : 'flex'),
+ paddingTop: 10,
+ paddingRight: 30,
+ paddingBottom: 10,
+ paddingLeft: 30,
+ pointerEvents: ({ disabled }) => disabled && 'none'
+ },
+ option_selected: {
+ color: colors.text_primary
+ },
+ option_focused: {
+ backgroundColor: '#fcfcff',
+ color: colors.text_primary
+ },
+ truncate: {
+ overflow: 'hidden',
+ paddingRight: 20,
+ textOverflow: 'ellipsis',
+ whiteSpace: 'nowrap',
+ width: '100%'
+ }
+}))(SingleSelectInput)
diff --git a/ui/src/components/internal/inputs/SwitchInput.js b/ui/src/components/internal/inputs/SwitchInput.js
new file mode 100644
index 0000000..3a14244
--- /dev/null
+++ b/ui/src/components/internal/inputs/SwitchInput.js
@@ -0,0 +1,131 @@
+import classNames from 'classnames'
+import injectSheet from 'react-jss'
+import PropTypes from 'prop-types'
+import React from 'react'
+
+import * as mixins from 'styles/mixins'
+import withUniqueId from 'components/decorators/withUniqueId'
+
+function SwitchInput({ activeLabel, classes, disabled, input, label, uniqueId }) {
+ const name = (input && input.name) || 'switch'
+
+ const isActive = typeof input.value === 'boolean' ? input.value : input.value === 't'
+
+ const toggleValue = () => input.onChange(!isActive)
+
+ const handleKeyDown = (e) => {
+ if (e.key === ' ' || e.key === 'Enter') {
+ e.preventDefault()
+ toggleValue()
+ }
+ }
+
+ const renderLabel = () => {
+ if (!activeLabel && !label) {
+ return null
+ }
+
+ const labelText = (isActive && activeLabel) ? activeLabel : label
+
+ return labelText && (
+
+ {labelText}
+
+ )
+ }
+
+ return (
+
+ {renderLabel()}
+
+ {input.name && }
+
+
+ )
+}
+
+SwitchInput.propTypes = {
+ activeLabel: PropTypes.string,
+ disabled: PropTypes.bool,
+ label: PropTypes.string,
+ reverse: PropTypes.bool
+}
+
+SwitchInput.defaultProps = {
+ activeLabel: null,
+ disabled: false,
+ label: null,
+ reverse: false
+}
+
+SwitchInput = withUniqueId()(SwitchInput)
+
+export default injectSheet(({ colors, shadows, typography, units }) => ({
+ switchInputWrapper: {
+ alignItems: 'center',
+ display: 'flex',
+ marginTop: 10,
+ marginBottom: ({ spaced }) => (spaced ? units.inputMargin_spaced : units.inputMargin)
+ },
+ label: ({ reverse }) => ({
+ ...typography.regularSquished,
+
+ color: colors.switchInputLabel,
+ marginRight: reverse ? 0 : units.switchInputLabelHorizontalMargin,
+ marginLeft: reverse ? units.switchInputLabelHorizontalMargin : 0,
+ order: reverse ? 1 : 0
+ }),
+ switchInput: {
+ ...mixins.size(units.switchInputWidth, units.switchInputKnobSize),
+ ...mixins.transitionSimple(),
+
+ backgroundColor: colors.switchInputBackground,
+ borderColor: colors.switchInputBorder,
+ borderRadius: units.switchInputKnobSize / 2,
+ borderStyle: 'solid',
+ borderWidth: units.switchInputBorderWidth,
+ cursor: 'pointer',
+ display: 'inline-block',
+ position: 'relative',
+
+ '&:after': {
+ ...mixins.size(units.switchInputKnobSize),
+ ...mixins.transitionSimple(),
+
+ backgroundColor: colors.switchInputKnobBackground,
+ borderRadius: '50%',
+ boxShadow: shadows.switchInputKnob,
+ boxSizing: 'border-box',
+ content: "''",
+ display: 'block',
+ marginLeft: -units.switchInputBorderWidth,
+ position: 'absolute',
+ top: -units.switchInputBorderWidth,
+ left: 0
+ }
+ },
+ switchInput_active: {
+ backgroundColor: colors.switchInputBackground_active,
+
+ '&:after': {
+ backgroundColor: colors.switchInputKnobBackground_active,
+ marginLeft: -units.switchInputKnobSize + units.switchInputBorderWidth,
+ left: '100%'
+ }
+ },
+ switchInput_disabled: {
+ backgroundColor: colors.switchInputBackground_disabled,
+ pointerEvents: 'none'
+ }
+}))(SwitchInput)
diff --git a/ui/src/components/internal/menu/Menu.js b/ui/src/components/internal/menu/Menu.js
new file mode 100644
index 0000000..693576c
--- /dev/null
+++ b/ui/src/components/internal/menu/Menu.js
@@ -0,0 +1,48 @@
+import ClickOutside from 'react-click-outside'
+import injectSheet from 'react-jss'
+import PropTypes from 'prop-types'
+import React from 'react'
+
+import * as mixins from 'styles/mixins'
+
+function Menu({
+ closeMenu, children, classes
+}) {
+ return (
+
+ {children}
+
+ )
+}
+
+Menu.propTypes = {
+ isHidden: PropTypes.bool,
+ closeMenu: PropTypes.func.isRequired,
+ width: PropTypes.oneOfType([ PropTypes.string, PropTypes.number ]),
+ children: PropTypes.node
+}
+
+Menu.defaultProps = {
+ isHidden: false,
+ width: 'auto',
+ /*
+ *Menu Components render
even though the record is empty
+ to ensure that Popper.js can properly position the element and
+ there is no shift when a new record is added.
+ */
+ children: null
+}
+
+export default injectSheet(({ colors, shadows, units }) => ({
+ menu: ({ isHidden, width }) => ({
+ ...mixins.transitionSimple(),
+
+ backgroundColor: colors.menuBackground,
+ borderRadius: units.menuBorderRadius,
+ boxShadow: shadows.menu,
+ opacity: isHidden ? 0 : 1,
+ overflow: 'hidden',
+ pointerEvents: isHidden ? 'none' : 'auto',
+ width
+ })
+}))(Menu)
diff --git a/ui/src/components/internal/menu/MenuBody.js b/ui/src/components/internal/menu/MenuBody.js
new file mode 100644
index 0000000..af506c3
--- /dev/null
+++ b/ui/src/components/internal/menu/MenuBody.js
@@ -0,0 +1,34 @@
+import injectSheet from 'react-jss'
+import PropTypes from 'prop-types'
+import React from 'react'
+
+function MenuBody({
+ children, classes
+}) {
+ return (
+
+ {children}
+
+ )
+}
+
+MenuBody.propTypes = {
+ children: PropTypes.node
+}
+
+MenuBody.defaultProps = {
+ children: null
+}
+
+export default injectSheet(({ units }) => ({
+ menuBody: ({ children }) => {
+ const hasChildren = React.Children.toArray(children).length > 0
+
+ return {
+ maxHeight: units.menuBodyMaxHeight,
+ overflowY: 'auto',
+ paddingTop: hasChildren ? units.menuVerticalPadding : 0,
+ paddingBottom: hasChildren ? units.menuVerticalPadding : 0
+ }
+ }
+}))(MenuBody)
diff --git a/ui/src/components/internal/menu/MenuContainer.js b/ui/src/components/internal/menu/MenuContainer.js
new file mode 100644
index 0000000..29eaf71
--- /dev/null
+++ b/ui/src/components/internal/menu/MenuContainer.js
@@ -0,0 +1,97 @@
+import PropTypes from 'prop-types'
+import React, { PureComponent } from 'react'
+import { Manager, Reference, Popper } from 'react-popper'
+import { withTheme } from 'react-jss'
+
+class MenuContainer extends PureComponent {
+ constructor() {
+ super()
+
+ this.referenceNode = null
+
+ this.state = {
+ isActive: false
+ }
+ }
+
+ openMenu = () => this.setState({ isActive: true })
+
+ closeMenu = (e) => {
+ /*
+ Ensure closeMenu passed to ClickOutside doesn't interfere with toggleMenu
+ */
+ if (this.referenceNode.contains(e.target)) {
+ return
+ }
+
+ this.setState({ isActive: false })
+ }
+
+ toggleMenu = () => this.setState(prevState => ({ isActive: !prevState.isActive }))
+
+ render() {
+ const {
+ isOpen, placement, variation, horizontalOffset, verticalOffset, children, theme
+ } = this.props
+ const { isActive } = this.state
+
+ const childrenArray = React.Children.toArray(children)
+ if (childrenArray.length > 2) {
+ throw new Error('MenuContainer cannot have more than two children.')
+ }
+
+ const referenceElement = React.cloneElement(childrenArray[0], {
+ onClick: this.toggleMenu,
+ onMouseDown: (e) => { e.preventDefault() },
+ isActive: isActive || isOpen
+ })
+
+ const popperElement = React.cloneElement(childrenArray[1], {
+ closeMenu: this.closeMenu
+ })
+
+ return (
+
+ { this.referenceNode = node }}>
+ {({ ref }) => (
+
+ {referenceElement}
+
+ )}
+
+ {(isActive || isOpen) && (
+
+ {({ ref, style }) => (
+
+ {popperElement}
+
+ )}
+
+ )}
+
+ )
+ }
+}
+
+MenuContainer.propTypes = {
+ isOpen: PropTypes.bool,
+ placement: PropTypes.oneOf([ 'auto', 'top', 'right', 'bottom', 'left' ]),
+ variation: PropTypes.oneOf([ 'start', 'end' ]),
+ horizontalOffset: PropTypes.oneOfType([ PropTypes.string, PropTypes.number ]),
+ verticalOffset: PropTypes.oneOfType([ PropTypes.string, PropTypes.number ]),
+ children: PropTypes.node.isRequired
+}
+
+MenuContainer.defaultProps = {
+ isOpen: false,
+ placement: 'bottom',
+ variation: 'end',
+ horizontalOffset: 0,
+ verticalOffset: 0
+}
+
+export default withTheme(MenuContainer)
diff --git a/ui/src/components/internal/menu/MenuDivider.js b/ui/src/components/internal/menu/MenuDivider.js
new file mode 100644
index 0000000..76f2bc3
--- /dev/null
+++ b/ui/src/components/internal/menu/MenuDivider.js
@@ -0,0 +1,18 @@
+import injectSheet from 'react-jss'
+import React from 'react'
+
+import * as mixins from 'styles/mixins'
+
+function MenuDivider({ classes }) {
+ return
+}
+
+export default injectSheet(({ colors, units }) => ({
+ menuDivider: {
+ ...mixins.size('100%', 1),
+
+ backgroundColor: colors.menuDividerBackground,
+ marginTop: units.menuDividerVerticalMargin,
+ marginBottom: units.menuDividerVerticalMargin
+ }
+}))(MenuDivider)
diff --git a/ui/src/components/internal/menu/MenuFooter.js b/ui/src/components/internal/menu/MenuFooter.js
new file mode 100644
index 0000000..c93a539
--- /dev/null
+++ b/ui/src/components/internal/menu/MenuFooter.js
@@ -0,0 +1,26 @@
+import injectSheet from 'react-jss'
+import React from 'react'
+
+function MenuFooter({
+ children, classes
+}) {
+ return (
+
+ {children}
+
+ )
+}
+
+export default injectSheet(({ colors, typography, units }) => ({
+ menuFooter: {
+ ...typography.regularSmallSquished,
+
+ alignItems: 'center',
+ backgroundColor: colors.menuSectionBackground,
+ borderTopColor: colors.menuSectionBorder,
+ borderTopStyle: 'solid',
+ borderTopWidth: 1,
+ display: 'flex',
+ height: units.menuFooterHeight
+ }
+}))(MenuFooter)
diff --git a/ui/src/components/internal/menu/MenuHeading.js b/ui/src/components/internal/menu/MenuHeading.js
new file mode 100644
index 0000000..1241d96
--- /dev/null
+++ b/ui/src/components/internal/menu/MenuHeading.js
@@ -0,0 +1,19 @@
+import injectSheet from 'react-jss'
+import React from 'react'
+
+function MenuHeading({ children, classes }) {
+ return (
+
+ {children}
+
+ )
+}
+
+export default injectSheet(({ colors, typography, units }) => ({
+ menuHeading: {
+ ...typography.lightSmall,
+
+ color: colors.text_pale,
+ padding: [ units.menuHeadingHorizontalPadding, units.menuHeadingVerticalPadding ]
+ }
+}))(MenuHeading)
diff --git a/ui/src/components/internal/menu/MenuItem.js b/ui/src/components/internal/menu/MenuItem.js
new file mode 100644
index 0000000..7b6eca3
--- /dev/null
+++ b/ui/src/components/internal/menu/MenuItem.js
@@ -0,0 +1,57 @@
+import classNames from 'classnames'
+import injectSheet from 'react-jss'
+import PropTypes from 'prop-types'
+import React from 'react'
+
+import MenuLink from 'components/internal/menu/MenuLink'
+
+function MenuItem({
+ onClick, variant, children, classes
+}) {
+ return (
+
+ {children}
+
+ )
+}
+
+MenuItem.propTypes = {
+ onClick: PropTypes.func,
+ pale: PropTypes.bool,
+ variant: PropTypes.oneOf([ 'regular', 'small' ])
+}
+
+MenuItem.defaultProps = {
+ onClick: null,
+ pale: false,
+ variant: 'regular'
+}
+
+export default injectSheet(({ colors, typography, units }) => ({
+ menuItem: {
+ ...MenuLink.commonStyles({ colors, units }),
+
+ alignItems: 'center',
+ display: 'flex',
+
+ '& .icon': {
+ marginLeft: 10
+ },
+
+ '&:hover': {
+ color: ({ onClick }) => onClick && colors.text_primary
+ }
+ },
+ menuItem_regular: {
+ ...typography.regularSquished
+ },
+ menuItem_small: {
+ ...typography.regularSmallSpacedSquished
+ }
+}))(MenuItem)
diff --git a/ui/src/components/internal/menu/MenuLink.js b/ui/src/components/internal/menu/MenuLink.js
new file mode 100644
index 0000000..77b586b
--- /dev/null
+++ b/ui/src/components/internal/menu/MenuLink.js
@@ -0,0 +1,58 @@
+import injectSheet from 'react-jss'
+import PropTypes from 'prop-types'
+import React from 'react'
+import { NavLink } from 'react-router-dom'
+
+import * as mixins from 'styles/mixins'
+import cleanProps from 'lib/cleanProps'
+
+function MenuLink({
+ pale, classes, children, ...other
+}) {
+ return (
+
+ {children}
+
+ )
+}
+
+MenuLink.propTypes = {
+ pale: PropTypes.bool
+}
+
+MenuLink.defaultProps = {
+ pale: false
+}
+
+MenuLink.commonStyles = ({ colors, units }) => ({
+ ...mixins.transitionSimple(),
+
+ color: ({ pale }) => (pale ? colors.text_pale : colors.text_dark),
+ cursor: ({ to, onClick }) => (to || onClick) && 'pointer',
+ paddingTop: units.menuLinkVerticalPadding,
+ paddingRight: units.menuLinkHorizontalPadding,
+ paddingBottom: units.menuLinkVerticalPadding,
+ paddingLeft: units.menuLinkHorizontalPadding
+})
+
+export default injectSheet(({ colors, typography, units }) => ({
+ menuLink: {
+ ...MenuLink.commonStyles({ colors, units }),
+ ...typography.regularSquished,
+
+ display: 'block',
+
+ '&:hover': {
+ backgroundColor: colors.menuLink_hover,
+ color: colors.text_primary
+ }
+ },
+ menuLink_active: {
+ backgroundColor: colors.menuLink_hover,
+ color: ({ pale }) => (pale ? colors.text_pale : colors.text_primary)
+ }
+}))(MenuLink)
diff --git a/ui/src/components/internal/menu/MenuSearchHeader.js b/ui/src/components/internal/menu/MenuSearchHeader.js
new file mode 100644
index 0000000..0665eeb
--- /dev/null
+++ b/ui/src/components/internal/menu/MenuSearchHeader.js
@@ -0,0 +1,64 @@
+import injectSheet from 'react-jss'
+import PropTypes from 'prop-types'
+import React, { Component } from 'react'
+
+import FontIcon from 'components/FontIcon'
+import MenuLink from 'components/internal/menu/MenuLink'
+
+class MenuSearchHeader extends Component {
+ componentDidMount() {
+ this.triggerInputFocus()
+ }
+
+ triggerInputFocus() {
+ this.input.focus()
+ }
+
+ render() {
+ const { onChange, value, classes } = this.props
+
+ return (
+
+ { this.input = input }} onChange={e => onChange(e.target.value)} value={value} />
+ this.triggerInputFocus()} />
+
+ )
+ }
+}
+
+MenuSearchHeader.propTypes = {
+ onChange: PropTypes.func,
+ pale: PropTypes.bool
+}
+
+MenuSearchHeader.defaultProps = {
+ onChange: () => null,
+ pale: true
+}
+
+export default injectSheet(({ colors, typography, units }) => ({
+ menuSearchHeaderWrapper: {
+ ...MenuLink.commonStyles({ colors, units }), // Uses 'pale' prop
+ ...typography.regular,
+
+ alignItems: 'center',
+ backgroundColor: colors.menuSectionBackground,
+ borderBottomColor: colors.menuSectionBorder,
+ borderBottomStyle: 'solid',
+ borderBottomWidth: 1,
+ cursor: 'text',
+ display: 'flex',
+ height: units.menuSearchHeaderHeight,
+ width: '100%',
+
+ '& input': {
+ backgroundColor: 'transparent',
+ border: 'none',
+ width: '100%'
+ },
+
+ '&:hover': {
+ color: colors.text_primary
+ }
+ }
+}))(MenuSearchHeader)
diff --git a/ui/src/components/internal/menus/ProjectMenu.js b/ui/src/components/internal/menus/ProjectMenu.js
new file mode 100644
index 0000000..a4eeb7a
--- /dev/null
+++ b/ui/src/components/internal/menus/ProjectMenu.js
@@ -0,0 +1,99 @@
+import PropTypes from 'prop-types'
+import React, { Component } from 'react'
+import { withTheme } from 'react-jss'
+
+import FontIcon from 'components/FontIcon'
+import Menu from 'components/internal/menu/Menu'
+import MenuBody from 'components/internal/menu/MenuBody'
+import MenuFooter from 'components/internal/menu/MenuFooter'
+import MenuItem from 'components/internal/menu/MenuItem'
+import MenuSearchHeader from 'components/internal/menu/MenuSearchHeader'
+import MenuLink from 'components/internal/menu/MenuLink'
+
+const SEARCH_VISIBILITY_THRESHOLD = 5
+
+class ProjectMenu extends Component {
+ constructor() {
+ super()
+
+ this.state = { searchTerm: '' }
+ }
+
+ onClickCreateNewProject = (e) => {
+ const { closeMenu, openProjectDialog } = this.props
+
+ closeMenu(e)
+ openProjectDialog()
+ }
+
+ filterProjectsBySearchTerm() {
+ const { projects } = this.props
+ const { searchTerm } = this.state
+
+ if (!searchTerm) {
+ return null
+ }
+
+ return projects.filter(project => project.name.toLowerCase().includes(searchTerm.toLowerCase()))
+ }
+
+ render() {
+ const { isHidden, projects: allProjects, closeMenu, theme } = this.props
+ const { searchTerm } = this.state
+
+ const filteredProjects = this.filterProjectsBySearchTerm()
+ const projects = filteredProjects || allProjects
+
+ return (
+
+ {allProjects.length >= SEARCH_VISIBILITY_THRESHOLD && (
+ this.setState({ searchTerm: value })}
+ value={searchTerm}
+ />
+ )}
+ {projects.length > 0 && (
+
+
+ All projects
+
+ {projects.map(project => (
+
+ {project.name}
+
+ ))}
+
+ )}
+ {filteredProjects && filteredProjects.length === 0 && (
+
+ No projects found.
+
+ )}
+ {allProjects.length > 0 && (
+
+
+ Create New Project
+
+
+
+ )}
+
+ )
+ }
+}
+
+ProjectMenu.propTypes = {
+ openProjectDialog: PropTypes.func,
+ projects: PropTypes.array
+}
+
+ProjectMenu.defaultProps = {
+ openProjectDialog: () => null,
+ projects: null
+}
+
+export default withTheme(ProjectMenu)
diff --git a/ui/src/components/internal/menus/TeamMenu.js b/ui/src/components/internal/menus/TeamMenu.js
new file mode 100644
index 0000000..149bbd6
--- /dev/null
+++ b/ui/src/components/internal/menus/TeamMenu.js
@@ -0,0 +1,95 @@
+import PropTypes from 'prop-types'
+import React, { Component } from 'react'
+import { withTheme } from 'react-jss'
+
+import FontIcon from 'components/FontIcon'
+import Menu from 'components/internal/menu/Menu'
+import MenuBody from 'components/internal/menu/MenuBody'
+import MenuFooter from 'components/internal/menu/MenuFooter'
+import MenuItem from 'components/internal/menu/MenuItem'
+import MenuSearchHeader from 'components/internal/menu/MenuSearchHeader'
+import MenuLink from 'components/internal/menu/MenuLink'
+
+const SEARCH_VISIBILITY_THRESHOLD = 5
+
+class TeamMenu extends Component {
+ constructor() {
+ super()
+
+ this.state = { searchTerm: '' }
+ }
+
+ onClickCreateNewTeam = (e) => {
+ const { closeMenu, openTeamDialog } = this.props
+
+ closeMenu(e)
+ openTeamDialog()
+ }
+
+ filterTeamsBySearchTerm() {
+ const { teams } = this.props
+ const { searchTerm } = this.state
+
+ if (!searchTerm) {
+ return null
+ }
+
+ return teams.filter(team => team.name.toLowerCase().includes(searchTerm.toLowerCase()))
+ }
+
+ render() {
+ const { isHidden, teams: allTeams, closeMenu, theme } = this.props
+ const { searchTerm } = this.state
+
+ const filteredTeams = this.filterTeamsBySearchTerm()
+ const teams = filteredTeams || allTeams
+
+ return (
+
+ {allTeams.length >= SEARCH_VISIBILITY_THRESHOLD && (
+ this.setState({ searchTerm: value })}
+ value={searchTerm}
+ />
+ )}
+ {teams.length > 0 && (
+
+
+ All teams
+
+ {teams.map(team => (
+
+ {team.name}
+
+ ))}
+
+ )}
+ {filteredTeams && filteredTeams.length === 0 && (
+
+ No teams found.
+
+ )}
+ {allTeams.length > 0 && (
+
+
+ Create New Team
+
+
+
+ )}
+
+ )
+ }
+}
+
+TeamMenu.propTypes = {
+ openTeamDialog: PropTypes.func,
+ teams: PropTypes.array
+}
+
+TeamMenu.defaultProps = {
+ openTeamDialog: () => null,
+ teams: null
+}
+
+export default withTheme(TeamMenu)
diff --git a/ui/src/components/internal/menus/UserMenu.js b/ui/src/components/internal/menus/UserMenu.js
new file mode 100644
index 0000000..6885939
--- /dev/null
+++ b/ui/src/components/internal/menus/UserMenu.js
@@ -0,0 +1,45 @@
+import gql from 'graphql-tag'
+import React from 'react'
+
+import Menu from 'components/internal/menu/Menu'
+import MenuBody from 'components/internal/menu/MenuBody'
+import MenuFooter from 'components/internal/menu/MenuFooter'
+import MenuItem from 'components/internal/menu/MenuItem'
+import MenuLink from 'components/internal/menu/MenuLink'
+import { logout } from 'client/methods'
+import { withMutation } from 'lib/data'
+
+function UserMenu({ closeMenu, destroySession }) {
+ const handleLogout = () => destroySession().then(() => logout())
+
+ return (
+
+
+
+ Profile
+
+ {/*
+ Notifications
+ */}
+
+ Settings
+
+
+
+
+ Logout
+
+
+
+ )
+}
+
+UserMenu = withMutation(gql`
+ mutation LogoutMutation {
+ destroySession {
+ id
+ }
+ }
+`)(UserMenu)
+
+export default UserMenu
diff --git a/ui/src/components/internal/modals/PictureModal.js b/ui/src/components/internal/modals/PictureModal.js
new file mode 100644
index 0000000..e64e94a
--- /dev/null
+++ b/ui/src/components/internal/modals/PictureModal.js
@@ -0,0 +1,20 @@
+import React from 'react'
+
+import Modal from 'components/internal/Modal'
+import UpdateProfilePictureForm from 'components/internal/forms/UpdateProfilePictureForm'
+import Spacer from 'components/Spacer'
+import { DialogTitle } from 'components/internal/typography'
+
+function PictureModal(props) {
+ const title = 'Change Profile Picture'
+
+ return (
+
+ Change Profile Picture
+
+
+
+ )
+}
+
+export default PictureModal
diff --git a/ui/src/components/internal/modals/RecordModal.js b/ui/src/components/internal/modals/RecordModal.js
new file mode 100644
index 0000000..ff924b0
--- /dev/null
+++ b/ui/src/components/internal/modals/RecordModal.js
@@ -0,0 +1,26 @@
+import React from 'react'
+
+import Modal from 'components/internal/Modal'
+import RecordForm from 'components/internal/forms/RecordForm'
+import Spacer from 'components/Spacer'
+import { DialogTitle } from 'components/internal/typography'
+
+function RecordModal({ formValues, onFormSubmit, ...other }) {
+ const action = formValues.id ? 'Edit' : 'New'
+
+ const title = `${action} Record`
+
+ return (
+
+ {title}
+
+
+
+ )
+}
+
+export default RecordModal
diff --git a/ui/src/components/internal/pageToolbar/PageToolbar.js b/ui/src/components/internal/pageToolbar/PageToolbar.js
new file mode 100644
index 0000000..d171ccc
--- /dev/null
+++ b/ui/src/components/internal/pageToolbar/PageToolbar.js
@@ -0,0 +1,274 @@
+import _ from 'lodash'
+import injectSheet from 'react-jss'
+import PropTypes from 'prop-types'
+import React, { Component } from 'react'
+
+import IconButton from 'components/internal/buttons/IconButton'
+import MenuContainer from 'components/internal/menu/MenuContainer'
+import SortAndFilterMenu from 'components/internal/pageToolbar/SortAndFilterMenu'
+import Text from 'components/typography/Text'
+import TextInput from 'components/inputs/TextInput'
+
+class PageToolbar extends Component {
+ constructor() {
+ super()
+
+ this.state = {
+ appliedFilters: null,
+ appliedSorting: null,
+ searchTerm: ''
+ }
+ }
+
+ getDefaultFilters = () => {
+ const { filterOptions } = this.props
+
+ return (
+ filterOptions
+ && filterOptions.map(filterOption => (filterOption.type === 'multiple'
+ ? filterOption.items.filter(item => item.isDefault).map(item => item.value)
+ : filterOption.items.find(item => item.isDefault).value))
+ )
+ }
+
+ getDefaultSorting = () => {
+ const { sortOptions } = this.props
+
+ return sortOptions && sortOptions.findIndex(sortOption => sortOption.isDefault)
+ }
+
+ getAppliedFilterCount = () => {
+ const { appliedFilters, appliedSorting } = this.state
+
+ const defaultFilters = this.getDefaultFilters()
+ const defaultSorting = this.getDefaultSorting()
+
+ const appliedMultipleSelectFiltersCount = (filter, i) => {
+ if (filter.length !== defaultFilters[i].length) {
+ return 1
+ }
+
+ return filter.every(item => defaultFilters[i].includes(item)) ? 0 : 1
+ }
+
+ const appliedSingleSelectFilterCount = (filter, i) => (filter !== defaultFilters[i] ? 1 : 0)
+
+ const appliedFiltersCount = () => {
+ if (appliedFilters === null) {
+ return 0
+ }
+
+ return appliedFilters
+ .map((filter, i) => (Array.isArray(filter)
+ ? appliedMultipleSelectFiltersCount(filter, i)
+ : appliedSingleSelectFilterCount(filter, i)))
+ .reduce((acc, current) => acc + current, 0)
+ }
+
+ const appliedSortingCount = () => {
+ if (appliedSorting === null) {
+ return 0
+ }
+
+ return defaultSorting !== appliedSorting ? 1 : 0
+ }
+
+ return appliedFiltersCount() + appliedSortingCount()
+ }
+
+ handleSearchChange = (searchTerm) => {
+ this.setState({ searchTerm: searchTerm || '' }, this.applyModifiers)
+ }
+
+ handleSortingAndFilters = (appliedSorting, appliedFilters) => {
+ this.setState({ appliedFilters, appliedSorting }, this.applyModifiers)
+ }
+
+ applyModifiers = () => {
+ const { filterOptions, onApplyModifiers, records, searchKeys, sortOptions } = this.props
+ const { appliedFilters, appliedSorting, searchTerm } = this.state
+
+ const searchedRecords = searchTerm
+ ? records.filter(item => searchKeys.some(
+ searchKey => item[searchKey].toLowerCase().includes(searchTerm.toLowerCase())
+ ))
+ : records
+
+ const filteredRecords = appliedFilters
+ ? searchedRecords.filter(
+ record => filterOptions.every(
+ (filterOption, i) => filterOption.filter(record, appliedFilters[i])
+ )
+ )
+ : searchedRecords
+
+ const sortedRecords = appliedSorting
+ ? _.orderBy(
+ filteredRecords,
+ sortOptions[appliedSorting].keys.map(
+ key => (typeof key === 'string' ? (item => item[key].toLowerCase()) : key)
+ ),
+ sortOptions[appliedSorting].orders
+ )
+ : filteredRecords
+
+ onApplyModifiers(sortedRecords)
+ }
+
+ renderTitle = () => {
+ const { title, classes } = this.props
+
+ return (
+
+ {title}
+
+ )
+ }
+
+ renderActions = () => {
+ const { actions } = this.props
+
+ if (!actions) {
+ return null
+ }
+
+ return actions.filter(action => (action.isVisible !== undefined ? action.isVisible : true)).map(
+ action =>
+ )
+ }
+
+ renderSearch = () => {
+ const { searchKeys, classes } = this.props
+ const { searchTerm } = this.state
+
+ if (!searchKeys) {
+ // for the gap of expandable search
+ return
+ }
+
+ return (
+
+ this.handleSearchChange(e.target.value)}
+ input={{ name: 'search' }}
+ value={searchTerm}
+ placeholder="Search"
+ stretched={false}
+ variant="expandable"
+ />
+
+ )
+ }
+
+ renderSortAndFilter = () => {
+ const { filterOptions, sortOptions, classes } = this.props
+ const { appliedFilters, appliedSorting } = this.state
+
+ if (!sortOptions && !filterOptions) {
+ return null
+ }
+
+ const defaultFilters = this.getDefaultFilters()
+ const defaultSorting = this.getDefaultSorting()
+ const appliedFilterCount = this.getAppliedFilterCount()
+
+ return (
+
+
+ 0 && appliedFilterCount}
+ icon="filter"
+ activeIcon={appliedFilterCount > 0}
+ placeholder={[ sortOptions && 'Sort', filterOptions && 'Filter' ]
+ .filter(Boolean)
+ .join(' / ')}
+ input={{ name: 'filter' }}
+ stretched={false}
+ variant="simple"
+ />
+
+
+
+ )
+ }
+
+ render() {
+ const { classes } = this.props
+
+ return (
+
+ {this.renderTitle()}
+ {this.renderActions()}
+ {this.renderSearch()}
+ {this.renderSortAndFilter()}
+
+ )
+ }
+}
+
+PageToolbar.propTypes = {
+ actions: PropTypes.arrayOf(PropTypes.shape({
+ icon: PropTypes.string.isRequired,
+ isVisible: PropTypes.bool,
+ label: PropTypes.string,
+ onClick: PropTypes.func.isRequired
+ })),
+ filterOptions: PropTypes.arrayOf(
+ PropTypes.shape({
+ filter: PropTypes.func.isRequired,
+ items: PropTypes.arrayOf(
+ PropTypes.shape({
+ isDefault: PropTypes.bool,
+ label: PropTypes.string.isRequired,
+ value: PropTypes.string.isRequired
+ })
+ ),
+ type: PropTypes.oneOf([ 'single', 'multiple' ])
+ })
+ ),
+ searchKeys: PropTypes.array,
+ sortOptions: PropTypes.arrayOf(
+ PropTypes.shape({
+ isDefault: PropTypes.bool,
+ keys: PropTypes.arrayOf(PropTypes.oneOfType([ PropTypes.string, PropTypes.func ])),
+ label: PropTypes.string.isRequired,
+ orders: PropTypes.arrayOf(PropTypes.string)
+ })
+ ),
+ title: PropTypes.string.isRequired
+}
+
+PageToolbar.defaultProps = {
+ actions: null,
+ filterOptions: null,
+ searchKeys: null,
+ sortOptions: null
+}
+
+export default injectSheet(({ units }) => ({
+ section: {
+ display: 'flex',
+ marginBottom: units.pageTitleMarginBottom
+ },
+ heading: {
+ alignItems: 'center',
+ display: 'inline-flex',
+ marginRight: units.pageTitleMarginRight
+ },
+ filterField: {
+ width: units.sortAndFilterInputWidth
+ },
+ searchField: {
+ flex: 1
+ }
+}))(PageToolbar)
diff --git a/ui/src/components/internal/pageToolbar/SortAndFilterMenu.js b/ui/src/components/internal/pageToolbar/SortAndFilterMenu.js
new file mode 100644
index 0000000..8f7e431
--- /dev/null
+++ b/ui/src/components/internal/pageToolbar/SortAndFilterMenu.js
@@ -0,0 +1,155 @@
+import injectSheet from 'react-jss'
+import PropTypes from 'prop-types'
+import React, { Component, Fragment } from 'react'
+
+import CheckboxInput from 'components/internal/inputs/CheckboxInput'
+import FilledButton from 'components/buttons/FilledButton'
+import Menu from 'components/internal/menu/Menu'
+import MenuBody from 'components/internal/menu/MenuBody'
+import MenuDivider from 'components/internal/menu/MenuDivider'
+import MenuFooter from 'components/internal/menu/MenuFooter'
+import MenuHeading from 'components/internal/menu/MenuHeading'
+import RadioInput from 'components/internal/inputs/RadioInput'
+
+class SortAndFilterMenu extends Component {
+ constructor(props) {
+ super(props)
+
+ this.state = {
+ filters: props.appliedFilters || props.defaultFilters,
+ sorting: props.appliedSorting !== null ? props.appliedSorting : props.defaultSorting
+ }
+ }
+
+ onClear = (e) => {
+ const { applySortingAndFilters, defaultFilters, defaultSorting, closeMenu } = this.props
+
+ e.persist()
+ this.setState(
+ { filters: defaultFilters, sorting: defaultSorting }, () => {
+ applySortingAndFilters(null, null)
+ closeMenu(e)
+ }
+ )
+ }
+
+ onApply = (e) => {
+ const { applySortingAndFilters, closeMenu } = this.props
+ const { filters, sorting } = this.state
+
+ applySortingAndFilters(sorting, filters)
+ closeMenu(e)
+ }
+
+ changeSorting(itemValue) {
+ this.setState({ sorting: itemValue })
+ }
+
+ changeFilters(itemValue, index) {
+ const { filters } = this.state
+
+ const changeCheckboxFilter = filter => (filter.includes(itemValue)
+ ? filter.filter(value => value !== itemValue)
+ : filter.concat(itemValue))
+
+ const changeFilterByType = filter => (Array.isArray(filter)
+ ? changeCheckboxFilter(filter) : itemValue)
+
+ const changeFilterOptionsValueAtIndex = () => filters.map(
+ (filter, i) => (i === index ? changeFilterByType(filter) : filter)
+ )
+
+ this.setState({
+ filters: changeFilterOptionsValueAtIndex()
+ })
+ }
+
+ render() {
+ const { filterOptions, sortOptions, closeMenu, isHidden, classes } = this.props
+ const { filters, sorting } = this.state
+
+ return (
+
+
+ {sortOptions && Sort By }
+ {sortOptions && (
+ this.changeSorting(i) }}
+ options={sortOptions.map((sortOption, i) => ({
+ description: sortOption.label,
+ value: i
+ }))}
+ />
+ )}
+ {filterOptions && sortOptions && }
+ {filterOptions
+ && filterOptions.map((filterOption, i) => (
+
+
+ {`Filter By ${filterOption.label || ''}`}
+
+ {filterOption.type === 'multiple' ? (
+ filterOption.items.map(item => (
+ this.changeFilters(item.value, i)
+ }}
+ />
+ ))
+ ) : (
+ this.changeFilters(val, i) }}
+ option={filterOption.items.map(item => ({
+ description: item.label,
+ value: item.value
+ }))}
+ />
+ )}
+
+ ))}
+
+
+
+
+
+
+
+
+ )
+ }
+}
+
+SortAndFilterMenu.propTypes = {
+ appliedFilters: PropTypes.arrayOf(PropTypes.oneOfType([ PropTypes.array, PropTypes.string ])),
+ appliedSorting: PropTypes.number,
+ applySortingAndFilters: PropTypes.func,
+ defaultFilters: PropTypes.arrayOf(PropTypes.oneOfType([ PropTypes.array, PropTypes.string ])),
+ defaultSorting: PropTypes.number
+}
+
+SortAndFilterMenu.defaultProps = {
+ appliedFilters: null,
+ appliedSorting: null,
+ applySortingAndFilters: () => null,
+ defaultFilters: null,
+ defaultSorting: null
+}
+
+export default injectSheet(({ units }) => ({
+ clearApplyContainer: {
+ alignItems: 'center',
+ display: 'flex',
+ padding: [ 0, units.sortAndFilterMenuItemHorizontalPadding ]
+ }
+}))(SortAndFilterMenu)
diff --git a/ui/src/components/internal/panel/Panel.js b/ui/src/components/internal/panel/Panel.js
new file mode 100644
index 0000000..30c0f7c
--- /dev/null
+++ b/ui/src/components/internal/panel/Panel.js
@@ -0,0 +1,27 @@
+import injectSheet from 'react-jss'
+import React from 'react'
+
+function Panel({ children, classes }) {
+ return (
+
+ {children}
+
+ )
+}
+
+export default injectSheet(({ colors, shadows, units }) => ({
+ panel: {
+ backgroundColor: colors.panelBackground,
+ borderRadius: units.panelBorderRadius,
+ boxShadow: shadows.panel,
+ display: 'flex',
+ flexDirection: 'column',
+ overflow: 'hidden',
+ position: 'relative',
+ width: '100%',
+
+ '& + &': {
+ marginTop: units.panelMarginTop
+ }
+ }
+}))(Panel)
diff --git a/ui/src/components/internal/panel/PanelBody.js b/ui/src/components/internal/panel/PanelBody.js
new file mode 100644
index 0000000..62c4a2c
--- /dev/null
+++ b/ui/src/components/internal/panel/PanelBody.js
@@ -0,0 +1,19 @@
+import injectSheet from 'react-jss'
+import React from 'react'
+
+function PanelBody({ children, classes }) {
+ return (
+
+ {children}
+
+ )
+}
+
+export default injectSheet(({ units }) => ({
+ panelBody: {
+ paddingTop: units.panelBodyVerticalPadding,
+ paddingRight: units.panelBodyHorizontalPadding,
+ paddingBottom: units.panelBodyVerticalPadding,
+ paddingLeft: units.panelBodyHorizontalPadding
+ }
+}))(PanelBody)
diff --git a/ui/src/components/internal/panel/PanelContainer.js b/ui/src/components/internal/panel/PanelContainer.js
new file mode 100644
index 0000000..87c641d
--- /dev/null
+++ b/ui/src/components/internal/panel/PanelContainer.js
@@ -0,0 +1,17 @@
+import React from 'react'
+import { withTheme } from 'react-jss'
+
+import Column from 'components/internal/Column'
+import Row from 'components/internal/Row'
+
+function PanelContainer({ children, theme }) {
+ return (
+
+
+ {children}
+
+
+ )
+}
+
+export default withTheme(PanelContainer)
diff --git a/ui/src/components/internal/panel/PanelDetails.js b/ui/src/components/internal/panel/PanelDetails.js
new file mode 100644
index 0000000..47914c3
--- /dev/null
+++ b/ui/src/components/internal/panel/PanelDetails.js
@@ -0,0 +1,18 @@
+import injectSheet from 'react-jss'
+import React from 'react'
+
+function PanelDetails({ children, classes }) {
+ return {children}
+}
+
+export default injectSheet(({ typography, units }) => ({
+ panelDetails: {
+ ...typography.lightSpaced,
+
+ marginBottom: units.panelDetailsMarginBottom,
+
+ '& b': {
+ ...typography.semiboldSmall
+ }
+ }
+}))(PanelDetails)
diff --git a/ui/src/components/internal/panel/PanelHeader.js b/ui/src/components/internal/panel/PanelHeader.js
new file mode 100644
index 0000000..d585f4d
--- /dev/null
+++ b/ui/src/components/internal/panel/PanelHeader.js
@@ -0,0 +1,36 @@
+import injectSheet from 'react-jss'
+import React from 'react'
+
+import FontIcon from 'components/FontIcon'
+
+function PanelHeader({ children, classes, icon }) {
+ return (
+
+ {icon && (
+
+
+
+ )}
+ {children}
+
+ )
+}
+
+export default injectSheet(({ colors, units }) => ({
+ panelHeader: {
+ alignItems: 'center',
+ backgroundColor: colors.panelHeaderBackgroundColor,
+ borderBottomColor: colors.panelHeaderBottomBorder,
+ borderBottomStyle: 'solid',
+ borderBottomWidth: 1,
+ display: 'flex',
+ paddingBottom: units.panelHeaderVerticalPadding,
+ paddingLeft: units.panelHeaderHorizontalPadding,
+ paddingRight: units.panelHeaderHorizontalPadding,
+ paddingTop: units.panelHeaderVerticalPadding
+ },
+ iconWrapper: {
+ color: colors.panelHeaderIcon,
+ marginRight: units.panelHeaderIconWrapperMarginRight
+ }
+}))(PanelHeader)
diff --git a/ui/src/components/internal/panel/PanelHeading.js b/ui/src/components/internal/panel/PanelHeading.js
new file mode 100644
index 0000000..f0b8470
--- /dev/null
+++ b/ui/src/components/internal/panel/PanelHeading.js
@@ -0,0 +1,16 @@
+import injectSheet from 'react-jss'
+import React from 'react'
+
+function PanelHeading({ classes, children }) {
+ return {children}
+}
+
+PanelHeading = injectSheet(({ typography, units }) => ({
+ panelHeading: {
+ ...typography.semiboldSquished,
+
+ marginBottom: units.panelHeadingMarginBottom
+ }
+}))(PanelHeading)
+
+export default PanelHeading
diff --git a/ui/src/components/internal/panel/PanelSubHeading.js b/ui/src/components/internal/panel/PanelSubHeading.js
new file mode 100644
index 0000000..73f09c2
--- /dev/null
+++ b/ui/src/components/internal/panel/PanelSubHeading.js
@@ -0,0 +1,16 @@
+import injectSheet from 'react-jss'
+import React from 'react'
+
+function PanelSubHeading({ classes, children }) {
+ return {children}
+}
+
+PanelSubHeading = injectSheet(({ typography, units }) => ({
+ panelSubHeading: {
+ ...typography.semibold,
+
+ marginBottom: units.panelSubHeadingMarginBottom
+ }
+}))(PanelSubHeading)
+
+export default PanelSubHeading
diff --git a/ui/src/components/internal/panel/PanelTable.js b/ui/src/components/internal/panel/PanelTable.js
new file mode 100644
index 0000000..f3bffe7
--- /dev/null
+++ b/ui/src/components/internal/panel/PanelTable.js
@@ -0,0 +1,147 @@
+import injectSheet from 'react-jss'
+import PropTypes from 'prop-types'
+import React, { Fragment } from 'react'
+
+import LoaderView from 'components/LoaderView'
+import Text from 'components/typography/Text'
+
+function PanelTable({ emptyText, loading, records, classes }) {
+ const renderPanelTable = () => {
+ if (loading) {
+ return
+ }
+
+ if (!records.length) {
+ return {emptyText}
+ }
+
+ return (
+
+ {records.map((record, index) => (
+ // eslint-disable-next-line react/no-array-index-key
+
+
+
+ {record.name}
+ {record.badge && (
+ {record.badge}
+ )}
+
+ {record.details && (
+
{record.details}
+ )}
+ {record.action}
+
+
+ {record.status && (
+ {`${record.status}:`}
+ )}
+ {record.time}
+
+
+ ))}
+
+ )
+ }
+
+ return (
+
+ {renderPanelTable()}
+
+ )
+}
+
+PanelTable.propTypes = {
+ emptyText: PropTypes.string,
+ link: PropTypes.string,
+ loading: PropTypes.bool,
+ records: PropTypes.arrayOf(
+ PropTypes.shape({
+ name: PropTypes.string,
+ badge: PropTypes.string,
+ details: PropTypes.string,
+ action: PropTypes.node,
+ status: PropTypes.string,
+ time: PropTypes.string
+ })
+ )
+}
+
+PanelTable.defaultProps = {
+ emptyText: null,
+ link: null,
+ loading: false,
+ records: []
+}
+
+PanelTable = injectSheet(({ colors, typography, units }) => ({
+ panelTableWrapper: {
+ marginBottom: units.panelTableMarginBottom
+ },
+ panelTableCell: {
+ alignItems: 'center',
+ borderBottomColor: colors.panelTableBorder,
+ borderBottomStyle: 'solid',
+ borderBottomWidth: units.panelTableBorderWidth,
+ display: 'flex',
+ flexDirection: 'row',
+ paddingTop: units.panelTableCellVerticalPadding,
+ paddingBottom: units.panelTableCellVerticalPadding,
+
+ '&:first-child': {
+ borderTopColor: colors.panelTableBorder,
+ borderTopStyle: 'solid',
+ borderTopWidth: units.panelTableBorderWidth
+ }
+ },
+ panelTableCellMainContent: {
+ ...typography.semiboldSmallSquished,
+
+ width: '100%'
+ },
+ panelTableCellBadge: {
+ ...typography.regularSmallCompact,
+
+ backgroundColor: colors.panelTableCellBadgeBackground,
+ borderRadius: units.panelTableCellBadgeBorderRadius,
+ color: colors.panelTableCellBadgeColor,
+ height: units.panelTableCellBadgeHeight,
+ marginLeft: units.panelTableCellMarginLeft,
+ paddingTop: units.panelTableCellBadgeVerticalPadding,
+ paddingRight: units.panelTableCellBadgeHorizontalPadding,
+ paddingBottom: units.panelTableCellBadgeVerticalPadding,
+ paddingLeft: units.panelTableCellBadgeHorizontalPadding,
+ textTransform: 'uppercase'
+ },
+ panelTableCellSideContent: {
+ ...typography.mediumSquished,
+
+ alignItems: 'center',
+ borderLeftColor: colors.panelTableBorder,
+ borderLeftStyle: 'solid',
+ borderLeftWidth: units.panelTableBorderWidth,
+ color: colors.panelTableSecondaryText,
+ display: 'flex',
+ flexDirection: 'column',
+ height: units.panelTableCellHeight,
+ justifyContent: 'center',
+ minWidth: units.panelTableCellSideContentMinWidth
+ },
+ panelTableCellStatus: {
+ ...typography.lightSmall
+ },
+ panelTableCellSubtitle: {
+ ...typography.mediumSquished,
+
+ color: colors.panelTableSecondaryText,
+ display: 'inline-block',
+ marginTop: units.panelTableCellSubtitleMarginTop,
+ marginRight: units.panelTableCellSubtitleMarginRight,
+ overflow: 'hidden',
+ textOverflow: 'ellipsis',
+ whiteSpace: 'nowrap',
+ width: units.panelTableCellSubtitleWidth
+ }
+}))(PanelTable)
+
+export default PanelTable
diff --git a/ui/src/components/internal/panel/index.js b/ui/src/components/internal/panel/index.js
new file mode 100644
index 0000000..6af9130
--- /dev/null
+++ b/ui/src/components/internal/panel/index.js
@@ -0,0 +1,8 @@
+export { default as Panel } from './Panel'
+export { default as PanelBody } from './PanelBody'
+export { default as PanelContainer } from './PanelContainer'
+export { default as PanelDetails } from './PanelDetails'
+export { default as PanelHeader } from './PanelHeader'
+export { default as PanelHeading } from './PanelHeading'
+export { default as PanelSubHeading } from './PanelSubHeading'
+export { default as PanelTable } from './PanelTable'
diff --git a/ui/src/components/internal/sidePane/SidePane.js b/ui/src/components/internal/sidePane/SidePane.js
new file mode 100644
index 0000000..cf09352
--- /dev/null
+++ b/ui/src/components/internal/sidePane/SidePane.js
@@ -0,0 +1,39 @@
+import injectSheet from 'react-jss'
+import React from 'react'
+
+import BaseModal from 'components/BaseModal'
+
+function SidePane({ classes, children, ...other }) {
+ return (
+
+ {children}
+
+ )
+}
+
+export default injectSheet(({ colors, shadows, units }) => ({
+ contentBase: {
+ backgroundColor: colors.sidePaneBackground,
+ boxShadow: shadows.sidePane,
+ display: 'flex',
+ flexDirection: 'column',
+ padding: units.sidePanePadding,
+ position: 'absolute',
+ top: 0,
+ right: 0,
+ bottom: 0,
+ transform: 'translateX(100%)',
+ width: units.sidePaneWidth
+ },
+ contentAfterOpen: {
+ transform: 'translateX(0)'
+ },
+ contentBeforeClose: {
+ transform: 'translateX(100%)'
+ }
+}))(SidePane)
diff --git a/ui/src/components/internal/sidePane/SidePaneBody.js b/ui/src/components/internal/sidePane/SidePaneBody.js
new file mode 100644
index 0000000..8d31337
--- /dev/null
+++ b/ui/src/components/internal/sidePane/SidePaneBody.js
@@ -0,0 +1,24 @@
+import injectSheet from 'react-jss'
+import PropTypes from 'prop-types'
+import React from 'react'
+
+function SidePaneBody({ classes, children }) {
+ return (
+
+ {children}
+
+ )
+}
+
+SidePaneBody.propTypes = {
+ children: PropTypes.node.isRequired
+}
+
+export default injectSheet(({ units }) => ({
+ sidePaneBody: {
+ flex: '1 1 auto',
+ overflowY: 'auto',
+ padding: units.sidePaneBodyPadding,
+ position: 'relative' // For positioning submit button
+ }
+}))(SidePaneBody)
diff --git a/ui/src/components/internal/sidePane/SidePaneFormFooter.js b/ui/src/components/internal/sidePane/SidePaneFormFooter.js
new file mode 100644
index 0000000..154e703
--- /dev/null
+++ b/ui/src/components/internal/sidePane/SidePaneFormFooter.js
@@ -0,0 +1,21 @@
+import injectSheet from 'react-jss'
+import PropTypes from 'prop-types'
+import React from 'react'
+
+function SidePaneFormFooter({ classes, children }) {
+ return (
+
+ {children}
+
+ )
+}
+
+SidePaneFormFooter.propTypes = {
+ children: PropTypes.node.isRequired
+}
+
+export default injectSheet(() => ({
+ sidePaneFormFooter: {
+ float: 'right'
+ }
+}))(SidePaneFormFooter)
diff --git a/ui/src/components/internal/sidePane/SidePaneHeader.js b/ui/src/components/internal/sidePane/SidePaneHeader.js
new file mode 100644
index 0000000..c895cc9
--- /dev/null
+++ b/ui/src/components/internal/sidePane/SidePaneHeader.js
@@ -0,0 +1,41 @@
+import injectSheet from 'react-jss'
+import PropTypes from 'prop-types'
+import React from 'react'
+
+import FontIcon from 'components/FontIcon'
+
+function SidePaneHeader({ onArrowClick, classes, children }) {
+ return (
+
+
+ {children}
+
+ )
+}
+
+SidePaneHeader.propTypes = {
+ onArrowClick: PropTypes.func,
+ children: PropTypes.node.isRequired
+}
+
+SidePaneHeader.defaultProps = {
+ onArrowClick: () => null
+}
+
+export default injectSheet(({ colors, gradients, shadows, units }) => ({
+ sidePaneHeader: {
+ boxShadow: shadows.sidePaneHeader,
+ backgroundImage: gradients.sidePaneHeader,
+ flex: '0 0 auto',
+ padding: units.sidePaneHeaderPadding,
+ position: 'relative',
+
+ '& .icon': {
+ color: colors.sidePaneHeaderArrow,
+ cursor: 'pointer',
+ position: 'absolute',
+ top: units.sidePaneHeaderArrowTop,
+ left: units.sidePaneHeaderArrowLeft
+ }
+ }
+}))(SidePaneHeader)
diff --git a/ui/src/components/internal/sidePane/index.js b/ui/src/components/internal/sidePane/index.js
new file mode 100644
index 0000000..dcad477
--- /dev/null
+++ b/ui/src/components/internal/sidePane/index.js
@@ -0,0 +1,4 @@
+export { default as SidePane } from './SidePane'
+export { default as SidePaneBody } from './SidePaneBody'
+export { default as SidePaneFormFooter } from './SidePaneFormFooter'
+export { default as SidePaneHeader } from './SidePaneHeader'
diff --git a/ui/src/components/internal/sidePanes/EntitySidePane.js b/ui/src/components/internal/sidePanes/EntitySidePane.js
new file mode 100644
index 0000000..ddc5975
--- /dev/null
+++ b/ui/src/components/internal/sidePanes/EntitySidePane.js
@@ -0,0 +1,27 @@
+import React from 'react'
+
+import EntityForm from 'components/internal/forms/EntityForm'
+import { SidePane, SidePaneBody, SidePaneHeader } from 'components/internal/sidePane'
+import { SidePaneTitle } from 'components/internal/typography'
+
+function EntitySidePane({ entities, formValues, onFormSubmit, onRequestClose, ...other }) {
+ const action = formValues.id ? 'Edit' : 'New'
+
+ const title = `${action} Entity`
+
+ return (
+
+
+
+ {title}
+
+
+
+
+
+
+
+ )
+}
+
+export default EntitySidePane
diff --git a/ui/src/components/internal/sidePanes/FieldSidePane.js b/ui/src/components/internal/sidePanes/FieldSidePane.js
new file mode 100644
index 0000000..9ca688f
--- /dev/null
+++ b/ui/src/components/internal/sidePanes/FieldSidePane.js
@@ -0,0 +1,27 @@
+import React from 'react'
+
+import FieldForm from 'components/internal/forms/FieldForm'
+import { SidePane, SidePaneBody, SidePaneHeader } from 'components/internal/sidePane'
+import { SidePaneTitle } from 'components/internal/typography'
+
+function FieldSidePane({ entities, formValues, onFormSubmit, onRequestClose, ...other }) {
+ const action = formValues.id ? 'Edit' : 'New'
+
+ const title = `${action} Field`
+
+ return (
+
+
+
+ {title}
+
+
+
+
+
+
+
+ )
+}
+
+export default FieldSidePane
diff --git a/ui/src/components/internal/sidePanes/RecordSidePane.js b/ui/src/components/internal/sidePanes/RecordSidePane.js
new file mode 100644
index 0000000..0d9a599
--- /dev/null
+++ b/ui/src/components/internal/sidePanes/RecordSidePane.js
@@ -0,0 +1,27 @@
+import React from 'react'
+
+import RecordForm from 'components/internal/forms/RecordForm'
+import { SidePane, SidePaneBody, SidePaneHeader } from 'components/internal/sidePane'
+import { SidePaneTitle } from 'components/internal/typography'
+
+function RecordSidePane({ fields, formValues, onFormSubmit, onRequestClose, ...other }) {
+ const action = formValues.id ? 'Edit' : 'New'
+
+ const title = `${action} Record`
+
+ return (
+
+
+
+ {title}
+
+
+
+
+
+
+
+ )
+}
+
+export default RecordSidePane
diff --git a/ui/src/components/internal/sidebar/Sidebar.js b/ui/src/components/internal/sidebar/Sidebar.js
new file mode 100644
index 0000000..9204e82
--- /dev/null
+++ b/ui/src/components/internal/sidebar/Sidebar.js
@@ -0,0 +1,66 @@
+import classNames from 'classnames'
+import injectSheet from 'react-jss'
+import PropTypes from 'prop-types'
+import React from 'react'
+import { Link, Route, Switch } from 'react-router-dom'
+
+import Logo from 'components/Logo'
+import ProjectSidebar from 'components/internal/sidebars/ProjectSidebar'
+import TeamSidebar from 'components/internal/sidebars/TeamSidebar'
+import UserSidebar from 'components/internal/sidebars/UserSidebar'
+
+function Sidebar({ fluid, classes }) {
+ return (
+
+
+
+
+
+ null} />
+ null} />
+
+
+
+
+
+ )
+}
+
+Sidebar.propTypes = {
+ fluid: PropTypes.bool
+}
+
+Sidebar.defaultProps = {
+ fluid: false
+}
+
+export default injectSheet(({ units }) => ({
+ sidebarContainer: {
+ display: 'flex',
+ flexDirection: 'column',
+ paddingLeft: units.sidebarHorizontalPadding,
+ paddingRight: units.sidebarHorizontalPadding
+ },
+ sidebarContainer_fixed: {
+ overflowX: 'hidden',
+ position: 'fixed',
+ top: 0,
+ left: 0,
+ bottom: 0,
+ width: units.sidebarWidth
+ },
+ sidebarHeader: {
+ display: 'inline-block',
+ marginRight: -1 * units.sidebarHorizontalPadding,
+ marginLeft: -1 * units.sidebarHorizontalPadding,
+ paddingTop: units.sidebarHeaderPaddingTop,
+ paddingRight: units.sidebarItemPaddingHorizontal,
+ paddingBottom: units.sidebarHeaderPaddingBottom,
+ paddingLeft: units.sidebarItemPaddingHorizontal
+ }
+}))(Sidebar)
diff --git a/ui/src/components/internal/sidebar/SidebarBreadcrumb.js b/ui/src/components/internal/sidebar/SidebarBreadcrumb.js
new file mode 100644
index 0000000..6ceb2fd
--- /dev/null
+++ b/ui/src/components/internal/sidebar/SidebarBreadcrumb.js
@@ -0,0 +1,119 @@
+import injectSheet from 'react-jss'
+import PropTypes from 'prop-types'
+import React, { Fragment } from 'react'
+import { Link } from 'react-router-dom'
+
+import FontIcon from 'components/FontIcon'
+import { SidebarBreadcrumbText, SidebarBreadcrumbTitle } from 'components/internal/typography'
+
+function SidebarBreadcrumb({
+ icon, leftItems, rightItems, link, text, title, classes
+}) {
+ const renderSidebarHeader = () => (
+
+ {icon && }
+
+ {title}
+
+
+ )
+
+ const renderBreadcrumbLeftItems = () => (
+ leftItems && leftItems.map((item, index) => (
+
+
+
+ {item}
+
+
+ ))
+ )
+
+ return (
+
+
+ {link ? (
+
+ {renderSidebarHeader()}
+
+ ) : renderSidebarHeader()}
+
+ {text && (
+
+ {text}
+
+ )}
+ {renderBreadcrumbLeftItems()}
+
+
+
+
+ {rightItems}
+
+
+ )
+}
+
+SidebarBreadcrumb.propTypes = {
+ icon: PropTypes.string,
+ leftItems: PropTypes.arrayOf(PropTypes.node),
+ rightItems: PropTypes.arrayOf(PropTypes.node),
+ text: PropTypes.string,
+ title: PropTypes.string
+}
+
+SidebarBreadcrumb.defaultProps = {
+ icon: null,
+ leftItems: null,
+ rightItems: null,
+ text: null,
+ title: null
+}
+
+export default injectSheet(({ colors, units }) => ({
+ sidebarBreadcrumb: {
+ alignItems: 'center',
+ display: 'flex',
+ marginRight: -1 * units.sidebarHorizontalPadding,
+ marginLeft: -1 * units.sidebarHorizontalPadding,
+ justifyContent: 'space-between',
+ paddingTop: units.sidebarBreadcrumbPaddingVertical,
+ paddingRight: units.sidebarItemPaddingHorizontal,
+ paddingBottom: units.sidebarBreadcrumbPaddingVertical,
+ paddingLeft: units.sidebarItemPaddingHorizontal
+ },
+ sidebarBreadcrumbTitleWrapper: {
+ alignItems: 'center',
+ display: 'inline-flex',
+
+ '& .icon': {
+ color: colors.text_pale,
+ paddingRight: units.sidebarBreadcrumbIconPaddingRight
+ }
+ },
+ sidebarBreadcrumbTextWrapper: {
+ alignItems: 'center',
+ display: 'flex',
+ marginTop: units.sidebarBreadcrumbTextWrapperMarginTop
+ },
+ sidebarBreadcrumbItem: {
+ alignSelf: 'stretch',
+ alignItems: 'center',
+ display: 'flex'
+ },
+ sidebarBreadcrumbHeaderWrapper: {
+ width: '100%'
+ },
+ itemSeparator: {
+ alignSelf: 'stretch',
+ backgroundColor: colors.sidebarBreadcrumbItemBorder,
+ marginTop: units.sidebarBreadcrumbItemMarginVertical,
+ marginRight: units.sidebarBreadcrumbItemMarginHorizontal,
+ marginBottom: units.sidebarBreadcrumbItemMarginVertical,
+ marginLeft: units.sidebarBreadcrumbItemMarginHorizontal,
+ width: 1
+ },
+ rightItems: {
+ display: 'flex'
+ }
+}))(SidebarBreadcrumb)
diff --git a/ui/src/components/internal/sidebar/SidebarItem.js b/ui/src/components/internal/sidebar/SidebarItem.js
new file mode 100644
index 0000000..1d19b79
--- /dev/null
+++ b/ui/src/components/internal/sidebar/SidebarItem.js
@@ -0,0 +1,98 @@
+import injectSheet from 'react-jss'
+import PropTypes from 'prop-types'
+import React from 'react'
+import { NavLink } from 'react-router-dom'
+
+import * as mixins from 'styles/mixins'
+import FontIcon from 'components/FontIcon'
+import { SidebarItemText } from 'components/internal/typography'
+
+function SidebarItem({ item, classes }) {
+ return (
+
+
+
+
+
+ {item.name}
+
+
+ )
+}
+
+SidebarItem.propTypes = {
+ item: PropTypes.shape({
+ icon: PropTypes.string,
+ name: PropTypes.string,
+ url: PropTypes.string
+ }).isRequired
+}
+
+export default injectSheet(({
+ fonts, colors, shadows, units
+}) => ({
+ sidebarItem: {
+ alignItems: 'center',
+ display: 'inline-flex',
+ marginRight: -1 * units.sidebarHorizontalPadding,
+ marginLeft: -1 * units.sidebarHorizontalPadding,
+ paddingTop: units.sidebarItemPaddingVertical,
+ paddingRight: units.sidebarItemPaddingHorizontal,
+ paddingBottom: units.sidebarItemPaddingVertical,
+ paddingLeft: units.sidebarItemPaddingHorizontal,
+
+ '&:hover': {
+ '& $sidebarItemText': {
+ ...fonts.poppinsSemibold
+ },
+
+ '& $sidebarItemIconWrapper': {
+ '& .icon': {
+ color: colors.text_dark
+ }
+ }
+ }
+ },
+ sidebarItem_active: {
+ '& $sidebarItemText': {
+ ...fonts.poppinsSemibold
+ },
+
+ '& $sidebarItemIconWrapper': {
+ backgroundColor: colors.sidebarItemBackground_active,
+ boxShadow: shadows.sidebarItem_active,
+
+ '& .icon': {
+ color: colors.text_dark
+ }
+ }
+ },
+ sidebarItemText: { // Used for sidebarItem_active and to override class in SidebarItemText
+ },
+ sidebarItemIconWrapper: {
+ ...mixins.size(units.sidebarItemIconSize),
+ ...mixins.transitionSimple(),
+
+ alignItems: 'center',
+ borderRadius: '50%',
+ display: 'inline-flex',
+ justifyContent: 'center',
+ marginRight: units.sidebarItemIconMarginRight,
+ /*
+ minWidth is added to ensure that `sidebarItemIconWrapper` doesn't get squished
+ if `SideBarText` exceeds the parent's width.
+ */
+ minWidth: units.sidebarItemIconSize,
+ textAlign: 'center',
+
+ '& .icon': {
+ ...mixins.transitionSimple(),
+
+ color: colors.text_pale
+ }
+ }
+}))(SidebarItem)
diff --git a/ui/src/components/internal/sidebars/ProjectSidebar.js b/ui/src/components/internal/sidebars/ProjectSidebar.js
new file mode 100644
index 0000000..50151c3
--- /dev/null
+++ b/ui/src/components/internal/sidebars/ProjectSidebar.js
@@ -0,0 +1,44 @@
+import gql from 'graphql-tag'
+import PropTypes from 'prop-types'
+import React, { Fragment } from 'react'
+
+import Divider from 'components/internal/Divider'
+import SidebarBreadcrumb from 'components/internal/sidebar/SidebarBreadcrumb'
+import SidebarItem from 'components/internal/sidebar/SidebarItem'
+import { withClientQuery } from 'lib/data'
+
+function ProjectSidebar({ match, project: { name } = {} }) {
+ return (
+
+
+
+
+
+
+
+
+
+
+
+
+ )
+}
+
+ProjectSidebar.propTypes = {
+ match: PropTypes.object.isRequired
+}
+
+ProjectSidebar = withClientQuery(gql`
+ query ProjectSidebar($id: ID!) {
+ project(id: $id) {
+ id
+ name
+ }
+ }
+`, {
+ options: ({ match }) => ({
+ variables: { id: match.params.projectId }
+ })
+})(ProjectSidebar)
+
+export default ProjectSidebar
diff --git a/ui/src/components/internal/sidebars/TeamSidebar.js b/ui/src/components/internal/sidebars/TeamSidebar.js
new file mode 100644
index 0000000..1718f2d
--- /dev/null
+++ b/ui/src/components/internal/sidebars/TeamSidebar.js
@@ -0,0 +1,52 @@
+import gql from 'graphql-tag'
+import PropTypes from 'prop-types'
+import React, { Fragment } from 'react'
+
+import Divider from 'components/internal/Divider'
+import SidebarBreadcrumb from 'components/internal/sidebar/SidebarBreadcrumb'
+import SidebarItem from 'components/internal/sidebar/SidebarItem'
+import Spacer from 'components/Spacer'
+import { withClientQuery } from 'lib/data'
+
+function TeamSidebar({ match, team: { name } = {} }) {
+ const items = [
+ { name: 'Projects', url: `${match.url}/projects`, icon: 'project' },
+ { name: 'Members', url: `${match.url}/members`, icon: 'person' },
+ { name: 'Settings', url: `${match.url}/settings`, icon: 'setting' }
+
+ // Temporarily hidden for launch
+ // { name: 'Billing', url: `${match.url}/billing`, icon: 'billing' },
+ ]
+
+ return (
+
+
+
+
+
+
+
+
+ {items.map(item => )}
+
+ )
+}
+
+TeamSidebar.propTypes = {
+ match: PropTypes.object.isRequired
+}
+
+TeamSidebar = withClientQuery(gql`
+ query TeamSidebar($id: ID!) {
+ team(id: $id) {
+ id
+ name
+ }
+ }
+`, {
+ options: ({ match }) => ({
+ variables: { id: match.params.teamId }
+ })
+})(TeamSidebar)
+
+export default TeamSidebar
diff --git a/ui/src/components/internal/sidebars/UserSidebar.js b/ui/src/components/internal/sidebars/UserSidebar.js
new file mode 100644
index 0000000..826792b
--- /dev/null
+++ b/ui/src/components/internal/sidebars/UserSidebar.js
@@ -0,0 +1,38 @@
+import React, { Fragment } from 'react'
+
+import AppContext from 'components/AppContext'
+import Divider from 'components/internal/Divider'
+import SidebarBreadcrumb from 'components/internal/sidebar/SidebarBreadcrumb'
+import SidebarItem from 'components/internal/sidebar/SidebarItem'
+import Spacer from 'components/Spacer'
+import { User } from 'models'
+
+function UserSidebar() {
+ const items = [
+ { name: 'Teams', url: '/user/teams', icon: 'team' },
+ { name: 'Profile', url: '/user/profile', icon: 'person' },
+ { name: 'Settings', url: '/user/settings', icon: 'setting' }
+
+ // Temporarily hidden for launch
+ // { name: 'Notifications', url: '/user/notifications', icon: 'notification-bell' },
+ ]
+
+ return (
+
+
+
+
+ {({ currentUser }) => (
+
+ )}
+
+
+
+
+
+ {items.map(item => )}
+
+ )
+}
+
+export default UserSidebar
diff --git a/ui/src/components/internal/tab/Tab.js b/ui/src/components/internal/tab/Tab.js
new file mode 100644
index 0000000..deef7ad
--- /dev/null
+++ b/ui/src/components/internal/tab/Tab.js
@@ -0,0 +1,119 @@
+import classNames from 'classnames'
+import injectSheet from 'react-jss'
+import PropTypes from 'prop-types'
+import React from 'react'
+import { Tab as ReactTab } from 'react-tabs'
+
+import * as mixins from 'styles/mixins'
+import cleanProps from 'lib/cleanProps'
+
+function Tab({ variant, classes, children, ...other }) {
+ return (
+
+ {children}
+
+ )
+}
+
+Tab.propTypes = {
+ variant: PropTypes.oneOf([ 'fixed', 'fluid' ])
+}
+
+Tab.defaultProps = {
+ variant: 'fluid'
+}
+
+Tab = injectSheet(({ colors, shadows, typography, units }) => ({
+ tab: {
+ ...mixins.transitionSimple(),
+
+ color: colors.tab,
+ cursor: 'pointer',
+ display: 'flex',
+ justifyContent: 'center',
+ outline: 'none',
+ position: 'relative',
+
+ '&:hover': {
+ color: colors.tab_hover
+ },
+
+ '&::after': {
+ content: '" "',
+ display: 'block',
+ position: 'absolute',
+ bottom: 0,
+ left: 0,
+ right: 0
+ },
+
+ '& > *': {
+ margin: 10
+ }
+ },
+ tab_fluid: {
+ ...typography.regular,
+
+ flex: '1 0 auto',
+ padding: units.tabPadding,
+ textTransform: 'uppercase',
+
+ '&::after': {
+ backgroundColor: colors.tabUnderline,
+ height: units.tabUnderlineHeight
+ }
+ },
+ tab_fixed: {
+ ...mixins.size(units.tabWidth_fixed, units.tabHeight_fixed),
+ ...typography.regularSmallSpacedSquished,
+
+ alignItems: 'center',
+ display: 'inline-flex',
+ justifyContent: 'center',
+
+ '&::after': {
+ backgroundColor: 'transparent',
+ height: units.tabUnderlineHeight_selected
+ },
+
+ '&:not(last-child)': {
+ '&::before': {
+ ...mixins.size(1, units.tabDividerHeight),
+
+ backgroundColor: colors.tabDividerBackground,
+ content: '" "',
+ position: 'absolute',
+ right: 0
+ }
+ }
+ },
+ tab_selected: {
+ color: colors.tab_selected,
+
+ '&::after': {
+ backgroundColor: colors.tab_selected,
+ boxShadow: shadows.tab_selected,
+ height: units.tabUnderlineHeight_selected
+ }
+ },
+ tab_fluidSelected: {
+ ...typography.semiboldExtraSmall
+ },
+ tab_fixedSelected: {
+ ...typography.semiboldExtraSmallSquished
+ }
+}))(Tab)
+
+Tab.tabsRole = 'Tab'
+
+export default Tab
diff --git a/ui/src/components/internal/tab/TabLink.js b/ui/src/components/internal/tab/TabLink.js
new file mode 100644
index 0000000..dc5aeca
--- /dev/null
+++ b/ui/src/components/internal/tab/TabLink.js
@@ -0,0 +1,73 @@
+import injectSheet from 'react-jss'
+import PropTypes from 'prop-types'
+import React from 'react'
+import { NavLink } from 'react-router-dom'
+
+import * as mixins from 'styles/mixins'
+
+function TabLink({ to, children, classes }) {
+ return (
+
+ {children}
+
+ )
+}
+
+TabLink.propTypes = {
+ to: PropTypes.oneOfType([ PropTypes.string, PropTypes.object ]).isRequired
+}
+
+TabLink = injectSheet(({ colors, shadows, typography, units }) => ({
+ tabLink: {
+ ...mixins.size(units.tabWidth_fixed, units.tabHeight_fixed),
+ ...mixins.transitionSimple(),
+ ...typography.regularSmallSpacedSquished,
+
+ alignItems: 'center',
+ color: colors.tab,
+ cursor: 'pointer',
+ display: 'inline-flex',
+ justifyContent: 'center',
+ outline: 'none',
+ position: 'relative',
+
+ '&:hover': {
+ color: colors.tabLink_hover
+ },
+
+ '&::after': {
+ backgroundColor: 'transparent',
+ content: '" "',
+ display: 'block',
+ height: units.tabLinkUnderlineHeight_active,
+ position: 'absolute',
+ bottom: 0,
+ left: 0,
+ right: 0
+ },
+
+ '&:not(last-child)': {
+ '&::before': {
+ ...mixins.size(1, units.tabDividerHeight),
+
+ backgroundColor: colors.tabDividerBackground,
+ content: '" "',
+ position: 'absolute',
+ right: 0
+ }
+ }
+ },
+ tabLink_active: {
+ ...typography.semiboldExtraSmallSquished,
+
+ color: colors.tabLink_active,
+
+ '&::after': {
+ backgroundColor: colors.tabLink_active,
+ boxShadow: shadows.tabLink_active,
+ height: units.tabLinkUnderlineHeight_active
+ }
+ }
+}))(TabLink)
+
+export default TabLink
diff --git a/ui/src/components/internal/tab/TabList.js b/ui/src/components/internal/tab/TabList.js
new file mode 100644
index 0000000..ae46b7c
--- /dev/null
+++ b/ui/src/components/internal/tab/TabList.js
@@ -0,0 +1,27 @@
+import injectSheet from 'react-jss'
+import React from 'react'
+import { TabList as ReactTabList } from 'react-tabs'
+
+import cleanProps from 'lib/cleanProps'
+import * as mixins from 'styles/mixins'
+
+function TabList({ classes, children, ...other }) {
+ return (
+
+ {children}
+
+ )
+}
+
+TabList = injectSheet(() => ({
+ tabList: {
+ ...mixins.listless,
+
+ alignItems: 'center',
+ display: 'flex'
+ }
+}))(TabList)
+
+TabList.tabsRole = 'TabList'
+
+export default TabList
diff --git a/ui/src/components/internal/tab/index.js b/ui/src/components/internal/tab/index.js
new file mode 100644
index 0000000..cf2f855
--- /dev/null
+++ b/ui/src/components/internal/tab/index.js
@@ -0,0 +1,7 @@
+import { Tabs, TabPanel } from 'react-tabs'
+
+export { default as Tab } from './Tab'
+export { default as TabLink } from './TabLink'
+export { default as TabList } from './TabList'
+
+export { Tabs, TabPanel }
diff --git a/ui/src/components/internal/typography/BackLink.js b/ui/src/components/internal/typography/BackLink.js
new file mode 100644
index 0000000..22434f6
--- /dev/null
+++ b/ui/src/components/internal/typography/BackLink.js
@@ -0,0 +1,29 @@
+import injectSheet from 'react-jss'
+import React from 'react'
+import { Link } from 'react-router-dom'
+
+import FontIcon from 'components/FontIcon'
+
+function BackLink({ classes, children, ...other }) {
+ return (
+
+
+ {children}
+
+ )
+}
+
+export default injectSheet(({ colors, typography, units }) => ({
+ backLink: {
+ ...typography.semiboldExtraSmallSquished,
+
+ alignItems: 'center',
+ color: colors.text_primary,
+ display: 'flex',
+ marginBottom: units.backLinkMarginBottom,
+
+ '& .icon': {
+ marginRight: 5
+ }
+ }
+}))(BackLink)
diff --git a/ui/src/components/internal/typography/CellContent.js b/ui/src/components/internal/typography/CellContent.js
new file mode 100644
index 0000000..c192915
--- /dev/null
+++ b/ui/src/components/internal/typography/CellContent.js
@@ -0,0 +1,18 @@
+import React from 'react'
+
+import BaseText from 'components/typography/BaseText'
+
+function CellContent({ children, ...other }) {
+ return (
+
+ {children}
+
+ )
+}
+
+export default CellContent
diff --git a/ui/src/components/internal/typography/CellLabel.js b/ui/src/components/internal/typography/CellLabel.js
new file mode 100644
index 0000000..4954cd9
--- /dev/null
+++ b/ui/src/components/internal/typography/CellLabel.js
@@ -0,0 +1,25 @@
+import React from 'react'
+import injectSheet from 'react-jss'
+
+import BaseText from 'components/typography/BaseText'
+
+function CellLabel({ classes, children, ...other }) {
+ return (
+
+ {children}
+
+ )
+}
+
+export default injectSheet({
+ cellLabel: {
+ lineHeight: 1,
+ paddingBottom: 10
+ }
+})(CellLabel)
diff --git a/ui/src/components/internal/typography/CellText.js b/ui/src/components/internal/typography/CellText.js
new file mode 100644
index 0000000..93241b7
--- /dev/null
+++ b/ui/src/components/internal/typography/CellText.js
@@ -0,0 +1,18 @@
+import React from 'react'
+
+import BaseText from 'components/typography/BaseText'
+
+function CellText({ children, ...other }) {
+ return (
+
+ {children}
+
+ )
+}
+
+export default CellText
diff --git a/ui/src/components/internal/typography/CellTitle.js b/ui/src/components/internal/typography/CellTitle.js
new file mode 100644
index 0000000..b92d27e
--- /dev/null
+++ b/ui/src/components/internal/typography/CellTitle.js
@@ -0,0 +1,18 @@
+import React from 'react'
+
+import BaseText from 'components/typography/BaseText'
+
+function CellTitle({ children, ...other }) {
+ return (
+
+ {children}
+
+ )
+}
+
+export default CellTitle
diff --git a/ui/src/components/internal/typography/Description.js b/ui/src/components/internal/typography/Description.js
new file mode 100644
index 0000000..5977b67
--- /dev/null
+++ b/ui/src/components/internal/typography/Description.js
@@ -0,0 +1,26 @@
+import injectSheet from 'react-jss'
+import React from 'react'
+
+import BaseText from 'components/typography/BaseText'
+
+function Description({ classes, children, ...other }) {
+ return (
+
+ {children}
+
+ )
+}
+
+export default injectSheet(({ units }) => ({
+ description: {
+ lineHeight: 1.83,
+ maxWidth: units.internalDescriptionMaxWidth,
+ textAlign: 'center'
+ }
+}))(Description)
diff --git a/ui/src/components/internal/typography/DialogDescription.js b/ui/src/components/internal/typography/DialogDescription.js
new file mode 100644
index 0000000..844a361
--- /dev/null
+++ b/ui/src/components/internal/typography/DialogDescription.js
@@ -0,0 +1,24 @@
+import injectSheet from 'react-jss'
+import React from 'react'
+
+import BaseText from 'components/typography/BaseText'
+
+function DialogDescription({ children, classes, ...other }) {
+ return (
+
+ {children}
+
+ )
+}
+
+export default injectSheet(({ units }) => ({
+ dialogDescription: {
+ marginBottom: units.dialogDescriptionMarginBottom
+ }
+}))(DialogDescription)
diff --git a/ui/src/components/internal/typography/DialogTitle.js b/ui/src/components/internal/typography/DialogTitle.js
new file mode 100644
index 0000000..3c09762
--- /dev/null
+++ b/ui/src/components/internal/typography/DialogTitle.js
@@ -0,0 +1,18 @@
+import React from 'react'
+
+import BaseText from 'components/typography/BaseText'
+
+function DialogTitle({ children, ...other }) {
+ return (
+
+ {children}
+
+ )
+}
+
+export default DialogTitle
diff --git a/ui/src/components/internal/typography/HeaderItemText.js b/ui/src/components/internal/typography/HeaderItemText.js
new file mode 100644
index 0000000..3a757a4
--- /dev/null
+++ b/ui/src/components/internal/typography/HeaderItemText.js
@@ -0,0 +1,28 @@
+import injectSheet from 'react-jss'
+import React from 'react'
+
+import BaseText from 'components/typography/BaseText'
+
+function HeaderItemText({ classes, children, ...other }) {
+ return (
+
+ {children}
+
+ )
+}
+
+export default injectSheet(() => ({
+ headerItemText: {
+ lineHeight: '17px',
+ overflow: 'hidden',
+ textOverflow: 'ellipsis',
+ whiteSpace: 'nowrap',
+ width: '100%'
+ }
+}))(HeaderItemText)
diff --git a/ui/src/components/internal/typography/HeaderItemTitle.js b/ui/src/components/internal/typography/HeaderItemTitle.js
new file mode 100644
index 0000000..d798c05
--- /dev/null
+++ b/ui/src/components/internal/typography/HeaderItemTitle.js
@@ -0,0 +1,18 @@
+import React from 'react'
+
+import BaseText from 'components/typography/BaseText'
+
+function HeaderItemTitle({ children, ...other }) {
+ return (
+
+ {children}
+
+ )
+}
+
+export default HeaderItemTitle
diff --git a/ui/src/components/internal/typography/Hint.js b/ui/src/components/internal/typography/Hint.js
new file mode 100644
index 0000000..2ccb768
--- /dev/null
+++ b/ui/src/components/internal/typography/Hint.js
@@ -0,0 +1,45 @@
+import injectSheet from 'react-jss'
+import PropTypes from 'prop-types'
+import React from 'react'
+
+import BaseText from 'components/typography/BaseText'
+import FontIcon from 'components/FontIcon'
+
+function Hint({ withIcon, children, classes, ...other }) {
+ return (
+
+ {withIcon && }
+ {children}
+
+ )
+}
+
+Hint.propTypes = {
+ variant: PropTypes.string,
+ withIcon: PropTypes.bool
+}
+
+Hint.defaultProps = {
+ variant: 'lightSmall',
+ withIcon: false
+}
+
+export default injectSheet(({ fonts, typography, units }) => ({
+ hint: {
+ display: 'flex',
+
+ '& strong': {
+ ...fonts.poppinsSemibold
+ },
+
+ '& .icon': {
+ lineHeight: ({ variant }) => typography[variant].lineHeight,
+ marginRight: units.hintIconMarginRight
+ }
+ }
+}))(Hint)
diff --git a/ui/src/components/internal/typography/LoaderText.js b/ui/src/components/internal/typography/LoaderText.js
new file mode 100644
index 0000000..d81eab3
--- /dev/null
+++ b/ui/src/components/internal/typography/LoaderText.js
@@ -0,0 +1,24 @@
+import injectSheet from 'react-jss'
+import React from 'react'
+
+import BaseText from 'components/typography/BaseText'
+
+function LoaderText({ classes, children, ...other }) {
+ return (
+
+ {children}
+
+ )
+}
+
+export default injectSheet(({ units }) => ({
+ loaderText: {
+ marginBottom: units.loaderTextMarginBottom
+ }
+}))(LoaderText)
diff --git a/ui/src/components/internal/typography/LoaderTitle.js b/ui/src/components/internal/typography/LoaderTitle.js
new file mode 100644
index 0000000..b04e47d
--- /dev/null
+++ b/ui/src/components/internal/typography/LoaderTitle.js
@@ -0,0 +1,25 @@
+import injectSheet from 'react-jss'
+import React from 'react'
+
+import BaseText from 'components/typography/BaseText'
+
+function LoaderTitle({ classes, children, ...other }) {
+ return (
+
+ {children}
+
+ )
+}
+
+export default injectSheet(({ units }) => ({
+ loaderTitle: {
+ marginTop: units.loaderTitleMarginTop,
+ marginBottom: units.loaderTitleMarginBottom
+ }
+}))(LoaderTitle)
diff --git a/ui/src/components/internal/typography/PageSubTitle.js b/ui/src/components/internal/typography/PageSubTitle.js
new file mode 100644
index 0000000..8bc08e1
--- /dev/null
+++ b/ui/src/components/internal/typography/PageSubTitle.js
@@ -0,0 +1,25 @@
+import injectSheet from 'react-jss'
+import React from 'react'
+
+import BaseText from 'components/typography/BaseText'
+
+function PageSubTitle({ classes, children, ...other }) {
+ return (
+
+ {children}
+
+ )
+}
+
+export default injectSheet(() => ({
+ pageSubTitle: {
+ marginBottom: 10,
+ marginTop: 15
+ }
+}))(PageSubTitle)
diff --git a/ui/src/components/internal/typography/PageTitle.js b/ui/src/components/internal/typography/PageTitle.js
new file mode 100644
index 0000000..3860312
--- /dev/null
+++ b/ui/src/components/internal/typography/PageTitle.js
@@ -0,0 +1,27 @@
+import injectSheet from 'react-jss'
+import React from 'react'
+
+import BaseText from 'components/typography/BaseText'
+
+function PageTitle({ classes, children, ...other }) {
+ return (
+
+ {children}
+
+ )
+}
+
+export default injectSheet(({ units }) => ({
+ pageTitle: {
+ alignItems: 'center',
+ display: 'inline-flex',
+ marginRight: units.pageTitleMarginRight,
+ marginBottom: units.pageTitleMarginBottom
+ }
+}))(PageTitle)
diff --git a/ui/src/components/internal/typography/PanelText.js b/ui/src/components/internal/typography/PanelText.js
new file mode 100644
index 0000000..d646fcf
--- /dev/null
+++ b/ui/src/components/internal/typography/PanelText.js
@@ -0,0 +1,34 @@
+import injectSheet from 'react-jss'
+import PropTypes from 'prop-types'
+import React from 'react'
+
+import BaseText from 'components/typography/BaseText'
+
+function PanelText({ children, classes, ...other }) {
+ return (
+
+ {children}
+
+ )
+}
+
+PanelText.propTypes = {
+ variant: PropTypes.string
+}
+
+PanelText.defaultProps = {
+ variant: 'regularSpaced'
+}
+
+export default injectSheet(({ fonts }) => ({
+ panelText: {
+ '& strong': {
+ ...fonts.poppinsSemibold
+ }
+ }
+}))(PanelText)
diff --git a/ui/src/components/internal/typography/RadioInputOption.js b/ui/src/components/internal/typography/RadioInputOption.js
new file mode 100644
index 0000000..5e2529c
--- /dev/null
+++ b/ui/src/components/internal/typography/RadioInputOption.js
@@ -0,0 +1,105 @@
+import classNames from 'classnames'
+import injectSheet from 'react-jss'
+import PropTypes from 'prop-types'
+import React, { Fragment } from 'react'
+
+import * as mixins from 'styles/mixins'
+
+function RadioInputOption({
+ inputValue,
+ option: { name, description, value, additionalFields },
+ onChange,
+ classes
+}) {
+ return (
+ onChange(value)}
+ onKeyPress={() => onChange(value)}
+ >
+ {name && (
+
+ {name}
+ :
+
+ )}
+ {description}
+ {value === inputValue && additionalFields}
+
+ )
+}
+
+RadioInputOption.propTypes = {
+ inputValue: PropTypes.oneOfType([ PropTypes.string, PropTypes.number ]),
+ onChange: PropTypes.func,
+ option: PropTypes.shape({
+ name: PropTypes.string,
+ description: PropTypes.string.isRequired,
+ value: PropTypes.oneOfType([ PropTypes.string, PropTypes.number ]).isRequired,
+ additionalFields: PropTypes.arrayOf(PropTypes.node)
+ }),
+ size: PropTypes.oneOf([ 'regular', 'small' ])
+}
+
+RadioInputOption.defaultProps = {
+ inputValue: null,
+ onChange: null,
+ option: null,
+ size: 'regular'
+}
+
+export default injectSheet(({ colors, typography, units }) => ({
+ radioInputOption: {
+ ...mixins.transitionSimple(),
+ ...typography.regularSpaced,
+
+ alignItems: 'center',
+ color: colors.text_pale,
+ cursor: 'pointer',
+ marginBottom: ({ size }) => (size === 'regular'
+ ? units.radioInputOptionMarginBottom_regular
+ : units.radioInputOptionMarginBottom_small),
+ paddingLeft: units.radioInputOptionPaddingLeft,
+ position: 'relative',
+
+ '& > [data-name]': {
+ ...typography.semiboldSmall,
+
+ color: colors.text_dark
+ },
+
+ '&:last-of-type': {
+ marginBottom: 0
+ },
+
+ '&::before': {
+ ...mixins.size(20),
+
+ backgroundColor: colors.radioInputBulletBackground,
+ borderRadius: '50%',
+ content: '" "',
+ display: 'block',
+ marginLeft: ({ size }) => size === 'small' && units.radioInputOptionMarginLeft_small,
+ position: 'absolute',
+ top: 4,
+ left: 0
+ }
+ },
+ radioInputOption_active: {
+ color: colors.text_dark,
+
+ '&::before': {
+ ...mixins.size(6),
+
+ backgroundColor: colors.radioInputBulletBackground_active,
+ borderColor: colors.radioInputBulletBorder_active,
+ borderStyle: 'solid',
+ borderWidth: units.radioInputBulletBorderWidth_active
+ }
+ }
+}))(RadioInputOption)
diff --git a/ui/src/components/internal/typography/SidePaneHint.js b/ui/src/components/internal/typography/SidePaneHint.js
new file mode 100644
index 0000000..36d9854
--- /dev/null
+++ b/ui/src/components/internal/typography/SidePaneHint.js
@@ -0,0 +1,24 @@
+import injectSheet from 'react-jss'
+import React from 'react'
+
+import BaseText from 'components/typography/BaseText'
+
+function SidePaneHint({ classes, children, ...other }) {
+ return (
+
+ {children}
+
+ )
+}
+
+export default injectSheet(({ units }) => ({
+ sidePaneHint: {
+ maxWidth: units.sidePaneHintMaxWidth
+ }
+}))(SidePaneHint)
diff --git a/ui/src/components/internal/typography/SidePaneSubtitle.js b/ui/src/components/internal/typography/SidePaneSubtitle.js
new file mode 100644
index 0000000..c7d82d1
--- /dev/null
+++ b/ui/src/components/internal/typography/SidePaneSubtitle.js
@@ -0,0 +1,18 @@
+import React from 'react'
+
+import BaseText from 'components/typography/BaseText'
+
+function SidePaneSubtitle({ classes, children, ...other }) {
+ return (
+
+ {children}
+
+ )
+}
+
+export default SidePaneSubtitle
diff --git a/ui/src/components/internal/typography/SidePaneTitle.js b/ui/src/components/internal/typography/SidePaneTitle.js
new file mode 100644
index 0000000..c2eaa19
--- /dev/null
+++ b/ui/src/components/internal/typography/SidePaneTitle.js
@@ -0,0 +1,24 @@
+import injectSheet from 'react-jss'
+import React from 'react'
+
+import BaseText from 'components/typography/BaseText'
+
+function SidePaneTitle({ classes, children, ...other }) {
+ return (
+
+ {children}
+
+ )
+}
+
+export default injectSheet(({ units }) => ({
+ sidePaneTitle: {
+ marginBottom: units.sidePaneTitleMarginBottom
+ }
+}))(SidePaneTitle)
diff --git a/ui/src/components/internal/typography/SidebarBreadcrumbText.js b/ui/src/components/internal/typography/SidebarBreadcrumbText.js
new file mode 100644
index 0000000..d0afce6
--- /dev/null
+++ b/ui/src/components/internal/typography/SidebarBreadcrumbText.js
@@ -0,0 +1,26 @@
+import injectSheet from 'react-jss'
+import React from 'react'
+
+import BaseText from 'components/typography/BaseText'
+
+function SidebarBreadcrumbText({ classes, children, ...other }) {
+ return (
+
+ {children}
+
+ )
+}
+
+export default injectSheet(() => ({
+ sidebarBreadcrumbText: {
+ overflow: 'hidden',
+ textOverflow: 'ellipsis',
+ whiteSpace: 'nowrap'
+ }
+}))(SidebarBreadcrumbText)
diff --git a/ui/src/components/internal/typography/SidebarBreadcrumbTitle.js b/ui/src/components/internal/typography/SidebarBreadcrumbTitle.js
new file mode 100644
index 0000000..7fb9a2e
--- /dev/null
+++ b/ui/src/components/internal/typography/SidebarBreadcrumbTitle.js
@@ -0,0 +1,18 @@
+import React from 'react'
+
+import BaseText from 'components/typography/BaseText'
+
+function SidebarBreadcrumbTitle({ children, ...other }) {
+ return (
+
+ {children}
+
+ )
+}
+
+export default SidebarBreadcrumbTitle
diff --git a/ui/src/components/internal/typography/SidebarItemText.js b/ui/src/components/internal/typography/SidebarItemText.js
new file mode 100644
index 0000000..02b6b4a
--- /dev/null
+++ b/ui/src/components/internal/typography/SidebarItemText.js
@@ -0,0 +1,18 @@
+import React from 'react'
+
+import BaseText from 'components/typography/BaseText'
+
+function SidebarItemText({ children, ...other }) {
+ return (
+
+ {children}
+
+ )
+}
+
+export default SidebarItemText
diff --git a/ui/src/components/internal/typography/SubTitle.js b/ui/src/components/internal/typography/SubTitle.js
new file mode 100644
index 0000000..2ea09be
--- /dev/null
+++ b/ui/src/components/internal/typography/SubTitle.js
@@ -0,0 +1,25 @@
+import injectSheet from 'react-jss'
+import React from 'react'
+
+import BaseText from 'components/typography/BaseText'
+
+function SubTitle({ classes, children, ...other }) {
+ return (
+
+ {children}
+
+ )
+}
+
+export default injectSheet(({ units }) => ({
+ subTitle: {
+ marginTop: units.internalSubTitleMarginVertical,
+ marginBottom: units.internalSubTitleMarginVertical
+ }
+}))(SubTitle)
diff --git a/ui/src/components/internal/typography/Text.js b/ui/src/components/internal/typography/Text.js
new file mode 100644
index 0000000..4472389
--- /dev/null
+++ b/ui/src/components/internal/typography/Text.js
@@ -0,0 +1,18 @@
+import React from 'react'
+
+import BaseText from 'components/typography/BaseText'
+
+function Text({ children, ...other }) {
+ return (
+
+ {children}
+
+ )
+}
+
+export default Text
diff --git a/ui/src/components/internal/typography/TextLink.js b/ui/src/components/internal/typography/TextLink.js
new file mode 100644
index 0000000..512a03a
--- /dev/null
+++ b/ui/src/components/internal/typography/TextLink.js
@@ -0,0 +1,47 @@
+import classNames from 'classnames'
+import injectSheet from 'react-jss'
+import PropTypes from 'prop-types'
+import React from 'react'
+
+import * as mixins from 'styles/mixins'
+import { TextLink as BaseTextLink } from 'components/typography'
+
+function TextLink({ inherit, variant, classes, ...other }) {
+ return (
+
+ )
+}
+
+TextLink.propTypes = {
+ inherit: PropTypes.bool,
+ variant: PropTypes.oneOf([ 'dark', 'pale' ])
+}
+
+TextLink.defaultProps = {
+ inherit: false,
+ variant: 'dark'
+}
+
+export default injectSheet(({ colors, typography }) => ({
+ textLink: ({ inherit }) => {
+ const inheritStyles = inherit && {
+ fontSize: 'inherit',
+ lineHeight: 'inherit'
+ }
+
+ return {
+ ...typography.semiboldExtraSmallSquished,
+ ...inheritStyles
+ }
+ },
+ textLink_dark: {
+ ...mixins.underline({ color: colors.text_dark, bottom: -2 }),
+
+ color: colors.text_dark
+ },
+ textLink_pale: {
+ ...mixins.underline({ color: colors.text_pale, bottom: -2 }),
+
+ color: colors.text_pale
+ }
+}))(TextLink)
diff --git a/ui/src/components/internal/typography/Title.js b/ui/src/components/internal/typography/Title.js
new file mode 100644
index 0000000..f0e0c19
--- /dev/null
+++ b/ui/src/components/internal/typography/Title.js
@@ -0,0 +1,18 @@
+import React from 'react'
+
+import BaseText from 'components/typography/BaseText'
+
+function Title({ children, ...other }) {
+ return (
+
+ {children}
+
+ )
+}
+
+export default Title
diff --git a/ui/src/components/internal/typography/index.js b/ui/src/components/internal/typography/index.js
new file mode 100644
index 0000000..326354a
--- /dev/null
+++ b/ui/src/components/internal/typography/index.js
@@ -0,0 +1,27 @@
+export { default as BackLink } from './BackLink'
+export { default as CellContent } from './CellContent'
+export { default as CellLabel } from './CellLabel'
+export { default as CellText } from './CellText'
+export { default as CellTitle } from './CellTitle'
+export { default as Description } from './Description'
+export { default as DialogDescription } from './DialogDescription'
+export { default as DialogTitle } from './DialogTitle'
+export { default as HeaderItemText } from './HeaderItemText'
+export { default as HeaderItemTitle } from './HeaderItemTitle'
+export { default as Hint } from './Hint'
+export { default as LoaderText } from './LoaderText'
+export { default as LoaderTitle } from './LoaderTitle'
+export { default as PageSubTitle } from './PageSubTitle'
+export { default as PageTitle } from './PageTitle'
+export { default as PanelText } from './PanelText'
+export { default as RadioInputOption } from './RadioInputOption'
+export { default as SidebarBreadcrumbText } from './SidebarBreadcrumbText'
+export { default as SidebarBreadcrumbTitle } from './SidebarBreadcrumbTitle'
+export { default as SidebarItemText } from './SidebarItemText'
+export { default as SidePaneHint } from './SidePaneHint'
+export { default as SidePaneSubtitle } from './SidePaneSubtitle'
+export { default as SidePaneTitle } from './SidePaneTitle'
+export { default as SubTitle } from './SubTitle'
+export { default as Text } from './Text'
+export { default as TextLink } from './TextLink'
+export { default as Title } from './Title'
diff --git a/ui/src/components/internal/views/EmptyView.js b/ui/src/components/internal/views/EmptyView.js
new file mode 100644
index 0000000..56d34ae
--- /dev/null
+++ b/ui/src/components/internal/views/EmptyView.js
@@ -0,0 +1,32 @@
+import PropTypes from 'prop-types'
+import React from 'react'
+
+import EmptyWrapper from 'components/internal/views/EmptyWrapper'
+import { LoaderTitle, LoaderText } from 'components/internal/typography'
+
+function EmptyView({ title, text }) {
+ return (
+
+ {title && (
+
+ {title}
+
+ )}
+
+ {text}
+
+
+ )
+}
+
+EmptyView.propTypes = {
+ title: PropTypes.string,
+ text: PropTypes.string
+}
+
+EmptyView.defaultProps = {
+ title: null,
+ text: null
+}
+
+export default EmptyView
diff --git a/ui/src/components/internal/views/EmptyWrapper.js b/ui/src/components/internal/views/EmptyWrapper.js
new file mode 100644
index 0000000..365e971
--- /dev/null
+++ b/ui/src/components/internal/views/EmptyWrapper.js
@@ -0,0 +1,64 @@
+import injectSheet from 'react-jss'
+import React from 'react'
+
+import * as mixins from 'styles/mixins'
+
+function EmptyWrapper({ classes, children }) {
+ return (
+
+ )
+}
+
+export default injectSheet(({ colors, units }) => {
+ const circleStyles = size => ({
+ ...mixins.size(size),
+
+ backgroundColor: colors.emptyWrapperCircleBackground,
+ borderRadius: '50%',
+ pointerEvents: 'none',
+ position: 'absolute',
+ zIndex: -1
+ })
+
+ return {
+ wrapper: {
+ ...mixins.size('100%', units.emptyWrapperHeight),
+
+ alignItems: 'center',
+ backgroundColor: colors.emptyWrapperBackground,
+ borderColor: colors.emptyWrapperBorder,
+ borderRadius: units.emptyWrapperBorderRadius,
+ borderStyle: 'solid',
+ borderWidth: 1,
+ display: 'flex',
+ flexDirection: 'column',
+ justifyContent: 'center',
+ overflow: 'hidden',
+ position: 'relative',
+ zIndex: 0
+ },
+ loaderCircle_small: {
+ ...circleStyles(units.emptyWrapperCircleSize_small),
+
+ top: units.emptyWrapperCircleShiftTop_small,
+ right: units.emptyWrapperCircleShiftRight_small
+ },
+ loaderCircle_medium: {
+ ...circleStyles(units.emptyWrapperCircleSize_medium),
+
+ top: units.emptyWrapperCircleShiftTop_medium,
+ left: units.emptyWrapperCircleShiftLeft_medium
+ },
+ loaderCircle_large: {
+ ...circleStyles(units.emptyWrapperCircleSize_large),
+
+ top: units.emptyWrapperCircleShiftTop_large,
+ right: units.emptyWrapperCircleShiftRight_large
+ }
+ }
+})(EmptyWrapper)
diff --git a/ui/src/components/layouts/ExternalLayout.js b/ui/src/components/layouts/ExternalLayout.js
new file mode 100644
index 0000000..0617a64
--- /dev/null
+++ b/ui/src/components/layouts/ExternalLayout.js
@@ -0,0 +1,95 @@
+import Helmet from 'react-helmet-async'
+import injectSheet from 'react-jss'
+import PropTypes from 'prop-types'
+import React, { Fragment } from 'react'
+
+import * as mixins from 'styles/mixins'
+import AlertBox from 'components/AlertBox'
+import Footer from 'components/external/Footer'
+import Header from 'components/external/Header'
+import resolveImage from 'lib/resolveImage'
+import ScrollToTop from 'components/ScrollToTop'
+
+function ExternalLayout({ isBare, classes, children }) {
+ const content = isBare ? children : (
+
+
+
+
+
+
+
+ {children}
+
+
+
+ )
+
+ return (
+
+
+
+
+
+
+
+
+
+ )
+}
+
+ExternalLayout.propTypes = {
+ isBare: PropTypes.bool,
+ children: PropTypes.node.isRequired
+}
+
+ExternalLayout.defaultProps = {
+ isBare: false
+}
+
+export default injectSheet(({ colors, gradients, units }) => ({
+ pageCircle: {
+ ...mixins.backgroundContain(),
+ ...mixins.responsiveProperties({
+ height: units.externalPageCircleSizeResponsive,
+ width: units.externalPageCircleSizeResponsive,
+ bottom: units.externalPageCircleShiftBottomResponsive,
+ left: units.externalPageCircleShiftLeftResponsive
+ }),
+
+ backgroundImage: `url(${resolveImage('external/page-circle.png')})`,
+ content: '" "',
+ pointerEvents: 'none',
+ position: 'absolute',
+ zIndex: -1
+ },
+ header: {
+ position: 'relative',
+ zIndex: 0,
+
+ '&::before': {
+ content: '" "',
+ backgroundImage: gradients.externalHeroBackground,
+ position: 'absolute',
+ top: '-150%',
+ right: '-15%',
+ left: '30%',
+ bottom: 0,
+ zIndex: -1,
+
+ ...mixins.responsiveProperties({
+ borderBottomRightRadius: { xs: '20%', sm: '25%', md: '30%' },
+ borderBottomLeftRadius: { xs: '70%' }
+ })
+ }
+ },
+ externalBackground: {
+ backgroundColor: colors.externalBackground,
+ overflow: 'hidden',
+ position: 'relative',
+ zIndex: 0
+ }
+}))(ExternalLayout)
diff --git a/ui/src/components/layouts/InternalLayout.js b/ui/src/components/layouts/InternalLayout.js
new file mode 100644
index 0000000..a3d7974
--- /dev/null
+++ b/ui/src/components/layouts/InternalLayout.js
@@ -0,0 +1,36 @@
+import injectSheet from 'react-jss'
+import PropTypes from 'prop-types'
+import React from 'react'
+
+import AlertBox from 'components/AlertBox'
+import Content from 'components/internal/Content'
+import Header from 'components/internal/Header'
+import ScrollToTop from 'components/ScrollToTop'
+import Sidebar from 'components/internal/sidebar/Sidebar'
+
+function InternalLayout({ fluid, classes, children }) {
+ return (
+
+
+
+ )
+}
+
+InternalLayout.propTypes = {
+ children: PropTypes.node.isRequired
+}
+
+export default injectSheet(({ colors, units }) => ({
+ internalLayout: {
+ backgroundColor: colors.internalBackground,
+ minHeight: '100%',
+ minWidth: units.internalLayoutMinWidth
+ }
+}))(InternalLayout)
diff --git a/ui/src/components/layouts/OnboardingLayout.js b/ui/src/components/layouts/OnboardingLayout.js
new file mode 100644
index 0000000..695e0dd
--- /dev/null
+++ b/ui/src/components/layouts/OnboardingLayout.js
@@ -0,0 +1,37 @@
+import injectSheet from 'react-jss'
+import PropTypes from 'prop-types'
+import React from 'react'
+
+import * as mixins from 'styles/mixins'
+import AlertBox from 'components/AlertBox'
+import Container from 'components/Container'
+import ScrollToTop from 'components/ScrollToTop'
+
+function OnboardingLayout({ classes, children }) {
+ return (
+
+
+
+ )
+}
+
+OnboardingLayout.propTypes = {
+ children: PropTypes.node.isRequired
+}
+
+export default injectSheet(({ gradients }) => ({
+ onboardingBackground: {
+ ...mixins.size('100vw', '100vh'),
+
+ backgroundImage: gradients.onboardingBackground,
+ display: 'flex',
+ flexDirection: 'column',
+ justifyContent: 'center',
+ overflow: 'hidden'
+ }
+}))(OnboardingLayout)
diff --git a/ui/src/components/onboarding/Body.js b/ui/src/components/onboarding/Body.js
new file mode 100644
index 0000000..f8d2b30
--- /dev/null
+++ b/ui/src/components/onboarding/Body.js
@@ -0,0 +1,22 @@
+import PropTypes from 'prop-types'
+import React from 'react'
+import injectSheet from 'react-jss'
+
+function Body({ classes, children }) {
+ return (
+
+ {children}
+
+ )
+}
+
+Body.propTypes = {
+ children: PropTypes.node.isRequired
+}
+
+export default injectSheet(() => ({
+ body: {
+ display: 'flex',
+ position: 'relative'
+ }
+}))(Body)
diff --git a/ui/src/components/onboarding/Card.js b/ui/src/components/onboarding/Card.js
new file mode 100644
index 0000000..b47d330
--- /dev/null
+++ b/ui/src/components/onboarding/Card.js
@@ -0,0 +1,41 @@
+import injectSheet from 'react-jss'
+import React from 'react'
+
+import * as mixins from 'styles/mixins'
+
+const circleSize = 320
+
+function Card({ classes, children }) {
+ return (
+
+ {children}
+
+ )
+}
+
+export default injectSheet(({ colors, shadows, units }) => ({
+ card: {
+ backgroundColor: colors.cardBackground,
+ borderRadius: units.cardBorderRadius,
+ boxShadow: shadows.card,
+ overflow: 'hidden',
+ paddingTop: units.cardVerticalPadding,
+ paddingRight: units.cardHorizontalPadding,
+ paddingBottom: units.cardVerticalPadding,
+ paddingLeft: units.cardHorizontalPadding,
+ position: 'relative',
+ width: units.cardWidth,
+
+ '&::after': {
+ ...mixins.circle({
+ size: circleSize,
+ color: colors.circleBackground_dark,
+ opacity: 1
+ }),
+
+ right: (-1 / 4) * circleSize,
+ bottom: (-2 / 3) * circleSize,
+ zIndex: -1
+ }
+ }
+}))(Card)
diff --git a/ui/src/components/onboarding/CardFootnote.js b/ui/src/components/onboarding/CardFootnote.js
new file mode 100644
index 0000000..295d6a0
--- /dev/null
+++ b/ui/src/components/onboarding/CardFootnote.js
@@ -0,0 +1,38 @@
+import classNames from 'classnames'
+import injectSheet from 'react-jss'
+import PropTypes from 'prop-types'
+import React from 'react'
+
+import * as mixins from 'styles/mixins'
+
+function CardFootnote({ hide, classes, children }) {
+ return (
+
+ {children}
+
+ )
+}
+
+CardFootnote.propTypes = {
+ hide: PropTypes.bool,
+ spaced: PropTypes.bool
+}
+
+CardFootnote.defaultProps = {
+ hide: false,
+ spaced: false
+}
+
+export default injectSheet(({ units }) => ({
+ cardFootnote: {
+ ...mixins.transitionSimple(),
+
+ paddingTop: ({ spaced }) => (
+ spaced ? units.cardFootnotePaddingTop * 2 : units.cardFootnotePaddingTop
+ ),
+ textAlign: 'center'
+ },
+ cardFootnote_hide: {
+ opacity: 0
+ }
+}))(CardFootnote)
diff --git a/ui/src/components/onboarding/CardWrapper.js b/ui/src/components/onboarding/CardWrapper.js
new file mode 100644
index 0000000..e1ccdc0
--- /dev/null
+++ b/ui/src/components/onboarding/CardWrapper.js
@@ -0,0 +1,55 @@
+import injectSheet from 'react-jss'
+import PropTypes from 'prop-types'
+import React from 'react'
+
+import * as mixins from 'styles/mixins'
+
+const circleSize = 521
+
+function CardWrapper({ classes, children }) {
+ return (
+
+ {children}
+
+ )
+}
+
+CardWrapper.propTypes = {
+ showSecondary: PropTypes.bool,
+ children: PropTypes.node.isRequired
+}
+
+CardWrapper.defaultProps = {
+ showSecondary: false
+}
+
+export default injectSheet(() => ({
+ cardWrapper: {
+ position: 'absolute',
+ right: 0,
+
+ '& > [data-card]:nth-of-type(1)': {
+ ...mixins.transitionSimple(),
+
+ opacity: ({ showSecondary }) => (showSecondary ? 0.21 : 1),
+ transform: ({ showSecondary }) => (showSecondary ? 'scale(0.9, 0.8) translateY(-15%)' : 'scale(1, 1) translateY(0)'),
+ zIndex: ({ showSecondary }) => (showSecondary ? 0 : 1)
+ },
+
+ '& > [data-card]:nth-of-type(2)': {
+ ...mixins.transitionSimple(),
+
+ opacity: ({ showSecondary }) => (showSecondary ? 1 : 0.21),
+ position: 'absolute',
+ transform: ({ showSecondary }) => (showSecondary ? 'scale(1, 1) translateY(-100%)' : 'scale(0.9, 0.8) translateY(-100%)'),
+ zIndex: ({ showSecondary }) => (showSecondary ? 1 : 0)
+ },
+
+ '&::before': {
+ ...mixins.circle({ size: circleSize }),
+
+ right: -0.4 * circleSize,
+ bottom: -0.5 * circleSize
+ }
+ }
+}))(CardWrapper)
diff --git a/ui/src/components/onboarding/Content.js b/ui/src/components/onboarding/Content.js
new file mode 100644
index 0000000..4c16733
--- /dev/null
+++ b/ui/src/components/onboarding/Content.js
@@ -0,0 +1,48 @@
+import injectSheet from 'react-jss'
+import PropTypes from 'prop-types'
+import React from 'react'
+import { Link } from 'react-router-dom'
+
+import * as mixins from 'styles/mixins'
+import Logo from 'components/Logo'
+import { Greeting } from 'components/onboarding/typography'
+
+const circleSize = 185
+
+function Content({ children, classes }) {
+ return (
+
+
+
+
+
+
+ {children}
+
+
+ )
+}
+
+Content.propTypes = {
+ children: PropTypes.node.isRequired
+}
+
+export default injectSheet(({ units }) => ({
+ content: {
+ height: units.onboardingContentHeight,
+ paddingTop: units.onboardingContentVerticalPadding,
+ position: 'relative',
+ width: units.onboardingContentWidth,
+
+ '&::after': {
+ ...mixins.circle({ size: circleSize }),
+
+ top: 0.5 * units.onboardingContentVerticalPadding,
+ left: -0.5 * circleSize
+ }
+ },
+ logoWrapper: {
+ position: 'absolute',
+ bottom: 40
+ }
+}))(Content)
diff --git a/ui/src/components/onboarding/FormFooter.js b/ui/src/components/onboarding/FormFooter.js
new file mode 100644
index 0000000..dcc5c6a
--- /dev/null
+++ b/ui/src/components/onboarding/FormFooter.js
@@ -0,0 +1,23 @@
+import PropTypes from 'prop-types'
+import React, { Fragment } from 'react'
+
+import ItemBar from 'components/ItemBar'
+import Spacer from 'components/Spacer'
+
+function FormFooter({ children }) {
+ return (
+
+
+
+
+ {children}
+
+
+ )
+}
+
+FormFooter.propTypes = {
+ children: PropTypes.node.isRequired
+}
+
+export default FormFooter
diff --git a/ui/src/components/onboarding/Header.js b/ui/src/components/onboarding/Header.js
new file mode 100644
index 0000000..3b79944
--- /dev/null
+++ b/ui/src/components/onboarding/Header.js
@@ -0,0 +1,56 @@
+import injectSheet from 'react-jss'
+import PropTypes from 'prop-types'
+import React from 'react'
+
+import * as mixins from 'styles/mixins'
+import ItemBar from 'components/ItemBar'
+import { Heading, NavLink, Text } from 'components/onboarding/typography'
+
+function Header({
+ heading, hint, link: { name, url }, classes
+}) {
+ return (
+
+
+
+ {heading}
+
+
+
+ {hint}
+
+
+
+ {name}
+
+
+
+
+ )
+}
+
+Header.propTypes = {
+ heading: PropTypes.string.isRequired,
+ hint: PropTypes.string.isRequired,
+ link: PropTypes.shape({
+ name: PropTypes.string,
+ url: PropTypes.string
+ }).isRequired
+}
+
+export default injectSheet(({ colors, units }) => ({
+ header: {
+ paddingBottom: units.onboardingHeaderPaddingBottom
+ },
+ navigation: {
+ alignItems: 'center',
+ display: 'flex'
+ },
+ separator: {
+ ...mixins.size(units.separatorWidth, 1),
+
+ backgroundColor: colors.separatorBackground,
+ marginRight: units.separatorHorizontalMargin,
+ marginLeft: units.separatorHorizontalMargin
+ }
+}))(Header)
diff --git a/ui/src/components/onboarding/forms/ConfirmUserForm.js b/ui/src/components/onboarding/forms/ConfirmUserForm.js
new file mode 100644
index 0000000..2afdb0e
--- /dev/null
+++ b/ui/src/components/onboarding/forms/ConfirmUserForm.js
@@ -0,0 +1,31 @@
+import React from 'react'
+import { Field, Form } from 'react-final-form'
+
+import FilledButton from 'components/buttons/FilledButton'
+import FormFooter from 'components/onboarding/FormFooter'
+import TextInput from 'components/inputs/TextInput'
+import { User } from 'models'
+
+function ConfirmUserForm(props) {
+ return (
+
+ )}
+ {...props}
+ />
+ )
+}
+
+export default ConfirmUserForm
diff --git a/ui/src/components/onboarding/forms/ForgotPasswordForm.js b/ui/src/components/onboarding/forms/ForgotPasswordForm.js
new file mode 100644
index 0000000..0eb5a29
--- /dev/null
+++ b/ui/src/components/onboarding/forms/ForgotPasswordForm.js
@@ -0,0 +1,27 @@
+import React from 'react'
+import { Field, Form } from 'react-final-form'
+
+import FilledButton from 'components/buttons/FilledButton'
+import FormFooter from 'components/onboarding/FormFooter'
+import TextInput from 'components/inputs/TextInput'
+import { User } from 'models'
+
+function ForgotPasswordForm({ onSubmit }) {
+ return (
+
+ )}
+ />
+ )
+}
+
+export default ForgotPasswordForm
diff --git a/ui/src/components/onboarding/forms/LoginForm.js b/ui/src/components/onboarding/forms/LoginForm.js
new file mode 100644
index 0000000..b53574e
--- /dev/null
+++ b/ui/src/components/onboarding/forms/LoginForm.js
@@ -0,0 +1,28 @@
+import React from 'react'
+import { Field, Form } from 'react-final-form'
+
+import FilledButton from 'components/buttons/FilledButton'
+import FormFooter from 'components/onboarding/FormFooter'
+import TextInput from 'components/inputs/TextInput'
+import { User } from 'models'
+
+function LoginForm({ onSubmit }) {
+ return (
+
+ )}
+ />
+ )
+}
+
+export default LoginForm
diff --git a/ui/src/components/onboarding/forms/ResetPasswordForm.js b/ui/src/components/onboarding/forms/ResetPasswordForm.js
new file mode 100644
index 0000000..7185a41
--- /dev/null
+++ b/ui/src/components/onboarding/forms/ResetPasswordForm.js
@@ -0,0 +1,29 @@
+import React from 'react'
+import { Field, Form } from 'react-final-form'
+
+import FilledButton from 'components/buttons/FilledButton'
+import FormFooter from 'components/onboarding/FormFooter'
+import TextInput from 'components/inputs/TextInput'
+import { User } from 'models'
+
+function ResetPasswordForm(props) {
+ return (
+
+ )}
+ {...props}
+ />
+ )
+}
+
+export default ResetPasswordForm
diff --git a/ui/src/components/onboarding/forms/SignupForm.js b/ui/src/components/onboarding/forms/SignupForm.js
new file mode 100644
index 0000000..97427b5
--- /dev/null
+++ b/ui/src/components/onboarding/forms/SignupForm.js
@@ -0,0 +1,27 @@
+import React from 'react'
+import { Field, Form } from 'react-final-form'
+
+import FilledButton from 'components/buttons/FilledButton'
+import FormFooter from 'components/onboarding/FormFooter'
+import TextInput from 'components/inputs/TextInput'
+import { User } from 'models'
+
+function SignupForm({ onSubmit }) {
+ return (
+
+ )}
+ />
+ )
+}
+
+export default SignupForm
diff --git a/ui/src/components/onboarding/typography/CardHeading.js b/ui/src/components/onboarding/typography/CardHeading.js
new file mode 100644
index 0000000..67ca84d
--- /dev/null
+++ b/ui/src/components/onboarding/typography/CardHeading.js
@@ -0,0 +1,26 @@
+import injectSheet from 'react-jss'
+import React from 'react'
+
+import BaseText from 'components/typography/BaseText'
+
+function CardHeading({ children, classes, ...other }) {
+ return (
+
+ {children}
+
+ )
+}
+
+export default injectSheet(({ units }) => ({
+ cardHeading: {
+ '& + p': {
+ marginTop: units.cardHeadingSpacing
+ }
+ }
+}))(CardHeading)
diff --git a/ui/src/components/onboarding/typography/CardText.js b/ui/src/components/onboarding/typography/CardText.js
new file mode 100644
index 0000000..277f900
--- /dev/null
+++ b/ui/src/components/onboarding/typography/CardText.js
@@ -0,0 +1,26 @@
+import injectSheet from 'react-jss'
+import React from 'react'
+
+import BaseText from 'components/typography/BaseText'
+
+function CardText({ children, classes, ...other }) {
+ return (
+
+ {children}
+
+ )
+}
+
+export default injectSheet(({ units }) => ({
+ cardText: {
+ '& + p': {
+ marginTop: units.cardTextSpacing
+ }
+ }
+}))(CardText)
diff --git a/ui/src/components/onboarding/typography/Greeting.js b/ui/src/components/onboarding/typography/Greeting.js
new file mode 100644
index 0000000..fb2a6c2
--- /dev/null
+++ b/ui/src/components/onboarding/typography/Greeting.js
@@ -0,0 +1,18 @@
+import React from 'react'
+
+import BaseText from 'components/typography/BaseText'
+
+function Greeting({ children, ...other }) {
+ return (
+
+ {children}
+
+ )
+}
+
+export default Greeting
diff --git a/ui/src/components/onboarding/typography/Heading.js b/ui/src/components/onboarding/typography/Heading.js
new file mode 100644
index 0000000..2d2da10
--- /dev/null
+++ b/ui/src/components/onboarding/typography/Heading.js
@@ -0,0 +1,13 @@
+import React from 'react'
+
+import BaseText from 'components/typography/BaseText'
+
+function Heading({ children, ...other }) {
+ return (
+
+ {children}
+
+ )
+}
+
+export default Heading
diff --git a/ui/src/components/onboarding/typography/NavLink.js b/ui/src/components/onboarding/typography/NavLink.js
new file mode 100644
index 0000000..917e44e
--- /dev/null
+++ b/ui/src/components/onboarding/typography/NavLink.js
@@ -0,0 +1,34 @@
+import injectSheet from 'react-jss'
+import PropTypes from 'prop-types'
+import React from 'react'
+import { Link } from 'react-router-dom'
+
+import * as mixins from 'styles/mixins'
+
+function NavLink({
+ to, children, classes
+}) {
+ return (
+
+ {children}
+
+ )
+}
+
+NavLink.propTypes = {
+ to: PropTypes.oneOfType([ PropTypes.string, PropTypes.object ]),
+ children: PropTypes.node.isRequired
+}
+
+NavLink.defaultProps = {
+ to: null
+}
+
+export default injectSheet(({ colors, typography }) => ({
+ navLink: {
+ ...mixins.animateUnderline({ bottom: -6 }),
+ ...typography.semibold,
+
+ color: colors.text_light
+ }
+}))(NavLink)
diff --git a/ui/src/components/onboarding/typography/Text.js b/ui/src/components/onboarding/typography/Text.js
new file mode 100644
index 0000000..966bb27
--- /dev/null
+++ b/ui/src/components/onboarding/typography/Text.js
@@ -0,0 +1,18 @@
+import React from 'react'
+
+import BaseText from 'components/typography/BaseText'
+
+function Text({ children, ...other }) {
+ return (
+
+ {children}
+
+ )
+}
+
+export default Text
diff --git a/ui/src/components/onboarding/typography/TextLink.js b/ui/src/components/onboarding/typography/TextLink.js
new file mode 100644
index 0000000..c5eab2a
--- /dev/null
+++ b/ui/src/components/onboarding/typography/TextLink.js
@@ -0,0 +1,37 @@
+import classNames from 'classnames'
+import injectSheet from 'react-jss'
+import PropTypes from 'prop-types'
+import React from 'react'
+
+import * as mixins from 'styles/mixins'
+import { TextLink as BaseTextLink } from 'components/typography'
+
+function TextLink({ variant, classes, ...other }) {
+ return (
+
+ )
+}
+
+TextLink.propTypes = {
+ variant: PropTypes.oneOf([ 'dark', 'light' ])
+}
+
+TextLink.defaultProps = {
+ variant: 'light'
+}
+
+export default injectSheet(({ colors, typography }) => ({
+ textLink: {
+ ...typography.boldSquished
+ },
+ textLink_dark: {
+ ...mixins.animateUnderline({ color: colors.text_dark, bottom: -2 }),
+
+ color: colors.text_dark
+ },
+ textLink_light: {
+ ...mixins.animateUnderline({ color: colors.text_light, bottom: -2 }),
+
+ color: colors.text_light
+ }
+}))(TextLink)
diff --git a/ui/src/components/onboarding/typography/index.js b/ui/src/components/onboarding/typography/index.js
new file mode 100644
index 0000000..a56b3e3
--- /dev/null
+++ b/ui/src/components/onboarding/typography/index.js
@@ -0,0 +1,7 @@
+export { default as CardHeading } from './CardHeading'
+export { default as CardText } from './CardText'
+export { default as Greeting } from './Greeting'
+export { default as Heading } from './Heading'
+export { default as NavLink } from './NavLink'
+export { default as Text } from './Text'
+export { default as TextLink } from './TextLink'
diff --git a/ui/src/components/pages/AssetsPage.js b/ui/src/components/pages/AssetsPage.js
new file mode 100644
index 0000000..a9e17fe
--- /dev/null
+++ b/ui/src/components/pages/AssetsPage.js
@@ -0,0 +1,195 @@
+import gql from 'graphql-tag'
+import Helmet from 'react-helmet-async'
+import React, { Fragment, useEffect, useState } from 'react'
+import Uppy from '@uppy/core'
+import { DashboardModal } from '@uppy/react'
+
+import AssetDialog from 'components/internal/dialogs/AssetDialog'
+import AssetList from 'components/internal/AssetList'
+import IconButton from 'components/internal/buttons/IconButton'
+import Loader from 'components/internal/Loader'
+import withConfirmation from 'components/internal/decorators/withConfirmation'
+import { Asset } from 'models'
+import { OptimisticResponseModes, MutationResponseModes, withMutation, withQuery } from 'lib/data'
+import { PageTitle } from 'components/internal/typography'
+
+function AssetsPage({ match, assets, confirm, loading, createAsset, updateAsset, destroyAsset }) {
+ const [ asset, setAsset ] = useState({})
+ const [ isAssetDialogOpen, setIsAssetDialogOpen ] = useState(false)
+ const [ isUppyDashboardModalOpen, setIsUppyDashboardModalOpen ] = useState(false)
+ const [ uppy ] = useState(Uppy({
+ id: 'assets-uppy',
+ restrictions: {
+ maxNumberOfFiles: 1
+ }
+ }))
+
+ const openAssetDialog = (value) => {
+ setAsset(value)
+ setIsAssetDialogOpen(true)
+ }
+
+ const closeAssetDialog = () => setIsAssetDialogOpen(false)
+
+ const openUppyDashboardModal = () => setIsUppyDashboardModalOpen(true)
+
+ const closeUppyDashboardModal = () => setIsUppyDashboardModalOpen(false)
+
+ useEffect(() => {
+ uppy.on('upload', ({ fileIDs }) => {
+ const fileID = fileIDs[0]
+ const { data, meta } = uppy.getFile(fileID)
+
+ createAsset({ file: data, name: meta.name, projectId: match.params.projectId }, {
+ onSuccess: (response) => {
+ const createdAsset = response.data.asset
+ if (!createdAsset) {
+ return
+ }
+
+ uppy.reset()
+ closeUppyDashboardModal()
+ }
+ })
+ })
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ }, [])
+
+ const handleFormSubmit = values => (
+ updateAsset(values, { onSuccess: closeAssetDialog })
+ )
+
+ const actions = [
+ { icon: 'edit', onClick: record => openAssetDialog(record) },
+ {
+ icon: 'trash',
+ onClick: record => confirm({
+ description: `By clicking on Confirm, you will delete the asset: ${Asset.getFullName(record)}`,
+ onConfirmClick: () => destroyAsset(record)
+ })
+ }
+ ]
+
+ return (
+
+
+
+ Assets
+
+
+
+
+ Assets
+
+
+
+
+
+
+
+
+
+
+
+
+ )
+}
+
+AssetsPage.fragments = {
+ assets: gql`
+ fragment AssetsPage_assets on Asset {
+ id
+ name
+ metadata
+ }
+ `
+}
+
+AssetsPage = withMutation(gql`
+ mutation CreateAssetMutation($input: CreateAssetInput!) {
+ asset: createAsset(input: $input) {
+ ...AssetsPage_assets
+ }
+ }
+
+ ${AssetsPage.fragments.assets}
+`, {
+ inputFilter: gql`
+ fragment CreateAssetInput on CreateAssetInput {
+ projectId
+ name
+ file
+ }
+ `,
+ mode: MutationResponseModes.PREPEND
+})(AssetsPage)
+
+AssetsPage = withMutation(gql`
+ mutation UpdateAssetMutation($id: ID!, $input: UpdateAssetInput!) {
+ updateAsset(id: $id, input: $input) {
+ ...AssetsPage_assets
+ }
+ }
+
+ ${AssetsPage.fragments.assets}
+`, {
+ inputFilter: gql`
+ fragment UpdateAssetInput on UpdateAssetInput {
+ name
+ }
+ `,
+ optimistic: { mode: OptimisticResponseModes.UPDATE, response: { __typename: 'Asset' } }
+})(AssetsPage)
+
+AssetsPage = withMutation(gql`
+ mutation ($id: ID!) {
+ destroyAsset(id: $id) {
+ id
+ }
+ }
+`, {
+ optimistic: { mode: OptimisticResponseModes.DESTROY, response: { __typename: 'Asset' } },
+ mode: MutationResponseModes.DELETE,
+ successAlert: ({ input }) => ({
+ message: `Successfully deleted asset: ${Asset.getFullName(input)}`
+ })
+})(AssetsPage)
+
+AssetsPage = withQuery(gql`
+ query AssetsPageQuery($projectId: ID!) {
+ assets(projectId: $projectId) {
+ ...AssetsPage_assets
+ }
+ }
+
+ ${AssetsPage.fragments.assets}
+`, {
+ options: ({ match }) => ({
+ variables: {
+ projectId: match.params.projectId
+ }
+ })
+})(AssetsPage)
+
+export default withConfirmation()(AssetsPage)
diff --git a/ui/src/components/pages/ConfirmPage.js b/ui/src/components/pages/ConfirmPage.js
new file mode 100644
index 0000000..6ba75c6
--- /dev/null
+++ b/ui/src/components/pages/ConfirmPage.js
@@ -0,0 +1,324 @@
+import gql from 'graphql-tag'
+import Helmet from 'react-helmet-async'
+import React, { Component, Fragment } from 'react'
+
+import Body from 'components/onboarding/Body'
+import Card from 'components/onboarding/Card'
+import CardFootnote from 'components/onboarding/CardFootnote'
+import CardWrapper from 'components/onboarding/CardWrapper'
+import ConfirmUserForm from 'components/onboarding/forms/ConfirmUserForm'
+import Content from 'components/onboarding/Content'
+import FilledButton from 'components/buttons/FilledButton'
+import Header from 'components/onboarding/Header'
+import LoaderView from 'components/LoaderView'
+import Spacer from 'components/Spacer'
+import { CardHeading, CardText, Text, TextLink } from 'components/onboarding/typography'
+import { withClientMutation, withMutation, withQuery } from 'lib/data'
+
+import SET_TOKEN from 'mutations/session'
+
+/* eslint-disable jsx-a11y/anchor-is-valid */
+
+class ConfirmPage extends Component {
+ constructor() {
+ super()
+
+ this.state = {
+ resendConfirmationStatus: ConfirmPage.resendConfirmationStatuses.NONE
+ }
+ }
+
+ handleLoginClick = () => {
+ const { history } = this.props
+
+ history.push('/login')
+ }
+
+ handleFormSubmit = (values) => {
+ const { confirmUser, createSession, setToken } = this.props
+
+ return confirmUser(values, {
+ onSuccess: ({ data: { confirmUser: { email } } }) => (
+ createSession({ email, password: values.password }, {
+ onSuccess: sessionResponse => setToken({ token: sessionResponse.data.session.token })
+ })
+ )
+ })
+ }
+
+ handleResendConfirmation = () => {
+ const { resendConfirmation, unconfirmedUser } = this.props
+
+ const email = unconfirmedUser ? unconfirmedUser.email : null
+
+ this.setState({
+ resendConfirmationStatus: ConfirmPage.resendConfirmationStatuses.LOADING
+ })
+
+ return resendConfirmation({ email }, {
+ onSuccess: () => this.setState({
+ resendConfirmationStatus: ConfirmPage.resendConfirmationStatuses.SUCCESS
+ }),
+ onFailure: () => this.setState({
+ resendConfirmationStatus: ConfirmPage.resendConfirmationStatuses.ERROR
+ })
+ })
+ }
+
+ renderLoadingContent = () => (
+
+ )
+
+ renderErrorContent = () => {
+ const { error } = this.props
+
+ const errorMessage = (
+ error.networkError
+ && error.networkError.result
+ && error.networkError.result.errors
+ && error.networkError.result.errors[0]
+ && error.networkError.result.errors[0].message
+ ) || 'Your confirmation token is invalid.'
+
+ return (
+
+
+ Uh oh!
+
+
+ {errorMessage}
+
+
+ Please try again later.
+
+
+ )
+ }
+
+ renderNotFound = () => (
+
+
+ Uh oh!
+
+
+ Your confirmation token is invalid.
+
+
+ Please check if the link is correct. If you have used this link before, try
+ logging in
+ .
+
+
+ )
+
+ renderConfirmedContent = () => (
+
+
+ Success!
+
+
+ You have confirmed your email.
+
+
+
+
+ )
+
+ renderConfirmationExpiredContent = () => {
+ const { resendConfirmationStatus } = this.state
+
+ return (
+
+
+ Uh oh!
+
+
+ Your confirmation token has expired.
+
+
+
+
+ {resendConfirmationStatus === ConfirmPage.resendConfirmationStatuses.NONE && (
+
+ )}
+
+ {resendConfirmationStatus === ConfirmPage.resendConfirmationStatuses.LOADING && (
+
+ Resending...
+
+ )}
+
+ {resendConfirmationStatus === ConfirmPage.resendConfirmationStatuses.ERROR && (
+
+ Failed to resend confirmation email.
+ Try again.
+
+ )}
+
+ {resendConfirmationStatus === ConfirmPage.resendConfirmationStatuses.SUCCESS && (
+
+ Re-sent confirmation email. If you don’t receive it within 5 minutes,
+ check your spam folder or
+ try again
+ .
+
+ )}
+
+ )
+ }
+
+ renderFormContent = () => {
+ const { match: { params: { token } } } = this.props
+
+ const initialValues = {
+ confirmationToken: token
+ }
+
+ return (
+
+ )
+ }
+
+ renderContent = () => {
+ const { error, loading, unconfirmedUser } = this.props
+
+ if (loading) {
+ return this.renderLoadingContent()
+ }
+
+ if (error) {
+ return this.renderErrorContent()
+ }
+
+ if (!unconfirmedUser) {
+ return this.renderNotFound()
+ }
+
+ if (unconfirmedUser.isConfirmed) {
+ return this.renderConfirmedContent()
+ }
+
+ if (unconfirmedUser.isConfirmationExpired) {
+ return this.renderConfirmationExpiredContent()
+ }
+
+ return this.renderFormContent()
+ }
+
+ renderCardFootnoteContent = () => {
+ const { loading, unconfirmedUser } = this.props
+
+ if (loading || !unconfirmedUser) {
+ return null
+ }
+
+ const contactSupport = (
+
+ Having trouble?
+
+ Contact support
+
+ for help.
+
+ )
+
+ const termsOfService = (
+
+ By clicking submit, you are agreeing to the
+
+ Terms of Service
+
+ .
+
+ )
+
+ if (!unconfirmedUser.isConfirmed && !unconfirmedUser.isConfirmationExpired) {
+ return termsOfService
+ }
+
+ return contactSupport
+ }
+
+ render() {
+ return (
+
+
+
+ Confirm Email
+
+
+
+
+
+
+
+ You are
+
+ almost there...
+
+
+
+
+ {this.renderContent()}
+
+
+ {this.renderCardFootnoteContent()}
+
+
+
+
+ )
+ }
+}
+
+ConfirmPage.resendConfirmationStatuses = Object.freeze({
+ NONE: 'none',
+ LOADING: 'loading',
+ ERROR: 'error',
+ SUCCESS: 'success'
+})
+
+ConfirmPage = withClientMutation(SET_TOKEN)(ConfirmPage)
+
+ConfirmPage = withMutation(gql`
+ mutation CreateSessionMutation($input: CreateSessionInput!) {
+ session: createSession(input: $input) {
+ id
+ token
+ }
+ }
+`)(ConfirmPage)
+
+ConfirmPage = withMutation(gql`
+ mutation ConfirmUserMutation($input: ConfirmUserInput!) {
+ confirmUser(input: $input) {
+ id
+ email
+ }
+ }
+`)(ConfirmPage)
+
+ConfirmPage = withMutation(gql`
+ mutation ResendConfirmation($input: ResendConfirmationInput!) {
+ resendConfirmation(input: $input) {
+ id
+ }
+ }
+`)(ConfirmPage)
+
+ConfirmPage = withQuery(gql`
+ query ConfirmPageQuery($confirmationToken: String!) {
+ unconfirmedUser(confirmationToken: $confirmationToken) {
+ id
+ email
+ isConfirmed
+ isConfirmationExpired
+ }
+ }
+`, {
+ options: ({ match }) => ({
+ variables: { confirmationToken: match.params.token }
+ })
+})(ConfirmPage)
+
+export default ConfirmPage
diff --git a/ui/src/components/pages/DashboardPage.js b/ui/src/components/pages/DashboardPage.js
new file mode 100644
index 0000000..2b277e3
--- /dev/null
+++ b/ui/src/components/pages/DashboardPage.js
@@ -0,0 +1,22 @@
+import Helmet from 'react-helmet-async'
+import React, { Fragment } from 'react'
+
+import { PageTitle } from 'components/internal/typography'
+
+function DashboardPage() {
+ return (
+
+
+
+ Dashboard
+
+
+
+
+ Dashboard
+
+
+ )
+}
+
+export default DashboardPage
diff --git a/ui/src/components/pages/EntitiesPage.js b/ui/src/components/pages/EntitiesPage.js
new file mode 100644
index 0000000..b4083d8
--- /dev/null
+++ b/ui/src/components/pages/EntitiesPage.js
@@ -0,0 +1,240 @@
+import _ from 'lodash'
+import gql from 'graphql-tag'
+import Helmet from 'react-helmet-async'
+import injectSheet from 'react-jss'
+import React, { Fragment } from 'react'
+
+import EntitySidePane from 'components/internal/sidePanes/EntitySidePane'
+import FilledButton from 'components/buttons/FilledButton'
+import IconButton from 'components/internal/buttons/IconButton'
+import ItemBar from 'components/ItemBar'
+import Loader from 'components/internal/Loader'
+import Table from 'components/internal/dataTable/Table'
+import Tag from 'components/internal/Tag'
+import useSidePane from 'lib/hooks/useSidePane'
+import withConfirmation from 'components/internal/decorators/withConfirmation'
+import { MutationResponseModes, OptimisticResponseModes, withMutation, withQuery } from 'lib/data'
+import { CellLabel, CellText, CellTitle, PageTitle } from 'components/internal/typography'
+
+function EntitiesPage({
+ confirm, createEntity, updateEntity, destroyEntity, history, loading, entities, match
+}) {
+ const [ entity, isEntitySidePaneOpen, openEntitySidePane, closeEntitySidePane ] = useSidePane()
+ const { params: { teamId, projectId } } = match
+
+ const goToEntity = id => history.push(`/teams/${teamId}/projects/${projectId}/entities/${id}`)
+
+ const handleFormSubmit = (values) => {
+ if (values.id) {
+ return updateEntity(values, { onSuccess: () => closeEntitySidePane() })
+ }
+
+ return createEntity(
+ values, { onSuccess: (
+ { data: { createEntity: { id } } }
+ ) => { goToEntity(id) } }
+ )
+ }
+
+ const labelRenderer = ({ record: { label } }) => (
+
+ Label
+ {label}
+
+ )
+
+ const nameRenderer = ({ record: { name } }) => (
+
+ Name
+ {name}
+
+ )
+
+ const parentNameRenderer = ({ record: { parentId } }) => {
+ const parent = entities.find(e => e.id === parentId)
+
+ return (
+
+ Parent
+ {(parent && parent.label) || '-'}
+
+ )
+ }
+
+ const isSingletonRenderer = ({ record: { singleton } }) => (
+
{singleton ? 'SINGLETON' : 'COLLECTION'}
+ )
+
+ const manageButtonRenderer = ({ record: { id } }) => {
+ const goToRecords = () => history.push(`/teams/${teamId}/projects/${projectId}/entities/${id}/records`)
+
+ return (
+
+ goToEntity(id)} />
+
+
+ )
+ }
+
+ const columns = [
+ { dataKey: 'label', bordered: false, flexGrow: 1, cellRenderer: labelRenderer },
+ { dataKey: 'name', flexGrow: 1, cellRenderer: nameRenderer },
+ { dataKey: 'parentId', flexGrow: 1, cellRenderer: parentNameRenderer },
+ { dataKey: 'singleton', width: 200, uppercase: true, cellRenderer: isSingletonRenderer },
+ { dataKey: 'manageButton', width: 350, cellRenderer: manageButtonRenderer }
+ ]
+
+ const actions = [
+ { icon: 'edit', onClick: record => openEntitySidePane(record) },
+ {
+ icon: 'trash',
+ onClick: record => confirm({
+ description: `By clicking on Confirm, you will delete the entity: ${record.name}`,
+ onConfirmClick: () => destroyEntity(record)
+ })
+ }
+ ]
+
+ const rootEntities = _.sortBy((entities || []).filter(e => !e.parentId), [ 'label' ])
+ const childEntities = _.sortBy((entities || []).filter(e => !!e.parentId), [ 'parentId', 'label' ])
+
+ return (
+
+
+
+ Entities
+
+
+
+
+ Entities
+
+
+ openEntitySidePane()} />
+
+ openEntitySidePane()
+ }}
+ />
+
+
+
+
+
+ )
+}
+
+EntitiesPage = injectSheet(({ colors, typography }) => ({
+ entityName: {
+ ...typography.semibold,
+
+ alignItems: 'center',
+ color: colors.text_dark,
+ display: 'flex',
+ lineHeight: 1
+ }
+}))(EntitiesPage)
+
+EntitiesPage.fragments = {
+ entities: gql`
+ fragment EntitiesPage_entities on Entity {
+ id
+ parentId
+ name
+ label
+ singleton
+ }
+ `
+}
+
+EntitiesPage = withMutation(gql`
+ mutation CreateEntityMutation($input: CreateEntityInput!) {
+ createEntity(input: $input) {
+ ...EntitiesPage_entities
+ }
+ }
+
+ ${EntitiesPage.fragments.entities}
+`, {
+ inputFilter: gql`
+ fragment CreateEntityInput on CreateEntityInput {
+ projectId
+ parentId
+ name
+ label
+ singleton
+ }
+ `,
+ mode: MutationResponseModes.APPEND
+})(EntitiesPage)
+
+EntitiesPage = withMutation(gql`
+ mutation UpdateEntityMutation($id: ID!, $input: UpdateEntityInput!) {
+ updateEntity(id: $id, input: $input) {
+ ...EntitiesPage_entities
+ }
+ }
+
+ ${EntitiesPage.fragments.entities}
+`, {
+ inputFilter: gql`
+ fragment UpdateEntityInput on UpdateEntityInput {
+ parentId
+ name
+ label
+ singleton
+ }
+ `,
+ optimistic: { mode: OptimisticResponseModes.UPDATE, response: { __typename: 'Entity' } }
+})(EntitiesPage)
+
+EntitiesPage = withMutation(gql`
+ mutation DestroyEntityMutation($id: ID!) {
+ destroyEntity(id: $id) {
+ id
+ }
+ }
+`, {
+ optimistic: { mode: OptimisticResponseModes.DESTROY, response: { __typename: 'Entity' } },
+ mode: MutationResponseModes.DELETE,
+ successAlert: ({ input: { name } }) => ({
+ message: `Successfully deleted entity: ${name}`
+ })
+})(EntitiesPage)
+
+EntitiesPage = withQuery(gql`
+ query EntitiesPageQuery($projectId: ID!) {
+ entities(projectId: $projectId) {
+ ...EntitiesPage_entities
+ }
+ }
+
+ ${EntitiesPage.fragments.entities}
+`, {
+ options: ({ match }) => ({
+ variables: {
+ projectId: match.params.projectId
+ }
+ })
+})(EntitiesPage)
+
+export default withConfirmation()(EntitiesPage)
diff --git a/ui/src/components/pages/EntityPage.js b/ui/src/components/pages/EntityPage.js
new file mode 100644
index 0000000..209ec05
--- /dev/null
+++ b/ui/src/components/pages/EntityPage.js
@@ -0,0 +1,85 @@
+import gql from 'graphql-tag'
+import Helmet from 'react-helmet-async'
+import injectSheet from 'react-jss'
+import React, { Fragment } from 'react'
+import { Redirect, Route, Switch } from 'react-router-dom'
+
+import FieldsPage from 'components/pages/FieldsPage'
+import Loader from 'components/internal/Loader'
+import RecordsPage from 'components/pages/RecordsPage'
+import Spacer from 'components/Spacer'
+import withConfirmation from 'components/internal/decorators/withConfirmation'
+import { BackLink, PageSubTitle } from 'components/internal/typography'
+import { withQuery } from 'lib/data'
+import { TabLink, TabList } from 'components/internal/tab'
+
+function EntityPage({ entity = {}, loading, match }) {
+ const { params: { teamId, projectId } } = match
+
+ if (loading) {
+ return
+ }
+
+ return (
+
+
+
+ Entity:
+ {' '}
+ {entity.label || ''}
+
+
+
+
+ Back to Entities
+
+
+ Entity:
+ {' '}
+ {entity.label}
+
+
+
+ Structure
+ Content
+
+
+
+
+
+
+
+
+
+
+ )
+}
+
+EntityPage = injectSheet(({ colors, typography }) => ({
+ entityName: {
+ ...typography.semibold,
+
+ alignItems: 'center',
+ color: colors.text_dark,
+ display: 'flex',
+ lineHeight: 1
+ }
+}))(EntityPage)
+
+EntityPage = withQuery(gql`
+ query EntityPageQuery($entityId: ID!) {
+ entity(id: $entityId) {
+ id
+ label
+ name
+ }
+ }
+`, {
+ options: ({ match }) => ({
+ variables: {
+ entityId: match.params.entityId
+ }
+ })
+})(EntityPage)
+
+export default withConfirmation()(EntityPage)
diff --git a/ui/src/components/pages/FieldsPage.js b/ui/src/components/pages/FieldsPage.js
new file mode 100644
index 0000000..995415b
--- /dev/null
+++ b/ui/src/components/pages/FieldsPage.js
@@ -0,0 +1,279 @@
+import _ from 'lodash'
+import gql from 'graphql-tag'
+import injectSheet from 'react-jss'
+import React, { Fragment } from 'react'
+
+import FieldSidePane from 'components/internal/sidePanes/FieldSidePane'
+import IconButton from 'components/internal/buttons/IconButton'
+import Loader from 'components/internal/Loader'
+import Spacer from 'components/Spacer'
+import Table from 'components/internal/dataTable/Table'
+import Tag from 'components/internal/Tag'
+import useSidePane from 'lib/hooks/useSidePane'
+import withConfirmation from 'components/internal/decorators/withConfirmation'
+import { Field } from 'models'
+import { MutationResponseModes, OptimisticResponseModes, withMutation, withQuery } from 'lib/data'
+import { CellLabel, CellText, CellTitle } from 'components/internal/typography'
+
+function FieldsPage({
+ confirm,
+ createField,
+ destroyField,
+ entities,
+ fields,
+ loading,
+ match,
+ sortFields,
+ updateField
+}) {
+ const [ field, isFieldSidePaneOpen, openFieldSidePane, closeFieldSidePane ] = useSidePane()
+
+ const rootFields = (fields || []).filter(Field.isRoot)
+
+ const handleFormSubmit = (values) => {
+ if (values.id) {
+ return updateField(values, { onSuccess: () => closeFieldSidePane() })
+ }
+
+ return createField(values, { onSuccess: () => closeFieldSidePane() })
+ }
+
+ const processFields = (entityFields = []) => entityFields.map((entityField) => {
+ const newField = _.omit(entityField, [ '__typename' ])
+ const childFields = fields.filter(f => f.parentId === entityField.id)
+
+ if (childFields.length && entityField.dataType === 'key_value') {
+ newField.children = processFields(childFields)
+ }
+
+ if (childFields.length && entityField.dataType === 'array') {
+ const subChildFields = fields.filter(f => f.parentId === childFields[0].id)
+
+ newField.children = processFields(subChildFields)
+ }
+
+ return newField
+ })
+
+ const labelRenderer = ({ record: { label } }) => (
+
+ Label
+ {label}
+
+ )
+
+ const nameRenderer = ({ record: { name } }) => (
+
+ Name
+ {name}
+
+ )
+
+ const dataTypeRenderer = ({ record: { dataType } }) => (
+
{Field.dataTypeList.find(dt => dt.value === dataType).label}
+ )
+
+ const columns = [
+ { dataKey: 'label', bordered: false, flexGrow: 1, cellRenderer: labelRenderer },
+ { dataKey: 'name', flexGrow: 1, cellRenderer: nameRenderer },
+ { dataKey: 'dataType', flexGrow: 1, cellRenderer: dataTypeRenderer }
+ ]
+
+ const actions = [
+ { icon: 'edit', onClick: record => openFieldSidePane(record) },
+ {
+ icon: 'trash',
+ onClick: record => confirm({
+ description: `By clicking on Confirm, you will delete the field: ${record.name}`,
+ onConfirmClick: () => destroyField(record)
+ })
+ }
+ ]
+
+ const processedFields = _.sortBy(processFields(rootFields), [ 'position' ])
+ if (loading) {
+ return
+ }
+
+ return (
+
+ openFieldSidePane()} />
+
+
+ openFieldSidePane()
+ }}
+ />
+
+ sortFields({
+ fields: sortedFields.map((f, index) => ({ id: f.id, position: index }))
+ })}
+ />
+
+
+
+ )
+}
+
+FieldsPage = injectSheet(({ colors, typography }) => ({
+ entityName: {
+ ...typography.semibold,
+
+ alignItems: 'center',
+ color: colors.text_dark,
+ display: 'flex',
+ lineHeight: 1
+ }
+}))(FieldsPage)
+
+FieldsPage.fragments = {
+ fields: gql`
+ fragment FieldsPage_fields on Field {
+ id
+ dataType
+ validations
+ settings
+ defaultValue
+ elementType
+ entityId
+ hint
+ label
+ name
+ parentId
+ position
+ referencedEntityId
+ }
+ `
+}
+
+FieldsPage = withMutation(gql`
+ mutation SortFieldsMutation($input: SortFieldsInput!) {
+ sortFields(input: $input) {
+ id
+ name
+ position
+ }
+ }
+`)(FieldsPage)
+
+FieldsPage = withMutation(gql`
+ mutation CreateFieldMutation($input: CreateFieldInput!) {
+ createField(input: $input) {
+ ...FieldsPage_fields
+ }
+ }
+
+ ${FieldsPage.fragments.fields}
+`, {
+ inputFilter: gql`
+ fragment CreateFieldInput on CreateFieldInput {
+ dataType
+ defaultValue
+ elementType
+ validations
+ settings
+ entityId
+ hint
+ label
+ name
+ position
+ referencedEntityId
+ children
+ }
+ `,
+ mode: MutationResponseModes.APPEND
+})(FieldsPage)
+
+FieldsPage = withMutation(gql`
+ mutation UpdateFieldMutation($id: ID!, $input: UpdateFieldInput!) {
+ updateField(id: $id, input: $input) {
+ ...FieldsPage_fields
+ }
+ }
+
+ ${FieldsPage.fragments.fields}
+`, {
+ inputFilter: gql`
+ fragment UpdateFieldInput on UpdateFieldInput {
+ id
+ children
+ dataType
+ validations
+ settings
+ defaultValue
+ elementType
+ hint
+ label
+ name
+ position
+ referencedEntityId
+ }
+ `,
+ mode: MutationResponseModes.CUSTOM,
+ updateData: ({ cachedData, responseRecords }) => {
+ const currentRecords = cachedData.fields
+ cachedData.fields = _.unionWith(currentRecords, responseRecords, _.isEqual)
+ }
+})(FieldsPage)
+
+FieldsPage = withMutation(gql`
+ mutation DestroyFieldMutation($id: ID!) {
+ destroyField(id: $id) {
+ id
+ }
+ }
+`, {
+ optimistic: { mode: OptimisticResponseModes.DESTROY, response: { __typename: 'Field' } },
+ mode: MutationResponseModes.DELETE,
+ successAlert: ({ input: { name } }) => ({
+ message: `Successfully deleted field: ${name}`
+ })
+})(FieldsPage)
+
+FieldsPage = withQuery(gql`
+ query FieldsPageQuery($entityId: ID!, $projectId: ID!) {
+ fields(entityId: $entityId) {
+ ...FieldsPage_fields
+ }
+
+ entity(id: $entityId) {
+ id
+ label
+ name
+ }
+
+ entities(projectId: $projectId) {
+ id
+ label
+ name
+ }
+ }
+
+ ${FieldsPage.fragments.fields}
+`, {
+ options: ({ match }) => ({
+ variables: {
+ entityId: match.params.entityId,
+ projectId: match.params.projectId
+ }
+ })
+})(FieldsPage)
+
+export default withConfirmation()(FieldsPage)
diff --git a/ui/src/components/pages/ForgotPasswordPage.js b/ui/src/components/pages/ForgotPasswordPage.js
new file mode 100644
index 0000000..2a76120
--- /dev/null
+++ b/ui/src/components/pages/ForgotPasswordPage.js
@@ -0,0 +1,69 @@
+import gql from 'graphql-tag'
+import React, { Fragment } from 'react'
+
+import Body from 'components/onboarding/Body'
+import Card from 'components/onboarding/Card'
+import CardFootnote from 'components/onboarding/CardFootnote'
+import CardWrapper from 'components/onboarding/CardWrapper'
+import Content from 'components/onboarding/Content'
+import ForgotPasswordForm from 'components/onboarding/forms/ForgotPasswordForm'
+import Header from 'components/onboarding/Header'
+import Helmet from 'react-helmet-async'
+import { Text, TextLink } from 'components/onboarding/typography'
+import { withMutation } from 'lib/data'
+
+function ForgotPasswordPage({ forgotPassword }) {
+ const handleSubmit = (values, formApi) => (
+ forgotPassword(values, { onSuccess: () => formApi.reset() })
+ )
+
+ return (
+
+
+
+ Forgot password
+
+
+
+
+
+
+
+ Don't worry.
+
+ It happens to the best of us.
+
+
+
+
+
+
+
+
+
+ Remember your password?
+
+ Login here
+
+ .
+
+
+
+
+
+ )
+}
+
+ForgotPasswordPage = withMutation(gql`
+ mutation ForgotPasswordMutation($input: ForgotPasswordInput!) {
+ forgotPassword(input: $input) {
+ success
+ }
+ }
+`, {
+ successAlert: ({ input }) => ({
+ message: `We have sent a reset password link to ${input.email}.`
+ })
+})(ForgotPasswordPage)
+
+export default ForgotPasswordPage
diff --git a/ui/src/components/pages/HomePage.js b/ui/src/components/pages/HomePage.js
new file mode 100644
index 0000000..cd354da
--- /dev/null
+++ b/ui/src/components/pages/HomePage.js
@@ -0,0 +1,80 @@
+import gql from 'graphql-tag'
+import injectSheet from 'react-jss'
+import React, { Fragment } from 'react'
+import { Link } from 'react-router-dom'
+
+import Footer from 'components/external/Footer'
+import GridContainer from 'components/external/GridContainer'
+import GridItem from 'components/external/GridItem'
+import Header from 'components/external/Header'
+import Logo from 'components/Logo'
+import { Heading } from 'components/external/typography'
+import { withMutation } from 'lib/data'
+
+function HomePage({ classes }) {
+ return (
+
+
+
+
+
+
+
+
+
+
+ Headless CMS for React.js
+
+
+
+
+
+
+
+
+
+ )
+}
+
+HomePage = injectSheet(({ gradients, units }) => ({
+ hero: {
+ height: units.externalHeroHeight,
+ position: 'relative',
+ zIndex: 0,
+
+ '&::before': {
+ backgroundImage: gradients.externalHeroBackground,
+ backgroundRepeat: 'no-repeat',
+ backgroundSize: 'contain',
+ content: '" "',
+ minWidth: 3000,
+ position: 'absolute',
+ top: 0,
+ right: 0,
+ bottom: 0,
+ left: '50%',
+ transform: 'translateX(-50%)',
+ zIndex: -1
+ }
+ },
+ footerWrapper: {
+ overflow: 'hidden',
+ position: 'fixed',
+ right: 0,
+ bottom: 0,
+ left: 0,
+ zIndex: 0
+ }
+}))(HomePage)
+
+HomePage = withMutation(gql`
+ mutation CreateUserMutation($input: CreateUserInput!) {
+ createUser(input: $input) {
+ id
+ email
+ isConfirmationExpired
+ }
+ }
+`)(HomePage)
+
+export default HomePage
diff --git a/ui/src/components/pages/LoginPage.js b/ui/src/components/pages/LoginPage.js
new file mode 100644
index 0000000..752030c
--- /dev/null
+++ b/ui/src/components/pages/LoginPage.js
@@ -0,0 +1,70 @@
+import gql from 'graphql-tag'
+import React, { Fragment } from 'react'
+
+import Body from 'components/onboarding/Body'
+import Card from 'components/onboarding/Card'
+import CardFootnote from 'components/onboarding/CardFootnote'
+import CardWrapper from 'components/onboarding/CardWrapper'
+import Content from 'components/onboarding/Content'
+import Header from 'components/onboarding/Header'
+import Helmet from 'react-helmet-async'
+import LoginForm from 'components/onboarding/forms/LoginForm'
+import { Text, TextLink } from 'components/onboarding/typography'
+import { withClientMutation, withMutation } from 'lib/data'
+
+import SET_TOKEN from 'mutations/session'
+
+function LoginPage({ createSession, setToken }) {
+ const handleSubmit = values => (
+ createSession(values, {
+ onSuccess: response => setToken({ token: response.data.session.token })
+ })
+ )
+
+ return (
+
+
+
+ Log in
+
+
+
+
+
+
+
+ Welcome back. We missed you!
+
+
+
+
+
+
+
+
+
+ Forgot your password?
+
+ Reset it now
+
+ .
+
+
+
+
+
+ )
+}
+
+LoginPage = withMutation(gql`
+ mutation CreateSessionMutation($input: CreateSessionInput!) {
+ session: createSession(input: $input) {
+ id
+ token
+ }
+ }
+`)(LoginPage)
+
+LoginPage = withClientMutation(SET_TOKEN)(LoginPage)
+
+export default LoginPage
diff --git a/ui/src/components/pages/PrivacyPolicyPage.js b/ui/src/components/pages/PrivacyPolicyPage.js
new file mode 100644
index 0000000..5666af6
--- /dev/null
+++ b/ui/src/components/pages/PrivacyPolicyPage.js
@@ -0,0 +1,637 @@
+import Helmet from 'react-helmet-async'
+import React, { Fragment } from 'react'
+
+import GridContainer from 'components/external/GridContainer'
+import GridItem from 'components/external/GridItem'
+import {
+ PageHeading,
+ PageLink,
+ PageList,
+ PageListItem,
+ PageSubHeading,
+ PageText
+} from 'components/external/typography'
+
+const lastUpdated = 'August 15, 2019'
+
+function PrivacyPolicyPage() {
+ return (
+
+
+
+ Privacy Policy
+
+
+
+
+
+
+ Privacy Policy
+
+
+
+ Last Updated:
+ {' '}
+ {lastUpdated}
+
+
+
+ This privacy policy governs the manner in which KeepWorks Technologies Pvt Ltd.
+ (“KeepWorks”, “us”, “we”, or “our”) uses and protects any information that you provide
+ voluntarily to KeepWorks.
+
+
+
+ We are devoted to ensuring that your privacy is protected. If we require you to provide
+ certain information which helps in your identification when accessing your account
+ information, you can be assured that such information will be used only in accordance
+ with this privacy policy.
+
+
+
+ We may change this policy from time to time by updating this page. The changes in policy
+ due to any change made shall not compromise on the protection of personal information
+ that you have. We would require you to check this page regularly to ensure that you are
+ happy with the changes (if any). This policy is effective from
+ {' '}
+ {lastUpdated}
+ .
+
+
+
+ We have a firm policy of protecting the confidentiality and security of information that
+ we collect from our users. We do not share your non-public personal information with
+ unaffiliated third parties. Information is only shared with your consent except for the
+ specific purposes given below, in accordance with all applicable laws. We require you to
+ please read this policy carefully; it gives you important information about how we
+ handle your personal information.
+
+
+
+ KeepWorks may collect the following information when you sign-up as a new user:
+
+
+
+
+ Personal information, such as name and e-mail address
+
+
+ Content that you post in your account
+
+
+ Cookies
+
+
+ Log Files
+
+
+ Geo-Location information
+
+
+
+
+ We may collect personal information from users in a variety of ways, including, but not
+ limited to, when users visit our site, subscribe to newsletters, provide e-mail
+ addresses, fill out a form and in connection with other activities, services or features
+ that we make available on our site. Users may be asked for, as appropriate, name, e-mail
+ address, phone number, etc. Users may, however, visit our site anonymously. We collect
+ personal information from you only if you voluntarily submit such information to us. You
+ can always refuse to provide any personal information to us, however this may prevent
+ you from engaging in certain of our site related activities.
+
+
+
+ A Log-File is a file that keeps a registry of events, processes, messages and
+ communication between various communicating software applications and the operating
+ system. An IP address is personal information and you must let us know if you do not
+ want us to keep a record of your email id.
+
+
+
+ The sole objective of gathering this information is to understand and evaluate your
+ needs and:
+
+
+
+
+ To improve customer service: The information you provide helps us respond to your
+ customer service requests and support needs more efficiently.
+
+
+ To personalize user experience: We may use information in the aggregate to understand
+ how our users as a group use the services and resources provided on our site.
+
+
+ To improve our site: We may use feedback you provide to improve our products and/or
+ services.
+
+
+ To process payments: We may use the information users provide about themselves when
+ purchasing paid products and/or services within the service. We do not share this
+ information with third parties, except to the extent necessary to provide the service.
+
+
+
+
+ Disclosure of your information:
+
+
+
+ We may be required to disclose an individual’s personal information in response to a
+ lawful request by public authorities, including meeting national security or law
+ enforcement requirements. In response to a verified request by law enforcement or other
+ government officials relating to a criminal investigation or alleged illegal activity,
+ we can (and you authorize us) disclose your name, city, state, telephone number, e-mail
+ address, User ID history, fraud complaints and usage history, without a
+ summons/subpoena, in connection with an investigation of fraud, intellectual property
+ infringement, piracy or other unlawful activity.
+
+
+
+ In the event we undergo a business transaction such as a merger, acquisition or sale of
+ all or a portion of our assets, your personal information may be among the assets
+ transferred or examined during the due diligence process. You acknowledge and consent
+ that such transfers may occur and are permitted by this Privacy Policy, and that any
+ acquirer of our assets may continue to process your personal information as set forth in
+ this Privacy Policy. If our information practices change at any time in the future, we
+ will post the policy changes to this Site so that you may opt out of the new information
+ practices. We suggest that you check the Website periodically if you are concerned about
+ how your information is used.
+
+
+
+ KeepWorks Technologies Pvt Ltd. may disclose your personal data in the good faith belief
+ that such action is necessary to:
+
+
+
+
+ To conform to legal obligations
+
+
+ To provide for protection of personal safety of users of the service or the public
+
+
+ To provide for protection against legal disability
+
+
+ To prevent or investigate any wrongdoing in connection with the service
+
+
+ To protect and defend the rights or property of KeepWorks Technologies Pvt Ltd.
+
+
+
+
+ In cases of onward transfer to third parties of data of EU and Swiss individuals
+ received pursuant to the Privacy Shield Principles, KeepWorks is potentially liable.
+
+
+
+ We are committed to ensuring that your information is secure with us. In order to
+ prevent unauthorised access or disclosure we have put in place suitable physical,
+ electronic and managerial procedures to safeguard and secure the information we collect
+ online.
+
+
+
+ Cookies
+
+
+
+ We use traffic log cookies to identify which pages are being
+ used. This helps us analyse data about webpage traffic and improve our website in order
+ to tailor it to customer needs. We only use this information for statistical analysis
+ purposes and then the data is removed from the system.
+
+
+
+ Overall, cookies help us provide you with a better website, by enabling us to monitor
+ which pages you find useful and which you do not. A cookie in no way gives us access to
+ your computer or any information about you, other than the data you choose to share with
+ us.
+
+
+
+ Cookies are files with a small amount of data which may include an anonymous unique
+ identifier. Cookies are sent to your browser from a website and stored on your device.
+
+
+
+ Examples of cookies we use:
+
+
+
+
+ Session cookies: Also called a transient cookie, a cookie that is erased when the user
+ closes the web browser. These cookies are stored in temporary memory and not returned
+ after the browser is closed. These cookies do not collect information from the user’s
+ computer; they typically store information in the form of a session identification
+ which does not personally identify the user. We use session cookies to operate our
+ service.
+
+
+ Preference cookies: When using or browsing the website, our online services will
+ remember preferences you make; this improves your experience and makes using the
+ website, simpler, easier and more personal to you. We use preference cookies to
+ remember your preferences and various settings.
+
+
+ Security cookies: We use security cookies for security purposes. These cookies help to
+ prevent important personal information to be revealed or shared or used by third party
+ sources.
+
+
+
+
+ You have the ability to accept or decline cookies. Most web browsers automatically
+ accept cookies but you can usually modify your browser settings to decline cookies if
+ you prefer. This data from cookies is used to deliver customized content and promotions
+ within the website and service portal to customers whose behaviour indicates that they
+ are interested in a particular subject area. If you choose to decline cookies, you may
+ not be able to fully experience the interactive features of our Site.
+
+
+
+ Log Files
+
+
+
+ We may collect demographic information, such as your post code, age, gender,
+ preferences, interests and favourites using log files that are not associated with your
+ name or other personal information. There is also information about your computer
+ hardware and software that is automatically collected by us. This information can
+ include: your IP address, browser type, domain names, internet service provider (ISP),
+ the files viewed on our site (e.g., HTML pages, graphics, etc.), operating system,
+ clickstream data, access times and referring website addresses. This information is used
+ to maintain the quality of the Subscription Service, and to provide general statistics
+ regarding use of the Website. For these purposes, we do link this
+ automatically-collected data to Personal Information such as name, email address,
+ address, and phone number.
+
+
+
+ Legal basis for Processing Personal Data under the General Data Protection Regulation
+ (GDPR):
+
+
+
+ If you are from the European Economic Area (EEA), KeepWorks Technologies Pvt Ltd Legal
+ basis for collecting and using personal information described in this Privacy Policy
+ depends on the personal data we collect and the specific context in which we collect it.
+ The Data Protection Act and GDPR requires us to manage personal information in
+ accordance with Data Protection Principles and in particular requires us to process your
+ personal information fairly and lawfully. This means you are entitled to know how we
+ intend to use any information you provide. You can then decide whether you want to give
+ it to us in order that we may provide the product or service that you require.
+
+
+
+ On what basis will we process your personal information?
+
+
+
+ The basis for processing your personal information is:
+
+
+
+
+ It is necessary for the performance of a contract
+
+
+ It is necessary to comply with the mandatory legal or statutory obligations
+
+
+ It is necessary for public interest
+
+
+ It is necessary to protect the vital interest of you or other persons
+
+
+ Consent, whereby you have given us permission to use the personal data.
+
+
+
+
+ How long will your information be stored for?
+
+
+
+ We will store your personal information in a secure and protected environment for as
+ long as we believe it will better help us understand how we can serve you. However, this
+ will be only for a reasonable period and only for as long as is necessary. We may retain
+ and use your personal information to the extent necessary to comply with our legal
+ obligations.
+
+
+
+ We also retain Usage data for internal analysis purposes. Usage Data is generally
+ retained for a shorter period of time, except when data is used to strengthen the
+ security or to improve the functionality of our service, or we are legally obliged to
+ retain this data for a longer period.
+
+
+
+ If in any case, we have provided a password or other such data enabling you to access
+ various parts of our website, we require you to keep the password, including other login
+ details confidential. We also recommend that you do not share your password and/or login
+ details in any public forum or social media etc.
+
+
+
+ Your information, including Personal Data, may be transferred to – and maintained on –
+ computers located outside of your state, province, country or other governmental
+ jurisdiction where the data protection laws may differ from those of your jurisdiction.
+
+
+
+ If you are located outside India and choose to provide information to us, please note
+ that we transfer the data, including Personal Data, to India or to any place in the
+ world including the USA and process it there. We may also use a Cloud Distribution
+ Network which might spread the data all across the world to reduce latency and the data
+ may be processed by staff working for us anywhere across the world.
+
+
+
+ Your consent to this Privacy policy followed by your submission of such information
+ represents your agreement to that transfer.
+
+
+
+ Personal Data security
+
+
+
+ We take responsibility for the security of your data very seriously. Your data will be
+ held on secure servers, with all reasonable technological and operational measures put
+ in place to safeguard it from unauthorised access. Where possible any identifiable
+ information will be encrypted and transferred only by secure means.
+
+
+
+ Our policy on “Do Not Track” signals under the California Online Protection Act
+ (CalOPPA):
+
+
+
+ We do not support “Do Not Track” (DNT). Do Not Track is a preference you can set in your
+ web browser to inform websites that you do not want to be tracked.
+
+
+
+ You can enable or disenable Do Not Track by visiting the Preference or settings page of
+ your web browser
+
+
+
+ Your Rights under General Data Protection Regulation (GDPR):
+
+
+
+ If you are a resident of the EEA, you have certain data protection rights. KeepWorks
+ Technologies Pvt Ltd. takes all necessary steps to allow you to correct, amend, delete
+ or limit the use of your personal data.
+
+
+
+ In certain circumstances, you have the following Data Protection Rights:
+
+
+
+
+ The right to be informed: We are publishing this Privacy Policy to keep you informed
+ as to what we do with your personal information. We strive to be transparent about how
+ we use it.
+
+
+ The right to access: You have the right to access your information. If you wish to
+ access the personal information we hold about you, you may please contact us.
+
+
+ The right to erasure: This right is sometimes known as ‘the right to be forgotten’. If
+ you want us to erase all your personal information and we do not hold a legal reason
+ to continue to process and hold it, please contact us.
+
+
+ The right to rectification: If the information we hold about you is inaccurate or
+ incomplete, you have the right to ask us to rectify it. If the data has been passed to
+ a third party with your consent or for legal reasons, then we must also ask them to
+ rectify the data.
+
+
+ The right to data portability: We allow you to obtain and reuse your personal data for
+ your own purposes across services in a safe and secure way without this affecting the
+ usability of your data. The data must be held by us by consent or for the performance
+ of a contract.
+
+
+ The right to object: Data subjects have the right to object to our processing of your
+ data even if it is based on our legitimate interests.
+
+
+ The right to withdraw consent: If you have given us your consent to process your data
+ but change your mind later, you have the right to withdraw your consent at any time,
+ and we will stop processing your data.
+
+
+ The right to complain to a higher authority: Data subjects have the right to complain
+ to the ICO or any other appropriate authority if they feel that we are not meeting
+ their obligations in terms of GDPR or has not responded to solve a problem.
+
+
+
+
+ Please note that we may ask you to verify your identity before responding to such
+ requests.
+
+
+
+ Service Providers
+
+
+
+ In certain instances, we may employ third parties to facilitate and provide services on
+ our behalf. Also, they may be required to perform service related services or provide
+ assistance to us in analysing how our services are used. These third parties do not have
+ the obligation to use or disclose any of your Personal Data; they can only access your
+ Personal Data only to perform tasks on our behalf.
+
+
+
+ Analytics
+
+
+
+ We may use third-party Service Providers to monitor and analyse the use of our Service.
+
+
+
+ We assure you that we inform third party service providers to adhere to these privacy
+ policy and not to outsource or share or otherwise dispose off your personal data without
+ our prior written approval.
+
+
+
+ Google Analytics:
+
+ Google Analytics is a web analytics service offered by Google that tracks and reports
+ website traffic. Google uses the data collected to track and monitor the use of our
+ Service. This data is shared with other Google services. Google may use the collected
+ data to contextualise and personalise the ads of its own advertising network.
+
+
+
+ You can opt-out of having made your activity on the Service available to Google
+ Analytics by installing the Google Analytics opt-out browser add-on. The add-on prevents
+ the Google Analytics JavaScript (ga.js, analytics.js and dc.js) from sharing information
+ with Google Analytics about visits activity.
+
+
+
+ For more information on the privacy practices of Google, please visit the Google Privacy
+ Terms web page:
+ {' '}
+ https://policies.google.com/privacy?hl=en
+
+
+
+ Mixpanel:
+
+ Mixpanel is provided by Mixpanel Inc.
+
+
+
+ You can prevent Mixpanel from using your information for analytics purposes by
+ opting-out. To opt-out of Mixpanel service, please visit this page:
+ {' '}
+ https://mixpanel.com/optout/
+
+
+
+ For more information on what type of information Mixpanel collects, please visit the
+ Terms of Use page of Mixpanel:
+ {' '}
+ https://mixpanel.com/terms/
+
+
+
+ Payments:
+
+
+
+ We use third party services for payment processing. Any information provided by you
+ regarding payments or transactions will be directly routed to our third party processors
+ whose use of your personal information is governed by their privacy policy. We do not
+ store or collect any of your payment card details.
+
+
+
+ These payment processors adhere to the standards set by PCI-DSS as managed by the PCI
+ Security Standards Council, which is a joint effort of brands like Visa, MasterCard,
+ American Express and Discover. PCI-DSS requirements help ensure the secure handling of
+ payment information.
+
+
+
+ The payment processors we work with are:
+
+
+
+ RazorPay:
+
+ Their Privacy Policy can be viewed at
+ {' '}
+ https://razorpay.com/privacy
+
+
+
+ Stripe:
+
+ Their Privacy Policy can be viewed at
+ {' '}
+ https://stripe.com/us/privacy
+
+
+
+ Information about children:
+
+
+
+ Our services are not intended or addressed to anyone under the age of 18 years. We do
+ not knowingly collect information about children (i.e. anyone under age of 18). If you
+ have reason to believe that we have collected information pertaining to a child or
+ anyone under the age of 18, please contact us, so that we may delete the information.
+
+
+
+ Our website may contain links to other websites of interest. However, once you have used
+ these links to leave our site, you should note that we do not have any control over that
+ other website. Therefore, we cannot be responsible for the protection and privacy of any
+ information which you provide whilst visiting such sites and such sites are not governed
+ by this privacy statement. You should exercise caution and look at the privacy statement
+ applicable to the website in question.
+
+
+
+ We may update our Privacy Policy from time to time. We will notify you of any changes by
+ posting the new Privacy Policy on this page.
+
+
+
+ We will let you know via email and/or a prominent notice on our Service, prior to the
+ change becoming effective and update the “effective date” at the top of this Privacy
+ Policy.
+
+
+
+ You may choose to restrict the collection or use of your personal information in the
+ following ways:
+
+
+
+
+ whenever you are asked to fill in a form on the website, look for the box that you can
+ click to indicate that you do not want the information to be used by anybody for
+ direct marketing purposes.
+
+
+ if you have previously agreed to us using your personal information for direct
+ marketing purposes, you may change your mind at any time by writing to or emailing us
+ at
+ {' '}
+ support@claycms.io
+ .
+
+
+
+
+ We will not sell, distribute or lease your personal information to third parties unless
+ we have your permission or are required by law to do so. We may use your personal
+ information to send you promotional information about third parties which we think you
+ may find interesting if you tell us that you wish this to happen.
+
+
+
+ If you would like a copy of the information please write to:
+ {' '}
+ support@claycms.io
+
+
+
+ If you believe that any information we are holding on you is incorrect or incomplete,
+ please write to or email us as soon as possible, at the above address. We will promptly
+ correct any information found to be incorrect.
+
+
+
+ If there are any questions regarding this Privacy Policy please write to:
+ {' '}
+ legal@claycms.io
+
+
+
+
+ )
+}
+
+export default PrivacyPolicyPage
diff --git a/ui/src/components/pages/ProjectPage.js b/ui/src/components/pages/ProjectPage.js
new file mode 100644
index 0000000..2713829
--- /dev/null
+++ b/ui/src/components/pages/ProjectPage.js
@@ -0,0 +1,69 @@
+import gql from 'graphql-tag'
+import Helmet from 'react-helmet-async'
+import React, { Fragment } from 'react'
+import { Redirect, Route, Switch } from 'react-router-dom'
+
+import AssetsPage from 'components/pages/AssetsPage'
+import DashboardPage from 'components/pages/DashboardPage'
+import EntitiesPage from 'components/pages/EntitiesPage'
+import EntityPage from 'components/pages/EntityPage'
+import Loader from 'components/internal/Loader'
+import ProjectSettingsPage from 'components/pages/ProjectSettingsPage'
+import { withQuery } from 'lib/data'
+
+function ProjectPage({ history, match, project, loading }) {
+ if (!loading && !project) {
+ history.push(`/teams/${match.params.teamId}`)
+ return null
+ }
+
+ if (loading) {
+ return
+ }
+
+ return (
+
+
+
+ {project.name}
+
+
+
+
+
+
+
+
+
+
+
+
+ )
+}
+
+ProjectPage.fragments = {
+ project: gql`
+ fragment ProjectPage_project on Project {
+ id
+ name
+ teamId
+ isRestoring
+ }
+ `
+}
+
+ProjectPage = withQuery(gql`
+ query ProjectPage($id: ID!) {
+ project(id: $id) {
+ ...ProjectPage_project
+ }
+ }
+
+ ${ProjectPage.fragments.project}
+`, {
+ options: ({ match }) => ({
+ variables: { id: match.params.projectId }
+ })
+})(ProjectPage)
+
+export default ProjectPage
diff --git a/ui/src/components/pages/ProjectSettingsPage.js b/ui/src/components/pages/ProjectSettingsPage.js
new file mode 100644
index 0000000..82f0f17
--- /dev/null
+++ b/ui/src/components/pages/ProjectSettingsPage.js
@@ -0,0 +1,321 @@
+import gql from 'graphql-tag'
+import Helmet from 'react-helmet-async'
+import injectSheet from 'react-jss'
+import React, { Fragment, useState } from 'react'
+
+import ChangeProjectNameForm from 'components/internal/forms/ChangeProjectNameForm'
+import Column from 'components/internal/Column'
+import CopyToClipboard from 'components/internal/CopyToClipboard'
+import FilledButton from 'components/buttons/FilledButton'
+import ImportProjectDialog from 'components/internal/dialogs/ImportProjectDialog'
+import Row from 'components/internal/Row'
+import Spacer from 'components/Spacer'
+import Text from 'components/typography/Text'
+import { KeyPair } from 'models'
+import { MutationResponseModes, withQueries, withClientQuery, withMutation } from 'lib/data'
+import { PageTitle } from 'components/internal/typography'
+import { Panel, PanelBody, PanelContainer, PanelHeader, PanelTable } from 'components/internal/panel'
+import { parseServerDateTime } from 'lib/dateTime'
+
+function ProjectSettingsPage({
+ classes,
+ exportProject,
+ exports,
+ importProject,
+ keyPairs,
+ loading,
+ project,
+ restores,
+ updateProject
+}) {
+ const [ isImportDialogOpen, setIsImportDialogOpen ] = useState(false)
+ const [ isImportButtonDisabled, setIsImportButtonDisabled ] = useState(false)
+
+ const openImportDialog = () => setIsImportDialogOpen(true)
+ const closeImportDialog = () => setIsImportDialogOpen(false)
+
+ const onChangeProjectNameFormSubmit = values => (
+ updateProject({ id: project.id, ...values })
+ )
+
+ const renderKeyPairs = () => (keyPairs || []).map(keyPair => (
+
+ {KeyPair.fields.map(({ name, label }) => {
+ const value = keyPair[name]
+
+ return (
+
+
+ {label}
+
+
+ {value}
+
+
+
+ )
+ })}
+
+ ))
+
+ const onProjectImportSubmit = (values) => {
+ importProject({ id: project.id, ...values }, {
+ onSuccess: ({ data: { importProject: { status } = {} } = {} }) => {
+ if (status === 'pending') {
+ setIsImportButtonDisabled(true)
+ }
+ }
+ }).then(closeImportDialog)
+ }
+
+ const processedExports = (exports || []).map(({ createdAt, status, file }, index) => ({
+ name: `#${index + 1}`,
+ badge: status,
+ details: file,
+ action: file && ,
+ time: parseServerDateTime(createdAt).fromNow()
+ }))
+
+ const processedImports = (restores || []).map(({ createdAt, status, url }, index) => ({
+ name: `#${index + 1}`,
+ badge: status,
+ details: url,
+ action: url && ,
+ time: parseServerDateTime(createdAt).fromNow()
+ }))
+
+ return (
+
+
+
+ Settings
+
+
+
+
+ Settings
+
+
+
+
+
+ Project Keys
+
+
+ {renderKeyPairs()}
+
+
+
+
+
+ Change Project Name
+
+
+
+
+
+
+
+
+ Export/Import Project
+
+
+ exportProject({ id: project.id })} />
+
+
+ Exports
+
+
+
+ Imports
+
+
+
+
+
+
+
+ )
+}
+
+ProjectSettingsPage.fragments = {
+ keyPair: gql`
+ fragment ProjectSettingsPage_keyPair on KeyPair {
+ id
+ publicKey
+ privateKey
+ }
+ `,
+ project: gql`
+ fragment ProjectSettingsPage_project on Project {
+ id
+ name
+ isRestoring
+ }
+ `,
+ export: gql`
+ fragment ProjectSettingsPage_export on Export {
+ id
+ file
+ status
+ metadata
+ createdAt
+ }
+ `,
+ restore: gql`
+ fragment ProjectSettingsPage_restore on Restore {
+ id
+ url
+ status
+ createdAt
+ }
+ `
+}
+
+const ProjectSettingsPageQuery = gql`
+ query ProjectSettingsPage($projectId: ID!) {
+ keyPairs(projectId: $projectId) {
+ ...ProjectSettingsPage_keyPair
+ }
+ }
+
+ ${ProjectSettingsPage.fragments.keyPair}
+ ${ProjectSettingsPage.fragments.export}
+`
+
+const ProjectExportsQuery = gql`
+ query ProjectExports($projectId: ID!) {
+ exports(projectId: $projectId) {
+ ...ProjectSettingsPage_export
+ }
+ }
+
+ ${ProjectSettingsPage.fragments.export}
+`
+
+const ProjectRestoresQuery = gql`
+ query ProjectRestores($projectId: ID!) {
+ restores(projectId: $projectId) {
+ ...ProjectSettingsPage_restore
+ }
+ }
+
+ ${ProjectSettingsPage.fragments.restore}
+`
+
+const ProjectSettingsPageQueryConfig = {
+ options: ({ match }) => ({
+ variables: { projectId: match.params.projectId }
+ })
+}
+
+ProjectSettingsPage = withClientQuery(gql`
+ query ProjectQuery($id: ID!) {
+ project(id: $id) {
+ ...ProjectSettingsPage_project
+ }
+ }
+
+ ${ProjectSettingsPage.fragments.project}
+`, {
+ options: ({ match }) => ({
+ variables: { id: match.params.projectId }
+ })
+})(ProjectSettingsPage)
+
+ProjectSettingsPage = withMutation(gql`
+ mutation UpdateProjectMutation($id: ID!, $input: UpdateProjectInput!) {
+ updateProject(id: $id, input: $input) {
+ ...ProjectSettingsPage_project
+ }
+ }
+
+ ${ProjectSettingsPage.fragments.project}
+`, {
+ successAlert: ({ input: { name } }) => ({
+ message: `Successfully changed project name to: ${name}`
+ })
+})(ProjectSettingsPage)
+
+ProjectSettingsPage = withMutation(gql`
+ mutation ImportProjectMutation($id: ID!, $input: ImportProjectInput!) {
+ importProject(id: $id, input: $input) {
+ ...ProjectSettingsPage_restore
+ }
+ }
+
+ ${ProjectSettingsPage.fragments.restore}
+`, {
+ successAlert: {
+ message: 'Successfully started importing project. It may take a while.'
+ },
+ mode: MutationResponseModes.PREPEND,
+ query: ProjectRestoresQuery
+})(ProjectSettingsPage)
+
+ProjectSettingsPage = withMutation(gql`
+ mutation ExportProjectMutation($id: ID!) {
+ exportProject(id: $id) {
+ ...ProjectSettingsPage_export
+ }
+ }
+
+ ${ProjectSettingsPage.fragments.export}
+`, {
+ successAlert: {
+ message: 'Successfully started exporting project. It may take a while.'
+ },
+ mode: MutationResponseModes.PREPEND,
+ query: ProjectExportsQuery
+})(ProjectSettingsPage)
+
+ProjectSettingsPage = withQueries([
+ {
+ query: ProjectExportsQuery,
+ config: { ...ProjectSettingsPageQueryConfig }
+ },
+ {
+ query: ProjectRestoresQuery,
+ config: { ...ProjectSettingsPageQueryConfig }
+ },
+ {
+ query: ProjectSettingsPageQuery,
+ config: { ...ProjectSettingsPageQueryConfig }
+ }
+])(ProjectSettingsPage)
+
+export default injectSheet(({ colors, typography, units }) => ({
+ keyLabel: {
+ ...typography.regularSquished,
+
+ color: colors.text_pale,
+ paddingBottom: units.keyLabelPaddingBottom
+ },
+ keyValue: {
+ ...typography.regular,
+
+ alignItems: 'center',
+ display: 'flex',
+ color: colors.text_dark,
+ cursor: 'pointer',
+
+ '& .icon': {
+ marginLeft: 10
+ }
+ }
+}))(ProjectSettingsPage)
diff --git a/ui/src/components/pages/ProjectsPage.js b/ui/src/components/pages/ProjectsPage.js
new file mode 100644
index 0000000..21cb003
--- /dev/null
+++ b/ui/src/components/pages/ProjectsPage.js
@@ -0,0 +1,105 @@
+import gql from 'graphql-tag'
+import Helmet from 'react-helmet-async'
+import React, { Component, Fragment } from 'react'
+
+import DataTiles from 'components/internal/DataTiles'
+import IconButton from 'components/internal/buttons/IconButton'
+import Loader from 'components/internal/Loader'
+import ProjectDialog from 'components/internal/dialogs/ProjectDialog'
+import ProjectPage from 'components/pages/ProjectPage'
+import { MutationResponseModes, OptimisticResponseModes, withMutation, withQuery } from 'lib/data'
+import { PageTitle } from 'components/internal/typography'
+
+class ProjectsPage extends Component {
+ constructor() {
+ super()
+
+ this.state = {
+ isProjectDialogOpen: false
+ }
+ }
+
+ openProjectDialog = () => this.setState({ isProjectDialogOpen: true })
+
+ closeProjectDialog = () => this.setState({ isProjectDialogOpen: false })
+
+ handleFormSubmit = (values) => {
+ const { createProject, history } = this.props
+
+ return createProject(values, {
+ onSuccess: ({ data: { project: { id, teamId } } }) => {
+ this.closeProjectDialog()
+ history.push(`/teams/${teamId}/projects/${id}`)
+ }
+ })
+ }
+
+ render() {
+ const { loading, match, projects } = this.props
+ const { isProjectDialogOpen } = this.state
+
+ return (
+
+
+
+ Projects
+
+
+
+
+ Projects
+
+
+
+
+
+
+ `${match.url}/${id}`} />
+
+
+
+ )
+ }
+}
+
+ProjectsPage = withMutation(gql`
+ mutation CreateProjectMutation($input: CreateProjectInput!) {
+ project: createProject(input: $input) {
+ ...ProjectPage_project
+ }
+ }
+
+ ${ProjectPage.fragments.project}
+`, {
+ mode: MutationResponseModes.APPEND,
+ optimistic: { mode: OptimisticResponseModes.CREATE, response: { __typename: 'Project' } }
+})(ProjectsPage)
+
+export default withQuery(gql`
+ query ProjectsPageQuery($teamId: ID!) {
+ projects(teamId: $teamId) {
+ ...ProjectPage_project
+ }
+ }
+
+ ${ProjectPage.fragments.project}
+`, {
+ options: ({ match }) => ({
+ variables: { teamId: match.params.teamId }
+ })
+})(ProjectsPage)
diff --git a/ui/src/components/pages/RecordsPage.js b/ui/src/components/pages/RecordsPage.js
new file mode 100644
index 0000000..f2b4527
--- /dev/null
+++ b/ui/src/components/pages/RecordsPage.js
@@ -0,0 +1,387 @@
+import _ from 'lodash'
+import gql from 'graphql-tag'
+import injectSheet from 'react-jss'
+import React, { Fragment, useState } from 'react'
+import { withApollo } from 'react-apollo'
+
+import ActionList from 'components/ActionList'
+import ColorTile from 'components/internal/ColorTile'
+import Field from 'models/Field'
+import FontIcon from 'components/FontIcon'
+import IconButton from 'components/internal/buttons/IconButton'
+import Loader from 'components/internal/Loader'
+import Record from 'models/Record'
+import RecordModal from 'components/internal/modals/RecordModal'
+import Spacer from 'components/Spacer'
+import Table from 'components/internal/dataTable/Table'
+import useModal from 'lib/hooks/useModal'
+import withConfirmation from 'components/internal/decorators/withConfirmation'
+import { CellContent } from 'components/internal/typography'
+import { MutationResponseModes, OptimisticResponseModes, withMutation, withQuery } from 'lib/data'
+
+function RecordsPage({
+ confirm,
+ createRecord,
+ destroyRecord,
+ fields,
+ loading,
+ match,
+ records,
+ updateRecord,
+ client
+}) {
+ const [
+ selectedRecord, isModalOpen, openModal, closeModal
+ ] = useModal()
+
+ const [ entities, setEntities ] = useState([])
+ const [ entitiesLoading, setEntitiesLoading ] = useState(false)
+ const [ recordLoading, setRecordLoading ] = useState(false)
+
+ const getEntities = async (entityId) => {
+ setEntitiesLoading(true)
+ const { data } = await client.query({
+ query: RecordsPage.ENTITIES_QUERY,
+ variables: {
+ entityId
+ },
+ fetchPolicy: 'network-only'
+ })
+
+ const newEntities = [ ...entities, ...data.referencedEntities ]
+
+ setEntities(newEntities)
+ setEntitiesLoading(false)
+ return newEntities
+ }
+
+ const getRecord = async (recordId) => {
+ setRecordLoading(true)
+ const { data: { record } } = await client.query({
+ query: RecordsPage.RECORD_QUERY,
+ variables: {
+ recordId
+ },
+ fetchPolicy: 'network-only'
+ })
+
+ const newEntities = entities
+ .map((entity) => {
+ if (entity.id === record.entityId) {
+ entity.records.push(record)
+ }
+
+ return entity
+ })
+
+ setEntities(newEntities)
+ setRecordLoading(false)
+ return newEntities
+ }
+
+ const handleFormSubmit = (values) => {
+ if (values.id) {
+ return updateRecord(values, {
+ onSuccess: () => {
+ setEntities([]) // Reset entities so that they are refetched when modal reopens
+ closeModal()
+ }
+ })
+ }
+
+ return createRecord(values, { onSuccess: () => closeModal() })
+ }
+
+ const makePropertyRenderer = field => ({ record }) => {
+ let isValid = false
+ const value = record.traits[field.name]
+
+ if (_.isString(value) || _.isNumber(value)) {
+ isValid = true
+ }
+
+ if (field.dataType === 'boolean') {
+ const isActive = typeof value === 'boolean' ? value : value === 't'
+
+ return (
+
+ {isActive ? (
+
+ ) : (
+
+ )}
+
+ )
+ }
+
+ if (field.dataType === 'color' && value) {
+ return { }
+ }
+
+ return {isValid ? value : '-'}
+ }
+
+ const idRenderer = ({ record }) => {record.id}
+
+ const columnFields = _.sortBy((fields || []).filter(Field.isRoot).filter(Field.isColumn).filter(Field.isVisibleColumn), [ 'position' ])
+
+ const columns = columnFields.map(field => ({
+ dataKey: `traits.${field.name}`,
+ label: field.label,
+ flexGrow: 1,
+ cellRenderer: makePropertyRenderer(field)
+ }))
+
+ columns.unshift({
+ dataKey: 'id',
+ label: 'Id',
+ flexGrow: 0,
+ cellRenderer: idRenderer
+ })
+
+ columns.push({
+ dataKey: 'actions',
+ flexGrow: 0,
+ cellRenderer: ({ record }) => (
+ openModal(clickedRecord) },
+ {
+ icon: 'trash',
+ onClick: clickedRecord => confirm({
+ description: 'By clicking on Confirm, you will delete the record.',
+ onConfirmClick: () => destroyRecord(clickedRecord)
+ })
+ }
+ ]}
+ />
+ )
+ })
+
+ if (loading) {
+ return
+ }
+
+ // TODO: Move the method process from model?
+ const processedRecords = new Record(fields || []).process(records || [])
+
+ const sortField = columnFields[0] ? `traits.${columnFields[0].name}` : 'id'
+ const sortedRecords = _.sortBy(processedRecords, [ sortField ])
+
+ return (
+
+ openModal()} />
+
+
+ openModal()
+ }}
+ />
+
+
+
+
+
+ )
+}
+
+RecordsPage = injectSheet(({ colors, typography }) => ({
+ entityName: {
+ ...typography.semibold,
+
+ alignItems: 'center',
+ color: colors.text_dark,
+ display: 'flex',
+ lineHeight: 1
+ }
+}))(RecordsPage)
+
+RecordsPage.fragments = {
+ fields: gql`
+ fragment RecordsPage_fields on Field {
+ id
+ dataType
+ validations
+ settings
+ elementType
+ referencedEntityId
+ defaultValue
+ elementType
+ entityId
+ hint
+ label
+ name
+ parentId
+ position
+ referencedEntityId
+ }
+ `,
+ properties: gql`
+ fragment RecordsPage_properties on Property {
+ id
+ value
+ position
+ fieldId
+ linkedRecordId
+ parentId
+ asset {
+ id
+ fileOriginal
+ }
+ }
+ `
+}
+
+RecordsPage.fragments.records = gql`
+ fragment RecordsPage_records on Record {
+ id
+ entityId
+ createdAt
+ updatedAt
+
+ properties {
+ ...RecordsPage_properties
+ }
+ }
+
+ ${RecordsPage.fragments.properties}
+`
+
+RecordsPage = withMutation(gql`
+ mutation CreateRecordMutation($input: CreateRecordInput!) {
+ createRecord(input: $input) {
+ ...RecordsPage_records
+ }
+ }
+
+ ${RecordsPage.fragments.records}
+`, {
+ inputFilter: gql`
+ fragment CreateRecordInput on CreateRecordInput {
+ entityId
+ traits
+ }
+ `,
+ mode: MutationResponseModes.APPEND
+})(RecordsPage)
+
+RecordsPage = withMutation(gql`
+ mutation UpdateRecordMutation($id: ID!, $input: UpdateRecordInput!) {
+ updateRecord(id: $id, input: $input) {
+ ...RecordsPage_records
+ }
+ }
+
+ ${RecordsPage.fragments.records}
+`, {
+ inputFilter: gql`
+ fragment UpdateRecordInput on UpdateRecordInput {
+ id
+ traits
+ }
+ `
+})(RecordsPage)
+
+RecordsPage = withMutation(gql`
+ mutation DestroyRecordMutation($id: ID!) {
+ destroyRecord(id: $id) {
+ id
+ }
+ }
+`, {
+ optimistic: { mode: OptimisticResponseModes.DESTROY, response: { __typename: 'Record' } },
+ mode: MutationResponseModes.DELETE,
+ successAlert: () => ({
+ message: 'Successfully deleted record'
+ })
+})(RecordsPage)
+
+RecordsPage = withQuery(gql`
+ query RecordsPageQuery($entityId: ID!) {
+ records(entityId: $entityId) {
+ ...RecordsPage_records
+ }
+
+ entity(id: $entityId) {
+ id
+ label
+ name
+ }
+
+ fields(entityId: $entityId) {
+ ...RecordsPage_fields
+ }
+ }
+
+ ${RecordsPage.fragments.records}
+ ${RecordsPage.fragments.fields}
+`, {
+ options: ({ match }) => ({
+ variables: {
+ entityId: match.params.entityId
+ }
+ })
+})(RecordsPage)
+
+RecordsPage.ENTITIES_QUERY = gql`
+ query RecordsPageEntitiesQuery($entityId: ID!) {
+ referencedEntities(entityId: $entityId) {
+ id
+ label
+ name
+ label
+ parentId
+
+ fields {
+ ...RecordsPage_fields
+ }
+
+ records {
+ ...RecordsPage_records
+ }
+ }
+ }
+
+ ${RecordsPage.fragments.records}
+ ${RecordsPage.fragments.fields}
+`
+
+RecordsPage.RECORD_QUERY = gql`
+ query RecordsPageRecordQuery($recordId: ID!) {
+ record(recordId: $recordId) {
+ entityId
+
+ ...RecordsPage_records
+ }
+ }
+
+ ${RecordsPage.fragments.records}
+`
+RecordsPage = withApollo(RecordsPage)
+
+export default withConfirmation()(RecordsPage)
diff --git a/ui/src/components/pages/ResetPasswordPage.js b/ui/src/components/pages/ResetPasswordPage.js
new file mode 100644
index 0000000..02454d8
--- /dev/null
+++ b/ui/src/components/pages/ResetPasswordPage.js
@@ -0,0 +1,85 @@
+import gql from 'graphql-tag'
+import qs from 'qs'
+import React, { Component, Fragment } from 'react'
+
+import Body from 'components/onboarding/Body'
+import Card from 'components/onboarding/Card'
+import CardWrapper from 'components/onboarding/CardWrapper'
+import Content from 'components/onboarding/Content'
+import Header from 'components/onboarding/Header'
+import Helmet from 'react-helmet-async'
+import ResetPasswordForm from 'components/onboarding/forms/ResetPasswordForm'
+import { withMutation } from 'lib/data'
+
+class ResetPasswordPage extends Component {
+ constructor(props) {
+ super(props)
+
+ const { token: resetPasswordToken } = qs.parse(
+ document.location.search,
+ { ignoreQueryPrefix: true }
+ )
+
+ this.state = { resetPasswordToken }
+ }
+
+ componentDidMount() {
+ const { history } = this.props
+ const { resetPasswordToken } = this.state
+
+ if (!resetPasswordToken) {
+ history.replace('/forgot-password')
+ }
+ }
+
+ handleSubmit = (values) => {
+ const { history, resetPassword } = this.props
+
+ return resetPassword(values, { onSuccess: () => history.replace('/login') })
+ }
+
+ render() {
+ const { resetPasswordToken } = this.state
+
+ return (
+
+
+
+ Reset password
+
+
+
+
+
+
+
+ We can't wait to welcome you back.
+
+
+
+
+
+
+
+
+
+ )
+ }
+}
+
+ResetPasswordPage = withMutation(gql`
+ mutation ResetPasswordMutation($input: ResetPasswordInput!) {
+ resetPassword(input: $input) {
+ success
+ }
+ }
+`, {
+ successAlert: {
+ message: 'You may now use your new password to login.'
+ }
+})(ResetPasswordPage)
+
+export default ResetPasswordPage
diff --git a/ui/src/components/pages/SignupPage.js b/ui/src/components/pages/SignupPage.js
new file mode 100644
index 0000000..428b728
--- /dev/null
+++ b/ui/src/components/pages/SignupPage.js
@@ -0,0 +1,202 @@
+import gql from 'graphql-tag'
+import Helmet from 'react-helmet-async'
+import React, { Component, Fragment } from 'react'
+
+import Body from 'components/onboarding/Body'
+import Card from 'components/onboarding/Card'
+import CardFootnote from 'components/onboarding/CardFootnote'
+import CardWrapper from 'components/onboarding/CardWrapper'
+import Content from 'components/onboarding/Content'
+import FilledButton from 'components/buttons/FilledButton'
+import Header from 'components/onboarding/Header'
+import SignupForm from 'components/onboarding/forms/SignupForm'
+import Spacer from 'components/Spacer'
+import { CardText, Text, TextLink } from 'components/onboarding/typography'
+import { withMutation } from 'lib/data'
+
+/* eslint-disable jsx-a11y/anchor-is-valid */
+
+class SignupPage extends Component {
+ constructor() {
+ super()
+
+ this.state = {
+ resendConfirmationStatus: SignupPage.resendConfirmationStatuses.NONE,
+ user: null
+ }
+ }
+
+ static getDerivedStateFromProps(props) {
+ const { history, location } = props
+
+ if (location.state && location.state.newUser) {
+ const user = Object.assign({}, location.state.newUser)
+
+ // Clear state from location for next re-render.
+ history.replace({ state: {} })
+
+ return { user }
+ }
+
+ // Return null to indicate no change to state.
+ return null
+ }
+
+ handleFormSubmit = (values) => {
+ const { createUser } = this.props
+
+ return createUser(values, {
+ onSuccess: (response) => {
+ const user = response.data.createUser
+
+ this.setState({ user })
+ }
+ })
+ }
+
+ handleResendConfirmation = () => {
+ const { resendConfirmation } = this.props
+ const { user } = this.state
+
+ this.setState({
+ resendConfirmationStatus: SignupPage.resendConfirmationStatuses.LOADING
+ })
+
+ return resendConfirmation({ email: user ? user.email : null }, {
+ onSuccess: response => this.setState({
+ resendConfirmationStatus: SignupPage.resendConfirmationStatuses.SUCCESS,
+ user: response.data.resendConfirmation
+ }),
+ onFailure: () => this.setState({
+ resendConfirmationStatus: SignupPage.resendConfirmationStatuses.ERROR
+ })
+ })
+ }
+
+ render() {
+ const { resendConfirmationStatus, user } = this.state
+
+ return (
+
+
+
+ Sign up
+
+
+
+
+
+
+
+ Get started now for free.
+
+
+
+
+
+
+
+
+ {user && user.email && (
+
+
+ {user.isConfirmationExpired
+ ? 'Your confirmation link has expired for the email address:'
+ : 'We have sent a confirmation link to the email address:'
+ }
+
+
+ {user.email}
+
+
+ )}
+ {user && resendConfirmationStatus === SignupPage.resendConfirmationStatuses.NONE
+ && (
+
+ {!user.isConfirmationExpired && (
+
+ If you don’t receive it within 5 minutes, check your spam folder or
+ resend your confirmation email
+ .
+
+ )}
+ {user.isConfirmationExpired && (
+
+
+
+
+ )}
+
+ )
+ }
+ {user && resendConfirmationStatus === SignupPage.resendConfirmationStatuses.LOADING
+ && (
+
+ Resending...
+
+ )
+ }
+ {user && resendConfirmationStatus === SignupPage.resendConfirmationStatuses.ERROR
+ && (
+
+ Failed to resend confirmation email.
+ Try again.
+
+ )
+ }
+ {user && resendConfirmationStatus === SignupPage.resendConfirmationStatuses.SUCCESS
+ && (
+
+ Resent confirmation email. If you don’t receive it soon,
+ check your spam folder or
+ try again
+ .
+
+ )
+ }
+
+
+
+
+ By clicking submit, you are agreeing to the
+
+ Terms of Service
+
+ .
+
+
+
+
+
+ )
+ }
+}
+
+SignupPage.resendConfirmationStatuses = Object.freeze({
+ NONE: 'none',
+ LOADING: 'loading',
+ ERROR: 'error',
+ SUCCESS: 'success'
+})
+
+SignupPage = withMutation(gql`
+ mutation CreateUserMutation($input: CreateUserInput!) {
+ createUser(input: $input) {
+ id
+ email
+ isConfirmationExpired
+ }
+ }
+`)(SignupPage)
+
+SignupPage = withMutation(gql`
+ mutation ResendConfirmation($input: ResendConfirmationInput!) {
+ resendConfirmation(input: $input) {
+ id
+ email
+ isConfirmationExpired
+ }
+ }
+`)(SignupPage)
+
+export default SignupPage
diff --git a/ui/src/components/pages/TeamBillingPage.js b/ui/src/components/pages/TeamBillingPage.js
new file mode 100644
index 0000000..826b900
--- /dev/null
+++ b/ui/src/components/pages/TeamBillingPage.js
@@ -0,0 +1,22 @@
+import Helmet from 'react-helmet-async'
+import React, { Fragment } from 'react'
+
+import { PageTitle } from 'components/internal/typography'
+
+function TeamBillingPage() {
+ return (
+
+
+
+ Billing
+
+
+
+
+ Billing
+
+
+ )
+}
+
+export default TeamBillingPage
diff --git a/ui/src/components/pages/TeamMembersPage.js b/ui/src/components/pages/TeamMembersPage.js
new file mode 100644
index 0000000..2a13843
--- /dev/null
+++ b/ui/src/components/pages/TeamMembersPage.js
@@ -0,0 +1,290 @@
+import gql from 'graphql-tag'
+import Helmet from 'react-helmet-async'
+import injectSheet from 'react-jss'
+import React, { Component, Fragment } from 'react'
+
+import AppContext from 'components/AppContext'
+import BoxCell from 'components/internal/dataTable/BoxCell'
+import FontIcon from 'components/FontIcon'
+import IconButton from 'components/internal/buttons/IconButton'
+import Loader from 'components/internal/Loader'
+import ProfilePicture from 'components/internal/ProfilePicture'
+import Table from 'components/internal/dataTable/Table'
+import TeamMemberDialog from 'components/internal/dialogs/TeamMemberDialog'
+import { MutationResponseModes, OptimisticResponseModes, withMutation, withQuery } from 'lib/data'
+import { PageTitle } from 'components/internal/typography'
+import { TeamMembership, User } from 'models'
+
+const statusMap = {
+ CONFIRMED: {
+ icon: 'round-tick',
+ color: '#6cf1ba'
+ },
+ UNCONFIRMED: {
+ icon: 'slide-right',
+ color: '#ffd76e'
+ }
+}
+
+class TeamMembersPage extends Component {
+ constructor() {
+ super()
+
+ this.state = {
+ isTeamMemberDialogOpen: false,
+ teamMembership: {}
+ }
+ }
+
+ static getDerivedStateFromProps(props) {
+ const { history, location } = props
+
+ if (location.state && location.state.newMember) {
+ // Clear state from location for next re-render.
+ history.replace({ state: {} })
+
+ return {
+ isTeamMemberDialogOpen: true
+ }
+ }
+
+ // Return null to indicate no change to state.
+ return null
+ }
+
+ openTeamMembersDialog = teamMembership => this.setState({
+ teamMembership, isTeamMemberDialogOpen: true
+ })
+
+ closeTeamMembersDialog = () => this.setState({ isTeamMemberDialogOpen: false })
+
+ handleFormSubmit = (values) => {
+ const { createTeamMembership, updateTeamMembership } = this.props
+
+ if (values.id) {
+ return updateTeamMembership(values, { onSuccess: () => this.closeTeamMembersDialog() })
+ }
+
+ return createTeamMembership(values, { onSuccess: () => this.closeTeamMembersDialog() })
+ }
+
+ profileCellRenderer = ({ record: { user } }) =>
+
+ fullNameCellRenderer = ({ record: { user } }) => {
+ const { classes } = this.props
+
+ return (
+
+ {User.fullName(user)}
+
+ )
+ }
+
+ emailCellRenderer = ({ record: { user: { email } } }) => {
+ const { classes } = this.props
+
+ return (
+
+ {email}
+
+ )
+ }
+
+ statusCellRenderer = ({ record: { user: { isConfirmed } } }) => {
+ const { classes } = this.props
+
+ return (
+
+ {isConfirmed ? 'Normal' : 'Pending'}
+
+
+
+
+ )
+ }
+
+ render() {
+ const {
+ destroyTeamMembership,
+ loading,
+ match: { params: { teamId } },
+ teamMemberships
+ } = this.props
+ const { isTeamMemberDialogOpen, teamMembership } = this.state
+
+ const columns = [
+ { dataKey: 'user.profile', flexGrow: 1, cellWrapper: BoxCell, cellRenderer: this.profileCellRenderer },
+ { dataKey: 'user.firstName', flexGrow: 1, bordered: false, cellRenderer: this.fullNameCellRenderer },
+ { dataKey: 'user.email', flexGrow: 1, pale: true, cellRenderer: this.emailCellRenderer },
+ { dataKey: 'role', width: 150, uppercase: true },
+ { dataKey: 'user.status', width: 150, uppercase: true, cellRenderer: this.statusCellRenderer }
+ ]
+
+ const actions = [
+ { icon: 'edit', onClick: record => this.openTeamMembersDialog(record) },
+ { icon: 'trash', onClick: record => destroyTeamMembership(record) }
+ ]
+
+ return (
+
+ {({ currentUser }) => {
+ const currentUserTeamMembership = teamMemberships && teamMemberships.find(
+ membership => membership.userId === currentUser.id
+ )
+ const canInvite = TeamMembership.authorize(currentUserTeamMembership, 'invite')
+
+ const rowProps = ({ record: { role, user } }) => ({
+ isPale: !user.isConfirmed,
+ showActions: canInvite && role !== 'owner'
+ })
+
+ return (
+
+
+
+ Members
+
+
+
+
+ Members
+
+
+ {canInvite && this.openTeamMembersDialog()} />}
+
+ this.openTeamMembersDialog()
+ }}
+ />
+
+
+
+
+
+ )
+ }}
+
+ )
+ }
+}
+
+TeamMembersPage = injectSheet(({ typography }) => ({
+ fullName: {
+ ...typography.semibold
+ },
+ email: {
+ ...typography.regularSquished
+ },
+ status: {
+ alignItems: 'center',
+ display: 'flex',
+ justifyContent: 'space-between'
+ }
+}))(TeamMembersPage)
+
+TeamMembersPage.fragments = {
+ teamMemberships: gql`
+ fragment TeamMembersPage_teamMemberships on TeamMembership {
+ id
+ userId
+ role
+
+ user {
+ id
+ email
+ firstName
+ lastName
+ profilePictureThumbnail
+ isConfirmed
+ }
+ }
+ `
+}
+
+TeamMembersPage = withMutation(gql`
+ mutation CreateTeamMembershipMutation($input: CreateTeamMembershipInput!) {
+ createTeamMembership(input: $input) {
+ ...TeamMembersPage_teamMemberships
+ }
+ }
+
+ ${TeamMembersPage.fragments.teamMemberships}
+`, {
+ inputFilter: gql`
+ fragment CreateTeamMembershipInput on CreateTeamMembershipInput {
+ teamId
+ email
+ role
+ }
+ `,
+ mode: MutationResponseModes.APPEND
+})(TeamMembersPage)
+
+TeamMembersPage = withMutation(gql`
+ mutation TeamMembershipMutation($id: ID!, $input: UpdateTeamMembershipInput!) {
+ updateTeamMembership(id: $id, input: $input) {
+ ...TeamMembersPage_teamMemberships
+ }
+ }
+
+ ${TeamMembersPage.fragments.teamMemberships}
+`, {
+ inputFilter: gql`
+ fragment UpdateTeamMembershipInput on UpdateTeamMembershipInput {
+ id
+ role
+ }
+ `,
+ optimistic: { mode: OptimisticResponseModes.UPDATE, response: { __typename: 'TeamMembership' } }
+})(TeamMembersPage)
+
+TeamMembersPage = withMutation(gql`
+ mutation DestroyTeamMembershipMutation($id: ID!) {
+ destroyTeamMembership(id: $id) {
+ id
+ }
+ }
+`, {
+ optimistic: { mode: OptimisticResponseModes.DESTROY, response: { __typename: 'TeamMembership' } },
+ mode: MutationResponseModes.DELETE,
+ successAlert: ({ input: { user: { email } } }) => ({
+ message: `Successfully removed ${email} from the team.`
+ })
+})(TeamMembersPage)
+
+TeamMembersPage = withQuery(gql`
+ query TeamMembersPage($teamId: ID) {
+ teamMemberships(teamId: $teamId) {
+ ...TeamMembersPage_teamMemberships
+ }
+ }
+
+ ${TeamMembersPage.fragments.teamMemberships}
+`, {
+ options: ({ match }) => ({
+ variables: { teamId: match.params.teamId }
+ })
+})(TeamMembersPage)
+
+export default TeamMembersPage
diff --git a/ui/src/components/pages/TeamPage.js b/ui/src/components/pages/TeamPage.js
new file mode 100644
index 0000000..4add177
--- /dev/null
+++ b/ui/src/components/pages/TeamPage.js
@@ -0,0 +1,63 @@
+import gql from 'graphql-tag'
+import Helmet from 'react-helmet-async'
+import React, { Fragment } from 'react'
+import { Redirect, Route, Switch } from 'react-router-dom'
+
+import ProjectPage from 'components/pages/ProjectPage'
+import ProjectsPage from 'components/pages/ProjectsPage'
+import TeamBillingPage from 'components/pages/TeamBillingPage'
+import TeamMembersPage from 'components/pages/TeamMembersPage'
+import TeamSettingsPage from 'components/pages/TeamSettingsPage'
+import { withQuery } from 'lib/data'
+
+function TeamPage({ match, team }) {
+ if (!team) {
+ // TODO: Handle NOT_FOUND: The resource you are looking for does not exist.
+ return null
+ }
+
+ return (
+
+
+
+ {team.name}
+
+
+
+
+ { /* `exact` ensures that Project* pages get rendered */ }
+
+
+
+
+
+
+
+
+ )
+}
+
+TeamPage.fragments = {
+ team: gql`
+ fragment TeamPage_team on Team {
+ id
+ name
+ }
+ `
+}
+
+TeamPage = withQuery(gql`
+ query TeamPage($id: ID!) {
+ team(id: $id) {
+ ...TeamPage_team
+ }
+ }
+
+ ${TeamPage.fragments.team}
+`, {
+ options: ({ match }) => ({
+ variables: { id: match.params.teamId }
+ })
+})(TeamPage)
+
+export default TeamPage
diff --git a/ui/src/components/pages/TeamSettingsPage.js b/ui/src/components/pages/TeamSettingsPage.js
new file mode 100644
index 0000000..91bcb29
--- /dev/null
+++ b/ui/src/components/pages/TeamSettingsPage.js
@@ -0,0 +1,241 @@
+import _ from 'lodash'
+import gql from 'graphql-tag'
+import Helmet from 'react-helmet-async'
+import React, { Component, Fragment } from 'react'
+
+import AppContext from 'components/AppContext'
+import ChangeTeamNameForm from 'components/internal/forms/ChangeTeamNameForm'
+import CreateTransferRequestForm from 'components/internal/forms/CreateTransferRequestForm'
+import FilledButton from 'components/buttons/FilledButton'
+import ItemBar from 'components/ItemBar'
+import Loader from 'components/internal/Loader'
+import Spacer from 'components/Spacer'
+import Text from 'components/typography/Text'
+import { PageTitle, PanelText, TextLink } from 'components/internal/typography'
+import { Panel, PanelBody, PanelContainer, PanelHeader } from 'components/internal/panel'
+import { User } from 'models'
+import { withQuery, withMutation } from 'lib/data'
+
+class TeamSettingsPage extends Component {
+ onCancelTransferRequest = () => {
+ const { cancelTransferRequest, team } = this.props
+
+ cancelTransferRequest({ id: team.id })
+ }
+
+ transferableUsersList = () => {
+ const { teamMemberships } = this.props
+
+ const options = teamMemberships
+ .filter(({ role }) => role !== 'owner')
+ .map(({ user: { id, firstName, lastName, email } }) => {
+ const fullName = User.fullName({ firstName, lastName })
+ const label = fullName.length ? `${fullName} (${email})` : email
+
+ return { value: id, label }
+ })
+
+ return _.orderBy(options, [ option => option.label.toLowerCase() ], [ 'asc' ])
+ }
+
+ renderChangeTeamNamePanel = () => {
+ const { team, updateTeam } = this.props
+
+ return (
+
+
+ Change Team Name
+
+
+
+
+
+ )
+ }
+
+ renderTransferOwnershipPanel = (currentUser) => {
+ const {
+ createTransferRequest,
+ teamMemberships,
+ team: { id, isTransferRequested, transferOwner }
+ } = this.props
+
+ const owner = teamMemberships.find(({ role }) => role === 'owner').user
+ const isOwner = owner.id === currentUser.id
+
+ const transferableUsersList = this.transferableUsersList()
+
+ return (
+
+
+ Transfer Ownership
+
+
+ {isOwner && !isTransferRequested && (
+
+
+ You (
+ {currentUser.email}
+ ) are the current owner of this team. Once you transfer this team,
+ {' '}
+ only the new owner can transfer it back.
+
+
+ {transferableUsersList.length ? (
+
+ ) : (
+
+ You need to
+ {' '}
+ invite someone
+ {' '}
+ to your team before you can transfer ownership.
+
+ )}
+
+ )}
+ {isOwner && isTransferRequested && (
+
+
+ Waiting for
+ {' '}
+ {User.fullName(transferOwner)}
+ {` (${transferOwner.email}) `}
+ to accept your transfer request.
+
+
+
+ )}
+ {!isOwner && (
+
+ Only the team owner,
+ {' '}
+ {User.fullName(owner)}
+ {` (${owner.email}), `}
+ can transfer ownership of this team.
+
+ )}
+
+
+ )
+ }
+
+ render() {
+ const { loading } = this.props
+
+ return (
+
+ {({ currentUser }) => (
+
+
+
+ Team Settings
+
+
+
+
+ Settings
+
+
+ {loading ? (
+
+ ) : (
+
+ {this.renderChangeTeamNamePanel()}
+ {this.renderTransferOwnershipPanel(currentUser)}
+
+ )}
+
+ )}
+
+ )
+ }
+}
+
+TeamSettingsPage = withQuery(gql`
+ query TeamQuery($id: ID!) {
+ team(id: $id) {
+ id
+ name
+ isTransferRequested
+ transferOwner {
+ id
+ email
+ firstName
+ lastName
+ }
+ }
+
+ teamMemberships(teamId: $id) {
+ id
+ role
+
+ user {
+ id
+ email
+ firstName
+ lastName
+ }
+ }
+ }
+`, {
+ options: ({ match }) => ({
+ variables: { id: match.params.teamId }
+ })
+})(TeamSettingsPage)
+
+TeamSettingsPage = withMutation(gql`
+ mutation UpdateTeamMutation($id: ID!, $input: UpdateTeamInput!) {
+ updateTeam(id: $id, input: $input) {
+ id
+ name
+ }
+ }
+`, {
+ successAlert: ({ input: { name } }) => ({
+ message: `Successfully changed team name to: ${name}`
+ })
+})(TeamSettingsPage)
+
+TeamSettingsPage = withMutation(gql`
+ mutation CreateTransferRequestMutator($id: ID!, $input: CreateTransferRequestInput!) {
+ createTransferRequest(id: $id, input: $input) {
+ id
+ isTransferRequested
+
+ transferOwner {
+ id
+ firstName
+ lastName
+ email
+ }
+ }
+ }
+`, {
+ successAlert: {
+ message: 'Successfully created the transfer request.'
+ }
+})(TeamSettingsPage)
+
+TeamSettingsPage = withMutation(gql`
+ mutation CancelTransferRequestMutation($id: ID!) {
+ cancelTransferRequest(id: $id) {
+ id
+ isTransferRequested
+ }
+ }
+`, {
+ successAlert: {
+ title: 'Canceled.',
+ message: 'Successfully canceled the transfer request.'
+ }
+})(TeamSettingsPage)
+
+export default TeamSettingsPage
diff --git a/ui/src/components/pages/TeamsPage.js b/ui/src/components/pages/TeamsPage.js
new file mode 100644
index 0000000..e546bf8
--- /dev/null
+++ b/ui/src/components/pages/TeamsPage.js
@@ -0,0 +1,100 @@
+import gql from 'graphql-tag'
+import Helmet from 'react-helmet-async'
+import React, { Component, Fragment } from 'react'
+
+import DataTiles from 'components/internal/DataTiles'
+import IconButton from 'components/internal/buttons/IconButton'
+import Loader from 'components/internal/Loader'
+import TeamDialog from 'components/internal/dialogs/TeamDialog'
+import TeamPage from 'components/pages/TeamPage'
+import { MutationResponseModes, OptimisticResponseModes, withMutation, withQuery } from 'lib/data'
+import { PageTitle } from 'components/internal/typography'
+
+class TeamsPage extends Component {
+ constructor() {
+ super()
+
+ this.state = {
+ isTeamDialogOpen: false
+ }
+ }
+
+ openTeamDialog = () => this.setState({ isTeamDialogOpen: true })
+
+ closeTeamDialog = () => this.setState({ isTeamDialogOpen: false })
+
+ handleFormSubmit = (values) => {
+ const { createTeam, history } = this.props
+
+ return createTeam(values, {
+ onSuccess: ({ data: { team: { id } } }) => {
+ this.closeTeamDialog()
+ history.push(`/teams/${id}`)
+ }
+ })
+ }
+
+ render() {
+ const { loading, teams } = this.props
+ const { isTeamDialogOpen } = this.state
+
+ return (
+
+
+
+ Teams
+
+
+
+
+ Teams
+
+
+
+
+
+
+ `/teams/${id}`} />
+
+
+
+ )
+ }
+}
+
+TeamsPage = withMutation(gql`
+ mutation CreateTeamMutation($input: CreateTeamInput!) {
+ team: createTeam(input: $input) {
+ ...TeamPage_team
+ }
+ }
+
+ ${TeamPage.fragments.team}
+`, {
+ mode: MutationResponseModes.APPEND,
+ optimistic: { mode: OptimisticResponseModes.CREATE, response: { __typename: 'Team' } }
+})(TeamsPage)
+
+export default withQuery(gql`
+ query TeamsPageQuery {
+ teams {
+ ...TeamPage_team
+ }
+ }
+
+ ${TeamPage.fragments.team}
+`)(TeamsPage)
diff --git a/ui/src/components/pages/TermsOfServicePage.js b/ui/src/components/pages/TermsOfServicePage.js
new file mode 100644
index 0000000..601aee9
--- /dev/null
+++ b/ui/src/components/pages/TermsOfServicePage.js
@@ -0,0 +1,354 @@
+import Helmet from 'react-helmet-async'
+import React, { Fragment } from 'react'
+
+import GridContainer from 'components/external/GridContainer'
+import GridItem from 'components/external/GridItem'
+import {
+ PageHeading,
+ PageSubHeading,
+ PageText
+} from 'components/external/typography'
+
+const lastUpdated = 'August 15, 2019'
+
+function TermsOfServicePage() {
+ return (
+
+
+
+ Terms of Service
+
+
+
+
+
+
+ Terms of Service
+
+
+
+ Last Updated:
+ {' '}
+ {lastUpdated}
+
+
+
+ These Terms of Service (“Terms”, “Terms of Service”) govern your relationship with
+ https://claycms.io website (the “Service”) operated by KeepWorks Technologies Pvt Ltd
+ (“us”, “we”, or “our”).
+
+
+
+ Please read these Terms of Service carefully before using the Service.
+
+
+
+ Your access to and use of the Service is conditioned on your acceptance of and
+ compliance with these Terms. These Terms apply to all visitors, users and others who
+ access or use the Service.
+
+
+
+ By accessing or using the Service you agree to be bound by these Terms. If you disagree
+ with any part of the terms then you may not access the Service.
+
+
+
+ Subscriptions
+
+
+
+ Some parts of the Service are billed on a subscription basis (“Subscription(s)”). You
+ will be billed in advance on a recurring and periodic basis (“Billing Cycle”). Billing
+ cycles are set either on a monthly or annual basis, depending on the type of
+ subscription plan you select when purchasing a Subscription.
+
+
+
+ At the end of each Billing Cycle, your Subscription will automatically renew under the
+ exact same conditions unless you cancel it or KeepWorks Technologies Pvt Ltd cancels it.
+ You may cancel your Subscription renewal either through your online account management
+ page or by contacting KeepWorks Technologies Pvt Ltd customer support team.
+
+
+
+ A valid payment method, including credit card, is required to process the payment for
+ your Subscription. You shall provide KeepWorks Technologies Pvt Ltd with accurate and
+ complete billing information including full name, address, state, zip code, telephone
+ number, and a valid payment method information. By submitting such payment information,
+ you automatically authorize KeepWorks Technologies Pvt Ltd to charge all Subscription
+ fees incurred through your account to any such payment instruments.
+
+
+
+ Should automatic billing fail to occur for any reason, KeepWorks Technologies Pvt Ltd
+ will issue an electronic invoice indicating that you must proceed manually, within a
+ certain deadline date, with the full payment corresponding to the billing period as
+ indicated on the invoice.
+
+
+
+ Free Trial
+
+
+
+ KeepWorks Technologies Pvt Ltd may, at its sole discretion, offer a Subscription with a
+ free trial for a limited period of time (“Free Trial”).
+
+
+
+ You may be required to enter your billing information in order to sign up for the Free
+ Trial.
+
+
+
+ If you do enter your billing information when signing up for the Free Trial, you will
+ not be charged by KeepWorks Technologies Pvt Ltd until the Free Trial has expired. On
+ the last day of the Free Trial period, unless you cancelled your Subscription, you will
+ be automatically charged the applicable Subscription fees for the type of Subscription
+ you have selected.
+
+
+
+ At any time and without notice, KeepWorks Technologies Pvt Ltd reserves the right to
+ (i) modify the terms and conditions of the Free Trial offer, or (ii) cancel such Free
+ Trial offer.
+
+
+
+ Fee Changes
+
+
+
+ KeepWorks Technologies Pvt Ltd, in its sole discretion and at any time, may modify the
+ Subscription fees for the Subscriptions. Any Subscription fee change will become
+ effective at the end of the then-current Billing Cycle.
+
+
+
+ KeepWorks Technologies Pvt Ltd will provide you with a reasonable prior notice of any
+ change in Subscription fees to give you an opportunity to terminate your Subscription
+ before such change becomes effective.
+
+
+
+ Your continued use of the Service after the Subscription fee change comes into effect
+ constitutes your agreement to pay the modified Subscription fee amount.
+
+
+
+ Refunds
+
+
+
+ Certain refund requests for Subscriptions may be considered by KeepWorks Technologies
+ Pvt Ltd on a case-by-case basis and granted in sole discretion of KeepWorks Technologies
+ Pvt Ltd.
+
+
+
+ Content
+
+
+
+ Our Service allows you to post, link, store, share and otherwise make available certain
+ information, text, graphics, videos, or other material (“Content”). You are responsible
+ for the Content that you post to the Service, including its legality, reliability, and
+ appropriateness.
+
+
+
+ By posting Content to the Service, you grant us the right and license to use, modify,
+ perform, display, reproduce, and distribute such Content on and through the Service. You
+ retain any and all of your rights to any Content you submit, post or display on or
+ through the Service and you are responsible for protecting those rights.
+
+
+
+ You represent and warrant that: (i) the Content is yours (you own it) or you have the
+ right to use it and grant us the rights and license as provided in these Terms, and (ii)
+ the posting of your Content on or through the Service does not violate the privacy
+ rights, publicity rights, copyrights, contract rights or any other rights of any person.
+
+
+
+ Accounts
+
+
+
+ When you create an account with us, you must provide us information that is accurate,
+ complete, and current at all times. Failure to do so constitutes a breach of the Terms,
+ which may result in immediate termination of your account on our Service.
+
+
+
+ You are responsible for safeguarding the password that you use to access the Service and
+ for any activities or actions under your password, whether your password is with our
+ Service or a third-party service.
+
+
+
+ You agree not to disclose your password to any third party. You must notify us
+ immediately upon becoming aware of any breach of security or unauthorized use of your
+ account.
+
+
+
+ Copyright Policy
+
+
+
+ We respect the intellectual property rights of others. It is our policy to respond to
+ any claim that Content posted on the Service infringes the copyright or other
+ intellectual property infringement (“Infringement”) of any person.
+
+
+
+ If you are a copyright owner, or authorized on behalf of one, and you believe that the
+ copyrighted work has been copied in a way that constitutes copyright infringement that
+ is taking place through the Service, you must submit your notice in writing to the
+ attention of “Copyright Infringement” of legal@claycms.io and include in your notice a
+ detailed description of the alleged Infringement.
+
+
+
+ You may be held accountable for damages (including costs and attorneys’ fees) for
+ misrepresenting that any Content is infringing your copyright.
+
+
+
+ Intellectual Property
+
+
+
+ The Service and its original content (excluding Content provided by users), features and
+ functionality are and will remain the exclusive property of KeepWorks Technologies Pvt
+ Ltd and its licensors. The Service is protected by copyright, trademark, and other laws
+ of both the India and foreign countries. Our trademarks and trade dress may not be used
+ in connection with any product or service without the prior written consent of KeepWorks
+ Technologies Pvt Ltd.
+
+
+
+ Links To Other Web Sites
+
+
+
+ Our Service may contain links to third-party web sites or services that are not owned or
+ controlled by KeepWorks Technologies Pvt Ltd.
+
+
+
+ KeepWorks Technologies Pvt Ltd has no control over, and assumes no responsibility for,
+ the content, privacy policies, or practices of any third party web sites or services.
+ You further acknowledge and agree that KeepWorks Technologies Pvt Ltd shall not be
+ responsible or liable, directly or indirectly, for any damage or loss caused or alleged
+ to be caused by or in connection with use of or reliance on any such content, goods or
+ services available on or through any such web sites or services.
+
+
+
+ We strongly advise you to read the terms and conditions and privacy policies of any
+ third-party web sites or services that you visit.
+
+
+
+ Termination
+
+
+
+ We may terminate or suspend your account immediately, without prior notice or liability,
+ for any reason whatsoever, including without limitation if you breach the Terms.
+
+
+
+ Upon termination, your right to use the Service will immediately cease. If you wish to
+ terminate your account, you may simply discontinue using the Service.
+
+
+
+ Limitation Of Liability
+
+
+
+ In no event shall KeepWorks Technologies Pvt Ltd, nor its directors, employees,
+ partners, agents, suppliers, or affiliates, be liable for any indirect, incidental,
+ special, consequential or punitive damages, including without limitation, loss of
+ profits, data, use, goodwill, or other intangible losses, resulting from (i) your access
+ to or use of or inability to access or use the Service; (ii) any conduct or content of
+ any third party on the Service; (iii) any content obtained from the Service; and (iv)
+ unauthorized access, use or alteration of your transmissions or content, whether based
+ on warranty, contract, tort (including negligence) or any other legal theory, whether or
+ not we have been informed of the possibility of such damage, and even if a remedy set
+ forth herein is found to have failed of its essential purpose.
+
+
+
+ Disclaimer
+
+
+
+ Your use of the Service is at your sole risk. The Service is provided on an “AS IS” and
+ “AS AVAILABLE” basis. The Service is provided without warranties of any kind, whether
+ express or implied, including, but not limited to, implied warranties of
+ merchantability, fitness for a particular purpose, non-infringement or course of
+ performance.
+
+
+
+ KeepWorks Technologies Pvt Ltd its subsidiaries, affiliates, and its licensors do not
+ warrant that a) the Service will function uninterrupted, secure or available at any
+ particular time or location; b) any errors or defects will be corrected; c) the Service
+ is free of viruses or other harmful components; or d) the results of using the Service
+ will meet your requirements.
+
+
+
+ Governing Law
+
+
+
+ These Terms shall be governed and construed in accordance with the laws of Karnataka,
+ India, without regard to its conflict of law provisions.
+
+
+
+ Our failure to enforce any right or provision of these Terms will not be considered a
+ waiver of those rights. If any provision of these Terms is held to be invalid or
+ unenforceable by a court, the remaining provisions of these Terms will remain in effect.
+ These Terms constitute the entire agreement between us regarding our Service, and
+ supersede and replace any prior agreements we might have between us regarding the
+ Service.
+
+
+
+ Changes
+
+
+
+ We reserve the right, at our sole discretion, to modify or replace these Terms at any
+ time. If a revision is material we will try to provide at least 30 days notice prior to
+ any new terms taking effect. What constitutes a material change will be determined at
+ our sole discretion.
+
+
+
+ By continuing to access or use our Service after those revisions become effective, you
+ agree to be bound by the revised terms. If you do not agree to the new terms, please
+ stop using the Service.
+
+
+
+ Contact Us
+
+
+
+ If you have any questions about these Terms, please contact us.
+
+
+
+
+ )
+}
+
+export default TermsOfServicePage
diff --git a/ui/src/components/pages/TransferRequestPage.js b/ui/src/components/pages/TransferRequestPage.js
new file mode 100644
index 0000000..5c6aa68
--- /dev/null
+++ b/ui/src/components/pages/TransferRequestPage.js
@@ -0,0 +1,60 @@
+import gql from 'graphql-tag'
+import Helmet from 'react-helmet-async'
+import React, { Component, Fragment } from 'react'
+
+import Loader from 'components/internal/Loader'
+
+import { withMutation } from 'lib/data'
+
+class TransferRequestPage extends Component {
+ componentDidMount() {
+ const { match: { params }, acceptTransferRequest, history, rejectTransferRequest } = this.props
+
+ const action = params.action === 'accept' ? acceptTransferRequest : rejectTransferRequest
+
+ action(params).finally(() => {
+ history.push('/user/teams')
+ })
+ }
+
+ render() {
+ return (
+
+
+
+ Processing Transfer Request...
+
+
+
+
+ )
+ }
+}
+
+TransferRequestPage = withMutation(gql`
+ mutation AcceptTransferRequestMutation($input: AcceptTransferRequestInput!) {
+ acceptTransferRequest(input: $input) {
+ success
+ }
+ }
+`, {
+ successAlert: {
+ title: 'Accepted.',
+ message: 'Successfully accepted the transfer request.'
+ }
+})(TransferRequestPage)
+
+TransferRequestPage = withMutation(gql`
+ mutation RejectTransferRequestMutation($input: RejectTransferRequestInput!) {
+ rejectTransferRequest(input: $input) {
+ success
+ }
+ }
+`, {
+ successAlert: {
+ title: 'Rejected.',
+ message: 'Successfully rejected the transfer request.'
+ }
+})(TransferRequestPage)
+
+export default TransferRequestPage
diff --git a/ui/src/components/pages/UserNotificationsPage.js b/ui/src/components/pages/UserNotificationsPage.js
new file mode 100644
index 0000000..9572ad2
--- /dev/null
+++ b/ui/src/components/pages/UserNotificationsPage.js
@@ -0,0 +1,22 @@
+import Helmet from 'react-helmet-async'
+import React, { Fragment } from 'react'
+
+import { PageTitle } from 'components/internal/typography'
+
+function UserNotificationsPage() {
+ return (
+
+
+
+ Notifications
+
+
+
+
+ Notifications
+
+
+ )
+}
+
+export default UserNotificationsPage
diff --git a/ui/src/components/pages/UserPage.js b/ui/src/components/pages/UserPage.js
new file mode 100644
index 0000000..bb4fa20
--- /dev/null
+++ b/ui/src/components/pages/UserPage.js
@@ -0,0 +1,20 @@
+import React from 'react'
+import { Route, Switch } from 'react-router-dom'
+
+import TeamsPage from 'components/pages/TeamsPage'
+import UserProfilePage from 'components/pages/UserProfilePage'
+import UserNotificationsPage from 'components/pages/UserNotificationsPage'
+import UserSettingsPage from 'components/pages/UserSettingsPage'
+
+function UserPage({ match }) {
+ return (
+
+
+
+
+
+
+ )
+}
+
+export default UserPage
diff --git a/ui/src/components/pages/UserProfilePage.js b/ui/src/components/pages/UserProfilePage.js
new file mode 100644
index 0000000..066eb4d
--- /dev/null
+++ b/ui/src/components/pages/UserProfilePage.js
@@ -0,0 +1,130 @@
+import _ from 'lodash'
+import gql from 'graphql-tag'
+import Helmet from 'react-helmet-async'
+import injectSheet from 'react-jss'
+import React, { Fragment, Component } from 'react'
+
+import AppContext from 'components/AppContext'
+import UpdateProfileForm from 'components/internal/forms/UpdateProfileForm'
+import Column from 'components/internal/Column'
+import IconButton from 'components/internal/buttons/IconButton'
+import PictureModal from 'components/internal/modals/PictureModal'
+import ProfilePicture from 'components/internal/ProfilePicture'
+import Row from 'components/internal/Row'
+import { PageTitle } from 'components/internal/typography'
+import { Panel, PanelBody, PanelContainer } from 'components/internal/panel'
+import { withMutation } from 'lib/data'
+
+class UserProfilePage extends Component {
+ constructor() {
+ super()
+
+ this.state = {
+ isPictureModalOpen: false
+ }
+ }
+
+ openPictureModal = () => this.setState({ isPictureModalOpen: true })
+
+ closePictureModal = () => this.setState({ isPictureModalOpen: false })
+
+ onPictureFormSubmit = (values) => {
+ const { updateProfile } = this.props
+
+ return updateProfile({ profilePicture: values.file }, {
+ onSuccess: () => {
+ this.closePictureModal()
+ }
+ })
+ }
+
+ render() {
+ const { classes, updateProfile } = this.props
+ const { isPictureModalOpen } = this.state
+
+ return (
+
+ {({ currentUser }) => {
+ const changeNameInitialValues = _.pick(currentUser, [ 'firstName', 'lastName' ])
+
+ return (
+
+
+
+ Profile
+
+
+
+
+ Profile
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ )
+ }}
+
+ )
+ }
+}
+
+UserProfilePage = withMutation(gql`
+ mutation UpdateProfileMutation($input: UpdateProfileInput!) {
+ updateProfile(input: $input) {
+ id
+ firstName
+ lastName
+ profilePictureThumbnail
+ profilePictureNormal
+ }
+ }
+`, {
+ successAlert: {
+ message: 'Successfully updated your profile.'
+ }
+})(UserProfilePage)
+
+export default injectSheet({
+ wrapper: {
+ position: 'relative'
+ },
+ buttonWrapper: {
+ cursor: 'pointer',
+ position: 'absolute',
+ right: 0,
+ top: 0
+ }
+})(UserProfilePage)
diff --git a/ui/src/components/pages/UserSettingsPage.js b/ui/src/components/pages/UserSettingsPage.js
new file mode 100644
index 0000000..935573c
--- /dev/null
+++ b/ui/src/components/pages/UserSettingsPage.js
@@ -0,0 +1,50 @@
+import gql from 'graphql-tag'
+import Helmet from 'react-helmet-async'
+import React, { Fragment } from 'react'
+
+import ChangePasswordForm from 'components/internal/forms/ChangePasswordForm'
+import Text from 'components/typography/Text'
+import { Panel, PanelBody, PanelContainer, PanelHeader } from 'components/internal/panel'
+import { PageTitle } from 'components/internal/typography'
+import { withMutation } from 'lib/data'
+
+function UserSettingsPage({ updatePassword }) {
+ return (
+
+
+
+ Settings
+
+
+
+
+ Settings
+
+
+
+
+
+ Change Password
+
+
+
+
+
+
+
+ )
+}
+
+UserSettingsPage = withMutation(gql`
+ mutation UpdatePasswordMutation($input: UpdatePasswordInput!) {
+ updatePassword(input: $input) {
+ success
+ }
+ }
+`, {
+ successAlert: {
+ message: 'Successfully changed your password.'
+ }
+})(UserSettingsPage)
+
+export default UserSettingsPage
diff --git a/ui/src/components/routers/ExternalRouter.js b/ui/src/components/routers/ExternalRouter.js
new file mode 100644
index 0000000..55f5e80
--- /dev/null
+++ b/ui/src/components/routers/ExternalRouter.js
@@ -0,0 +1,31 @@
+import React, { Suspense } from 'react'
+import { Route, Switch } from 'react-router-dom'
+
+import AppLoader from 'components/AppLoader'
+import ExternalRoute from 'components/routes/ExternalRoute'
+import HomePage from 'components/pages/HomePage'
+import lazy from 'lib/lazy'
+import PrivacyPolicyPage from 'components/pages/PrivacyPolicyPage'
+import TermsOfServicePage from 'components/pages/TermsOfServicePage'
+
+const OnboardingRouter = lazy(() => import('components/routers/OnboardingRouter'))
+
+function ExternalRouter() {
+ return (
+
+
+
+
+
+ (
+ }>
+
+
+ )}
+ />
+
+ )
+}
+
+export default ExternalRouter
diff --git a/ui/src/components/routers/InternalRouter.js b/ui/src/components/routers/InternalRouter.js
new file mode 100644
index 0000000..deb9222
--- /dev/null
+++ b/ui/src/components/routers/InternalRouter.js
@@ -0,0 +1,27 @@
+import React from 'react'
+import { Route, Switch } from 'react-router-dom'
+
+import InternalRoute from 'components/routes/InternalRoute'
+import TeamPage from 'components/pages/TeamPage'
+import TransferRequestPage from 'components/pages/TransferRequestPage'
+import UserPage from 'components/pages/UserPage'
+
+function InternalRouter() {
+ return (
+
+
+
+
+
+ (
+
+ Not Found
+
+ )}
+ />
+
+ )
+}
+
+export default InternalRouter
diff --git a/ui/src/components/routers/OnboardingRouter.js b/ui/src/components/routers/OnboardingRouter.js
new file mode 100644
index 0000000..76f3f8f
--- /dev/null
+++ b/ui/src/components/routers/OnboardingRouter.js
@@ -0,0 +1,35 @@
+import React, { Suspense } from 'react'
+import { Route, Switch } from 'react-router-dom'
+
+import AppLoader from 'components/AppLoader'
+import ConfirmPage from 'components/pages/ConfirmPage'
+import ForgotPasswordPage from 'components/pages/ForgotPasswordPage'
+import lazy from 'lib/lazy'
+import LoginPage from 'components/pages/LoginPage'
+import OnboardingRoute from 'components/routes/OnboardingRoute'
+import ResetPasswordPage from 'components/pages/ResetPasswordPage'
+import SignupPage from 'components/pages/SignupPage'
+
+const InternalRouter = lazy(() => import('components/routers/InternalRouter'))
+
+function OnboardingRouter() {
+ return (
+
+
+
+
+
+
+
+ (
+ }>
+
+
+ )}
+ />
+
+ )
+}
+
+export default OnboardingRouter
diff --git a/ui/src/components/routes/ExternalRoute.js b/ui/src/components/routes/ExternalRoute.js
new file mode 100644
index 0000000..0db9bee
--- /dev/null
+++ b/ui/src/components/routes/ExternalRoute.js
@@ -0,0 +1,17 @@
+import React from 'react'
+
+import ExternalLayout from 'components/layouts/ExternalLayout'
+import ProtectedRoute from 'components/routes/ProtectedRoute'
+import { User } from 'models'
+
+function ExternalRoute(props) {
+ return (
+
+ )
+}
+
+export default ExternalRoute
diff --git a/ui/src/components/routes/InternalFluidRoute.js b/ui/src/components/routes/InternalFluidRoute.js
new file mode 100644
index 0000000..60d3576
--- /dev/null
+++ b/ui/src/components/routes/InternalFluidRoute.js
@@ -0,0 +1,18 @@
+import React from 'react'
+
+import InternalLayout from 'components/layouts/InternalLayout'
+import ProtectedRoute from 'components/routes/ProtectedRoute'
+import { User } from 'models'
+
+function InternalFluidRoute(props) {
+ return (
+
+ )
+}
+
+export default InternalFluidRoute
diff --git a/ui/src/components/routes/InternalRoute.js b/ui/src/components/routes/InternalRoute.js
new file mode 100644
index 0000000..5d4b321
--- /dev/null
+++ b/ui/src/components/routes/InternalRoute.js
@@ -0,0 +1,17 @@
+import React from 'react'
+
+import InternalLayout from 'components/layouts/InternalLayout'
+import ProtectedRoute from 'components/routes/ProtectedRoute'
+import { User } from 'models'
+
+function InternalRoute(props) {
+ return (
+
+ )
+}
+
+export default InternalRoute
diff --git a/ui/src/components/routes/OnboardingRoute.js b/ui/src/components/routes/OnboardingRoute.js
new file mode 100644
index 0000000..38c5041
--- /dev/null
+++ b/ui/src/components/routes/OnboardingRoute.js
@@ -0,0 +1,17 @@
+import React from 'react'
+
+import OnboardingLayout from 'components/layouts/OnboardingLayout'
+import ProtectedRoute from 'components/routes/ProtectedRoute'
+import { User } from 'models'
+
+function OnboardingRoute(props) {
+ return (
+
+ )
+}
+
+export default OnboardingRoute
diff --git a/ui/src/components/routes/ProtectedRoute.js b/ui/src/components/routes/ProtectedRoute.js
new file mode 100644
index 0000000..475fc56
--- /dev/null
+++ b/ui/src/components/routes/ProtectedRoute.js
@@ -0,0 +1,67 @@
+import PropTypes from 'prop-types'
+import React from 'react'
+import { Redirect, Route } from 'react-router-dom'
+
+import AppContext from 'components/AppContext'
+import { User } from 'models'
+import { withClientMutation } from 'lib/data'
+
+import SET_REFERRER from 'mutations/referrer'
+
+function ProtectedRoute({
+ component: Component,
+ layout: Layout,
+ layoutProps,
+ location,
+ requiredUserSessionState,
+ setReferrer,
+ ...other
+}) {
+ return (
+ (
+
+ {({ currentUser, referrer }) => {
+ if (requiredUserSessionState === User.sessionStates.LOGGED_OUT && currentUser) {
+ const route = (referrer && referrer.url) || '/user/teams'
+
+ setReferrer({ url: null })
+
+ return
+ }
+
+ if (requiredUserSessionState === User.sessionStates.LOGGED_IN && !currentUser) {
+ const { pathname, search, hash } = location
+
+ setReferrer({ url: (pathname || '') + (search || '') + (hash || '') })
+
+ return
+ }
+
+ return (
+
+
+
+ )
+ }}
+
+ )}
+ {...other}
+ />
+ )
+}
+
+ProtectedRoute.propTypes = {
+ component: PropTypes.func.isRequired,
+ layout: PropTypes.func.isRequired,
+ layoutProps: PropTypes.object,
+ requiredUserSessionState: PropTypes.oneOf(Object.values(User.sessionStates)).isRequired
+}
+
+ProtectedRoute.defaultProps = {
+ layoutProps: null
+}
+
+ProtectedRoute = withClientMutation(SET_REFERRER)(ProtectedRoute)
+
+export default ProtectedRoute
diff --git a/ui/src/components/typography/BaseText.js b/ui/src/components/typography/BaseText.js
new file mode 100644
index 0000000..ffa3e3e
--- /dev/null
+++ b/ui/src/components/typography/BaseText.js
@@ -0,0 +1,83 @@
+import classNames from 'classnames'
+import injectSheet from 'react-jss'
+import PropTypes from 'prop-types'
+import React from 'react'
+
+function BaseText({
+ tag: TextTag,
+ variant,
+ classes,
+ children,
+ className
+}) {
+ return (
+
+ {children}
+
+ )
+}
+
+BaseText.propTypes = {
+ tag: PropTypes.oneOf([ 'div', 'h1', 'h2', 'h3', 'p', 'span' ]).isRequired,
+ align: PropTypes.oneOf([ 'left', 'center', 'right', 'inherit' ]),
+ className: PropTypes.string,
+ color: PropTypes.oneOf([ 'primary', 'pale', 'light', 'dark', 'darker', 'error' ]),
+ variant: PropTypes.oneOf([
+ 'lightSquished',
+ 'lightSmall',
+ 'light',
+ 'lightMedium',
+ 'lightMediumResponsive',
+ 'lightLarge',
+ 'lightExtraLarge',
+ 'lightExtraLargeSquished',
+ 'regularSmallCompact',
+ 'regularSmall',
+ 'regularSmallSpaced',
+ 'regularSmallSquished',
+ 'regularSmallSpacedSquished',
+ 'regularSmallSquishedResponsive',
+ 'regular',
+ 'regularSpaced',
+ 'regularSquished',
+ 'regularSquishedResponsive',
+ 'regularMedium',
+ 'regularMediumResponsive',
+ 'regularMediumSquished',
+ 'regularLarge',
+ 'medium',
+ 'semiboldExtraSmallSquished',
+ 'semiboldSmallSquished',
+ 'semiboldSmallSpaced',
+ 'semiboldSmall',
+ 'semiboldSmallResponsive',
+ 'semiboldSquished',
+ 'semiboldSquishedResponsive',
+ 'semibold',
+ 'semiboldMedium',
+ 'semiboldLarge',
+ 'semiboldLargeSquished',
+ 'semiboldExtraLarge',
+ 'bold',
+ 'boldMedium',
+ 'boldLargeResponsive',
+ 'boldExtraLargeResponsive',
+ 'blackLarge'
+ ])
+}
+
+BaseText.defaultProps = {
+ align: 'inherit',
+ className: '',
+ color: 'light',
+ variant: 'regular'
+}
+
+export default injectSheet(({ colors, typography }) => ({
+ ...typography,
+
+ text: ({ align, color }) => ({
+ color: `${colors[`text_${color}`]}`,
+ textAlign: align
+ })
+}))(BaseText)
diff --git a/ui/src/components/typography/IconLink.js b/ui/src/components/typography/IconLink.js
new file mode 100644
index 0000000..b6a07bc
--- /dev/null
+++ b/ui/src/components/typography/IconLink.js
@@ -0,0 +1,61 @@
+import classNames from 'classnames'
+import injectSheet from 'react-jss'
+import PropTypes from 'prop-types'
+import React from 'react'
+import { Link } from 'react-router-dom'
+
+import cleanProps from 'lib/cleanProps'
+import FontIcon from 'components/FontIcon'
+
+function IconLink({ href, icon, iconSize, to, children, className, classes, ...other }) {
+ const LinkTag = to ? Link : 'a'
+
+ const linkProps = {}
+
+ if (to) {
+ linkProps.to = to
+ } else if (href) {
+ linkProps.href = href
+ linkProps.target = '_blank'
+ }
+
+ return (
+
+ {children}
+ {icon && }
+
+ )
+}
+
+IconLink.propTypes = {
+ children: PropTypes.node.isRequired,
+ href: PropTypes.string,
+ icon: PropTypes.string,
+ to: PropTypes.oneOfType([ PropTypes.string, PropTypes.object ])
+}
+
+IconLink.defaultProps = {
+ href: null,
+ icon: null,
+ to: null
+}
+
+IconLink = injectSheet(({ colors, typography, units }) => ({
+ iconLinkWrapper: {
+ ...typography.mediumSquished,
+
+ alignItems: 'center',
+ color: colors.iconLink,
+ display: 'flex',
+
+ '& .icon': {
+ marginLeft: units.iconLinkIconMarginLeft
+ }
+ }
+}))(IconLink)
+
+export default IconLink
diff --git a/ui/src/components/typography/Paragraph.js b/ui/src/components/typography/Paragraph.js
new file mode 100644
index 0000000..c2ddad8
--- /dev/null
+++ b/ui/src/components/typography/Paragraph.js
@@ -0,0 +1,13 @@
+import React from 'react'
+
+import BaseText from 'components/typography/BaseText'
+
+function Paragraph({ children, ...other }) {
+ return (
+
+ {children}
+
+ )
+}
+
+export default Paragraph
diff --git a/ui/src/components/typography/Text.js b/ui/src/components/typography/Text.js
new file mode 100644
index 0000000..eb1cf82
--- /dev/null
+++ b/ui/src/components/typography/Text.js
@@ -0,0 +1,13 @@
+import React from 'react'
+
+import BaseText from 'components/typography/BaseText'
+
+function Text({ children, ...other }) {
+ return (
+
+ {children}
+
+ )
+}
+
+export default Text
diff --git a/ui/src/components/typography/TextLink.js b/ui/src/components/typography/TextLink.js
new file mode 100644
index 0000000..9d93ad8
--- /dev/null
+++ b/ui/src/components/typography/TextLink.js
@@ -0,0 +1,40 @@
+import classNames from 'classnames'
+import PropTypes from 'prop-types'
+import React from 'react'
+import { Link } from 'react-router-dom'
+
+import cleanProps from 'lib/cleanProps'
+
+function TextLink({
+ href, to, children, className, classes, ...other
+}) {
+ const LinkTag = to ? Link : 'a'
+
+ const linkProps = {}
+
+ if (to) {
+ linkProps.to = to
+ } else if (href) {
+ linkProps.href = href
+ linkProps.target = '_blank'
+ }
+
+ return (
+
+ {children}
+
+ )
+}
+
+TextLink.propTypes = {
+ children: PropTypes.node.isRequired,
+ href: PropTypes.string,
+ to: PropTypes.oneOfType([ PropTypes.string, PropTypes.object ])
+}
+
+TextLink.defaultProps = {
+ href: null,
+ to: null
+}
+
+export default TextLink
diff --git a/ui/src/components/typography/index.js b/ui/src/components/typography/index.js
new file mode 100644
index 0000000..d34259b
--- /dev/null
+++ b/ui/src/components/typography/index.js
@@ -0,0 +1,3 @@
+export { default as Paragraph } from './Paragraph'
+export { default as Text } from './Text'
+export { default as TextLink } from './TextLink'
diff --git a/ui/src/constants/settings.js b/ui/src/constants/settings.js
new file mode 100644
index 0000000..aecda66
--- /dev/null
+++ b/ui/src/constants/settings.js
@@ -0,0 +1,3 @@
+export default Object.freeze({
+ SUPPORT_EMAIL_ADDRESS: 'support@claycms.io'
+})
diff --git a/ui/src/index.js b/ui/src/index.js
new file mode 100644
index 0000000..1176d81
--- /dev/null
+++ b/ui/src/index.js
@@ -0,0 +1,38 @@
+import * as Sentry from '@sentry/browser'
+import React from 'react'
+import { render } from 'react-dom'
+
+import * as models from 'models'
+import Root from './Root'
+
+const rootEl = document.getElementById('root')
+
+const renderClient = () => {
+ render(
+ ,
+ rootEl
+ )
+}
+
+const initErrorTracking = () => Sentry.init({
+ dsn: process.env.SENTRY_DSN,
+ release: process.env.HEROKU_SLUG_COMMIT
+})
+
+const autoBindModelMethods = () => {
+ Object.values(models).forEach((model) => {
+ const actions = Object.getOwnPropertyNames(model)
+
+ actions
+ .filter(action => typeof model[action] === 'function')
+ .forEach((action) => { model[action] = model[action].bind(model) })
+ })
+}
+
+const initApp = () => {
+ initErrorTracking()
+ autoBindModelMethods()
+ renderClient()
+}
+
+initApp()
diff --git a/ui/src/lib/cleanProps.js b/ui/src/lib/cleanProps.js
new file mode 100644
index 0000000..7897ab2
--- /dev/null
+++ b/ui/src/lib/cleanProps.js
@@ -0,0 +1,19 @@
+/*
+ * Removes extra props injected by HOCs
+ *
+ * Example Warning:
+ * "Unknown props `input`, `meta`, `theme` on tag. Remove these props from the element."
+ *
+ * More information: https://facebook.github.io/react/warnings/unknown-prop.html
+ */
+export default function (props, otherProps = []) {
+ const cleanedProps = Object.assign({}, props)
+
+ otherProps.forEach((prop) => {
+ delete cleanedProps[prop]
+ })
+ // react-jss
+ delete cleanedProps.theme
+
+ return cleanedProps
+}
diff --git a/ui/src/lib/data.js b/ui/src/lib/data.js
new file mode 100644
index 0000000..528df95
--- /dev/null
+++ b/ui/src/lib/data.js
@@ -0,0 +1,334 @@
+import { compose, graphql } from 'react-apollo'
+import { filter } from 'graphql-anywhere'
+
+import getRandomNumber from 'lib/getRandomNumber'
+import isPromise from 'lib/isPromise'
+import parseError from 'lib/errorParser'
+import { showAlertFailure, showAlertSuccess } from 'client/methods'
+
+const OptimisticResponseModes = Object.freeze({
+ CREATE: 'CREATE',
+ UPDATE: 'UPDATE',
+ DESTROY: 'DESTROY'
+})
+
+const MutationResponseModes = Object.freeze({
+ IGNORE: 'IGNORE',
+ APPEND: 'APPEND',
+ PREPEND: 'PREPEND',
+ DELETE: 'DELETE',
+ CUSTOM: 'CUSTOM'
+})
+
+// Helpers
+
+const findMutationMethodName = mutation => (
+ mutation.definitions[0].selectionSet.selections[0].name.value
+)
+
+const findMutationMethodNameOrAlias = (mutation) => {
+ const selection = mutation.definitions[0].selectionSet.selections[0]
+ return (selection.alias && selection.alias.value) || selection.name.value
+}
+
+const findQueryFieldNameOrAlias = (query) => {
+ const selection = query.definitions[0].selectionSet.selections[0]
+ return (selection.alias && selection.alias.value) || selection.name.value
+}
+
+const findAllQueryFieldNamesOrAliases = query => (
+ query.definitions[0].selectionSet.selections.map(selection => (
+ (selection.alias && selection.alias.value) || selection.name.value
+ ))
+)
+
+const addRecords = (
+ currentRecords = [],
+ responseRecords = [],
+ mode = MutationResponseModes.APPEND
+) => {
+ switch (mode) {
+ case MutationResponseModes.APPEND:
+ return currentRecords.push(...responseRecords)
+ case MutationResponseModes.PREPEND:
+ return currentRecords.unshift(...responseRecords)
+ default:
+ throw new Error('Incorrect `mode` specified when using addRecords.')
+ }
+}
+
+const deleteRecords = (currentRecords = [], responseRecords = [], key = 'id') => {
+ const deleteIds = responseRecords.map(record => record[key])
+
+ deleteIds.forEach((deleteId) => {
+ const index = currentRecords.findIndex(record => record[key] === deleteId)
+
+ if (index !== -1) {
+ currentRecords.splice(index, 1)
+ }
+ })
+}
+
+const optimisticCreateResponse = (other = {}) => ({ input }) => ({
+ id: getRandomNumber(),
+ createdAt: +new Date(),
+ updatedAt: +new Date(),
+ ...input,
+ ...other
+})
+
+const optimisticUpdateResponse = (other = {}) => ({ id, input }) => ({
+ id,
+ updatedAt: +new Date(),
+ ...input,
+ ...other
+})
+
+const optimisticDestroyResponse = (other = {}) => ({ id }) => ({
+ id,
+ ...other
+})
+
+// React apollo HOCs
+
+const withClientMutation = (mutation) => {
+ const methodName = findMutationMethodName(mutation)
+
+ return graphql(mutation, {
+ props: ({ mutate }) => ({
+ [methodName]: variables => mutate({ variables })
+ })
+ })
+}
+
+const withClientQuery = (query, config = {}) => {
+ const configOptions = config.options || {}
+ delete config.options
+
+ const configWithDefaults = Object.assign({
+ props: ({ data }) => ({ ...data }),
+ options: (props) => {
+ const defaultOptions = {
+ fetchPolicy: 'cache-only'
+ }
+
+ if (typeof configOptions === 'function') {
+ return {
+ ...defaultOptions,
+ ...configOptions(props)
+ }
+ }
+
+ return {
+ ...defaultOptions,
+ ...configOptions
+ }
+ }
+ }, config)
+
+ if (configWithDefaults.name) { // Automatically set by the field.
+ throw new Error('You cannot override the `name` when using `withClientQuery`.')
+ }
+
+ return graphql(query, configWithDefaults)
+}
+
+const withMutation = (mutation, {
+ context,
+ query: mutationQuery,
+ mode = MutationResponseModes.IGNORE,
+ inputFilter,
+ refetch = false,
+ optimistic,
+ updateData,
+ successAlert
+} = {}) => {
+ const methodName = findMutationMethodName(mutation)
+
+ return graphql(mutation, {
+ props: ({ mutate, ownProps: { query: componentQuery, variables, ...props } }) => ({
+ [methodName]: (values, { onSuccess, onFailure } = {}) => {
+ const { id, ...rawInput } = values || {}
+ const input = inputFilter ? filter(inputFilter, rawInput) : rawInput
+
+ const mutationConfig = {
+ variables: { id, input },
+ errorPolicy: 'none' // Ensure all errors are in catch block
+ }
+
+ const responseName = findMutationMethodNameOrAlias(mutation)
+
+ const query = mutationQuery || componentQuery
+
+ if (optimistic) {
+ const { mode: optimisticMode, response: optimisticResponse } = optimistic
+
+ if (optimisticResponse) {
+ const { __typename, ...other } = optimisticResponse
+
+ let response = null
+
+ if (!optimisticMode || !__typename) {
+ throw new Error('You must specify both `mode` and `__typename` for `optimistic` shorthand.')
+ }
+
+ if (!Object.prototype.hasOwnProperty.call(OptimisticResponseModes, optimisticMode)) {
+ throw new Error('Incorrect mode specified when using `optimistic` shorthand.')
+ }
+
+ if (optimisticMode === OptimisticResponseModes.CREATE) {
+ response = optimisticCreateResponse({ __typename, ...other })
+ }
+
+ if (optimisticMode === OptimisticResponseModes.UPDATE) {
+ response = optimisticUpdateResponse({ __typename, ...other })
+ }
+
+ if (optimisticMode === OptimisticResponseModes.DESTROY) {
+ response = optimisticDestroyResponse({ __typename, ...other })
+ }
+
+ mutationConfig.optimisticResponse = {
+ __typename: 'Mutation',
+ [responseName]: response({ id, input: rawInput })
+ }
+ }
+ }
+
+ if (mode !== MutationResponseModes.IGNORE) {
+ mutationConfig.update = (cache, { data }) => {
+ const cachedData = cache.readQuery({ query, variables })
+ const fieldName = findQueryFieldNameOrAlias(query)
+
+ /*
+ Convert response to array for mutations like BatchCreate*
+ */
+ const responseRecords = (data[responseName].constructor === Array)
+ ? data[responseName] : [ data[responseName] ]
+
+ if (mode === MutationResponseModes.APPEND || mode === MutationResponseModes.PREPEND) {
+ addRecords(cachedData[fieldName], responseRecords, mode)
+ } else if (mode === MutationResponseModes.DELETE) {
+ deleteRecords(cachedData[fieldName], responseRecords)
+ } else if (mode === MutationResponseModes.CUSTOM) {
+ if (!updateData) {
+ throw new Error('You must specify `updateData` for CUSTOM mutation mode.')
+ }
+
+ updateData({ cache, cachedData, responseRecords, variables })
+ }
+
+ cache.writeQuery({ data: cachedData, query, variables })
+ }
+ }
+
+ if (refetch) {
+ mutationConfig.refetchQueries = [ { query, variables } ]
+ }
+
+ if (context) {
+ if (typeof context === 'function') {
+ mutationConfig.context = context(props)
+ } else {
+ mutationConfig.context = context
+ }
+ }
+
+ return mutate({ ...mutationConfig })
+ .then((response) => {
+ if (successAlert) {
+ showAlertSuccess(typeof successAlert === 'function' ? successAlert({ response, input }) : successAlert)
+ }
+
+ if (typeof onSuccess === 'function') {
+ const result = onSuccess(response)
+ if (isPromise(result)) {
+ return result
+ }
+ }
+
+ return Promise.resolve()
+ }).catch((error) => {
+ const { alert, submissionError } = parseError(error)
+
+ if (submissionError) {
+ if (typeof onFailure === 'function') {
+ onFailure(submissionError)
+ }
+
+ return Promise.resolve(submissionError)
+ }
+
+ if (alert) {
+ showAlertFailure(alert)
+ }
+
+ if (typeof onFailure === 'function') {
+ onFailure(error)
+ }
+
+ return Promise.resolve(error)
+ })
+ }
+ })
+ })
+}
+
+const withQuery = (query, config = {}) => {
+ const configOptions = config.options || {}
+ delete config.options
+
+ const configWithDefaults = Object.assign({
+ props: ({ data }) => {
+ const fieldNames = findAllQueryFieldNamesOrAliases(query)
+ const loading = data.loading && fieldNames.every(fieldName => !data[fieldName])
+ const reloading = data.loading
+
+ return { ...data, loading, reloading, query }
+ },
+ options: (props) => {
+ const defaultOptions = {
+ fetchPolicy: 'cache-and-network',
+ errorPolicy: 'none'
+ }
+
+ if (typeof configOptions === 'function') {
+ return {
+ ...defaultOptions,
+ ...configOptions(props)
+ }
+ }
+
+ return {
+ ...defaultOptions,
+ ...configOptions
+ }
+ }
+ }, config)
+
+ if (configWithDefaults.name) { // Automatically set by the field.
+ throw new Error('You cannot override the `name` when using `withQuery`.')
+ }
+
+ return graphql(query, configWithDefaults)
+}
+
+/*
+ This function was created to handle components like `Header.js` wherein we need
+ to break a query into multiple queries.
+
+ Sample use cases -
+ 1. You can skip one of the queries without affecting others within the component.
+ 2. Pass an individual query as argument to withMutation HOC to handle update
+*/
+const withQueries = queries => compose(queries.map(({ query, config }) => withQuery(query, config)))
+
+export {
+ MutationResponseModes,
+ OptimisticResponseModes,
+ withClientMutation,
+ withClientQuery,
+ withMutation,
+ withQueries,
+ withQuery
+}
diff --git a/ui/src/lib/dateTime.js b/ui/src/lib/dateTime.js
new file mode 100644
index 0000000..8e98b55
--- /dev/null
+++ b/ui/src/lib/dateTime.js
@@ -0,0 +1,7 @@
+import moment from 'moment'
+
+const parseServerDateTime = dateTime => moment(dateTime)
+
+export { parseServerDateTime }
+
+export default moment
diff --git a/ui/src/lib/errorParser.js b/ui/src/lib/errorParser.js
new file mode 100644
index 0000000..b75b49f
--- /dev/null
+++ b/ui/src/lib/errorParser.js
@@ -0,0 +1,87 @@
+import { FORM_ERROR, setIn } from 'final-form'
+
+const errorTypes = Object.freeze({
+ UNPROCESSABLE_ENTITY: 'UNPROCESSABLE_ENTITY',
+ UNAUTHORIZED: 'UNAUTHORIZED',
+ FORBIDDEN: 'FORBIDDEN',
+ NOT_FOUND: 'NOT_FOUND',
+ INTERNAL_SERVER_ERROR: 'INTERNAL_SERVER_ERROR'
+})
+
+/*
+ https://www.apollographql.com/docs/link/links/http.html#Errors
+ https://github.com/apollographql/apollo-link/tree/master/packages/apollo-link-error
+*/
+
+const parseSubmissionError = graphQLErrors => (
+ graphQLErrors.reduce((errors, { type, path, message }) => {
+ if (type && type === errorTypes.UNPROCESSABLE_ENTITY && path) {
+ const field = path.join('.')
+
+ if (field === 'base') {
+ return setIn(errors, FORM_ERROR, message)
+ }
+
+ return setIn(errors, field, message)
+ }
+
+ return errors
+ }, {})
+)
+
+const parseError = ({ networkError, graphQLErrors = [] }) => {
+ let message = null
+ let submissionError = null
+
+ if (networkError) {
+ const { response, result: { exception, errors } = {} } = networkError
+
+ if (!response) {
+ // Server is down / No internet.
+ message = 'Please try again after some time.'
+ } else if (exception) {
+ /*
+ Internal Server Errors with status 500.
+ For eg: `#`
+ In development, they are not formatted and therefore are not part of the `errors` key.
+ */
+ message = exception
+ } else if (errors) {
+ errors.forEach(({ type, path, message: errorMessage }) => {
+ if (type === errorTypes.UNAUTHORIZED) {
+ message = errorMessage || 'You seem to have logged out.'
+ } else if (type === errorTypes.FORBIDDEN) {
+ message = errorMessage || 'You are not allowed to do that.'
+ } else if (type === errorTypes.NOT_FOUND) {
+ message = errorMessage || 'That resource does not exist.'
+ } else if (type === errorTypes.INTERNAL_SERVER_ERROR) {
+ message = errorMessage || 'Something went wrong. Our engineers are looking into it.'
+ } else if (type === errorTypes.UNPROCESSABLE_ENTITY && (!path || path === '')) {
+ message = errorMessage
+ }
+ })
+
+ if (!message) {
+ submissionError = parseSubmissionError(errors)
+ }
+ }
+ } else {
+ /*
+ Errors with status 200
+ For eg: Variable input of type `*Input!` was provided invalid value
+ */
+ graphQLErrors.forEach(({ message: errorMessage }) => {
+ if (process.env.NODE_ENV === 'development') {
+ message = errorMessage
+ } else {
+ message = "We've been notified. Please try after some time."
+ }
+ })
+ }
+
+ const alert = { message }
+
+ return { alert, submissionError }
+}
+
+export default parseError
diff --git a/ui/src/lib/filesize.js b/ui/src/lib/filesize.js
new file mode 100644
index 0000000..89ca2e9
--- /dev/null
+++ b/ui/src/lib/filesize.js
@@ -0,0 +1,3 @@
+import filesize from 'filesize'
+
+export default filesize
diff --git a/ui/src/lib/formMutators.js b/ui/src/lib/formMutators.js
new file mode 100644
index 0000000..0989471
--- /dev/null
+++ b/ui/src/lib/formMutators.js
@@ -0,0 +1,14 @@
+const clear = (fields, state, utils) => {
+ fields.forEach((field) => {
+ utils.changeValue(state, field, () => undefined)
+ })
+}
+
+const set = (args, state, utils) => {
+ const [ fieldList ] = args
+ Object.keys(fieldList).forEach((field) => {
+ utils.changeValue(state, field, () => fieldList[field])
+ })
+}
+
+export { clear, set }
diff --git a/ui/src/lib/getRandomNumber.js b/ui/src/lib/getRandomNumber.js
new file mode 100644
index 0000000..7dd7253
--- /dev/null
+++ b/ui/src/lib/getRandomNumber.js
@@ -0,0 +1,3 @@
+export default function () {
+ return Math.round(Math.random() * -1000000)
+}
diff --git a/ui/src/lib/hooks/useModal.js b/ui/src/lib/hooks/useModal.js
new file mode 100644
index 0000000..7424542
--- /dev/null
+++ b/ui/src/lib/hooks/useModal.js
@@ -0,0 +1,15 @@
+import { useState } from 'react'
+
+export default function () {
+ const [ isModalOpen, setIsModalOpen ] = useState(false)
+ const [ selectedItem, setSelectedItem ] = useState({})
+
+ const openModal = (item) => {
+ setSelectedItem(item)
+ setIsModalOpen(true)
+ }
+
+ const closeModal = () => setIsModalOpen(false)
+
+ return [ selectedItem, isModalOpen, openModal, closeModal ]
+}
diff --git a/ui/src/lib/hooks/useSidePane.js b/ui/src/lib/hooks/useSidePane.js
new file mode 100644
index 0000000..c0fc1ac
--- /dev/null
+++ b/ui/src/lib/hooks/useSidePane.js
@@ -0,0 +1,15 @@
+import { useState } from 'react'
+
+export default function () {
+ const [ isSidePaneOpen, setIsSidePaneOpen ] = useState(false)
+ const [ selectedItem, setSelectedItem ] = useState({})
+
+ const openSidePane = (item) => {
+ setSelectedItem(item)
+ setIsSidePaneOpen(true)
+ }
+
+ const closeSidePane = () => setIsSidePaneOpen(false)
+
+ return [ selectedItem, isSidePaneOpen, openSidePane, closeSidePane ]
+}
diff --git a/ui/src/lib/isPromise.js b/ui/src/lib/isPromise.js
new file mode 100644
index 0000000..acdab3e
--- /dev/null
+++ b/ui/src/lib/isPromise.js
@@ -0,0 +1,3 @@
+export default obj => !!obj
+ && (typeof obj === 'object' || typeof obj === 'function')
+ && typeof obj.then === 'function'
diff --git a/ui/src/lib/isRetina.js b/ui/src/lib/isRetina.js
new file mode 100644
index 0000000..3b1a7eb
--- /dev/null
+++ b/ui/src/lib/isRetina.js
@@ -0,0 +1,15 @@
+export default function isRetina() {
+ let mediaQuery
+
+ if (typeof window !== 'undefined' && window !== null) {
+ mediaQuery = `(-webkit-min-device-pixel-ratio: 2),
+ (min--moz-device-pixel-ratio: 2),
+ (-o-min-device-pixel-ratio: 2/1),
+ (min-resolution: 2dppx),
+ (min-device-pixel-ratio: 2)`
+
+ return window.matchMedia && window.matchMedia(mediaQuery).matches
+ }
+
+ return false
+}
diff --git a/ui/src/lib/lazy.js b/ui/src/lib/lazy.js
new file mode 100644
index 0000000..b6dc835
--- /dev/null
+++ b/ui/src/lib/lazy.js
@@ -0,0 +1,23 @@
+import { lazy } from 'react'
+
+const MAX_RETRIES = 4
+const INTERVAL = 1000
+
+function retry(fn, retriesLeft = MAX_RETRIES) {
+ return new Promise((resolve, reject) => {
+ fn()
+ .then(resolve)
+ .catch((err) => {
+ setTimeout(() => {
+ if (retriesLeft === 1) {
+ reject(new Error(`${err} after ${MAX_RETRIES} retries`))
+ return
+ }
+
+ retry(fn, retriesLeft - 1).then(resolve, reject)
+ }, INTERVAL)
+ })
+ })
+}
+
+export default fn => lazy(() => retry(() => fn()))
diff --git a/ui/src/lib/objectToList.js b/ui/src/lib/objectToList.js
new file mode 100644
index 0000000..3bbb8ef
--- /dev/null
+++ b/ui/src/lib/objectToList.js
@@ -0,0 +1,3 @@
+export default function (object) {
+ return Object.keys(object).map(key => ({ label: object[key], value: object[key] }))
+}
diff --git a/ui/src/lib/resolveImage.js b/ui/src/lib/resolveImage.js
new file mode 100644
index 0000000..288a3f0
--- /dev/null
+++ b/ui/src/lib/resolveImage.js
@@ -0,0 +1,19 @@
+import path from 'path'
+
+import isRetina from 'lib/isRetina'
+
+const retinaImageSuffix = '@2x'
+const req = require.context('images', true)
+
+export default function resolveImage(source) {
+ let filename = source
+
+ if (isRetina()) {
+ const extension = path.extname(source)
+
+ filename = filename.replace(extension, '')
+ filename = `${filename}${retinaImageSuffix}${extension}`
+ }
+
+ return req(`./${filename}`)
+}
diff --git a/ui/src/lib/resolveImageUrl.js b/ui/src/lib/resolveImageUrl.js
new file mode 100644
index 0000000..28f0396
--- /dev/null
+++ b/ui/src/lib/resolveImageUrl.js
@@ -0,0 +1,9 @@
+import isRetina from 'lib/isRetina'
+
+export default function resolveImageUrl(url1x, url2x) {
+ if (isRetina()) {
+ return url2x
+ }
+
+ return url1x
+}
diff --git a/ui/src/lib/toSentence.js b/ui/src/lib/toSentence.js
new file mode 100644
index 0000000..4cb328d
--- /dev/null
+++ b/ui/src/lib/toSentence.js
@@ -0,0 +1,11 @@
+export default function (words, { separator = ', ', lastSeparator = ' and ' } = {}) {
+ if (words.length === 0) {
+ return ''
+ }
+
+ if (words.length === 1) {
+ return words[0]
+ }
+
+ return words.slice(0, -1).join(separator) + lastSeparator + words[words.length - 1]
+}
diff --git a/ui/src/lib/toString.js b/ui/src/lib/toString.js
new file mode 100644
index 0000000..6e5fb9a
--- /dev/null
+++ b/ui/src/lib/toString.js
@@ -0,0 +1,10 @@
+export default function (value) {
+ if (Array.isArray(value)) {
+ return value
+ }
+
+ if (typeof value === 'string' || value instanceof String) {
+ return value
+ }
+ return String(value)
+}
diff --git a/ui/src/lib/validators.js b/ui/src/lib/validators.js
new file mode 100644
index 0000000..0f3c4cc
--- /dev/null
+++ b/ui/src/lib/validators.js
@@ -0,0 +1,11 @@
+import { string } from 'yup'
+
+const allowBlank = () => undefined
+
+const required = value => (
+ string().required().validate(value)
+ .then(() => undefined)
+ .catch(e => e.message)
+)
+
+export { allowBlank, required }
diff --git a/ui/src/models/Asset.js b/ui/src/models/Asset.js
new file mode 100644
index 0000000..8e51e0d
--- /dev/null
+++ b/ui/src/models/Asset.js
@@ -0,0 +1,24 @@
+import filesize from 'lib/filesize'
+
+import BaseModel from './BaseModel'
+
+class Asset extends BaseModel {
+ static getFullName(asset) {
+ let { name } = asset
+ const extension = this.getExtension(asset)
+ name = name.replace(/\.[^/.]+$/, '') // Remove file extension
+
+ return `${name}.${extension}`
+ }
+
+ static getExtension(asset) {
+ const fileExtension = asset.metadata.extension
+ return fileExtension.replace(/\./g, '')
+ }
+
+ static getFormattedSize(asset) {
+ return filesize(asset.metadata.size)
+ }
+}
+
+export default Asset
diff --git a/ui/src/models/BaseModel.js b/ui/src/models/BaseModel.js
new file mode 100644
index 0000000..6a03592
--- /dev/null
+++ b/ui/src/models/BaseModel.js
@@ -0,0 +1,69 @@
+import { addMethod, mixed, lazy, object, reach, setLocale, string } from 'yup'
+import { setIn } from 'final-form'
+
+/* eslint-disable no-template-curly-in-string */
+
+setLocale({
+ mixed: {
+ required: "can't be blank"
+ },
+
+ string: {
+ email: 'is not a valid email',
+ max: 'is too long (maximum is ${max} characters)',
+ min: 'is too short (minimum is ${min} characters)'
+ }
+})
+
+function equalTo(ref, msg) {
+ return mixed().test({
+ name: 'equalTo',
+ exclusive: false,
+ message: msg || "doesn't match ${reference}",
+ params: {
+ reference: ref.path
+ },
+ test(value) {
+ return value === this.resolve(ref)
+ }
+ })
+}
+
+addMethod(string, 'equalTo', equalTo)
+
+/* eslint-enable no-template-curly-in-string */
+
+class BaseModel {
+ static validate(values, fields = []) {
+ const validationSchema = lazy(() => {
+ if (fields.length === 0) {
+ return this.schema
+ }
+
+ return object(fields.reduce((fieldList, field) => {
+ fieldList[field] = reach(this.schema, field)
+ return fieldList
+ }, {}))
+ })
+
+ return this.validateWithSchema(values, validationSchema)
+ }
+
+ static validateSchema(values, schema) {
+ const validationSchema = object(schema)
+
+ return this.validateWithSchema(values, validationSchema)
+ }
+
+ static validateWithSchema(values, validationSchema) {
+ return validationSchema.validate(values, { strict: true, abortEarly: false })
+ .then(() => {})
+ .catch(e => e.inner.reduce((errors, { path, message }) => setIn(errors, path, message), {}))
+ }
+
+ static authorize(record = {}, action) {
+ return this.permissions[action].indexOf(record.role) !== -1
+ }
+}
+
+export default BaseModel
diff --git a/ui/src/models/Entity.js b/ui/src/models/Entity.js
new file mode 100644
index 0000000..f3a52a6
--- /dev/null
+++ b/ui/src/models/Entity.js
@@ -0,0 +1,20 @@
+import { object, string } from 'yup'
+
+import BaseModel from './BaseModel'
+
+class Entity extends BaseModel {
+ static validateCreate(values) {
+ return this.validate(values, [ 'name', 'label' ])
+ }
+
+ static validateUpdate(values) {
+ return this.validate(values, [ 'name', 'label' ])
+ }
+}
+
+Entity.schema = object({
+ name: string().required(),
+ label: string().required()
+})
+
+export default Entity
diff --git a/ui/src/models/Field.js b/ui/src/models/Field.js
new file mode 100644
index 0000000..3792c64
--- /dev/null
+++ b/ui/src/models/Field.js
@@ -0,0 +1,48 @@
+import { object, string } from 'yup'
+
+import BaseModel from './BaseModel'
+
+class Field extends BaseModel {
+ static validateCreate(values) {
+ return this.validate(values, [ 'name', 'label' ])
+ }
+
+ static validateUpdate(values) {
+ return this.validate(values, [ 'name', 'label' ])
+ }
+
+ static isColumn(field) {
+ return ![ 'key_value', 'array' ].some(dataType => dataType === field.dataType)
+ }
+
+ static isRoot(field) {
+ return !field.parentId
+ }
+
+ static isVisibleColumn(field) {
+ return !(field.settings.visibility === false)
+ }
+}
+
+Field.schema = object({
+ name: string().required(),
+ label: string().required()
+})
+
+Field.dataTypeList = [
+ { value: 'single_line_text', label: 'Single-Line Text' },
+ { value: 'multiple_line_text', label: 'Multiple-Line Text' },
+ { value: 'number', label: 'Number' },
+ { value: 'decimal', label: 'Decimal' },
+ { value: 'boolean', label: 'Boolean' },
+ { value: 'image', label: 'Image' },
+ { value: 'file', label: 'File' },
+ { value: 'key_value', label: 'Key-Value' },
+ { value: 'array', label: 'Array' },
+ { value: 'reference', label: 'Reference' },
+ { value: 'color', label: 'Color Picker' }
+]
+
+Field.elementTypeList = Field.dataTypeList.filter(({ value }) => value !== 'array')
+
+export default Field
diff --git a/ui/src/models/KeyPair.js b/ui/src/models/KeyPair.js
new file mode 100644
index 0000000..eefe6d5
--- /dev/null
+++ b/ui/src/models/KeyPair.js
@@ -0,0 +1,10 @@
+import BaseModel from './BaseModel'
+
+class KeyPair extends BaseModel {
+}
+
+KeyPair.fields = Object.freeze([
+ { name: 'publicKey', label: 'Public Key' }
+])
+
+export default KeyPair
diff --git a/ui/src/models/Project.js b/ui/src/models/Project.js
new file mode 100644
index 0000000..c57ced9
--- /dev/null
+++ b/ui/src/models/Project.js
@@ -0,0 +1,19 @@
+import { object, string } from 'yup'
+
+import BaseModel from './BaseModel'
+
+class Project extends BaseModel {
+ static validateCreate(values) {
+ return this.validate(values, [ 'name' ])
+ }
+
+ static validateUpdate(values) {
+ return this.validate(values, [ 'name' ])
+ }
+}
+
+Project.schema = object({
+ name: string().required()
+})
+
+export default Project
diff --git a/ui/src/models/Record.js b/ui/src/models/Record.js
new file mode 100644
index 0000000..13e2dda
--- /dev/null
+++ b/ui/src/models/Record.js
@@ -0,0 +1,149 @@
+import _ from 'lodash'
+import { object, mixed } from 'yup'
+
+import BaseModel from './BaseModel'
+import Field from './Field'
+
+class Record extends BaseModel {
+ constructor(fields = []) {
+ super()
+
+ this.fieldsMapping = fields.reduce((mapping, field) => {
+ mapping[field.id] = field
+
+ return mapping
+ }, {})
+ }
+
+ summarize = (record, entity) => {
+ const baseText = `${entity.label} - ID: ${record.id}`
+ const rootProperties = record.properties.filter(p => !p.parentId)
+
+ const values = rootProperties.map((property) => {
+ const field = this.fieldsMapping[property.fieldId]
+
+ if (Field.isColumn(field)) {
+ return `${field.label}: ${property.value}`
+ }
+
+ return null
+ })
+
+ const contentText = _.compact(values).join(' / ')
+
+ return `${baseText} - ${contentText}`
+ }
+
+ process = records => (records || []).map((record) => {
+ const traits = {}
+ const { properties } = record
+ const rootProperties = properties.filter(p => !p.parentId)
+ rootProperties.forEach((property) => {
+ const field = this.fieldsMapping[property.fieldId]
+
+ if (field) {
+ traits[field.name] = this.propertyValue(property, properties)
+ }
+ })
+
+ return {
+ id: record.id,
+ createdAt: record.createdAt,
+ updatedAt: record.updatedAt,
+ traits
+ }
+ })
+
+ propertyValue = (property, properties) => {
+ const { dataType } = this.fieldsMapping[property.fieldId] || {}
+ const childProperties = properties.filter(p => p.parentId === property.id)
+
+ if (dataType === 'key_value') {
+ return childProperties.reduce((processedValue, child) => {
+ const field = this.fieldsMapping[child.fieldId]
+ if (field) {
+ processedValue[field.name] = this.propertyValue(child, properties)
+ }
+
+ return processedValue
+ }, {})
+ }
+
+ if (dataType === 'array') {
+ return _.sortBy(childProperties, [ 'position' ])
+ .map((child) => {
+ const field = this.fieldsMapping[child.fieldId]
+
+ if (field.dataType === 'reference') {
+ return this.propertyValue(child, properties)
+ }
+
+ return {
+ id: child.id,
+ position: child.position,
+ value: this.propertyValue(child, properties)
+ }
+ })
+ }
+
+ if (dataType === 'reference') {
+ return {
+ id: property.linkedRecordId
+ }
+ }
+
+ if (dataType === 'image' || dataType === 'file') {
+ return property.asset && property.asset.fileOriginal
+ }
+
+ return property.value
+ }
+
+ static summarizeKeyValuePair = (childFields, values) => {
+ const baseText = ''
+
+ if (!values) return baseText
+
+ const summarizedValues = childFields.map((field) => {
+ if (Field.isColumn(field)) {
+ return values[field.name] ? `${field.label}: ${values[field.name]}` : null
+ }
+
+ return null
+ })
+
+ const containsAllNonPrimitive = summarizedValues.every(value => value === null)
+
+ return containsAllNonPrimitive ? baseText : _.compact(summarizedValues).join(' • ')
+ }
+
+ static validateCreate = (fields, entities) => (
+ values => BaseModel.validateWithSchema(values, Record.schema(fields, entities))
+ )
+}
+
+const traitsSchema = (fields, entities = []) => object(
+ fields.reduce((acc, field) => {
+ if (field.dataType === 'reference') {
+ const referenceEntity = entities.filter(entity => entity.id === field.referencedEntityId)[0]
+
+ if (referenceEntity) {
+ return { ...acc, [field.name]: Record.schema(referenceEntity.fields, entities, true) }
+ }
+ }
+
+ if (!field.parentId && field.validations.presence) {
+ return { ...acc, [field.name]: field.dataType !== 'array' && mixed().required() }
+ }
+
+ return acc
+ }, {})
+)
+
+Record.schema = (fields, entities, required = false) => object().shape({
+ traits: required
+ ? traitsSchema(fields, entities)
+ : traitsSchema(fields, entities).default(null).nullable()
+}).default(object().shape({ traits: {} }))
+
+export default Record
diff --git a/ui/src/models/Restore.js b/ui/src/models/Restore.js
new file mode 100644
index 0000000..2156055
--- /dev/null
+++ b/ui/src/models/Restore.js
@@ -0,0 +1,15 @@
+import { object, string } from 'yup'
+
+import BaseModel from './BaseModel'
+
+class Restore extends BaseModel {
+ static validateCreate(values) {
+ return this.validate(values, [ 'url' ])
+ }
+}
+
+Restore.schema = object({
+ url: string().required()
+})
+
+export default Restore
diff --git a/ui/src/models/Team.js b/ui/src/models/Team.js
new file mode 100644
index 0000000..ff33bd2
--- /dev/null
+++ b/ui/src/models/Team.js
@@ -0,0 +1,23 @@
+import { object, string } from 'yup'
+
+import BaseModel from './BaseModel'
+
+class Team extends BaseModel {
+ static validateCreate(values) {
+ return this.validate(values, [ 'name' ])
+ }
+
+ static validateUpdate(values) {
+ return this.validate(values, [ 'name' ])
+ }
+
+ static validateCreateTransferRequest(values) {
+ return this.validateSchema(values, { userId: string().required() })
+ }
+}
+
+Team.schema = object({
+ name: string().required()
+})
+
+export default Team
diff --git a/ui/src/models/TeamMembership.js b/ui/src/models/TeamMembership.js
new file mode 100644
index 0000000..19ff636
--- /dev/null
+++ b/ui/src/models/TeamMembership.js
@@ -0,0 +1,36 @@
+import { object, string } from 'yup'
+
+import BaseModel from './BaseModel'
+
+class TeamMembership extends BaseModel {
+ static validateCreate(values) {
+ return this.validate(values, [ 'teamId', 'email', 'role' ])
+ }
+
+ static validateUpdate(values) {
+ return this.validate(values, [ 'id', 'role' ])
+ }
+}
+
+TeamMembership.roleList = [
+ { value: 'editor', label: 'Editor', hint: 'Can manage content within projects.' },
+ { value: 'developer', label: 'Developer', hint: 'Can manage structure and content within projects.' },
+ { value: 'manager', label: 'Manager', hint: 'Can create projects, can invite others to the team.' }
+]
+
+TeamMembership.roles = TeamMembership.roleList.map(role => role.value)
+
+TeamMembership.defaultRole = 'editor'
+
+TeamMembership.permissions = {
+ invite: [ 'owner', 'manager' ]
+}
+
+TeamMembership.schema = object({
+ id: string().required(),
+ teamId: string().required(),
+ email: string().email().required(),
+ role: string().oneOf(TeamMembership.roles).required()
+})
+
+export default TeamMembership
diff --git a/ui/src/models/User.js b/ui/src/models/User.js
new file mode 100644
index 0000000..9796521
--- /dev/null
+++ b/ui/src/models/User.js
@@ -0,0 +1,36 @@
+import { object, string } from 'yup'
+
+import BaseModel from './BaseModel'
+
+class User extends BaseModel {
+ static validateSignup(values) {
+ return this.validate(values, [ 'email' ])
+ }
+
+ static validateProfile(values) {
+ return this.validate(values, [ 'firstName', 'lastName' ])
+ }
+
+ static fullName({ firstName, lastName }) {
+ return [ firstName, lastName ].map(str => str && str).join(' ').trim()
+ }
+
+ static initials({ firstName, lastName }) {
+ return [ firstName, lastName ]
+ .map(str => str && str.charAt(0).toUpperCase())
+ .join('')
+ }
+}
+
+User.schema = object({
+ email: string().email().required(),
+ firstName: string().required(),
+ lastName: string().required()
+})
+
+User.sessionStates = Object.freeze({
+ LOGGED_IN: 'logged_in',
+ LOGGED_OUT: 'logged_out'
+})
+
+export default User
diff --git a/ui/src/models/index.js b/ui/src/models/index.js
new file mode 100644
index 0000000..ce76607
--- /dev/null
+++ b/ui/src/models/index.js
@@ -0,0 +1,9 @@
+export { default as Asset } from './Asset'
+export { default as Entity } from './Entity'
+export { default as Field } from './Field'
+export { default as KeyPair } from './KeyPair'
+export { default as Project } from './Project'
+export { default as Restore } from './Restore'
+export { default as Team } from './Team'
+export { default as TeamMembership } from './TeamMembership'
+export { default as User } from './User'
diff --git a/ui/src/mutations/alert.js b/ui/src/mutations/alert.js
new file mode 100644
index 0000000..412f2b8
--- /dev/null
+++ b/ui/src/mutations/alert.js
@@ -0,0 +1,21 @@
+import gql from 'graphql-tag'
+
+const ALERT_FAILURE = gql`
+ mutation OpenFailureAlertMutation($alert: AlertInput!) {
+ openFailureAlert(alert: $alert) @client
+ }
+`
+
+const ALERT_SUCCESS = gql`
+ mutation OpenSuccessAlertMutation($alert: AlertInput!) {
+ openSuccessAlert(alert: $alert) @client
+ }
+`
+
+const CLOSE_ALERT = gql`
+ mutation CloseAlertMutation {
+ closeAlert @client
+ }
+`
+
+export { ALERT_FAILURE, ALERT_SUCCESS, CLOSE_ALERT }
diff --git a/ui/src/mutations/referrer.js b/ui/src/mutations/referrer.js
new file mode 100644
index 0000000..6b77d33
--- /dev/null
+++ b/ui/src/mutations/referrer.js
@@ -0,0 +1,9 @@
+import gql from 'graphql-tag'
+
+const SET_REFERRER = gql`
+ mutation SetReferrerMutation($url: String!) {
+ setReferrer(url: $url) @client
+ }
+`
+
+export default SET_REFERRER
diff --git a/ui/src/mutations/session.js b/ui/src/mutations/session.js
new file mode 100644
index 0000000..5b27d3a
--- /dev/null
+++ b/ui/src/mutations/session.js
@@ -0,0 +1,9 @@
+import gql from 'graphql-tag'
+
+const SET_TOKEN = gql`
+ mutation SetTokenMutation($token: String!) {
+ setToken(token: $token) @client
+ }
+`
+
+export default SET_TOKEN
diff --git a/ui/src/queries/alert.js b/ui/src/queries/alert.js
new file mode 100644
index 0000000..ff24f98
--- /dev/null
+++ b/ui/src/queries/alert.js
@@ -0,0 +1,15 @@
+import gql from 'graphql-tag'
+
+const GET_ALERT = gql`
+ query AlertQuery {
+ alert @client {
+ isOpen
+ icon
+ message
+ title
+ variant
+ }
+ }
+`
+
+export default GET_ALERT
diff --git a/ui/src/queries/referrer.js b/ui/src/queries/referrer.js
new file mode 100644
index 0000000..b4605af
--- /dev/null
+++ b/ui/src/queries/referrer.js
@@ -0,0 +1,11 @@
+import gql from 'graphql-tag'
+
+const GET_REFERRER = gql`
+ query ReferrerQuery {
+ referrer @client {
+ url
+ }
+ }
+`
+
+export default GET_REFERRER
diff --git a/ui/src/queries/session.js b/ui/src/queries/session.js
new file mode 100644
index 0000000..61c7eb4
--- /dev/null
+++ b/ui/src/queries/session.js
@@ -0,0 +1,11 @@
+import gql from 'graphql-tag'
+
+const GET_TOKEN = gql`
+ query SessionQuery {
+ session @client {
+ token
+ }
+ }
+`
+
+export default GET_TOKEN
diff --git a/ui/src/resolvers/alert.js b/ui/src/resolvers/alert.js
new file mode 100644
index 0000000..424f807
--- /dev/null
+++ b/ui/src/resolvers/alert.js
@@ -0,0 +1,62 @@
+const defaults = {
+ alert: {
+ isOpen: false,
+ variant: 'failure',
+ icon: null,
+ title: null,
+ message: null,
+ __typename: 'Alert'
+ }
+}
+
+const resolvers = {
+ Mutation: {
+ closeAlert: (_obj, _args, { cache }) => {
+ cache.writeData({
+ data: {
+ alert: {
+ isOpen: false,
+ __typename: 'Alert'
+ }
+ }
+ })
+
+ return null
+ },
+ openFailureAlert: (_obj, args, { cache }) => {
+ cache.writeData({
+ data: {
+ alert: {
+ ...defaults.alert,
+ ...args.alert,
+ isOpen: true,
+ variant: 'failure'
+ }
+ }
+ })
+
+ return null
+ },
+ openSuccessAlert: (_obj, args, { cache }) => {
+ cache.writeData({
+ data: {
+ alert: {
+ ...defaults.alert,
+ ...args.alert,
+ isOpen: true,
+ variant: 'success'
+ }
+ }
+ })
+
+ return null
+ }
+ }
+}
+
+const alert = {
+ defaults,
+ resolvers
+}
+
+export default alert
diff --git a/ui/src/resolvers/index.js b/ui/src/resolvers/index.js
new file mode 100644
index 0000000..1616295
--- /dev/null
+++ b/ui/src/resolvers/index.js
@@ -0,0 +1,9 @@
+import _ from 'lodash'
+
+import alert from './alert'
+import referrer from './referrer'
+import session from './session'
+
+const resolvers = _.merge(alert, referrer, session)
+
+export default resolvers
diff --git a/ui/src/resolvers/referrer.js b/ui/src/resolvers/referrer.js
new file mode 100644
index 0000000..5999de3
--- /dev/null
+++ b/ui/src/resolvers/referrer.js
@@ -0,0 +1,30 @@
+const defaults = {
+ referrer: {
+ url: null,
+ __typename: 'Referrer'
+ }
+}
+
+const resolvers = {
+ Mutation: {
+ setReferrer: (_obj, { url }, { cache }) => {
+ cache.writeData({
+ data: {
+ referrer: {
+ url,
+ __typename: 'Referrer'
+ }
+ }
+ })
+
+ return null
+ }
+ }
+}
+
+const referrer = {
+ defaults,
+ resolvers
+}
+
+export default referrer
diff --git a/ui/src/resolvers/session.js b/ui/src/resolvers/session.js
new file mode 100644
index 0000000..2ee3411
--- /dev/null
+++ b/ui/src/resolvers/session.js
@@ -0,0 +1,30 @@
+const defaults = {
+ session: {
+ token: null,
+ __typename: 'Session'
+ }
+}
+
+const resolvers = {
+ Mutation: {
+ setToken: (_obj, { token }, { cache }) => {
+ cache.writeData({
+ data: {
+ session: {
+ token,
+ __typename: 'Session'
+ }
+ }
+ })
+
+ return null
+ }
+ }
+}
+
+const session = {
+ defaults,
+ resolvers
+}
+
+export default session
diff --git a/ui/src/styles/mixins.js b/ui/src/styles/mixins.js
new file mode 100644
index 0000000..3b5bd48
--- /dev/null
+++ b/ui/src/styles/mixins.js
@@ -0,0 +1,138 @@
+import _ from 'lodash'
+
+const placeholder = styles => ({
+ '&::-webkit-input-placeholder': {
+ /* Chrome/Opera/Safari */
+ ...styles
+ },
+ '&::-moz-placeholder': {
+ /* Firefox 19+ */
+ ...styles
+ },
+ '&:-ms-input-placeholder': {
+ /* IE 10+ */
+ ...styles
+ },
+ '&:-moz-placeholder': {
+ /* Firefox 18- */
+ ...styles
+ }
+})
+
+const backgroundContain = () => ({
+ backgroundPosition: '50% 50%',
+ backgroundRepeat: 'no-repeat',
+ backgroundSize: 'contain'
+})
+
+const backgroundCover = () => ({
+ backgroundPosition: '50% 50%',
+ backgroundRepeat: 'no-repeat',
+ backgroundSize: 'cover'
+})
+
+const listless = {
+ listStyle: 'none',
+ margin: 0,
+ padding: 0
+}
+
+const size = (width, height = width) => ({ width, height })
+
+const circle = ({ size: diameter, color = '#fff', opacity = 0.04 }) => ({
+ ...size(diameter),
+
+ backgroundColor: color,
+ borderRadius: '50%',
+ content: '" "',
+ opacity,
+ pointerEvents: 'none',
+ position: 'absolute'
+})
+
+const animateUnderline = ({ color = '#fff', bottom = 0 } = {}) => ({
+ position: 'relative',
+
+ '&::after': {
+ backgroundColor: color,
+ content: '" "',
+ height: 2,
+ position: 'absolute',
+ right: 0,
+ bottom,
+ left: 0,
+ transform: 'scaleX(0)',
+ transformOrigin: '100% 50%',
+ transition: 'transform .4s cubic-bezier(.23, 1, .32, 1)'
+ },
+
+ '&:hover': {
+ '&::after': {
+ transform: 'scaleX(1)',
+ transformOrigin: '0 50%'
+ }
+ }
+})
+
+const underline = ({ color = '#fff', bottom = 0 } = {}) => ({
+ position: 'relative',
+
+ '&::after': {
+ backgroundColor: color,
+ content: '" "',
+ height: 2,
+ position: 'absolute',
+ right: 0,
+ bottom,
+ left: 0
+ }
+})
+
+const breakpoints = {
+ xs: '@media (min-width: 0)',
+ sm: '@media (min-width: 576px)',
+ md: '@media (min-width: 768px)',
+ lg: '@media (min-width: 992px)',
+ xl: '@media (min-width: 1200px)'
+}
+
+const responsiveProperty = (property, values) => (
+ Object.keys(values).reduce((prop, value) => {
+ prop[breakpoints[value]] = { [property]: values[value] }
+
+ return prop
+ }, {})
+)
+
+const responsiveProperties = ({ wrapper, ...properties }) => (
+ Object.keys(properties).reduce((propertyList, property) => {
+ _.merge(propertyList, responsiveProperty(property, properties[property]))
+
+ return propertyList
+ }, Object.values(breakpoints).reduce((list, breakpoint) => ({ ...list, [breakpoint]: {} }), {}))
+)
+
+const makeTransition = ({ transitionDuration = '0.3s', transitionTimingFunction = 'ease' } = {}) => (transitionProperty = 'all') => ({
+ transitionProperty,
+ transitionDuration,
+ transitionTimingFunction
+})
+
+const transitionSimple = makeTransition()
+const transitionFluid = makeTransition({ transitionTimingFunction: 'cubic-bezier(0.25, 0.46, 0.45, 0.94)' })
+
+export {
+ animateUnderline,
+ backgroundContain,
+ backgroundCover,
+ breakpoints,
+ circle,
+ listless,
+ placeholder,
+ responsiveProperties,
+ responsiveProperty,
+ size,
+ transitionFluid,
+ transitionSimple,
+ underline
+}
diff --git a/ui/src/styles/theme.js b/ui/src/styles/theme.js
new file mode 100644
index 0000000..56f6879
--- /dev/null
+++ b/ui/src/styles/theme.js
@@ -0,0 +1,1097 @@
+import * as mixins from 'styles/mixins'
+
+const fonts = {
+ poppinsExtralight: {
+ fontFamily: 'Poppins, Arial, sans-serif',
+ fontWeight: 200
+ },
+ poppinsLight: {
+ fontFamily: 'Poppins, Arial, sans-serif',
+ fontWeight: 300
+ },
+ poppinsRegular: {
+ fontFamily: 'Poppins, Arial, sans-serif',
+ fontWeight: 400
+ },
+ poppinsMedium: {
+ fontFamily: 'Poppins, Arial, sans-serif',
+ fontWeight: 500
+ },
+ poppinsSemibold: {
+ fontFamily: 'Poppins, Arial, sans-serif',
+ fontWeight: 600
+ },
+ poppinsBold: {
+ fontFamily: 'Poppins, Arial, sans-serif',
+ fontWeight: 700
+ },
+ robotoBold: {
+ fontFamily: 'Roboto, Arial, sans-serif',
+ fontWeight: 700
+ },
+ robotoBlack: {
+ fontFamily: 'Roboto, Arial, sans-serif',
+ fontWeight: 900
+ }
+}
+
+const theme = {
+ fonts,
+
+ colors: {
+ alert_failure: '#fd8376',
+ alert_success: '#6cf1ba',
+ alertBackground: '#3d3e45',
+ alertText: '#fff',
+
+ assetBoxBackground: '#fcfcff',
+
+ badgeBackground_primary: '#fd8376',
+ badgeText_primary: '#fff',
+
+ baseModalBackground: '#fff',
+ baseModalOverlayBackground: 'rgba(255, 255, 255, 0.43)',
+
+ baseSliderHandleBackground: '#fff',
+ baseSliderHandleBorder: '#ffcec9',
+ baseSliderRailBackground: '#e5e5ee',
+ baseSliderTrackBackground: '#e5e5ee',
+
+ buttonBackground_flat: '#fcfcff',
+ buttonBorder_flat: '#e5e5ee',
+ button_flatHover: '',
+
+ buttonGroupInputBackground: '#fcfcff',
+ buttonGroupInputBorder: '#e5e5ee',
+ buttonGroupInputBorder_active: '#ffcec9',
+ buttonGroupInputBorderRight: '#ececf2',
+
+ cardBackground: '#fff',
+
+ checkboxBackground: '#f3f3f9',
+ checkboxBackground_checked: '#6abbed',
+ checkboxBorder: '#e8e8f1',
+ checkboxTick: '#fff',
+
+ circleBackground_dark: '#fafafd',
+
+ closeIcon: '#a1a4b8',
+
+ codeBackground: '#f7f7fa',
+
+ colorSwatchBorder: '#e5e5ee',
+
+ columnBorder: '#f3f3f9',
+
+ colorTileDefaultBackground: '#fff',
+ colorTileBoxShadow: '0px 4px 6px 0px rgba(10, 25, 39, 0.08)',
+
+ containedButtonBackground: '#fcfcff',
+ containedButtonBackground_hover: '#fcfcff',
+ containedButtonBorder_active: '#ffcec9',
+ containedButtonBorder: '#e5e5ee',
+
+ dataTableBoxCellBorderRight: '#f3f3f9',
+
+ dataTableBoxCellBackground: '#fcfcff',
+
+ dataTableBorderedCellBorderBackground: '#f3f3f9',
+
+ dataTableHeaderBackground: '#fcfcff',
+ dataTableRow_selected: '#ffcec9',
+ dataTableRowBackground: '#fff',
+
+ dateRangePickerDateHoverBackground: '#ffcec9',
+ dateRangePickerFirstAndLastWeekBackground: '#f7f7fa',
+ dateRangePickerMonthNav: '#a1a4b8',
+ dateRangePickerOutsideDate: '#a1a4b8',
+ dateRangePickerSelectedRangeBackground: '#ffcec9',
+ dateRangePickerSelectedRangeExtremeDate: '#fff',
+ dateRangePickerSelectedRangeExtremeDateBackground: 'linear-gradient(#7dead9, #6abbed)',
+ dateRangePickerWeekHeaderBorder: '#f3f3f9',
+ dateRangePickerWrapperBackground: '#fff',
+
+ dataTileBackground: '#fff',
+
+ dialogCircleBackground: '#fafafd',
+
+ emptyWrapperBackground: '#f5f5f8',
+ emptyWrapperBorder: '#ececf2',
+ emptyWrapperCircleBackground: '#f2f2f6',
+
+ externalBackground: '#fff',
+
+ externalButtonBackground: '#fff',
+ externalButtonBackground_disabled: '#ffc3bc',
+
+ externalFieldErrorBackground: '#ffdbd7',
+
+ externalInputBackground: '#6abbed',
+ externalInputPlaceholder: '#fff',
+ externalInputText: '#fff',
+
+ externalListItemBulletColor: '#fd8376',
+
+ externalVerticalDividerBackground: '#ededee',
+
+ fieldErrorBackground: 'rgba(253, 131, 118, 0.2)',
+
+ headerItemBackground_hover: 'rgba(255, 255, 255, 0.129)',
+ headerItemBorder: 'rgba(255, 255, 255, 0.1)',
+
+ hintBoxBackground_dark: '#f5f5f8',
+ hintBoxBackground_light: '#fbfbfc',
+ hintBoxBorder_alert: '#fedad6',
+ hintBoxBorder: '#ececf2',
+
+ iconButtonBackground: '#fff',
+
+ iconLink: '#fe8577',
+
+ imageTileBackground: '#fff',
+
+ imageDropInputModifierIconColor: '#a1a4b8',
+
+ inputBorder: '#e5e5ee',
+ inputBorder_hover: '#6abbed',
+ inputIcon: '#a1a4b8',
+ inputIcon_active: '#6abbed',
+ inputPlaceholder: '#a1a4b8',
+ inputText: '#26272d',
+
+ internalBackground: '#f3f3f9',
+
+ internalBoxBackground: '#f5f5f8',
+ internalBoxBorder: '#ececf2',
+
+ internalCardBackground: '#fff',
+
+ internalDividerBackground: '#ececf2',
+
+ menuBackground: '#fff',
+
+ menuDividerBackground: '#ececf2',
+ menuSectionBackground: '#fcfcff',
+ menuSectionBorder: '#e8e8f1',
+
+ menuLinkBackground_action: '#efeff4',
+
+ menuLink_hover: '#fcfcff',
+
+ modalBackground: '#f7f7fa',
+
+ panelBackground: '#fff',
+ panelHeaderBottomBorder: '#e8e8f1',
+ panelHeaderBackgroundColor: '#fcfcff',
+ panelHeaderIcon: '#6abbed',
+
+ panelTableBorder: '#f3f3f9',
+ panelTableCellBadgeBackground: 'rgb(254, 218, 214, 0.2)',
+ panelTableCellBadgeColor: '#ff9388',
+ panelTableSecondaryText: '#a1a4b8',
+
+ profilePictureBackground: '#fff',
+ profilePictureBackground_inverted: '#6abbed',
+ profilePictureBackground_empty: '#dcdde6',
+ profilePictureBorder: '#fff',
+
+ radioInputBulletBackground: '#efeff4',
+ radioInputBulletBackground_active: '#fff',
+ radioInputBulletBorder_active: '#fd8376',
+
+ separatorBackground: 'rgba(255, 255, 255, 0.3)',
+
+ sidebarBreadcrumbItemBorder: '#ececf2',
+
+ sidebarItemBackground_active: '#fff',
+
+ sidePaneBackground: '#f7f7fa',
+
+ sidePaneHeaderArrow: '#c0f5ff',
+
+ switchInputBackground: '#efeff4',
+ switchInputBackground_active: '#fe8577',
+ switchInputBackground_disabled: '#efefef',
+ switchInputBorder: '#e8e8f1',
+ switchInputKnobBackground: '#fff',
+ switchInputKnobBackground_active: '#fcfcff',
+ switchInputLabel: '#a1a4b8',
+
+ tab: '#a1a4b8',
+ tabDividerBackground: '#ececf2',
+ tab_hover: '#26272d',
+ tab_selected: '#26272d',
+ tabUnderline: '#ececf2',
+ tabUnderline_selected: '#26272d',
+ tabLink_hover: '#26272d',
+ tabLink_active: '#26272d',
+
+ tag: '#6abbed',
+ tagBackground: '#b1fff3',
+ tagRemoveBackground: '#ff8b7b',
+
+ text_primary: '#6abbed',
+ text_pale: '#a1a4b8',
+ text_light: '#fff',
+ text_dark: '#26272d',
+ text_darkDisabled: 'rgba(38, 39, 45, 0.1)', // rgba version of text_dark with alpha set to 0.1
+ text_darker: '#3d3e45',
+ text_error: '#f25c5d',
+
+ tooltipBackground: '#3d3e45',
+
+ uploadInputBackground: '#fafafa',
+ uploadInputBorder: '#eeeeee'
+ },
+
+ gradients: {
+ button: 'linear-gradient(90deg, #6abbed, #7dead9)',
+ externalHeroBackground: 'linear-gradient(315deg, #7dead9, #6abbed)',
+ internalHeader: 'linear-gradient(90deg, #6abbed, #7dead9)',
+ onboardingBackground: 'linear-gradient(-45deg, #6abbed, #7dead9)',
+ sidePaneHeader: 'linear-gradient(90deg, #6abbed, #7dead9)'
+ },
+
+ shadows: {
+ alert: '0px 29px 50px 0px rgba(10, 25, 39, 0.16)',
+ assetBox: '0px 1px 2px 0px rgba(10, 25, 39, 0.1)',
+ assetBox_hover: '0px 10px 20px 0px rgba(10, 25, 39, 0.07)',
+ baseSliderHandle: '0px 4px 6px 0px rgba(10, 25, 39, 0.08)',
+ button_color: '0px 10px 13px 0px rgba(10, 25, 39, 0.13)',
+ button_colorHover: '0px 10px 13px 0px rgba(10, 25, 39, 0.18)',
+ button_flatHover: '0px 10px 13px 0px rgba(10, 25, 39, 0.08)',
+ buttonGroupInput_active: '0px 10px 13px 0px rgba(10, 25, 39, 0.09)',
+ card: '0px 10px 20px 0px rgba(10, 25, 39, 0.04)',
+ panel: '0px 1px 2px 0px rgba(10, 25, 39, 0.06)',
+ dataTableRow: '0px 1px 2px 0px rgba(10, 25, 39, 0.06)',
+ dataTableRow_hover: '0px 10px 20px 0px rgba(10, 25, 39, 0.04)',
+ dataTile: '0px 1px 2px 0px rgba(10, 25, 39, 0.06)',
+ dataTile_hover: '0px 10px 20px 0px rgba(10, 25, 39, 0.04)',
+ dialog: '0px 29px 50px 0px rgba(10, 25, 39, 0.09)',
+ externalButton: '0px 12px 38px 0px rgba(137, 3, 0, 0.15)',
+ iconButton: '0px 2px 4px 0px 0px rgba(10, 25, 39, 0.05)',
+ iconButton_heavy: '0px 10px 13px 0px rgba(10, 25, 39, 0.13)',
+ iconButton_hover: '0px 2px 4px 0px rgba(10, 25, 39, 0.18)',
+ imageTile: '0px 1px 2px 0px rgba(10, 25, 39, 0.06)',
+ imageTile_hover: '0px 10px 20px 0px rgba(10, 25, 39, 0.04)',
+ internalCard: '0px 1px 2px 0px rgba(10, 25, 39, 0.06)',
+ internalHeader: '0px 13px 29px 0px rgba(10, 25, 39, 0.13)',
+ internalLayout: '0px 1px 2px 0px rgba(10, 25, 39, 0.06)',
+ internalLayout_hover: '0px 10px 20px 0px rgba(10, 25, 39, 0.04)',
+ menu: '0px 13px 29px 0px rgba(10, 25, 39, 0.13)',
+ profilePicture: '0px 13px 29px rgba(10, 25, 39, 0.13)',
+ sidebarItem_active: '0px 10px 20px 0px rgba(10, 25, 39, 0.08)',
+ sidePane: '0px 29px 50px 0px rgba(10, 25, 39, 0.13)',
+ sidePaneHeader: '0px 13px 29px 0px rgba(10, 25, 39, 0.13)',
+ switchInputKnob: '0px 4px 6px 0px rgba(10, 25, 39, 0.08)',
+ tab_selected: '0px 2px 7px 0px rgba(10, 25, 39, 0.2)',
+ tabLink_active: '0px 2px 7px 0px rgba(10, 25, 39, 0.2)',
+ tooltip: '0px 9px 13px 0px rgba(10, 25, 39, 0.13)'
+ },
+
+ typography: {
+ lightSmall: {
+ ...fonts.poppinsLight,
+
+ fontSize: 12,
+ letterSpacing: '-0.025em',
+ lineHeight: 1.6
+ },
+ light: {
+ ...fonts.poppinsLight,
+
+ fontSize: 13,
+ letterSpacing: '-0.025em',
+ lineHeight: 1.2
+ },
+ lightSquished: {
+ ...fonts.poppinsLight,
+
+ fontSize: 14,
+ letterSpacing: '-0.025em',
+ lineHeight: 1.3
+ },
+ lightSpaced: {
+ ...fonts.poppinsLight,
+
+ fontSize: 14,
+ letterSpacing: '-0.025em',
+ lineHeight: 2
+ },
+ lightMedium: {
+ ...fonts.poppinsLight,
+
+ fontSize: 16,
+ letterSpacing: '-0.01em',
+ lineHeight: 2
+ },
+ lightMediumResponsive: {
+ ...fonts.poppinsLight,
+ ...mixins.responsiveProperty('fontSize', { xs: 18, xl: 16 }),
+
+ letterSpacing: '-0.01em',
+ lineHeight: 2
+ },
+ lightLarge: {
+ ...fonts.poppinsLight,
+
+ fontSize: 22,
+ letterSpacing: '-0.01em',
+ lineHeight: 1
+ },
+ lightExtraLarge: {
+ ...fonts.poppinsLight,
+
+ fontSize: 36,
+ letterSpacing: '-0.025em',
+ lineHeight: 1
+ },
+ lightExtraLargeSquished: {
+ ...fonts.poppinsLight,
+
+ fontSize: 32,
+ letterSpacing: '-0.025em',
+ lineHeight: 1
+ },
+ regularSmallCompact: {
+ ...fonts.poppinsRegular,
+
+ fontSize: 10,
+ letterSpacing: '0.01em',
+ lineHeight: 1
+ },
+ regularSmall: {
+ ...fonts.poppinsRegular,
+
+ fontSize: 10,
+ lineHeight: 1
+ },
+ regularSmallSpaced: {
+ ...fonts.poppinsRegular,
+
+ fontSize: 12,
+ letterSpacing: '0.01em',
+ lineHeight: 1
+ },
+ regularSmallSquished: {
+ ...fonts.poppinsRegular,
+
+ fontSize: 12,
+ letterSpacing: '-0.01em',
+ lineHeight: 1
+ },
+ regularSmallSpacedSquished: {
+ ...fonts.poppinsRegular,
+
+ fontSize: 12,
+ letterSpacing: '-0.025em',
+ lineHeight: 1
+ },
+ regularSmallSquishedResponsive: {
+ ...fonts.poppinsRegular,
+ ...mixins.responsiveProperty('fontSize', { xs: 16, xl: 12 }),
+
+ letterSpacing: '-0.01em',
+ lineHeight: 1
+ },
+ regular: {
+ ...fonts.poppinsRegular,
+
+ fontSize: 13,
+ letterSpacing: '0.01em',
+ lineHeight: 1.2
+ },
+ regularSpaced: {
+ ...fonts.poppinsRegular,
+
+ fontSize: 14,
+ letterSpacing: '-0.025em',
+ lineHeight: 1.857
+ },
+ regularSquished: {
+ ...fonts.poppinsRegular,
+
+ fontSize: 14,
+ letterSpacing: '-0.025em',
+ lineHeight: 1.3
+ },
+ regularSquishedResponsive: {
+ ...fonts.poppinsRegular,
+ ...mixins.responsiveProperty('fontSize', { xs: 16, xl: 14 }),
+
+ letterSpacing: '-0.025em',
+ lineHeight: 1.3
+ },
+ regularMedium: {
+ ...fonts.poppinsRegular,
+
+ fontSize: 16,
+ letterSpacing: '-0.025em',
+ lineHeight: 1.75
+ },
+ regularMediumResponsive: {
+ ...fonts.poppinsRegular,
+ ...mixins.responsiveProperty('fontSize', { xs: 18, xl: 16 }),
+
+ letterSpacing: '-0.025em',
+ lineHeight: 1.75
+ },
+ regularMediumSquished: {
+ ...fonts.poppinsRegular,
+
+ fontSize: 16,
+ letterSpacing: '-0.025em',
+ lineHeight: 1
+ },
+ regularLarge: {
+ ...fonts.poppinsExtralight,
+
+ fontSize: 52,
+ letterSpacing: '-0.025em',
+ lineHeight: 1.385
+ },
+ medium: {
+ ...fonts.poppinsMedium,
+
+ fontSize: 16,
+ letterSpacing: '-0.01em',
+ lineHeight: 2
+ },
+ mediumSmall: {
+ ...fonts.poppinsMedium,
+
+ fontSize: 14,
+ letterSpacing: '-0.01em',
+ lineHeight: 2
+ },
+ mediumSquished: {
+ ...fonts.poppinsMedium,
+
+ fontSize: 12,
+ letterSpacing: '-0.025em',
+ lineHeight: 1
+ },
+ semiboldExtraSmall: {
+ ...fonts.poppinsSemibold,
+
+ fontSize: 13,
+ letterSpacing: '0.01em',
+ lineHeight: 1.2
+ },
+ semiboldExtraSmallSquished: {
+ ...fonts.poppinsSemibold,
+
+ fontSize: 12,
+ letterSpacing: '-0.025em',
+ lineHeight: 1
+ },
+ semiboldExtraSmallSpaced: {
+ ...fonts.poppinsSemibold,
+
+ fontSize: 13,
+ letterSpacing: '0.05em',
+ lineHeight: 1
+ },
+ semiboldSmallSquished: {
+ ...fonts.poppinsSemibold,
+
+ fontSize: 14,
+ letterSpacing: '-0.025em',
+ lineHeight: 1
+ },
+ semiboldSmallSpaced: {
+ ...fonts.poppinsSemibold,
+
+ fontSize: 15,
+ letterSpacing: '0.01em',
+ lineHeight: 1
+ },
+ semiboldSmall: {
+ ...fonts.poppinsSemibold,
+
+ fontSize: 14,
+ lineHeight: 1.2
+ },
+ semiboldSmallResponsive: {
+ ...fonts.poppinsSemibold,
+ ...mixins.responsiveProperty('fontSize', { xs: 16, xl: 14 }),
+
+ lineHeight: 1.2
+ },
+ semiboldSquished: {
+ ...fonts.poppinsSemibold,
+
+ fontSize: 22,
+ letterSpacing: '-0.025em',
+ lineHeight: 1.2
+ },
+ semiboldSquishedResponsive: {
+ ...fonts.poppinsSemibold,
+ ...mixins.responsiveProperty('fontSize', { xs: 18, xl: 16 }),
+
+ letterSpacing: '-0.01em',
+ lineHeight: 1
+ },
+ semibold: {
+ ...fonts.poppinsSemibold,
+
+ fontSize: 17,
+ letterSpacing: '-0.025em',
+ lineHeight: 1.2
+ },
+ semiboldMedium: {
+ ...fonts.poppinsSemibold,
+
+ fontSize: 20,
+ letterSpacing: '-0.025em',
+ lineHeight: 1.3
+ },
+ semiboldLarge: {
+ ...fonts.poppinsSemibold,
+
+ fontSize: 26,
+ letterSpacing: '-0.025em',
+ lineHeight: 1.3
+ },
+ semiboldLargeSquished: {
+ ...fonts.poppinsSemibold,
+
+ fontSize: 32,
+ letterSpacing: '-0.025em',
+ lineHeight: 1
+ },
+ semiboldExtraLarge: {
+ ...fonts.poppinsSemibold,
+
+ fontSize: 36,
+ letterSpacing: '-0.025em',
+ lineHeight: 1.1
+ },
+ bold: {
+ ...fonts.poppinsBold,
+
+ fontSize: 13,
+ letterSpacing: '0.05em',
+ lineHeight: 1.2
+ },
+ boldSquished: {
+ ...fonts.poppinsBold,
+
+ fontSize: 13,
+ letterSpacing: '-0.025em',
+ lineHeight: 1.2
+ },
+ boldMedium: {
+ ...fonts.poppinsBold,
+
+ fontSize: 16,
+ letterSpacing: '-0.025em',
+ lineHeight: 1.2
+ },
+ boldLargeResponsive: {
+ ...fonts.poppinsBold,
+ ...mixins.responsiveProperty('fontSize', { xs: 34, xl: 36 }),
+
+ letterSpacing: '-0.01em',
+ lineHeight: 1.278
+ },
+ boldExtraLargeResponsive: {
+ ...fonts.poppinsBold,
+ ...mixins.responsiveProperty('fontSize', { xs: 40, xl: 48 }),
+
+ letterSpacing: '-0.01em',
+ lineHeight: 1.354
+ },
+ robotoBold: {
+ ...fonts.robotoBold,
+
+ fontSize: 12,
+ lineHeight: 1
+ },
+ robotoBoldSpaced: {
+ ...fonts.robotoBold,
+
+ fontSize: 11,
+ letterSpacing: '0.05em',
+ lineHeight: 1
+ },
+ blackLarge: {
+ ...fonts.robotoBlack,
+
+ fontSize: 36,
+ letterSpacing: '-0.01em',
+ lineHeight: 1.27
+ }
+ },
+
+ units: {
+ alertBorderRadius: 4,
+ alertHeaderPaddingBottom: 25,
+ alertIconMargin: 10,
+ alertPositionRight: 20,
+ alertPositionBottom: 20,
+ alertVerticalPadding: 30,
+ alertHorizontalPadding: 40,
+ alertWidth: { xs: 350, xl: 488 },
+
+ assetBoxMarginBottom: 20,
+ assetBoxMarginRight: 20,
+ assetBoxPadding: 25,
+
+ assetBoxActionMarginRight: 15,
+
+ assetBoxNamePaddingTop: 50,
+ assetBoxNamePaddingBottom: 10,
+
+ buttonBorderRadius: 4,
+ buttonHeight_large: 50,
+ buttonHeight_normal: 50,
+ buttonHeight_small: 40,
+ buttonHeight_tiny: 25,
+ buttonHorizontalPadding_normal: 30,
+ buttonHorizontalPadding_small: 20,
+ buttonHorizontalPadding_tiny: 10,
+ buttonMarginRight: 10,
+ buttonMinWidth_large: 210,
+ buttonMinWidth_normal: 155,
+ buttonMinWidth_small: 106,
+ buttonMinWidth_tiny: 60,
+
+ buttonGroupInputHeight_small: 40,
+ buttonGroupInputHeight_normal: 40,
+ buttonGroupInputHeight_large: 50,
+ buttonGroupInputMinWidth_small: 40,
+ buttonGroupInputMinWidth_normal: 70,
+ buttonGroupInputMinWidth_large: 100,
+ buttonGroupInputPadding_small: 12,
+ buttonGroupInputPaddingHorizontal_normal: 30,
+ buttonGroupInputPaddingVertical_normal: 15,
+ buttonGroupInputPadding_large: 20,
+ buttonGroupInputBorderRadius: 4,
+ buttonGroupInputBorderWidth: 1,
+ buttonInputGroupMarginRight: 35,
+
+ buttonInputGroupLabelMarginRight: 10,
+
+ buttonInputGroupIconMarginRight: 10,
+
+ cardBorderRadius: 4,
+ cardVerticalPadding: 30,
+ cardHorizontalPadding: 50,
+ cardWidth: 400,
+
+ cardHeadingSpacing: 30,
+ cardTextSpacing: 20,
+
+ cardFootnotePaddingTop: 25,
+
+ checkboxTickFontSize: 12,
+ checkboxBorderRadius: 4,
+ checkboxBorderWidth: 1,
+ checkboxHorizontalMargin: 10,
+ checkboxHorizontalPadding: 10,
+ checkboxSize: 15,
+ checkboxVerticalPadding: 5,
+
+ codeBorderRadius: 2,
+ codeHorizontalPadding: 24,
+ codeVerticalPadding: 12,
+
+ colorPickerInputPopoverZindex: 2,
+ colorPickerInputPopoverTop: 0,
+ colorPickerInputPopoverLeft: 130,
+
+ colorTileMargin: 10,
+ colorTileBorderRadius: 5,
+ colorTileColorHeight: 20,
+ colorTileColorWidth: 35,
+
+ columnPaddingHorizontal: 15,
+
+ containerHorizontalPadding: 15,
+ containerWidth: 900,
+
+ dataTableCellBorderHeight: 50,
+ dataTableCellPadding: 25,
+
+ dataTableCheckboxBorderRadius: 4,
+ dataTableCheckboxBorderWidth: 1,
+ dataTableCheckboxSize: 24,
+ dataTableCheckboxSize_compact: 18,
+ dataTableCheckboxWrapperHorizontalPadding: 20,
+
+ dataTableDragHandleHorizontalPadding: 20,
+
+ dataTableHeaderHeight: 50,
+
+ dataTableRowActionPaddingBottom: 16,
+ dataTableRowActionPaddingRight: 25,
+ dataTableRowActionPaddingTop: 16,
+
+ dataTableRowBorderRadius: 4,
+ dataTableRowHeight: 90,
+ dataTableRowHeight_compact: 60,
+ dataTableRowMarginBottom: 20,
+ dataTableRowMarginBottom_compact: 2,
+
+ dataTileBorderRadius: 4,
+ dataTileHeight: 80,
+ dataTileHorizontalPadding: 50,
+
+ dataTileCircleShiftLeft: 20,
+ dataTileCircleShiftLeft_large: -18,
+ dataTileCircleShiftTop_large: -14,
+ dataTileCircleSize: 10,
+ dataTileCircleSize_large: 66,
+
+ dataTilesColumnGap: '30px',
+ dataTilesRowGap: '30px',
+
+ dialogBorderRadius: 4,
+ dialogHeaderPaddingBottom: 20,
+ dialogMarginTop: 50,
+ dialogPadding: 50,
+ dialogWidth: 443,
+
+ dialogCircleShiftBottom_small: -200,
+ dialogCircleShiftLeft_small: -65,
+ dialogCircleShiftRight_small: -100,
+ dialogCircleShiftTop_small: -85,
+ dialogCircleSize_large: 320,
+ dialogCircleSize_small: 198,
+
+ dialogDescriptionMarginBottom: 40,
+
+ drawerGroupIconMarginHorizontal: 20,
+
+ externalButtonBorderRadius: 4,
+ externalButtonHeight: 48,
+ externalButtonHorizontalPadding: 35,
+ externalButtonMarginTopResponsive: { xs: -20, xl: 0 },
+ externalButtonMarginLeftResponsive: { xs: 150, xl: -50 },
+ externalButtonWidth: 123,
+
+ externalFieldErrorArrowShiftHorizontal: 40,
+ externalFieldErrorArrowShiftVertical: 2,
+ externalFieldErrorArrowSize: 11,
+ externalFieldErrorHorizontalPadding: 25,
+ externalFieldErrorShiftLeft: 20,
+ externalFieldErrorShiftTop: -20,
+ externalFieldErrorVerticalPadding: 15,
+
+ externalFooterLinksMarginRightResponsive: { xs: 0, xl: 30 },
+
+ externalFooterLinkWrapperMarginBottomResponsive: { xs: 30, xl: 0 },
+
+ externalFooterPaddingBottomResponsive: { xs: 40 },
+ externalFooterPaddingTopResponsive: { xs: 100, xl: 150 },
+
+ externalFooterWrapperPaddingTop: 100,
+
+ externalGridContainerMaxWidthResponsive: {
+ xs: 'none',
+ md: 750,
+ lg: 970,
+ xl: 1180
+ },
+
+ externalHeaderPaddingTopResponsive: { xs: 30, lg: 30, xl: '3vw' },
+ externalHeaderPaddingBottomResponsive: { xs: 80, lg: 80, xl: '7vw' },
+
+ externalHeaderLinksMarginRight: 40,
+
+ externalHeadingMarginTopResponsive: { xs: 35, lg: 85 },
+ externalHeadingWidthResponsive: { xs: 'auto', lg: 650 },
+
+ externalHeroHeight: 600,
+
+ externalInputBorderRadius: 4,
+ externalInputHeight: 84,
+ externalInputPaddingLeft: 40,
+ externalInputPaddingRight: 60,
+ externalInputWidth: 350,
+
+ externalListItemBulletSize: 9,
+ externalListItemPaddingLeft: 15,
+
+ externalNavLinkHorizontalPadding_button: 35,
+
+ externalParagraphMarginTop: 30,
+
+ externalPageCircleShiftBottomResponsive: { xs: -100, xl: -330 },
+ externalPageCircleShiftLeftResponsive: { xs: -50, xl: -100 },
+ externalPageCircleSizeResponsive: { xs: 350, xl: 700 },
+
+ externalPageHeadingMarginBottomResponsive: { xs: 70, xl: 80 },
+ externalPageHeadingMarginTopResponsive: { xs: 35 },
+
+ externalTextLinkMarginTop: 50,
+
+ externalTextMarginTop: 50,
+
+ externalVerticalDividerHeight: 32,
+ externalVerticalDividerHorizontalMargin: 30,
+
+ fieldErrorVerticalPadding: 5,
+ fieldErrorHorizontalPadding: 10,
+
+ fieldHintPaddingLeft: 25,
+ fieldHintTop: 6,
+
+ headerItemContentMarginTop: 10,
+
+ headerItemHorizonalPadding: 25,
+ headerItemWidth: 190,
+ headerItemWidth_wide: 225,
+
+ headerMenuVerticalOffset: 4,
+
+ iconButtonSize_tiny: 36,
+ iconButtonSize_small: 55,
+
+ iconLinkIconMarginLeft: 7,
+
+ imageTileBorderRadius: 4,
+ imageTileInnerPadding: 35,
+
+ inputBorderMarginHorizontal_focus: -15,
+
+ inputMargin: 20,
+ inputMargin_spaced: 35,
+ inputPaddingBottom: 15,
+ inputPaddingTop: 25,
+ inputHorizontalPadding: 30,
+
+ internalContainerHorizontalPadding: 15,
+
+ hintBoxBorderRadius: 4,
+ hintBoxVerticalPadding: 20,
+ hintBoxPaddingRight: 50,
+ hintBoxPaddingLeft: 25,
+
+ hintIconMarginRight: 10,
+
+ internalBoxBorderRadius: 4,
+ internalBoxPaddingVertical: 50,
+
+ internalCardHorizontalPadding: 50,
+
+ internalContentPaddingBottom_fluid: 20,
+ internalContentPaddingBottom: 50,
+ internalContentPaddingHorizontal_fluid: 20,
+ internalContentPaddingLeft: 40,
+ internalContentPaddingRight: 70,
+ internalContentPaddingTop: 150,
+
+ internalDescriptionMaxWidth: 335,
+
+ internalHeaderBorderRadius: 4,
+ internalHeaderHeight: 68,
+ internalHeaderRight: 20,
+ internalHeaderTop: 20,
+
+ internalLayoutMinWidth: 1200,
+
+ internalLayoutBoxNumberHeight: 80,
+
+ internalSubTitleMarginVertical: 50,
+
+ keyLabelPaddingBottom: 10,
+
+ emptyWrapperBorderRadius: 4,
+ emptyWrapperHeight: 425,
+
+ emptyWrapperCircleShiftLeft_medium: -70,
+ emptyWrapperCircleShiftRight_large: 110,
+ emptyWrapperCircleShiftRight_small: 55,
+ emptyWrapperCircleShiftTop_large: 275,
+ emptyWrapperCircleShiftTop_medium: -150,
+ emptyWrapperCircleShiftTop_small: -85,
+ emptyWrapperCircleSize_large: 629,
+ emptyWrapperCircleSize_medium: 315,
+ emptyWrapperCircleSize_small: 149,
+
+ iconButtonHorizontalMargin: 5,
+
+ loaderImageMarginBottom: 15,
+
+ loaderTextMarginBottom: 45,
+
+ loaderTitleMarginBottom: 30,
+ loaderTitleMarginTop: 25,
+
+ menuBodyMaxHeight: 300,
+
+ menuBorderRadius: 4,
+ menuVerticalPadding: 10,
+
+ menuDividerVerticalMargin: 10,
+ menuFooterHeight: 45,
+
+ menuHeadingHorizontalPadding: 5,
+ menuHeadingVerticalPadding: 10,
+
+ menuLinkHorizontalPadding: 25,
+ menuLinkVerticalPadding: 10,
+
+ menuSearchHeaderHeight: 40,
+
+ modalCloseButtonPositionTop: 40,
+ modalCloseButtonPositionRight: 40,
+
+ modalHorizontalPadding: 245,
+ modalVerticalPadding: 120,
+
+ onboardingContentHeight: 490,
+ onboardingContentWidth: 400,
+ onboardingContentVerticalPadding: 20,
+
+ onboardingHeaderPaddingBottom: 60,
+
+ pageTitleMarginBottom: 40,
+ pageTitleMarginRight: 25,
+
+ panelBodyHorizontalPadding: 50,
+ panelBodyVerticalPadding: 30,
+
+ panelContainerWidth: 700,
+
+ panelDetailsMarginBottom: 35,
+
+ panelBorderRadius: 4,
+ panelMarginTop: 30,
+
+ panelHeaderHorizontalPadding: 50,
+ panelHeaderIconWrapperMarginRight: 10,
+ panelHeaderVerticalPadding: 25,
+
+ panelHeadingMarginBottom: 30,
+
+ panelSubHeadingMarginBottom: 20,
+
+ panelTableBorderWidth: 1,
+ panelTableMarginBottom: 20,
+
+ panelTableCellBadgeBorderRadius: 10,
+ panelTableCellBadgeHeight: 20,
+ panelTableCellBadgeHorizontalPadding: 10,
+ panelTableCellBadgeVerticalPadding: 5,
+ panelTableCellHeight: 40,
+ panelTableCellMarginLeft: 20,
+ panelTableCellSideContentMinWidth: 130,
+ panelTableCellSideContentPaddingLeft: 10,
+ panelTableCellSubtitleMarginTop: 15,
+ panelTableCellSubtitleMarginRight: 10,
+ panelTableCellSubtitleWidth: 250,
+ panelTableCellVerticalPadding: 18,
+ panelTableColumnMarginRight: 60,
+ panelTableColumnPaddingRight: 60,
+
+ profilePictureBorderWidth: 2,
+ profilePictureSize_tiny: 32,
+ profilePictureSize_small: 48,
+ profilePictureSize_large: 200,
+
+ separatorHorizontalMargin: 15,
+ separatorWidth: 20,
+
+ radioInputBulletBorderWidth_active: 7,
+ radioInputLabelMarginBottom: 20,
+ radioInputOptionMarginBottom_regular: 16,
+ radioInputOptionMarginBottom_small: 10,
+ radioInputOptionPaddingLeft: 35,
+ radioInputOptionMarginLeft_small: 10,
+
+ roleHintsMarginBottom: -20,
+ roleHintsMarginTop: 10,
+
+ roleHintMinHeight: 40,
+ roleHintPaddingLeft: 25,
+
+ rowMarginHorizontal: -15,
+
+ sidebarWidth: 220,
+
+ sidebarBreadcrumbIconPaddingRight: 15,
+
+ sidebarBreadcrumbPaddingVertical: 20,
+
+ sidebarBreadcrumbTextWrapperMarginTop: 15,
+
+ sidebarBreadcrumbItemMarginHorizontal: 16,
+ sidebarBreadcrumbItemMarginVertical: -12,
+ sidebarBreadcrumbItemPaddingVertical: 20,
+
+ sidebarHeaderPaddingTop: 35,
+ sidebarHeaderPaddingBottom: 25,
+
+ sidebarHorizontalPadding: 20,
+
+ sidebarItemIconMarginRight: 15,
+ sidebarItemIconSize: 36,
+
+ sidebarItemPaddingHorizontal: 40,
+ sidebarItemPaddingVertical: 4,
+
+ sidePaneWidth: 550,
+
+ sidePaneBodyPadding: 50,
+
+ sidePaneButtonMargin: -20,
+
+ sidePaneHeaderArrowLeft: 20,
+ sidePaneHeaderArrowTop: 55,
+ sidePaneHeaderPadding: 50,
+
+ sidePaneHintMaxWidth: 380,
+
+ sidePaneTitleMarginBottom: 20,
+
+ sortAndFilterInputWidth: 150,
+ sortAndFilterMenuItemHorizontalPadding: 10,
+
+ switchInputBorderWidth: 1,
+ switchInputKnobSize: 20,
+ switchInputLabelHorizontalMargin: 10,
+ switchInputWidth: 36,
+
+ tabPadding: 15,
+ tabUnderlineHeight: 1,
+ tabUnderlineHeight_selected: 2,
+ tabDividerHeight: 40,
+ tabHeight_fixed: 60,
+ tabWidth_fixed: 125,
+ tabLinkUnderlineHeight_active: 2,
+
+ tagBorderRadius: 10,
+ tagHeight: 20,
+ tagMarginRight: 5,
+ tagPaddingHorizontal: 8,
+
+ templateTabsWidth: 210,
+
+ textInputHorizontalMargin_expandable: 10,
+
+ tooltipPadding: 10,
+
+ tooltipArrowSize: 10,
+ tooltipArrowVerticalShift: -2,
+
+ uploadInputBorderRadius: 2,
+ uploadInputBorderWidth: 2,
+ uploadInputPadding: 20,
+ uploadInputPreviewWidth: 200,
+ uploadInputPreviewHeight: 100
+ },
+
+ zIndexes: {
+ internalHeader: 100,
+ modal: 200,
+ menu: 300,
+ tooltip: 400,
+ alert: 500,
+ appLoader: 1000
+ }
+}
+
+export default theme
diff --git a/ui/src/template.ejs b/ui/src/template.ejs
new file mode 100644
index 0000000..a667bb4
--- /dev/null
+++ b/ui/src/template.ejs
@@ -0,0 +1,8 @@
+
+
+
+
+
+
+
+
diff --git a/ui/static.json b/ui/static.json
new file mode 100644
index 0000000..590443c
--- /dev/null
+++ b/ui/static.json
@@ -0,0 +1,13 @@
+{
+ "root": "dist/",
+ "canonical_host": "${HOST}",
+ "canonical_subdomains": true,
+ "https_only": true,
+ "routes": {
+ "/javascripts/*": "/javascripts/",
+ "/stylesheets/*": "/stylesheets/",
+ "/images/*": "/images/",
+ "/fonts/*": "/fonts/",
+ "/**": "index.html"
+ }
+}
diff --git a/ui/webpack.config.js b/ui/webpack.config.js
new file mode 100644
index 0000000..b39a48d
--- /dev/null
+++ b/ui/webpack.config.js
@@ -0,0 +1,223 @@
+const dotenv = require('dotenv')
+const path = require('path')
+const webpack = require('webpack')
+const merge = require('webpack-merge')
+const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin
+const CompressionPlugin = require('compression-webpack-plugin')
+const DotenvPlugin = require('webpack-dotenv-plugin')
+const FaviconsWebpackPlugin = require('favicons-webpack-plugin')
+const HtmlWebpackPlugin = require('html-webpack-plugin')
+const LodashModuleReplacementPlugin = require('lodash-webpack-plugin')
+const MiniCssExtractPlugin = require('mini-css-extract-plugin')
+const OptimizeCSSAssetsPlugin = require('optimize-css-assets-webpack-plugin')
+const SentryWebpackPlugin = require('@sentry/webpack-plugin')
+const TerserPlugin = require('terser-webpack-plugin')
+const WebpackNotifierPlugin = require('webpack-notifier')
+
+const envFilename = (() => {
+ if (process.env.HEROKU) {
+ return '.env.heroku'
+ }
+
+ if (!process.env.NODE_ENV || process.env.NODE_ENV === 'development') {
+ return '.env'
+ }
+
+ return `.env.${process.env.NODE_ENV}`
+})()
+
+const PATHS = {
+ env: path.join(__dirname, envFilename),
+ envExample: path.join(__dirname, '.env.example'),
+ js: path.join(__dirname, 'src/index.js'),
+ css: path.join(__dirname, 'src/assets/stylesheets/main.css'),
+ favicon: path.join(__dirname, 'src/assets/images/favicon.png')
+}
+
+// Load environment variables to be used in webpack.config.js
+dotenv.config({ path: PATHS.env })
+
+const config = {}
+
+config.common = {
+ context: path.join(__dirname, 'src'),
+ output: {
+ path: path.join(__dirname, 'dist'),
+ publicPath: process.env.PUBLIC_PATH
+ },
+ module: {
+ rules: [
+ {
+ test: /\.js$/,
+ use: {
+ loader: 'babel-loader',
+ options: {
+ cacheDirectory: true
+ }
+ },
+ exclude: /(node_modules|bower_components)/
+ }, {
+ test: /\.(jpe?g|png|gif|svg)$/,
+ use: 'file-loader?name=images/[name]-[hash:8].[ext]'
+ }, {
+ test: /\.(woff|woff2|ttf|eot)(\?v=[0-9]\.[0-9]\.[0-9])?$/,
+ use: 'file-loader?name=fonts/[name]-[hash:8].[ext]'
+ }
+ ]
+ },
+ resolve: {
+ alias: {
+ client: path.join(__dirname, 'src', 'client'),
+ components: path.join(__dirname, 'src', 'components'),
+ constants: path.join(__dirname, 'src', 'constants'),
+ fonts: path.join(__dirname, 'src', 'assets', 'fonts'),
+ images: path.join(__dirname, 'src', 'assets', 'images'),
+ lib: path.join(__dirname, 'src', 'lib'),
+ models: path.join(__dirname, 'src', 'models'),
+ mutations: path.join(__dirname, 'src', 'mutations'),
+ node_modules: path.join(__dirname, 'node_modules'),
+ queries: path.join(__dirname, 'src', 'queries'),
+ styles: path.join(__dirname, 'src', 'styles')
+ }
+ },
+ plugins: [
+ new DotenvPlugin({
+ sample: PATHS.envExample,
+ path: PATHS.env
+ }),
+ new LodashModuleReplacementPlugin({
+ collections: true,
+ paths: true,
+ shorthands: true
+ }),
+ new FaviconsWebpackPlugin({
+ logo: PATHS.favicon,
+ background: 'transparent',
+ prefix: 'images/favicons-[hash]/'
+ }),
+ new HtmlWebpackPlugin({ template: 'template.ejs' }),
+ new BundleAnalyzerPlugin({
+ analyzerMode: process.env.PROFILE ? 'server' : 'disabled'
+ })
+ ]
+}
+
+config.development = {
+ mode: 'development',
+ devtool: 'cheap-module-eval-source-map',
+ entry: {
+ app: [
+ PATHS.js,
+ PATHS.css
+ ]
+ },
+ output: {
+ filename: 'javascripts/[name].js',
+ },
+ devServer: {
+ host: process.env.HOST,
+ port: process.env.PORT,
+ hotOnly: true,
+ historyApiFallback: true,
+ open: true,
+ noInfo: true,
+ stats: 'errors-only'
+ },
+ plugins: [
+ new webpack.HotModuleReplacementPlugin(),
+ new webpack.NamedModulesPlugin(),
+ new WebpackNotifierPlugin()
+ ],
+ module: {
+ rules: [
+ // Preloaders
+ {
+ test: /\.js$/,
+ enforce: 'pre',
+ loader: 'eslint-loader',
+ options: {
+ emitWarning: true,
+ }
+ },
+
+ {
+ test: /\.(css)$/,
+ use: ['style-loader', 'css-loader']
+ }
+ ]
+ }
+}
+
+config.production = {
+ mode: 'production',
+ devtool: 'nosources-source-map',
+ entry: {
+ app: [
+ PATHS.js,
+ PATHS.css
+ ]
+ },
+ output: {
+ filename: 'javascripts/[name]-[chunkhash:8].js',
+ sourceMapFilename: 'javascripts/[name]-[chunkhash:8].map.js'
+ },
+ optimization: {
+ minimizer: [
+ new TerserPlugin({
+ cache: true,
+ parallel: true,
+ sourceMap: true
+ }),
+ new OptimizeCSSAssetsPlugin({})
+ ],
+ splitChunks: {
+ cacheGroups: {
+ styles: {
+ name: 'styles',
+ test: /\.css$/,
+ chunks: 'all',
+ enforce: true
+ }
+ }
+ }
+ },
+ plugins: [
+ new webpack.optimize.ModuleConcatenationPlugin(),
+ new webpack.HashedModuleIdsPlugin(),
+ new MiniCssExtractPlugin({
+ filename: 'stylesheets/[name]-[contenthash:8].css'
+ }),
+ new CompressionPlugin({
+ algorithm: 'gzip',
+ test: /\.js$|\.css$|\.html$/,
+ threshold: 10240,
+ minRatio: 0.8
+ }),
+ new SentryWebpackPlugin({
+ include: './dist',
+ release: process.env.HEROKU_SLUG_COMMIT
+ })
+ ],
+ module: {
+ rules: [
+ // Preloaders
+ {
+ test: /\.js$/,
+ enforce: 'pre',
+ loader: 'eslint-loader'
+ },
+
+ {
+ test: /\.css$/,
+ use: [
+ MiniCssExtractPlugin.loader,
+ 'css-loader'
+ ]
+ }
+ ]
+ }
+}
+
+config.staging = config.production;
+
+module.exports = merge(config.common, config[process.env.NODE_ENV]);
diff --git a/ui/yarn.lock b/ui/yarn.lock
new file mode 100644
index 0000000..a551517
--- /dev/null
+++ b/ui/yarn.lock
@@ -0,0 +1,8869 @@
+# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.
+# yarn lockfile v1
+
+
+"@babel/code-frame@7.0.0-beta.44":
+ version "7.0.0-beta.44"
+ resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.0.0-beta.44.tgz#2a02643368de80916162be70865c97774f3adbd9"
+ dependencies:
+ "@babel/highlight" "7.0.0-beta.44"
+
+"@babel/code-frame@^7.0.0":
+ version "7.0.0"
+ resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.0.0.tgz#06e2ab19bdb535385559aabb5ba59729482800f8"
+ dependencies:
+ "@babel/highlight" "^7.0.0"
+
+"@babel/generator@7.0.0-beta.44":
+ version "7.0.0-beta.44"
+ resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-7.0.0-beta.44.tgz#c7e67b9b5284afcf69b309b50d7d37f3e5033d42"
+ dependencies:
+ "@babel/types" "7.0.0-beta.44"
+ jsesc "^2.5.1"
+ lodash "^4.2.0"
+ source-map "^0.5.0"
+ trim-right "^1.0.1"
+
+"@babel/helper-function-name@7.0.0-beta.44":
+ version "7.0.0-beta.44"
+ resolved "https://registry.yarnpkg.com/@babel/helper-function-name/-/helper-function-name-7.0.0-beta.44.tgz#e18552aaae2231100a6e485e03854bc3532d44dd"
+ dependencies:
+ "@babel/helper-get-function-arity" "7.0.0-beta.44"
+ "@babel/template" "7.0.0-beta.44"
+ "@babel/types" "7.0.0-beta.44"
+
+"@babel/helper-get-function-arity@7.0.0-beta.44":
+ version "7.0.0-beta.44"
+ resolved "https://registry.yarnpkg.com/@babel/helper-get-function-arity/-/helper-get-function-arity-7.0.0-beta.44.tgz#d03ca6dd2b9f7b0b1e6b32c56c72836140db3a15"
+ dependencies:
+ "@babel/types" "7.0.0-beta.44"
+
+"@babel/helper-module-imports@^7.0.0-beta.49":
+ version "7.0.0-beta.49"
+ resolved "https://registry.yarnpkg.com/@babel/helper-module-imports/-/helper-module-imports-7.0.0-beta.49.tgz#41d7d59891016c493432a46f7464446552890c75"
+ dependencies:
+ "@babel/types" "7.0.0-beta.49"
+ lodash "^4.17.5"
+
+"@babel/helper-split-export-declaration@7.0.0-beta.44":
+ version "7.0.0-beta.44"
+ resolved "https://registry.yarnpkg.com/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.0.0-beta.44.tgz#c0b351735e0fbcb3822c8ad8db4e583b05ebd9dc"
+ dependencies:
+ "@babel/types" "7.0.0-beta.44"
+
+"@babel/highlight@7.0.0-beta.44":
+ version "7.0.0-beta.44"
+ resolved "https://registry.yarnpkg.com/@babel/highlight/-/highlight-7.0.0-beta.44.tgz#18c94ce543916a80553edcdcf681890b200747d5"
+ dependencies:
+ chalk "^2.0.0"
+ esutils "^2.0.2"
+ js-tokens "^3.0.0"
+
+"@babel/highlight@^7.0.0":
+ version "7.0.0"
+ resolved "https://registry.yarnpkg.com/@babel/highlight/-/highlight-7.0.0.tgz#f710c38c8d458e6dd9a201afb637fcb781ce99e4"
+ dependencies:
+ chalk "^2.0.0"
+ esutils "^2.0.2"
+ js-tokens "^4.0.0"
+
+"@babel/runtime@7.0.0":
+ version "7.0.0"
+ resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.0.0.tgz#adeb78fedfc855aa05bc041640f3f6f98e85424c"
+ dependencies:
+ regenerator-runtime "^0.12.0"
+
+"@babel/runtime@^7.1.2", "@babel/runtime@^7.3.4":
+ version "7.7.7"
+ resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.7.7.tgz#194769ca8d6d7790ec23605af9ee3e42a0aa79cf"
+ integrity sha512-uCnC2JEVAu8AKB5do1WRIsvrdJ0flYx/A/9f/6chdacnEZ7LmavjdsDXr5ksYBegxtuTPR5Va9/+13QF/kFkCA==
+ dependencies:
+ regenerator-runtime "^0.13.2"
+
+"@babel/runtime@^7.3.1":
+ version "7.3.1"
+ resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.3.1.tgz#574b03e8e8a9898eaf4a872a92ea20b7846f6f2a"
+ dependencies:
+ regenerator-runtime "^0.12.0"
+
+"@babel/runtime@^7.4.5":
+ version "7.5.5"
+ resolved "https://registry.npmjs.org/@babel/runtime/-/runtime-7.5.5.tgz#74fba56d35efbeca444091c7850ccd494fd2f132"
+ integrity sha512-28QvEGyQyNkB0/m2B4FU7IEZGK2NUrcMtT6BZEFALTguLk+AUT6ofsHtPk5QyjAdUkpMJ+/Em+quwz4HOt30AQ==
+ dependencies:
+ regenerator-runtime "^0.13.2"
+
+"@babel/template@7.0.0-beta.44":
+ version "7.0.0-beta.44"
+ resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.0.0-beta.44.tgz#f8832f4fdcee5d59bf515e595fc5106c529b394f"
+ dependencies:
+ "@babel/code-frame" "7.0.0-beta.44"
+ "@babel/types" "7.0.0-beta.44"
+ babylon "7.0.0-beta.44"
+ lodash "^4.2.0"
+
+"@babel/traverse@7.0.0-beta.44":
+ version "7.0.0-beta.44"
+ resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.0.0-beta.44.tgz#a970a2c45477ad18017e2e465a0606feee0d2966"
+ dependencies:
+ "@babel/code-frame" "7.0.0-beta.44"
+ "@babel/generator" "7.0.0-beta.44"
+ "@babel/helper-function-name" "7.0.0-beta.44"
+ "@babel/helper-split-export-declaration" "7.0.0-beta.44"
+ "@babel/types" "7.0.0-beta.44"
+ babylon "7.0.0-beta.44"
+ debug "^3.1.0"
+ globals "^11.1.0"
+ invariant "^2.2.0"
+ lodash "^4.2.0"
+
+"@babel/types@7.0.0-beta.44":
+ version "7.0.0-beta.44"
+ resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.0.0-beta.44.tgz#6b1b164591f77dec0a0342aca995f2d046b3a757"
+ dependencies:
+ esutils "^2.0.2"
+ lodash "^4.2.0"
+ to-fast-properties "^2.0.0"
+
+"@babel/types@7.0.0-beta.49", "@babel/types@^7.0.0-beta.49":
+ version "7.0.0-beta.49"
+ resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.0.0-beta.49.tgz#b7e3b1c3f4d4cfe11bdf8c89f1efd5e1617b87a6"
+ dependencies:
+ esutils "^2.0.2"
+ lodash "^4.17.5"
+ to-fast-properties "^2.0.0"
+
+"@icons/material@^0.2.4":
+ version "0.2.4"
+ resolved "https://registry.yarnpkg.com/@icons/material/-/material-0.2.4.tgz#e90c9f71768b3736e76d7dd6783fc6c2afa88bc8"
+
+"@mrmlnc/readdir-enhanced@^2.2.1":
+ version "2.2.1"
+ resolved "https://registry.yarnpkg.com/@mrmlnc/readdir-enhanced/-/readdir-enhanced-2.2.1.tgz#524af240d1a360527b730475ecfa1344aa540dde"
+ dependencies:
+ call-me-maybe "^1.0.1"
+ glob-to-regexp "^0.3.0"
+
+"@nodelib/fs.stat@^1.1.2":
+ version "1.1.3"
+ resolved "https://registry.yarnpkg.com/@nodelib/fs.stat/-/fs.stat-1.1.3.tgz#2b5a3ab3f918cca48a8c754c08168e3f03eba61b"
+
+"@sentry/browser@^4.6.3":
+ version "4.6.3"
+ resolved "https://registry.yarnpkg.com/@sentry/browser/-/browser-4.6.3.tgz#e683c46c34937d997923be7270b8f071d6957cc6"
+ dependencies:
+ "@sentry/core" "4.6.3"
+ "@sentry/types" "4.5.3"
+ "@sentry/utils" "4.6.3"
+ tslib "^1.9.3"
+
+"@sentry/cli@^1.35.5":
+ version "1.39.0"
+ resolved "https://registry.yarnpkg.com/@sentry/cli/-/cli-1.39.0.tgz#126a20ade9df90ce3e9b99f5b36c8ca6c8ee5b9d"
+ dependencies:
+ fs-copy-file-sync "^1.1.1"
+ https-proxy-agent "^2.2.1"
+ mkdirp "^0.5.1"
+ node-fetch "^2.1.2"
+ progress "2.0.0"
+ proxy-from-env "^1.0.0"
+
+"@sentry/core@4.6.3":
+ version "4.6.3"
+ resolved "https://registry.yarnpkg.com/@sentry/core/-/core-4.6.3.tgz#5c120dea76d0b19757296e9c244f8c3939de744e"
+ dependencies:
+ "@sentry/hub" "4.6.3"
+ "@sentry/minimal" "4.6.3"
+ "@sentry/types" "4.5.3"
+ "@sentry/utils" "4.6.3"
+ tslib "^1.9.3"
+
+"@sentry/hub@4.6.3":
+ version "4.6.3"
+ resolved "https://registry.yarnpkg.com/@sentry/hub/-/hub-4.6.3.tgz#522c4fa5ac1434dcfbaa326ab279b81d595ec739"
+ dependencies:
+ "@sentry/types" "4.5.3"
+ "@sentry/utils" "4.6.3"
+ tslib "^1.9.3"
+
+"@sentry/minimal@4.6.3":
+ version "4.6.3"
+ resolved "https://registry.yarnpkg.com/@sentry/minimal/-/minimal-4.6.3.tgz#5ff78eefaa15cda6508b181d4f4e757ec1385828"
+ dependencies:
+ "@sentry/hub" "4.6.3"
+ "@sentry/types" "4.5.3"
+ tslib "^1.9.3"
+
+"@sentry/types@4.5.3":
+ version "4.5.3"
+ resolved "https://registry.yarnpkg.com/@sentry/types/-/types-4.5.3.tgz#3350dce2b7f9b936a8c327891c12e3aef7bd8852"
+
+"@sentry/utils@4.6.3":
+ version "4.6.3"
+ resolved "https://registry.yarnpkg.com/@sentry/utils/-/utils-4.6.3.tgz#0dc4748ef4ead089ceb2c4a3670339df8be71679"
+ dependencies:
+ "@sentry/types" "4.5.3"
+ tslib "^1.9.3"
+
+"@sentry/webpack-plugin@^1.6.2":
+ version "1.6.2"
+ resolved "https://registry.yarnpkg.com/@sentry/webpack-plugin/-/webpack-plugin-1.6.2.tgz#47b1a43f7e691c1c8dc45677f7380c5e1aa6596d"
+ dependencies:
+ "@sentry/cli" "^1.35.5"
+
+"@types/sortablejs@^1.10.0":
+ version "1.10.2"
+ resolved "https://registry.yarnpkg.com/@types/sortablejs/-/sortablejs-1.10.2.tgz#d749f6fe9965322b2107f6711a6fed692a71941e"
+ integrity sha512-aWK2oTpbjNmLyexl95L4ttd0kFIvbMIf1JR2YbNhUwIk9Y1cOwfAfyvfxBBmtg1ZDy64gpbgEdFjyqnzjh+3/A==
+
+"@types/zen-observable@^0.8.0":
+ version "0.8.0"
+ resolved "https://registry.yarnpkg.com/@types/zen-observable/-/zen-observable-0.8.0.tgz#8b63ab7f1aa5321248aad5ac890a485656dcea4d"
+
+"@uppy/core@^0.30.2":
+ version "0.30.2"
+ resolved "https://registry.yarnpkg.com/@uppy/core/-/core-0.30.2.tgz#af50fd9bed5f5921a3e08484f6de030098e5f2a0"
+ dependencies:
+ "@uppy/store-default" "^0.28.2"
+ "@uppy/utils" "^0.30.2"
+ cuid "^2.1.1"
+ lodash.throttle "^4.1.1"
+ mime-match "^1.0.2"
+ namespace-emitter "^2.0.1"
+ preact "^8.2.9"
+ prettier-bytes "^1.0.4"
+
+"@uppy/dashboard@^0.30.2":
+ version "0.30.2"
+ resolved "https://registry.yarnpkg.com/@uppy/dashboard/-/dashboard-0.30.2.tgz#b5113ffc0b31ca379029d9a34aead891cd96c635"
+ dependencies:
+ "@uppy/informer" "^0.30.2"
+ "@uppy/provider-views" "^0.30.2"
+ "@uppy/status-bar" "^0.30.2"
+ "@uppy/thumbnail-generator" "^0.30.2"
+ "@uppy/utils" "^0.30.2"
+ classnames "^2.2.6"
+ cuid "^2.1.1"
+ drag-drop "2.13.3"
+ lodash.throttle "^4.1.1"
+ preact "^8.2.9"
+ preact-css-transition-group "^1.3.0"
+ prettier-bytes "^1.0.4"
+ resize-observer-polyfill "^1.5.0"
+
+"@uppy/drag-drop@^0.30.2":
+ version "0.30.2"
+ resolved "https://registry.yarnpkg.com/@uppy/drag-drop/-/drag-drop-0.30.2.tgz#5e01ecdcada23d9ef3839a66a58886042816bca2"
+ dependencies:
+ "@uppy/utils" "^0.30.2"
+ drag-drop "2.13.3"
+ preact "^8.2.9"
+
+"@uppy/informer@^0.30.2":
+ version "0.30.2"
+ resolved "https://registry.yarnpkg.com/@uppy/informer/-/informer-0.30.2.tgz#d009831e97f61ec6243fefbb61993e28974b3258"
+ dependencies:
+ "@uppy/utils" "^0.30.2"
+ preact "^8.2.9"
+
+"@uppy/progress-bar@^0.30.2":
+ version "0.30.2"
+ resolved "https://registry.yarnpkg.com/@uppy/progress-bar/-/progress-bar-0.30.2.tgz#0a022d584cbc7f440211c589448b2e881156bab1"
+ dependencies:
+ "@uppy/utils" "^0.30.2"
+ preact "^8.2.9"
+
+"@uppy/provider-views@^0.30.2":
+ version "0.30.2"
+ resolved "https://registry.yarnpkg.com/@uppy/provider-views/-/provider-views-0.30.2.tgz#4fa8f1374a6c98ec130a60ec85a64d8f9a58fb86"
+ dependencies:
+ "@uppy/utils" "^0.30.2"
+ classnames "^2.2.6"
+ preact "^8.2.9"
+
+"@uppy/react@^0.30.2":
+ version "0.30.2"
+ resolved "https://registry.yarnpkg.com/@uppy/react/-/react-0.30.2.tgz#b9f1c8516dada8583b9469bed36f3edc81f5bdcb"
+ dependencies:
+ "@uppy/dashboard" "^0.30.2"
+ "@uppy/drag-drop" "^0.30.2"
+ "@uppy/progress-bar" "^0.30.2"
+ "@uppy/status-bar" "^0.30.2"
+ "@uppy/utils" "^0.30.2"
+ prop-types "^15.6.1"
+
+"@uppy/status-bar@^0.30.2":
+ version "0.30.2"
+ resolved "https://registry.yarnpkg.com/@uppy/status-bar/-/status-bar-0.30.2.tgz#a71d871da2e0870c00a93362fd93e8dd9c0446a6"
+ dependencies:
+ "@uppy/utils" "^0.30.2"
+ classnames "^2.2.6"
+ lodash.throttle "^4.1.1"
+ preact "^8.2.9"
+ prettier-bytes "^1.0.4"
+
+"@uppy/store-default@^0.28.2":
+ version "0.28.2"
+ resolved "https://registry.yarnpkg.com/@uppy/store-default/-/store-default-0.28.2.tgz#96bdb22e4f6321dcbf1fe5cde57f14c3771044ce"
+
+"@uppy/thumbnail-generator@^0.30.2":
+ version "0.30.2"
+ resolved "https://registry.yarnpkg.com/@uppy/thumbnail-generator/-/thumbnail-generator-0.30.2.tgz#b1f1046f43bd6dbfacbd008f7cd3552bc55b3407"
+ dependencies:
+ "@uppy/utils" "^0.30.2"
+
+"@uppy/utils@^0.30.2":
+ version "0.30.2"
+ resolved "https://registry.yarnpkg.com/@uppy/utils/-/utils-0.30.2.tgz#878209dc0396fd71ae5ac6f2c7fd99c1c7fbe551"
+ dependencies:
+ lodash.throttle "^4.1.1"
+
+"@webassemblyjs/ast@1.8.3":
+ version "1.8.3"
+ resolved "https://registry.yarnpkg.com/@webassemblyjs/ast/-/ast-1.8.3.tgz#63a741bd715a6b6783f2ea5c6ab707516aa215eb"
+ dependencies:
+ "@webassemblyjs/helper-module-context" "1.8.3"
+ "@webassemblyjs/helper-wasm-bytecode" "1.8.3"
+ "@webassemblyjs/wast-parser" "1.8.3"
+
+"@webassemblyjs/floating-point-hex-parser@1.8.3":
+ version "1.8.3"
+ resolved "https://registry.yarnpkg.com/@webassemblyjs/floating-point-hex-parser/-/floating-point-hex-parser-1.8.3.tgz#f198a2d203b3c50846a064f5addd6a133ef9bc0e"
+
+"@webassemblyjs/helper-api-error@1.8.3":
+ version "1.8.3"
+ resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-api-error/-/helper-api-error-1.8.3.tgz#3b708f6926accd64dcbaa7ba5b63db5660ff4f66"
+
+"@webassemblyjs/helper-buffer@1.8.3":
+ version "1.8.3"
+ resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-buffer/-/helper-buffer-1.8.3.tgz#f3150a23ffaba68621e1f094c8a14bebfd53dd48"
+
+"@webassemblyjs/helper-code-frame@1.8.3":
+ version "1.8.3"
+ resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-code-frame/-/helper-code-frame-1.8.3.tgz#f43ac605789b519d95784ef350fd2968aebdd3ef"
+ dependencies:
+ "@webassemblyjs/wast-printer" "1.8.3"
+
+"@webassemblyjs/helper-fsm@1.8.3":
+ version "1.8.3"
+ resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-fsm/-/helper-fsm-1.8.3.tgz#46aaa03f41082a916850ebcb97e9fc198ef36a9c"
+
+"@webassemblyjs/helper-module-context@1.8.3":
+ version "1.8.3"
+ resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-module-context/-/helper-module-context-1.8.3.tgz#150da405d90c8ea81ae0b0e1965b7b64e585634f"
+ dependencies:
+ "@webassemblyjs/ast" "1.8.3"
+ mamacro "^0.0.3"
+
+"@webassemblyjs/helper-wasm-bytecode@1.8.3":
+ version "1.8.3"
+ resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-wasm-bytecode/-/helper-wasm-bytecode-1.8.3.tgz#12f55bbafbbc7ddf9d8059a072cb7b0c17987901"
+
+"@webassemblyjs/helper-wasm-section@1.8.3":
+ version "1.8.3"
+ resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-wasm-section/-/helper-wasm-section-1.8.3.tgz#9e79456d9719e116f4f8998ee62ab54ba69a6cf3"
+ dependencies:
+ "@webassemblyjs/ast" "1.8.3"
+ "@webassemblyjs/helper-buffer" "1.8.3"
+ "@webassemblyjs/helper-wasm-bytecode" "1.8.3"
+ "@webassemblyjs/wasm-gen" "1.8.3"
+
+"@webassemblyjs/ieee754@1.8.3":
+ version "1.8.3"
+ resolved "https://registry.yarnpkg.com/@webassemblyjs/ieee754/-/ieee754-1.8.3.tgz#0a89355b1f6c9d08d0605c2acbc2a6fe3141f5b4"
+ dependencies:
+ "@xtuc/ieee754" "^1.2.0"
+
+"@webassemblyjs/leb128@1.8.3":
+ version "1.8.3"
+ resolved "https://registry.yarnpkg.com/@webassemblyjs/leb128/-/leb128-1.8.3.tgz#b7fd9d7c039e34e375c4473bd4dc89ce8228b920"
+ dependencies:
+ "@xtuc/long" "4.2.2"
+
+"@webassemblyjs/utf8@1.8.3":
+ version "1.8.3"
+ resolved "https://registry.yarnpkg.com/@webassemblyjs/utf8/-/utf8-1.8.3.tgz#75712db52cfdda868731569ddfe11046f1f1e7a2"
+
+"@webassemblyjs/wasm-edit@1.8.3":
+ version "1.8.3"
+ resolved "https://registry.yarnpkg.com/@webassemblyjs/wasm-edit/-/wasm-edit-1.8.3.tgz#23c3c6206b096f9f6aa49623a5310a102ef0fb87"
+ dependencies:
+ "@webassemblyjs/ast" "1.8.3"
+ "@webassemblyjs/helper-buffer" "1.8.3"
+ "@webassemblyjs/helper-wasm-bytecode" "1.8.3"
+ "@webassemblyjs/helper-wasm-section" "1.8.3"
+ "@webassemblyjs/wasm-gen" "1.8.3"
+ "@webassemblyjs/wasm-opt" "1.8.3"
+ "@webassemblyjs/wasm-parser" "1.8.3"
+ "@webassemblyjs/wast-printer" "1.8.3"
+
+"@webassemblyjs/wasm-gen@1.8.3":
+ version "1.8.3"
+ resolved "https://registry.yarnpkg.com/@webassemblyjs/wasm-gen/-/wasm-gen-1.8.3.tgz#1a433b8ab97e074e6ac2e25fcbc8cb6125400813"
+ dependencies:
+ "@webassemblyjs/ast" "1.8.3"
+ "@webassemblyjs/helper-wasm-bytecode" "1.8.3"
+ "@webassemblyjs/ieee754" "1.8.3"
+ "@webassemblyjs/leb128" "1.8.3"
+ "@webassemblyjs/utf8" "1.8.3"
+
+"@webassemblyjs/wasm-opt@1.8.3":
+ version "1.8.3"
+ resolved "https://registry.yarnpkg.com/@webassemblyjs/wasm-opt/-/wasm-opt-1.8.3.tgz#54754bcf88f88e92b909416a91125301cc81419c"
+ dependencies:
+ "@webassemblyjs/ast" "1.8.3"
+ "@webassemblyjs/helper-buffer" "1.8.3"
+ "@webassemblyjs/wasm-gen" "1.8.3"
+ "@webassemblyjs/wasm-parser" "1.8.3"
+
+"@webassemblyjs/wasm-parser@1.8.3":
+ version "1.8.3"
+ resolved "https://registry.yarnpkg.com/@webassemblyjs/wasm-parser/-/wasm-parser-1.8.3.tgz#d12ed19d1b8e8667a7bee040d2245aaaf215340b"
+ dependencies:
+ "@webassemblyjs/ast" "1.8.3"
+ "@webassemblyjs/helper-api-error" "1.8.3"
+ "@webassemblyjs/helper-wasm-bytecode" "1.8.3"
+ "@webassemblyjs/ieee754" "1.8.3"
+ "@webassemblyjs/leb128" "1.8.3"
+ "@webassemblyjs/utf8" "1.8.3"
+
+"@webassemblyjs/wast-parser@1.8.3":
+ version "1.8.3"
+ resolved "https://registry.yarnpkg.com/@webassemblyjs/wast-parser/-/wast-parser-1.8.3.tgz#44aa123e145503e995045dc3e5e2770069da117b"
+ dependencies:
+ "@webassemblyjs/ast" "1.8.3"
+ "@webassemblyjs/floating-point-hex-parser" "1.8.3"
+ "@webassemblyjs/helper-api-error" "1.8.3"
+ "@webassemblyjs/helper-code-frame" "1.8.3"
+ "@webassemblyjs/helper-fsm" "1.8.3"
+ "@xtuc/long" "4.2.2"
+
+"@webassemblyjs/wast-printer@1.8.3":
+ version "1.8.3"
+ resolved "https://registry.yarnpkg.com/@webassemblyjs/wast-printer/-/wast-printer-1.8.3.tgz#b1177780b266b1305f2eeba87c4d6aa732352060"
+ dependencies:
+ "@webassemblyjs/ast" "1.8.3"
+ "@webassemblyjs/wast-parser" "1.8.3"
+ "@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"
+
+"@xtuc/long@4.2.2":
+ version "4.2.2"
+ resolved "https://registry.yarnpkg.com/@xtuc/long/-/long-4.2.2.tgz#d291c6a4e97989b5c61d9acf396ae4fe133a718d"
+
+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:
+ version "1.3.5"
+ resolved "https://registry.yarnpkg.com/accepts/-/accepts-1.3.5.tgz#eb777df6011723a3b14e8a72c0805c8e86746bd2"
+ dependencies:
+ mime-types "~2.1.18"
+ negotiator "0.6.1"
+
+acorn-dynamic-import@^4.0.0:
+ version "4.0.0"
+ resolved "https://registry.yarnpkg.com/acorn-dynamic-import/-/acorn-dynamic-import-4.0.0.tgz#482210140582a36b83c3e342e1cfebcaa9240948"
+
+acorn-jsx@^5.0.0:
+ version "5.0.1"
+ resolved "https://registry.yarnpkg.com/acorn-jsx/-/acorn-jsx-5.0.1.tgz#32a064fd925429216a09b141102bfdd185fae40e"
+
+acorn@^5.7.3:
+ version "5.7.3"
+ resolved "https://registry.yarnpkg.com/acorn/-/acorn-5.7.3.tgz#67aa231bf8812974b85235a96771eb6bd07ea279"
+
+acorn@^6.0.5, acorn@^6.0.7:
+ version "6.1.0"
+ resolved "https://registry.yarnpkg.com/acorn/-/acorn-6.1.0.tgz#b0a3be31752c97a0f7013c5f4903b71a05db6818"
+
+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"
+
+agent-base@^4.1.0:
+ version "4.2.1"
+ resolved "https://registry.yarnpkg.com/agent-base/-/agent-base-4.2.1.tgz#d89e5999f797875674c07d87f260fc41e83e8ca9"
+ dependencies:
+ es6-promisify "^5.0.0"
+
+airbnb-prop-types@^2.10.0, airbnb-prop-types@^2.13.2, airbnb-prop-types@^2.8.1:
+ version "2.14.0"
+ resolved "https://registry.npmjs.org/airbnb-prop-types/-/airbnb-prop-types-2.14.0.tgz#3d45cb1459f4ce78fdf1240563d1aa2315391168"
+ integrity sha512-Yb09vUkr3KP9r9NqfRuYtDYZG76wt8mhTUi2Vfzsghk+qkg01/gOc9NU8n63ZcMCLzpAdMEXyKjCHlxV62yN1A==
+ dependencies:
+ array.prototype.find "^2.1.0"
+ function.prototype.name "^1.1.1"
+ has "^1.0.3"
+ is-regex "^1.0.4"
+ object-is "^1.0.1"
+ object.assign "^4.1.0"
+ object.entries "^1.1.0"
+ prop-types "^15.7.2"
+ prop-types-exact "^1.2.0"
+ react-is "^16.8.6"
+
+ajv-errors@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/ajv-errors/-/ajv-errors-1.0.0.tgz#ecf021fa108fd17dfb5e6b383f2dd233e31ffc59"
+
+ajv-keywords@^3.1.0:
+ version "3.2.0"
+ resolved "https://registry.yarnpkg.com/ajv-keywords/-/ajv-keywords-3.2.0.tgz#e86b819c602cf8821ad637413698f1dec021847a"
+
+ajv@^5.1.0:
+ version "5.5.2"
+ resolved "https://registry.yarnpkg.com/ajv/-/ajv-5.5.2.tgz#73b5eeca3fab653e3d3f9422b341ad42205dc965"
+ dependencies:
+ co "^4.6.0"
+ fast-deep-equal "^1.0.0"
+ fast-json-stable-stringify "^2.0.0"
+ json-schema-traverse "^0.3.0"
+
+ajv@^6.1.0:
+ version "6.5.3"
+ resolved "https://registry.yarnpkg.com/ajv/-/ajv-6.5.3.tgz#71a569d189ecf4f4f321224fecb166f071dd90f9"
+ 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.9.1:
+ version "6.9.2"
+ resolved "https://registry.yarnpkg.com/ajv/-/ajv-6.9.2.tgz#4927adb83e7f48e5a32b45729744c71ec39c9c7b"
+ 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"
+
+alphanum-sort@^1.0.0:
+ version "1.0.2"
+ resolved "https://registry.yarnpkg.com/alphanum-sort/-/alphanum-sort-1.0.2.tgz#97a1119649b211ad33691d9f9f486a8ec9fbe0a3"
+
+ansi-colors@^3.0.0:
+ version "3.0.5"
+ resolved "https://registry.yarnpkg.com/ansi-colors/-/ansi-colors-3.0.5.tgz#cb9dc64993b64fd6945485f797fc3853137d9a7b"
+
+ansi-escapes@^3.2.0:
+ version "3.2.0"
+ resolved "https://registry.yarnpkg.com/ansi-escapes/-/ansi-escapes-3.2.0.tgz#8780b98ff9dbf5638152d1f1fe5c1d7b4442976b"
+
+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.0.0:
+ version "4.0.0"
+ resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-4.0.0.tgz#70de791edf021404c3fd615aa89118ae0432e5a9"
+
+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"
+
+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"
+
+apollo-cache-inmemory@1.4.3:
+ version "1.4.3"
+ resolved "https://registry.yarnpkg.com/apollo-cache-inmemory/-/apollo-cache-inmemory-1.4.3.tgz#aded4fb8b3de9e2fb2573a6c03591b07ef98ed36"
+ dependencies:
+ apollo-cache "^1.1.26"
+ apollo-utilities "^1.1.3"
+ optimism "^0.6.9"
+ tslib "^1.9.3"
+
+apollo-cache-persist@^0.1.1:
+ version "0.1.1"
+ resolved "https://registry.yarnpkg.com/apollo-cache-persist/-/apollo-cache-persist-0.1.1.tgz#e6cfe1983b998982a679aaf05241d3ed395edb1e"
+
+apollo-cache@1.1.26, apollo-cache@^1.1.26:
+ version "1.1.26"
+ resolved "https://registry.yarnpkg.com/apollo-cache/-/apollo-cache-1.1.26.tgz#5afe023270effbc2063d90f51d8e56bce274ab37"
+ dependencies:
+ apollo-utilities "^1.1.3"
+ tslib "^1.9.3"
+
+apollo-client@^2.4.13:
+ version "2.4.13"
+ resolved "https://registry.yarnpkg.com/apollo-client/-/apollo-client-2.4.13.tgz#09829fcbd68e069de9840d0a10764d7c6a3d0787"
+ dependencies:
+ "@types/zen-observable" "^0.8.0"
+ apollo-cache "1.1.26"
+ apollo-link "^1.0.0"
+ apollo-link-dedup "^1.0.0"
+ apollo-utilities "1.1.3"
+ symbol-observable "^1.0.2"
+ tslib "^1.9.3"
+ zen-observable "^0.8.0"
+
+apollo-link-debounce@^2.1.0:
+ version "2.1.0"
+ resolved "https://registry.yarnpkg.com/apollo-link-debounce/-/apollo-link-debounce-2.1.0.tgz#8bf34988d94ab28d73d23de0cf26777235a6bfd7"
+ integrity sha512-tpAb3558SoZVKL8Y5N1pyrw4ko9eYWGpveKCp1fjfDz8bA4977fyfR4OZdrxwBtTjhYf5wNq2N9YTtKaq3OZIg==
+ dependencies:
+ apollo-link "^1.2.2"
+ zen-observable-ts "^0.8.9"
+
+apollo-link-dedup@^1.0.0:
+ version "1.0.10"
+ resolved "https://registry.yarnpkg.com/apollo-link-dedup/-/apollo-link-dedup-1.0.10.tgz#7b94589fe7f969777efd18a129043c78430800ae"
+ dependencies:
+ apollo-link "^1.2.3"
+
+apollo-link-error@1.1.7:
+ version "1.1.7"
+ resolved "https://registry.yarnpkg.com/apollo-link-error/-/apollo-link-error-1.1.7.tgz#6233a339d732def831af2dd417065b2ffd9feb5c"
+ dependencies:
+ apollo-link "^1.2.8"
+ apollo-link-http-common "^0.2.10"
+
+apollo-link-http-common@^0.2.10:
+ version "0.2.10"
+ resolved "https://registry.yarnpkg.com/apollo-link-http-common/-/apollo-link-http-common-0.2.10.tgz#b5bbf502ff40a81cc00281ba3b8543b7ad866dfe"
+ dependencies:
+ apollo-link "^1.2.8"
+
+apollo-link-http-common@^0.2.8:
+ version "0.2.8"
+ resolved "https://registry.yarnpkg.com/apollo-link-http-common/-/apollo-link-http-common-0.2.8.tgz#c6deedfc2739db8b11013c3c2d2ccd657152941f"
+ dependencies:
+ apollo-link "^1.2.6"
+
+apollo-link-state@^0.4.1:
+ version "0.4.2"
+ resolved "https://registry.yarnpkg.com/apollo-link-state/-/apollo-link-state-0.4.2.tgz#ac00e9be9b0ca89eae0be6ba31fe904b80bbe2e8"
+ dependencies:
+ apollo-utilities "^1.0.8"
+ graphql-anywhere "^4.1.0-alpha.0"
+
+apollo-link@^1.0.0, apollo-link@^1.2.3, apollo-link@^1.2.6:
+ version "1.2.6"
+ resolved "https://registry.yarnpkg.com/apollo-link/-/apollo-link-1.2.6.tgz#d9b5676d79c01eb4e424b95c7171697f6ad2b8da"
+ dependencies:
+ apollo-utilities "^1.0.0"
+ zen-observable-ts "^0.8.13"
+
+apollo-link@^1.2.2:
+ version "1.2.11"
+ resolved "https://registry.yarnpkg.com/apollo-link/-/apollo-link-1.2.11.tgz#493293b747ad3237114ccd22e9f559e5e24a194d"
+ integrity sha512-PQvRCg13VduLy3X/0L79M6uOpTh5iHdxnxYuo8yL7sJlWybKRJwsv4IcRBJpMFbChOOaHY7Og9wgPo6DLKDKDA==
+ dependencies:
+ apollo-utilities "^1.2.1"
+ ts-invariant "^0.3.2"
+ tslib "^1.9.3"
+ zen-observable-ts "^0.8.18"
+
+apollo-link@^1.2.8:
+ version "1.2.8"
+ resolved "https://registry.yarnpkg.com/apollo-link/-/apollo-link-1.2.8.tgz#0f252adefd5047ac1a9f35ba9439d216587dcd84"
+ dependencies:
+ zen-observable-ts "^0.8.15"
+
+apollo-upload-client@^10.0.0:
+ version "10.0.0"
+ resolved "https://registry.yarnpkg.com/apollo-upload-client/-/apollo-upload-client-10.0.0.tgz#6cc3d0ea2aef40bc237b655f5042809cacee1859"
+ dependencies:
+ apollo-link "^1.2.6"
+ apollo-link-http-common "^0.2.8"
+ extract-files "^5.0.0"
+
+apollo-utilities@1.1.3, apollo-utilities@^1.1.3:
+ version "1.1.3"
+ resolved "https://registry.yarnpkg.com/apollo-utilities/-/apollo-utilities-1.1.3.tgz#a8883c0392f6b46eac0d366204ebf34be9307c87"
+ dependencies:
+ fast-json-stable-stringify "^2.0.0"
+ tslib "^1.9.3"
+
+apollo-utilities@^1.0.0, apollo-utilities@^1.0.27, apollo-utilities@^1.0.8:
+ version "1.0.27"
+ resolved "https://registry.yarnpkg.com/apollo-utilities/-/apollo-utilities-1.0.27.tgz#77c550f9086552376eca3a48e234a1466b5b057e"
+ dependencies:
+ fast-json-stable-stringify "^2.0.0"
+
+apollo-utilities@^1.2.1:
+ version "1.2.1"
+ resolved "https://registry.yarnpkg.com/apollo-utilities/-/apollo-utilities-1.2.1.tgz#1c3a1ebf5607d7c8efe7636daaf58e7463b41b3c"
+ integrity sha512-Zv8Udp9XTSFiN8oyXOjf6PMHepD4yxxReLsl6dPUy5Ths7jti3nmlBzZUOxuTWRwZn0MoclqL7RQ5UEJN8MAxg==
+ dependencies:
+ fast-json-stable-stringify "^2.0.0"
+ ts-invariant "^0.2.1"
+ tslib "^1.9.3"
+
+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.4"
+ resolved "https://registry.yarnpkg.com/are-we-there-yet/-/are-we-there-yet-1.1.4.tgz#bb5dca382bb94f05e15194373d16fd3ba1ca110d"
+ 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@^3.0.0:
+ version "3.0.0"
+ resolved "https://registry.yarnpkg.com/aria-query/-/aria-query-3.0.0.tgz#65b3fcc1ca1155a8c9ae64d6eee297f15d5133cc"
+ dependencies:
+ ast-types-flow "0.0.7"
+ commander "^2.11.0"
+
+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@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/array-find/-/array-find-1.0.0.tgz#6c8e286d11ed768327f8e62ecee87353ca3e78b8"
+
+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.1"
+ resolved "https://registry.yarnpkg.com/array-flatten/-/array-flatten-2.1.1.tgz#426bb9da84090c1838d812c8150af20a8331e296"
+
+array-includes@^3.0.3:
+ version "3.0.3"
+ resolved "https://registry.yarnpkg.com/array-includes/-/array-includes-3.0.3.tgz#184b48f62d92d7452bb31b323165c7f8bd02266d"
+ dependencies:
+ define-properties "^1.1.2"
+ es-abstract "^1.7.0"
+
+array-union@^1.0.1, array-union@^1.0.2:
+ 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.find@^2.1.0:
+ version "2.1.0"
+ resolved "https://registry.npmjs.org/array.prototype.find/-/array.prototype.find-2.1.0.tgz#630f2eaf70a39e608ac3573e45cf8ccd0ede9ad7"
+ integrity sha512-Wn41+K1yuO5p7wRZDl7890c3xvv5UBrfVXTVIe28rSQb6LS0fZMDrQB6PAcxQFRFy6vJTLDc3A2+3CjQdzVKRg==
+ dependencies:
+ define-properties "^1.1.3"
+ es-abstract "^1.13.0"
+
+array.prototype.flat@^1.2.1:
+ version "1.2.1"
+ resolved "https://registry.npmjs.org/array.prototype.flat/-/array.prototype.flat-1.2.1.tgz#812db8f02cad24d3fab65dd67eabe3b8903494a4"
+ integrity sha512-rVqIs330nLJvfC7JqYvEWwqVr5QjYF1ib02i3YJtR/fICO6527Tjpc/e4Mvmxh3GIePPreRXMdaGyC99YphWEw==
+ dependencies:
+ define-properties "^1.1.2"
+ es-abstract "^1.10.0"
+ function-bind "^1.1.1"
+
+arrify@^1.0.1:
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/arrify/-/arrify-1.0.1.tgz#898508da2226f380df904728456849c1501a4b0d"
+
+asap@~2.0.3:
+ version "2.0.6"
+ resolved "https://registry.yarnpkg.com/asap/-/asap-2.0.6.tgz#e50347611d7e690943208bbdafebcbc2fb866d46"
+
+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.3"
+ resolved "https://registry.yarnpkg.com/asn1/-/asn1-0.2.3.tgz#dac8787713c9966849fc8180777ebe9c1ddf3b86"
+
+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:
+ version "1.4.1"
+ resolved "https://registry.yarnpkg.com/assert/-/assert-1.4.1.tgz#99912d591836b5a6f5b345c0f07eefc08fc65d91"
+ dependencies:
+ 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, 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"
+
+astral-regex@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/astral-regex/-/astral-regex-1.0.0.tgz#6c8c3fb827dd43ee3918f27b82782ab7658a6fd9"
+
+async-each@^1.0.0:
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/async-each/-/async-each-1.0.1.tgz#19d386a1d9edc6e7c1c85d388aedbcc56d33602d"
+
+async-limiter@~1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/async-limiter/-/async-limiter-1.0.0.tgz#78faed8c3d074ab81f22b4e985d79e8738f720f8"
+
+async@^1.5.0, async@^1.5.2:
+ version "1.5.2"
+ resolved "https://registry.yarnpkg.com/async/-/async-1.5.2.tgz#ec6a61ae56480c0c3cb241c95618e20892f9672a"
+
+asynckit@^0.4.0:
+ version "0.4.0"
+ resolved "https://registry.yarnpkg.com/asynckit/-/asynckit-0.4.0.tgz#c79ed97f7f34cb8f2ba1bc9790bcc366474b4b79"
+
+atob@^2.0.0:
+ version "2.1.1"
+ resolved "https://registry.yarnpkg.com/atob/-/atob-2.1.1.tgz#ae2d5a729477f289d60dd7f96a6314a22dd6c22a"
+
+attr-accept@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/attr-accept/-/attr-accept-2.0.0.tgz#8422fef5ee4a511c207796c888227ab5de03306f"
+ integrity sha512-I9SDP4Wvh2ItYYoafEg8hFpsBe96pfQ+eabceShXt3sw2fbIP96+Aoj9zZE0vkZNAkXXzHJATVRuWz+h9FxJxQ==
+
+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.6.0:
+ version "1.7.0"
+ resolved "https://registry.yarnpkg.com/aws4/-/aws4-1.7.0.tgz#d4d0e9b9dbfca77bf08eeb0a8a471550fe39e289"
+
+axobject-query@^2.0.2:
+ version "2.0.2"
+ resolved "https://registry.yarnpkg.com/axobject-query/-/axobject-query-2.0.2.tgz#ea187abe5b9002b377f925d8bf7d1c561adf38f9"
+ dependencies:
+ ast-types-flow "0.0.7"
+
+babel-code-frame@^6.26.0:
+ version "6.26.0"
+ resolved "https://registry.yarnpkg.com/babel-code-frame/-/babel-code-frame-6.26.0.tgz#63fd43f7dc1e3bb7ce35947db8fe369a3f58c74b"
+ dependencies:
+ chalk "^1.1.3"
+ esutils "^2.0.2"
+ js-tokens "^3.0.2"
+
+babel-core@^6.26.0:
+ version "6.26.3"
+ resolved "https://registry.yarnpkg.com/babel-core/-/babel-core-6.26.3.tgz#b2e2f09e342d0f0c88e2f02e067794125e75c207"
+ dependencies:
+ babel-code-frame "^6.26.0"
+ babel-generator "^6.26.0"
+ babel-helpers "^6.24.1"
+ babel-messages "^6.23.0"
+ babel-register "^6.26.0"
+ babel-runtime "^6.26.0"
+ babel-template "^6.26.0"
+ babel-traverse "^6.26.0"
+ babel-types "^6.26.0"
+ babylon "^6.18.0"
+ convert-source-map "^1.5.1"
+ debug "^2.6.9"
+ json5 "^0.5.1"
+ lodash "^4.17.4"
+ minimatch "^3.0.4"
+ path-is-absolute "^1.0.1"
+ private "^0.1.8"
+ slash "^1.0.0"
+ source-map "^0.5.7"
+
+babel-eslint@^8.2.6:
+ version "8.2.6"
+ resolved "https://registry.yarnpkg.com/babel-eslint/-/babel-eslint-8.2.6.tgz#6270d0c73205628067c0f7ae1693a9e797acefd9"
+ dependencies:
+ "@babel/code-frame" "7.0.0-beta.44"
+ "@babel/traverse" "7.0.0-beta.44"
+ "@babel/types" "7.0.0-beta.44"
+ babylon "7.0.0-beta.44"
+ eslint-scope "3.7.1"
+ eslint-visitor-keys "^1.0.0"
+
+babel-generator@^6.26.0:
+ version "6.26.1"
+ resolved "https://registry.yarnpkg.com/babel-generator/-/babel-generator-6.26.1.tgz#1844408d3b8f0d35a404ea7ac180f087a601bd90"
+ dependencies:
+ babel-messages "^6.23.0"
+ babel-runtime "^6.26.0"
+ babel-types "^6.26.0"
+ detect-indent "^4.0.0"
+ jsesc "^1.3.0"
+ lodash "^4.17.4"
+ source-map "^0.5.7"
+ trim-right "^1.0.1"
+
+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-helper-call-delegate@^6.24.1:
+ version "6.24.1"
+ resolved "https://registry.yarnpkg.com/babel-helper-call-delegate/-/babel-helper-call-delegate-6.24.1.tgz#ece6aacddc76e41c3461f88bfc575bd0daa2df8d"
+ dependencies:
+ babel-helper-hoist-variables "^6.24.1"
+ babel-runtime "^6.22.0"
+ babel-traverse "^6.24.1"
+ babel-types "^6.24.1"
+
+babel-helper-define-map@^6.24.1:
+ version "6.26.0"
+ resolved "https://registry.yarnpkg.com/babel-helper-define-map/-/babel-helper-define-map-6.26.0.tgz#a5f56dab41a25f97ecb498c7ebaca9819f95be5f"
+ dependencies:
+ babel-helper-function-name "^6.24.1"
+ babel-runtime "^6.26.0"
+ babel-types "^6.26.0"
+ lodash "^4.17.4"
+
+babel-helper-function-name@^6.24.1:
+ version "6.24.1"
+ resolved "https://registry.yarnpkg.com/babel-helper-function-name/-/babel-helper-function-name-6.24.1.tgz#d3475b8c03ed98242a25b48351ab18399d3580a9"
+ dependencies:
+ babel-helper-get-function-arity "^6.24.1"
+ babel-runtime "^6.22.0"
+ babel-template "^6.24.1"
+ babel-traverse "^6.24.1"
+ babel-types "^6.24.1"
+
+babel-helper-get-function-arity@^6.24.1:
+ version "6.24.1"
+ resolved "https://registry.yarnpkg.com/babel-helper-get-function-arity/-/babel-helper-get-function-arity-6.24.1.tgz#8f7782aa93407c41d3aa50908f89b031b1b6853d"
+ dependencies:
+ babel-runtime "^6.22.0"
+ babel-types "^6.24.1"
+
+babel-helper-hoist-variables@^6.24.1:
+ version "6.24.1"
+ resolved "https://registry.yarnpkg.com/babel-helper-hoist-variables/-/babel-helper-hoist-variables-6.24.1.tgz#1ecb27689c9d25513eadbc9914a73f5408be7a76"
+ dependencies:
+ babel-runtime "^6.22.0"
+ babel-types "^6.24.1"
+
+babel-helper-optimise-call-expression@^6.24.1:
+ version "6.24.1"
+ resolved "https://registry.yarnpkg.com/babel-helper-optimise-call-expression/-/babel-helper-optimise-call-expression-6.24.1.tgz#f7a13427ba9f73f8f4fa993c54a97882d1244257"
+ dependencies:
+ babel-runtime "^6.22.0"
+ babel-types "^6.24.1"
+
+babel-helper-regex@^6.24.1:
+ version "6.26.0"
+ resolved "https://registry.yarnpkg.com/babel-helper-regex/-/babel-helper-regex-6.26.0.tgz#325c59f902f82f24b74faceed0363954f6495e72"
+ dependencies:
+ babel-runtime "^6.26.0"
+ babel-types "^6.26.0"
+ lodash "^4.17.4"
+
+babel-helper-replace-supers@^6.24.1:
+ version "6.24.1"
+ resolved "https://registry.yarnpkg.com/babel-helper-replace-supers/-/babel-helper-replace-supers-6.24.1.tgz#bf6dbfe43938d17369a213ca8a8bf74b6a90ab1a"
+ dependencies:
+ babel-helper-optimise-call-expression "^6.24.1"
+ babel-messages "^6.23.0"
+ babel-runtime "^6.22.0"
+ babel-template "^6.24.1"
+ babel-traverse "^6.24.1"
+ babel-types "^6.24.1"
+
+babel-helpers@^6.24.1:
+ version "6.24.1"
+ resolved "https://registry.yarnpkg.com/babel-helpers/-/babel-helpers-6.24.1.tgz#3471de9caec388e5c850e597e58a26ddf37602b2"
+ dependencies:
+ babel-runtime "^6.22.0"
+ babel-template "^6.24.1"
+
+babel-loader@^7.1.5:
+ version "7.1.5"
+ resolved "https://registry.yarnpkg.com/babel-loader/-/babel-loader-7.1.5.tgz#e3ee0cd7394aa557e013b02d3e492bfd07aa6d68"
+ dependencies:
+ find-cache-dir "^1.0.0"
+ loader-utils "^1.0.2"
+ mkdirp "^0.5.1"
+
+babel-messages@^6.23.0:
+ version "6.23.0"
+ resolved "https://registry.yarnpkg.com/babel-messages/-/babel-messages-6.23.0.tgz#f3cdf4703858035b2a2951c6ec5edf6c62f2630e"
+ dependencies:
+ babel-runtime "^6.22.0"
+
+babel-plugin-check-es2015-constants@^6.22.0:
+ version "6.22.0"
+ resolved "https://registry.yarnpkg.com/babel-plugin-check-es2015-constants/-/babel-plugin-check-es2015-constants-6.22.0.tgz#35157b101426fd2ffd3da3f75c7d1e91835bbf8a"
+ dependencies:
+ babel-runtime "^6.22.0"
+
+babel-plugin-lodash@3.3.4:
+ version "3.3.4"
+ resolved "https://registry.yarnpkg.com/babel-plugin-lodash/-/babel-plugin-lodash-3.3.4.tgz#4f6844358a1340baed182adbeffa8df9967bc196"
+ dependencies:
+ "@babel/helper-module-imports" "^7.0.0-beta.49"
+ "@babel/types" "^7.0.0-beta.49"
+ glob "^7.1.1"
+ lodash "^4.17.10"
+ require-package-name "^2.0.1"
+
+babel-plugin-syntax-class-properties@^6.8.0:
+ version "6.13.0"
+ resolved "https://registry.yarnpkg.com/babel-plugin-syntax-class-properties/-/babel-plugin-syntax-class-properties-6.13.0.tgz#d7eb23b79a317f8543962c505b827c7d6cac27de"
+
+babel-plugin-syntax-dynamic-import@^6.18.0:
+ version "6.18.0"
+ resolved "https://registry.yarnpkg.com/babel-plugin-syntax-dynamic-import/-/babel-plugin-syntax-dynamic-import-6.18.0.tgz#8d6a26229c83745a9982a441051572caa179b1da"
+
+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-syntax-object-rest-spread@^6.8.0:
+ version "6.13.0"
+ resolved "https://registry.yarnpkg.com/babel-plugin-syntax-object-rest-spread/-/babel-plugin-syntax-object-rest-spread-6.13.0.tgz#fd6536f2bce13836ffa3a5458c4903a597bb3bf5"
+
+babel-plugin-transform-class-properties@^6.24.1:
+ version "6.24.1"
+ resolved "https://registry.yarnpkg.com/babel-plugin-transform-class-properties/-/babel-plugin-transform-class-properties-6.24.1.tgz#6a79763ea61d33d36f37b611aa9def81a81b46ac"
+ dependencies:
+ babel-helper-function-name "^6.24.1"
+ babel-plugin-syntax-class-properties "^6.8.0"
+ babel-runtime "^6.22.0"
+ babel-template "^6.24.1"
+
+babel-plugin-transform-es2015-arrow-functions@^6.22.0:
+ version "6.22.0"
+ resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-arrow-functions/-/babel-plugin-transform-es2015-arrow-functions-6.22.0.tgz#452692cb711d5f79dc7f85e440ce41b9f244d221"
+ dependencies:
+ babel-runtime "^6.22.0"
+
+babel-plugin-transform-es2015-block-scoped-functions@^6.22.0:
+ version "6.22.0"
+ resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-block-scoped-functions/-/babel-plugin-transform-es2015-block-scoped-functions-6.22.0.tgz#bbc51b49f964d70cb8d8e0b94e820246ce3a6141"
+ dependencies:
+ babel-runtime "^6.22.0"
+
+babel-plugin-transform-es2015-block-scoping@^6.24.1:
+ version "6.26.0"
+ resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-block-scoping/-/babel-plugin-transform-es2015-block-scoping-6.26.0.tgz#d70f5299c1308d05c12f463813b0a09e73b1895f"
+ dependencies:
+ babel-runtime "^6.26.0"
+ babel-template "^6.26.0"
+ babel-traverse "^6.26.0"
+ babel-types "^6.26.0"
+ lodash "^4.17.4"
+
+babel-plugin-transform-es2015-classes@^6.24.1:
+ version "6.24.1"
+ resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-classes/-/babel-plugin-transform-es2015-classes-6.24.1.tgz#5a4c58a50c9c9461e564b4b2a3bfabc97a2584db"
+ dependencies:
+ babel-helper-define-map "^6.24.1"
+ babel-helper-function-name "^6.24.1"
+ babel-helper-optimise-call-expression "^6.24.1"
+ babel-helper-replace-supers "^6.24.1"
+ babel-messages "^6.23.0"
+ babel-runtime "^6.22.0"
+ babel-template "^6.24.1"
+ babel-traverse "^6.24.1"
+ babel-types "^6.24.1"
+
+babel-plugin-transform-es2015-computed-properties@^6.24.1:
+ version "6.24.1"
+ resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-computed-properties/-/babel-plugin-transform-es2015-computed-properties-6.24.1.tgz#6fe2a8d16895d5634f4cd999b6d3480a308159b3"
+ dependencies:
+ babel-runtime "^6.22.0"
+ babel-template "^6.24.1"
+
+babel-plugin-transform-es2015-destructuring@^6.22.0:
+ version "6.23.0"
+ resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-destructuring/-/babel-plugin-transform-es2015-destructuring-6.23.0.tgz#997bb1f1ab967f682d2b0876fe358d60e765c56d"
+ dependencies:
+ babel-runtime "^6.22.0"
+
+babel-plugin-transform-es2015-duplicate-keys@^6.24.1:
+ version "6.24.1"
+ resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-duplicate-keys/-/babel-plugin-transform-es2015-duplicate-keys-6.24.1.tgz#73eb3d310ca969e3ef9ec91c53741a6f1576423e"
+ dependencies:
+ babel-runtime "^6.22.0"
+ babel-types "^6.24.1"
+
+babel-plugin-transform-es2015-for-of@^6.22.0:
+ version "6.23.0"
+ resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-for-of/-/babel-plugin-transform-es2015-for-of-6.23.0.tgz#f47c95b2b613df1d3ecc2fdb7573623c75248691"
+ dependencies:
+ babel-runtime "^6.22.0"
+
+babel-plugin-transform-es2015-function-name@^6.24.1:
+ version "6.24.1"
+ resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-function-name/-/babel-plugin-transform-es2015-function-name-6.24.1.tgz#834c89853bc36b1af0f3a4c5dbaa94fd8eacaa8b"
+ dependencies:
+ babel-helper-function-name "^6.24.1"
+ babel-runtime "^6.22.0"
+ babel-types "^6.24.1"
+
+babel-plugin-transform-es2015-literals@^6.22.0:
+ version "6.22.0"
+ resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-literals/-/babel-plugin-transform-es2015-literals-6.22.0.tgz#4f54a02d6cd66cf915280019a31d31925377ca2e"
+ dependencies:
+ babel-runtime "^6.22.0"
+
+babel-plugin-transform-es2015-modules-amd@^6.24.1:
+ version "6.24.1"
+ resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-modules-amd/-/babel-plugin-transform-es2015-modules-amd-6.24.1.tgz#3b3e54017239842d6d19c3011c4bd2f00a00d154"
+ dependencies:
+ babel-plugin-transform-es2015-modules-commonjs "^6.24.1"
+ babel-runtime "^6.22.0"
+ babel-template "^6.24.1"
+
+babel-plugin-transform-es2015-modules-commonjs@^6.24.1:
+ version "6.26.2"
+ resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-modules-commonjs/-/babel-plugin-transform-es2015-modules-commonjs-6.26.2.tgz#58a793863a9e7ca870bdc5a881117ffac27db6f3"
+ dependencies:
+ babel-plugin-transform-strict-mode "^6.24.1"
+ babel-runtime "^6.26.0"
+ babel-template "^6.26.0"
+ babel-types "^6.26.0"
+
+babel-plugin-transform-es2015-modules-systemjs@^6.24.1:
+ version "6.24.1"
+ resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-modules-systemjs/-/babel-plugin-transform-es2015-modules-systemjs-6.24.1.tgz#ff89a142b9119a906195f5f106ecf305d9407d23"
+ dependencies:
+ babel-helper-hoist-variables "^6.24.1"
+ babel-runtime "^6.22.0"
+ babel-template "^6.24.1"
+
+babel-plugin-transform-es2015-modules-umd@^6.24.1:
+ version "6.24.1"
+ resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-modules-umd/-/babel-plugin-transform-es2015-modules-umd-6.24.1.tgz#ac997e6285cd18ed6176adb607d602344ad38468"
+ dependencies:
+ babel-plugin-transform-es2015-modules-amd "^6.24.1"
+ babel-runtime "^6.22.0"
+ babel-template "^6.24.1"
+
+babel-plugin-transform-es2015-object-super@^6.24.1:
+ version "6.24.1"
+ resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-object-super/-/babel-plugin-transform-es2015-object-super-6.24.1.tgz#24cef69ae21cb83a7f8603dad021f572eb278f8d"
+ dependencies:
+ babel-helper-replace-supers "^6.24.1"
+ babel-runtime "^6.22.0"
+
+babel-plugin-transform-es2015-parameters@^6.24.1:
+ version "6.24.1"
+ resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-parameters/-/babel-plugin-transform-es2015-parameters-6.24.1.tgz#57ac351ab49caf14a97cd13b09f66fdf0a625f2b"
+ dependencies:
+ babel-helper-call-delegate "^6.24.1"
+ babel-helper-get-function-arity "^6.24.1"
+ babel-runtime "^6.22.0"
+ babel-template "^6.24.1"
+ babel-traverse "^6.24.1"
+ babel-types "^6.24.1"
+
+babel-plugin-transform-es2015-shorthand-properties@^6.24.1:
+ version "6.24.1"
+ resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-shorthand-properties/-/babel-plugin-transform-es2015-shorthand-properties-6.24.1.tgz#24f875d6721c87661bbd99a4622e51f14de38aa0"
+ dependencies:
+ babel-runtime "^6.22.0"
+ babel-types "^6.24.1"
+
+babel-plugin-transform-es2015-spread@^6.22.0:
+ version "6.22.0"
+ resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-spread/-/babel-plugin-transform-es2015-spread-6.22.0.tgz#d6d68a99f89aedc4536c81a542e8dd9f1746f8d1"
+ dependencies:
+ babel-runtime "^6.22.0"
+
+babel-plugin-transform-es2015-sticky-regex@^6.24.1:
+ version "6.24.1"
+ resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-sticky-regex/-/babel-plugin-transform-es2015-sticky-regex-6.24.1.tgz#00c1cdb1aca71112cdf0cf6126c2ed6b457ccdbc"
+ dependencies:
+ babel-helper-regex "^6.24.1"
+ babel-runtime "^6.22.0"
+ babel-types "^6.24.1"
+
+babel-plugin-transform-es2015-template-literals@^6.22.0:
+ version "6.22.0"
+ resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-template-literals/-/babel-plugin-transform-es2015-template-literals-6.22.0.tgz#a84b3450f7e9f8f1f6839d6d687da84bb1236d8d"
+ dependencies:
+ babel-runtime "^6.22.0"
+
+babel-plugin-transform-es2015-typeof-symbol@^6.22.0:
+ version "6.23.0"
+ resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-typeof-symbol/-/babel-plugin-transform-es2015-typeof-symbol-6.23.0.tgz#dec09f1cddff94b52ac73d505c84df59dcceb372"
+ dependencies:
+ babel-runtime "^6.22.0"
+
+babel-plugin-transform-es2015-unicode-regex@^6.24.1:
+ version "6.24.1"
+ resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-unicode-regex/-/babel-plugin-transform-es2015-unicode-regex-6.24.1.tgz#d38b12f42ea7323f729387f18a7c5ae1faeb35e9"
+ dependencies:
+ babel-helper-regex "^6.24.1"
+ babel-runtime "^6.22.0"
+ regexpu-core "^2.0.0"
+
+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-object-rest-spread@^6.26.0:
+ version "6.26.0"
+ resolved "https://registry.yarnpkg.com/babel-plugin-transform-object-rest-spread/-/babel-plugin-transform-object-rest-spread-6.26.0.tgz#0f36692d50fef6b7e2d4b3ac1478137a963b7b06"
+ dependencies:
+ babel-plugin-syntax-object-rest-spread "^6.8.0"
+ babel-runtime "^6.26.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-plugin-transform-regenerator@^6.24.1:
+ version "6.26.0"
+ resolved "https://registry.yarnpkg.com/babel-plugin-transform-regenerator/-/babel-plugin-transform-regenerator-6.26.0.tgz#e0703696fbde27f0a3efcacf8b4dca2f7b3a8f2f"
+ dependencies:
+ regenerator-transform "^0.10.0"
+
+babel-plugin-transform-strict-mode@^6.24.1:
+ version "6.24.1"
+ resolved "https://registry.yarnpkg.com/babel-plugin-transform-strict-mode/-/babel-plugin-transform-strict-mode-6.24.1.tgz#d5faf7aa578a65bbe591cf5edae04a0c67020758"
+ dependencies:
+ babel-runtime "^6.22.0"
+ babel-types "^6.24.1"
+
+babel-preset-es2015@^6.24.1:
+ version "6.24.1"
+ resolved "https://registry.yarnpkg.com/babel-preset-es2015/-/babel-preset-es2015-6.24.1.tgz#d44050d6bc2c9feea702aaf38d727a0210538939"
+ dependencies:
+ babel-plugin-check-es2015-constants "^6.22.0"
+ babel-plugin-transform-es2015-arrow-functions "^6.22.0"
+ babel-plugin-transform-es2015-block-scoped-functions "^6.22.0"
+ babel-plugin-transform-es2015-block-scoping "^6.24.1"
+ babel-plugin-transform-es2015-classes "^6.24.1"
+ babel-plugin-transform-es2015-computed-properties "^6.24.1"
+ babel-plugin-transform-es2015-destructuring "^6.22.0"
+ babel-plugin-transform-es2015-duplicate-keys "^6.24.1"
+ babel-plugin-transform-es2015-for-of "^6.22.0"
+ babel-plugin-transform-es2015-function-name "^6.24.1"
+ babel-plugin-transform-es2015-literals "^6.22.0"
+ babel-plugin-transform-es2015-modules-amd "^6.24.1"
+ babel-plugin-transform-es2015-modules-commonjs "^6.24.1"
+ babel-plugin-transform-es2015-modules-systemjs "^6.24.1"
+ babel-plugin-transform-es2015-modules-umd "^6.24.1"
+ babel-plugin-transform-es2015-object-super "^6.24.1"
+ babel-plugin-transform-es2015-parameters "^6.24.1"
+ babel-plugin-transform-es2015-shorthand-properties "^6.24.1"
+ babel-plugin-transform-es2015-spread "^6.22.0"
+ babel-plugin-transform-es2015-sticky-regex "^6.24.1"
+ babel-plugin-transform-es2015-template-literals "^6.22.0"
+ babel-plugin-transform-es2015-typeof-symbol "^6.22.0"
+ babel-plugin-transform-es2015-unicode-regex "^6.24.1"
+ babel-plugin-transform-regenerator "^6.24.1"
+
+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-register@^6.26.0:
+ version "6.26.0"
+ resolved "https://registry.yarnpkg.com/babel-register/-/babel-register-6.26.0.tgz#6ed021173e2fcb486d7acb45c6009a856f647071"
+ dependencies:
+ babel-core "^6.26.0"
+ babel-runtime "^6.26.0"
+ core-js "^2.5.0"
+ home-or-tmp "^2.0.0"
+ lodash "^4.17.4"
+ mkdirp "^0.5.1"
+ source-map-support "^0.4.15"
+
+babel-runtime@6.x, babel-runtime@^6.18.0, babel-runtime@^6.22.0, babel-runtime@^6.26.0, babel-runtime@^6.5.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-template@^6.24.1, babel-template@^6.26.0:
+ version "6.26.0"
+ resolved "https://registry.yarnpkg.com/babel-template/-/babel-template-6.26.0.tgz#de03e2d16396b069f46dd9fff8521fb1a0e35e02"
+ dependencies:
+ babel-runtime "^6.26.0"
+ babel-traverse "^6.26.0"
+ babel-types "^6.26.0"
+ babylon "^6.18.0"
+ lodash "^4.17.4"
+
+babel-traverse@^6.24.1, babel-traverse@^6.26.0:
+ version "6.26.0"
+ resolved "https://registry.yarnpkg.com/babel-traverse/-/babel-traverse-6.26.0.tgz#46a9cbd7edcc62c8e5c064e2d2d8d0f4035766ee"
+ dependencies:
+ babel-code-frame "^6.26.0"
+ babel-messages "^6.23.0"
+ babel-runtime "^6.26.0"
+ babel-types "^6.26.0"
+ babylon "^6.18.0"
+ debug "^2.6.8"
+ globals "^9.18.0"
+ invariant "^2.2.2"
+ lodash "^4.17.4"
+
+babel-types@^6.19.0, babel-types@^6.24.1, 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"
+
+babylon@7.0.0-beta.44:
+ version "7.0.0-beta.44"
+ resolved "https://registry.yarnpkg.com/babylon/-/babylon-7.0.0-beta.44.tgz#89159e15e6e30c5096e22d738d8c0af8a0e8ca1d"
+
+babylon@^6.18.0:
+ version "6.18.0"
+ resolved "https://registry.yarnpkg.com/babylon/-/babylon-6.18.0.tgz#af2f3b88fa6f5c1e4c634d1a0f8eac4f55b395e3"
+
+balanced-match@^0.4.2:
+ version "0.4.2"
+ resolved "https://registry.npmjs.org/balanced-match/-/balanced-match-0.4.2.tgz#cb3f3e3c732dc0f01ee70b403f302e61d7709838"
+ integrity sha1-yz8+PHMtwPAe5wtAPzAuYddwmDg=
+
+balanced-match@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.0.tgz#89b4d199ab2bee49de164ea02b89ce462d71b767"
+
+base16@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/base16/-/base16-1.0.0.tgz#e297f60d7ec1014a7a971a39ebc8a98c0b681e70"
+
+base64-js@^1.0.2:
+ version "1.3.0"
+ resolved "https://registry.yarnpkg.com/base64-js/-/base64-js-1.3.0.tgz#cab1e6118f051095e58b5281aea8c1cd22bfc0e3"
+
+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@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.1"
+ resolved "https://registry.yarnpkg.com/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.1.tgz#63bc5dcb61331b92bc05fd528953c33462a06f8d"
+ dependencies:
+ tweetnacl "^0.14.3"
+
+bfj@^6.1.1:
+ version "6.1.1"
+ resolved "https://registry.yarnpkg.com/bfj/-/bfj-6.1.1.tgz#05a3b7784fbd72cfa3c22e56002ef99336516c48"
+ dependencies:
+ bluebird "^3.5.1"
+ check-types "^7.3.0"
+ hoopy "^0.1.2"
+ tryer "^1.0.0"
+
+big.js@^3.1.3:
+ version "3.2.0"
+ resolved "https://registry.yarnpkg.com/big.js/-/big.js-3.2.0.tgz#a5fc298b81b9e0dca2e458824784b65c52ba588e"
+
+bignumber.js@^2.1.0:
+ version "2.4.0"
+ resolved "https://registry.yarnpkg.com/bignumber.js/-/bignumber.js-2.4.0.tgz#838a992da9f9d737e0f4b2db0be62bb09dd0c5e8"
+
+bignumber.js@^8.0.1:
+ version "8.1.1"
+ resolved "https://registry.yarnpkg.com/bignumber.js/-/bignumber.js-8.1.1.tgz#4b072ae5aea9c20f6730e4e5d529df1271c4d885"
+ integrity sha512-QD46ppGintwPGuL1KqmwhR0O+N2cZUg8JG/VzwI2e28sM9TqHjQB10lI4QAaMHVbLzwVLLAwEglpKPViWX+5NQ==
+
+binary-extensions@^1.0.0:
+ version "1.11.0"
+ resolved "https://registry.yarnpkg.com/binary-extensions/-/binary-extensions-1.11.0.tgz#46aa1751fb6a2f93ee5e689bb1087d4b14c6c205"
+
+blob-to-buffer@^1.0.2:
+ version "1.2.8"
+ resolved "https://registry.yarnpkg.com/blob-to-buffer/-/blob-to-buffer-1.2.8.tgz#78eeeb332f1280ed0ca6fb2b60693a8c6d36903a"
+
+bluebird@^3.5.1:
+ version "3.5.1"
+ resolved "https://registry.yarnpkg.com/bluebird/-/bluebird-3.5.1.tgz#d9551f9de98f1fcda1e683d17ee91a0602ee2eb9"
+
+bmp-js@0.0.1:
+ version "0.0.1"
+ resolved "https://registry.yarnpkg.com/bmp-js/-/bmp-js-0.0.1.tgz#5ad0147099d13a9f38aa7b99af1d6e78666ed37f"
+
+bmp-js@0.0.3:
+ version "0.0.3"
+ resolved "https://registry.yarnpkg.com/bmp-js/-/bmp-js-0.0.3.tgz#64113e9c7cf1202b376ed607bf30626ebe57b18a"
+
+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.18.2:
+ version "1.18.2"
+ resolved "https://registry.yarnpkg.com/body-parser/-/body-parser-1.18.2.tgz#87678a19d84b47d859b83199bd59bce222b10454"
+ dependencies:
+ bytes "3.0.0"
+ content-type "~1.0.4"
+ debug "2.6.9"
+ depd "~1.1.1"
+ http-errors "~1.6.2"
+ iconv-lite "0.4.19"
+ on-finished "~2.3.0"
+ qs "6.5.1"
+ raw-body "2.3.2"
+ type-is "~1.6.15"
+
+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"
+
+boom@4.x.x:
+ version "4.3.1"
+ resolved "https://registry.yarnpkg.com/boom/-/boom-4.3.1.tgz#4f8a3005cb4a7e3889f749030fd25b96e01d2e31"
+ dependencies:
+ hoek "4.x.x"
+
+boom@5.x.x:
+ version "5.2.0"
+ resolved "https://registry.yarnpkg.com/boom/-/boom-5.2.0.tgz#5dd9da6ee3a5f302077436290cb717d3f4a54e02"
+ dependencies:
+ hoek "4.x.x"
+
+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"
+
+braces@^2.3.0, braces@^2.3.1:
+ 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"
+
+brcast@^2.0.2:
+ version "2.0.2"
+ resolved "https://registry.npmjs.org/brcast/-/brcast-2.0.2.tgz#2db16de44140e418dc37fab10beec0369e78dcef"
+ integrity sha512-Tfn5JSE7hrUlFcOoaLzVvkbgIemIorMIyoMr3TgvszWW7jFt2C9PdeMLtysYD9RU0MmU17b69+XJG1eRY2OBRg==
+
+brcast@^3.0.1:
+ version "3.0.1"
+ resolved "https://registry.yarnpkg.com/brcast/-/brcast-3.0.1.tgz#6256a8349b20de9eed44257a9b24d71493cd48dd"
+
+brorand@^1.0.1:
+ version "1.1.0"
+ resolved "https://registry.yarnpkg.com/brorand/-/brorand-1.1.0.tgz#12c25efe40a45e3c323eb8675a0a0ce57b22371f"
+
+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-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.1"
+ resolved "https://registry.yarnpkg.com/browserify-des/-/browserify-des-1.0.1.tgz#3343124db6d7ad53e26a8826318712bdc8450f9c"
+ dependencies:
+ cipher-base "^1.0.1"
+ des.js "^1.0.0"
+ inherits "^2.0.1"
+
+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:
+ version "0.2.0"
+ resolved "https://registry.yarnpkg.com/browserify-zlib/-/browserify-zlib-0.2.0.tgz#2869459d9aa3be245fe8fe2ca1f46e2e7f54d73f"
+ dependencies:
+ pako "~1.0.5"
+
+browserslist@^4.0.0:
+ version "4.0.1"
+ resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-4.0.1.tgz#61c05ce2a5843c7d96166408bc23d58b5416e818"
+ dependencies:
+ caniuse-lite "^1.0.30000865"
+ electron-to-chromium "^1.3.52"
+ node-releases "^1.0.0-alpha.10"
+
+buffer-alloc-unsafe@^0.1.0:
+ version "0.1.1"
+ resolved "https://registry.yarnpkg.com/buffer-alloc-unsafe/-/buffer-alloc-unsafe-0.1.1.tgz#ffe1f67551dd055737de253337bfe853dfab1a6a"
+
+buffer-alloc@^1.1.0:
+ version "1.1.0"
+ resolved "https://registry.yarnpkg.com/buffer-alloc/-/buffer-alloc-1.1.0.tgz#05514d33bf1656d3540c684f65b1202e90eca303"
+ dependencies:
+ buffer-alloc-unsafe "^0.1.0"
+ buffer-fill "^0.1.0"
+
+buffer-equal@0.0.1:
+ version "0.0.1"
+ resolved "https://registry.yarnpkg.com/buffer-equal/-/buffer-equal-0.0.1.tgz#91bc74b11ea405bc916bc6aa908faafa5b4aac4b"
+
+buffer-fill@^0.1.0:
+ version "0.1.1"
+ resolved "https://registry.yarnpkg.com/buffer-fill/-/buffer-fill-0.1.1.tgz#76d825c4d6e50e06b7a31eb520c04d08cc235071"
+
+buffer-from@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/buffer-from/-/buffer-from-1.0.0.tgz#4cb8832d23612589b0406e9e2956c17f06fdf531"
+
+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"
+
+builtin-modules@^1.0.0:
+ version "1.1.1"
+ resolved "https://registry.yarnpkg.com/builtin-modules/-/builtin-modules-1.1.1.tgz#270f076c5a72c02f5b65a47df94c5fe3a278892f"
+
+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"
+
+cacache@^11.0.2, cacache@^11.2.0:
+ version "11.2.0"
+ resolved "https://registry.yarnpkg.com/cacache/-/cacache-11.2.0.tgz#617bdc0b02844af56310e411c0878941d5739965"
+ dependencies:
+ bluebird "^3.5.1"
+ chownr "^1.0.1"
+ figgy-pudding "^3.1.0"
+ glob "^7.1.2"
+ graceful-fs "^4.1.11"
+ lru-cache "^4.1.3"
+ mississippi "^3.0.0"
+ mkdirp "^0.5.1"
+ move-concurrently "^1.0.1"
+ promise-inflight "^1.0.1"
+ rimraf "^2.6.2"
+ ssri "^6.0.0"
+ unique-filename "^1.1.0"
+ y18n "^4.0.0"
+
+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"
+
+call-me-maybe@^1.0.1:
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/call-me-maybe/-/call-me-maybe-1.0.1.tgz#26d208ea89e37b5cbde60250a15f031c16a4d66b"
+
+callsites@^3.0.0:
+ version "3.0.0"
+ resolved "https://registry.yarnpkg.com/callsites/-/callsites-3.0.0.tgz#fb7eb569b72ad7a45812f93fd9430a3e410b3dd3"
+
+camel-case@3.0.x:
+ version "3.0.0"
+ resolved "https://registry.yarnpkg.com/camel-case/-/camel-case-3.0.0.tgz#ca3c3688a4e9cf3a4cda777dc4dcbc713249cf73"
+ dependencies:
+ no-case "^2.2.0"
+ upper-case "^1.1.1"
+
+camelcase@^2.0.1:
+ version "2.1.1"
+ resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-2.1.1.tgz#7c1d16d679a1bbe59ca02cacecfb011e201f5a1f"
+
+camelcase@^4.1.0:
+ version "4.1.0"
+ resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-4.1.0.tgz#d545635be1e33c542649c69173e5de6acfae34dd"
+
+camelcase@^5.0.0:
+ version "5.0.0"
+ resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-5.0.0.tgz#03295527d58bd3cd4aa75363f35b2e8d97be2f42"
+
+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.30000865:
+ version "1.0.30000865"
+ resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30000865.tgz#70026616e8afe6e1442f8bb4e1092987d81a2f25"
+
+caseless@~0.12.0:
+ version "0.12.0"
+ resolved "https://registry.yarnpkg.com/caseless/-/caseless-0.12.0.tgz#1b681c21ff84033c826543090689420d187151dc"
+
+chalk@2.4.2, 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@^1.1.3:
+ 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.0, chalk@^2.1.0, chalk@^2.4.1:
+ version "2.4.1"
+ resolved "https://registry.yarnpkg.com/chalk/-/chalk-2.4.1.tgz#18c49ab16a037b6eb0152cc83e3471338215b66e"
+ dependencies:
+ ansi-styles "^3.2.1"
+ escape-string-regexp "^1.0.5"
+ supports-color "^5.3.0"
+
+chardet@^0.7.0:
+ version "0.7.0"
+ resolved "https://registry.yarnpkg.com/chardet/-/chardet-0.7.0.tgz#90094849f0937f2eedc2425d0d28a9e5f0cbad9e"
+
+check-types@^7.3.0:
+ version "7.3.0"
+ resolved "https://registry.yarnpkg.com/check-types/-/check-types-7.3.0.tgz#468f571a4435c24248f5fd0cb0e8d87c3c341e7d"
+
+cheerio@^0.19.0:
+ version "0.19.0"
+ resolved "https://registry.yarnpkg.com/cheerio/-/cheerio-0.19.0.tgz#772e7015f2ee29965096d71ea4175b75ab354925"
+ dependencies:
+ css-select "~1.0.0"
+ dom-serializer "~0.1.0"
+ entities "~1.1.1"
+ htmlparser2 "~3.8.1"
+ lodash "^3.2.0"
+
+chokidar@^2.0.0, chokidar@^2.0.2:
+ version "2.0.3"
+ resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-2.0.3.tgz#dcbd4f6cbb2a55b4799ba8a840ac527e5f4b1176"
+ dependencies:
+ anymatch "^2.0.0"
+ async-each "^1.0.0"
+ braces "^2.3.0"
+ glob-parent "^3.1.0"
+ inherits "^2.0.1"
+ is-binary-path "^1.0.0"
+ is-glob "^4.0.0"
+ normalize-path "^2.1.1"
+ path-is-absolute "^1.0.0"
+ readdirp "^2.0.0"
+ upath "^1.0.0"
+ optionalDependencies:
+ fsevents "^1.1.2"
+
+chownr@^1.0.1:
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/chownr/-/chownr-1.0.1.tgz#e2a75042a9551908bebd25b8523d5f9769d79181"
+
+chrome-trace-event@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/chrome-trace-event/-/chrome-trace-event-1.0.0.tgz#45a91bd2c20c9411f0963b5aaeb9a1b95e09cc48"
+ 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.0, 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-css@4.1.x:
+ version "4.1.11"
+ resolved "https://registry.yarnpkg.com/clean-css/-/clean-css-4.1.11.tgz#2ecdf145aba38f54740f26cefd0ff3e03e125d6a"
+ dependencies:
+ source-map "0.5.x"
+
+cli-cursor@^2.1.0:
+ version "2.1.0"
+ resolved "https://registry.yarnpkg.com/cli-cursor/-/cli-cursor-2.1.0.tgz#b35dac376479facc3e94747d41d0d0f5238ffcb5"
+ dependencies:
+ restore-cursor "^2.0.0"
+
+cli-width@^2.0.0:
+ version "2.2.0"
+ resolved "https://registry.yarnpkg.com/cli-width/-/cli-width-2.2.0.tgz#ff19ede8a9a5e579324147b0c11f0fbcbabed639"
+
+cliui@^3.0.3:
+ version "3.2.0"
+ resolved "https://registry.yarnpkg.com/cliui/-/cliui-3.2.0.tgz#120601537a916d29940f934da3b48d585a39213d"
+ dependencies:
+ string-width "^1.0.1"
+ strip-ansi "^3.0.1"
+ wrap-ansi "^2.0.0"
+
+cliui@^4.0.0:
+ version "4.1.0"
+ resolved "https://registry.yarnpkg.com/cliui/-/cliui-4.1.0.tgz#348422dbe82d800b3022eef4f6ac10bf2e4d1b49"
+ dependencies:
+ string-width "^2.1.1"
+ strip-ansi "^4.0.0"
+ wrap-ansi "^2.0.0"
+
+clone-stats@^0.0.1:
+ version "0.0.1"
+ resolved "https://registry.yarnpkg.com/clone-stats/-/clone-stats-0.0.1.tgz#b88f94a82cf38b8791d58046ea4029ad88ca99d1"
+
+clone@^1.0.0, clone@^1.0.2:
+ version "1.0.4"
+ resolved "https://registry.yarnpkg.com/clone/-/clone-1.0.4.tgz#da309cc263df15994c688ca902179ca3c7cd7c7e"
+
+co@^4.6.0:
+ version "4.6.0"
+ resolved "https://registry.yarnpkg.com/co/-/co-4.6.0.tgz#6ea6bdf3d853ae54ccb8e47bfa0bf3f9031fb184"
+
+coa@~2.0.1:
+ version "2.0.1"
+ resolved "https://registry.yarnpkg.com/coa/-/coa-2.0.1.tgz#f3f8b0b15073e35d70263fb1042cb2c023db38af"
+ dependencies:
+ 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"
+
+codemirror@^5.45.0:
+ version "5.45.0"
+ resolved "https://registry.yarnpkg.com/codemirror/-/codemirror-5.45.0.tgz#db5ebbb3bf44028c684053f3954d011efcec27ad"
+ integrity sha512-c19j644usCE8gQaXa0jqn2B/HN9MnB2u6qPIrrhrMkB+QAP42y8G4QnTwuwbVSoUS1jEl7JU9HZMGhCDL0nsAw==
+
+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.1"
+ resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-1.9.1.tgz#c1261107aeb2f294ebffec9ed9ecad529a6097ed"
+ dependencies:
+ color-name "^1.1.1"
+
+color-name@^1.0.0, color-name@^1.1.1:
+ version "1.1.3"
+ resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.3.tgz#a7d0558bd89c42f795dd42328f740831ca53bc25"
+
+color-string@^1.5.2:
+ version "1.5.2"
+ resolved "https://registry.yarnpkg.com/color-string/-/color-string-1.5.2.tgz#26e45814bc3c9a7cbd6751648a41434514a773a9"
+ dependencies:
+ color-name "^1.0.0"
+ simple-swizzle "^0.2.2"
+
+color@^3.0.0:
+ version "3.0.0"
+ resolved "https://registry.yarnpkg.com/color/-/color-3.0.0.tgz#d920b4328d534a3ac8295d68f7bd4ba6c427be9a"
+ dependencies:
+ color-convert "^1.9.1"
+ color-string "^1.5.2"
+
+colors@^1.1.2:
+ version "1.2.4"
+ resolved "https://registry.yarnpkg.com/colors/-/colors-1.2.4.tgz#e0cb41d3e4b20806b3bfc27f4559f01b94bc2f7c"
+
+colors@~1.1.2:
+ version "1.1.2"
+ resolved "https://registry.yarnpkg.com/colors/-/colors-1.1.2.tgz#168a4701756b6a7f51a12ce0c97bfa28c084ed63"
+
+combined-stream@1.0.6, combined-stream@~1.0.5:
+ version "1.0.6"
+ resolved "https://registry.yarnpkg.com/combined-stream/-/combined-stream-1.0.6.tgz#723e7df6e801ac5613113a7e445a9b69cb632818"
+ dependencies:
+ delayed-stream "~1.0.0"
+
+commander@2.15.x, commander@~2.15.0:
+ version "2.15.1"
+ resolved "https://registry.yarnpkg.com/commander/-/commander-2.15.1.tgz#df46e867d0fc2aec66a34662b406a9ccafff5b0f"
+
+commander@2.19.0, commander@^2.11.0, commander@^2.18.0:
+ version "2.19.0"
+ resolved "https://registry.yarnpkg.com/commander/-/commander-2.19.0.tgz#f6198aa84e5b83c46054b94ddedbfed5ee9ff12a"
+
+commander@^2.19.0:
+ version "2.20.0"
+ resolved "https://registry.yarnpkg.com/commander/-/commander-2.20.0.tgz#d58bb2b5c1ee8f87b0d340027e9e94e222c5a422"
+ integrity sha512-7j2y+40w61zy6YC2iRNpUe/NwhNyoXrYpHMrSunaMG64nRnaf96zO/KMQR4OyN/UnE5KLyEBnKHd4aG3rskjpQ==
+
+commander@~2.17.1:
+ version "2.17.1"
+ resolved "https://registry.yarnpkg.com/commander/-/commander-2.17.1.tgz#bd77ab7de6de94205ceacc72f1716d29f20a77bf"
+
+commondir@^1.0.1:
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/commondir/-/commondir-1.0.1.tgz#ddd800da0c66127393cca5950ea968a3aaf1253b"
+
+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.2.1"
+ resolved "https://registry.yarnpkg.com/component-emitter/-/component-emitter-1.2.1.tgz#137918d6d78283f7df7a6b7c5a63e140e69425e6"
+
+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.13:
+ version "2.0.13"
+ resolved "https://registry.yarnpkg.com/compressible/-/compressible-2.0.13.tgz#0d1020ab924b2fdb4d6279875c7d6daba6baa7a9"
+ dependencies:
+ mime-db ">= 1.33.0 < 2"
+
+compression-webpack-plugin@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/compression-webpack-plugin/-/compression-webpack-plugin-2.0.0.tgz#46476350c1eb27f783dccc79ac2f709baa2cffbc"
+ dependencies:
+ cacache "^11.2.0"
+ find-cache-dir "^2.0.0"
+ neo-async "^2.5.0"
+ schema-utils "^1.0.0"
+ serialize-javascript "^1.4.0"
+ webpack-sources "^1.0.1"
+
+compression@^1.5.2:
+ version "1.7.2"
+ resolved "http://registry.npmjs.org/compression/-/compression-1.7.2.tgz#aaffbcd6aaf854b44ebb280353d5ad1651f59a69"
+ dependencies:
+ accepts "~1.3.4"
+ bytes "3.0.0"
+ compressible "~2.0.13"
+ debug "2.6.9"
+ on-headers "~1.0.1"
+ safe-buffer "5.1.1"
+ 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.6.0:
+ version "1.6.0"
+ resolved "https://registry.yarnpkg.com/concat-stream/-/concat-stream-1.6.0.tgz#0aac662fd52be78964d5532f694784e70110acf7"
+ dependencies:
+ inherits "^2.0.3"
+ readable-stream "^2.2.2"
+ typedarray "^0.0.6"
+
+concat-stream@^1.4.7, concat-stream@^1.5.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"
+
+connect-history-api-fallback@^1.3.0:
+ version "1.5.0"
+ resolved "https://registry.yarnpkg.com/connect-history-api-fallback/-/connect-history-api-fallback-1.5.0.tgz#b06873934bc5e344fef611a196a6faae0aee015a"
+
+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"
+
+"consolidated-events@^1.1.1 || ^2.0.0":
+ version "2.0.2"
+ resolved "https://registry.npmjs.org/consolidated-events/-/consolidated-events-2.0.2.tgz#da8d8f8c2b232831413d9e190dc11669c79f4a91"
+ integrity sha512-2/uRVMdRypf5z/TW/ncD/66l75P5hH2vM/GR8Jf8HLc2xnfJtmina6F6du8+v4Z2vTrMo7jC+W1tmEEuuELgkQ==
+
+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.2:
+ version "0.5.2"
+ resolved "https://registry.yarnpkg.com/content-disposition/-/content-disposition-0.5.2.tgz#0cf68bb9ddf5f2be7961c3a85178cb85dba78cb4"
+
+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.5.1:
+ version "1.5.1"
+ resolved "https://registry.yarnpkg.com/convert-source-map/-/convert-source-map-1.5.1.tgz#b8278097b9bc229365de5c62cf5fcaed8b5599e5"
+
+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.3.1:
+ version "0.3.1"
+ resolved "https://registry.yarnpkg.com/cookie/-/cookie-0.3.1.tgz#e7e0a1f9ef43b4c8ba925c5c5a96e806d16873bb"
+
+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.0.8:
+ version "3.0.8"
+ resolved "https://registry.yarnpkg.com/copy-to-clipboard/-/copy-to-clipboard-3.0.8.tgz#f4e82f4a8830dce4666b7eb8ded0c9bcc313aba9"
+ dependencies:
+ toggle-selection "^1.0.3"
+
+core-js@^1.0.0:
+ version "1.2.7"
+ resolved "https://registry.yarnpkg.com/core-js/-/core-js-1.2.7.tgz#652294c14651db28fa93bd2d5ff2983a4f08c636"
+
+core-js@^2.4.0, core-js@^2.4.1, core-js@^2.5.0:
+ version "2.5.5"
+ resolved "https://registry.yarnpkg.com/core-js/-/core-js-2.5.5.tgz#b14dde936c640c0579a6b50cabcc132dd6127e3b"
+
+core-js@^2.5.1:
+ version "2.6.9"
+ resolved "https://registry.npmjs.org/core-js/-/core-js-2.6.9.tgz#6b4b214620c834152e179323727fc19741b084f2"
+ integrity sha512-HOpZf6eXmnl7la+cUdMnLvUxKNqLUzJvgIziQ0DiF3JwSImNphIqdGqzj6hIKyX04MmV0poclQ7+wjWvxQyR2A==
+
+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.0.5"
+ resolved "https://registry.yarnpkg.com/cosmiconfig/-/cosmiconfig-5.0.5.tgz#a809e3c2306891ce17ab70359dc8bdf661fe2cd0"
+ dependencies:
+ is-directory "^0.3.1"
+ js-yaml "^3.9.0"
+ parse-json "^4.0.0"
+
+create-ecdh@^4.0.0:
+ version "4.0.1"
+ resolved "https://registry.yarnpkg.com/create-ecdh/-/create-ecdh-4.0.1.tgz#44223dfed533193ba5ba54e0df5709b89acf1f82"
+ 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.2.2:
+ version "0.2.2"
+ resolved "https://registry.yarnpkg.com/create-react-context/-/create-react-context-0.2.2.tgz#9836542f9aaa22868cd7d4a6f82667df38019dca"
+ dependencies:
+ fbjs "^0.8.0"
+ gud "^1.0.0"
+
+cross-spawn@^5.0.1:
+ version "5.1.0"
+ resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-5.1.0.tgz#e8bd0efee58fcff6f8f94510a0a554bbfa235449"
+ dependencies:
+ lru-cache "^4.0.1"
+ shebang-command "^1.2.0"
+ 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"
+ dependencies:
+ nice-try "^1.0.4"
+ path-key "^2.0.1"
+ semver "^5.5.0"
+ shebang-command "^1.2.0"
+ which "^1.2.9"
+
+cryptiles@3.x.x:
+ version "3.1.2"
+ resolved "https://registry.yarnpkg.com/cryptiles/-/cryptiles-3.1.2.tgz#a89fbb220f5ce25ec56e8c4aa8a4fd7b5b0d29fe"
+ dependencies:
+ boom "5.x.x"
+
+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.5.0"
+ resolved "https://registry.yarnpkg.com/css-animation/-/css-animation-1.5.0.tgz#c96b9097a5ef74a7be8480b45cc44e4ec6ca2bf5"
+ dependencies:
+ babel-runtime "6.x"
+ component-classes "^1.2.5"
+
+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@^3.0.0:
+ version "3.0.1"
+ resolved "https://registry.yarnpkg.com/css-declaration-sorter/-/css-declaration-sorter-3.0.1.tgz#d0e3056b0fd88dc1ea9dceff435adbe9c702a7f8"
+ dependencies:
+ postcss "^6.0.0"
+ timsort "^0.3.0"
+
+css-loader@^1.0.1:
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/css-loader/-/css-loader-1.0.1.tgz#6885bb5233b35ec47b006057da01cc640b6b79fe"
+ dependencies:
+ babel-code-frame "^6.26.0"
+ css-selector-tokenizer "^0.7.0"
+ icss-utils "^2.1.0"
+ loader-utils "^1.0.2"
+ lodash "^4.17.11"
+ postcss "^6.0.23"
+ postcss-modules-extract-imports "^1.2.0"
+ postcss-modules-local-by-default "^1.2.0"
+ postcss-modules-scope "^1.1.0"
+ postcss-modules-values "^1.3.0"
+ postcss-value-parser "^3.3.0"
+ source-list-map "^2.0.0"
+
+css-select-base-adapter@~0.1.0:
+ version "0.1.0"
+ resolved "https://registry.yarnpkg.com/css-select-base-adapter/-/css-select-base-adapter-0.1.0.tgz#0102b3d14630df86c3eb9fa9f5456270106cf990"
+
+css-select@^1.1.0:
+ version "1.2.0"
+ resolved "https://registry.yarnpkg.com/css-select/-/css-select-1.2.0.tgz#2b3a110539c5355f1cd8d314623e870b121ec858"
+ dependencies:
+ boolbase "~1.0.0"
+ css-what "2.1"
+ domutils "1.5.1"
+ nth-check "~1.0.1"
+
+css-select@~1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/css-select/-/css-select-1.0.0.tgz#b1121ca51848dd264e2244d058cee254deeb44b0"
+ dependencies:
+ boolbase "~1.0.0"
+ css-what "1.0"
+ domutils "1.4"
+ nth-check "~1.0.0"
+
+css-select@~1.3.0-rc0:
+ version "1.3.0-rc0"
+ resolved "https://registry.yarnpkg.com/css-select/-/css-select-1.3.0-rc0.tgz#6f93196aaae737666ea1036a8cb14a8fcb7a9231"
+ dependencies:
+ boolbase "^1.0.0"
+ css-what "2.1"
+ domutils "1.5.1"
+ nth-check "^1.0.1"
+
+css-selector-tokenizer@^0.7.0:
+ version "0.7.0"
+ resolved "https://registry.yarnpkg.com/css-selector-tokenizer/-/css-selector-tokenizer-0.7.0.tgz#e6988474ae8c953477bf5e7efecfceccd9cf4c86"
+ dependencies:
+ cssesc "^0.1.0"
+ fastparse "^1.1.1"
+ regexpu-core "^1.0.0"
+
+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-alpha25:
+ version "1.0.0-alpha25"
+ resolved "https://registry.yarnpkg.com/css-tree/-/css-tree-1.0.0-alpha25.tgz#1bbfabfbf6eeef4f01d9108ff2edd0be2fe35597"
+ dependencies:
+ mdn-data "^1.0.0"
+ 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-url-regex@^1.1.0:
+ version "1.1.0"
+ resolved "https://registry.yarnpkg.com/css-url-regex/-/css-url-regex-1.1.0.tgz#83834230cc9f74c457de59eebd1543feeb83b7ec"
+
+css-vendor@^0.3.8:
+ version "0.3.8"
+ resolved "https://registry.yarnpkg.com/css-vendor/-/css-vendor-0.3.8.tgz#6421cfd3034ce664fe7673972fd0119fc28941fa"
+ dependencies:
+ is-in-browser "^1.0.2"
+
+css-what@1.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/css-what/-/css-what-1.0.0.tgz#d7cc2df45180666f99d2b14462639469e00f736c"
+
+css-what@2.1:
+ version "2.1.0"
+ resolved "https://registry.yarnpkg.com/css-what/-/css-what-2.1.0.tgz#9467d032c38cfaefb9f2d79501253062f87fa1bd"
+
+cssesc@^0.1.0:
+ version "0.1.0"
+ resolved "https://registry.yarnpkg.com/cssesc/-/cssesc-0.1.0.tgz#c814903e45623371a0477b40109aaafbeeaddbb4"
+
+cssnano-preset-default@^4.0.0:
+ version "4.0.0"
+ resolved "https://registry.yarnpkg.com/cssnano-preset-default/-/cssnano-preset-default-4.0.0.tgz#c334287b4f7d49fb2d170a92f9214655788e3b6b"
+ dependencies:
+ css-declaration-sorter "^3.0.0"
+ cssnano-util-raw-cache "^4.0.0"
+ postcss "^6.0.0"
+ postcss-calc "^6.0.0"
+ postcss-colormin "^4.0.0"
+ postcss-convert-values "^4.0.0"
+ postcss-discard-comments "^4.0.0"
+ postcss-discard-duplicates "^4.0.0"
+ postcss-discard-empty "^4.0.0"
+ postcss-discard-overridden "^4.0.0"
+ postcss-merge-longhand "^4.0.0"
+ postcss-merge-rules "^4.0.0"
+ postcss-minify-font-values "^4.0.0"
+ postcss-minify-gradients "^4.0.0"
+ postcss-minify-params "^4.0.0"
+ postcss-minify-selectors "^4.0.0"
+ postcss-normalize-charset "^4.0.0"
+ postcss-normalize-display-values "^4.0.0"
+ postcss-normalize-positions "^4.0.0"
+ postcss-normalize-repeat-style "^4.0.0"
+ postcss-normalize-string "^4.0.0"
+ postcss-normalize-timing-functions "^4.0.0"
+ postcss-normalize-unicode "^4.0.0"
+ postcss-normalize-url "^4.0.0"
+ postcss-normalize-whitespace "^4.0.0"
+ postcss-ordered-values "^4.0.0"
+ postcss-reduce-initial "^4.0.0"
+ postcss-reduce-transforms "^4.0.0"
+ postcss-svgo "^4.0.0"
+ postcss-unique-selectors "^4.0.0"
+
+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.0:
+ version "4.0.0"
+ resolved "https://registry.yarnpkg.com/cssnano-util-raw-cache/-/cssnano-util-raw-cache-4.0.0.tgz#be0a2856e25f185f5f7a2bcc0624e28b7f179a9f"
+ dependencies:
+ postcss "^6.0.0"
+
+cssnano-util-same-parent@^4.0.0:
+ version "4.0.0"
+ resolved "https://registry.yarnpkg.com/cssnano-util-same-parent/-/cssnano-util-same-parent-4.0.0.tgz#d2a3de1039aa98bc4ec25001fa050330c2a16dac"
+
+cssnano@^4.1.0:
+ version "4.1.0"
+ resolved "https://registry.yarnpkg.com/cssnano/-/cssnano-4.1.0.tgz#682c37b84b9b7df616450a5a8dc9269b9bd10734"
+ dependencies:
+ cosmiconfig "^5.0.0"
+ cssnano-preset-default "^4.0.0"
+ is-resolvable "^1.0.0"
+ postcss "^6.0.0"
+
+csso@^3.5.0:
+ version "3.5.1"
+ resolved "https://registry.yarnpkg.com/csso/-/csso-3.5.1.tgz#7b9eb8be61628973c1b261e169d2f024008e758b"
+ dependencies:
+ css-tree "1.0.0-alpha.29"
+
+cuid@^2.1.1:
+ version "2.1.4"
+ resolved "https://registry.yarnpkg.com/cuid/-/cuid-2.1.4.tgz#e1178eb92a05ef9364b29a9942feed36d3604cdd"
+
+cyclist@~0.2.2:
+ version "0.2.2"
+ resolved "https://registry.yarnpkg.com/cyclist/-/cyclist-0.2.2.tgz#1b33792e11e914a2fd6d6ed6447464444e5fa640"
+
+d3-array@^1.2.0:
+ version "1.2.4"
+ resolved "https://registry.npmjs.org/d3-array/-/d3-array-1.2.4.tgz#635ce4d5eea759f6f605863dbcfc30edc737f71f"
+ integrity sha512-KHW6M86R+FUPYGb3R5XiYjXPq7VzwxZ22buHhAEVG5ztoEcZZMLov530mmccaqA1GghZArjQV46fuc8kUqhhHw==
+
+d3-collection@1:
+ version "1.0.7"
+ resolved "https://registry.npmjs.org/d3-collection/-/d3-collection-1.0.7.tgz#349bd2aa9977db071091c13144d5e4f16b5b310e"
+ integrity sha512-ii0/r5f4sjKNTfh84Di+DpztYwqKhEyUlKoPrzUFfeSkWxjW49xU2QzO9qrPrNkpdI0XJkfzvmTu8V2Zylln6A==
+
+d3-color@1:
+ version "1.3.0"
+ resolved "https://registry.npmjs.org/d3-color/-/d3-color-1.3.0.tgz#675818359074215b020dc1d41d518136dcb18fa9"
+ integrity sha512-NHODMBlj59xPAwl2BDiO2Mog6V+PrGRtBfWKqKRrs9MCqlSkIEb0Z/SfY7jW29ReHTDC/j+vwXhnZcXI3+3fbg==
+
+d3-format@1:
+ version "1.3.2"
+ resolved "https://registry.npmjs.org/d3-format/-/d3-format-1.3.2.tgz#6a96b5e31bcb98122a30863f7d92365c00603562"
+ integrity sha512-Z18Dprj96ExragQ0DeGi+SYPQ7pPfRMtUXtsg/ChVIKNBCzjO8XYJvRTC1usblx52lqge56V5ect+frYTQc8WQ==
+
+d3-interpolate@1, d3-interpolate@^1.3.0:
+ version "1.3.2"
+ resolved "https://registry.npmjs.org/d3-interpolate/-/d3-interpolate-1.3.2.tgz#417d3ebdeb4bc4efcc8fd4361c55e4040211fd68"
+ integrity sha512-NlNKGopqaz9qM1PXh9gBF1KSCVh+jSFErrSlD/4hybwoNX/gt1d8CDbDW+3i+5UOHhjC6s6nMvRxcuoMVNgL2w==
+ dependencies:
+ d3-color "1"
+
+d3-path@1:
+ version "1.0.8"
+ resolved "https://registry.npmjs.org/d3-path/-/d3-path-1.0.8.tgz#4a0606a794d104513ec4a8af43525f374b278719"
+ integrity sha512-J6EfUNwcMQ+aM5YPOB8ZbgAZu6wc82f/0WFxrxwV6Ll8wBwLaHLKCqQ5Imub02JriCVVdPjgI+6P3a4EWJCxAg==
+
+d3-scale@^2.1.0:
+ version "2.2.2"
+ resolved "https://registry.npmjs.org/d3-scale/-/d3-scale-2.2.2.tgz#4e880e0b2745acaaddd3ede26a9e908a9e17b81f"
+ integrity sha512-LbeEvGgIb8UMcAa0EATLNX0lelKWGYDQiPdHj+gLblGVhGLyNbaCn3EvrJf0A3Y/uOOU5aD6MTh5ZFCdEwGiCw==
+ dependencies:
+ d3-array "^1.2.0"
+ d3-collection "1"
+ d3-format "1"
+ d3-interpolate "1"
+ d3-time "1"
+ d3-time-format "2"
+
+d3-shape@^1.2.0:
+ version "1.3.5"
+ resolved "https://registry.npmjs.org/d3-shape/-/d3-shape-1.3.5.tgz#e81aea5940f59f0a79cfccac012232a8987c6033"
+ integrity sha512-VKazVR3phgD+MUCldapHD7P9kcrvPcexeX/PkMJmkUov4JM8IxsSg1DvbYoYich9AtdTsa5nNk2++ImPiDiSxg==
+ dependencies:
+ d3-path "1"
+
+d3-time-format@2:
+ version "2.1.3"
+ resolved "https://registry.npmjs.org/d3-time-format/-/d3-time-format-2.1.3.tgz#ae06f8e0126a9d60d6364eac5b1533ae1bac826b"
+ integrity sha512-6k0a2rZryzGm5Ihx+aFMuO1GgelgIz+7HhB4PH4OEndD5q2zGn1mDfRdNrulspOfR6JXkb2sThhDK41CSK85QA==
+ dependencies:
+ d3-time "1"
+
+d3-time@1:
+ version "1.0.11"
+ resolved "https://registry.npmjs.org/d3-time/-/d3-time-1.0.11.tgz#1d831a3e25cd189eb256c17770a666368762bbce"
+ integrity sha512-Z3wpvhPLW4vEScGeIMUckDW7+3hWKOQfAWg/U7PlWBnQmeKQ00gCUsTtWSYulrKNA7ta8hJ+xXc6MHrMuITwEw==
+
+damerau-levenshtein@^1.0.4:
+ version "1.0.4"
+ resolved "https://registry.yarnpkg.com/damerau-levenshtein/-/damerau-levenshtein-1.0.4.tgz#03191c432cb6eea168bb77f3a55ffdccb8978514"
+
+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-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.1.2, debug@^2.2.0, debug@^2.3.3, debug@^2.6.8, 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.0:
+ version "3.1.0"
+ resolved "https://registry.yarnpkg.com/debug/-/debug-3.1.0.tgz#5bb5a0672628b64149566ba16819e61518c67261"
+ dependencies:
+ ms "2.0.0"
+
+debug@^3.2.5:
+ 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:
+ version "4.1.0"
+ resolved "https://registry.yarnpkg.com/debug/-/debug-4.1.0.tgz#373687bffa678b38b1cd91f861b63850035ddc87"
+ dependencies:
+ ms "^2.1.1"
+
+debug@^4.1.1:
+ version "4.1.1"
+ resolved "https://registry.yarnpkg.com/debug/-/debug-4.1.1.tgz#3b72260255109c6b589cee050f1d516139664791"
+ dependencies:
+ ms "^2.1.1"
+
+debug@~2.2.0:
+ version "2.2.0"
+ resolved "https://registry.yarnpkg.com/debug/-/debug-2.2.0.tgz#f87057e995b1a1f6ae6a4960664137bc56f039da"
+ dependencies:
+ ms "0.7.1"
+
+decamelize@^1.1.1, decamelize@^1.2.0:
+ version "1.2.0"
+ resolved "https://registry.yarnpkg.com/decamelize/-/decamelize-1.2.0.tgz#f6534d15148269b20352e7bee26f501f9a191290"
+
+decamelize@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/decamelize/-/decamelize-2.0.0.tgz#656d7bbc8094c4c788ea53c5840908c9c7d063c7"
+ dependencies:
+ xregexp "4.0.0"
+
+decimal.js-light@^2.4.1:
+ version "2.5.0"
+ resolved "https://registry.npmjs.org/decimal.js-light/-/decimal.js-light-2.5.0.tgz#ca7faf504c799326df94b0ab920424fdfc125348"
+ integrity sha512-b3VJCbd2hwUpeRGG3Toob+CRo8W22xplipNhP3tN7TSVB/cyMX71P1vM2Xjc9H74uV6dS2hDDmo/rHq8L87Upg==
+
+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-extend@^0.5.1:
+ version "0.5.1"
+ resolved "https://registry.yarnpkg.com/deep-extend/-/deep-extend-0.5.1.tgz#b894a9dd90d3023fbf1c55a394fb858eb2066f1f"
+
+deep-is@~0.1.3:
+ version "0.1.3"
+ resolved "https://registry.yarnpkg.com/deep-is/-/deep-is-0.1.3.tgz#b369d6fb5dbc13eecf524f91b070feedc357cf34"
+
+deepmerge@^1.5.1:
+ version "1.5.2"
+ resolved "https://registry.npmjs.org/deepmerge/-/deepmerge-1.5.2.tgz#10499d868844cdad4fee0842df8c7f6f0c95a753"
+ integrity sha512-95k0GDqvBjZavkuvzx/YqVLv/6YYa17fz6ILMSf7neqQITCPbnfEnQvEgMPNjH4kgobe7+WIL0yJEHku+H3qtQ==
+
+default-gateway@^4.0.1:
+ version "4.1.2"
+ resolved "https://registry.yarnpkg.com/default-gateway/-/default-gateway-4.1.2.tgz#b49196b51b26609e5d1af636287517a11a9aaf42"
+ dependencies:
+ execa "^1.0.0"
+ ip-regex "^2.1.0"
+
+define-properties@^1.1.2:
+ version "1.1.2"
+ resolved "https://registry.yarnpkg.com/define-properties/-/define-properties-1.1.2.tgz#83a73f2fea569898fb737193c8f873caf6d45c94"
+ dependencies:
+ foreach "^2.0.5"
+ object-keys "^1.0.8"
+
+define-properties@^1.1.3:
+ version "1.1.3"
+ resolved "https://registry.npmjs.org/define-properties/-/define-properties-1.1.3.tgz#cf88da6cbee26fe6db7094f61d870cbd84cee9f1"
+ integrity sha512-3MqfYKj2lLzdMSf8ZIZE/V+Zuy+BgD6f164e8K2w7dgnpKArBDerGYpM46IYYcjnkdPNMjPk9A6VFB8+3SKlXQ==
+ 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"
+
+del@^3.0.0:
+ version "3.0.0"
+ resolved "https://registry.yarnpkg.com/del/-/del-3.0.0.tgz#53ecf699ffcbcb39637691ab13baf160819766e5"
+ dependencies:
+ globby "^6.1.0"
+ is-path-cwd "^1.0.0"
+ is-path-in-cwd "^1.0.0"
+ p-map "^1.1.1"
+ pify "^3.0.0"
+ rimraf "^2.2.8"
+
+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.1:
+ version "1.1.1"
+ resolved "https://registry.yarnpkg.com/depd/-/depd-1.1.1.tgz#5783b4e1c459f06fa5ca27f991f3d06e7a310359"
+
+depd@~1.1.1, depd@~1.1.2:
+ version "1.1.2"
+ resolved "https://registry.yarnpkg.com/depd/-/depd-1.1.2.tgz#9bcd52e14c097763e749b274c4346ed2e560b5a9"
+
+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"
+
+detect-indent@^4.0.0:
+ version "4.0.0"
+ resolved "https://registry.yarnpkg.com/detect-indent/-/detect-indent-4.0.0.tgz#f76d064352cdf43a1cb6ce619c4ee3a9475de208"
+ dependencies:
+ repeating "^2.0.0"
+
+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"
+
+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"
+
+dir-glob@^2.2.1:
+ version "2.2.2"
+ resolved "https://registry.yarnpkg.com/dir-glob/-/dir-glob-2.2.2.tgz#fa09f0694153c8918b18ba0deafae94769fc50c4"
+ dependencies:
+ path-type "^3.0.0"
+
+direction@^1.0.1:
+ version "1.0.3"
+ resolved "https://registry.npmjs.org/direction/-/direction-1.0.3.tgz#5030e1e091e923904067d015dbaafd08f4d27d26"
+ integrity sha512-8bHRqMt4w/kND19KBksE4NOJo+gIOPuiZfxQvbd6xikfKbuNBYBdLIw0hA/4lWzBaDpwpW+Olmg1BjD9+0LU2w==
+
+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"
+ 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"
+ dependencies:
+ esutils "^2.0.2"
+
+document.contains@^1.0.1:
+ version "1.0.1"
+ resolved "https://registry.npmjs.org/document.contains/-/document.contains-1.0.1.tgz#a18339ec8e74f407fa34709b65f45605b38a3e1f"
+ integrity sha512-A1KqlZq1w605bwiiLqVZehWE9S9UYlUXPoduFWi64pNVNQ9vy6wwH/7BS+iEfSlF1YyZgcg5PZw5HqDi7FCrUw==
+ dependencies:
+ define-properties "^1.1.3"
+
+dom-align@^1.7.0:
+ version "1.8.0"
+ resolved "https://registry.yarnpkg.com/dom-align/-/dom-align-1.8.0.tgz#c0e89b5b674c6e836cd248c52c2992135f093654"
+
+dom-converter@~0.1:
+ version "0.1.4"
+ resolved "https://registry.yarnpkg.com/dom-converter/-/dom-converter-0.1.4.tgz#a45ef5727b890c9bffe6d7c876e7b19cb0e17f3b"
+ dependencies:
+ utila "~0.3"
+
+dom-helpers@^3.4.0:
+ version "3.4.0"
+ resolved "https://registry.npmjs.org/dom-helpers/-/dom-helpers-3.4.0.tgz#e9b369700f959f62ecde5a6babde4bccd9169af8"
+ integrity sha512-LnuPJ+dwqKDIyotW1VzmOZ5TONUN7CwkCR5hrgawTUbkBGYdeoNLZo6nNfGkCrjtE1nXXaj7iMMpDa8/d9WoIA==
+ dependencies:
+ "@babel/runtime" "^7.1.2"
+
+dom-serializer@0, dom-serializer@~0.1.0:
+ version "0.1.0"
+ resolved "https://registry.yarnpkg.com/dom-serializer/-/dom-serializer-0.1.0.tgz#073c697546ce0780ce23be4a28e293e40bc30c82"
+ dependencies:
+ domelementtype "~1.1.1"
+ entities "~1.1.1"
+
+dom-walk@^0.1.0:
+ version "0.1.1"
+ resolved "https://registry.yarnpkg.com/dom-walk/-/dom-walk-0.1.1.tgz#672226dc74c8f799ad35307df936aba11acd6018"
+
+domain-browser@^1.1.1:
+ version "1.2.0"
+ resolved "https://registry.yarnpkg.com/domain-browser/-/domain-browser-1.2.0.tgz#3d31f50191a6749dd1375a7f522e823d42e54eda"
+
+domelementtype@1, domelementtype@~1.1.1:
+ version "1.1.3"
+ resolved "https://registry.yarnpkg.com/domelementtype/-/domelementtype-1.1.3.tgz#bd28773e2642881aec51544924299c5cd822185b"
+
+domhandler@2.1:
+ version "2.1.0"
+ resolved "https://registry.yarnpkg.com/domhandler/-/domhandler-2.1.0.tgz#d2646f5e57f6c3bab11cf6cb05d3c0acf7412594"
+ dependencies:
+ domelementtype "1"
+
+domhandler@2.3:
+ version "2.3.0"
+ resolved "https://registry.yarnpkg.com/domhandler/-/domhandler-2.3.0.tgz#2de59a0822d5027fabff6f032c2b25a2a8abe738"
+ dependencies:
+ domelementtype "1"
+
+domutils@1.1:
+ version "1.1.6"
+ resolved "https://registry.yarnpkg.com/domutils/-/domutils-1.1.6.tgz#bddc3de099b9a2efacc51c623f28f416ecc57485"
+ dependencies:
+ domelementtype "1"
+
+domutils@1.4:
+ version "1.4.3"
+ resolved "https://registry.yarnpkg.com/domutils/-/domutils-1.4.3.tgz#0865513796c6b306031850e175516baf80b72a6f"
+ dependencies:
+ domelementtype "1"
+
+domutils@1.5, domutils@1.5.1:
+ version "1.5.1"
+ resolved "https://registry.yarnpkg.com/domutils/-/domutils-1.5.1.tgz#dcd8488a26f563d61079e48c9f7b7e32373682cf"
+ dependencies:
+ dom-serializer "0"
+ domelementtype "1"
+
+dot-prop@^4.1.1:
+ version "4.2.0"
+ resolved "https://registry.yarnpkg.com/dot-prop/-/dot-prop-4.2.0.tgz#1f19e0c2e1aa0e32797c49799f2837ac6af69c57"
+ dependencies:
+ is-obj "^1.0.0"
+
+dotenv-safe@^5.0.1:
+ version "5.0.1"
+ resolved "https://registry.yarnpkg.com/dotenv-safe/-/dotenv-safe-5.0.1.tgz#8c4a79b8978fd4271b3d8ef17be2b2f04588af71"
+ dependencies:
+ dotenv "^5.0.0"
+
+dotenv@^5.0.0:
+ version "5.0.1"
+ resolved "https://registry.yarnpkg.com/dotenv/-/dotenv-5.0.1.tgz#a5317459bd3d79ab88cff6e44057a6a3fbb1fcef"
+
+draft-js@^0.10.5:
+ version "0.10.5"
+ resolved "https://registry.yarnpkg.com/draft-js/-/draft-js-0.10.5.tgz#bfa9beb018fe0533dbb08d6675c371a6b08fa742"
+ dependencies:
+ fbjs "^0.8.15"
+ immutable "~3.7.4"
+ object-assign "^4.1.0"
+
+draftjs-to-html@^0.8.4:
+ version "0.8.4"
+ resolved "https://registry.yarnpkg.com/draftjs-to-html/-/draftjs-to-html-0.8.4.tgz#bda0adc00945db1f1baf0191b34b49b24191f1df"
+
+draftjs-utils@^0.9.4:
+ version "0.9.4"
+ resolved "https://registry.yarnpkg.com/draftjs-utils/-/draftjs-utils-0.9.4.tgz#976c61aa133dbbbfedd65ae1dd6627d7b98c6f08"
+
+drag-drop@2.13.3:
+ version "2.13.3"
+ resolved "https://registry.yarnpkg.com/drag-drop/-/drag-drop-2.13.3.tgz#e4dc518ad1eddf33c53cb46759a32dd73f37dfd9"
+ dependencies:
+ blob-to-buffer "^1.0.2"
+ flatten "^1.0.2"
+ run-parallel "^1.0.0"
+
+duplexer@^0.1.1:
+ version "0.1.1"
+ resolved "https://registry.yarnpkg.com/duplexer/-/duplexer-0.1.1.tgz#ace6ff808c1ce66b57d1ebf97977acb02334cfc1"
+
+duplexify@^3.4.2, duplexify@^3.6.0:
+ version "3.6.0"
+ resolved "https://registry.yarnpkg.com/duplexify/-/duplexify-3.6.0.tgz#592903f5d80b38d037220541264d69a198fb3410"
+ 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.1"
+ resolved "https://registry.yarnpkg.com/ecc-jsbn/-/ecc-jsbn-0.1.1.tgz#0fc73a9ed5f0d53c38193398523ef7e543777505"
+ dependencies:
+ jsbn "~0.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"
+
+ejs@^2.6.1:
+ version "2.6.1"
+ resolved "https://registry.yarnpkg.com/ejs/-/ejs-2.6.1.tgz#498ec0d495655abc6f23cd61868d926464071aa0"
+
+electron-to-chromium@^1.3.52:
+ version "1.3.52"
+ resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.3.52.tgz#d2d9f1270ba4a3b967b831c40ef71fb4d9ab5ce0"
+
+elliptic@^6.0.0:
+ version "6.4.0"
+ resolved "https://registry.yarnpkg.com/elliptic/-/elliptic-6.4.0.tgz#cac9af8762c85836187003c8dfe193e5e2eae5df"
+ 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.3:
+ version "3.0.3"
+ resolved "https://registry.yarnpkg.com/email-addresses/-/email-addresses-3.0.3.tgz#fc3c6952f68da24239914e982c8a7783bc2ed96d"
+
+emoji-regex@^7.0.1, emoji-regex@^7.0.2:
+ version "7.0.3"
+ resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-7.0.3.tgz#933a04052860c85e83c122479c4748a8e4c72156"
+
+emojis-list@^2.0.0:
+ version "2.1.0"
+ resolved "https://registry.yarnpkg.com/emojis-list/-/emojis-list-2.1.0.tgz#4daa4d9db00f9819880c79fa457ae5b09a1fd389"
+
+encodeurl@~1.0.2:
+ version "1.0.2"
+ resolved "https://registry.yarnpkg.com/encodeurl/-/encodeurl-1.0.2.tgz#ad3ff4c86ec2d029322f5a02c3a9a606c95b3f59"
+
+encoding@^0.1.11:
+ version "0.1.12"
+ resolved "https://registry.yarnpkg.com/encoding/-/encoding-0.1.12.tgz#538b66f3ee62cd1ab51ec323829d1f9480c74beb"
+ dependencies:
+ iconv-lite "~0.4.13"
+
+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.1.0:
+ version "4.1.0"
+ resolved "https://registry.yarnpkg.com/enhanced-resolve/-/enhanced-resolve-4.1.0.tgz#41c7e0bfdfe74ac1ffe1e57ad6a5c6c9f3742a7f"
+ dependencies:
+ graceful-fs "^4.1.2"
+ memory-fs "^0.4.0"
+ tapable "^1.0.0"
+
+enhanced-resolve@~0.9.0:
+ version "0.9.1"
+ resolved "https://registry.yarnpkg.com/enhanced-resolve/-/enhanced-resolve-0.9.1.tgz#4d6e689b3725f86090927ccc86cd9f1635b89e2e"
+ dependencies:
+ graceful-fs "^4.1.2"
+ memory-fs "^0.2.0"
+ tapable "^0.1.8"
+
+entities@1.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/entities/-/entities-1.0.0.tgz#b2987aa3821347fcde642b24fdfc9e4fb712bf26"
+
+entities@~1.1.1:
+ version "1.1.1"
+ resolved "https://registry.yarnpkg.com/entities/-/entities-1.1.1.tgz#6e5c2d0a5621b5dadaecef80b90edfb5cd7772f0"
+
+enzyme-shallow-equal@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.npmjs.org/enzyme-shallow-equal/-/enzyme-shallow-equal-1.0.0.tgz#d8e4603495e6ea279038eef05a4bf4887b55dc69"
+ integrity sha512-VUf+q5o1EIv2ZaloNQQtWCJM9gpeux6vudGVH6vLmfPXFLRuxl5+Aq3U260wof9nn0b0i+P5OEUXm1vnxkRpXQ==
+ dependencies:
+ has "^1.0.3"
+ object-is "^1.0.1"
+
+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.10.0, es-abstract@^1.12.0, es-abstract@^1.13.0:
+ version "1.13.0"
+ resolved "https://registry.npmjs.org/es-abstract/-/es-abstract-1.13.0.tgz#ac86145fdd5099d8dd49558ccba2eaf9b88e24e9"
+ integrity sha512-vDZfg/ykNxQVwup/8E1BZhVzFfBxs9NqMzGcvIJrqg5k2/5Za2bWo40dK2J1pgLngZ7c+Shh8lwYtLGyrwPutg==
+ 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.11.0, es-abstract@^1.5.1, es-abstract@^1.6.1, es-abstract@^1.7.0:
+ version "1.12.0"
+ resolved "https://registry.yarnpkg.com/es-abstract/-/es-abstract-1.12.0.tgz#9dbbdd27c6856f0001421ca18782d786bf8a6165"
+ dependencies:
+ es-to-primitive "^1.1.1"
+ function-bind "^1.1.1"
+ has "^1.0.1"
+ is-callable "^1.1.3"
+ is-regex "^1.0.4"
+
+es-to-primitive@^1.1.1:
+ version "1.1.1"
+ resolved "https://registry.yarnpkg.com/es-to-primitive/-/es-to-primitive-1.1.1.tgz#45355248a88979034b6792e19bb81f2b7975dd0d"
+ dependencies:
+ is-callable "^1.1.1"
+ is-date-object "^1.0.1"
+ is-symbol "^1.0.1"
+
+es-to-primitive@^1.2.0:
+ version "1.2.0"
+ resolved "https://registry.npmjs.org/es-to-primitive/-/es-to-primitive-1.2.0.tgz#edf72478033456e8dda8ef09e00ad9650707f377"
+ integrity sha512-qZryBOJjV//LaxLTV6UC//WewneB3LcXOL9NP++ozKVXsIIIpm/2c13UDiD9Jp2eThsecw9m3jPqDwTyobcdbg==
+ dependencies:
+ is-callable "^1.1.4"
+ is-date-object "^1.0.1"
+ is-symbol "^1.0.2"
+
+es6-promise@^3.0.2:
+ version "3.3.1"
+ resolved "https://registry.yarnpkg.com/es6-promise/-/es6-promise-3.3.1.tgz#a08cdde84ccdbf34d027a1451bc91d4bcd28a613"
+
+es6-promise@^4.0.3:
+ version "4.2.4"
+ resolved "https://registry.yarnpkg.com/es6-promise/-/es6-promise-4.2.4.tgz#dc4221c2b16518760bd8c39a52d8f356fc00ed29"
+
+es6-promisify@^5.0.0:
+ version "5.0.0"
+ resolved "https://registry.yarnpkg.com/es6-promisify/-/es6-promisify-5.0.0.tgz#5109d62f3e56ea967c4b63505aef08291c8a5203"
+ dependencies:
+ es6-promise "^4.0.3"
+
+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@^13.1.0:
+ version "13.1.0"
+ resolved "https://registry.yarnpkg.com/eslint-config-airbnb-base/-/eslint-config-airbnb-base-13.1.0.tgz#b5a1b480b80dfad16433d6c4ad84e6605052c05c"
+ dependencies:
+ eslint-restricted-globals "^0.1.1"
+ object.assign "^4.1.0"
+ object.entries "^1.0.4"
+
+eslint-config-airbnb@17.1.0:
+ version "17.1.0"
+ resolved "https://registry.yarnpkg.com/eslint-config-airbnb/-/eslint-config-airbnb-17.1.0.tgz#3964ed4bc198240315ff52030bf8636f42bc4732"
+ dependencies:
+ eslint-config-airbnb-base "^13.1.0"
+ object.assign "^4.1.0"
+ object.entries "^1.0.4"
+
+eslint-import-resolver-node@^0.3.2:
+ version "0.3.2"
+ resolved "https://registry.yarnpkg.com/eslint-import-resolver-node/-/eslint-import-resolver-node-0.3.2.tgz#58f15fb839b8d0576ca980413476aab2472db66a"
+ dependencies:
+ debug "^2.6.9"
+ resolve "^1.5.0"
+
+eslint-import-resolver-webpack@0.11.0:
+ version "0.11.0"
+ resolved "https://registry.yarnpkg.com/eslint-import-resolver-webpack/-/eslint-import-resolver-webpack-0.11.0.tgz#75d08ee06fc55eb24bd75147b7b4b6756886b12f"
+ dependencies:
+ array-find "^1.0.0"
+ debug "^2.6.8"
+ enhanced-resolve "~0.9.0"
+ find-root "^1.1.0"
+ has "^1.0.1"
+ interpret "^1.0.0"
+ lodash "^4.17.4"
+ node-libs-browser "^1.0.0 || ^2.0.0"
+ resolve "^1.4.0"
+ semver "^5.3.0"
+
+eslint-loader@^2.1.2:
+ version "2.1.2"
+ resolved "https://registry.yarnpkg.com/eslint-loader/-/eslint-loader-2.1.2.tgz#453542a1230d6ffac90e4e7cb9cadba9d851be68"
+ dependencies:
+ loader-fs-cache "^1.0.0"
+ loader-utils "^1.0.2"
+ object-assign "^4.0.1"
+ object-hash "^1.1.4"
+ rimraf "^2.6.1"
+
+eslint-module-utils@^2.3.0:
+ version "2.3.0"
+ resolved "https://registry.yarnpkg.com/eslint-module-utils/-/eslint-module-utils-2.3.0.tgz#546178dab5e046c8b562bbb50705e2456d7bda49"
+ dependencies:
+ debug "^2.6.8"
+ pkg-dir "^2.0.0"
+
+eslint-plugin-import@2.16.0:
+ version "2.16.0"
+ resolved "https://registry.yarnpkg.com/eslint-plugin-import/-/eslint-plugin-import-2.16.0.tgz#97ac3e75d0791c4fac0e15ef388510217be7f66f"
+ dependencies:
+ contains-path "^0.1.0"
+ debug "^2.6.9"
+ doctrine "1.5.0"
+ eslint-import-resolver-node "^0.3.2"
+ eslint-module-utils "^2.3.0"
+ has "^1.0.3"
+ lodash "^4.17.11"
+ minimatch "^3.0.4"
+ read-pkg-up "^2.0.0"
+ resolve "^1.9.0"
+
+eslint-plugin-jsx-a11y@6.2.1:
+ version "6.2.1"
+ resolved "https://registry.yarnpkg.com/eslint-plugin-jsx-a11y/-/eslint-plugin-jsx-a11y-6.2.1.tgz#4ebba9f339b600ff415ae4166e3e2e008831cf0c"
+ dependencies:
+ aria-query "^3.0.0"
+ array-includes "^3.0.3"
+ ast-types-flow "^0.0.7"
+ axobject-query "^2.0.2"
+ damerau-levenshtein "^1.0.4"
+ emoji-regex "^7.0.2"
+ has "^1.0.3"
+ jsx-ast-utils "^2.0.1"
+
+eslint-plugin-react-hooks@^1.7.0:
+ version "1.7.0"
+ resolved "https://registry.yarnpkg.com/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-1.7.0.tgz#6210b6d5a37205f0b92858f895a4e827020a7d04"
+ integrity sha512-iXTCFcOmlWvw4+TOE8CLWj6yX1GwzT0Y6cUfHHZqWnSk144VmVIRcVGtUAzrLES7C798lmvnt02C7rxaOX1HNA==
+
+eslint-plugin-react@7.12.4:
+ version "7.12.4"
+ resolved "https://registry.yarnpkg.com/eslint-plugin-react/-/eslint-plugin-react-7.12.4.tgz#b1ecf26479d61aee650da612e425c53a99f48c8c"
+ dependencies:
+ array-includes "^3.0.3"
+ doctrine "^2.1.0"
+ has "^1.0.3"
+ jsx-ast-utils "^2.0.1"
+ object.fromentries "^2.0.0"
+ prop-types "^15.6.2"
+ resolve "^1.9.0"
+
+eslint-restricted-globals@^0.1.1:
+ version "0.1.1"
+ resolved "https://registry.yarnpkg.com/eslint-restricted-globals/-/eslint-restricted-globals-0.1.1.tgz#35f0d5cbc64c2e3ed62e93b4b1a7af05ba7ed4d7"
+
+eslint-scope@3.7.1:
+ version "3.7.1"
+ resolved "https://registry.yarnpkg.com/eslint-scope/-/eslint-scope-3.7.1.tgz#3d63c3edfda02e06e01a452ad88caacc7cdcb6e8"
+ dependencies:
+ esrecurse "^4.1.0"
+ estraverse "^4.1.1"
+
+eslint-scope@^4.0.0:
+ version "4.0.0"
+ resolved "https://registry.yarnpkg.com/eslint-scope/-/eslint-scope-4.0.0.tgz#50bf3071e9338bcdc43331794a0cb533f0136172"
+ dependencies:
+ esrecurse "^4.1.0"
+ estraverse "^4.1.1"
+
+eslint-utils@^1.3.1:
+ version "1.3.1"
+ resolved "https://registry.yarnpkg.com/eslint-utils/-/eslint-utils-1.3.1.tgz#9a851ba89ee7c460346f97cf8939c7298827e512"
+
+eslint-visitor-keys@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/eslint-visitor-keys/-/eslint-visitor-keys-1.0.0.tgz#3f3180fb2e291017716acb4c9d6d5b5c34a6a81d"
+
+eslint@^5.14.1:
+ version "5.14.1"
+ resolved "https://registry.yarnpkg.com/eslint/-/eslint-5.14.1.tgz#490a28906be313685c55ccd43a39e8d22efc04ba"
+ dependencies:
+ "@babel/code-frame" "^7.0.0"
+ ajv "^6.9.1"
+ chalk "^2.1.0"
+ cross-spawn "^6.0.5"
+ debug "^4.0.1"
+ doctrine "^3.0.0"
+ eslint-scope "^4.0.0"
+ eslint-utils "^1.3.1"
+ eslint-visitor-keys "^1.0.0"
+ espree "^5.0.1"
+ esquery "^1.0.1"
+ esutils "^2.0.2"
+ file-entry-cache "^5.0.1"
+ functional-red-black-tree "^1.0.1"
+ glob "^7.1.2"
+ globals "^11.7.0"
+ ignore "^4.0.6"
+ import-fresh "^3.0.0"
+ imurmurhash "^0.1.4"
+ inquirer "^6.2.2"
+ js-yaml "^3.12.0"
+ json-stable-stringify-without-jsonify "^1.0.1"
+ levn "^0.3.0"
+ lodash "^4.17.11"
+ minimatch "^3.0.4"
+ mkdirp "^0.5.1"
+ natural-compare "^1.4.0"
+ optionator "^0.8.2"
+ path-is-inside "^1.0.2"
+ progress "^2.0.0"
+ regexpp "^2.0.1"
+ semver "^5.5.1"
+ strip-ansi "^4.0.0"
+ strip-json-comments "^2.0.1"
+ table "^5.2.3"
+ text-table "^0.2.0"
+
+espree@^5.0.1:
+ version "5.0.1"
+ resolved "https://registry.yarnpkg.com/espree/-/espree-5.0.1.tgz#5d6526fa4fc7f0788a5cf75b15f30323e2f81f7a"
+ dependencies:
+ acorn "^6.0.7"
+ acorn-jsx "^5.0.0"
+ eslint-visitor-keys "^1.0.0"
+
+esprima@^4.0.0:
+ version "4.0.0"
+ resolved "https://registry.yarnpkg.com/esprima/-/esprima-4.0.0.tgz#4499eddcd1110e0b218bacf2fa7f7f59f55ca804"
+
+esquery@^1.0.1:
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/esquery/-/esquery-1.0.1.tgz#406c51658b1f5991a5f9b62b1dc25b00e3e5c708"
+ dependencies:
+ estraverse "^4.0.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"
+
+estraverse@^4.0.0, estraverse@^4.1.0, estraverse@^4.1.1:
+ version "4.2.0"
+ resolved "https://registry.yarnpkg.com/estraverse/-/estraverse-4.2.0.tgz#0dee3fed31fcd469618ce7342099fc1afa0bdb13"
+
+esutils@^2.0.2:
+ version "2.0.2"
+ resolved "https://registry.yarnpkg.com/esutils/-/esutils-2.0.2.tgz#0abf4f1caa5bcb1f7a9d8acc6dea4faaa04bac9b"
+
+etag@~1.8.1:
+ version "1.8.1"
+ resolved "https://registry.yarnpkg.com/etag/-/etag-1.8.1.tgz#41ae2eeb65efa62268aebfea83ac7d79299b0887"
+
+eventemitter3@^3.0.0:
+ version "3.1.0"
+ resolved "https://registry.yarnpkg.com/eventemitter3/-/eventemitter3-3.1.0.tgz#090b4d6cdbd645ed10bf750d4b5407942d7ba163"
+
+events@^1.0.0:
+ version "1.1.1"
+ resolved "https://registry.yarnpkg.com/events/-/events-1.1.1.tgz#9ebdb7635ad099c70dcc4c2a1f5004288e8bd924"
+
+eventsource@^1.0.7:
+ version "1.0.7"
+ resolved "https://registry.yarnpkg.com/eventsource/-/eventsource-1.0.7.tgz#8fbc72c93fcd34088090bc0a4e64f4b5cee6d8d0"
+ 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, execa@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/execa/-/execa-1.0.0.tgz#c6236a5bb4df6d6f15e88e7f017798216749ddd8"
+ 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"
+
+execa@^0.10.0:
+ version "0.10.0"
+ resolved "https://registry.yarnpkg.com/execa/-/execa-0.10.0.tgz#ff456a8f53f90f8eccc71a96d11bdfc7f082cb50"
+ dependencies:
+ cross-spawn "^6.0.0"
+ get-stream "^3.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"
+
+exenv@^1.2.0:
+ version "1.2.2"
+ resolved "https://registry.yarnpkg.com/exenv/-/exenv-1.2.2.tgz#2ae78e85d9894158670b03d47bec1f03bd91bb9d"
+
+exif-parser@^0.1.9:
+ version "0.1.12"
+ resolved "https://registry.yarnpkg.com/exif-parser/-/exif-parser-0.1.12.tgz#58a9d2d72c02c1f6f02a0ef4a9166272b7760922"
+
+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"
+ dependencies:
+ homedir-polyfill "^1.0.1"
+
+express@^4.16.2, express@^4.16.3:
+ version "4.16.3"
+ resolved "https://registry.yarnpkg.com/express/-/express-4.16.3.tgz#6af8a502350db3246ecc4becf6b5a34d22f7ed53"
+ dependencies:
+ accepts "~1.3.5"
+ array-flatten "1.1.1"
+ body-parser "1.18.2"
+ content-disposition "0.5.2"
+ content-type "~1.0.4"
+ cookie "0.3.1"
+ 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.1"
+ fresh "0.5.2"
+ merge-descriptors "1.0.1"
+ methods "~1.1.2"
+ on-finished "~2.3.0"
+ parseurl "~1.3.2"
+ path-to-regexp "0.1.7"
+ proxy-addr "~2.0.3"
+ qs "6.5.1"
+ range-parser "~1.2.0"
+ safe-buffer "5.1.1"
+ send "0.16.2"
+ serve-static "1.13.2"
+ setprototypeof "1.1.0"
+ statuses "~1.4.0"
+ type-is "~1.6.16"
+ 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.1:
+ version "3.0.1"
+ resolved "https://registry.yarnpkg.com/extend/-/extend-3.0.1.tgz#a755ea7bc1adfcc5a31ce7e762dbaadc5e636444"
+
+external-editor@^3.0.3:
+ version "3.0.3"
+ resolved "https://registry.yarnpkg.com/external-editor/-/external-editor-3.0.3.tgz#5866db29a97826dbe4bf3afd24070ead9ea43a27"
+ dependencies:
+ chardet "^0.7.0"
+ iconv-lite "^0.4.24"
+ tmp "^0.0.33"
+
+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"
+
+extract-files@^5.0.0:
+ version "5.0.0"
+ resolved "https://registry.yarnpkg.com/extract-files/-/extract-files-5.0.0.tgz#750f1e969b79b8e67a0ac40268dc6346fc339a3a"
+
+extract-zip@^1.6.5:
+ version "1.6.6"
+ resolved "https://registry.yarnpkg.com/extract-zip/-/extract-zip-1.6.6.tgz#1290ede8d20d0872b429fd3f351ca128ec5ef85c"
+ dependencies:
+ concat-stream "1.6.0"
+ debug "2.6.9"
+ mkdirp "0.5.0"
+ yauzl "2.4.1"
+
+extsprintf@1.3.0, extsprintf@^1.2.0:
+ version "1.3.0"
+ resolved "https://registry.yarnpkg.com/extsprintf/-/extsprintf-1.3.0.tgz#96918440e3041a7a414f8c52e3c574eb3c3e1e05"
+
+fast-deep-equal@^1.0.0:
+ version "1.1.0"
+ resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-1.1.0.tgz#c053477817c86b51daa853c81e059b733d023614"
+
+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-glob@^2.2.6:
+ version "2.2.6"
+ resolved "https://registry.yarnpkg.com/fast-glob/-/fast-glob-2.2.6.tgz#a5d5b697ec8deda468d85a74035290a025a95295"
+ dependencies:
+ "@mrmlnc/readdir-enhanced" "^2.2.1"
+ "@nodelib/fs.stat" "^1.1.2"
+ glob-parent "^3.1.0"
+ is-glob "^4.0.0"
+ merge2 "^1.2.3"
+ micromatch "^3.1.10"
+
+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, fast-levenshtein@~2.0.4:
+ version "2.0.6"
+ resolved "https://registry.yarnpkg.com/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz#3d8a5c66883a16a30ca8643e851f19baa7797917"
+
+fastparse@^1.1.1:
+ version "1.1.1"
+ resolved "https://registry.yarnpkg.com/fastparse/-/fastparse-1.1.1.tgz#d1e2643b38a94d7583b479060e6c4affc94071f8"
+
+favicons-webpack-plugin@^0.0.9:
+ version "0.0.9"
+ resolved "https://registry.yarnpkg.com/favicons-webpack-plugin/-/favicons-webpack-plugin-0.0.9.tgz#df63e80c556b804e4925ec8e05bee36391573dc9"
+ dependencies:
+ favicons "^4.8.3"
+ loader-utils "^0.2.14"
+ lodash "^4.11.1"
+
+favicons@^4.8.3:
+ version "4.8.6"
+ resolved "https://registry.yarnpkg.com/favicons/-/favicons-4.8.6.tgz#a2b13800ab3fec2715bc8f27fa841d038d4761e2"
+ dependencies:
+ async "^1.5.0"
+ cheerio "^0.19.0"
+ clone "^1.0.2"
+ colors "^1.1.2"
+ harmony-reflect "^1.4.2"
+ image-size "^0.4.0"
+ jimp "^0.2.13"
+ jsontoxml "0.0.11"
+ merge-defaults "^0.2.1"
+ mkdirp "^0.5.1"
+ node-rest-client "^1.5.1"
+ require-directory "^2.1.1"
+ svg2png "~3.0.1"
+ through2 "^2.0.0"
+ tinycolor2 "^1.1.2"
+ to-ico "^1.1.2"
+ underscore "^1.8.3"
+ vinyl "^1.1.0"
+
+faye-websocket@^0.10.0:
+ version "0.10.0"
+ resolved "https://registry.yarnpkg.com/faye-websocket/-/faye-websocket-0.10.0.tgz#4e492f8d04dfb6f89003507f6edbf2d501e7c6f4"
+ dependencies:
+ websocket-driver ">=0.5.1"
+
+faye-websocket@~0.11.1:
+ version "0.11.1"
+ resolved "https://registry.yarnpkg.com/faye-websocket/-/faye-websocket-0.11.1.tgz#f0efe18c4f56e4f40afc7e06c719fd5ee6188f38"
+ dependencies:
+ websocket-driver ">=0.5.1"
+
+fbemitter@^2.0.0:
+ version "2.1.1"
+ resolved "https://registry.yarnpkg.com/fbemitter/-/fbemitter-2.1.1.tgz#523e14fdaf5248805bb02f62efc33be703f51865"
+ dependencies:
+ fbjs "^0.8.4"
+
+fbjs-css-vars@^1.0.0:
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/fbjs-css-vars/-/fbjs-css-vars-1.0.1.tgz#836d876e887d702f45610f5ebd2fbeef649527fc"
+
+fbjs@^0.8.0, fbjs@^0.8.15, fbjs@^0.8.4:
+ version "0.8.17"
+ resolved "https://registry.yarnpkg.com/fbjs/-/fbjs-0.8.17.tgz#c4d598ead6949112653d6588b01a5cdcd9f90fdd"
+ dependencies:
+ core-js "^1.0.0"
+ isomorphic-fetch "^2.1.1"
+ loose-envify "^1.0.0"
+ object-assign "^4.1.0"
+ promise "^7.1.1"
+ setimmediate "^1.0.5"
+ ua-parser-js "^0.7.18"
+
+fbjs@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/fbjs/-/fbjs-1.0.0.tgz#52c215e0883a3c86af2a7a776ed51525ae8e0a5a"
+ dependencies:
+ core-js "^2.4.1"
+ fbjs-css-vars "^1.0.0"
+ isomorphic-fetch "^2.1.1"
+ loose-envify "^1.0.0"
+ object-assign "^4.1.0"
+ promise "^7.1.1"
+ setimmediate "^1.0.5"
+ ua-parser-js "^0.7.18"
+
+fd-slicer@~1.0.1:
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/fd-slicer/-/fd-slicer-1.0.1.tgz#8b5bcbd9ec327c5041bf9ab023fd6750f1177e65"
+ dependencies:
+ pend "~1.2.0"
+
+figgy-pudding@^3.1.0, figgy-pudding@^3.5.1:
+ version "3.5.1"
+ resolved "https://registry.yarnpkg.com/figgy-pudding/-/figgy-pudding-3.5.1.tgz#862470112901c727a0e495a80744bd5baa1d6790"
+
+figures@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/figures/-/figures-2.0.0.tgz#3ab1a2d2a62c8bfb431a0c94cb797a2fce27c962"
+ dependencies:
+ escape-string-regexp "^1.0.5"
+
+file-entry-cache@^5.0.1:
+ version "5.0.1"
+ resolved "https://registry.yarnpkg.com/file-entry-cache/-/file-entry-cache-5.0.1.tgz#ca0f6efa6dd3d561333fb14515065c2fafdf439c"
+ dependencies:
+ flat-cache "^2.0.1"
+
+file-loader@^3.0.1:
+ version "3.0.1"
+ resolved "https://registry.yarnpkg.com/file-loader/-/file-loader-3.0.1.tgz#f8e0ba0b599918b51adfe45d66d1e771ad560faa"
+ dependencies:
+ loader-utils "^1.0.2"
+ schema-utils "^1.0.0"
+
+file-selector@^0.1.12:
+ version "0.1.12"
+ resolved "https://registry.yarnpkg.com/file-selector/-/file-selector-0.1.12.tgz#fe726547be219a787a9dcc640575a04a032b1fd0"
+ integrity sha512-Kx7RTzxyQipHuiqyZGf+Nz4vY9R1XGxuQl/hLoJwq+J4avk/9wxxgZyHKtbyIPJmbD4A66DWGYfyykWNpcYutQ==
+ dependencies:
+ tslib "^1.9.0"
+
+file-type@^3.1.0, file-type@^3.8.0:
+ version "3.9.0"
+ resolved "https://registry.yarnpkg.com/file-type/-/file-type-3.9.0.tgz#257a078384d1db8087bc449d107d52a52672b9e9"
+
+filesize@^3.6.1:
+ version "3.6.1"
+ resolved "https://registry.yarnpkg.com/filesize/-/filesize-3.6.1.tgz#090bb3ee01b6f801a8a8be99d31710b3422bb317"
+
+filesize@^4.1.2:
+ version "4.1.2"
+ resolved "https://registry.yarnpkg.com/filesize/-/filesize-4.1.2.tgz#fcd570af1353cea97897be64f56183adb995994b"
+
+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"
+
+final-form-arrays@^3.0.2:
+ version "3.0.2"
+ resolved "https://registry.yarnpkg.com/final-form-arrays/-/final-form-arrays-3.0.2.tgz#9f3bef778dec61432357744eb6f3abef7e7f3847"
+ integrity sha512-TfO8aZNz3RrsZCDx8GHMQcyztDNpGxSSi9w4wpSNKlmv2PfFWVVM8P7Yj5tj4n0OWax+x5YwTLhT5BnqSlCi+w==
+
+final-form-calculate@^1.3.1:
+ version "1.3.1"
+ resolved "https://registry.yarnpkg.com/final-form-calculate/-/final-form-calculate-1.3.1.tgz#463089114245afa97fea94712bfbfca11da8413e"
+
+final-form@^4.11.1:
+ version "4.18.6"
+ resolved "https://registry.yarnpkg.com/final-form/-/final-form-4.18.6.tgz#3539c53d04e6603a0cc11c78dc830755add986d4"
+ integrity sha512-LssWTXTGkoNv5WxowFTTUyC86oLYlEHH2glARBxOq8RpjiQdOyUyVG7qfPaCeOwoOw7Y5wrhSWdePyOydqSrCw==
+ dependencies:
+ "@babel/runtime" "^7.3.1"
+
+finalhandler@1.1.1:
+ version "1.1.1"
+ resolved "https://registry.yarnpkg.com/finalhandler/-/finalhandler-1.1.1.tgz#eebf4ed840079c83f4249038c9d703008301b105"
+ dependencies:
+ debug "2.6.9"
+ encodeurl "~1.0.2"
+ escape-html "~1.0.3"
+ on-finished "~2.3.0"
+ parseurl "~1.3.2"
+ statuses "~1.4.0"
+ unpipe "~1.0.0"
+
+find-cache-dir@^0.1.1:
+ version "0.1.1"
+ resolved "https://registry.yarnpkg.com/find-cache-dir/-/find-cache-dir-0.1.1.tgz#c8defae57c8a52a8a784f9e31c57c742e993a0b9"
+ dependencies:
+ commondir "^1.0.1"
+ mkdirp "^0.5.1"
+ pkg-dir "^1.0.0"
+
+find-cache-dir@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/find-cache-dir/-/find-cache-dir-1.0.0.tgz#9288e3e9e3cc3748717d39eade17cf71fc30ee6f"
+ dependencies:
+ commondir "^1.0.1"
+ make-dir "^1.0.0"
+ pkg-dir "^2.0.0"
+
+find-cache-dir@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/find-cache-dir/-/find-cache-dir-2.0.0.tgz#4c1faed59f45184530fb9d7fa123a4d04a98472d"
+ dependencies:
+ commondir "^1.0.1"
+ make-dir "^1.0.0"
+ pkg-dir "^3.0.0"
+
+find-root@^1.1.0:
+ version "1.1.0"
+ resolved "https://registry.yarnpkg.com/find-root/-/find-root-1.1.0.tgz#abcfc8ba76f708c42a97b3d685b7e9450bfb9ce4"
+
+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"
+ dependencies:
+ locate-path "^3.0.0"
+
+findup-sync@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/findup-sync/-/findup-sync-2.0.0.tgz#9326b1488c22d1a6088650a86901b2d9a90a2cbc"
+ dependencies:
+ detect-file "^1.0.0"
+ is-glob "^3.1.0"
+ micromatch "^3.0.4"
+ resolve-dir "^1.0.1"
+
+flat-cache@^2.0.1:
+ version "2.0.1"
+ resolved "https://registry.yarnpkg.com/flat-cache/-/flat-cache-2.0.1.tgz#5d296d6f04bda44a4630a301413bdbc2ec085ec0"
+ dependencies:
+ flatted "^2.0.0"
+ rimraf "2.6.3"
+ write "1.0.3"
+
+flatted@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/flatted/-/flatted-2.0.0.tgz#55122b6536ea496b4b44893ee2608141d10d9916"
+
+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.0.3"
+ resolved "https://registry.yarnpkg.com/flush-write-stream/-/flush-write-stream-1.0.3.tgz#c5d586ef38af6097650b49bc41b55fabb19f35bd"
+ dependencies:
+ inherits "^2.0.1"
+ readable-stream "^2.0.4"
+
+flux@^3.1.3:
+ version "3.1.3"
+ resolved "https://registry.yarnpkg.com/flux/-/flux-3.1.3.tgz#d23bed515a79a22d933ab53ab4ada19d05b2f08a"
+ dependencies:
+ fbemitter "^2.0.0"
+ fbjs "^0.8.0"
+
+fn-name@~2.0.1:
+ version "2.0.1"
+ resolved "https://registry.yarnpkg.com/fn-name/-/fn-name-2.0.1.tgz#5214d7537a4d06a4a301c0cc262feb84188002e7"
+
+follow-redirects@^1.0.0:
+ version "1.4.1"
+ resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.4.1.tgz#d8120f4518190f55aac65bb6fc7b85fcd666d6aa"
+ dependencies:
+ debug "^3.1.0"
+
+for-each@^0.3.2:
+ version "0.3.2"
+ resolved "https://registry.yarnpkg.com/for-each/-/for-each-0.3.2.tgz#2c40450b9348e97f281322593ba96704b9abd4d4"
+ dependencies:
+ is-function "~1.0.0"
+
+for-in@^1.0.2:
+ version "1.0.2"
+ resolved "https://registry.yarnpkg.com/for-in/-/for-in-1.0.2.tgz#81068d295a8142ec0ac726c6e2200c30fb6d5e80"
+
+foreach@^2.0.5:
+ version "2.0.5"
+ resolved "https://registry.yarnpkg.com/foreach/-/foreach-2.0.5.tgz#0bee005018aeb260d0a3af3ae658dd0136ec1b99"
+
+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@~2.3.1:
+ version "2.3.2"
+ resolved "https://registry.yarnpkg.com/form-data/-/form-data-2.3.2.tgz#4970498be604c20c005d4f5c23aecd21d6b49099"
+ 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-copy-file-sync@^1.1.1:
+ version "1.1.1"
+ resolved "https://registry.yarnpkg.com/fs-copy-file-sync/-/fs-copy-file-sync-1.1.1.tgz#11bf32c096c10d126e5f6b36d06eece776062918"
+
+fs-extra@7.0.1:
+ 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-extra@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/fs-extra/-/fs-extra-1.0.0.tgz#cd3ce5f7e7cb6145883fcae3191e9877f8587950"
+ dependencies:
+ graceful-fs "^4.1.2"
+ jsonfile "^2.1.0"
+ klaw "^1.0.0"
+
+fs-minipass@^1.2.5:
+ version "1.2.5"
+ resolved "https://registry.yarnpkg.com/fs-minipass/-/fs-minipass-1.2.5.tgz#06c277218454ec288df77ada54a03b8702aacb9d"
+ dependencies:
+ minipass "^2.2.1"
+
+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.1.2:
+ version "1.2.3"
+ resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-1.2.3.tgz#08292982e7059f6674c93d8b829c1e8604979ac0"
+ dependencies:
+ nan "^2.9.2"
+ node-pre-gyp "^0.9.0"
+
+function-bind@^1.0.2, function-bind@^1.1.0, function-bind@^1.1.1:
+ version "1.1.1"
+ resolved "https://registry.yarnpkg.com/function-bind/-/function-bind-1.1.1.tgz#a56899d3ea3c9bab874bb9773b7c5ede92f4895d"
+
+function.prototype.name@^1.1.1:
+ version "1.1.1"
+ resolved "https://registry.npmjs.org/function.prototype.name/-/function.prototype.name-1.1.1.tgz#6d252350803085abc2ad423d4fe3be2f9cbda392"
+ integrity sha512-e1NzkiJuw6xqVH7YSdiW/qDHebcmMhPNe6w+4ZYYEg0VA+LaLzx37RimbPLuonHhYGFGPx1ME2nSi74JiaCr/Q==
+ dependencies:
+ define-properties "^1.1.3"
+ function-bind "^1.1.1"
+ functions-have-names "^1.1.1"
+ is-callable "^1.1.4"
+
+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"
+
+functions-have-names@^1.1.1:
+ version "1.1.1"
+ resolved "https://registry.npmjs.org/functions-have-names/-/functions-have-names-1.1.1.tgz#79d35927f07b8e7103d819fed475b64ccf7225ea"
+ integrity sha512-U0kNHUoxwPNPWOJaMG7Z00d4a/qZVrFtzWJRaK8V9goaVOCXBSQSJpt3MYGNtkScKEBKovxLjnNdC9MlXwo5Pw==
+
+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"
+
+get-caller-file@^1.0.1:
+ version "1.0.2"
+ resolved "https://registry.yarnpkg.com/get-caller-file/-/get-caller-file-1.0.2.tgz#f702e63127e7e231c160a80c1554acb70d5047e5"
+
+get-stream@^2.0.0:
+ version "2.3.1"
+ resolved "https://registry.yarnpkg.com/get-stream/-/get-stream-2.3.1.tgz#5f38f93f346009666ee0150a054167f91bdd95de"
+ dependencies:
+ object-assign "^4.0.1"
+ pinkie-promise "^2.0.0"
+
+get-stream@^3.0.0:
+ version "3.0.0"
+ resolved "https://registry.yarnpkg.com/get-stream/-/get-stream-3.0.0.tgz#8e943d1358dc37555054ecbe2edb05aa174ede14"
+
+get-stream@^4.0.0:
+ version "4.1.0"
+ resolved "https://registry.yarnpkg.com/get-stream/-/get-stream-4.1.0.tgz#c1b255575f3dc21d59bfc79cd3d2b46b1c3a54b5"
+ 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"
+
+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-to-regexp@^0.3.0:
+ version "0.3.0"
+ resolved "https://registry.yarnpkg.com/glob-to-regexp/-/glob-to-regexp-0.3.0.tgz#8c5a1494d2066c570cc3bfe4496175acc4d502ab"
+
+glob@^7.0.3, glob@^7.0.5, glob@^7.1.1, glob@^7.1.2:
+ version "7.1.2"
+ resolved "https://registry.yarnpkg.com/glob/-/glob-7.1.2.tgz#c19c9df9a028702d678612384a6552404c636d15"
+ 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.3:
+ version "7.1.3"
+ resolved "https://registry.yarnpkg.com/glob/-/glob-7.1.3.tgz#3960832d3f1574108342dafd3a67b332c0969df1"
+ 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-cache@^1.2.1:
+ version "1.2.1"
+ resolved "https://registry.npmjs.org/global-cache/-/global-cache-1.2.1.tgz#39ca020d3dd7b3f0934c52b75363f8d53312c16d"
+ integrity sha512-EOeUaup5DgWKlCMhA9YFqNRIlZwoxt731jCh47WBV9fQqHgXhr3Fa55hfgIUqilIcPsfdNKN7LHjrNY+Km40KA==
+ dependencies:
+ define-properties "^1.1.2"
+ is-symbol "^1.0.1"
+
+global-modules@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/global-modules/-/global-modules-1.0.0.tgz#6d770f0eb523ac78164d72b5e71a8877265cc3ea"
+ dependencies:
+ global-prefix "^1.0.1"
+ is-windows "^1.0.1"
+ resolve-dir "^1.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"
+ 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@^4.3.0, global@~4.3.0:
+ version "4.3.2"
+ resolved "https://registry.yarnpkg.com/global/-/global-4.3.2.tgz#e76989268a6c74c38908b1305b10fc0e394e9d0f"
+ dependencies:
+ min-document "^2.19.0"
+ process "~0.5.1"
+
+globals@^11.1.0, globals@^11.7.0:
+ version "11.7.0"
+ resolved "https://registry.yarnpkg.com/globals/-/globals-11.7.0.tgz#a583faa43055b1aca771914bf68258e2fc125673"
+
+globals@^9.18.0:
+ version "9.18.0"
+ resolved "https://registry.yarnpkg.com/globals/-/globals-9.18.0.tgz#aa3896b3e69b487f17e31ed2143d69a8e30c2d8a"
+
+globby@9.0.0:
+ version "9.0.0"
+ resolved "https://registry.yarnpkg.com/globby/-/globby-9.0.0.tgz#3800df736dc711266df39b4ce33fe0d481f94c23"
+ dependencies:
+ array-union "^1.0.2"
+ dir-glob "^2.2.1"
+ fast-glob "^2.2.6"
+ glob "^7.1.3"
+ ignore "^4.0.3"
+ pify "^4.0.1"
+ slash "^2.0.0"
+
+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"
+
+graceful-fs@^4.1.11, graceful-fs@^4.1.2, graceful-fs@^4.1.6, graceful-fs@^4.1.9:
+ version "4.1.11"
+ resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.1.11.tgz#0e8bdfe4d1ddb8854d64e04ea7c00e2a026e5658"
+
+graphql-anywhere@^4.1.0-alpha.0:
+ version "4.1.24"
+ resolved "https://registry.yarnpkg.com/graphql-anywhere/-/graphql-anywhere-4.1.24.tgz#59e2a8bfd3d00ac75f6a377e31e4e9fdaa5ce786"
+ dependencies:
+ apollo-utilities "^1.0.27"
+
+graphql-anywhere@^4.1.28:
+ version "4.1.28"
+ resolved "https://registry.yarnpkg.com/graphql-anywhere/-/graphql-anywhere-4.1.28.tgz#532ced541ca9f463030e14d3a7ede5b7282a12a8"
+ dependencies:
+ apollo-utilities "^1.1.3"
+ tslib "^1.9.3"
+
+graphql-tag@^2.10.1:
+ version "2.10.1"
+ resolved "https://registry.yarnpkg.com/graphql-tag/-/graphql-tag-2.10.1.tgz#10aa41f1cd8fae5373eaf11f1f67260a3cad5e02"
+
+graphql@^14.1.1:
+ version "14.1.1"
+ resolved "https://registry.yarnpkg.com/graphql/-/graphql-14.1.1.tgz#d5d77df4b19ef41538d7215d1e7a28834619fac0"
+ dependencies:
+ iterall "^1.2.2"
+
+growly@^1.3.0:
+ version "1.3.0"
+ resolved "https://registry.yarnpkg.com/growly/-/growly-1.3.0.tgz#f10748cbe76af964b7c96c93c6bcc28af120c081"
+
+gud@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/gud/-/gud-1.0.0.tgz#a489581b17e6a70beca9abe3ae57de7a499852c0"
+
+gzip-size@^5.0.0:
+ version "5.0.0"
+ resolved "https://registry.yarnpkg.com/gzip-size/-/gzip-size-5.0.0.tgz#a55ecd99222f4c48fd8c01c625ce3b349d0a0e80"
+ dependencies:
+ duplexer "^0.1.1"
+ pify "^3.0.0"
+
+handle-thing@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/handle-thing/-/handle-thing-2.0.0.tgz#0e039695ff50c93fc288557d696f3c1dc6776754"
+
+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.0.3:
+ version "5.0.3"
+ resolved "https://registry.yarnpkg.com/har-validator/-/har-validator-5.0.3.tgz#ba402c266194f15956ef15e0fcf242993f6a7dfd"
+ dependencies:
+ ajv "^5.1.0"
+ har-schema "^2.0.0"
+
+harmony-reflect@^1.4.2:
+ version "1.6.0"
+ resolved "https://registry.yarnpkg.com/harmony-reflect/-/harmony-reflect-1.6.0.tgz#9c28a77386ec225f7b5d370f9861ba09c4eea58f"
+
+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-symbols@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/has-symbols/-/has-symbols-1.0.0.tgz#ba1a8f1af2a0fc39650f5c850367704122063b44"
+
+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.3:
+ version "1.0.3"
+ resolved "https://registry.yarnpkg.com/has/-/has-1.0.3.tgz#722d7cbfc1f6aa8241f16dd814e011e1f41e8796"
+ dependencies:
+ function-bind "^1.1.1"
+
+has@^1.0.1:
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/has/-/has-1.0.1.tgz#8461733f538b0837c9361e39a9ab9e9704dc2f28"
+ dependencies:
+ function-bind "^1.0.2"
+
+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.3"
+ resolved "https://registry.yarnpkg.com/hash.js/-/hash.js-1.1.3.tgz#340dedbe6290187151c1ea1d777a3448935df846"
+ dependencies:
+ inherits "^2.0.3"
+ minimalistic-assert "^1.0.0"
+
+hasha@^2.2.0:
+ version "2.2.0"
+ resolved "https://registry.yarnpkg.com/hasha/-/hasha-2.2.0.tgz#78d7cbfc1e6d66303fe79837365984517b2f6ee1"
+ dependencies:
+ is-stream "^1.0.1"
+ pinkie-promise "^2.0.0"
+
+hawk@~6.0.2:
+ version "6.0.2"
+ resolved "https://registry.yarnpkg.com/hawk/-/hawk-6.0.2.tgz#af4d914eb065f9b5ce4d9d11c1cb2126eecc3038"
+ dependencies:
+ boom "4.x.x"
+ cryptiles "3.x.x"
+ hoek "4.x.x"
+ sntp "2.x.x"
+
+he@1.1.x:
+ version "1.1.1"
+ resolved "https://registry.yarnpkg.com/he/-/he-1.1.1.tgz#93410fd21b009735151f8868c2f271f3427e23fd"
+
+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"
+
+history@^4.7.2:
+ version "4.7.2"
+ resolved "https://registry.yarnpkg.com/history/-/history-4.7.2.tgz#22b5c7f31633c5b8021c7f4a8a954ac139ee8d5b"
+ dependencies:
+ invariant "^2.2.1"
+ loose-envify "^1.2.0"
+ resolve-pathname "^2.2.0"
+ value-equal "^0.4.0"
+ warning "^3.0.0"
+
+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"
+
+hoek@4.x.x:
+ version "4.2.1"
+ resolved "https://registry.yarnpkg.com/hoek/-/hoek-4.2.1.tgz#9634502aa12c445dd5a7c5734b572bb8738aacbb"
+
+hoist-non-react-statics@^2.3.1, hoist-non-react-statics@^2.5.0:
+ version "2.5.5"
+ resolved "https://registry.yarnpkg.com/hoist-non-react-statics/-/hoist-non-react-statics-2.5.5.tgz#c5903cf409c0dfd908f388e619d86b9c1174cb47"
+
+hoist-non-react-statics@^3.0.0:
+ version "3.2.1"
+ resolved "https://registry.yarnpkg.com/hoist-non-react-statics/-/hoist-non-react-statics-3.2.1.tgz#c09c0555c84b38a7ede6912b61efddafd6e75e1e"
+ dependencies:
+ react-is "^16.3.2"
+
+hoist-non-react-statics@^3.2.1, hoist-non-react-statics@^3.3.0:
+ version "3.3.0"
+ resolved "https://registry.yarnpkg.com/hoist-non-react-statics/-/hoist-non-react-statics-3.3.0.tgz#b09178f0122184fb95acf525daaecb4d8f45958b"
+ dependencies:
+ react-is "^16.7.0"
+
+home-or-tmp@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/home-or-tmp/-/home-or-tmp-2.0.0.tgz#e36c3f2d2cae7d746a857e38d18d5f32a7882db8"
+ dependencies:
+ os-homedir "^1.0.0"
+ os-tmpdir "^1.0.1"
+
+homedir-polyfill@^1.0.1:
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/homedir-polyfill/-/homedir-polyfill-1.0.1.tgz#4c2bbc8a758998feebf5ed68580f76d46768b4bc"
+ dependencies:
+ parse-passwd "^1.0.0"
+
+hoopy@^0.1.2:
+ version "0.1.4"
+ resolved "https://registry.yarnpkg.com/hoopy/-/hoopy-0.1.4.tgz#609207d661100033a9a9402ad3dea677381c1b1d"
+
+hosted-git-info@^2.1.4:
+ version "2.6.0"
+ resolved "https://registry.yarnpkg.com/hosted-git-info/-/hosted-git-info-2.6.0.tgz#23235b29ab230c576aab0d4f13fc046b0b038222"
+
+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.1"
+ resolved "https://registry.yarnpkg.com/html-comment-regex/-/html-comment-regex-1.1.1.tgz#668b93776eaae55ebde8f3ad464b307a4963625e"
+
+html-entities@^1.2.0:
+ version "1.2.1"
+ resolved "https://registry.yarnpkg.com/html-entities/-/html-entities-1.2.1.tgz#0df29351f0721163515dfb9e5543e5f6eed5162f"
+
+html-minifier@^3.2.3:
+ version "3.5.15"
+ resolved "https://registry.yarnpkg.com/html-minifier/-/html-minifier-3.5.15.tgz#f869848d4543cbfd84f26d5514a2a87cbf9a05e0"
+ dependencies:
+ camel-case "3.0.x"
+ clean-css "4.1.x"
+ commander "2.15.x"
+ he "1.1.x"
+ param-case "2.1.x"
+ relateurl "0.2.x"
+ uglify-js "3.3.x"
+
+html-to-draftjs@^1.4.0:
+ version "1.4.0"
+ resolved "https://registry.yarnpkg.com/html-to-draftjs/-/html-to-draftjs-1.4.0.tgz#8a3cbbba5b49d50be8ce85cc08b112d5bf00fc1d"
+
+html-webpack-plugin@^3.2.0:
+ version "3.2.0"
+ resolved "https://registry.yarnpkg.com/html-webpack-plugin/-/html-webpack-plugin-3.2.0.tgz#b01abbd723acaaa7b37b6af4492ebda03d9dd37b"
+ dependencies:
+ html-minifier "^3.2.3"
+ loader-utils "^0.2.16"
+ lodash "^4.17.3"
+ pretty-error "^2.0.2"
+ tapable "^1.0.0"
+ toposort "^1.0.0"
+ util.promisify "1.0.0"
+
+htmlparser2@~3.3.0:
+ version "3.3.0"
+ resolved "https://registry.yarnpkg.com/htmlparser2/-/htmlparser2-3.3.0.tgz#cc70d05a59f6542e43f0e685c982e14c924a9efe"
+ dependencies:
+ domelementtype "1"
+ domhandler "2.1"
+ domutils "1.1"
+ readable-stream "1.0"
+
+htmlparser2@~3.8.1:
+ version "3.8.3"
+ resolved "https://registry.yarnpkg.com/htmlparser2/-/htmlparser2-3.8.3.tgz#996c28b191516a8be86501a7d79757e5c70c1068"
+ dependencies:
+ domelementtype "1"
+ domhandler "2.3"
+ domutils "1.5"
+ entities "1.0"
+ readable-stream "1.1"
+
+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.6.2:
+ version "1.6.2"
+ resolved "https://registry.yarnpkg.com/http-errors/-/http-errors-1.6.2.tgz#0a002cc85707192a7e7946ceedc11155f60ec736"
+ dependencies:
+ depd "1.1.1"
+ inherits "2.0.3"
+ setprototypeof "1.0.3"
+ statuses ">= 1.3.1 < 2"
+
+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-parser-js@>=0.4.0:
+ version "0.4.12"
+ resolved "https://registry.yarnpkg.com/http-parser-js/-/http-parser-js-0.4.12.tgz#b9cfbf4a2cf26f0fc34b10ca1489a27771e3474f"
+
+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"
+ 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.17.0"
+ resolved "https://registry.yarnpkg.com/http-proxy/-/http-proxy-1.17.0.tgz#7ad38494658f84605e2f6db4436df410f4e5be9a"
+ dependencies:
+ eventemitter3 "^3.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"
+
+https-proxy-agent@^2.2.1:
+ version "2.2.1"
+ resolved "https://registry.yarnpkg.com/https-proxy-agent/-/https-proxy-agent-2.2.1.tgz#51552970fa04d723e04c56d04178c3f92592bbc0"
+ dependencies:
+ agent-base "^4.1.0"
+ debug "^3.1.0"
+
+hyphenate-style-name@^1.0.2:
+ version "1.0.2"
+ resolved "https://registry.yarnpkg.com/hyphenate-style-name/-/hyphenate-style-name-1.0.2.tgz#31160a36930adaf1fc04c6074f7eb41465d4ec4b"
+
+iconv-lite@0.4.19:
+ version "0.4.19"
+ resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.19.tgz#f7468f60135f5e5dad3399c0a81be9a1603a082b"
+
+iconv-lite@^0.4.24:
+ 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"
+
+iconv-lite@^0.4.4, iconv-lite@~0.4.13:
+ version "0.4.23"
+ resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.23.tgz#297871f63be507adcfbfca715d0cd0eed84e9a63"
+ dependencies:
+ safer-buffer ">= 2.1.2 < 3"
+
+icss-replace-symbols@^1.1.0:
+ version "1.1.0"
+ resolved "https://registry.yarnpkg.com/icss-replace-symbols/-/icss-replace-symbols-1.1.0.tgz#06ea6f83679a7749e386cfe1fe812ae5db223ded"
+
+icss-utils@^2.1.0:
+ version "2.1.0"
+ resolved "https://registry.yarnpkg.com/icss-utils/-/icss-utils-2.1.0.tgz#83f0a0ec378bf3246178b6c2ad9136f135b1c962"
+ dependencies:
+ postcss "^6.0.1"
+
+ieee754@^1.1.4:
+ version "1.1.11"
+ resolved "https://registry.yarnpkg.com/ieee754/-/ieee754-1.1.11.tgz#c16384ffe00f5b7835824e67b6f2bd44a5229455"
+
+iferr@^0.1.5:
+ version "0.1.5"
+ resolved "https://registry.yarnpkg.com/iferr/-/iferr-0.1.5.tgz#c60eed69e6d8fdb6b3104a1fcbca1c192dc5b501"
+
+iframe-resizer@^3.6.5:
+ version "3.6.5"
+ resolved "https://registry.yarnpkg.com/iframe-resizer/-/iframe-resizer-3.6.5.tgz#78a6c449bf186d2ed5f4c37d1f1ae19c7044145c"
+
+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.3, ignore@^4.0.6:
+ version "4.0.6"
+ resolved "https://registry.yarnpkg.com/ignore/-/ignore-4.0.6.tgz#750e3db5862087b4737ebac8207ffd1ef27b25fc"
+
+image-size@^0.4.0:
+ version "0.4.0"
+ resolved "https://registry.yarnpkg.com/image-size/-/image-size-0.4.0.tgz#d4b4e1f61952e4cbc1cea9a6b0c915fecb707510"
+
+image-size@^0.5.0:
+ version "0.5.5"
+ resolved "https://registry.yarnpkg.com/image-size/-/image-size-0.5.5.tgz#09dfd4ab9d20e29eb1c3e80b8990378df9e3cb9c"
+
+imageoptim-cli@2.3.5:
+ version "2.3.5"
+ resolved "https://registry.yarnpkg.com/imageoptim-cli/-/imageoptim-cli-2.3.5.tgz#f08c4e3015662e740f7b62b1d9fd1a64bec96675"
+ dependencies:
+ chalk "2.4.2"
+ commander "2.19.0"
+ execa "1.0.0"
+ fs-extra "7.0.1"
+ globby "9.0.0"
+ pretty-bytes "5.1.0"
+
+immutable-tuple@^0.4.9:
+ version "0.4.9"
+ resolved "https://registry.yarnpkg.com/immutable-tuple/-/immutable-tuple-0.4.9.tgz#473ebdd6c169c461913a454bf87ef8f601a20ff0"
+
+immutable@~3.7.4:
+ version "3.7.6"
+ resolved "https://registry.yarnpkg.com/immutable/-/immutable-3.7.6.tgz#13b4d3cb12befa15482a26fe1b2ebae640071e4b"
+
+import-fresh@^3.0.0:
+ version "3.0.0"
+ resolved "https://registry.yarnpkg.com/import-fresh/-/import-fresh-3.0.0.tgz#a3d897f420cab0e671236897f75bc14b4885c390"
+ dependencies:
+ parent-module "^1.0.0"
+ resolve-from "^4.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"
+ 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"
+
+indexes-of@^1.0.1:
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/indexes-of/-/indexes-of-1.0.1.tgz#f30f716c8e2bd346c7b67d3df3915566a7c05607"
+
+indexof@0.0.1:
+ version "0.0.1"
+ resolved "https://registry.yarnpkg.com/indexof/-/indexof-0.0.1.tgz#82dc336d232b9062179d05ab3293a66059fd435d"
+
+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.3, inherits@^2.0.1, inherits@^2.0.3, inherits@~2.0.1, inherits@~2.0.3:
+ version "2.0.3"
+ resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.3.tgz#633c2c83e3da42a502f52466022480f4208261de"
+
+inherits@2.0.1:
+ version "2.0.1"
+ resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.1.tgz#b17d08d326b4423e568eff719f91b0b1cbdf69f1"
+
+ini@^1.3.4, ini@~1.3.0:
+ version "1.3.5"
+ resolved "https://registry.yarnpkg.com/ini/-/ini-1.3.5.tgz#eee25f56db1c9ec6085e0c22778083f596abf927"
+
+inquirer@^6.2.2:
+ version "6.2.2"
+ resolved "https://registry.yarnpkg.com/inquirer/-/inquirer-6.2.2.tgz#46941176f65c9eb20804627149b743a218f25406"
+ dependencies:
+ ansi-escapes "^3.2.0"
+ chalk "^2.4.2"
+ cli-cursor "^2.1.0"
+ cli-width "^2.0.0"
+ external-editor "^3.0.3"
+ figures "^2.0.0"
+ lodash "^4.17.11"
+ mute-stream "0.0.7"
+ run-async "^2.2.0"
+ rxjs "^6.4.0"
+ string-width "^2.1.0"
+ strip-ansi "^5.0.0"
+ through "^2.3.6"
+
+internal-ip@^4.2.0:
+ version "4.2.0"
+ resolved "https://registry.yarnpkg.com/internal-ip/-/internal-ip-4.2.0.tgz#46e81b638d84c338e5c67e42b1a17db67d0814fa"
+ dependencies:
+ default-gateway "^4.0.1"
+ ipaddr.js "^1.9.0"
+
+interpret@^1.0.0, interpret@^1.1.0:
+ version "1.1.0"
+ resolved "https://registry.yarnpkg.com/interpret/-/interpret-1.1.0.tgz#7ed1b1410c6a0e0f78cf95d3b8440c63f78b8614"
+
+invariant@^2.2.0, invariant@^2.2.1, 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"
+
+invert-kv@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/invert-kv/-/invert-kv-1.0.0.tgz#104a8e4aaca6d3d8cd157a8ef8bfab2d7a3ffdb6"
+
+invert-kv@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/invert-kv/-/invert-kv-2.0.0.tgz#7393f5afa59ec9ff5f67a27620d11c226e3eec02"
+
+ip-regex@^1.0.1:
+ version "1.0.3"
+ resolved "https://registry.yarnpkg.com/ip-regex/-/ip-regex-1.0.3.tgz#dc589076f659f419c222039a33316f1c7387effd"
+
+ip-regex@^2.1.0:
+ version "2.1.0"
+ resolved "https://registry.yarnpkg.com/ip-regex/-/ip-regex-2.1.0.tgz#fa78bf5d2e6913c911ce9f819ee5146bb6d844e9"
+
+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.6.0:
+ version "1.6.0"
+ resolved "https://registry.yarnpkg.com/ipaddr.js/-/ipaddr.js-1.6.0.tgz#e3fa357b773da619f26e95f049d055c72796f86b"
+
+ipaddr.js@^1.9.0:
+ version "1.9.0"
+ resolved "https://registry.yarnpkg.com/ipaddr.js/-/ipaddr.js-1.9.0.tgz#37df74e430a0e47550fe54a2defe30d8acd95f65"
+
+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-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-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-buffer@^1.1.5:
+ version "1.1.6"
+ resolved "https://registry.yarnpkg.com/is-buffer/-/is-buffer-1.1.6.tgz#efaa2ea9daa0d7ab2ea13a97b2b8ad51fefbe8be"
+
+is-builtin-module@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/is-builtin-module/-/is-builtin-module-1.0.0.tgz#540572d34f7ac3119f8f76c30cbc1b1e037affbe"
+ dependencies:
+ builtin-modules "^1.0.0"
+
+is-callable@^1.1.1, is-callable@^1.1.3:
+ version "1.1.3"
+ resolved "https://registry.yarnpkg.com/is-callable/-/is-callable-1.1.3.tgz#86eb75392805ddc33af71c92a0eedf74ee7604b2"
+
+is-callable@^1.1.4:
+ version "1.1.4"
+ resolved "https://registry.npmjs.org/is-callable/-/is-callable-1.1.4.tgz#1e1adf219e1eeb684d691f9d6a05ff0d30a24d75"
+ integrity sha512-r5p9sxJjYnArLjObpjA4xu5EKI3CuKHkJXMhT7kwbpUyIFD1n5PMAsoPvWnvtZiNz7LjkYDRZhd7FlI0eMijEA==
+
+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-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-function@^1.0.1, is-function@~1.0.0:
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/is-function/-/is-function-1.0.1.tgz#12cfb98b65b57dd3d193a3121f5f6e2f437602b5"
+
+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:
+ version "4.0.0"
+ resolved "https://registry.yarnpkg.com/is-glob/-/is-glob-4.0.0.tgz#9521c76845cc2610a85203ddf080a958c2ffabc0"
+ dependencies:
+ is-extglob "^2.1.1"
+
+is-in-browser@^1.0.2, is-in-browser@^1.1.3:
+ version "1.1.3"
+ resolved "https://registry.yarnpkg.com/is-in-browser/-/is-in-browser-1.1.3.tgz#56ff4db683a078c6082eb95dad7dc62e1d04f835"
+
+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@^4.0.0:
+ version "4.0.0"
+ resolved "https://registry.yarnpkg.com/is-number/-/is-number-4.0.0.tgz#0026e37f5454d73e356dfe6564699867c6a7f0ff"
+
+is-obj@^1.0.0:
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/is-obj/-/is-obj-1.0.1.tgz#3e4729ac1f5fde025cd7d83a896dab9f4f67db0f"
+
+is-odd@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/is-odd/-/is-odd-2.0.0.tgz#7646624671fd7ea558ccd9a2795182f2958f1b24"
+ dependencies:
+ is-number "^4.0.0"
+
+is-path-cwd@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/is-path-cwd/-/is-path-cwd-1.0.0.tgz#d225ec23132e89edd38fda767472e62e65f1106d"
+
+is-path-in-cwd@^1.0.0:
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/is-path-in-cwd/-/is-path-in-cwd-1.0.1.tgz#5ac48b345ef675339bd6c7a48a912110b241cf52"
+ dependencies:
+ is-path-inside "^1.0.0"
+
+is-path-inside@^1.0.0:
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/is-path-inside/-/is-path-inside-1.0.1.tgz#8ef5b7de50437a3fdca6b4e865ef7aa55cb48036"
+ dependencies:
+ path-is-inside "^1.0.1"
+
+is-plain-object@^2.0.1, 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-promise@^2.1.0:
+ version "2.1.0"
+ resolved "https://registry.yarnpkg.com/is-promise/-/is-promise-2.1.0.tgz#79a2a9ece7f096e80f36d2b2f3bc16c1ff4bf3fa"
+
+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-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.0.1, is-stream@^1.1.0:
+ version "1.1.0"
+ resolved "https://registry.yarnpkg.com/is-stream/-/is-stream-1.1.0.tgz#12d4a3dd4e68e0b79ceb8dbc84173ae80d91ca44"
+
+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.1:
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/is-symbol/-/is-symbol-1.0.1.tgz#3cc59f00025194b6ab2e38dbae6689256b660572"
+
+is-symbol@^1.0.2:
+ version "1.0.2"
+ resolved "https://registry.npmjs.org/is-symbol/-/is-symbol-1.0.2.tgz#a055f6ae57192caee329e7a860118b497a950f38"
+ integrity sha512-HS8bZ9ox60yCJLH9snBpIwv9pYUAkcuLhSA1oero1UB5y9aiQpRA8y2ex945AOtCZL1lJDeIk3G5LthswI46Lw==
+ dependencies:
+ has-symbols "^1.0.0"
+
+is-touch-device@^1.0.1:
+ version "1.0.1"
+ resolved "https://registry.npmjs.org/is-touch-device/-/is-touch-device-1.0.1.tgz#9a2fd59f689e9a9bf6ae9a86924c4ba805a42eab"
+ integrity sha512-LAYzo9kMT1b2p19L/1ATGt2XcSilnzNlyvq6c0pbPRVisLbAPpLqr53tIJS00kvrTkj0HtR8U7+u8X0yR8lPSw==
+
+is-typedarray@~1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/is-typedarray/-/is-typedarray-1.0.0.tgz#e479c80858df0c1b11ddda6940f96011fcda4a9a"
+
+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@0.0.1:
+ version "0.0.1"
+ resolved "https://registry.yarnpkg.com/isarray/-/isarray-0.0.1.tgz#8a18acfca9a8f4177e09abfc6038939b05d1eedf"
+
+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"
+
+isomorphic-fetch@^2.1.1:
+ version "2.2.1"
+ resolved "https://registry.yarnpkg.com/isomorphic-fetch/-/isomorphic-fetch-2.2.1.tgz#611ae1acf14f5e81f729507472819fe9733558a9"
+ dependencies:
+ node-fetch "^1.0.1"
+ whatwg-fetch ">=0.10.0"
+
+isstream@~0.1.2:
+ version "0.1.2"
+ resolved "https://registry.yarnpkg.com/isstream/-/isstream-0.1.2.tgz#47e63f7af55afa6f92e1500e690eb8b8529c099a"
+
+iterall@^1.2.2:
+ version "1.2.2"
+ resolved "https://registry.yarnpkg.com/iterall/-/iterall-1.2.2.tgz#92d70deb8028e0c39ff3164fdbf4d8b088130cd7"
+
+jimp@^0.2.13, jimp@^0.2.21:
+ version "0.2.28"
+ resolved "https://registry.yarnpkg.com/jimp/-/jimp-0.2.28.tgz#dd529a937190f42957a7937d1acc3a7762996ea2"
+ dependencies:
+ bignumber.js "^2.1.0"
+ bmp-js "0.0.3"
+ es6-promise "^3.0.2"
+ exif-parser "^0.1.9"
+ file-type "^3.1.0"
+ jpeg-js "^0.2.0"
+ load-bmfont "^1.2.3"
+ mime "^1.3.4"
+ mkdirp "0.5.1"
+ pixelmatch "^4.0.0"
+ pngjs "^3.0.0"
+ read-chunk "^1.0.1"
+ request "^2.65.0"
+ stream-to-buffer "^0.1.0"
+ tinycolor2 "^1.1.2"
+ url-regex "^3.0.0"
+
+jpeg-js@^0.1.1:
+ version "0.1.2"
+ resolved "https://registry.yarnpkg.com/jpeg-js/-/jpeg-js-0.1.2.tgz#135b992c0575c985cfa0f494a3227ed238583ece"
+
+jpeg-js@^0.2.0:
+ version "0.2.0"
+ resolved "https://registry.yarnpkg.com/jpeg-js/-/jpeg-js-0.2.0.tgz#53e448ec9d263e683266467e9442d2c5a2ef5482"
+
+js-tokens@^3.0.0, "js-tokens@^3.0.0 || ^4.0.0", js-tokens@^3.0.2:
+ version "3.0.2"
+ resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-3.0.2.tgz#9866df395102130e38f7f996bceb65443209c25b"
+
+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.12.0, js-yaml@^3.9.0:
+ version "3.12.0"
+ resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-3.12.0.tgz#eaed656ec8344f10f527c6bfa1b6e2244de167d1"
+ dependencies:
+ argparse "^1.0.7"
+ esprima "^4.0.0"
+
+js-yaml@~3.10.0:
+ version "3.10.0"
+ resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-3.10.0.tgz#2e78441646bd4682e963f22b6e92823c309c62dc"
+ 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@^1.3.0:
+ version "1.3.0"
+ resolved "https://registry.yarnpkg.com/jsesc/-/jsesc-1.3.0.tgz#46c3fec8c1892b12b0833db9bc7622176dbab34b"
+
+jsesc@^2.5.1:
+ version "2.5.1"
+ resolved "https://registry.yarnpkg.com/jsesc/-/jsesc-2.5.1.tgz#e421a2a8e20d6b0819df28908f782526b96dd1fe"
+
+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-schema-traverse@^0.3.0:
+ version "0.3.1"
+ resolved "https://registry.yarnpkg.com/json-schema-traverse/-/json-schema-traverse-0.3.1.tgz#349a6d44c53a51de89b40805c5d5e59b417d3340"
+
+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@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"
+
+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.2:
+ version "3.3.2"
+ resolved "https://registry.yarnpkg.com/json3/-/json3-3.3.2.tgz#3c0434743df93e2f5c42aee7b19bcb483575f4e1"
+
+json5@^0.5.0, json5@^0.5.1:
+ version "0.5.1"
+ resolved "https://registry.yarnpkg.com/json5/-/json5-0.5.1.tgz#1eade7acc012034ad84e2396767ead9fa5495821"
+
+jsonfile@^2.1.0:
+ version "2.4.0"
+ resolved "https://registry.yarnpkg.com/jsonfile/-/jsonfile-2.4.0.tgz#3736a2b428b87bbda0cc83b53fa3d633a35c2ae8"
+ optionalDependencies:
+ graceful-fs "^4.1.6"
+
+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"
+
+jsontoxml@0.0.11:
+ version "0.0.11"
+ resolved "https://registry.yarnpkg.com/jsontoxml/-/jsontoxml-0.0.11.tgz#373ab5b2070be3737a5fb3e32fd1b7b81870caa4"
+
+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"
+
+jss-camel-case@^6.1.0:
+ version "6.1.0"
+ resolved "https://registry.yarnpkg.com/jss-camel-case/-/jss-camel-case-6.1.0.tgz#ccb1ff8d6c701c02a1fed6fb6fb6b7896e11ce44"
+ dependencies:
+ hyphenate-style-name "^1.0.2"
+
+jss-compose@^5.0.0:
+ version "5.0.0"
+ resolved "https://registry.yarnpkg.com/jss-compose/-/jss-compose-5.0.0.tgz#ce01b2e4521d65c37ea42cf49116e5f7ab596484"
+ dependencies:
+ warning "^3.0.0"
+
+jss-default-unit@^8.0.2:
+ version "8.0.2"
+ resolved "https://registry.yarnpkg.com/jss-default-unit/-/jss-default-unit-8.0.2.tgz#cc1e889bae4c0b9419327b314ab1c8e2826890e6"
+
+jss-expand@^5.3.0:
+ version "5.3.0"
+ resolved "https://registry.yarnpkg.com/jss-expand/-/jss-expand-5.3.0.tgz#02be076efe650125c842f5bb6fb68786fe441ed6"
+
+jss-extend@^6.2.0:
+ version "6.2.0"
+ resolved "https://registry.yarnpkg.com/jss-extend/-/jss-extend-6.2.0.tgz#4af09d0b72fb98ee229970f8ca852fec1ca2a8dc"
+ dependencies:
+ warning "^3.0.0"
+
+jss-global@^3.0.0:
+ version "3.0.0"
+ resolved "https://registry.yarnpkg.com/jss-global/-/jss-global-3.0.0.tgz#e19e5c91ab2b96353c227e30aa2cbd938cdaafa2"
+
+jss-nested@^6.0.1:
+ version "6.0.1"
+ resolved "https://registry.yarnpkg.com/jss-nested/-/jss-nested-6.0.1.tgz#ef992b79d6e8f63d939c4397b9d99b5cbbe824ca"
+ dependencies:
+ warning "^3.0.0"
+
+jss-preset-default@^4.3.0:
+ version "4.5.0"
+ resolved "https://registry.yarnpkg.com/jss-preset-default/-/jss-preset-default-4.5.0.tgz#d3a457012ccd7a551312014e394c23c4b301cadd"
+ dependencies:
+ jss-camel-case "^6.1.0"
+ jss-compose "^5.0.0"
+ jss-default-unit "^8.0.2"
+ jss-expand "^5.3.0"
+ jss-extend "^6.2.0"
+ jss-global "^3.0.0"
+ jss-nested "^6.0.1"
+ jss-props-sort "^6.0.0"
+ jss-template "^1.0.1"
+ jss-vendor-prefixer "^7.0.0"
+
+jss-props-sort@^6.0.0:
+ version "6.0.0"
+ resolved "https://registry.yarnpkg.com/jss-props-sort/-/jss-props-sort-6.0.0.tgz#9105101a3b5071fab61e2d85ea74cc22e9b16323"
+
+jss-template@^1.0.1:
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/jss-template/-/jss-template-1.0.1.tgz#09aed9d86cc547b07f53ef355d7e1777f7da430a"
+ dependencies:
+ warning "^3.0.0"
+
+jss-vendor-prefixer@^7.0.0:
+ version "7.0.0"
+ resolved "https://registry.yarnpkg.com/jss-vendor-prefixer/-/jss-vendor-prefixer-7.0.0.tgz#0166729650015ef19d9f02437c73667231605c71"
+ dependencies:
+ css-vendor "^0.3.8"
+
+jss@^9.7.0:
+ version "9.8.1"
+ resolved "https://registry.yarnpkg.com/jss/-/jss-9.8.1.tgz#e2ff250777ad657430e6edc47a63516541b888fa"
+ dependencies:
+ is-in-browser "^1.1.3"
+ symbol-observable "^1.1.0"
+ warning "^3.0.0"
+
+jsx-ast-utils@^2.0.1:
+ version "2.0.1"
+ resolved "https://registry.yarnpkg.com/jsx-ast-utils/-/jsx-ast-utils-2.0.1.tgz#e801b1b39985e20fffc87b40e3748080e2dcac7f"
+ dependencies:
+ array-includes "^3.0.3"
+
+jump.js@^1.0.2:
+ version "1.0.2"
+ resolved "https://registry.yarnpkg.com/jump.js/-/jump.js-1.0.2.tgz#e0641b47f40a38f2139c25fda0500bf28e43015a"
+
+kew@^0.7.0:
+ version "0.7.0"
+ resolved "https://registry.yarnpkg.com/kew/-/kew-0.7.0.tgz#79d93d2d33363d6fdd2970b335d9141ad591d79b"
+
+killable@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/killable/-/killable-1.0.0.tgz#da8b84bd47de5395878f95d64d02f2449fe05e6b"
+
+kind-of@^3.0.2, kind-of@^3.0.3, kind-of@^3.2.0:
+ version "3.2.2"
+ resolved "https://registry.yarnpkg.com/kind-of/-/kind-of-3.2.2.tgz#31ea21a734bab9bbb0f32466d893aea51e4a3c64"
+ dependencies:
+ is-buffer "^1.1.5"
+
+kind-of@^4.0.0:
+ version "4.0.0"
+ resolved "https://registry.yarnpkg.com/kind-of/-/kind-of-4.0.0.tgz#20813df3d712928b207378691a45066fae72dd57"
+ dependencies:
+ is-buffer "^1.1.5"
+
+kind-of@^5.0.0:
+ version "5.1.0"
+ resolved "https://registry.yarnpkg.com/kind-of/-/kind-of-5.1.0.tgz#729c91e2d857b7a419a1f9aa65685c4c33f5845d"
+
+kind-of@^6.0.0, kind-of@^6.0.2:
+ version "6.0.2"
+ resolved "https://registry.yarnpkg.com/kind-of/-/kind-of-6.0.2.tgz#01146b36a6218e64e58f3a8d66de5d7fc6f6d051"
+
+klaw@^1.0.0:
+ version "1.3.1"
+ resolved "https://registry.yarnpkg.com/klaw/-/klaw-1.3.1.tgz#4088433b46b3b1ba259d78785d8e96f73ba02439"
+ optionalDependencies:
+ graceful-fs "^4.1.9"
+
+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"
+ dependencies:
+ lodash "^4.17.5"
+ webpack-sources "^1.1.0"
+
+lcid@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/lcid/-/lcid-1.0.0.tgz#308accafa0bc483a3867b4b6f2b9506251d1b835"
+ dependencies:
+ invert-kv "^1.0.0"
+
+lcid@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/lcid/-/lcid-2.0.0.tgz#6ef5d2df60e52f82eb228a4c373e8d1f397253cf"
+ dependencies:
+ invert-kv "^2.0.0"
+
+levn@^0.3.0, levn@~0.3.0:
+ version "0.3.0"
+ resolved "https://registry.yarnpkg.com/levn/-/levn-0.3.0.tgz#3b09924edf9f083c0490fdd4c0bc4421e04764ee"
+ dependencies:
+ prelude-ls "~1.1.2"
+ type-check "~0.3.2"
+
+load-bmfont@^1.2.3:
+ version "1.3.0"
+ resolved "https://registry.yarnpkg.com/load-bmfont/-/load-bmfont-1.3.0.tgz#bb7e7c710de6bcafcb13cb3b8c81e0c0131ecbc9"
+ dependencies:
+ buffer-equal "0.0.1"
+ mime "^1.3.4"
+ parse-bmfont-ascii "^1.0.3"
+ parse-bmfont-binary "^1.0.5"
+ parse-bmfont-xml "^1.1.0"
+ xhr "^2.0.1"
+ xtend "^4.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-fs-cache@^1.0.0:
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/loader-fs-cache/-/loader-fs-cache-1.0.1.tgz#56e0bf08bd9708b26a765b68509840c8dec9fdbc"
+ dependencies:
+ find-cache-dir "^0.1.1"
+ mkdirp "0.5.1"
+
+loader-runner@^2.3.0:
+ version "2.3.0"
+ resolved "https://registry.yarnpkg.com/loader-runner/-/loader-runner-2.3.0.tgz#f482aea82d543e07921700d5a46ef26fdac6b8a2"
+
+loader-utils@^0.2.14, loader-utils@^0.2.16:
+ version "0.2.17"
+ resolved "https://registry.yarnpkg.com/loader-utils/-/loader-utils-0.2.17.tgz#f86e6374d43205a6e6c60e9196f17c0299bfb348"
+ dependencies:
+ big.js "^3.1.3"
+ emojis-list "^2.0.0"
+ json5 "^0.5.0"
+ object-assign "^4.0.1"
+
+loader-utils@^1.0.2, loader-utils@^1.1.0:
+ version "1.1.0"
+ resolved "https://registry.yarnpkg.com/loader-utils/-/loader-utils-1.1.0.tgz#c98aef488bcceda2ffb5e2de646d6a754429f5cd"
+ dependencies:
+ big.js "^3.1.3"
+ emojis-list "^2.0.0"
+ json5 "^0.5.0"
+
+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"
+ dependencies:
+ p-locate "^3.0.0"
+ path-exists "^3.0.0"
+
+lodash-webpack-plugin@^0.11.5:
+ version "0.11.5"
+ resolved "https://registry.yarnpkg.com/lodash-webpack-plugin/-/lodash-webpack-plugin-0.11.5.tgz#c4bd064b4f561c3f823fa5982bdeb12c475390b9"
+ dependencies:
+ lodash "^4.17.4"
+
+lodash._getnative@^3.0.0:
+ version "3.9.1"
+ resolved "https://registry.yarnpkg.com/lodash._getnative/-/lodash._getnative-3.9.1.tgz#570bc7dede46d61cdcde687d65d3eecbaa3aaff5"
+
+lodash.curry@^4.0.1:
+ version "4.1.1"
+ resolved "https://registry.yarnpkg.com/lodash.curry/-/lodash.curry-4.1.1.tgz#248e36072ede906501d75966200a86dab8b23170"
+
+lodash.debounce@^4.0.8:
+ version "4.0.8"
+ resolved "https://registry.npmjs.org/lodash.debounce/-/lodash.debounce-4.0.8.tgz#82d79bff30a67c4005ffd5e2515300ad9ca4d7af"
+ integrity sha1-gteb/zCmfEAF/9XiUVMArZyk168=
+
+lodash.flow@^3.3.0:
+ version "3.5.0"
+ resolved "https://registry.yarnpkg.com/lodash.flow/-/lodash.flow-3.5.0.tgz#87bf40292b8cf83e4e8ce1a3ae4209e20071675a"
+
+lodash.flowright@^3.5.0:
+ version "3.5.0"
+ resolved "https://registry.yarnpkg.com/lodash.flowright/-/lodash.flowright-3.5.0.tgz#2b5fff399716d7e7dc5724fe9349f67065184d67"
+
+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.isequal@^4.5.0:
+ version "4.5.0"
+ resolved "https://registry.yarnpkg.com/lodash.isequal/-/lodash.isequal-4.5.0.tgz#415c4478f2bcc30120c22ce10ed3226f7d3e18e0"
+
+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.merge@^4.6.1:
+ version "4.6.1"
+ resolved "https://registry.yarnpkg.com/lodash.merge/-/lodash.merge-4.6.1.tgz#adc25d9cb99b9391c59624f379fbba60d7111d54"
+
+lodash.throttle@^4.1.1:
+ version "4.1.1"
+ resolved "https://registry.yarnpkg.com/lodash.throttle/-/lodash.throttle-4.1.1.tgz#c23e91b710242ac70c37f1e1cda9274cc39bf2f4"
+
+lodash.uniq@^4.5.0:
+ version "4.5.0"
+ resolved "https://registry.yarnpkg.com/lodash.uniq/-/lodash.uniq-4.5.0.tgz#d0225373aeb652adc1bc82e4945339a842754773"
+
+lodash@^3.2.0:
+ version "3.10.1"
+ resolved "https://registry.yarnpkg.com/lodash/-/lodash-3.10.1.tgz#5bf45e8e49ba4189e17d482789dfd15bd140b7b6"
+
+lodash@^4.0.1, lodash@^4.11.1, lodash@^4.17.10, lodash@^4.17.11, lodash@^4.17.3, lodash@^4.17.4, lodash@^4.17.5, lodash@^4.2.0:
+ version "4.17.11"
+ resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.11.tgz#b39ea6229ef607ecd89e2c8df12536891cac9b8d"
+
+lodash@^4.1.1, lodash@~4.17.4:
+ version "4.17.15"
+ resolved "https://registry.npmjs.org/lodash/-/lodash-4.17.15.tgz#b447f6670a0455bbfeedd11392eff330ea097548"
+ integrity sha512-8xOcRHvCjnocdS5cpwXQXVzmmh5e5+saE2QGoeQmbKmRS6J3VQppPOIt0MnmE+4xlZoumy0GPG0D0MVIQbNA1A==
+
+lodash@~2.4.1:
+ version "2.4.2"
+ resolved "https://registry.yarnpkg.com/lodash/-/lodash-2.4.2.tgz#fadd834b9683073da179b3eae6d9c0d15053f73e"
+
+loglevel@^1.4.1:
+ version "1.6.1"
+ resolved "https://registry.yarnpkg.com/loglevel/-/loglevel-1.6.1.tgz#e0fc95133b6ef276cdc8887cdaf24aa6f156f8fa"
+
+loose-envify@^1.0.0, loose-envify@^1.1.0, loose-envify@^1.2.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"
+
+lower-case@^1.1.1:
+ version "1.1.4"
+ resolved "https://registry.yarnpkg.com/lower-case/-/lower-case-1.1.4.tgz#9a2cabd1b9e8e0ae993a4bf7d5875c39c42e8eac"
+
+lru-cache@^4.0.1, lru-cache@^4.1.3:
+ version "4.1.3"
+ resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-4.1.3.tgz#a1175cf3496dfc8436c156c334b4955992bce69c"
+ dependencies:
+ pseudomap "^1.0.2"
+ yallist "^2.1.2"
+
+make-dir@^1.0.0:
+ version "1.2.0"
+ resolved "https://registry.yarnpkg.com/make-dir/-/make-dir-1.2.0.tgz#6d6a49eead4aae296c53bbf3a1a008bd6c89469b"
+ dependencies:
+ pify "^3.0.0"
+
+mamacro@^0.0.3:
+ version "0.0.3"
+ resolved "https://registry.yarnpkg.com/mamacro/-/mamacro-0.0.3.tgz#ad2c9576197c9f1abf308d0787865bd975a3f3e4"
+
+map-age-cleaner@^0.1.1:
+ version "0.1.2"
+ resolved "https://registry.yarnpkg.com/map-age-cleaner/-/map-age-cleaner-0.1.2.tgz#098fb15538fd3dbe461f12745b0ca8568d4e3f74"
+ dependencies:
+ p-defer "^1.0.0"
+
+map-cache@^0.2.2:
+ version "0.2.2"
+ resolved "https://registry.yarnpkg.com/map-cache/-/map-cache-0.2.2.tgz#c32abd0bd6525d9b051645bb4f26ac5dc98a0dbf"
+
+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"
+
+material-colors@^1.2.1:
+ version "1.2.6"
+ resolved "https://registry.yarnpkg.com/material-colors/-/material-colors-1.2.6.tgz#6d1958871126992ceecc72f4bcc4d8f010865f46"
+
+math-expression-evaluator@^1.2.14:
+ version "1.2.17"
+ resolved "https://registry.npmjs.org/math-expression-evaluator/-/math-expression-evaluator-1.2.17.tgz#de819fdbcd84dccd8fae59c6aeb79615b9d266ac"
+ integrity sha1-3oGf282E3M2PrlnGrreWFbnSZqw=
+
+md5.js@^1.3.4:
+ version "1.3.4"
+ resolved "https://registry.yarnpkg.com/md5.js/-/md5.js-1.3.4.tgz#e9bdbde94a20a5ac18b04340fc5764d5b09d901d"
+ dependencies:
+ hash-base "^3.0.0"
+ inherits "^2.0.1"
+
+mdn-data@^1.0.0, 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"
+
+mem@^4.0.0:
+ version "4.0.0"
+ resolved "https://registry.yarnpkg.com/mem/-/mem-4.0.0.tgz#6437690d9471678f6cc83659c00cbafcd6b0cdaf"
+ dependencies:
+ map-age-cleaner "^0.1.1"
+ mimic-fn "^1.0.0"
+ p-is-promise "^1.1.0"
+
+memory-fs@^0.2.0:
+ version "0.2.0"
+ resolved "https://registry.yarnpkg.com/memory-fs/-/memory-fs-0.2.0.tgz#f2bb25368bc121e391c2520de92969caee0a0290"
+
+memory-fs@^0.4.0, memory-fs@^0.4.1, memory-fs@~0.4.1:
+ version "0.4.1"
+ resolved "https://registry.yarnpkg.com/memory-fs/-/memory-fs-0.4.1.tgz#3a9a20b8462523e447cfbc7e8bb80ed667bfc552"
+ dependencies:
+ errno "^0.1.3"
+ readable-stream "^2.0.1"
+
+merge-defaults@^0.2.1:
+ version "0.2.1"
+ resolved "https://registry.yarnpkg.com/merge-defaults/-/merge-defaults-0.2.1.tgz#dd42248eb96bb6a51521724321c72ff9583dde80"
+ dependencies:
+ lodash "~2.4.1"
+
+merge-descriptors@1.0.1:
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/merge-descriptors/-/merge-descriptors-1.0.1.tgz#b00aaa556dd8b44568150ec9d1b953f3f90cbb61"
+
+merge2@^1.2.3:
+ version "1.2.3"
+ resolved "https://registry.yarnpkg.com/merge2/-/merge2-1.2.3.tgz#7ee99dbd69bb6481689253f018488a1b902b0ed5"
+
+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, micromatch@^3.1.8:
+ 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"
+
+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.33.0 < 2", mime-db@~1.33.0:
+ version "1.33.0"
+ resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.33.0.tgz#a3492050a5cb9b63450541e39d9788d2272783db"
+
+mime-match@^1.0.2:
+ version "1.0.2"
+ resolved "https://registry.yarnpkg.com/mime-match/-/mime-match-1.0.2.tgz#3f87c31e9af1a5fd485fb9db134428b23bbb7ba8"
+ dependencies:
+ wildcard "^1.1.0"
+
+mime-types@^2.1.12, mime-types@~2.1.17, mime-types@~2.1.18:
+ version "2.1.18"
+ resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.18.tgz#6f323f60a83d11146f831ff11fd66e2fe5503bb8"
+ dependencies:
+ mime-db "~1.33.0"
+
+mime@1.4.1:
+ version "1.4.1"
+ resolved "https://registry.yarnpkg.com/mime/-/mime-1.4.1.tgz#121f9ebc49e3766f311a76e1fa1c8003c4b03aa6"
+
+mime@^1.3.4:
+ version "1.6.0"
+ resolved "https://registry.yarnpkg.com/mime/-/mime-1.6.0.tgz#32cd9e5c64553bd58d19a568af452acff04981b1"
+
+mime@^2.3.1:
+ version "2.3.1"
+ resolved "https://registry.yarnpkg.com/mime/-/mime-2.3.1.tgz#b1621c54d63b97c47d3cfe7f7215f7d64517c369"
+
+mimic-fn@^1.0.0:
+ version "1.2.0"
+ resolved "https://registry.yarnpkg.com/mimic-fn/-/mimic-fn-1.2.0.tgz#820c86a39334640e99516928bd03fca88057d022"
+
+min-document@^2.19.0:
+ version "2.19.0"
+ resolved "https://registry.yarnpkg.com/min-document/-/min-document-2.19.0.tgz#7bd282e3f5842ed295bb748cdd9f1ffa2c824685"
+ dependencies:
+ dom-walk "^0.1.0"
+
+mini-css-extract-plugin@^0.5.0:
+ version "0.5.0"
+ resolved "https://registry.yarnpkg.com/mini-css-extract-plugin/-/mini-css-extract-plugin-0.5.0.tgz#ac0059b02b9692515a637115b0cc9fed3a35c7b0"
+ dependencies:
+ loader-utils "^1.1.0"
+ schema-utils "^1.0.0"
+ webpack-sources "^1.1.0"
+
+minimalistic-assert@^1.0.0:
+ 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@^3.0.2, minimatch@^3.0.4:
+ 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:
+ version "0.0.8"
+ resolved "https://registry.yarnpkg.com/minimist/-/minimist-0.0.8.tgz#857fcabfc3397d2625b8228262e86aa7a011b05d"
+
+minimist@^1.2.0:
+ version "1.2.0"
+ resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.0.tgz#a35008b20f41383eec1fb914f4cd5df79a264284"
+
+minipass@^2.2.1, minipass@^2.2.4:
+ version "2.2.4"
+ resolved "https://registry.yarnpkg.com/minipass/-/minipass-2.2.4.tgz#03c824d84551ec38a8d1bb5bc350a5a30a354a40"
+ dependencies:
+ safe-buffer "^5.1.1"
+ yallist "^3.0.0"
+
+minizlib@^1.1.0:
+ version "1.1.0"
+ resolved "https://registry.yarnpkg.com/minizlib/-/minizlib-1.1.0.tgz#11e13658ce46bc3a70a267aac58359d1e0c29ceb"
+ dependencies:
+ minipass "^2.2.1"
+
+mississippi@^3.0.0:
+ version "3.0.0"
+ resolved "https://registry.yarnpkg.com/mississippi/-/mississippi-3.0.0.tgz#ea0a3291f97e0b5e8776b363d5f0a12d94c67022"
+ 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.1"
+ resolved "https://registry.yarnpkg.com/mixin-deep/-/mixin-deep-1.3.1.tgz#a49e7268dce1a0d9698e45326c5626df3543d0fe"
+ dependencies:
+ for-in "^1.0.2"
+ is-extendable "^1.0.1"
+
+mkdirp@0.5.0:
+ version "0.5.0"
+ resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-0.5.0.tgz#1d73076a6df986cd9344e15e71fcc05a4c9abf12"
+ dependencies:
+ minimist "0.0.8"
+
+mkdirp@0.5.1, mkdirp@0.5.x, mkdirp@^0.5.0, mkdirp@^0.5.1, mkdirp@~0.5.0, 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"
+
+moment@>=1.6.0, moment@^2.24.0:
+ version "2.24.0"
+ resolved "https://registry.npmjs.org/moment/-/moment-2.24.0.tgz#0d055d53f5052aa653c9f6eb68bb5d12bf5c2b5b"
+ integrity sha512-bV7f+6l2QigeBBZSM/6yTNq4P2fNpSWj/0e7jQcy87A8e7o2nAfP/34/2ky5Vw4B9S446EtIhodAzkFCcR4dQg==
+
+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@0.7.1:
+ version "0.7.1"
+ resolved "https://registry.yarnpkg.com/ms/-/ms-0.7.1.tgz#9cd13c03adbff25b65effde7ce864ee952017098"
+
+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"
+
+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"
+
+mute-stream@0.0.7:
+ version "0.0.7"
+ resolved "https://registry.yarnpkg.com/mute-stream/-/mute-stream-0.0.7.tgz#3075ce93bc21b8fab43e1bc4da7e8115ed1e7bab"
+
+namespace-emitter@^2.0.1:
+ version "2.0.1"
+ resolved "https://registry.yarnpkg.com/namespace-emitter/-/namespace-emitter-2.0.1.tgz#978d51361c61313b4e6b8cf6f3853d08dfa2b17c"
+
+nan@^2.9.2:
+ version "2.10.0"
+ resolved "https://registry.yarnpkg.com/nan/-/nan-2.10.0.tgz#96d0cd610ebd58d4b4de9cc0c6828cda99c7548f"
+
+nanomatch@^1.2.9:
+ version "1.2.9"
+ resolved "https://registry.yarnpkg.com/nanomatch/-/nanomatch-1.2.9.tgz#879f7150cb2dab7a471259066c104eee6e0fa7c2"
+ 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-odd "^2.0.0"
+ 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"
+
+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.0:
+ version "2.2.1"
+ resolved "https://registry.yarnpkg.com/needle/-/needle-2.2.1.tgz#b5e325bd3aae8c2678902fa296f729455d1d3a7d"
+ dependencies:
+ debug "^2.1.2"
+ iconv-lite "^0.4.4"
+ sax "^1.2.4"
+
+negotiator@0.6.1:
+ version "0.6.1"
+ resolved "https://registry.yarnpkg.com/negotiator/-/negotiator-0.6.1.tgz#2b327184e8992101177b28563fb5e7102acd0ca9"
+
+neo-async@^2.5.0:
+ version "2.5.1"
+ resolved "https://registry.yarnpkg.com/neo-async/-/neo-async-2.5.1.tgz#acb909e327b1e87ec9ef15f41b8a269512ad41ee"
+
+nice-try@^1.0.4:
+ version "1.0.4"
+ resolved "https://registry.yarnpkg.com/nice-try/-/nice-try-1.0.4.tgz#d93962f6c52f2c1558c0fbda6d512819f1efe1c4"
+
+no-case@^2.2.0:
+ version "2.3.2"
+ resolved "https://registry.yarnpkg.com/no-case/-/no-case-2.3.2.tgz#60b813396be39b3f1288a4c1ed5d1e7d28b464ac"
+ dependencies:
+ lower-case "^1.1.1"
+
+node-fetch@^1.0.1:
+ version "1.7.3"
+ resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-1.7.3.tgz#980f6f72d85211a5347c6b2bc18c5b84c3eb47ef"
+ dependencies:
+ encoding "^0.1.11"
+ is-stream "^1.0.1"
+
+node-fetch@^2.1.2:
+ version "2.3.0"
+ resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.3.0.tgz#1a1d940bbfb916a1d3e0219f037e89e71f8c5fa5"
+
+node-forge@0.7.5:
+ version "0.7.5"
+ resolved "https://registry.yarnpkg.com/node-forge/-/node-forge-0.7.5.tgz#6c152c345ce11c52f465c2abd957e8639cd674df"
+
+"node-libs-browser@^1.0.0 || ^2.0.0", node-libs-browser@^2.0.0:
+ version "2.1.0"
+ resolved "https://registry.yarnpkg.com/node-libs-browser/-/node-libs-browser-2.1.0.tgz#5f94263d404f6e44767d726901fff05478d600df"
+ 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 "^1.0.0"
+ https-browserify "^1.0.0"
+ os-browserify "^0.3.0"
+ path-browserify "0.0.0"
+ 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.10.3"
+ vm-browserify "0.0.4"
+
+node-notifier@^5.1.2:
+ version "5.2.1"
+ resolved "https://registry.yarnpkg.com/node-notifier/-/node-notifier-5.2.1.tgz#fa313dd08f5517db0e2502e5758d664ac69f9dea"
+ dependencies:
+ growly "^1.3.0"
+ semver "^5.4.1"
+ shellwords "^0.1.1"
+ which "^1.3.0"
+
+node-pre-gyp@^0.9.0:
+ version "0.9.1"
+ resolved "https://registry.yarnpkg.com/node-pre-gyp/-/node-pre-gyp-0.9.1.tgz#f11c07516dd92f87199dbc7e1838eab7cd56c9e0"
+ dependencies:
+ detect-libc "^1.0.2"
+ mkdirp "^0.5.1"
+ needle "^2.2.0"
+ nopt "^4.0.1"
+ npm-packlist "^1.1.6"
+ npmlog "^4.0.2"
+ rc "^1.1.7"
+ rimraf "^2.6.1"
+ semver "^5.3.0"
+ tar "^4"
+
+node-releases@^1.0.0-alpha.10:
+ version "1.0.0-alpha.10"
+ resolved "https://registry.yarnpkg.com/node-releases/-/node-releases-1.0.0-alpha.10.tgz#61c8d5f9b5b2e05d84eba941d05b6f5202f68a2a"
+ dependencies:
+ semver "^5.3.0"
+
+node-rest-client@^1.5.1:
+ version "1.8.0"
+ resolved "https://registry.yarnpkg.com/node-rest-client/-/node-rest-client-1.8.0.tgz#8d3c566b817e27394cb7273783a41caefe3e5955"
+ dependencies:
+ debug "~2.2.0"
+ xml2js ">=0.2.4"
+
+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:
+ version "2.4.0"
+ resolved "https://registry.yarnpkg.com/normalize-package-data/-/normalize-package-data-2.4.0.tgz#12f95a307d58352075a04907b84ac8be98ac012f"
+ dependencies:
+ hosted-git-info "^2.1.4"
+ is-builtin-module "^1.0.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-url@^3.0.0:
+ version "3.2.0"
+ resolved "https://registry.yarnpkg.com/normalize-url/-/normalize-url-3.2.0.tgz#98d0948afc82829f374320f405fe9ca55a5f8567"
+
+normalize.css@^8.0.1:
+ version "8.0.1"
+ resolved "https://registry.yarnpkg.com/normalize.css/-/normalize.css-8.0.1.tgz#9b98a208738b9cc2634caacbc42d131c97487bf3"
+
+npm-bundled@^1.0.1:
+ version "1.0.3"
+ resolved "https://registry.yarnpkg.com/npm-bundled/-/npm-bundled-1.0.3.tgz#7e71703d973af3370a9591bafe3a63aca0be2308"
+
+npm-packlist@^1.1.6:
+ version "1.1.10"
+ resolved "https://registry.yarnpkg.com/npm-packlist/-/npm-packlist-1.1.10.tgz#1039db9e985727e464df066f4cf0ab6ef85c398a"
+ 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@^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.1, nth-check@~1.0.0, nth-check@~1.0.1:
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/nth-check/-/nth-check-1.0.1.tgz#9929acdf628fc2c41098deab82ac580cf149aae4"
+ dependencies:
+ boolbase "~1.0.0"
+
+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"
+
+numbro@^2.1.2:
+ version "2.1.2"
+ resolved "https://registry.yarnpkg.com/numbro/-/numbro-2.1.2.tgz#2d51104f09b5d69aef7e15bb565d7795e47ecfd6"
+ integrity sha512-7w833BxZmKGLE9HI0aREtNVRVH6WTYUUlWf4qgA5gKNhPQ4F/MRZ14sc0v8eoLORprk9ZTVwYaLwj8N3Zgxwiw==
+ dependencies:
+ bignumber.js "^8.0.1"
+
+oauth-sign@~0.8.2:
+ version "0.8.2"
+ resolved "https://registry.yarnpkg.com/oauth-sign/-/oauth-sign-0.8.2.tgz#46a6ab7f0aead8deae9ec0565780b7d4efeb9d43"
+
+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-hash@^1.1.4:
+ version "1.3.0"
+ resolved "https://registry.yarnpkg.com/object-hash/-/object-hash-1.3.0.tgz#76d9ba6ff113cf8efc0d996102851fe6723963e2"
+
+object-is@^1.0.1:
+ version "1.0.1"
+ resolved "https://registry.npmjs.org/object-is/-/object-is-1.0.1.tgz#0aa60ec9989a0b3ed795cf4d06f62cf1ad6539b6"
+ integrity sha1-CqYOyZiaCz7Xlc9NBvYs8a1lObY=
+
+object-keys@^1.0.11, object-keys@^1.0.8:
+ version "1.0.11"
+ resolved "https://registry.yarnpkg.com/object-keys/-/object-keys-1.0.11.tgz#c54601778ad560f1142ce0e01bcca8b56d13426d"
+
+object-keys@^1.0.12:
+ version "1.1.1"
+ resolved "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz#1c47f272df277f3b1daf061677d9c82e2322c60e"
+ integrity sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==
+
+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:
+ version "4.1.0"
+ resolved "https://registry.yarnpkg.com/object.assign/-/object.assign-4.1.0.tgz#968bf1100d7956bb3ca086f006f846b3bc4008da"
+ dependencies:
+ define-properties "^1.1.2"
+ function-bind "^1.1.1"
+ has-symbols "^1.0.0"
+ object-keys "^1.0.11"
+
+object.entries@^1.0.4:
+ version "1.0.4"
+ resolved "https://registry.yarnpkg.com/object.entries/-/object.entries-1.0.4.tgz#1bf9a4dd2288f5b33f3a993d257661f05d161a5f"
+ dependencies:
+ define-properties "^1.1.2"
+ es-abstract "^1.6.1"
+ function-bind "^1.1.0"
+ has "^1.0.1"
+
+object.entries@^1.1.0:
+ version "1.1.0"
+ resolved "https://registry.npmjs.org/object.entries/-/object.entries-1.1.0.tgz#2024fc6d6ba246aee38bdb0ffd5cfbcf371b7519"
+ integrity sha512-l+H6EQ8qzGRxbkHOd5I/aHRhHDKoQXQ8g0BYt4uSweQU1/J6dZUOyWh9a2Vky35YCKjzmgxOzta2hH6kf9HuXA==
+ dependencies:
+ define-properties "^1.1.3"
+ es-abstract "^1.12.0"
+ function-bind "^1.1.1"
+ has "^1.0.3"
+
+object.fromentries@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/object.fromentries/-/object.fromentries-2.0.0.tgz#49a543d92151f8277b3ac9600f1e930b189d30ab"
+ dependencies:
+ define-properties "^1.1.2"
+ es-abstract "^1.11.0"
+ function-bind "^1.1.1"
+ has "^1.0.1"
+
+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.0.4:
+ version "1.0.4"
+ resolved "https://registry.yarnpkg.com/object.values/-/object.values-1.0.4.tgz#e524da09b4f66ff05df457546ec72ac99f13069a"
+ dependencies:
+ define-properties "^1.1.2"
+ es-abstract "^1.6.1"
+ function-bind "^1.1.0"
+ has "^1.0.1"
+
+object.values@^1.1.0:
+ version "1.1.0"
+ resolved "https://registry.npmjs.org/object.values/-/object.values-1.1.0.tgz#bf6810ef5da3e5325790eaaa2be213ea84624da9"
+ integrity sha512-8mf0nKLAoFX6VlNVdhGj31SVYpaNFtUnuoOXWyFEstsWRgU837AK+JYM0iAxwkSzGRbwn8cbFmgbyxj1j4VbXg==
+ dependencies:
+ define-properties "^1.1.3"
+ es-abstract "^1.12.0"
+ function-bind "^1.1.1"
+ has "^1.0.3"
+
+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.1:
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/on-headers/-/on-headers-1.0.1.tgz#928f5d0f470d49342651ea6794b0857c100693f7"
+
+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"
+
+onetime@^2.0.0:
+ version "2.0.1"
+ resolved "https://registry.yarnpkg.com/onetime/-/onetime-2.0.1.tgz#067428230fd67443b2794b22bba528b6867962d4"
+ dependencies:
+ mimic-fn "^1.0.0"
+
+opener@^1.5.1:
+ version "1.5.1"
+ resolved "https://registry.yarnpkg.com/opener/-/opener-1.5.1.tgz#6d2f0e77f1a0af0032aca716c2c1fbb8e7e8abed"
+
+opn@^5.1.0:
+ version "5.3.0"
+ resolved "https://registry.yarnpkg.com/opn/-/opn-5.3.0.tgz#64871565c863875f052cfdf53d3e3cb5adb53b1c"
+ dependencies:
+ is-wsl "^1.1.0"
+
+optimism@^0.6.9:
+ version "0.6.9"
+ resolved "https://registry.yarnpkg.com/optimism/-/optimism-0.6.9.tgz#19258ff8b3be0cea29ac35f06bff818e026e30bb"
+ dependencies:
+ immutable-tuple "^0.4.9"
+
+optimize-css-assets-webpack-plugin@5.0.1:
+ version "5.0.1"
+ resolved "https://registry.yarnpkg.com/optimize-css-assets-webpack-plugin/-/optimize-css-assets-webpack-plugin-5.0.1.tgz#9eb500711d35165b45e7fd60ba2df40cb3eb9159"
+ dependencies:
+ cssnano "^4.1.0"
+ last-call-webpack-plugin "^3.0.0"
+
+optionator@^0.8.2:
+ version "0.8.2"
+ resolved "https://registry.yarnpkg.com/optionator/-/optionator-0.8.2.tgz#364c5e409d3f4d6301d6c0b4c05bba50180aeb64"
+ dependencies:
+ deep-is "~0.1.3"
+ fast-levenshtein "~2.0.4"
+ levn "~0.3.0"
+ prelude-ls "~1.1.2"
+ type-check "~0.3.2"
+ wordwrap "~1.0.0"
+
+original@^1.0.0:
+ version "1.0.2"
+ resolved "https://registry.yarnpkg.com/original/-/original-1.0.2.tgz#e442a61cffe1c5fd20a65f3261c26663b303f25f"
+ dependencies:
+ url-parse "^1.4.3"
+
+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-locale@^1.4.0:
+ version "1.4.0"
+ resolved "https://registry.yarnpkg.com/os-locale/-/os-locale-1.4.0.tgz#20f9f17ae29ed345e8bde583b13d2009803c14d9"
+ dependencies:
+ lcid "^1.0.0"
+
+os-locale@^3.0.0:
+ version "3.0.1"
+ resolved "https://registry.yarnpkg.com/os-locale/-/os-locale-3.0.1.tgz#3b014fbf01d87f60a1e5348d80fe870dc82c4620"
+ dependencies:
+ execa "^0.10.0"
+ lcid "^2.0.0"
+ mem "^4.0.0"
+
+os-shim@^0.1.2:
+ version "0.1.3"
+ resolved "https://registry.yarnpkg.com/os-shim/-/os-shim-0.1.3.tgz#6b62c3791cf7909ea35ed46e17658bb417cb3917"
+
+os-tmpdir@^1.0.0, os-tmpdir@^1.0.1, os-tmpdir@~1.0.2:
+ version "1.0.2"
+ resolved "https://registry.yarnpkg.com/os-tmpdir/-/os-tmpdir-1.0.2.tgz#bbe67406c79aa85c5cfec766fe5734555dfa1274"
+
+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-defer@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/p-defer/-/p-defer-1.0.0.tgz#9f6eb182f6c9aa8cd743004a7d4f96b196b0fb0c"
+
+p-finally@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/p-finally/-/p-finally-1.0.0.tgz#3fbcfb15b899a44123b34b6dcc18b724336a2cae"
+
+p-is-promise@^1.1.0:
+ version "1.1.0"
+ resolved "https://registry.yarnpkg.com/p-is-promise/-/p-is-promise-1.1.0.tgz#9c9456989e9f6588017b0434d56097675c3da05e"
+
+p-limit@^1.1.0:
+ version "1.2.0"
+ resolved "https://registry.yarnpkg.com/p-limit/-/p-limit-1.2.0.tgz#0e92b6bedcb59f022c13d0f1949dc82d15909f1c"
+ dependencies:
+ p-try "^1.0.0"
+
+p-limit@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/p-limit/-/p-limit-2.0.0.tgz#e624ed54ee8c460a778b3c9f3670496ff8a57aec"
+ 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"
+ dependencies:
+ p-limit "^2.0.0"
+
+p-map@^1.1.1:
+ version "1.2.0"
+ resolved "https://registry.yarnpkg.com/p-map/-/p-map-1.2.0.tgz#e4e94f311eabbc8633a1e79908165fca26241b6b"
+
+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.0.0"
+ resolved "https://registry.yarnpkg.com/p-try/-/p-try-2.0.0.tgz#85080bb87c64688fa47996fe8f7dfbe8211760b1"
+
+pako@~1.0.5:
+ version "1.0.6"
+ resolved "https://registry.yarnpkg.com/pako/-/pako-1.0.6.tgz#0101211baa70c4bca4a0f63f2206e97b7dfaf258"
+
+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"
+
+param-case@2.1.x:
+ version "2.1.1"
+ resolved "https://registry.yarnpkg.com/param-case/-/param-case-2.1.1.tgz#df94fd8cf6531ecf75e6bef9a0858fbc72be2247"
+ dependencies:
+ no-case "^2.2.0"
+
+parent-module@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/parent-module/-/parent-module-1.0.0.tgz#df250bdc5391f4a085fb589dad761f5ad6b865b5"
+ dependencies:
+ callsites "^3.0.0"
+
+parse-asn1@^5.0.0:
+ version "5.1.1"
+ resolved "https://registry.yarnpkg.com/parse-asn1/-/parse-asn1-5.1.1.tgz#f6bf293818332bd0dab54efb16087724745e6ca8"
+ 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"
+
+parse-bmfont-ascii@^1.0.3:
+ version "1.0.6"
+ resolved "https://registry.yarnpkg.com/parse-bmfont-ascii/-/parse-bmfont-ascii-1.0.6.tgz#11ac3c3ff58f7c2020ab22769079108d4dfa0285"
+
+parse-bmfont-binary@^1.0.5:
+ version "1.0.6"
+ resolved "https://registry.yarnpkg.com/parse-bmfont-binary/-/parse-bmfont-binary-1.0.6.tgz#d038b476d3e9dd9db1e11a0b0e53a22792b69006"
+
+parse-bmfont-xml@^1.1.0:
+ version "1.1.3"
+ resolved "https://registry.yarnpkg.com/parse-bmfont-xml/-/parse-bmfont-xml-1.1.3.tgz#d6b66a371afd39c5007d9f0eeb262a4f2cce7b7c"
+ dependencies:
+ xml-parse-from-string "^1.0.0"
+ xml2js "^0.4.5"
+
+parse-headers@^2.0.0:
+ version "2.0.1"
+ resolved "https://registry.yarnpkg.com/parse-headers/-/parse-headers-2.0.1.tgz#6ae83a7aa25a9d9b700acc28698cd1f1ed7e9536"
+ dependencies:
+ for-each "^0.3.2"
+ trim "0.0.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-passwd@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/parse-passwd/-/parse-passwd-1.0.0.tgz#6d5b934a456993b23d37f40a382d6f1666a8e5c6"
+
+parse-png@^1.0.0, parse-png@^1.1.1:
+ version "1.1.2"
+ resolved "https://registry.yarnpkg.com/parse-png/-/parse-png-1.1.2.tgz#f5c2ad7c7993490986020a284c19aee459711ff2"
+ dependencies:
+ pngjs "^3.2.0"
+
+parseurl@~1.3.2:
+ version "1.3.2"
+ resolved "https://registry.yarnpkg.com/parseurl/-/parseurl-1.3.2.tgz#fc289d4ed8993119460c156253262cdc8de65bf3"
+
+pascalcase@^0.1.1:
+ version "0.1.1"
+ resolved "https://registry.yarnpkg.com/pascalcase/-/pascalcase-0.1.1.tgz#b363e55e8006ca6fe21784d2db22bd15d7917f14"
+
+path-browserify@0.0.0:
+ version "0.0.0"
+ resolved "https://registry.yarnpkg.com/path-browserify/-/path-browserify-0.0.0.tgz#a0b870729aae214005b7d5032ec2cbbb0fb4451a"
+
+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-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.1, 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"
+
+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"
+
+path-parse@^1.0.5:
+ version "1.0.5"
+ resolved "https://registry.yarnpkg.com/path-parse/-/path-parse-1.0.5.tgz#3c1adf871ea9cd6c9431b6ea2bd74a0ff055c4c1"
+
+path-parse@^1.0.6:
+ version "1.0.6"
+ resolved "https://registry.yarnpkg.com/path-parse/-/path-parse-1.0.6.tgz#d62dbb5679405d72c4737ec58600e9ddcf06d24c"
+
+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-to-regexp@^1.7.0:
+ version "1.7.0"
+ resolved "https://registry.yarnpkg.com/path-to-regexp/-/path-to-regexp-1.7.0.tgz#59fde0f435badacba103a84e9d3bc64e96b9937d"
+ dependencies:
+ isarray "0.0.1"
+
+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@^3.0.0:
+ version "3.0.0"
+ resolved "https://registry.yarnpkg.com/path-type/-/path-type-3.0.0.tgz#cef31dc8e0a1a3bb0d105c0cd97cf3bf47f4e36f"
+ dependencies:
+ pify "^3.0.0"
+
+path@^0.12.7:
+ version "0.12.7"
+ resolved "https://registry.yarnpkg.com/path/-/path-0.12.7.tgz#d4dc2a506c4ce2197eb481ebfcd5b36c0140b10f"
+ dependencies:
+ process "^0.11.1"
+ util "^0.10.3"
+
+pbkdf2@^3.0.3:
+ version "3.0.16"
+ resolved "https://registry.yarnpkg.com/pbkdf2/-/pbkdf2-3.0.16.tgz#7404208ec6b01b62d85bf83853a8064f8d9c2a5c"
+ 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"
+
+pend@~1.2.0:
+ version "1.2.0"
+ resolved "https://registry.yarnpkg.com/pend/-/pend-1.2.0.tgz#7a57eb550a6783f9115331fcf4663d5c8e007a50"
+
+performance-now@^2.1.0:
+ version "2.1.0"
+ resolved "https://registry.yarnpkg.com/performance-now/-/performance-now-2.1.0.tgz#6309f4e0e5fa913ec1c69307ae364b4b377c9e7b"
+
+phantomjs-prebuilt@^2.1.10:
+ version "2.1.16"
+ resolved "https://registry.yarnpkg.com/phantomjs-prebuilt/-/phantomjs-prebuilt-2.1.16.tgz#efd212a4a3966d3647684ea8ba788549be2aefef"
+ dependencies:
+ es6-promise "^4.0.3"
+ extract-zip "^1.6.5"
+ fs-extra "^1.0.0"
+ hasha "^2.2.0"
+ kew "^0.7.0"
+ progress "^1.1.8"
+ request "^2.81.0"
+ request-progress "^2.0.1"
+ which "^1.2.10"
+
+pify@^2.0.0:
+ version "2.3.0"
+ resolved "https://registry.yarnpkg.com/pify/-/pify-2.3.0.tgz#ed141a6ac043a849ea588498e7dca8b15330e90c"
+
+pify@^3.0.0:
+ version "3.0.0"
+ resolved "https://registry.yarnpkg.com/pify/-/pify-3.0.0.tgz#e5a4acd2c101fdf3d9a4d07f0dbc4db49dd28176"
+
+pify@^4.0.1:
+ version "4.0.1"
+ resolved "https://registry.yarnpkg.com/pify/-/pify-4.0.1.tgz#4b2cd25c50d598735c50292224fd8c6df41e3231"
+
+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"
+
+pixelmatch@^4.0.0:
+ version "4.0.2"
+ resolved "https://registry.yarnpkg.com/pixelmatch/-/pixelmatch-4.0.2.tgz#8f47dcec5011b477b67db03c243bc1f3085e8854"
+ dependencies:
+ pngjs "^3.0.0"
+
+pkg-dir@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/pkg-dir/-/pkg-dir-1.0.0.tgz#7a4b508a8d5bb2d629d447056ff4e9c9314cf3d4"
+ dependencies:
+ find-up "^1.0.0"
+
+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"
+ dependencies:
+ find-up "^3.0.0"
+
+pluralize@^7.0.0:
+ version "7.0.0"
+ resolved "https://registry.yarnpkg.com/pluralize/-/pluralize-7.0.0.tgz#298b89df8b93b0221dbf421ad2b1b1ea23fc6777"
+
+pn@^1.0.0:
+ version "1.1.0"
+ resolved "https://registry.yarnpkg.com/pn/-/pn-1.1.0.tgz#e2f4cef0e219f463c179ab37463e4e1ecdccbafb"
+
+pngjs@^3.0.0, pngjs@^3.2.0:
+ version "3.3.3"
+ resolved "https://registry.yarnpkg.com/pngjs/-/pngjs-3.3.3.tgz#85173703bde3edac8998757b96e5821d0966a21b"
+
+popper.js@^1.14.4:
+ version "1.14.5"
+ resolved "https://registry.yarnpkg.com/popper.js/-/popper.js-1.14.5.tgz#98abcce7c7c34c4ee47fcbc6b3da8af2c0a127bc"
+
+portfinder@^1.0.9:
+ version "1.0.13"
+ resolved "https://registry.yarnpkg.com/portfinder/-/portfinder-1.0.13.tgz#bb32ecd87c27104ae6ee44b5a3ccbf0ebb1aede9"
+ dependencies:
+ async "^1.5.2"
+ debug "^2.2.0"
+ mkdirp "0.5.x"
+
+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-calc@^6.0.0:
+ version "6.0.1"
+ resolved "https://registry.yarnpkg.com/postcss-calc/-/postcss-calc-6.0.1.tgz#3d24171bbf6e7629d422a436ebfe6dd9511f4330"
+ dependencies:
+ css-unit-converter "^1.1.1"
+ postcss "^6.0.0"
+ postcss-selector-parser "^2.2.2"
+ reduce-css-calc "^2.0.0"
+
+postcss-colormin@^4.0.0:
+ version "4.0.1"
+ resolved "https://registry.yarnpkg.com/postcss-colormin/-/postcss-colormin-4.0.1.tgz#6f1c18a0155bc69613f2ff13843e2e4ae8ff0bbe"
+ dependencies:
+ browserslist "^4.0.0"
+ color "^3.0.0"
+ has "^1.0.0"
+ postcss "^6.0.0"
+ postcss-value-parser "^3.0.0"
+
+postcss-convert-values@^4.0.0:
+ version "4.0.0"
+ resolved "https://registry.yarnpkg.com/postcss-convert-values/-/postcss-convert-values-4.0.0.tgz#77d77d9aed1dc4e6956e651cc349d53305876f62"
+ dependencies:
+ postcss "^6.0.0"
+ postcss-value-parser "^3.0.0"
+
+postcss-discard-comments@^4.0.0:
+ version "4.0.0"
+ resolved "https://registry.yarnpkg.com/postcss-discard-comments/-/postcss-discard-comments-4.0.0.tgz#9684a299e76b3e93263ef8fd2adbf1a1c08fd88d"
+ dependencies:
+ postcss "^6.0.0"
+
+postcss-discard-duplicates@^4.0.0:
+ version "4.0.0"
+ resolved "https://registry.yarnpkg.com/postcss-discard-duplicates/-/postcss-discard-duplicates-4.0.0.tgz#42f3c267f85fa909e042c35767ecfd65cb2bd72c"
+ dependencies:
+ postcss "^6.0.0"
+
+postcss-discard-empty@^4.0.0:
+ version "4.0.0"
+ resolved "https://registry.yarnpkg.com/postcss-discard-empty/-/postcss-discard-empty-4.0.0.tgz#55e18a59c74128e38c7d2804bcfa4056611fb97f"
+ dependencies:
+ postcss "^6.0.0"
+
+postcss-discard-overridden@^4.0.0:
+ version "4.0.0"
+ resolved "https://registry.yarnpkg.com/postcss-discard-overridden/-/postcss-discard-overridden-4.0.0.tgz#4a0bf85978784cf1f81ed2c1c1fd9d964a1da1fa"
+ dependencies:
+ postcss "^6.0.0"
+
+postcss-merge-longhand@^4.0.0:
+ version "4.0.1"
+ resolved "https://registry.yarnpkg.com/postcss-merge-longhand/-/postcss-merge-longhand-4.0.1.tgz#98cdebda7cc1bada4ae03c76ed1c44c97a574019"
+ dependencies:
+ postcss "^6.0.0"
+ postcss-value-parser "^3.0.0"
+ stylehacks "^4.0.0"
+
+postcss-merge-rules@^4.0.0:
+ version "4.0.1"
+ resolved "https://registry.yarnpkg.com/postcss-merge-rules/-/postcss-merge-rules-4.0.1.tgz#430fd59b3f2ed2e8afcd0b31278eda39854abb10"
+ dependencies:
+ browserslist "^4.0.0"
+ caniuse-api "^3.0.0"
+ cssnano-util-same-parent "^4.0.0"
+ postcss "^6.0.0"
+ postcss-selector-parser "^3.0.0"
+ vendors "^1.0.0"
+
+postcss-minify-font-values@^4.0.0:
+ version "4.0.0"
+ resolved "https://registry.yarnpkg.com/postcss-minify-font-values/-/postcss-minify-font-values-4.0.0.tgz#4cc33d283d6a81759036e757ef981d92cbd85bed"
+ dependencies:
+ postcss "^6.0.0"
+ postcss-value-parser "^3.0.0"
+
+postcss-minify-gradients@^4.0.0:
+ version "4.0.0"
+ resolved "https://registry.yarnpkg.com/postcss-minify-gradients/-/postcss-minify-gradients-4.0.0.tgz#3fc3916439d27a9bb8066db7cdad801650eb090e"
+ dependencies:
+ cssnano-util-get-arguments "^4.0.0"
+ is-color-stop "^1.0.0"
+ postcss "^6.0.0"
+ postcss-value-parser "^3.0.0"
+
+postcss-minify-params@^4.0.0:
+ version "4.0.0"
+ resolved "https://registry.yarnpkg.com/postcss-minify-params/-/postcss-minify-params-4.0.0.tgz#05e9166ee48c05af651989ce84d39c1b4d790674"
+ dependencies:
+ alphanum-sort "^1.0.0"
+ cssnano-util-get-arguments "^4.0.0"
+ postcss "^6.0.0"
+ postcss-value-parser "^3.0.0"
+ uniqs "^2.0.0"
+
+postcss-minify-selectors@^4.0.0:
+ version "4.0.0"
+ resolved "https://registry.yarnpkg.com/postcss-minify-selectors/-/postcss-minify-selectors-4.0.0.tgz#b1e9f6c463416d3fcdcb26e7b785d95f61578aad"
+ dependencies:
+ alphanum-sort "^1.0.0"
+ has "^1.0.0"
+ postcss "^6.0.0"
+ postcss-selector-parser "^3.0.0"
+
+postcss-modules-extract-imports@^1.2.0:
+ version "1.2.0"
+ resolved "https://registry.yarnpkg.com/postcss-modules-extract-imports/-/postcss-modules-extract-imports-1.2.0.tgz#66140ecece38ef06bf0d3e355d69bf59d141ea85"
+ dependencies:
+ postcss "^6.0.1"
+
+postcss-modules-local-by-default@^1.2.0:
+ version "1.2.0"
+ resolved "https://registry.yarnpkg.com/postcss-modules-local-by-default/-/postcss-modules-local-by-default-1.2.0.tgz#f7d80c398c5a393fa7964466bd19500a7d61c069"
+ dependencies:
+ css-selector-tokenizer "^0.7.0"
+ postcss "^6.0.1"
+
+postcss-modules-scope@^1.1.0:
+ version "1.1.0"
+ resolved "https://registry.yarnpkg.com/postcss-modules-scope/-/postcss-modules-scope-1.1.0.tgz#d6ea64994c79f97b62a72b426fbe6056a194bb90"
+ dependencies:
+ css-selector-tokenizer "^0.7.0"
+ postcss "^6.0.1"
+
+postcss-modules-values@^1.3.0:
+ version "1.3.0"
+ resolved "https://registry.yarnpkg.com/postcss-modules-values/-/postcss-modules-values-1.3.0.tgz#ecffa9d7e192518389f42ad0e83f72aec456ea20"
+ dependencies:
+ icss-replace-symbols "^1.1.0"
+ postcss "^6.0.1"
+
+postcss-normalize-charset@^4.0.0:
+ version "4.0.0"
+ resolved "https://registry.yarnpkg.com/postcss-normalize-charset/-/postcss-normalize-charset-4.0.0.tgz#24527292702d5e8129eafa3d1de49ed51a6ab730"
+ dependencies:
+ postcss "^6.0.0"
+
+postcss-normalize-display-values@^4.0.0:
+ version "4.0.0"
+ resolved "https://registry.yarnpkg.com/postcss-normalize-display-values/-/postcss-normalize-display-values-4.0.0.tgz#950e0c7be3445770a160fffd6b6644c3c0cd8f89"
+ dependencies:
+ cssnano-util-get-match "^4.0.0"
+ postcss "^6.0.0"
+ postcss-value-parser "^3.0.0"
+
+postcss-normalize-positions@^4.0.0:
+ version "4.0.0"
+ resolved "https://registry.yarnpkg.com/postcss-normalize-positions/-/postcss-normalize-positions-4.0.0.tgz#ee9343ab981b822c63ab72615ecccd08564445a3"
+ dependencies:
+ cssnano-util-get-arguments "^4.0.0"
+ has "^1.0.0"
+ postcss "^6.0.0"
+ postcss-value-parser "^3.0.0"
+
+postcss-normalize-repeat-style@^4.0.0:
+ version "4.0.0"
+ resolved "https://registry.yarnpkg.com/postcss-normalize-repeat-style/-/postcss-normalize-repeat-style-4.0.0.tgz#b711c592cf16faf9ff575e42fa100b6799083eff"
+ dependencies:
+ cssnano-util-get-arguments "^4.0.0"
+ cssnano-util-get-match "^4.0.0"
+ postcss "^6.0.0"
+ postcss-value-parser "^3.0.0"
+
+postcss-normalize-string@^4.0.0:
+ version "4.0.0"
+ resolved "https://registry.yarnpkg.com/postcss-normalize-string/-/postcss-normalize-string-4.0.0.tgz#718cb6d30a6fac6ac6a830e32c06c07dbc66fe5d"
+ dependencies:
+ has "^1.0.0"
+ postcss "^6.0.0"
+ postcss-value-parser "^3.0.0"
+
+postcss-normalize-timing-functions@^4.0.0:
+ version "4.0.0"
+ resolved "https://registry.yarnpkg.com/postcss-normalize-timing-functions/-/postcss-normalize-timing-functions-4.0.0.tgz#0351f29886aa981d43d91b2c2bd1aea6d0af6d23"
+ dependencies:
+ cssnano-util-get-match "^4.0.0"
+ postcss "^6.0.0"
+ postcss-value-parser "^3.0.0"
+
+postcss-normalize-unicode@^4.0.0:
+ version "4.0.0"
+ resolved "https://registry.yarnpkg.com/postcss-normalize-unicode/-/postcss-normalize-unicode-4.0.0.tgz#5acd5d47baea5d17674b2ccc4ae5166fa88cdf97"
+ dependencies:
+ postcss "^6.0.0"
+ postcss-value-parser "^3.0.0"
+
+postcss-normalize-url@^4.0.0:
+ version "4.0.0"
+ resolved "https://registry.yarnpkg.com/postcss-normalize-url/-/postcss-normalize-url-4.0.0.tgz#b7a9c8ad26cf26694c146eb2d68bd0cf49956f0d"
+ dependencies:
+ is-absolute-url "^2.0.0"
+ normalize-url "^3.0.0"
+ postcss "^6.0.0"
+ postcss-value-parser "^3.0.0"
+
+postcss-normalize-whitespace@^4.0.0:
+ version "4.0.0"
+ resolved "https://registry.yarnpkg.com/postcss-normalize-whitespace/-/postcss-normalize-whitespace-4.0.0.tgz#1da7e76b10ae63c11827fa04fc3bb4a1efe99cc0"
+ dependencies:
+ postcss "^6.0.0"
+ postcss-value-parser "^3.0.0"
+
+postcss-ordered-values@^4.0.0:
+ version "4.0.0"
+ resolved "https://registry.yarnpkg.com/postcss-ordered-values/-/postcss-ordered-values-4.0.0.tgz#58b40c74f72e022eb34152c12e4b0f9354482fc2"
+ dependencies:
+ cssnano-util-get-arguments "^4.0.0"
+ postcss "^6.0.0"
+ postcss-value-parser "^3.0.0"
+
+postcss-reduce-initial@^4.0.0:
+ version "4.0.1"
+ resolved "https://registry.yarnpkg.com/postcss-reduce-initial/-/postcss-reduce-initial-4.0.1.tgz#f2d58f50cea2b0c5dc1278d6ea5ed0ff5829c293"
+ dependencies:
+ browserslist "^4.0.0"
+ caniuse-api "^3.0.0"
+ has "^1.0.0"
+ postcss "^6.0.0"
+
+postcss-reduce-transforms@^4.0.0:
+ version "4.0.0"
+ resolved "https://registry.yarnpkg.com/postcss-reduce-transforms/-/postcss-reduce-transforms-4.0.0.tgz#f645fc7440c35274f40de8104e14ad7163edf188"
+ dependencies:
+ cssnano-util-get-match "^4.0.0"
+ has "^1.0.0"
+ postcss "^6.0.0"
+ postcss-value-parser "^3.0.0"
+
+postcss-selector-parser@^2.2.2:
+ version "2.2.3"
+ resolved "https://registry.yarnpkg.com/postcss-selector-parser/-/postcss-selector-parser-2.2.3.tgz#f9437788606c3c9acee16ffe8d8b16297f27bb90"
+ dependencies:
+ flatten "^1.0.2"
+ indexes-of "^1.0.1"
+ uniq "^1.0.1"
+
+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-svgo@^4.0.0:
+ version "4.0.0"
+ resolved "https://registry.yarnpkg.com/postcss-svgo/-/postcss-svgo-4.0.0.tgz#c0bbad02520fc636c9d78b0e8403e2e515c32285"
+ dependencies:
+ is-svg "^3.0.0"
+ postcss "^6.0.0"
+ postcss-value-parser "^3.0.0"
+ svgo "^1.0.0"
+
+postcss-unique-selectors@^4.0.0:
+ version "4.0.0"
+ resolved "https://registry.yarnpkg.com/postcss-unique-selectors/-/postcss-unique-selectors-4.0.0.tgz#04c1e9764c75874261303402c41f0e9769fc5501"
+ dependencies:
+ alphanum-sort "^1.0.0"
+ postcss "^6.0.0"
+ uniqs "^2.0.0"
+
+postcss-value-parser@^3.0.0, postcss-value-parser@^3.3.0:
+ version "3.3.0"
+ resolved "https://registry.yarnpkg.com/postcss-value-parser/-/postcss-value-parser-3.3.0.tgz#87f38f9f18f774a4ab4c8a232f5c5ce8872a9d15"
+
+postcss@^6.0.0, postcss@^6.0.1, postcss@^6.0.23:
+ version "6.0.23"
+ resolved "https://registry.yarnpkg.com/postcss/-/postcss-6.0.23.tgz#61c82cc328ac60e677645f979054eb98bc0e3324"
+ dependencies:
+ chalk "^2.4.1"
+ source-map "^0.6.1"
+ supports-color "^5.4.0"
+
+pre-commit@^1.2.2:
+ version "1.2.2"
+ resolved "https://registry.yarnpkg.com/pre-commit/-/pre-commit-1.2.2.tgz#dbcee0ee9de7235e57f79c56d7ce94641a69eec6"
+ dependencies:
+ cross-spawn "^5.0.1"
+ spawn-sync "^1.0.15"
+ which "1.2.x"
+
+preact-css-transition-group@^1.3.0:
+ version "1.3.0"
+ resolved "https://registry.yarnpkg.com/preact-css-transition-group/-/preact-css-transition-group-1.3.0.tgz#06fe468b26f7802e95b829a762db0bc199aef399"
+
+preact@^8.2.9:
+ version "8.3.1"
+ resolved "https://registry.yarnpkg.com/preact/-/preact-8.3.1.tgz#ed34f79d09edc5efd32a378a3416ef5dc531e3ac"
+
+prelude-ls@~1.1.2:
+ version "1.1.2"
+ resolved "https://registry.yarnpkg.com/prelude-ls/-/prelude-ls-1.1.2.tgz#21932a549f5e52ffd9a827f570e04be62a97da54"
+
+prettier-bytes@^1.0.4:
+ version "1.0.4"
+ resolved "https://registry.yarnpkg.com/prettier-bytes/-/prettier-bytes-1.0.4.tgz#994b02aa46f699c50b6257b5faaa7fe2557e62d6"
+
+pretty-bytes@5.1.0:
+ version "5.1.0"
+ resolved "https://registry.yarnpkg.com/pretty-bytes/-/pretty-bytes-5.1.0.tgz#6237ecfbdc6525beaef4de722cc60a58ae0e6c6d"
+
+pretty-error@^2.0.2:
+ version "2.1.1"
+ resolved "https://registry.yarnpkg.com/pretty-error/-/pretty-error-2.1.1.tgz#5f4f87c8f91e5ae3f3ba87ab4cf5e03b1a17f1a3"
+ dependencies:
+ renderkid "^2.0.1"
+ utila "~0.4"
+
+private@^0.1.6, private@^0.1.8:
+ 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.0"
+ resolved "https://registry.yarnpkg.com/process-nextick-args/-/process-nextick-args-2.0.0.tgz#a37d732f4271b4ab1ad070d35508e8290788ffaa"
+
+process@^0.11.1, process@^0.11.10:
+ version "0.11.10"
+ resolved "https://registry.yarnpkg.com/process/-/process-0.11.10.tgz#7332300e840161bda3e69a1d1d91a7d4bc16f182"
+
+process@~0.5.1:
+ version "0.5.2"
+ resolved "https://registry.yarnpkg.com/process/-/process-0.5.2.tgz#1638d8a8e34c2f440a91db95ab9aeb677fc185cf"
+
+progress@2.0.0, progress@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/progress/-/progress-2.0.0.tgz#8a1be366bf8fc23db2bd23f10c6fe920b4389d1f"
+
+progress@^1.1.8:
+ version "1.1.8"
+ resolved "https://registry.yarnpkg.com/progress/-/progress-1.1.8.tgz#e260c78f6161cdd9b0e56cc3e0a85de17c7a57be"
+
+promise-inflight@^1.0.1:
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/promise-inflight/-/promise-inflight-1.0.1.tgz#98472870bf228132fcbdd868129bad12c3c029e3"
+
+promise@^7.1.1:
+ version "7.3.1"
+ resolved "https://registry.yarnpkg.com/promise/-/promise-7.3.1.tgz#064b72602b18f90f29192b8b1bc418ffd1ebd3bf"
+ dependencies:
+ asap "~2.0.3"
+
+prop-types-exact@^1.2.0:
+ version "1.2.0"
+ resolved "https://registry.npmjs.org/prop-types-exact/-/prop-types-exact-1.2.0.tgz#825d6be46094663848237e3925a98c6e944e9869"
+ integrity sha512-K+Tk3Kd9V0odiXFP9fwDHUYRyvK3Nun3GVyPapSIs5OBkITAm15W0CPFD/YKTkMUAbc0b9CUwRQp2ybiBIq+eA==
+ dependencies:
+ has "^1.0.3"
+ object.assign "^4.1.0"
+ reflect.ownkeys "^0.2.0"
+
+prop-types@15.x, prop-types@^15.5.0, prop-types@^15.5.10, prop-types@^15.5.4, prop-types@^15.5.8, prop-types@^15.6.0, prop-types@^15.6.1, prop-types@^15.6.2:
+ version "15.6.2"
+ resolved "https://registry.yarnpkg.com/prop-types/-/prop-types-15.6.2.tgz#05d5ca77b4453e985d60fc7ff8c859094a497102"
+ dependencies:
+ loose-envify "^1.3.1"
+ object-assign "^4.1.1"
+
+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"
+
+property-expr@^1.5.0:
+ version "1.5.1"
+ resolved "https://registry.yarnpkg.com/property-expr/-/property-expr-1.5.1.tgz#22e8706894a0c8e28d58735804f6ba3a3673314f"
+
+proxy-addr@~2.0.3:
+ version "2.0.3"
+ resolved "https://registry.yarnpkg.com/proxy-addr/-/proxy-addr-2.0.3.tgz#355f262505a621646b3130a728eb647e22055341"
+ dependencies:
+ forwarded "~0.1.2"
+ ipaddr.js "1.6.0"
+
+proxy-from-env@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/proxy-from-env/-/proxy-from-env-1.0.0.tgz#33c50398f70ea7eb96d21f7b817630a55791c7ee"
+
+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"
+
+public-encrypt@^4.0.0:
+ version "4.0.2"
+ resolved "https://registry.yarnpkg.com/public-encrypt/-/public-encrypt-4.0.2.tgz#46eb9107206bf73489f8b85b69d91334c6610994"
+ 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"
+
+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"
+ dependencies:
+ end-of-stream "^1.1.0"
+ once "^1.3.1"
+
+pumpify@^1.3.3:
+ version "1.5.0"
+ resolved "https://registry.yarnpkg.com/pumpify/-/pumpify-1.5.0.tgz#30c905a26c88fa0074927af07256672b474b1c15"
+ 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.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.0"
+ resolved "https://registry.yarnpkg.com/punycode/-/punycode-2.1.0.tgz#5f863edc89b96db09074bad7947bf09056ca4e7d"
+
+pure-color@^1.2.0:
+ version "1.3.0"
+ resolved "https://registry.yarnpkg.com/pure-color/-/pure-color-1.3.0.tgz#1fe064fb0ac851f0de61320a8bf796836422f33e"
+
+q@^1.1.2:
+ version "1.5.1"
+ resolved "https://registry.yarnpkg.com/q/-/q-1.5.1.tgz#7e32f75b41381291d04611f1bf14109ac00651d7"
+
+qs@6.5.1, qs@~6.5.1:
+ version "6.5.1"
+ resolved "https://registry.yarnpkg.com/qs/-/qs-6.5.1.tgz#349cdf6eef89ec45c12d7d5eb3fc0c870343a6d8"
+
+qs@^6.6.0:
+ version "6.6.0"
+ resolved "https://registry.yarnpkg.com/qs/-/qs-6.6.0.tgz#a99c0f69a8d26bf7ef012f871cdabb0aee4424c2"
+
+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.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/querystringify/-/querystringify-2.0.0.tgz#fa3ed6e68eb15159457c89b37bc6472833195755"
+
+raf@^3.4.0, raf@^3.4.1:
+ 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:
+ version "2.0.6"
+ resolved "https://registry.yarnpkg.com/randombytes/-/randombytes-2.0.6.tgz#d302c522948588848a8d300c932b44c24231da80"
+ 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.0.3, range-parser@~1.2.0:
+ version "1.2.0"
+ resolved "https://registry.yarnpkg.com/range-parser/-/range-parser-1.2.0.tgz#f49be6b487894ddc40dcc94a322f611092e00d5e"
+
+raw-body@2.3.2:
+ version "2.3.2"
+ resolved "https://registry.yarnpkg.com/raw-body/-/raw-body-2.3.2.tgz#bcd60c77d3eb93cde0050295c3f379389bc88f89"
+ dependencies:
+ bytes "3.0.0"
+ http-errors "1.6.2"
+ iconv-lite "0.4.19"
+ unpipe "1.0.0"
+
+rc-align@^2.4.0:
+ version "2.4.3"
+ resolved "https://registry.yarnpkg.com/rc-align/-/rc-align-2.4.3.tgz#b9b3c2a6d68adae71a8e1d041cd5e3b2a655f99a"
+ 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.6.0"
+ resolved "https://registry.yarnpkg.com/rc-animate/-/rc-animate-2.6.0.tgz#ca8440d042781af7a1329d84f97ea94794c5ec15"
+ dependencies:
+ babel-runtime "6.x"
+ classnames "^2.2.6"
+ css-animation "^1.3.2"
+ prop-types "15.x"
+ raf "^3.4.0"
+ react-lifecycles-compat "^3.0.4"
+
+rc-slider@^8.6.6:
+ version "8.6.6"
+ resolved "https://registry.yarnpkg.com/rc-slider/-/rc-slider-8.6.6.tgz#008c5a5ccf29d83787d846b6b1d0bbb8e2a121c8"
+ dependencies:
+ babel-runtime "6.x"
+ classnames "^2.2.5"
+ 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.2"
+ resolved "https://registry.yarnpkg.com/rc-trigger/-/rc-trigger-2.6.2.tgz#a9c09ba5fad63af3b2ec46349c7db6cb46657001"
+ 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"
+
+rc-util@^4.0.4, rc-util@^4.4.0:
+ version "4.6.0"
+ resolved "https://registry.yarnpkg.com/rc-util/-/rc-util-4.6.0.tgz#ba33721783192ec4f3afb259e182b04e55deb7f6"
+ dependencies:
+ add-dom-event-listener "^1.1.0"
+ babel-runtime "6.x"
+ prop-types "^15.5.10"
+ shallowequal "^0.2.2"
+
+rc@^1.1.7:
+ version "1.2.7"
+ resolved "https://registry.yarnpkg.com/rc/-/rc-1.2.7.tgz#8a10ca30d588d00464360372b890d06dacd02297"
+ dependencies:
+ deep-extend "^0.5.1"
+ ini "~1.3.0"
+ minimist "^1.2.0"
+ strip-json-comments "~2.0.1"
+
+react-apollo@^2.4.1:
+ version "2.4.1"
+ resolved "https://registry.yarnpkg.com/react-apollo/-/react-apollo-2.4.1.tgz#89db63ebacf01c1603553bb476f089492aaeab2c"
+ dependencies:
+ fbjs "^1.0.0"
+ hoist-non-react-statics "^3.0.0"
+ invariant "^2.2.2"
+ lodash.flowright "^3.5.0"
+ lodash.isequal "^4.5.0"
+ prop-types "^15.6.0"
+
+react-avatar-editor@^11.0.6:
+ version "11.0.6"
+ resolved "https://registry.yarnpkg.com/react-avatar-editor/-/react-avatar-editor-11.0.6.tgz#c6194397bf9d27ab35c9741959fce1313c2f2c52"
+ dependencies:
+ prop-types "^15.5.8"
+
+react-base16-styling@^0.6.0:
+ version "0.6.0"
+ resolved "https://registry.yarnpkg.com/react-base16-styling/-/react-base16-styling-0.6.0.tgz#ef2156d66cf4139695c8a167886cb69ea660792c"
+ dependencies:
+ base16 "^1.0.0"
+ lodash.curry "^4.0.1"
+ lodash.flow "^3.3.0"
+ pure-color "^1.2.0"
+
+"react-click-outside@https://github.com/tj/react-click-outside#master":
+ version "1.1.1"
+ resolved "https://github.com/tj/react-click-outside#a833ddc5be47490307f9fcc6ed09d8c353108510"
+
+react-codemirror2@^5.1.0:
+ version "5.1.0"
+ resolved "https://registry.yarnpkg.com/react-codemirror2/-/react-codemirror2-5.1.0.tgz#62de4460178adea40eb52eabf7491669bf3794b8"
+ integrity sha512-Cksbgbviuf2mJfMyrKmcu7ycK6zX/ukuQO8dvRZdFWqATf5joalhjFc6etnBdGCcPA2LbhIwz+OPnQxLN/j1Fw==
+
+react-color@^2.18.0:
+ version "2.18.0"
+ resolved "https://registry.yarnpkg.com/react-color/-/react-color-2.18.0.tgz#34956f0bac394f6c3bc01692fd695644cc775ffd"
+ integrity sha512-FyVeU1kQiSokWc8NPz22azl1ezLpJdUyTbWL0LPUpcuuYDrZ/Y1veOk9rRK5B3pMlyDGvTk4f4KJhlkIQNRjEA==
+ dependencies:
+ "@icons/material" "^0.2.4"
+ lodash "^4.17.11"
+ material-colors "^1.2.1"
+ prop-types "^15.5.10"
+ reactcss "^1.2.0"
+ tinycolor2 "^1.4.1"
+
+react-dates@^20.2.5:
+ version "20.2.5"
+ resolved "https://registry.npmjs.org/react-dates/-/react-dates-20.2.5.tgz#e354f009f18e8bb79c19da3be265224f5727fe1e"
+ integrity sha512-cSaUo0MiCOApT+8GP+ACFcIYGdgmJia3rHrV4JmwgnJdRneNzkxZNHp3huaMvAuLrT5Kolg7xD4r6W05BzsnLw==
+ dependencies:
+ "@babel/runtime" "^7.4.5"
+ airbnb-prop-types "^2.10.0"
+ consolidated-events "^1.1.1 || ^2.0.0"
+ enzyme-shallow-equal "^1.0.0"
+ is-touch-device "^1.0.1"
+ lodash "^4.1.1"
+ object.assign "^4.1.0"
+ object.values "^1.0.4"
+ prop-types "^15.6.1"
+ raf "^3.4.1"
+ react-moment-proptypes "^1.6.0"
+ react-outside-click-handler "^1.2.0"
+ react-portal "^4.1.5"
+ react-with-direction "^1.3.0"
+ react-with-styles "^3.2.0"
+ react-with-styles-interface-css "^4.0.2"
+
+react-dom@16.8.3:
+ version "16.8.3"
+ resolved "https://registry.yarnpkg.com/react-dom/-/react-dom-16.8.3.tgz#ae236029e66210783ac81999d3015dfc475b9c32"
+ dependencies:
+ loose-envify "^1.1.0"
+ object-assign "^4.1.1"
+ prop-types "^15.6.2"
+ scheduler "^0.13.3"
+
+react-dropzone@^10.0.6:
+ version "10.2.1"
+ resolved "https://registry.yarnpkg.com/react-dropzone/-/react-dropzone-10.2.1.tgz#b7520124c4a3b66f96d49f7879027c7a475eaa20"
+ integrity sha512-Me5nOu8hK9/Xyg5easpdfJ6SajwUquqYR/2YTdMotsCUgJ1pHIIwNsv0n+qcIno0tWR2V2rVQtj2r/hXYs2TnQ==
+ dependencies:
+ attr-accept "^2.0.0"
+ file-selector "^0.1.12"
+ prop-types "^15.7.2"
+
+react-fast-compare@^2.0.2:
+ version "2.0.4"
+ resolved "https://registry.yarnpkg.com/react-fast-compare/-/react-fast-compare-2.0.4.tgz#e84b4d455b0fec113e0402c329352715196f81f9"
+
+react-final-form-arrays@^2.0.1:
+ version "2.0.3"
+ resolved "https://registry.yarnpkg.com/react-final-form-arrays/-/react-final-form-arrays-2.0.3.tgz#938bd0d62741ab59c6a680968f79eac029e37333"
+ integrity sha512-iHGGAbOVsTlp/lhE6EUsqzhjPZSvcExLWE91Qx0remHOJvQ2wkxHe8HTA9KBlId8YJ1fkz6g417GTq/1IFnL9w==
+ dependencies:
+ "@babel/runtime" "^7.3.4"
+ react-lifecycles-compat "^3.0.4"
+
+react-final-form@^4.0.2:
+ version "4.1.0"
+ resolved "https://registry.yarnpkg.com/react-final-form/-/react-final-form-4.1.0.tgz#4e1b513de164771b2b824f3fb9c0548014255971"
+ integrity sha512-O8p1EPQ/PFWNcX3bYGsLzuo/KnGeNfGfFi2UAX8jXLXrGcGdTfZMnyo/DFHdEKA9aKso61d/PHekQ9sst0cOmw==
+ dependencies:
+ "@babel/runtime" "^7.3.4"
+
+react-helmet-async@^0.2.0:
+ version "0.2.0"
+ resolved "https://registry.yarnpkg.com/react-helmet-async/-/react-helmet-async-0.2.0.tgz#d20d8725c1dcdcc95d54e281a1040af47c3abffa"
+ dependencies:
+ invariant "^2.2.4"
+ prop-types "^15.6.1"
+ react-fast-compare "^2.0.2"
+ shallowequal "^1.0.2"
+
+react-hot-loader@4.7.1:
+ version "4.7.1"
+ resolved "https://registry.yarnpkg.com/react-hot-loader/-/react-hot-loader-4.7.1.tgz#1260b939297859cff35aff61412887a92c9b4275"
+ dependencies:
+ fast-levenshtein "^2.0.6"
+ global "^4.3.0"
+ hoist-non-react-statics "^2.5.0"
+ loader-utils "^1.1.0"
+ lodash.merge "^4.6.1"
+ prop-types "^15.6.1"
+ react-lifecycles-compat "^3.0.4"
+ shallowequal "^1.0.2"
+ source-map "^0.7.3"
+
+react-iframe-resizer-super@^0.2.2:
+ version "0.2.2"
+ resolved "https://registry.yarnpkg.com/react-iframe-resizer-super/-/react-iframe-resizer-super-0.2.2.tgz#6838b52f1bdc85f004b71090aaca7fde8f26da1d"
+ dependencies:
+ babel-runtime "^6.5.0"
+
+react-is@^16.3.2:
+ version "16.5.2"
+ resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.5.2.tgz#e2a7b7c3f5d48062eb769fcb123505eb928722e3"
+
+react-is@^16.7.0:
+ version "16.8.1"
+ resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.8.1.tgz#a80141e246eb894824fb4f2901c0c50ef31d4cdb"
+
+react-is@^16.8.1:
+ version "16.8.3"
+ resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.8.3.tgz#4ad8b029c2a718fc0cfc746c8d4e1b7221e5387d"
+
+react-is@^16.8.6:
+ version "16.8.6"
+ resolved "https://registry.npmjs.org/react-is/-/react-is-16.8.6.tgz#5bbc1e2d29141c9fbdfed456343fe2bc430a6a16"
+ integrity sha512-aUk3bHfZ2bRSVFFbbeVS4i+lNPZr3/WM5jT2J5omUVV1zzcs1nAaf3l51ctA5FFvCRbhrH0bdAsRRQddFJZPtA==
+
+react-json-view@^1.19.1:
+ version "1.19.1"
+ resolved "https://registry.yarnpkg.com/react-json-view/-/react-json-view-1.19.1.tgz#95d8e59e024f08a25e5dc8f076ae304eed97cf5c"
+ dependencies:
+ flux "^3.1.3"
+ react-base16-styling "^0.6.0"
+ react-lifecycles-compat "^3.0.4"
+ react-textarea-autosize "^6.1.0"
+
+react-jss@8.6.1:
+ version "8.6.1"
+ resolved "https://registry.yarnpkg.com/react-jss/-/react-jss-8.6.1.tgz#a06e2e1d2c4d91b4d11befda865e6c07fbd75252"
+ dependencies:
+ hoist-non-react-statics "^2.5.0"
+ jss "^9.7.0"
+ jss-preset-default "^4.3.0"
+ prop-types "^15.6.0"
+ theming "^1.3.0"
+
+react-lifecycles-compat@^3.0.0, 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-modal@^3.8.1:
+ version "3.8.1"
+ resolved "https://registry.yarnpkg.com/react-modal/-/react-modal-3.8.1.tgz#7300f94a6f92a2e17994de0be6ccb61734464c9e"
+ dependencies:
+ exenv "^1.2.0"
+ prop-types "^15.5.10"
+ react-lifecycles-compat "^3.0.0"
+ warning "^3.0.0"
+
+react-moment-proptypes@^1.6.0:
+ version "1.6.0"
+ resolved "https://registry.npmjs.org/react-moment-proptypes/-/react-moment-proptypes-1.6.0.tgz#8ec266ee392a08ba3412d2df2eebf833ab1046df"
+ integrity sha512-4h7EuhDMTzQqZ+02KUUO+AVA7PqhbD88yXB740nFpNDyDS/bj9jiPyn2rwr9sa8oDyaE1ByFN9+t5XPyPTmN6g==
+ dependencies:
+ moment ">=1.6.0"
+
+react-outside-click-handler@^1.2.0:
+ version "1.2.4"
+ resolved "https://registry.npmjs.org/react-outside-click-handler/-/react-outside-click-handler-1.2.4.tgz#020428af92fcee61c42a194c7e3016c43fe99534"
+ integrity sha512-FwLnTllTa65O/HjIyDgIrlAKcgPeXQnRUE+iR1EV4NY5opzN37S87+AtO1FF0rAa8qBDKj2QuNp4VfkjmkiB7g==
+ dependencies:
+ airbnb-prop-types "^2.13.2"
+ consolidated-events "^1.1.1 || ^2.0.0"
+ document.contains "^1.0.1"
+ object.values "^1.1.0"
+ prop-types "^15.7.2"
+
+react-popper@^1.3.3:
+ version "1.3.3"
+ resolved "https://registry.yarnpkg.com/react-popper/-/react-popper-1.3.3.tgz#2c6cef7515a991256b4f0536cd4bdcb58a7b6af6"
+ dependencies:
+ "@babel/runtime" "^7.1.2"
+ create-react-context "<=0.2.2"
+ popper.js "^1.14.4"
+ prop-types "^15.6.1"
+ typed-styles "^0.0.7"
+ warning "^4.0.2"
+
+react-portal@^4.1.5:
+ version "4.2.0"
+ resolved "https://registry.npmjs.org/react-portal/-/react-portal-4.2.0.tgz#5400831cdb0ae64dccb8128121cf076089ab1afd"
+ integrity sha512-Zf+vGQ/VEAb5XAy+muKEn48yhdCNYPZaB1BWg1xc8sAZWD8pXTgPtQT4ihBdmWzsfCq8p8/kqf0GWydSBqc+Eg==
+ dependencies:
+ prop-types "^15.5.8"
+
+react-resize-aware@^2.7.2:
+ version "2.7.2"
+ resolved "https://registry.yarnpkg.com/react-resize-aware/-/react-resize-aware-2.7.2.tgz#38a0040daaa28dfa9b88994889fbb1e2aa66df83"
+
+react-resize-detector@^2.3.0:
+ version "2.3.0"
+ resolved "https://registry.npmjs.org/react-resize-detector/-/react-resize-detector-2.3.0.tgz#57bad1ae26a28a62a2ddb678ba6ffdf8fa2b599c"
+ integrity sha512-oCAddEWWeFWYH5FAcHdBYcZjAw9fMzRUK9sWSx6WvSSOPVRxcHd5zTIGy/mOus+AhN/u6T4TMiWxvq79PywnJQ==
+ dependencies:
+ lodash.debounce "^4.0.8"
+ lodash.throttle "^4.1.1"
+ prop-types "^15.6.0"
+ resize-observer-polyfill "^1.5.0"
+
+react-router-dom@4.3.1:
+ version "4.3.1"
+ resolved "https://registry.yarnpkg.com/react-router-dom/-/react-router-dom-4.3.1.tgz#4c2619fc24c4fa87c9fd18f4fb4a43fe63fbd5c6"
+ dependencies:
+ history "^4.7.2"
+ invariant "^2.2.4"
+ loose-envify "^1.3.1"
+ prop-types "^15.6.1"
+ react-router "^4.3.1"
+ warning "^4.0.1"
+
+react-router@^4.3.1:
+ version "4.3.1"
+ resolved "https://registry.yarnpkg.com/react-router/-/react-router-4.3.1.tgz#aada4aef14c809cb2e686b05cee4742234506c4e"
+ dependencies:
+ history "^4.7.2"
+ hoist-non-react-statics "^2.5.0"
+ invariant "^2.2.4"
+ loose-envify "^1.3.1"
+ path-to-regexp "^1.7.0"
+ prop-types "^15.6.1"
+ warning "^4.0.1"
+
+react-smooth@^1.0.0:
+ version "1.0.2"
+ resolved "https://registry.npmjs.org/react-smooth/-/react-smooth-1.0.2.tgz#f7a2d932ece8db898646078c3c97f3e9533e0486"
+ integrity sha512-pIGzL1g9VGAsRsdZQokIK0vrCkcdKtnOnS1gyB2rrowdLy69lNSWoIjCTWAfgbiYvria8tm5hEZqj+jwXMkV4A==
+ dependencies:
+ lodash "~4.17.4"
+ prop-types "^15.6.0"
+ raf "^3.4.0"
+ react-transition-group "^2.5.0"
+
+react-sortablejs@^2.0.7:
+ version "2.0.7"
+ resolved "https://registry.yarnpkg.com/react-sortablejs/-/react-sortablejs-2.0.7.tgz#654fd40f36918e78ac9e40a059e27862a96e786b"
+ integrity sha512-1IbPXUiOVcgUhfBj31bOPbPtwPTO5xlHJ3VJqdJaKeZX5ohpe73reltx6BWITyLzNSsTuHw4u3ulQiV6sUsjNA==
+ dependencies:
+ "@types/sortablejs" "^1.10.0"
+ classnames "^2.2.6"
+ sortablejs "1.10.1"
+ tiny-invariant "^1.0.6"
+
+react-tabs@^3.0.0:
+ version "3.0.0"
+ resolved "https://registry.yarnpkg.com/react-tabs/-/react-tabs-3.0.0.tgz#60311a17c755eb6aa9b3310123e67db421605127"
+ dependencies:
+ classnames "^2.2.0"
+ prop-types "^15.5.0"
+
+react-textarea-autosize@^6.1.0:
+ version "6.1.0"
+ resolved "https://registry.yarnpkg.com/react-textarea-autosize/-/react-textarea-autosize-6.1.0.tgz#df91387f8a8f22020b77e3833c09829d706a09a5"
+ dependencies:
+ prop-types "^15.6.0"
+
+react-transition-group@^2.5.0:
+ version "2.9.0"
+ resolved "https://registry.npmjs.org/react-transition-group/-/react-transition-group-2.9.0.tgz#df9cdb025796211151a436c69a8f3b97b5b07c8d"
+ integrity sha512-+HzNTCHpeQyl4MJ/bdE0u6XRMe9+XG/+aL4mCxVN4DnPBQ0/5bfHWPDuOZUzYdMj94daZaZdCCc1Dzt9R/xSSg==
+ dependencies:
+ dom-helpers "^3.4.0"
+ loose-envify "^1.4.0"
+ prop-types "^15.6.2"
+ react-lifecycles-compat "^3.0.4"
+
+react-virtualized-auto-sizer@^1.0.2:
+ version "1.0.2"
+ resolved "https://registry.yarnpkg.com/react-virtualized-auto-sizer/-/react-virtualized-auto-sizer-1.0.2.tgz#a61dd4f756458bbf63bd895a92379f9b70f803bd"
+
+react-with-direction@^1.3.0:
+ version "1.3.0"
+ resolved "https://registry.npmjs.org/react-with-direction/-/react-with-direction-1.3.0.tgz#9885f5941aa986be753db95a41e8f3d8f8de97ff"
+ integrity sha512-2TflEebNckTNUybw3Rzqjg4BwM/H380ZL5lsbZ5f4UTY2JyE5uQdQZK5T2w+BDJSAMcqoA2RDJYa4e7Cl6C2Kg==
+ dependencies:
+ airbnb-prop-types "^2.8.1"
+ brcast "^2.0.2"
+ deepmerge "^1.5.1"
+ direction "^1.0.1"
+ hoist-non-react-statics "^2.3.1"
+ object.assign "^4.1.0"
+ object.values "^1.0.4"
+ prop-types "^15.6.0"
+
+react-with-styles-interface-css@^4.0.2:
+ version "4.0.3"
+ resolved "https://registry.npmjs.org/react-with-styles-interface-css/-/react-with-styles-interface-css-4.0.3.tgz#c4a61277b2b8e4126b2cd25eca3ac4097bd2af09"
+ integrity sha512-wE43PIyjal2dexxyyx4Lhbcb+E42amoYPnkunRZkb9WTA+Z+9LagbyxwsI352NqMdFmghR0opg29dzDO4/YXbw==
+ dependencies:
+ array.prototype.flat "^1.2.1"
+ global-cache "^1.2.1"
+
+react-with-styles@^3.2.0:
+ version "3.2.3"
+ resolved "https://registry.npmjs.org/react-with-styles/-/react-with-styles-3.2.3.tgz#b058584065bb36c0d80ccc911725492692db8a61"
+ integrity sha512-MTI1UOvMHABRLj5M4WpODfwnveHaip6X7QUMI2x6zovinJiBXxzhA9AJP7MZNaKqg1JRFtHPXZdroUC8KcXwlQ==
+ dependencies:
+ hoist-non-react-statics "^3.2.1"
+ object.assign "^4.1.0"
+ prop-types "^15.6.2"
+ react-with-direction "^1.3.0"
+
+react@16.8.3:
+ version "16.8.3"
+ resolved "https://registry.yarnpkg.com/react/-/react-16.8.3.tgz#c6f988a2ce895375de216edcfaedd6b9a76451d9"
+ dependencies:
+ loose-envify "^1.1.0"
+ object-assign "^4.1.1"
+ prop-types "^15.6.2"
+ scheduler "^0.13.3"
+
+reactcss@^1.2.0:
+ version "1.2.3"
+ resolved "https://registry.yarnpkg.com/reactcss/-/reactcss-1.2.3.tgz#c00013875e557b1cf0dfd9a368a1c3dab3b548dd"
+ dependencies:
+ lodash "^4.0.1"
+
+read-chunk@^1.0.1:
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/read-chunk/-/read-chunk-1.0.1.tgz#5f68cab307e663f19993527d9b589cace4661194"
+
+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@^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.4, readable-stream@^2.0.6, readable-stream@^2.1.5, readable-stream@^2.2.2, readable-stream@^2.3.3:
+ 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@1.0:
+ version "1.0.34"
+ resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-1.0.34.tgz#125820e34bc842d2f2aaafafe4c2916ee32c157c"
+ dependencies:
+ core-util-is "~1.0.0"
+ inherits "~2.0.1"
+ isarray "0.0.1"
+ string_decoder "~0.10.x"
+
+readable-stream@1.1:
+ version "1.1.13"
+ resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-1.1.13.tgz#f6eef764f514c89e2b9e23146a75ba106756d23e"
+ dependencies:
+ core-util-is "~1.0.0"
+ inherits "~2.0.1"
+ isarray "0.0.1"
+ string_decoder "~0.10.x"
+
+readable-stream@^3.0.6:
+ version "3.1.1"
+ resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-3.1.1.tgz#ed6bbc6c5ba58b090039ff18ce670515795aeb06"
+ dependencies:
+ inherits "^2.0.3"
+ string_decoder "^1.1.1"
+ util-deprecate "^1.0.1"
+
+readdirp@^2.0.0:
+ version "2.1.0"
+ resolved "https://registry.yarnpkg.com/readdirp/-/readdirp-2.1.0.tgz#4ed0ad060df3073300c48440373f72d1cc642d78"
+ dependencies:
+ graceful-fs "^4.1.2"
+ minimatch "^3.0.2"
+ readable-stream "^2.0.2"
+ set-immediate-shim "^1.0.1"
+
+recharts-scale@^0.4.2:
+ version "0.4.2"
+ resolved "https://registry.npmjs.org/recharts-scale/-/recharts-scale-0.4.2.tgz#b66315d985cd9b80d5f7d977a5aab9a305abc354"
+ integrity sha512-p/cKt7j17D1CImLgX2f5+6IXLbRHGUQkogIp06VUoci/XkhOQiGSzUrsD1uRmiI7jha4u8XNFOjkHkzzBPivMg==
+ dependencies:
+ decimal.js-light "^2.4.1"
+
+recharts@^1.6.2:
+ version "1.6.2"
+ resolved "https://registry.npmjs.org/recharts/-/recharts-1.6.2.tgz#4ced884f04b680e8dac5d3e109f99b0e7cfb9b0f"
+ integrity sha512-NqVN8Hq5wrrBthTxQB+iCnZjup1dc+AYRIB6Q9ck9UjdSJTt4PbLepGpudQEYJEN5iIpP/I2vThC4uiTJa7xUQ==
+ dependencies:
+ classnames "^2.2.5"
+ core-js "^2.5.1"
+ d3-interpolate "^1.3.0"
+ d3-scale "^2.1.0"
+ d3-shape "^1.2.0"
+ lodash "^4.17.5"
+ prop-types "^15.6.0"
+ react-resize-detector "^2.3.0"
+ react-smooth "^1.0.0"
+ recharts-scale "^0.4.2"
+ reduce-css-calc "^1.3.0"
+
+reduce-css-calc@^1.3.0:
+ version "1.3.0"
+ resolved "https://registry.npmjs.org/reduce-css-calc/-/reduce-css-calc-1.3.0.tgz#747c914e049614a4c9cfbba629871ad1d2927716"
+ integrity sha1-dHyRTgSWFKTJz7umKYca0dKSdxY=
+ dependencies:
+ balanced-match "^0.4.2"
+ math-expression-evaluator "^1.2.14"
+ reduce-function-call "^1.0.1"
+
+reduce-css-calc@^2.0.0:
+ version "2.1.4"
+ resolved "https://registry.yarnpkg.com/reduce-css-calc/-/reduce-css-calc-2.1.4.tgz#c20e9cda8445ad73d4ff4bea960c6f8353791708"
+ dependencies:
+ css-unit-converter "^1.1.1"
+ postcss-value-parser "^3.3.0"
+
+reduce-function-call@^1.0.1:
+ version "1.0.2"
+ resolved "https://registry.npmjs.org/reduce-function-call/-/reduce-function-call-1.0.2.tgz#5a200bf92e0e37751752fe45b0ab330fd4b6be99"
+ integrity sha1-WiAL+S4ON3UXUv5FsKszD9S2vpk=
+ dependencies:
+ balanced-match "^0.4.2"
+
+reflect.ownkeys@^0.2.0:
+ version "0.2.0"
+ resolved "https://registry.npmjs.org/reflect.ownkeys/-/reflect.ownkeys-0.2.0.tgz#749aceec7f3fdf8b63f927a04809e90c5c0b3460"
+ integrity sha1-dJrO7H8/34tj+SegSAnpDFwLNGA=
+
+regenerate@^1.2.1:
+ version "1.3.3"
+ resolved "https://registry.yarnpkg.com/regenerate/-/regenerate-1.3.3.tgz#0c336d3980553d755c39b586ae3b20aa49c82b7f"
+
+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.12.0:
+ version "0.12.1"
+ resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.12.1.tgz#fa1a71544764c036f8c49b13a08b2594c9f8a0de"
+ integrity sha512-odxIc1/vDlo4iZcfXqRYFj0vpXFNoGdKMAUieAlFYO6m/nl5e9KR/beGf41z4a1FI+aQgtjhuaSlDxQ0hmkrHg==
+
+regenerator-runtime@^0.13.2:
+ version "0.13.3"
+ resolved "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.3.tgz#7cf6a77d8f5c6f60eb73c5fc1955b2ceb01e6bf5"
+ integrity sha512-naKIZz2GQ8JWh///G7L3X6LaQUAMp2lvb1rvwwsURe/VXwD6VMfr+/1NuNw3ag8v2kY1aQ/go5SNn79O9JU7yw==
+
+regenerator-transform@^0.10.0:
+ version "0.10.1"
+ resolved "https://registry.yarnpkg.com/regenerator-transform/-/regenerator-transform-0.10.1.tgz#1e4996837231da8b7f3cf4114d71b5691a0680dd"
+ dependencies:
+ babel-runtime "^6.18.0"
+ babel-types "^6.19.0"
+ private "^0.1.6"
+
+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"
+
+regexpp@^2.0.1:
+ version "2.0.1"
+ resolved "https://registry.yarnpkg.com/regexpp/-/regexpp-2.0.1.tgz#8d19d31cf632482b589049f8281f93dbcba4d07f"
+
+regexpu-core@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/regexpu-core/-/regexpu-core-1.0.0.tgz#86a763f58ee4d7c2f6b102e4764050de7ed90c6b"
+ dependencies:
+ regenerate "^1.2.1"
+ regjsgen "^0.2.0"
+ regjsparser "^0.1.4"
+
+regexpu-core@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/regexpu-core/-/regexpu-core-2.0.0.tgz#49d038837b8dcf8bfa5b9a42139938e6ea2ae240"
+ dependencies:
+ regenerate "^1.2.1"
+ regjsgen "^0.2.0"
+ regjsparser "^0.1.4"
+
+regjsgen@^0.2.0:
+ version "0.2.0"
+ resolved "https://registry.yarnpkg.com/regjsgen/-/regjsgen-0.2.0.tgz#6c016adeac554f75823fe37ac05b92d5a4edb1f7"
+
+regjsparser@^0.1.4:
+ version "0.1.5"
+ resolved "https://registry.yarnpkg.com/regjsparser/-/regjsparser-0.1.5.tgz#7ee8f84dc6fa792d3fd0ae228d24bd949ead205c"
+ dependencies:
+ jsesc "~0.5.0"
+
+relateurl@0.2.x:
+ version "0.2.7"
+ resolved "https://registry.yarnpkg.com/relateurl/-/relateurl-0.2.7.tgz#54dbf377e51440aca90a4cd274600d3ff2d888a9"
+
+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"
+
+renderkid@^2.0.1:
+ version "2.0.1"
+ resolved "https://registry.yarnpkg.com/renderkid/-/renderkid-2.0.1.tgz#898cabfc8bede4b7b91135a3ffd323e58c0db319"
+ dependencies:
+ css-select "^1.1.0"
+ dom-converter "~0.1"
+ htmlparser2 "~3.3.0"
+ strip-ansi "^3.0.0"
+ utila "~0.3"
+
+repeat-element@^1.1.2:
+ version "1.1.2"
+ resolved "https://registry.yarnpkg.com/repeat-element/-/repeat-element-1.1.2.tgz#ef089a178d1483baae4d93eb98b4f9e4e11d990a"
+
+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"
+
+replace-ext@0.0.1:
+ version "0.0.1"
+ resolved "https://registry.yarnpkg.com/replace-ext/-/replace-ext-0.0.1.tgz#29bbd92078a739f0bcce2b4ee41e837953522924"
+
+request-progress@^2.0.1:
+ version "2.0.1"
+ resolved "https://registry.yarnpkg.com/request-progress/-/request-progress-2.0.1.tgz#5d36bb57961c673aa5b788dbc8141fdf23b44e08"
+ dependencies:
+ throttleit "^1.0.0"
+
+request@^2.65.0, request@^2.81.0:
+ version "2.85.0"
+ resolved "https://registry.yarnpkg.com/request/-/request-2.85.0.tgz#5a03615a47c61420b3eb99b7dba204f83603e1fa"
+ dependencies:
+ aws-sign2 "~0.7.0"
+ aws4 "^1.6.0"
+ caseless "~0.12.0"
+ combined-stream "~1.0.5"
+ extend "~3.0.1"
+ forever-agent "~0.6.1"
+ form-data "~2.3.1"
+ har-validator "~5.0.3"
+ hawk "~6.0.2"
+ http-signature "~1.2.0"
+ is-typedarray "~1.0.0"
+ isstream "~0.1.2"
+ json-stringify-safe "~5.0.1"
+ mime-types "~2.1.17"
+ oauth-sign "~0.8.2"
+ performance-now "^2.1.0"
+ qs "~6.5.1"
+ safe-buffer "^5.1.1"
+ stringstream "~0.0.5"
+ tough-cookie "~2.3.3"
+ tunnel-agent "^0.6.0"
+ uuid "^3.1.0"
+
+require-directory@^2.1.1:
+ version "2.1.1"
+ resolved "https://registry.yarnpkg.com/require-directory/-/require-directory-2.1.1.tgz#8c64ad5fd30dab1c976e2344ffe7f792a6a6df42"
+
+require-main-filename@^1.0.1:
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/require-main-filename/-/require-main-filename-1.0.1.tgz#97f717b69d48784f5f526a6c5aa8ffdda055a4d1"
+
+require-package-name@^2.0.1:
+ version "2.0.1"
+ resolved "https://registry.yarnpkg.com/require-package-name/-/require-package-name-2.0.1.tgz#c11e97276b65b8e2923f75dabf5fb2ef0c3841b9"
+
+requires-port@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/requires-port/-/requires-port-1.0.0.tgz#925d2601d39ac485e091cf0da5c6e694dc3dcaff"
+
+resize-img@^1.1.0:
+ version "1.1.2"
+ resolved "https://registry.yarnpkg.com/resize-img/-/resize-img-1.1.2.tgz#fad650faf3ef2c53ea63112bc272d95e9d92550e"
+ dependencies:
+ bmp-js "0.0.1"
+ file-type "^3.8.0"
+ get-stream "^2.0.0"
+ jimp "^0.2.21"
+ jpeg-js "^0.1.1"
+ parse-png "^1.1.1"
+
+resize-observer-polyfill@^1.5.0:
+ version "1.5.0"
+ resolved "https://registry.yarnpkg.com/resize-observer-polyfill/-/resize-observer-polyfill-1.5.0.tgz#660ff1d9712a2382baa2cad450a4716209f9ca69"
+
+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"
+ 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"
+
+resolve-pathname@^2.2.0:
+ version "2.2.0"
+ resolved "https://registry.yarnpkg.com/resolve-pathname/-/resolve-pathname-2.2.0.tgz#7e9ae21ed815fd63ab189adeee64dc831eefa879"
+
+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.4.0:
+ version "1.7.1"
+ resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.7.1.tgz#aadd656374fd298aee895bc026b8297418677fd3"
+ dependencies:
+ path-parse "^1.0.5"
+
+resolve@^1.5.0, resolve@^1.9.0:
+ version "1.9.0"
+ resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.9.0.tgz#a14c6fdfa8f92a7df1d996cb7105fa744658ea06"
+ dependencies:
+ path-parse "^1.0.6"
+
+restore-cursor@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/restore-cursor/-/restore-cursor-2.0.0.tgz#9f7ee287f82fd326d4fd162923d62129eee0dfaf"
+ dependencies:
+ onetime "^2.0.0"
+ signal-exit "^3.0.2"
+
+ret@~0.1.10:
+ version "0.1.15"
+ resolved "https://registry.yarnpkg.com/ret/-/ret-0.1.15.tgz#b8a4825d5bdb1fc3f6f53c2bc33f81388681c7bc"
+
+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.6.3, rimraf@^2.6.3:
+ version "2.6.3"
+ resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-2.6.3.tgz#b2d104fe0d8fb27cf9e0a1cda8262dd3833c6cab"
+ dependencies:
+ glob "^7.1.3"
+
+rimraf@^2.2.8, rimraf@^2.5.4, rimraf@^2.6.1, rimraf@^2.6.2:
+ version "2.6.2"
+ resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-2.6.2.tgz#2ed8150d24a16ea8651e6d6ef0f47c4158ce7a36"
+ dependencies:
+ glob "^7.0.5"
+
+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-async@^2.2.0:
+ version "2.3.0"
+ resolved "https://registry.yarnpkg.com/run-async/-/run-async-2.3.0.tgz#0371ab4ae0bdd720d4166d7dfda64ff7a445a6c0"
+ dependencies:
+ is-promise "^2.1.0"
+
+run-parallel@^1.0.0:
+ version "1.1.9"
+ resolved "https://registry.yarnpkg.com/run-parallel/-/run-parallel-1.1.9.tgz#c9dd3a7cf9f4b2c4b6244e173a6ed866e61dd679"
+
+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"
+
+rxjs@^6.4.0:
+ version "6.4.0"
+ resolved "https://registry.yarnpkg.com/rxjs/-/rxjs-6.4.0.tgz#f3bb0fe7bda7fb69deac0c16f17b50b0b8790504"
+ dependencies:
+ tslib "^1.9.0"
+
+safe-buffer@5.1.1, safe-buffer@^5.0.1, safe-buffer@^5.1.0, safe-buffer@^5.1.1, safe-buffer@~5.1.0, safe-buffer@~5.1.1:
+ version "5.1.1"
+ resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.1.1.tgz#893312af69b2123def71f57889001671eeb2c853"
+
+safe-buffer@^5.1.2:
+ version "5.1.2"
+ resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.1.2.tgz#991ec69d296e0313747d59bdfd2b745c35f8828d"
+
+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":
+ version "2.1.2"
+ resolved "https://registry.yarnpkg.com/safer-buffer/-/safer-buffer-2.1.2.tgz#44fa161b0187b9549dd84bb91802f9bd8385cd6a"
+
+sax@>=0.6.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.13.3:
+ version "0.13.3"
+ resolved "https://registry.yarnpkg.com/scheduler/-/scheduler-0.13.3.tgz#bed3c5850f62ea9c716a4d781f9daeb9b2a58896"
+ dependencies:
+ loose-envify "^1.1.0"
+ object-assign "^4.1.1"
+
+schema-utils@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/schema-utils/-/schema-utils-1.0.0.tgz#0b79a93204d7b600d4b2850d1f66c2a34951c770"
+ dependencies:
+ ajv "^6.1.0"
+ ajv-errors "^1.0.0"
+ ajv-keywords "^3.1.0"
+
+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.9.1:
+ version "1.10.3"
+ resolved "https://registry.yarnpkg.com/selfsigned/-/selfsigned-1.10.3.tgz#d628ecf9e3735f84e8bafba936b3cf85bea43823"
+ dependencies:
+ node-forge "0.7.5"
+
+"semver@2 || 3 || 4 || 5", semver@^5.4.1, semver@^5.5.0, semver@^5.6.0:
+ version "5.6.0"
+ resolved "https://registry.yarnpkg.com/semver/-/semver-5.6.0.tgz#7e74256fbaa49c75aa7c7a205cc22799cac80004"
+
+semver@^5.3.0, semver@^5.5.1:
+ version "5.5.1"
+ resolved "https://registry.yarnpkg.com/semver/-/semver-5.5.1.tgz#7dfdd8814bdb7cabc7be0fb1d734cfb66c940477"
+
+send@0.16.2:
+ version "0.16.2"
+ resolved "https://registry.yarnpkg.com/send/-/send-0.16.2.tgz#6ecca1e0f8c156d141597559848df64730a6bbc1"
+ 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.6.2"
+ mime "1.4.1"
+ ms "2.0.0"
+ on-finished "~2.3.0"
+ range-parser "~1.2.0"
+ statuses "~1.4.0"
+
+serialize-javascript@^1.4.0:
+ version "1.5.0"
+ resolved "https://registry.yarnpkg.com/serialize-javascript/-/serialize-javascript-1.5.0.tgz#1aa336162c88a890ddad5384baebc93a655161fe"
+
+serve-index@^1.7.2:
+ version "1.9.1"
+ resolved "https://registry.yarnpkg.com/serve-index/-/serve-index-1.9.1.tgz#d3768d69b1e7d82e5ce050fff5b453bea12a9239"
+ 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.13.2:
+ version "1.13.2"
+ resolved "https://registry.yarnpkg.com/serve-static/-/serve-static-1.13.2.tgz#095e8472fd5b46237db50ce486a43f4b86c6cec1"
+ dependencies:
+ encodeurl "~1.0.2"
+ escape-html "~1.0.3"
+ parseurl "~1.3.2"
+ send "0.16.2"
+
+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-immediate-shim@^1.0.1:
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/set-immediate-shim/-/set-immediate-shim-1.0.1.tgz#4b2b1b27eb808a9f8dcc481a58e5e56f599f3f61"
+
+set-value@^0.4.3:
+ version "0.4.3"
+ resolved "https://registry.yarnpkg.com/set-value/-/set-value-0.4.3.tgz#7db08f9d3d22dc7f78e53af3c3bf4666ecdfccf1"
+ dependencies:
+ extend-shallow "^2.0.1"
+ is-extendable "^0.1.1"
+ is-plain-object "^2.0.1"
+ to-object-path "^0.3.0"
+
+set-value@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/set-value/-/set-value-2.0.0.tgz#71ae4a88f0feefbbf52d1ea604f3fb315ebb6274"
+ 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, setimmediate@^1.0.5:
+ version "1.0.5"
+ resolved "https://registry.yarnpkg.com/setimmediate/-/setimmediate-1.0.5.tgz#290cbb232e306942d7d7ea9b83732ab7856f8285"
+
+setprototypeof@1.0.3:
+ version "1.0.3"
+ resolved "https://registry.yarnpkg.com/setprototypeof/-/setprototypeof-1.0.3.tgz#66567e37043eeb4f04d91bd658c0cbefb55b8e04"
+
+setprototypeof@1.1.0:
+ version "1.1.0"
+ resolved "https://registry.yarnpkg.com/setprototypeof/-/setprototypeof-1.1.0.tgz#d0bd85536887b6fe7c0d818cb962d9d91c54e656"
+
+sha.js@^2.4.0, sha.js@^2.4.8:
+ 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"
+
+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:
+ version "1.1.0"
+ resolved "https://registry.yarnpkg.com/shallowequal/-/shallowequal-1.1.0.tgz#188d521de95b9087404fd4dcb68b13df0ae4e7f8"
+
+shallowequal@^1.0.2:
+ version "1.0.2"
+ resolved "https://registry.yarnpkg.com/shallowequal/-/shallowequal-1.0.2.tgz#1561dbdefb8c01408100319085764da3fcf83f8f"
+
+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-regex@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/shebang-regex/-/shebang-regex-1.0.0.tgz#da42f49740c0b42db2ca9728571cb190c98efea3"
+
+shellwords@^0.1.1:
+ version "0.1.1"
+ resolved "https://registry.yarnpkg.com/shellwords/-/shellwords-0.1.1.tgz#d6b9181c1a48d397324c84871efbcfc73fc0654b"
+
+signal-exit@^3.0.0, signal-exit@^3.0.2:
+ version "3.0.2"
+ resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-3.0.2.tgz#b5fdc08f1287ea1178628e415e25132b73646c6d"
+
+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"
+
+slash@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/slash/-/slash-1.0.0.tgz#c41f2f6c39fc16d1cd17ad4b5d896114ae470d55"
+
+slash@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/slash/-/slash-2.0.0.tgz#de552851a1759df3a8f206535442f5ec4ddeab44"
+
+slice-ansi@^2.1.0:
+ version "2.1.0"
+ resolved "https://registry.yarnpkg.com/slice-ansi/-/slice-ansi-2.1.0.tgz#cacd7693461a637a5788d92a7dd4fba068e81636"
+ dependencies:
+ ansi-styles "^3.2.0"
+ astral-regex "^1.0.0"
+ is-fullwidth-code-point "^2.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"
+
+sntp@2.x.x:
+ version "2.1.0"
+ resolved "https://registry.yarnpkg.com/sntp/-/sntp-2.1.0.tgz#2c6cec14fedc2222739caf9b5c3d85d1cc5a2cc8"
+ dependencies:
+ hoek "4.x.x"
+
+sockjs-client@1.3.0:
+ version "1.3.0"
+ resolved "https://registry.yarnpkg.com/sockjs-client/-/sockjs-client-1.3.0.tgz#12fc9d6cb663da5739d3dc5fb6e8687da95cb177"
+ dependencies:
+ debug "^3.2.5"
+ eventsource "^1.0.7"
+ faye-websocket "~0.11.1"
+ inherits "^2.0.3"
+ json3 "^3.3.2"
+ url-parse "^1.4.3"
+
+sockjs@0.3.19:
+ version "0.3.19"
+ resolved "https://registry.yarnpkg.com/sockjs/-/sockjs-0.3.19.tgz#d976bbe800af7bd20ae08598d582393508993c0d"
+ dependencies:
+ faye-websocket "^0.10.0"
+ uuid "^3.0.1"
+
+sortablejs@1.10.1:
+ version "1.10.1"
+ resolved "https://registry.yarnpkg.com/sortablejs/-/sortablejs-1.10.1.tgz#3d52b00f871be00f00f84d99a60d120bf3dfe52c"
+ integrity sha512-N6r7GrVmO8RW1rn0cTdvK3JR0BcqecAJ0PmYMCL3ZuqTH3pY+9QyqkmJSkkLyyDvd+AJnwaxTP22Ybr/83V9hQ==
+
+source-list-map@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/source-list-map/-/source-list-map-2.0.0.tgz#aaa47403f7b245a92fbc97ea08f250d6087ed085"
+
+source-map-resolve@^0.5.0:
+ version "0.5.1"
+ resolved "https://registry.yarnpkg.com/source-map-resolve/-/source-map-resolve-0.5.1.tgz#7ad0f593f2281598e854df80f19aae4b92d7a11a"
+ dependencies:
+ atob "^2.0.0"
+ 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.4.15:
+ version "0.4.18"
+ resolved "https://registry.yarnpkg.com/source-map-support/-/source-map-support-0.4.18.tgz#0286a6de8be42641338594e97ccea75f0a2c585f"
+ dependencies:
+ source-map "^0.5.6"
+
+source-map-support@~0.5.10:
+ version "0.5.11"
+ resolved "https://registry.yarnpkg.com/source-map-support/-/source-map-support-0.5.11.tgz#efac2ce0800355d026326a0ca23e162aeac9a4e2"
+ integrity sha512-//sajEx/fGL3iw6fltKMdPvy8kL3kJ2O3iuYlRoT3k9Kb4BjOoZ+BZzaNHeuaruSt+Kf3Zk9tnfAQg9/AJqUVQ==
+ dependencies:
+ buffer-from "^1.0.0"
+ source-map "^0.6.0"
+
+source-map-support@~0.5.6:
+ version "0.5.9"
+ resolved "https://registry.yarnpkg.com/source-map-support/-/source-map-support-0.5.9.tgz#41bc953b2534267ea2d605bccfa7bfa3111ced5f"
+ 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.5.x, source-map@^0.5.0, source-map@^0.5.3, source-map@^0.5.6, source-map@^0.5.7:
+ 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.3:
+ version "0.7.3"
+ resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.7.3.tgz#5302f8169031735226544092e64981f751750383"
+
+spawn-sync@^1.0.15:
+ version "1.0.15"
+ resolved "https://registry.yarnpkg.com/spawn-sync/-/spawn-sync-1.0.15.tgz#b00799557eb7fb0c8376c29d44e8a1ea67e57476"
+ dependencies:
+ concat-stream "^1.4.7"
+ os-shim "^0.1.2"
+
+spdx-correct@^3.0.0:
+ version "3.0.0"
+ resolved "https://registry.yarnpkg.com/spdx-correct/-/spdx-correct-3.0.0.tgz#05a5b4d7153a195bc92c3c425b69f3b2a9524c82"
+ dependencies:
+ spdx-expression-parse "^3.0.0"
+ spdx-license-ids "^3.0.0"
+
+spdx-exceptions@^2.1.0:
+ version "2.1.0"
+ resolved "https://registry.yarnpkg.com/spdx-exceptions/-/spdx-exceptions-2.1.0.tgz#2c7ae61056c714a5b9b9b2b2af7d311ef5c78fe9"
+
+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.0"
+ resolved "https://registry.yarnpkg.com/spdx-license-ids/-/spdx-license-ids-3.0.0.tgz#7a7cd28470cc6d3a1cfe6d66886f6bc430d3ac87"
+
+spdy-transport@^3.0.0:
+ version "3.0.0"
+ resolved "https://registry.yarnpkg.com/spdy-transport/-/spdy-transport-3.0.0.tgz#00d4863a6400ad75df93361a1608605e5dcdcf31"
+ 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.0:
+ version "4.0.0"
+ resolved "https://registry.yarnpkg.com/spdy/-/spdy-4.0.0.tgz#81f222b5a743a329aa12cea6a390e60e9b613c52"
+ 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.14.1"
+ resolved "https://registry.yarnpkg.com/sshpk/-/sshpk-1.14.1.tgz#130f5975eddad963f1d56f92b9ac6c51fa9f83eb"
+ dependencies:
+ asn1 "~0.2.3"
+ assert-plus "^1.0.0"
+ dashdash "^1.12.0"
+ getpass "^0.1.1"
+ optionalDependencies:
+ bcrypt-pbkdf "^1.0.0"
+ ecc-jsbn "~0.1.1"
+ jsbn "~0.1.0"
+ tweetnacl "~0.14.0"
+
+ssri@^6.0.0:
+ version "6.0.1"
+ resolved "https://registry.yarnpkg.com/ssri/-/ssri-6.0.1.tgz#2a3c41b28dd45b62b63676ecb74001265ae9edd8"
+ dependencies:
+ figgy-pudding "^3.5.1"
+
+stable@~0.1.6:
+ 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.3.1 < 2", "statuses@>= 1.4.0 < 2", statuses@~1.4.0:
+ version "1.4.0"
+ resolved "https://registry.yarnpkg.com/statuses/-/statuses-1.4.0.tgz#bb73d446da2796106efcc1b601a253d6c46bd087"
+
+stream-browserify@^2.0.1:
+ version "2.0.1"
+ resolved "https://registry.yarnpkg.com/stream-browserify/-/stream-browserify-2.0.1.tgz#66266ee5f9bdb9940a4e4514cafb43bb71e5c9db"
+ dependencies:
+ inherits "~2.0.1"
+ readable-stream "^2.0.2"
+
+stream-each@^1.1.0:
+ version "1.2.2"
+ resolved "https://registry.yarnpkg.com/stream-each/-/stream-each-1.2.2.tgz#8e8c463f91da8991778765873fe4d960d8f616bd"
+ dependencies:
+ end-of-stream "^1.1.0"
+ stream-shift "^1.0.0"
+
+stream-http@^2.7.2:
+ version "2.8.1"
+ resolved "https://registry.yarnpkg.com/stream-http/-/stream-http-2.8.1.tgz#d0441be1a457a73a733a8a7b53570bebd9ef66a4"
+ dependencies:
+ builtin-status-codes "^3.0.0"
+ inherits "^2.0.1"
+ readable-stream "^2.3.3"
+ to-arraybuffer "^1.0.0"
+ xtend "^4.0.0"
+
+stream-shift@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/stream-shift/-/stream-shift-1.0.0.tgz#d5c752825e5367e786f78e18e445ea223a155952"
+
+stream-to-buffer@^0.1.0:
+ version "0.1.0"
+ resolved "https://registry.yarnpkg.com/stream-to-buffer/-/stream-to-buffer-0.1.0.tgz#26799d903ab2025c9bd550ac47171b00f8dd80a9"
+ dependencies:
+ stream-to "~0.2.0"
+
+stream-to@~0.2.0:
+ version "0.2.2"
+ resolved "https://registry.yarnpkg.com/stream-to/-/stream-to-0.2.2.tgz#84306098d85fdb990b9fa300b1b3ccf55e8ef01d"
+
+string-width@^1.0.1, string-width@^1.0.2:
+ 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@^2.0.0, string-width@^2.1.0, string-width@^2.1.1:
+ 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:
+ version "3.0.0"
+ resolved "https://registry.yarnpkg.com/string-width/-/string-width-3.0.0.tgz#5a1690a57cc78211fffd9bf24bbe24d090604eb1"
+ dependencies:
+ emoji-regex "^7.0.1"
+ is-fullwidth-code-point "^2.0.0"
+ strip-ansi "^5.0.0"
+
+string_decoder@^1.0.0, string_decoder@^1.1.1, 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"
+
+string_decoder@~0.10.x:
+ version "0.10.31"
+ resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-0.10.31.tgz#62e203bc41766c6c28c9fc84301dab1c5310fa94"
+
+stringstream@~0.0.5:
+ version "0.0.5"
+ resolved "https://registry.yarnpkg.com/stringstream/-/stringstream-0.0.5.tgz#4e484cd4de5a0bbbee18e46307710a8a81621878"
+
+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:
+ version "5.0.0"
+ resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-5.0.0.tgz#f78f68b5d0866c20b2c9b8c61b5298508dc8756f"
+ dependencies:
+ ansi-regex "^4.0.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-json-comments@^2.0.1, 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"
+
+style-loader@0.23.1:
+ version "0.23.1"
+ resolved "https://registry.yarnpkg.com/style-loader/-/style-loader-0.23.1.tgz#cb9154606f3e771ab6c4ab637026a1049174d925"
+ dependencies:
+ loader-utils "^1.1.0"
+ schema-utils "^1.0.0"
+
+stylehacks@^4.0.0:
+ version "4.0.0"
+ resolved "https://registry.yarnpkg.com/stylehacks/-/stylehacks-4.0.0.tgz#64b323951c4a24e5fc7b2ec06c137bf32d155e8a"
+ dependencies:
+ browserslist "^4.0.0"
+ postcss "^6.0.0"
+ postcss-selector-parser "^3.0.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, supports-color@^5.4.0, supports-color@^5.5.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"
+
+svg2png@~3.0.1:
+ version "3.0.1"
+ resolved "https://registry.yarnpkg.com/svg2png/-/svg2png-3.0.1.tgz#a2644d68b0231ac00af431aa163714ff17106447"
+ dependencies:
+ phantomjs-prebuilt "^2.1.10"
+ pn "^1.0.0"
+ yargs "^3.31.0"
+
+svgo@^1.0.0:
+ version "1.0.5"
+ resolved "https://registry.yarnpkg.com/svgo/-/svgo-1.0.5.tgz#7040364c062a0538abacff4401cea6a26a7a389a"
+ dependencies:
+ coa "~2.0.1"
+ colors "~1.1.2"
+ css-select "~1.3.0-rc0"
+ css-select-base-adapter "~0.1.0"
+ css-tree "1.0.0-alpha25"
+ css-url-regex "^1.1.0"
+ csso "^3.5.0"
+ js-yaml "~3.10.0"
+ mkdirp "~0.5.1"
+ object.values "^1.0.4"
+ sax "~1.2.4"
+ stable "~0.1.6"
+ unquote "~1.1.1"
+ util.promisify "~1.0.0"
+
+symbol-observable@^1.0.2, symbol-observable@^1.1.0:
+ version "1.2.0"
+ resolved "https://registry.yarnpkg.com/symbol-observable/-/symbol-observable-1.2.0.tgz#c22688aed4eab3cdc2dfeacbb561660560a00804"
+
+synchronous-promise@^2.0.5:
+ version "2.0.6"
+ resolved "https://registry.yarnpkg.com/synchronous-promise/-/synchronous-promise-2.0.6.tgz#de76e0ea2b3558c1e673942e47e714a930fa64aa"
+
+table@^5.2.3:
+ version "5.2.3"
+ resolved "https://registry.yarnpkg.com/table/-/table-5.2.3.tgz#cde0cc6eb06751c009efab27e8c820ca5b67b7f2"
+ dependencies:
+ ajv "^6.9.1"
+ lodash "^4.17.11"
+ slice-ansi "^2.1.0"
+ string-width "^3.0.0"
+
+tapable@^0.1.8:
+ version "0.1.10"
+ resolved "https://registry.yarnpkg.com/tapable/-/tapable-0.1.10.tgz#29c35707c2b70e50d07482b5d202e8ed446dafd4"
+
+tapable@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/tapable/-/tapable-1.0.0.tgz#cbb639d9002eed9c6b5975eb20598d7936f1f9f2"
+
+tapable@^1.1.0:
+ version "1.1.0"
+ resolved "https://registry.yarnpkg.com/tapable/-/tapable-1.1.0.tgz#0d076a172e3d9ba088fd2272b2668fb8d194b78c"
+
+tar@^4:
+ version "4.4.2"
+ resolved "https://registry.yarnpkg.com/tar/-/tar-4.4.2.tgz#60685211ba46b38847b1ae7ee1a24d744a2cd462"
+ dependencies:
+ chownr "^1.0.1"
+ fs-minipass "^1.2.5"
+ minipass "^2.2.4"
+ minizlib "^1.1.0"
+ mkdirp "^0.5.0"
+ safe-buffer "^5.1.2"
+ yallist "^3.0.2"
+
+terser-webpack-plugin@^1.1.0:
+ version "1.2.1"
+ resolved "https://registry.yarnpkg.com/terser-webpack-plugin/-/terser-webpack-plugin-1.2.1.tgz#7545da9ae5f4f9ae6a0ac961eb46f5e7c845cc26"
+ dependencies:
+ cacache "^11.0.2"
+ find-cache-dir "^2.0.0"
+ schema-utils "^1.0.0"
+ serialize-javascript "^1.4.0"
+ source-map "^0.6.1"
+ terser "^3.8.1"
+ webpack-sources "^1.1.0"
+ worker-farm "^1.5.2"
+
+terser-webpack-plugin@^1.2.3:
+ version "1.2.3"
+ resolved "https://registry.yarnpkg.com/terser-webpack-plugin/-/terser-webpack-plugin-1.2.3.tgz#3f98bc902fac3e5d0de730869f50668561262ec8"
+ integrity sha512-GOK7q85oAb/5kE12fMuLdn2btOS9OBZn4VsecpHDywoUC/jLhSAKOiYo0ezx7ss2EXPMzyEWFoE0s1WLE+4+oA==
+ dependencies:
+ cacache "^11.0.2"
+ find-cache-dir "^2.0.0"
+ schema-utils "^1.0.0"
+ serialize-javascript "^1.4.0"
+ source-map "^0.6.1"
+ terser "^3.16.1"
+ webpack-sources "^1.1.0"
+ worker-farm "^1.5.2"
+
+terser@^3.16.1:
+ version "3.17.0"
+ resolved "https://registry.yarnpkg.com/terser/-/terser-3.17.0.tgz#f88ffbeda0deb5637f9d24b0da66f4e15ab10cb2"
+ integrity sha512-/FQzzPJmCpjAH9Xvk2paiWrFq+5M6aVOf+2KRbwhByISDX/EujxsK+BAvrhb6H+2rtrLCHK9N01wO014vrIwVQ==
+ dependencies:
+ commander "^2.19.0"
+ source-map "~0.6.1"
+ source-map-support "~0.5.10"
+
+terser@^3.8.1:
+ version "3.13.1"
+ resolved "https://registry.yarnpkg.com/terser/-/terser-3.13.1.tgz#a02e8827fb9705fe7b609c31093d010b28cea8eb"
+ dependencies:
+ commander "~2.17.1"
+ source-map "~0.6.1"
+ source-map-support "~0.5.6"
+
+text-table@^0.2.0:
+ version "0.2.0"
+ resolved "https://registry.yarnpkg.com/text-table/-/text-table-0.2.0.tgz#7f5ee823ae805207c00af2df4a84ec3fcfa570b4"
+
+theming@^1.3.0:
+ version "1.3.0"
+ resolved "https://registry.yarnpkg.com/theming/-/theming-1.3.0.tgz#286d5bae80be890d0adc645e5ca0498723725bdc"
+ dependencies:
+ brcast "^3.0.1"
+ is-function "^1.0.1"
+ is-plain-object "^2.0.1"
+ prop-types "^15.5.8"
+
+throttleit@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/throttleit/-/throttleit-1.0.0.tgz#9e785836daf46743145a5984b6268d828528ac6c"
+
+through2@^2.0.0:
+ version "2.0.3"
+ resolved "https://registry.yarnpkg.com/through2/-/through2-2.0.3.tgz#0004569b37c7c74ba39c43f3ced78d1ad94140be"
+ dependencies:
+ readable-stream "^2.1.5"
+ xtend "~4.0.1"
+
+through@^2.3.6:
+ version "2.3.8"
+ resolved "https://registry.yarnpkg.com/through/-/through-2.3.8.tgz#0dd4c9ffaabc357960b1b724115d7e0e86a2e1f5"
+
+thunky@^1.0.2:
+ version "1.0.2"
+ resolved "https://registry.yarnpkg.com/thunky/-/thunky-1.0.2.tgz#a862e018e3fb1ea2ec3fce5d55605cf57f247371"
+
+timers-browserify@^2.0.4:
+ version "2.0.10"
+ resolved "https://registry.yarnpkg.com/timers-browserify/-/timers-browserify-2.0.10.tgz#1d28e3d2aadf1d5a5996c4e9f95601cd053480ae"
+ 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"
+ integrity sha512-FOyLWWVjG+aC0UqG76V53yAWdXfH8bO6FNmyZOuUrzDzK8DI3/JRY25UD7+g49JWM1LXwymsKERB+DzI0dTEQA==
+
+tinycolor2@^1.1.2, tinycolor2@^1.4.1:
+ version "1.4.1"
+ resolved "https://registry.yarnpkg.com/tinycolor2/-/tinycolor2-1.4.1.tgz#f4fad333447bc0b07d4dc8e9209d8f39a8ac77e8"
+
+tmp@^0.0.33:
+ version "0.0.33"
+ resolved "https://registry.yarnpkg.com/tmp/-/tmp-0.0.33.tgz#6d34335889768d21b2bcda0aa277ced3b1bfadf9"
+ dependencies:
+ os-tmpdir "~1.0.2"
+
+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"
+
+to-ico@^1.1.2:
+ version "1.1.5"
+ resolved "https://registry.yarnpkg.com/to-ico/-/to-ico-1.1.5.tgz#1d32da5f2c90922edee6b686d610c54527b5a8d5"
+ dependencies:
+ arrify "^1.0.1"
+ buffer-alloc "^1.1.0"
+ image-size "^0.5.0"
+ parse-png "^1.0.0"
+ resize-img "^1.1.0"
+
+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@^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.3:
+ version "1.0.6"
+ resolved "https://registry.yarnpkg.com/toggle-selection/-/toggle-selection-1.0.6.tgz#6e45b1263f2017fa0acc7d89d78b15b8bf77da32"
+
+toposort@^1.0.0:
+ version "1.0.7"
+ resolved "https://registry.yarnpkg.com/toposort/-/toposort-1.0.7.tgz#2e68442d9f64ec720b8cc89e6443ac6caa950029"
+
+toposort@^2.0.2:
+ version "2.0.2"
+ resolved "https://registry.yarnpkg.com/toposort/-/toposort-2.0.2.tgz#ae21768175d1559d48bef35420b2f4962f09c330"
+
+tough-cookie@~2.3.3:
+ version "2.3.4"
+ resolved "https://registry.yarnpkg.com/tough-cookie/-/tough-cookie-2.3.4.tgz#ec60cee38ac675063ffc97a5c18970578ee83655"
+ dependencies:
+ punycode "^1.4.1"
+
+trim-right@^1.0.1:
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/trim-right/-/trim-right-1.0.1.tgz#cb2e1203067e0c8de1f614094b9fe45704ea6003"
+
+trim@0.0.1:
+ version "0.0.1"
+ resolved "https://registry.yarnpkg.com/trim/-/trim-0.0.1.tgz#5858547f6b290757ee95cccc666fb50084c460dd"
+
+tryer@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/tryer/-/tryer-1.0.0.tgz#027b69fa823225e551cace3ef03b11f6ab37c1d7"
+
+ts-invariant@^0.2.1:
+ version "0.2.1"
+ resolved "https://registry.yarnpkg.com/ts-invariant/-/ts-invariant-0.2.1.tgz#3d587f9d6e3bded97bf9ec17951dd9814d5a9d3f"
+ integrity sha512-Z/JSxzVmhTo50I+LKagEISFJW3pvPCqsMWLamCTX8Kr3N5aMrnGOqcflbe5hLUzwjvgPfnLzQtHZv0yWQ+FIHg==
+ dependencies:
+ tslib "^1.9.3"
+
+ts-invariant@^0.3.2:
+ version "0.3.3"
+ resolved "https://registry.yarnpkg.com/ts-invariant/-/ts-invariant-0.3.3.tgz#b5742b1885ecf9e29c31a750307480f045ec0b16"
+ integrity sha512-UReOKsrJFGC9tUblgSRWo+BsVNbEd77Cl6WiV/XpMlkifXwNIJbknViCucHvVZkXSC/mcWeRnIGdY7uprcwvdQ==
+ dependencies:
+ tslib "^1.9.3"
+
+tslib@^1.9.0, tslib@^1.9.3:
+ version "1.9.3"
+ resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.9.3.tgz#d7e4dd79245d85428c4d7e4822a79917954ca286"
+
+tty-browserify@0.0.0:
+ version "0.0.0"
+ resolved "https://registry.yarnpkg.com/tty-browserify/-/tty-browserify-0.0.0.tgz#a157ba402da24e9bf957f9aa69d524eed42901a6"
+
+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.3.2:
+ version "0.3.2"
+ resolved "https://registry.yarnpkg.com/type-check/-/type-check-0.3.2.tgz#5884cab512cf1d355e3fb784f30804b2b520db72"
+ dependencies:
+ prelude-ls "~1.1.2"
+
+type-is@~1.6.15, type-is@~1.6.16:
+ version "1.6.16"
+ resolved "https://registry.yarnpkg.com/type-is/-/type-is-1.6.16.tgz#f89ce341541c672b25ee7ae3c73dee3b2be50194"
+ dependencies:
+ media-typer "0.3.0"
+ mime-types "~2.1.18"
+
+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"
+
+ua-parser-js@^0.7.18:
+ version "0.7.18"
+ resolved "https://registry.yarnpkg.com/ua-parser-js/-/ua-parser-js-0.7.18.tgz#a7bfd92f56edfb117083b69e31d2aa8882d4b1ed"
+
+uglify-js@3.3.x:
+ version "3.3.23"
+ resolved "https://registry.yarnpkg.com/uglify-js/-/uglify-js-3.3.23.tgz#48ea43e638364d18be292a6fdc2b5b7c35f239ab"
+ dependencies:
+ commander "~2.15.0"
+ source-map "~0.6.1"
+
+underscore@^1.8.3:
+ version "1.9.0"
+ resolved "https://registry.yarnpkg.com/underscore/-/underscore-1.9.0.tgz#31dbb314cfcc88f169cd3692d9149d81a00a73e4"
+
+union-value@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/union-value/-/union-value-1.0.0.tgz#5c71c34cb5bad5dcebe3ea0cd08207ba5aa1aea4"
+ dependencies:
+ arr-union "^3.1.0"
+ get-value "^2.0.6"
+ is-extendable "^0.1.1"
+ set-value "^0.4.3"
+
+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.0:
+ version "1.1.0"
+ resolved "https://registry.yarnpkg.com/unique-filename/-/unique-filename-1.1.0.tgz#d05f2fe4032560871f30e93cbe735eea201514f3"
+ dependencies:
+ unique-slug "^2.0.0"
+
+unique-slug@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/unique-slug/-/unique-slug-2.0.0.tgz#db6676e7c7cc0629878ff196097c78855ae9f4ab"
+ dependencies:
+ imurmurhash "^0.1.4"
+
+universalify@^0.1.0:
+ version "0.1.1"
+ resolved "https://registry.yarnpkg.com/universalify/-/universalify-0.1.1.tgz#fa71badd4437af4c148841e3b3b165f9e9e590b7"
+
+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.0.0:
+ version "1.0.5"
+ resolved "https://registry.yarnpkg.com/upath/-/upath-1.0.5.tgz#02cab9ecebe95bbec6d5fc2566325725ab6d1a73"
+
+upper-case@^1.1.1:
+ version "1.1.3"
+ resolved "https://registry.yarnpkg.com/upper-case/-/upper-case-1.1.3.tgz#f6b4501c2ec4cdd26ba78be7222961de77621598"
+
+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:
+ version "1.4.3"
+ resolved "https://registry.yarnpkg.com/url-parse/-/url-parse-1.4.3.tgz#bfaee455c889023219d757e045fa6a684ec36c15"
+ dependencies:
+ querystringify "^2.0.0"
+ requires-port "^1.0.0"
+
+url-regex@^3.0.0:
+ version "3.2.0"
+ resolved "https://registry.yarnpkg.com/url-regex/-/url-regex-3.2.0.tgz#dbad1e0c9e29e105dd0b1f09f6862f7fdb482724"
+ dependencies:
+ ip-regex "^1.0.1"
+
+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@^3.1.0:
+ version "3.1.0"
+ resolved "https://registry.yarnpkg.com/use/-/use-3.1.0.tgz#14716bf03fdfefd03040aef58d8b4b85f3a7c544"
+ dependencies:
+ kind-of "^6.0.2"
+
+util-deprecate@^1.0.1, 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, 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, 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"
+
+utila@~0.3:
+ version "0.3.3"
+ resolved "https://registry.yarnpkg.com/utila/-/utila-0.3.3.tgz#d7e8e7d7e309107092b05f8d9688824d633a4226"
+
+utila@~0.4:
+ version "0.4.0"
+ resolved "https://registry.yarnpkg.com/utila/-/utila-0.4.0.tgz#8a16a05d445657a3aea5eecc5b12a4fa5379772c"
+
+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.0.1:
+ version "3.2.1"
+ resolved "https://registry.yarnpkg.com/uuid/-/uuid-3.2.1.tgz#12c528bb9d58d0b9265d9a2f6f0fe8be17ff1f14"
+
+uuid@^3.1.0, uuid@^3.3.2:
+ version "3.3.2"
+ resolved "https://registry.yarnpkg.com/uuid/-/uuid-3.3.2.tgz#1b4af4955eb3077c501c23872fc6513811587131"
+
+v8-compile-cache@^2.0.2:
+ version "2.0.2"
+ resolved "https://registry.yarnpkg.com/v8-compile-cache/-/v8-compile-cache-2.0.2.tgz#a428b28bb26790734c4fc8bc9fa106fccebf6a6c"
+
+validate-npm-package-license@^3.0.1:
+ version "3.0.3"
+ resolved "https://registry.yarnpkg.com/validate-npm-package-license/-/validate-npm-package-license-3.0.3.tgz#81643bcbef1bdfecd4623793dc4648948ba98338"
+ dependencies:
+ spdx-correct "^3.0.0"
+ spdx-expression-parse "^3.0.0"
+
+value-equal@^0.4.0:
+ version "0.4.0"
+ resolved "https://registry.yarnpkg.com/value-equal/-/value-equal-0.4.0.tgz#c5bdd2f54ee093c04839d71ce2e4758a6890abc7"
+
+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.2"
+ resolved "https://registry.yarnpkg.com/vendors/-/vendors-1.0.2.tgz#7fcb5eef9f5623b156bcea89ec37d63676f21801"
+
+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"
+
+vinyl@^1.1.0:
+ version "1.2.0"
+ resolved "https://registry.yarnpkg.com/vinyl/-/vinyl-1.2.0.tgz#5c88036cf565e5df05558bfc911f8656df218884"
+ dependencies:
+ clone "^1.0.0"
+ clone-stats "^0.0.1"
+ replace-ext "0.0.1"
+
+vm-browserify@0.0.4:
+ version "0.0.4"
+ resolved "https://registry.yarnpkg.com/vm-browserify/-/vm-browserify-0.0.4.tgz#5d7ea45bbef9e4a6ff65f95438e0a87c357d5a73"
+ dependencies:
+ indexof "0.0.1"
+
+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.1, warning@^4.0.2:
+ version "4.0.2"
+ resolved "https://registry.yarnpkg.com/warning/-/warning-4.0.2.tgz#aa6876480872116fa3e11d434b0d0d8d91e44607"
+ dependencies:
+ loose-envify "^1.0.0"
+
+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@^1.5.0:
+ version "1.6.0"
+ resolved "https://registry.yarnpkg.com/watchpack/-/watchpack-1.6.0.tgz#4bc12c2ebe8aa277a71f1d3f14d685c7b446cd00"
+ dependencies:
+ chokidar "^2.0.2"
+ graceful-fs "^4.1.2"
+ neo-async "^2.5.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"
+
+webpack-bundle-analyzer@3.0.4:
+ version "3.0.4"
+ resolved "https://registry.yarnpkg.com/webpack-bundle-analyzer/-/webpack-bundle-analyzer-3.0.4.tgz#095638487a664162f19e3b2fb7e621b7002af4b8"
+ dependencies:
+ acorn "^5.7.3"
+ bfj "^6.1.1"
+ chalk "^2.4.1"
+ commander "^2.18.0"
+ ejs "^2.6.1"
+ express "^4.16.3"
+ filesize "^3.6.1"
+ gzip-size "^5.0.0"
+ lodash "^4.17.10"
+ mkdirp "^0.5.1"
+ opener "^1.5.1"
+ ws "^6.0.0"
+
+webpack-cli@3.2.3:
+ version "3.2.3"
+ resolved "https://registry.yarnpkg.com/webpack-cli/-/webpack-cli-3.2.3.tgz#13653549adfd8ccd920ad7be1ef868bacc22e346"
+ dependencies:
+ chalk "^2.4.1"
+ cross-spawn "^6.0.5"
+ enhanced-resolve "^4.1.0"
+ findup-sync "^2.0.0"
+ global-modules "^1.0.0"
+ import-local "^2.0.0"
+ interpret "^1.1.0"
+ loader-utils "^1.1.0"
+ supports-color "^5.5.0"
+ v8-compile-cache "^2.0.2"
+ yargs "^12.0.4"
+
+webpack-dev-middleware@^3.5.1:
+ version "3.6.0"
+ resolved "https://registry.yarnpkg.com/webpack-dev-middleware/-/webpack-dev-middleware-3.6.0.tgz#71f1b04e52ff8d442757af2be3a658237d53a3e5"
+ dependencies:
+ memory-fs "^0.4.1"
+ mime "^2.3.1"
+ range-parser "^1.0.3"
+ webpack-log "^2.0.0"
+
+webpack-dev-server@^3.2.1:
+ version "3.2.1"
+ resolved "https://registry.yarnpkg.com/webpack-dev-server/-/webpack-dev-server-3.2.1.tgz#1b45ce3ecfc55b6ebe5e36dab2777c02bc508c4e"
+ dependencies:
+ ansi-html "0.0.7"
+ bonjour "^3.5.0"
+ chokidar "^2.0.0"
+ compression "^1.5.2"
+ connect-history-api-fallback "^1.3.0"
+ debug "^4.1.1"
+ del "^3.0.0"
+ express "^4.16.2"
+ html-entities "^1.2.0"
+ http-proxy-middleware "^0.19.1"
+ import-local "^2.0.0"
+ internal-ip "^4.2.0"
+ ip "^1.1.5"
+ killable "^1.0.0"
+ loglevel "^1.4.1"
+ opn "^5.1.0"
+ portfinder "^1.0.9"
+ schema-utils "^1.0.0"
+ selfsigned "^1.9.1"
+ semver "^5.6.0"
+ serve-index "^1.7.2"
+ sockjs "0.3.19"
+ sockjs-client "1.3.0"
+ spdy "^4.0.0"
+ strip-ansi "^3.0.0"
+ supports-color "^6.1.0"
+ url "^0.11.0"
+ webpack-dev-middleware "^3.5.1"
+ webpack-log "^2.0.0"
+ yargs "12.0.2"
+
+webpack-dotenv-plugin@^2.1.0:
+ version "2.1.0"
+ resolved "https://registry.yarnpkg.com/webpack-dotenv-plugin/-/webpack-dotenv-plugin-2.1.0.tgz#366bb18712f414e8b86aa66408a9039d03dd7165"
+ dependencies:
+ dotenv-safe "^5.0.1"
+
+webpack-log@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/webpack-log/-/webpack-log-2.0.0.tgz#5b7928e0637593f119d32f6227c1e0ac31e1b47f"
+ dependencies:
+ ansi-colors "^3.0.0"
+ uuid "^3.3.2"
+
+webpack-merge@^4.2.1:
+ version "4.2.1"
+ resolved "https://registry.yarnpkg.com/webpack-merge/-/webpack-merge-4.2.1.tgz#5e923cf802ea2ace4fd5af1d3247368a633489b4"
+ dependencies:
+ lodash "^4.17.5"
+
+webpack-notifier@^1.7.0:
+ version "1.7.0"
+ resolved "https://registry.yarnpkg.com/webpack-notifier/-/webpack-notifier-1.7.0.tgz#4e03ea3ba1c0588d863935363f145d067815068a"
+ dependencies:
+ node-notifier "^5.1.2"
+ object-assign "^4.1.0"
+ strip-ansi "^3.0.1"
+
+webpack-sources@^1.0.1, webpack-sources@^1.1.0:
+ version "1.1.0"
+ resolved "https://registry.yarnpkg.com/webpack-sources/-/webpack-sources-1.1.0.tgz#a101ebae59d6507354d71d8013950a3a8b7a5a54"
+ dependencies:
+ source-list-map "^2.0.0"
+ source-map "~0.6.1"
+
+webpack-sources@^1.3.0:
+ version "1.3.0"
+ resolved "https://registry.yarnpkg.com/webpack-sources/-/webpack-sources-1.3.0.tgz#2a28dcb9f1f45fe960d8f1493252b5ee6530fa85"
+ dependencies:
+ source-list-map "^2.0.0"
+ source-map "~0.6.1"
+
+webpack@4.29.5:
+ version "4.29.5"
+ resolved "https://registry.yarnpkg.com/webpack/-/webpack-4.29.5.tgz#52b60a7b0838427c3a894cd801a11dc0836bc79f"
+ dependencies:
+ "@webassemblyjs/ast" "1.8.3"
+ "@webassemblyjs/helper-module-context" "1.8.3"
+ "@webassemblyjs/wasm-edit" "1.8.3"
+ "@webassemblyjs/wasm-parser" "1.8.3"
+ acorn "^6.0.5"
+ acorn-dynamic-import "^4.0.0"
+ ajv "^6.1.0"
+ ajv-keywords "^3.1.0"
+ chrome-trace-event "^1.0.0"
+ enhanced-resolve "^4.1.0"
+ eslint-scope "^4.0.0"
+ json-parse-better-errors "^1.0.2"
+ loader-runner "^2.3.0"
+ loader-utils "^1.1.0"
+ memory-fs "~0.4.1"
+ micromatch "^3.1.8"
+ mkdirp "~0.5.0"
+ neo-async "^2.5.0"
+ node-libs-browser "^2.0.0"
+ schema-utils "^1.0.0"
+ tapable "^1.1.0"
+ terser-webpack-plugin "^1.1.0"
+ watchpack "^1.5.0"
+ webpack-sources "^1.3.0"
+
+websocket-driver@>=0.5.1:
+ version "0.7.0"
+ resolved "https://registry.yarnpkg.com/websocket-driver/-/websocket-driver-0.7.0.tgz#0caf9d2d755d93aee049d4bdd0d3fe2cca2a24eb"
+ dependencies:
+ http-parser-js ">=0.4.0"
+ websocket-extensions ">=0.1.1"
+
+websocket-extensions@>=0.1.1:
+ version "0.1.3"
+ resolved "https://registry.yarnpkg.com/websocket-extensions/-/websocket-extensions-0.1.3.tgz#5d2ff22977003ec687a4b87073dfbbac146ccf29"
+
+whatwg-fetch@>=0.10.0:
+ version "3.0.0"
+ resolved "https://registry.yarnpkg.com/whatwg-fetch/-/whatwg-fetch-3.0.0.tgz#fc804e458cc460009b1a2b966bc8817d2578aefb"
+
+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.2.x, which@^1.2.9:
+ version "1.2.14"
+ resolved "https://registry.yarnpkg.com/which/-/which-1.2.14.tgz#9a87c4378f03e827cecaf1acdf56c736c01c14e5"
+ dependencies:
+ isexe "^2.0.0"
+
+which@^1.2.10, which@^1.3.0:
+ version "1.3.0"
+ resolved "https://registry.yarnpkg.com/which/-/which-1.3.0.tgz#ff04bdfc010ee547d780bec38e1ac1c2777d253a"
+ dependencies:
+ isexe "^2.0.0"
+
+which@^1.2.14:
+ version "1.3.1"
+ resolved "https://registry.yarnpkg.com/which/-/which-1.3.1.tgz#a45043d54f5805316da8d62f9f50918d3da70b0a"
+ dependencies:
+ isexe "^2.0.0"
+
+wide-align@^1.1.0:
+ version "1.1.2"
+ resolved "https://registry.yarnpkg.com/wide-align/-/wide-align-1.1.2.tgz#571e0f1b0604636ebc0dfc21b0339bbe31341710"
+ dependencies:
+ string-width "^1.0.2"
+
+wildcard@^1.1.0:
+ version "1.1.2"
+ resolved "https://registry.yarnpkg.com/wildcard/-/wildcard-1.1.2.tgz#a7020453084d8cd2efe70ba9d3696263de1710a5"
+
+window-size@^0.1.4:
+ version "0.1.4"
+ resolved "https://registry.yarnpkg.com/window-size/-/window-size-0.1.4.tgz#f8e1aa1ee5a53ec5bf151ffa09742a6ad7697876"
+
+wordwrap@~1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/wordwrap/-/wordwrap-1.0.0.tgz#27584810891456a4171c8d0226441ade90cbcaeb"
+
+worker-farm@^1.5.2:
+ version "1.6.0"
+ resolved "https://registry.yarnpkg.com/worker-farm/-/worker-farm-1.6.0.tgz#aecc405976fab5a95526180846f0dba288f3a4a0"
+ dependencies:
+ errno "~0.1.7"
+
+wrap-ansi@^2.0.0:
+ version "2.1.0"
+ resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-2.1.0.tgz#d8fc3d284dd05794fe84973caecdd1cf824fdd85"
+ dependencies:
+ string-width "^1.0.1"
+ strip-ansi "^3.0.1"
+
+wrappy@1:
+ version "1.0.2"
+ resolved "https://registry.yarnpkg.com/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f"
+
+write@1.0.3:
+ version "1.0.3"
+ resolved "https://registry.yarnpkg.com/write/-/write-1.0.3.tgz#0800e14523b923a387e415123c865616aae0f5c3"
+ dependencies:
+ mkdirp "^0.5.1"
+
+ws@^6.0.0:
+ version "6.0.0"
+ resolved "https://registry.yarnpkg.com/ws/-/ws-6.0.0.tgz#eaa494aded00ac4289d455bac8d84c7c651cef35"
+ dependencies:
+ async-limiter "~1.0.0"
+
+xhr@^2.0.1:
+ version "2.4.1"
+ resolved "https://registry.yarnpkg.com/xhr/-/xhr-2.4.1.tgz#ba982cced205ae5eec387169ac9dc77ca4853d38"
+ dependencies:
+ global "~4.3.0"
+ is-function "^1.0.1"
+ parse-headers "^2.0.0"
+ xtend "^4.0.0"
+
+xml-parse-from-string@^1.0.0:
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/xml-parse-from-string/-/xml-parse-from-string-1.0.1.tgz#a9029e929d3dbcded169f3c6e28238d95a5d5a28"
+
+xml2js@>=0.2.4, xml2js@^0.4.5:
+ version "0.4.19"
+ resolved "https://registry.yarnpkg.com/xml2js/-/xml2js-0.4.19.tgz#686c20f213209e94abf0d1bcf1efaa291c7827a7"
+ dependencies:
+ sax ">=0.6.0"
+ xmlbuilder "~9.0.1"
+
+xmlbuilder@~9.0.1:
+ version "9.0.7"
+ resolved "https://registry.yarnpkg.com/xmlbuilder/-/xmlbuilder-9.0.7.tgz#132ee63d2ec5565c557e20f4c22df9aca686b10d"
+
+xregexp@4.0.0:
+ version "4.0.0"
+ resolved "https://registry.yarnpkg.com/xregexp/-/xregexp-4.0.0.tgz#e698189de49dd2a18cc5687b05e17c8e43943020"
+
+xtend@^4.0.0, xtend@~4.0.1:
+ version "4.0.1"
+ resolved "https://registry.yarnpkg.com/xtend/-/xtend-4.0.1.tgz#a5c6d532be656e23db820efb943a1f04998d63af"
+
+y18n@^3.2.0:
+ version "3.2.1"
+ resolved "https://registry.yarnpkg.com/y18n/-/y18n-3.2.1.tgz#6d15fba884c08679c0d77e88e7759e811e07fa41"
+
+"y18n@^3.2.1 || ^4.0.0", 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.2:
+ version "3.0.2"
+ resolved "https://registry.yarnpkg.com/yallist/-/yallist-3.0.2.tgz#8452b4bb7e83c7c188d8041c1a837c773d6d8bb9"
+
+yargs-parser@^10.1.0:
+ version "10.1.0"
+ resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-10.1.0.tgz#7202265b89f7e9e9f2e5765e0fe735a905edbaa8"
+ dependencies:
+ camelcase "^4.1.0"
+
+yargs-parser@^11.1.1:
+ version "11.1.1"
+ resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-11.1.1.tgz#879a0865973bca9f6bab5cbdf3b1c67ec7d3bcf4"
+ dependencies:
+ camelcase "^5.0.0"
+ decamelize "^1.2.0"
+
+yargs@12.0.2:
+ version "12.0.2"
+ resolved "https://registry.yarnpkg.com/yargs/-/yargs-12.0.2.tgz#fe58234369392af33ecbef53819171eff0f5aadc"
+ dependencies:
+ cliui "^4.0.0"
+ decamelize "^2.0.0"
+ find-up "^3.0.0"
+ get-caller-file "^1.0.1"
+ os-locale "^3.0.0"
+ require-directory "^2.1.1"
+ require-main-filename "^1.0.1"
+ set-blocking "^2.0.0"
+ string-width "^2.0.0"
+ which-module "^2.0.0"
+ y18n "^3.2.1 || ^4.0.0"
+ yargs-parser "^10.1.0"
+
+yargs@^12.0.4:
+ version "12.0.5"
+ resolved "https://registry.yarnpkg.com/yargs/-/yargs-12.0.5.tgz#05f5997b609647b64f66b81e3b4b10a368e7ad13"
+ dependencies:
+ cliui "^4.0.0"
+ decamelize "^1.2.0"
+ find-up "^3.0.0"
+ get-caller-file "^1.0.1"
+ os-locale "^3.0.0"
+ require-directory "^2.1.1"
+ require-main-filename "^1.0.1"
+ set-blocking "^2.0.0"
+ string-width "^2.0.0"
+ which-module "^2.0.0"
+ y18n "^3.2.1 || ^4.0.0"
+ yargs-parser "^11.1.1"
+
+yargs@^3.31.0:
+ version "3.32.0"
+ resolved "https://registry.yarnpkg.com/yargs/-/yargs-3.32.0.tgz#03088e9ebf9e756b69751611d2a5ef591482c995"
+ dependencies:
+ camelcase "^2.0.1"
+ cliui "^3.0.3"
+ decamelize "^1.1.1"
+ os-locale "^1.4.0"
+ string-width "^1.0.1"
+ window-size "^0.1.4"
+ y18n "^3.2.0"
+
+yauzl@2.4.1:
+ version "2.4.1"
+ resolved "https://registry.yarnpkg.com/yauzl/-/yauzl-2.4.1.tgz#9528f442dab1b2284e58b4379bb194e22e0c4005"
+ dependencies:
+ fd-slicer "~1.0.1"
+
+yup@^0.26.10:
+ version "0.26.10"
+ resolved "https://registry.yarnpkg.com/yup/-/yup-0.26.10.tgz#3545839663289038faf25facfc07e11fd67c0cb1"
+ dependencies:
+ "@babel/runtime" "7.0.0"
+ fn-name "~2.0.1"
+ lodash "^4.17.10"
+ property-expr "^1.5.0"
+ synchronous-promise "^2.0.5"
+ toposort "^2.0.2"
+
+zen-observable-ts@^0.8.13:
+ version "0.8.13"
+ resolved "https://registry.yarnpkg.com/zen-observable-ts/-/zen-observable-ts-0.8.13.tgz#ae1fd77c84ef95510188b1f8bca579d7a5448fc2"
+ dependencies:
+ zen-observable "^0.8.0"
+
+zen-observable-ts@^0.8.15:
+ version "0.8.15"
+ resolved "https://registry.yarnpkg.com/zen-observable-ts/-/zen-observable-ts-0.8.15.tgz#6cf7df6aa619076e4af2f707ccf8a6290d26699b"
+ dependencies:
+ zen-observable "^0.8.0"
+
+zen-observable-ts@^0.8.18, zen-observable-ts@^0.8.9:
+ version "0.8.18"
+ resolved "https://registry.yarnpkg.com/zen-observable-ts/-/zen-observable-ts-0.8.18.tgz#ade44b1060cc4a800627856ec10b9c67f5f639c8"
+ integrity sha512-q7d05s75Rn1j39U5Oapg3HI2wzriVwERVo4N7uFGpIYuHB9ff02P/E92P9B8T7QVC93jCMHpbXH7X0eVR5LA7A==
+ dependencies:
+ tslib "^1.9.3"
+ zen-observable "^0.8.0"
+
+zen-observable@^0.8.0:
+ version "0.8.9"
+ resolved "https://registry.yarnpkg.com/zen-observable/-/zen-observable-0.8.9.tgz#0475c760ff0eda046bbdfa4dc3f95d392807ac53"