Support for grape and onboarding via react
This commit is contained in:
parent
9c162d3f0d
commit
4c5b997d65
133 changed files with 14283 additions and 14420 deletions
12
.bootstraprc
Normal file
12
.bootstraprc
Normal file
|
@ -0,0 +1,12 @@
|
||||||
|
{
|
||||||
|
"bootstrapVersion": 3,
|
||||||
|
"styleLoaders": ["style", "css", "sass"],
|
||||||
|
"extractStyles": true,
|
||||||
|
"styles": {
|
||||||
|
"mixins": true,
|
||||||
|
"grid": true,
|
||||||
|
"forms": true
|
||||||
|
},
|
||||||
|
|
||||||
|
"scripts": false
|
||||||
|
}
|
10
Gemfile
10
Gemfile
|
@ -112,7 +112,7 @@ gem 'countries'
|
||||||
group :development do
|
group :development do
|
||||||
gem 'traceroute'
|
gem 'traceroute'
|
||||||
gem 'debase'
|
gem 'debase'
|
||||||
gem 'ruby-debug-ide', '0.6.0'
|
gem 'ruby-debug-ide'
|
||||||
end
|
end
|
||||||
|
|
||||||
group :development, :test do
|
group :development, :test do
|
||||||
|
@ -155,3 +155,11 @@ gem 'foreman'
|
||||||
group :production do
|
group :production do
|
||||||
gem 'rails_autoscale_agent'
|
gem 'rails_autoscale_agent'
|
||||||
end
|
end
|
||||||
|
|
||||||
|
gem 'grape'
|
||||||
|
gem 'grape-entity', git: 'https://github.com/ruby-grape/grape-entity.git', ref: '0e04aa561373b510c2486282979085eaef2ae663'
|
||||||
|
gem 'grape-swagger'
|
||||||
|
gem 'grape-swagger-entity'
|
||||||
|
gem 'grape_url_validator'
|
||||||
|
gem 'grape_logging'
|
||||||
|
gem 'grape_devise', git: 'https://github.com/ericschultz/grape_devise.git'
|
||||||
|
|
87
Gemfile.lock
87
Gemfile.lock
|
@ -23,6 +23,24 @@ GIT
|
||||||
multi_json (~> 1.0)
|
multi_json (~> 1.0)
|
||||||
stripe (>= 1.31.0, <= 1.58.0)
|
stripe (>= 1.31.0, <= 1.58.0)
|
||||||
|
|
||||||
|
GIT
|
||||||
|
remote: https://github.com/ericschultz/grape_devise.git
|
||||||
|
revision: f1cdf2576476f0f9bf0b4f5c3e7cf07295933871
|
||||||
|
specs:
|
||||||
|
grape_devise (0.1.1)
|
||||||
|
devise (>= 2.2.8, < 4)
|
||||||
|
grape (> 0.7)
|
||||||
|
rails (> 3.2, < 5)
|
||||||
|
|
||||||
|
GIT
|
||||||
|
remote: https://github.com/ruby-grape/grape-entity.git
|
||||||
|
revision: 0e04aa561373b510c2486282979085eaef2ae663
|
||||||
|
ref: 0e04aa561373b510c2486282979085eaef2ae663
|
||||||
|
specs:
|
||||||
|
grape-entity (0.7.1)
|
||||||
|
activesupport (>= 3.0.0)
|
||||||
|
multi_json (>= 1.3.2)
|
||||||
|
|
||||||
GEM
|
GEM
|
||||||
remote: https://rubygems.org/
|
remote: https://rubygems.org/
|
||||||
specs:
|
specs:
|
||||||
|
@ -71,7 +89,11 @@ GEM
|
||||||
mail (> 2.2.5)
|
mail (> 2.2.5)
|
||||||
mime-types
|
mime-types
|
||||||
xml-simple
|
xml-simple
|
||||||
bcrypt (3.1.10)
|
axiom-types (0.1.1)
|
||||||
|
descendants_tracker (~> 0.0.4)
|
||||||
|
ice_nine (~> 0.11.0)
|
||||||
|
thread_safe (~> 0.3, >= 0.3.1)
|
||||||
|
bcrypt (3.1.11)
|
||||||
binding_of_caller (0.7.2)
|
binding_of_caller (0.7.2)
|
||||||
debug_inspector (>= 0.0.1)
|
debug_inspector (>= 0.0.1)
|
||||||
bootsnap (1.1.7)
|
bootsnap (1.1.7)
|
||||||
|
@ -95,6 +117,8 @@ GEM
|
||||||
simplecov
|
simplecov
|
||||||
url
|
url
|
||||||
coderay (1.1.2)
|
coderay (1.1.2)
|
||||||
|
coercible (1.0.0)
|
||||||
|
descendants_tracker (~> 0.0.1)
|
||||||
colorize (0.8.1)
|
colorize (0.8.1)
|
||||||
concurrent-ruby (1.0.5)
|
concurrent-ruby (1.0.5)
|
||||||
config (1.7.0)
|
config (1.7.0)
|
||||||
|
@ -115,7 +139,7 @@ GEM
|
||||||
database_cleaner (1.6.1)
|
database_cleaner (1.6.1)
|
||||||
debase (0.2.2)
|
debase (0.2.2)
|
||||||
debase-ruby_core_source (>= 0.10.2)
|
debase-ruby_core_source (>= 0.10.2)
|
||||||
debase-ruby_core_source (0.10.2)
|
debase-ruby_core_source (0.10.3)
|
||||||
debug_inspector (0.0.2)
|
debug_inspector (0.0.2)
|
||||||
deep_merge (1.2.1)
|
deep_merge (1.2.1)
|
||||||
delayed_job (4.1.2)
|
delayed_job (4.1.2)
|
||||||
|
@ -123,7 +147,9 @@ GEM
|
||||||
delayed_job_active_record (4.1.1)
|
delayed_job_active_record (4.1.1)
|
||||||
activerecord (>= 3.0, < 5.1)
|
activerecord (>= 3.0, < 5.1)
|
||||||
delayed_job (>= 3.0, < 5)
|
delayed_job (>= 3.0, < 5)
|
||||||
devise (3.4.1)
|
descendants_tracker (0.0.4)
|
||||||
|
thread_safe (~> 0.3, >= 0.3.1)
|
||||||
|
devise (3.5.10)
|
||||||
bcrypt (~> 3.0)
|
bcrypt (~> 3.0)
|
||||||
orm_adapter (~> 0.1)
|
orm_adapter (~> 0.1)
|
||||||
railties (>= 3.2.6, < 5)
|
railties (>= 3.2.6, < 5)
|
||||||
|
@ -166,6 +192,7 @@ GEM
|
||||||
dry-equalizer (~> 0.2)
|
dry-equalizer (~> 0.2)
|
||||||
dry-logic (~> 0.4, >= 0.4.0)
|
dry-logic (~> 0.4, >= 0.4.0)
|
||||||
dry-types (~> 0.12.0)
|
dry-types (~> 0.12.0)
|
||||||
|
equalizer (0.0.11)
|
||||||
erubis (2.7.0)
|
erubis (2.7.0)
|
||||||
execjs (2.5.2)
|
execjs (2.5.2)
|
||||||
factory_bot (4.8.2)
|
factory_bot (4.8.2)
|
||||||
|
@ -190,6 +217,23 @@ GEM
|
||||||
plissken
|
plissken
|
||||||
geocoder (1.2.11)
|
geocoder (1.2.11)
|
||||||
get_process_mem (0.2.1)
|
get_process_mem (0.2.1)
|
||||||
|
grape (1.0.3)
|
||||||
|
activesupport
|
||||||
|
builder
|
||||||
|
mustermann-grape (~> 1.0.0)
|
||||||
|
rack (>= 1.3.0)
|
||||||
|
rack-accept
|
||||||
|
virtus (>= 1.0.0)
|
||||||
|
grape-swagger (0.28.0)
|
||||||
|
grape (>= 0.16.2)
|
||||||
|
grape-swagger-entity (0.2.3)
|
||||||
|
grape-entity (>= 0.5.0)
|
||||||
|
grape-swagger (>= 0.20.4)
|
||||||
|
grape_logging (1.8.0)
|
||||||
|
grape
|
||||||
|
rack
|
||||||
|
grape_url_validator (1.0.0)
|
||||||
|
grape (>= 0.12.0)
|
||||||
hamster (3.0.0)
|
hamster (3.0.0)
|
||||||
concurrent-ruby (~> 1.0)
|
concurrent-ruby (~> 1.0)
|
||||||
hashie (3.4.1)
|
hashie (3.4.1)
|
||||||
|
@ -201,10 +245,12 @@ GEM
|
||||||
httparty (0.13.3)
|
httparty (0.13.3)
|
||||||
json (~> 1.8)
|
json (~> 1.8)
|
||||||
multi_xml (>= 0.5.2)
|
multi_xml (>= 0.5.2)
|
||||||
i18n (0.8.6)
|
i18n (0.9.5)
|
||||||
|
concurrent-ruby (~> 1.0)
|
||||||
i18n-js (3.0.2)
|
i18n-js (3.0.2)
|
||||||
i18n (~> 0.6, >= 0.6.6)
|
i18n (~> 0.6, >= 0.6.6)
|
||||||
i18n_data (0.8.0)
|
i18n_data (0.8.0)
|
||||||
|
ice_nine (0.11.2)
|
||||||
inflecto (0.0.2)
|
inflecto (0.0.2)
|
||||||
journey (1.0.4)
|
journey (1.0.4)
|
||||||
json (1.8.6)
|
json (1.8.6)
|
||||||
|
@ -226,9 +272,12 @@ GEM
|
||||||
money (6.10.0)
|
money (6.10.0)
|
||||||
i18n (>= 0.6.4, < 1.0)
|
i18n (>= 0.6.4, < 1.0)
|
||||||
msgpack (1.2.0)
|
msgpack (1.2.0)
|
||||||
multi_json (1.12.1)
|
multi_json (1.13.1)
|
||||||
multi_xml (0.5.5)
|
multi_xml (0.5.5)
|
||||||
multipart-post (2.0.0)
|
multipart-post (2.0.0)
|
||||||
|
mustermann (1.0.2)
|
||||||
|
mustermann-grape (1.0.0)
|
||||||
|
mustermann (~> 1.0.0)
|
||||||
nearest_time_zone (0.0.4)
|
nearest_time_zone (0.0.4)
|
||||||
andand
|
andand
|
||||||
kdtree
|
kdtree
|
||||||
|
@ -258,9 +307,11 @@ GEM
|
||||||
rabl (0.11.6)
|
rabl (0.11.6)
|
||||||
activesupport (>= 2.3.14)
|
activesupport (>= 2.3.14)
|
||||||
rack (1.4.7)
|
rack (1.4.7)
|
||||||
|
rack-accept (0.4.5)
|
||||||
|
rack (>= 0.4)
|
||||||
rack-attack (4.2.0)
|
rack-attack (4.2.0)
|
||||||
rack
|
rack
|
||||||
rack-cache (1.7.0)
|
rack-cache (1.7.2)
|
||||||
rack (>= 0.4)
|
rack (>= 0.4)
|
||||||
rack-ssl (1.3.4)
|
rack-ssl (1.3.4)
|
||||||
rack
|
rack
|
||||||
|
@ -292,7 +343,7 @@ GEM
|
||||||
rake (>= 0.8.7)
|
rake (>= 0.8.7)
|
||||||
rdoc (~> 3.4)
|
rdoc (~> 3.4)
|
||||||
thor (>= 0.14.6, < 2.0)
|
thor (>= 0.14.6, < 2.0)
|
||||||
rake (12.0.0)
|
rake (12.3.1)
|
||||||
rdoc (3.12.2)
|
rdoc (3.12.2)
|
||||||
json (~> 1.4)
|
json (~> 1.4)
|
||||||
require_all (1.3.2)
|
require_all (1.3.2)
|
||||||
|
@ -329,7 +380,7 @@ GEM
|
||||||
rspec-mocks (~> 3.5.0)
|
rspec-mocks (~> 3.5.0)
|
||||||
rspec-support (~> 3.5.0)
|
rspec-support (~> 3.5.0)
|
||||||
rspec-support (3.5.0)
|
rspec-support (3.5.0)
|
||||||
ruby-debug-ide (0.6.0)
|
ruby-debug-ide (0.6.1)
|
||||||
rake (>= 0.8.1)
|
rake (>= 0.8.1)
|
||||||
ruby-prof (0.15.9)
|
ruby-prof (0.15.9)
|
||||||
safe_yaml (1.0.4)
|
safe_yaml (1.0.4)
|
||||||
|
@ -359,7 +410,7 @@ GEM
|
||||||
test-unit (3.2.7)
|
test-unit (3.2.7)
|
||||||
power_assert
|
power_assert
|
||||||
thor (0.19.4)
|
thor (0.19.4)
|
||||||
thread_safe (0.3.5)
|
thread_safe (0.3.6)
|
||||||
tilt (1.4.1)
|
tilt (1.4.1)
|
||||||
timecop (0.7.3)
|
timecop (0.7.3)
|
||||||
traceroute (0.5.0)
|
traceroute (0.5.0)
|
||||||
|
@ -367,7 +418,7 @@ GEM
|
||||||
treetop (1.4.15)
|
treetop (1.4.15)
|
||||||
polyglot
|
polyglot
|
||||||
polyglot (>= 0.3.1)
|
polyglot (>= 0.3.1)
|
||||||
tzinfo (0.3.53)
|
tzinfo (0.3.54)
|
||||||
uglifier (2.7.1)
|
uglifier (2.7.1)
|
||||||
execjs (>= 0.3.0)
|
execjs (>= 0.3.0)
|
||||||
json (>= 1.8.0)
|
json (>= 1.8.0)
|
||||||
|
@ -377,7 +428,12 @@ GEM
|
||||||
unicode_utils (1.4.0)
|
unicode_utils (1.4.0)
|
||||||
url (0.3.2)
|
url (0.3.2)
|
||||||
vcr (2.9.3)
|
vcr (2.9.3)
|
||||||
warden (1.2.3)
|
virtus (1.0.5)
|
||||||
|
axiom-types (~> 0.1)
|
||||||
|
coercible (~> 1.0)
|
||||||
|
descendants_tracker (~> 0.0, >= 0.0.3)
|
||||||
|
equalizer (~> 0.0, >= 0.0.9)
|
||||||
|
warden (1.2.7)
|
||||||
rack (>= 1.0)
|
rack (>= 1.0)
|
||||||
webmock (1.21.0)
|
webmock (1.21.0)
|
||||||
addressable (>= 2.3.6)
|
addressable (>= 2.3.6)
|
||||||
|
@ -418,6 +474,13 @@ DEPENDENCIES
|
||||||
foreman
|
foreman
|
||||||
fullcontact
|
fullcontact
|
||||||
geocoder
|
geocoder
|
||||||
|
grape
|
||||||
|
grape-entity!
|
||||||
|
grape-swagger
|
||||||
|
grape-swagger-entity
|
||||||
|
grape_devise!
|
||||||
|
grape_logging
|
||||||
|
grape_url_validator
|
||||||
hamster
|
hamster
|
||||||
heroku-deflater
|
heroku-deflater
|
||||||
httparty
|
httparty
|
||||||
|
@ -446,7 +509,7 @@ DEPENDENCIES
|
||||||
roadie-rails
|
roadie-rails
|
||||||
rspec
|
rspec
|
||||||
rspec-rails
|
rspec-rails
|
||||||
ruby-debug-ide (= 0.6.0)
|
ruby-debug-ide
|
||||||
ruby-prof (= 0.15.9)
|
ruby-prof (= 0.15.9)
|
||||||
sass (= 3.2.19)
|
sass (= 3.2.19)
|
||||||
sass-rails (= 3.2.6)
|
sass-rails (= 3.2.6)
|
||||||
|
|
5
app/api/houdini/api.rb
Normal file
5
app/api/houdini/api.rb
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
# License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later
|
||||||
|
class Houdini::API < Grape::API
|
||||||
|
format :json
|
||||||
|
mount Houdini::V1::API => '/v1'
|
||||||
|
end
|
21
app/api/houdini/v1/api.rb
Normal file
21
app/api/houdini/v1/api.rb
Normal file
|
@ -0,0 +1,21 @@
|
||||||
|
require 'houdini/v1/validations'
|
||||||
|
class Houdini::V1::API < Grape::API
|
||||||
|
logger.formatter = GrapeLogging::Formatters::Rails.new
|
||||||
|
use GrapeLogging::Middleware::RequestLogger, { logger: logger }
|
||||||
|
content_type :json, 'application/json'
|
||||||
|
default_format :json
|
||||||
|
rescue_from Grape::Exceptions::ValidationErrors do |e|
|
||||||
|
output = {errors: e}
|
||||||
|
error! output, 400
|
||||||
|
end
|
||||||
|
|
||||||
|
#include Houdini::V1::Helpers::ApplicationHelper
|
||||||
|
mount Houdini::V1::Nonprofit => '/nonprofit'
|
||||||
|
# Additional mounts are added via generators above this line
|
||||||
|
# DON'T REMOVE THIS OR THE PREVIOUS LINES!!!
|
||||||
|
uriForHost = URI.parse(Settings.cdn.url)
|
||||||
|
add_swagger_documentation \
|
||||||
|
host: "#{uriForHost.host}#{Settings.cdn.port ? ":#{Settings.cdn.port}" : ""}",
|
||||||
|
schemes: [uriForHost.scheme],
|
||||||
|
base_path: '/api/v1'
|
||||||
|
end
|
30
app/api/houdini/v1/base_api.rb
Normal file
30
app/api/houdini/v1/base_api.rb
Normal file
|
@ -0,0 +1,30 @@
|
||||||
|
# License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later
|
||||||
|
class Houdini::V1::BaseAPI < Grape::API
|
||||||
|
#helpers ApplicationHelper
|
||||||
|
# helpers do
|
||||||
|
# def session
|
||||||
|
# env['rack.session']
|
||||||
|
# end
|
||||||
|
#
|
||||||
|
# def protect_against_forgery
|
||||||
|
# unless verified_request?
|
||||||
|
# error!('Unauthorized', 401)
|
||||||
|
# end
|
||||||
|
# end
|
||||||
|
#
|
||||||
|
# def verified_request?
|
||||||
|
# !protect_against_forgery? || request.get? || request.head? ||
|
||||||
|
# form_authenticity_token == request.headers['X-CSRF-Token'] ||
|
||||||
|
# form_authenticity_token == request.headers['X-Csrf-Token']
|
||||||
|
# end
|
||||||
|
#
|
||||||
|
# def form_authenticity_token
|
||||||
|
# session[:_csrf_token] ||= SecureRandom.base64(32)
|
||||||
|
# end
|
||||||
|
#
|
||||||
|
# def protect_against_forgery?
|
||||||
|
# allow_forgery_protection = Rails.configuration.action_controller.allow_forgery_protection
|
||||||
|
# allow_forgery_protection.nil? || allow_forgery_protection
|
||||||
|
# end
|
||||||
|
# end
|
||||||
|
end
|
4
app/api/houdini/v1/entities/nonprofit.rb
Normal file
4
app/api/houdini/v1/entities/nonprofit.rb
Normal file
|
@ -0,0 +1,4 @@
|
||||||
|
# License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later
|
||||||
|
class Houdini::V1::Entities::Nonprofit < Grape::Entity
|
||||||
|
expose :id
|
||||||
|
end
|
5
app/api/houdini/v1/entities/validation_error.rb
Normal file
5
app/api/houdini/v1/entities/validation_error.rb
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
# License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later
|
||||||
|
class Houdini::V1::Entities::ValidationError < Grape::Entity
|
||||||
|
expose :params, documentation: {type: 'String', desc: 'Params where the following had an error.', is_array: true}
|
||||||
|
expose :messages, documentation: {type:'String', desc: 'The validation messages for the params', is_array: true}
|
||||||
|
end
|
4
app/api/houdini/v1/entities/validation_errors.rb
Normal file
4
app/api/houdini/v1/entities/validation_errors.rb
Normal file
|
@ -0,0 +1,4 @@
|
||||||
|
# License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later
|
||||||
|
class Houdini::V1::Entities::ValidationErrors < Grape::Entity
|
||||||
|
expose :errors, documentation: {type: ValidationError, desc: 'errors', is_array:true}
|
||||||
|
end
|
45
app/api/houdini/v1/helpers/application_helper.rb
Normal file
45
app/api/houdini/v1/helpers/application_helper.rb
Normal file
|
@ -0,0 +1,45 @@
|
||||||
|
# License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later
|
||||||
|
module Houdini::V1::Helpers::ApplicationHelper
|
||||||
|
extend Grape::API::Helpers
|
||||||
|
|
||||||
|
|
||||||
|
def session
|
||||||
|
env['rack.session']
|
||||||
|
end
|
||||||
|
|
||||||
|
def protect_against_forgery
|
||||||
|
unless verified_request?
|
||||||
|
error!('Unauthorized', 401)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def verified_request?
|
||||||
|
!protect_against_forgery? || request.get? || request.head? ||
|
||||||
|
form_authenticity_token == request.headers['X-CSRF-Token'] ||
|
||||||
|
form_authenticity_token == request.headers['X-Csrf-Token']
|
||||||
|
end
|
||||||
|
|
||||||
|
def form_authenticity_token
|
||||||
|
session[:_csrf_token] ||= SecureRandom.base64(32)
|
||||||
|
end
|
||||||
|
|
||||||
|
def protect_against_forgery?
|
||||||
|
allow_forgery_protection = Rails.configuration.action_controller.allow_forgery_protection
|
||||||
|
allow_forgery_protection.nil? || allow_forgery_protection
|
||||||
|
end
|
||||||
|
|
||||||
|
|
||||||
|
# def rescue_ar_invalid( *class_to_hash)
|
||||||
|
# rescue_with ActiveRecord::RecordInvalid do |error|
|
||||||
|
# output = []
|
||||||
|
# error.record.errors do |attr,message|
|
||||||
|
# output.push({params: "#{class_to_hash[error.record.class]}['#{attr}']",
|
||||||
|
# message: message})
|
||||||
|
# end
|
||||||
|
# raise Grape::Exceptions::ValidationErrors.new(output)
|
||||||
|
#
|
||||||
|
# end
|
||||||
|
# end
|
||||||
|
|
||||||
|
end
|
||||||
|
|
19
app/api/houdini/v1/helpers/rescue_helper.rb
Normal file
19
app/api/houdini/v1/helpers/rescue_helper.rb
Normal file
|
@ -0,0 +1,19 @@
|
||||||
|
module Houdini::V1::Helpers::RescueHelper
|
||||||
|
require 'active_support/concern'
|
||||||
|
|
||||||
|
extend ActiveSupport::Concern
|
||||||
|
include Grape::DSL::Configuration
|
||||||
|
module ClassMethods
|
||||||
|
def rescue_ar_invalid( *class_to_hash)
|
||||||
|
rescue_with ActiveRecord::RecordInvalid do |error|
|
||||||
|
output = []
|
||||||
|
error.record.errors do |attr,message|
|
||||||
|
output.push({params: "#{class_to_hash[error.record.class]}['#{attr}']",
|
||||||
|
message: message})
|
||||||
|
end
|
||||||
|
raise Grape::Exceptions::ValidationErrors.new(output)
|
||||||
|
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
113
app/api/houdini/v1/nonprofit.rb
Normal file
113
app/api/houdini/v1/nonprofit.rb
Normal file
|
@ -0,0 +1,113 @@
|
||||||
|
# License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later
|
||||||
|
class Houdini::V1::Nonprofit < Houdini::V1::BaseAPI
|
||||||
|
helpers Houdini::V1::Helpers::ApplicationHelper, Houdini::V1::Helpers::RescueHelper
|
||||||
|
|
||||||
|
before do
|
||||||
|
protect_against_forgery
|
||||||
|
end
|
||||||
|
|
||||||
|
desc 'Return a nonprofit.' do
|
||||||
|
success Houdini::V1::Entities::Nonprofit
|
||||||
|
end
|
||||||
|
params do
|
||||||
|
requires :id, type: Integer, desc: 'Status id.'
|
||||||
|
end
|
||||||
|
route_param :id do
|
||||||
|
get do
|
||||||
|
np = Nonprofit.find(params[:id])
|
||||||
|
present np, as: Houdini::V1::Entities::Nonprofit
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
desc 'Register a nonprofit' do
|
||||||
|
success Houdini::V1::Entities::Nonprofit
|
||||||
|
|
||||||
|
#this needs to be a validation an array
|
||||||
|
failure [{code:400, message:'Validation Errors', model: Houdini::V1::Entities::ValidationErrors}]
|
||||||
|
end
|
||||||
|
|
||||||
|
params do
|
||||||
|
|
||||||
|
requires :nonprofit, type: Hash do
|
||||||
|
requires :name, type:String, desc: 'Organization Name', allow_blank: false, documentation: { param_type: 'body' }
|
||||||
|
optional :url, type:String, desc: 'Organization website URL', allow_blank:true, regexp: URI::regexp, documentation: { param_type: 'body' }
|
||||||
|
requires :zip_code, type:String, allow_blank: false, desc: "Organization Address ZIP Code", documentation: { param_type: 'body' }
|
||||||
|
requires :state_code, type:String, allow_blank: false, desc: "Organization Address State Code", documentation: { param_type: 'body' }
|
||||||
|
requires :city, type:String, allow_blank: false, desc: "Organization Address City", documentation: { param_type: 'body' }
|
||||||
|
optional :email, type:String, desc: 'Organization email (public)', regexp: Email::Regex, documentation: { param_type: 'body' }
|
||||||
|
optional :phone, type:String, desc: 'Organization phone (public)', documentation: { param_type: 'body' }
|
||||||
|
end
|
||||||
|
|
||||||
|
requires :user, type: Hash do
|
||||||
|
requires :name, type:String, desc: 'Full name', allow_blank:false, documentation: { param_type: 'body' }
|
||||||
|
requires :email, type:String, desc: 'Username', allow_blank: false, documentation: { param_type: 'body' }
|
||||||
|
requires :password, type:String, desc: 'Password', allow_blank: false, is_equal_to: :password_confirmation, documentation: { param_type: 'body' }
|
||||||
|
requires :password_confirmation, type:String, desc: 'Password confirmation', allow_blank: false, documentation: { param_type: 'body' }
|
||||||
|
end
|
||||||
|
|
||||||
|
|
||||||
|
end
|
||||||
|
post do
|
||||||
|
np = nil
|
||||||
|
u = nil
|
||||||
|
Qx.transaction do
|
||||||
|
begin
|
||||||
|
np = Nonprofit.new(OnboardAccounts.set_nonprofit_defaults(params[:nonprofit]))
|
||||||
|
|
||||||
|
begin
|
||||||
|
np.save!
|
||||||
|
rescue ActiveRecord::RecordInvalid => e
|
||||||
|
if (e.record.errors[:slug])
|
||||||
|
begin
|
||||||
|
slug = SlugNonprofitNamingAlgorithm.new(np.state_code_slug, np.city_slug).create_copy_name(np.slug)
|
||||||
|
np.slug = slug
|
||||||
|
np.save!
|
||||||
|
rescue UnableToCreateNameCopyError
|
||||||
|
raise Grape::Exceptions::ValidationErrors.new(errors:[Grape::Exceptions::Validation.new(
|
||||||
|
|
||||||
|
params: ["nonprofit[name]"],
|
||||||
|
message: "has an invalid slug. Contact support for help."
|
||||||
|
)])
|
||||||
|
end
|
||||||
|
else
|
||||||
|
raise e
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
u = User.new(params[:user])
|
||||||
|
u.save!
|
||||||
|
|
||||||
|
role = u.roles.build(host: np, name: 'nonprofit_admin')
|
||||||
|
role.save!
|
||||||
|
|
||||||
|
billing_plan = BillingPlan.find(Settings.default_bp.id)
|
||||||
|
b_sub = np.build_billing_subscription(billing_plan: billing_plan, status: 'active')
|
||||||
|
b_sub.save!
|
||||||
|
rescue ActiveRecord::RecordInvalid => e
|
||||||
|
class_to_name = {Nonprofit => 'nonprofit', User => 'user'}
|
||||||
|
if class_to_name[e.record.class]
|
||||||
|
errors = e.record.errors.keys.map {|k|
|
||||||
|
|
||||||
|
errors = e.record.errors[k].uniq
|
||||||
|
errors.map{|error| Grape::Exceptions::Validation.new(
|
||||||
|
|
||||||
|
params: ["#{class_to_name[e.record.class]}[#{k.to_s}]"],
|
||||||
|
message: error
|
||||||
|
|
||||||
|
)}
|
||||||
|
}
|
||||||
|
|
||||||
|
raise Grape::Exceptions::ValidationErrors.new(errors:errors.flatten)
|
||||||
|
else
|
||||||
|
raise e
|
||||||
|
end
|
||||||
|
|
||||||
|
end
|
||||||
|
end
|
||||||
|
#onboard callback
|
||||||
|
present np, with: Houdini::V1::Entities::Nonprofit
|
||||||
|
end
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
end
|
2
app/api/houdini/v1/validations.rb
Normal file
2
app/api/houdini/v1/validations.rb
Normal file
|
@ -0,0 +1,2 @@
|
||||||
|
# License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later
|
||||||
|
require 'houdini/v1/validators/is_equal_to'
|
8
app/api/houdini/v1/validators/is_equal_to.rb
Normal file
8
app/api/houdini/v1/validators/is_equal_to.rb
Normal file
|
@ -0,0 +1,8 @@
|
||||||
|
# License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later
|
||||||
|
class Houdini::V1::Validators::IsEqualTo < Grape::Validations::Base
|
||||||
|
def validate_param!(attr_name, params)
|
||||||
|
if params[attr_name] != params[@option]
|
||||||
|
fail Grape::Exceptions::Validation, params: [@scope.full_name(attr_name)], message: 'MESSAGE'
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
2
app/assets/javascripts/onboard.js
Normal file
2
app/assets/javascripts/onboard.js
Normal file
|
@ -0,0 +1,2 @@
|
||||||
|
// Place all the behaviors and hooks related to the matching controller here.
|
||||||
|
// All this logic will automatically be available in application.js.
|
6
app/controllers/onboard_controller.rb
Normal file
6
app/controllers/onboard_controller.rb
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
class OnboardController < ApplicationController
|
||||||
|
layout 'layouts/apified'
|
||||||
|
def index
|
||||||
|
|
||||||
|
end
|
||||||
|
end
|
|
@ -1,13 +1,13 @@
|
||||||
# License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later
|
# License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later
|
||||||
class Users::SessionsController < Devise::SessionsController
|
class Users::SessionsController < Devise::SessionsController
|
||||||
|
|
||||||
def create
|
def create
|
||||||
respond_to do |format|
|
respond_to do |format|
|
||||||
format.html { super }
|
format.html { super }
|
||||||
format.json {
|
format.json {
|
||||||
warden.authenticate!(:scope => resource_name, :recall => "#{controller_path}#new")
|
warden.authenticate!(:scope => resource_name, :recall => "#{controller_path}#new")
|
||||||
render :status => 200, :json => { :status => "Success" }
|
render :status => 200, :json => { :status => "Success" }
|
||||||
}
|
}
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
2
app/helpers/onboard_helper.rb
Normal file
2
app/helpers/onboard_helper.rb
Normal file
|
@ -0,0 +1,2 @@
|
||||||
|
module OnboardHelper
|
||||||
|
end
|
|
@ -82,7 +82,7 @@ class Nonprofit < ActiveRecord::Base
|
||||||
validates :city, presence: true
|
validates :city, presence: true
|
||||||
validates :state_code, presence: true
|
validates :state_code, presence: true
|
||||||
validates :email, format: { with: Email::Regex }, allow_blank: true
|
validates :email, format: { with: Email::Regex }, allow_blank: true
|
||||||
validates_uniqueness_of :slug, scope: [:city, :state_code]
|
validates_uniqueness_of :slug, scope: [:city_slug, :state_code_slug]
|
||||||
validates_presence_of :slug
|
validates_presence_of :slug
|
||||||
|
|
||||||
scope :vetted, -> {where(vetted: true)}
|
scope :vetted, -> {where(vetted: true)}
|
||||||
|
@ -140,9 +140,16 @@ class Nonprofit < ActiveRecord::Base
|
||||||
end
|
end
|
||||||
|
|
||||||
def set_slugs
|
def set_slugs
|
||||||
self.slug = Format::Url.convert_to_slug self.name
|
unless (self.slug)
|
||||||
self.city_slug = Format::Url.convert_to_slug self.city
|
self.slug = Format::Url.convert_to_slug self.name
|
||||||
self.state_code_slug = Format::Url.convert_to_slug self.state_code
|
end
|
||||||
|
unless (self.city_slug)
|
||||||
|
self.city_slug = Format::Url.convert_to_slug self.city
|
||||||
|
end
|
||||||
|
|
||||||
|
unless (self.state_code_slug)
|
||||||
|
self.state_code_slug = Format::Url.convert_to_slug self.state_code
|
||||||
|
end
|
||||||
self
|
self
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
27
app/views/layouts/apified.html.erb
Normal file
27
app/views/layouts/apified.html.erb
Normal file
|
@ -0,0 +1,27 @@
|
||||||
|
<%- # License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later -%>
|
||||||
|
<!doctype html>
|
||||||
|
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
|
||||||
|
|
||||||
|
<%= IncludeAsset.js '/client/js/i18n.js' %>
|
||||||
|
<script>
|
||||||
|
I18n.defaultLocale = "<%= I18n.default_locale %>"
|
||||||
|
I18n.locale = "<%= I18n.locale %>"
|
||||||
|
window._csrf = "<%= form_authenticity_token %>"
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<%= IncludeAsset.js 'app/react.js' %>
|
||||||
|
<%= IncludeAsset.js 'app/react-dom.js' %>
|
||||||
|
<%= IncludeAsset.js 'app/vendor.js' %>
|
||||||
|
<%= yield :javascripts %>
|
||||||
|
<%= render 'layouts/stylesheets' %>
|
||||||
|
<%= IncludeAsset.css 'client/css/global/page.css' %>
|
||||||
|
<%= IncludeAsset.css 'client/css/bootstrap.css' %>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<%= yield %>
|
||||||
|
|
||||||
|
</body>
|
||||||
|
</html>
|
7
app/views/onboard/index.html.erb
Normal file
7
app/views/onboard/index.html.erb
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
<% content_for :javascripts do %>
|
||||||
|
<%= IncludeAsset.js 'app/registration_pagex.js' %>
|
||||||
|
<% end %>
|
||||||
|
|
||||||
|
<div id="outlet"></div>
|
||||||
|
|
||||||
|
<script>LoadReactPage(document.getElementById('outlet'))</script>
|
|
@ -1,10 +1,10 @@
|
||||||
/* License: LGPL-3.0-or-later */
|
/* License: LGPL-3.0-or-later */
|
||||||
[class*="container"] {
|
/*[class*="container"] {*/
|
||||||
margin-left: auto;
|
/*margin-left: auto;*/
|
||||||
margin-right: auto;
|
/*margin-right: auto;*/
|
||||||
}
|
/*}*/
|
||||||
|
|
||||||
.container { max-width: 60rem; }
|
/*.container { max-width: 60rem; }*/
|
||||||
.container--medium { max-width: 50rem; }
|
/*.container--medium { max-width: 50rem; }*/
|
||||||
.container--narrow { max-width: 40rem; }
|
/*.container--narrow { max-width: 40rem; }*/
|
||||||
|
|
||||||
|
|
|
@ -2,7 +2,7 @@
|
||||||
@import 'commons.css'; /* npm */
|
@import 'commons.css'; /* npm */
|
||||||
@import 'colors.css'; /* contains variables */
|
@import 'colors.css'; /* contains variables */
|
||||||
@import 'shadows.css'; /* contains variables */
|
@import 'shadows.css'; /* contains variables */
|
||||||
@import 'typography.css';
|
/*@import 'typography.css';*/
|
||||||
@import 'icons.css';
|
@import 'icons.css';
|
||||||
@import 'containers.css';
|
@import 'containers.css';
|
||||||
@import 'buttons.css';
|
@import 'buttons.css';
|
||||||
|
|
|
@ -1,31 +1,31 @@
|
||||||
/* License: LGPL-3.0-or-later */
|
/* License: LGPL-3.0-or-later */
|
||||||
@font-face {
|
@font-face {
|
||||||
font-family: 'OpenSans';
|
font-family: 'Open Sans';
|
||||||
src: url('/fonts/OpenSans/OpenSans-Regular.ttf') format('truetype');
|
src: url('/fonts/Open_Sans/opensans-regular-webfont.ttf') format('truetype');
|
||||||
font-weight: 400;
|
font-weight: 400;
|
||||||
font-style: normal;
|
font-style: normal;
|
||||||
}
|
}
|
||||||
@font-face {
|
@font-face {
|
||||||
font-family: 'OpenSans';
|
font-family: 'Open Sans';
|
||||||
src: url('/fonts/OpenSans/OpenSans-Semibold.ttf') format('truetype');
|
src: url('/fonts/Open_Sans/OpenSans-Semibold.ttf') format('truetype');
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
font-style: normal;
|
font-style: normal;
|
||||||
}
|
}
|
||||||
@font-face {
|
@font-face {
|
||||||
font-family: 'OpenSans';
|
font-family: 'Open Sans';
|
||||||
src: url('/fonts/OpenSans/OpenSans-Bold.ttf') format('truetype');
|
src: url('/fonts/Open_Sans/opensans-bold-webfont.ttf') format('truetype');
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
font-style: normal;
|
font-style: normal;
|
||||||
}
|
}
|
||||||
|
|
||||||
body {
|
body {
|
||||||
font-family:
|
font-family:
|
||||||
|
OpenSans,
|
||||||
-apple-system,
|
-apple-system,
|
||||||
BlinkMacSystemFont,
|
BlinkMacSystemFont,
|
||||||
Segoe UI,
|
Segoe UI,
|
||||||
Helvetica Neue,
|
Helvetica Neue,
|
||||||
Helvetica,
|
Helvetica,
|
||||||
OpenSans,
|
|
||||||
sans-serif;
|
sans-serif;
|
||||||
line-height: 1.5;
|
line-height: 1.5;
|
||||||
color: var(--black);
|
color: var(--black);
|
||||||
|
|
|
@ -16,6 +16,10 @@ module Commitchange
|
||||||
# Custom directories with classes and modules you want to be autoloadable.
|
# Custom directories with classes and modules you want to be autoloadable.
|
||||||
# config.autoload_paths += %W(#{config.root}/extras)
|
# config.autoload_paths += %W(#{config.root}/extras)
|
||||||
config.autoload_paths += Dir["#{config.root}/lib/**/"]
|
config.autoload_paths += Dir["#{config.root}/lib/**/"]
|
||||||
|
|
||||||
|
config.paths.add File.join('app', 'api'), glob: File.join('**', '*.rb')
|
||||||
|
config.autoload_paths += Dir[Rails.root.join('app', 'api', '*')]
|
||||||
|
|
||||||
# Only load the plugins named here, in the order given (default is alphabetical).
|
# Only load the plugins named here, in the order given (default is alphabetical).
|
||||||
# :all can be used as a placeholder for all plugins not explicitly named.
|
# :all can be used as a placeholder for all plugins not explicitly named.
|
||||||
# config.plugins = [ :exception_notification, :ssl_requirement, :all ]
|
# config.plugins = [ :exception_notification, :ssl_requirement, :all ]
|
||||||
|
|
|
@ -8,13 +8,14 @@ Encoding.default_internal = Encoding::UTF_8
|
||||||
require 'dotenv'
|
require 'dotenv'
|
||||||
Dotenv.load ".env"
|
Dotenv.load ".env"
|
||||||
@env = Rails.env || 'development'
|
@env = Rails.env || 'development'
|
||||||
puts "config files .env .env.#{@env} ./config/settings.#{@env}.yml#{ @env != 'test' ? " ./config/#{ENV.fetch('ORG_NAME')}.yml": " "} #{ @env != 'test' ? " ./config/#{ENV.fetch('ORG_NAME')}.#{@env}.yml": " "} #{ @env == 'test' ? "./config/settings.test.yml" : ""}"
|
@org_name = ENV['ORG_NAME'] || 'default_organization'
|
||||||
|
puts "config files .env .env.#{@env} ./config/settings.#{@env}.yml#{ @env != 'test' ? " ./config/#{@org_name}.yml": " "} #{ @env != 'test' ? " ./config/#{@org_name}.#{@env}.yml": " "} #{ @env == 'test' ? "./config/settings.test.yml" : ""}"
|
||||||
Dotenv.load ".env.#{@env}" if File.file?(".env.#{@env}")
|
Dotenv.load ".env.#{@env}" if File.file?(".env.#{@env}")
|
||||||
if Rails.env == 'test'
|
if Rails.env == 'test'
|
||||||
Settings.add_source!("./config/settings.test.yml")
|
Settings.add_source!("./config/settings.test.yml")
|
||||||
else
|
else
|
||||||
Settings.add_source!("./config/#{ENV.fetch('ORG_NAME')}.yml")
|
Settings.add_source!("./config/#{@org_name}.yml")
|
||||||
Settings.add_source!("./config/#{ENV.fetch('ORG_NAME')}.#{Rails.env}.yml")
|
Settings.add_source!("./config/#{@org_name}.#{Rails.env}.yml")
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -39,4 +39,6 @@ Commitchange::Application.configure do
|
||||||
config.assets.debug = true
|
config.assets.debug = true
|
||||||
|
|
||||||
config.log_level = :debug
|
config.log_level = :debug
|
||||||
|
|
||||||
|
config.action_controller.allow_forgery_protection = false
|
||||||
end
|
end
|
||||||
|
|
11
config/initializers/reload_api.rb
Normal file
11
config/initializers/reload_api.rb
Normal file
|
@ -0,0 +1,11 @@
|
||||||
|
if Rails.env.development?
|
||||||
|
ActiveSupport::Dependencies.explicitly_unloadable_constants << 'Houdini::V1'
|
||||||
|
|
||||||
|
api_files = Dir[Rails.root.join('app', 'api', '**', '*.rb')]
|
||||||
|
api_reloader = ActiveSupport::FileUpdateChecker.new(api_files) do
|
||||||
|
Rails.application.reload_routes!
|
||||||
|
end
|
||||||
|
ActionDispatch::Callbacks.to_prepare do
|
||||||
|
api_reloader.execute_if_updated
|
||||||
|
end
|
||||||
|
end
|
|
@ -139,3 +139,31 @@ en:
|
||||||
twitter: "Tweet"
|
twitter: "Tweet"
|
||||||
twitter_message: "Join me in supporting"
|
twitter_message: "Join me in supporting"
|
||||||
finish: "Finish"
|
finish: "Finish"
|
||||||
|
registration:
|
||||||
|
get_started:
|
||||||
|
header: "Get started"
|
||||||
|
description: "Let's get started with Houdini. To begin, fill out your initial info nonprofit and user info."
|
||||||
|
wizard:
|
||||||
|
tabs:
|
||||||
|
nonprofit: "Nonprofit"
|
||||||
|
contact: "Contact"
|
||||||
|
nonprofit:
|
||||||
|
name: "Organization Name"
|
||||||
|
website: "Website URL"
|
||||||
|
email: "Org Email (public)"
|
||||||
|
phone: "Org Phone (public)"
|
||||||
|
city: "City"
|
||||||
|
state: "State"
|
||||||
|
zip: "Zip Code"
|
||||||
|
contact:
|
||||||
|
name: "Your Name"
|
||||||
|
email: "Your Email (used for login)"
|
||||||
|
password: "New Password"
|
||||||
|
password_confirmation: "Retype Password"
|
||||||
|
phone: "Your Phone (for account recovery)"
|
||||||
|
save_and_finish: "Save & Finish"
|
||||||
|
next: "Next"
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -1,12 +1,15 @@
|
||||||
# License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later
|
# License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later
|
||||||
Commitchange::Application.routes.draw do
|
Commitchange::Application.routes.draw do
|
||||||
|
mount Houdini::API => '/api'
|
||||||
|
|
||||||
if Rails.env == 'development'
|
if Rails.env == 'development'
|
||||||
get '/button_debug/embedded' => 'button_debug#embedded'
|
get '/button_debug/embedded' => 'button_debug#embedded'
|
||||||
get '/button_debug/button' => 'button_debug#button'
|
get '/button_debug/button' => 'button_debug#button'
|
||||||
get '/button_debug/embedded/:id' => 'button_debug#embedded'
|
get '/button_debug/embedded/:id' => 'button_debug#embedded'
|
||||||
get '/button_debug/button/:id' => 'button_debug#button'
|
get '/button_debug/button/:id' => 'button_debug#button'
|
||||||
end
|
end
|
||||||
|
get 'onboard' => 'onboard#index'
|
||||||
|
|
||||||
resources(:emails, {only: [:create]})
|
resources(:emails, {only: [:create]})
|
||||||
resources(:settings, {only: [:index]})
|
resources(:settings, {only: [:index]})
|
||||||
resources(:pricing, {only: [:index]})
|
resources(:pricing, {only: [:index]})
|
||||||
|
|
|
@ -48,7 +48,7 @@ page_editor:
|
||||||
editor: 'quill'
|
editor: 'quill'
|
||||||
|
|
||||||
language: 'en'
|
language: 'en'
|
||||||
available_locales: ['en']
|
available_locales: ['en', 'de']
|
||||||
|
|
||||||
intntl:
|
intntl:
|
||||||
currencies: ["usd"]
|
currencies: ["usd"]
|
||||||
|
|
|
@ -1,5 +0,0 @@
|
||||||
// License: LGPL-3.0-or-later
|
|
||||||
|
|
||||||
// require a root component here. This will be treated as the root of a webpack package
|
|
||||||
|
|
||||||
import "../src/components/registration_page/registration_page"
|
|
16
javascripts/app/registration_page.tsx
Normal file
16
javascripts/app/registration_page.tsx
Normal file
|
@ -0,0 +1,16 @@
|
||||||
|
// License: LGPL-3.0-or-later
|
||||||
|
|
||||||
|
// require a root component here. This will be treated as the root of a webpack package
|
||||||
|
import Root from "../src/components/common/Root"
|
||||||
|
import RegistrationPage from "../src/components/registration_page/RegistrationPage"
|
||||||
|
|
||||||
|
import * as ReactDOM from 'react-dom'
|
||||||
|
import * as React from 'react'
|
||||||
|
|
||||||
|
function LoadReactPage(element:HTMLElement) {
|
||||||
|
ReactDOM.render(<Root><RegistrationPage/></Root>, element)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
(window as any).LoadReactPage = LoadReactPage
|
||||||
|
|
|
@ -0,0 +1,38 @@
|
||||||
|
// License: LGPL-3.0-or-later
|
||||||
|
import * as React from 'react';
|
||||||
|
import 'jest';
|
||||||
|
import {shallow} from 'enzyme'
|
||||||
|
import toJson from 'enzyme-to-json'
|
||||||
|
import LabeledFieldComponent from './LabeledFieldComponent'
|
||||||
|
|
||||||
|
describe('LabeledFieldComponent', () => {
|
||||||
|
test('In Error with Children', () => {
|
||||||
|
let result = shallow(<LabeledFieldComponent inputId={"ID"} labelText={"Our Label"} inError={true}
|
||||||
|
error={"errorMessage"}>
|
||||||
|
<hr/>
|
||||||
|
</LabeledFieldComponent>)
|
||||||
|
expect(toJson(result)).toMatchSnapshot()
|
||||||
|
})
|
||||||
|
|
||||||
|
test('has error checked but no message so not really in error', () => {
|
||||||
|
let result = shallow(<LabeledFieldComponent inputId={"ID"} labelText={"Our Label"} inError={true} error={null}>
|
||||||
|
<hr/>
|
||||||
|
</LabeledFieldComponent>)
|
||||||
|
expect(toJson(result)).toMatchSnapshot()
|
||||||
|
})
|
||||||
|
|
||||||
|
test('no error', () => {
|
||||||
|
let result = shallow(<LabeledFieldComponent inputId={"ID"} labelText={"Our Label"} inError={false}>
|
||||||
|
<hr/>
|
||||||
|
</LabeledFieldComponent>)
|
||||||
|
expect(toJson(result)).toMatchSnapshot()
|
||||||
|
})
|
||||||
|
|
||||||
|
test('add extra classNames', () => {
|
||||||
|
let result = shallow(<LabeledFieldComponent inputId={"ID"} labelText={"Our Label"} inError={false}
|
||||||
|
className={"a_class another_class"}>
|
||||||
|
<hr/>
|
||||||
|
</LabeledFieldComponent>)
|
||||||
|
expect(toJson(result)).toMatchSnapshot()
|
||||||
|
})
|
||||||
|
})
|
31
javascripts/src/components/common/LabeledFieldComponent.tsx
Normal file
31
javascripts/src/components/common/LabeledFieldComponent.tsx
Normal file
|
@ -0,0 +1,31 @@
|
||||||
|
// License: LGPL-3.0-or-later
|
||||||
|
import * as React from 'react';
|
||||||
|
import StandardFieldComponent from "./StandardFieldComponent";
|
||||||
|
import { observer } from 'mobx-react';
|
||||||
|
import {Field} from "../../../../types/mobx-react-form";
|
||||||
|
import {injectIntl, InjectedIntl} from 'react-intl';
|
||||||
|
|
||||||
|
|
||||||
|
export interface LabeledFieldComponentProps
|
||||||
|
{
|
||||||
|
inputId: string
|
||||||
|
labelText: string
|
||||||
|
inError:boolean
|
||||||
|
error?:string
|
||||||
|
className?:string
|
||||||
|
}
|
||||||
|
|
||||||
|
@observer
|
||||||
|
export default class LabeledFieldComponent extends React.Component<LabeledFieldComponentProps, {}> {
|
||||||
|
render() {
|
||||||
|
let className = this.props.className || ""
|
||||||
|
let inError = this.props.inError && this.props.error !== null && this.props.error !== "";
|
||||||
|
className += " form-group"
|
||||||
|
className += inError ? " has-error" : ""
|
||||||
|
return <fieldset className={className}><label htmlFor={this.props.inputId} className="control-label">{this.props.labelText}</label>
|
||||||
|
<StandardFieldComponent inError={inError} error={this.props.error} >{this.props.children}</StandardFieldComponent>
|
||||||
|
</fieldset>;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
44
javascripts/src/components/common/Root.tsx
Normal file
44
javascripts/src/components/common/Root.tsx
Normal file
|
@ -0,0 +1,44 @@
|
||||||
|
// License: LGPL-3.0-or-later
|
||||||
|
import * as React from 'react';
|
||||||
|
import { observer, Provider } from 'mobx-react';
|
||||||
|
import { IntlProvider, addLocaleData} from 'react-intl';
|
||||||
|
const enLocaleData = require('react-intl/locale-data/en');
|
||||||
|
const deLocaleData = require('react-intl/locale-data/de');
|
||||||
|
const I18n = require('i18n')
|
||||||
|
import {convert} from 'dotize'
|
||||||
|
import {ApiManager} from "../../lib/api_manager";
|
||||||
|
import {APIS} from "../../../api";
|
||||||
|
import {CSRFInterceptor} from "../../lib/csrf_interceptor";
|
||||||
|
|
||||||
|
import * as CustomAPIS from "../../lib/apis"
|
||||||
|
|
||||||
|
addLocaleData([...enLocaleData, ...deLocaleData])
|
||||||
|
|
||||||
|
interface RootProps
|
||||||
|
{
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@observer
|
||||||
|
export default class Root extends React.Component<RootProps, {}> {
|
||||||
|
|
||||||
|
apiManager: ApiManager
|
||||||
|
|
||||||
|
render() {
|
||||||
|
if (!this.apiManager){
|
||||||
|
this.apiManager = new ApiManager(APIS.concat(CustomAPIS.APIS as Array<any>), CSRFInterceptor)
|
||||||
|
}
|
||||||
|
|
||||||
|
return <IntlProvider locale={I18n.locale} defaultLocale={I18n.defaultLocale} messages={convert(I18n.translations[I18n.locale])}>
|
||||||
|
<Provider ApiManager={this.apiManager}>
|
||||||
|
{this.props.children}
|
||||||
|
</Provider>
|
||||||
|
</IntlProvider>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,27 @@
|
||||||
|
// License: LGPL-3.0-or-later
|
||||||
|
import * as React from 'react';
|
||||||
|
import 'jest';
|
||||||
|
import {shallow,render} from 'enzyme'
|
||||||
|
const TestRenderer = require('react-test-renderer')
|
||||||
|
import StandardFieldComponent from './StandardFieldComponent'
|
||||||
|
import toJson from 'enzyme-to-json';
|
||||||
|
|
||||||
|
describe('StandardFieldComponent', () => {
|
||||||
|
test('works with no children', () => {
|
||||||
|
var field = shallow(<StandardFieldComponent inError={false} />)
|
||||||
|
|
||||||
|
|
||||||
|
expect(toJson(field)).toMatchSnapshot()
|
||||||
|
})
|
||||||
|
test('works with a child', () => {
|
||||||
|
var field = shallow(<StandardFieldComponent inError={false}><input/></StandardFieldComponent>);
|
||||||
|
|
||||||
|
expect(toJson(field)).toMatchSnapshot()
|
||||||
|
})
|
||||||
|
|
||||||
|
test('sets error message properly', () => {
|
||||||
|
var field = shallow(<StandardFieldComponent inError={true} error={"Something more"}><input/></StandardFieldComponent>);
|
||||||
|
|
||||||
|
expect(toJson(field)).toMatchSnapshot()
|
||||||
|
})
|
||||||
|
})
|
35
javascripts/src/components/common/StandardFieldComponent.tsx
Normal file
35
javascripts/src/components/common/StandardFieldComponent.tsx
Normal file
|
@ -0,0 +1,35 @@
|
||||||
|
// License: LGPL-3.0-or-later
|
||||||
|
import * as React from 'react';
|
||||||
|
|
||||||
|
export interface StandardFieldComponentProps
|
||||||
|
{
|
||||||
|
inError:boolean
|
||||||
|
error?:string
|
||||||
|
children?:React.ReactNode
|
||||||
|
[additional_properties:string]: any
|
||||||
|
}
|
||||||
|
|
||||||
|
export default class StandardFieldComponent extends React.Component<StandardFieldComponentProps, {}> {
|
||||||
|
constructor(props:StandardFieldComponentProps){
|
||||||
|
super(props)
|
||||||
|
}
|
||||||
|
renderChildren(){
|
||||||
|
return React.Children.map(this.props.children, child => {
|
||||||
|
return React.cloneElement(child as React.ReactElement<any>, {
|
||||||
|
className: "form-control"
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
render() {
|
||||||
|
let errorMessage = this.props.inError ? this.props.error : undefined
|
||||||
|
let errorDiv = this.props.inError? <div className="help-block" role="alert">{errorMessage}</div> : ""
|
||||||
|
|
||||||
|
return <div>
|
||||||
|
{this.renderChildren()}
|
||||||
|
{errorDiv}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -0,0 +1,75 @@
|
||||||
|
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||||
|
|
||||||
|
exports[`LabeledFieldComponent In Error with Children 1`] = `
|
||||||
|
<fieldset
|
||||||
|
className=" form-group has-error"
|
||||||
|
>
|
||||||
|
<label
|
||||||
|
className="control-label"
|
||||||
|
htmlFor="ID"
|
||||||
|
>
|
||||||
|
Our Label
|
||||||
|
</label>
|
||||||
|
<StandardFieldComponent
|
||||||
|
error="errorMessage"
|
||||||
|
inError={true}
|
||||||
|
>
|
||||||
|
<hr />
|
||||||
|
</StandardFieldComponent>
|
||||||
|
</fieldset>
|
||||||
|
`;
|
||||||
|
|
||||||
|
exports[`LabeledFieldComponent add extra classNames 1`] = `
|
||||||
|
<fieldset
|
||||||
|
className="a_class another_class form-group"
|
||||||
|
>
|
||||||
|
<label
|
||||||
|
className="control-label"
|
||||||
|
htmlFor="ID"
|
||||||
|
>
|
||||||
|
Our Label
|
||||||
|
</label>
|
||||||
|
<StandardFieldComponent
|
||||||
|
inError={false}
|
||||||
|
>
|
||||||
|
<hr />
|
||||||
|
</StandardFieldComponent>
|
||||||
|
</fieldset>
|
||||||
|
`;
|
||||||
|
|
||||||
|
exports[`LabeledFieldComponent has error checked but no message so not really in error 1`] = `
|
||||||
|
<fieldset
|
||||||
|
className=" form-group"
|
||||||
|
>
|
||||||
|
<label
|
||||||
|
className="control-label"
|
||||||
|
htmlFor="ID"
|
||||||
|
>
|
||||||
|
Our Label
|
||||||
|
</label>
|
||||||
|
<StandardFieldComponent
|
||||||
|
error={null}
|
||||||
|
inError={false}
|
||||||
|
>
|
||||||
|
<hr />
|
||||||
|
</StandardFieldComponent>
|
||||||
|
</fieldset>
|
||||||
|
`;
|
||||||
|
|
||||||
|
exports[`LabeledFieldComponent no error 1`] = `
|
||||||
|
<fieldset
|
||||||
|
className=" form-group"
|
||||||
|
>
|
||||||
|
<label
|
||||||
|
className="control-label"
|
||||||
|
htmlFor="ID"
|
||||||
|
>
|
||||||
|
Our Label
|
||||||
|
</label>
|
||||||
|
<StandardFieldComponent
|
||||||
|
inError={false}
|
||||||
|
>
|
||||||
|
<hr />
|
||||||
|
</StandardFieldComponent>
|
||||||
|
</fieldset>
|
||||||
|
`;
|
|
@ -0,0 +1,27 @@
|
||||||
|
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||||
|
|
||||||
|
exports[`StandardFieldComponent sets error message properly 1`] = `
|
||||||
|
<div>
|
||||||
|
<input
|
||||||
|
className="form-control"
|
||||||
|
key=".0"
|
||||||
|
/>
|
||||||
|
<div
|
||||||
|
className="help-block"
|
||||||
|
role="alert"
|
||||||
|
>
|
||||||
|
Something more
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
|
||||||
|
exports[`StandardFieldComponent works with a child 1`] = `
|
||||||
|
<div>
|
||||||
|
<input
|
||||||
|
className="form-control"
|
||||||
|
key=".0"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
|
||||||
|
exports[`StandardFieldComponent works with no children 1`] = `<div />`;
|
16
javascripts/src/components/common/fields.tsx
Normal file
16
javascripts/src/components/common/fields.tsx
Normal file
|
@ -0,0 +1,16 @@
|
||||||
|
// License: LGPL-3.0-or-later
|
||||||
|
import * as React from 'react';
|
||||||
|
import {observer} from "mobx-react";
|
||||||
|
import * as _ from 'lodash'
|
||||||
|
import {Field} from "../../../../types/mobx-react-form";
|
||||||
|
import LabeledFieldComponent from "./LabeledFieldComponent";
|
||||||
|
import {injectIntl, InjectedIntl} from 'react-intl';
|
||||||
|
|
||||||
|
|
||||||
|
export const BasicField = injectIntl(observer((props:{field:Field, intl?:InjectedIntl, wrapperClassName?:string}) =>{
|
||||||
|
return <LabeledFieldComponent
|
||||||
|
inputId={props.field.id} labelText={props.field.label} inError={props.field.hasError} error={props.field.error} className={props.wrapperClassName} >
|
||||||
|
|
||||||
|
<input {...props.field.bind()} className="form-control"/>
|
||||||
|
</LabeledFieldComponent>
|
||||||
|
}))
|
38
javascripts/src/components/common/layout.tsx
Normal file
38
javascripts/src/components/common/layout.tsx
Normal file
|
@ -0,0 +1,38 @@
|
||||||
|
// License: LGPL-3.0-or-later
|
||||||
|
import * as React from 'react';
|
||||||
|
import {observer} from "mobx-react";
|
||||||
|
import * as _ from 'lodash'
|
||||||
|
|
||||||
|
export const TwoColumnFields = observer((props:{children:Array<React.ReactElement<any>>}) => {
|
||||||
|
return <div className="clearfix">
|
||||||
|
{
|
||||||
|
_.take(props.children, 2).map((i:React.ReactElement<any>) => {
|
||||||
|
let className = "col-left-6"
|
||||||
|
if (_.last(props.children) !== i){
|
||||||
|
className += " u-paddingRight--10"
|
||||||
|
}
|
||||||
|
if (i.props['className']){
|
||||||
|
className += i.props['className']
|
||||||
|
}
|
||||||
|
|
||||||
|
return React.cloneElement(i, {wrapperClassName: className})
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
})
|
||||||
|
|
||||||
|
export const ThreeColumnFields = observer((props:{children:React.ReactElement<any>[]}) => {
|
||||||
|
return <div className="clearfix">
|
||||||
|
{
|
||||||
|
_.take(props.children, 3).map((i:React.ReactElement<any>) => {
|
||||||
|
let className = "col-left-4"
|
||||||
|
if (_.last(props.children) !== i){
|
||||||
|
className += " u-paddingRight--10"
|
||||||
|
}
|
||||||
|
if (i.props['className']){
|
||||||
|
className += i.props['className']
|
||||||
|
}
|
||||||
|
|
||||||
|
return React.cloneElement(i, {wrapperClassName: className})
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
})
|
53
javascripts/src/components/common/wizard/ManagedWrapper.ts
Normal file
53
javascripts/src/components/common/wizard/ManagedWrapper.ts
Normal file
|
@ -0,0 +1,53 @@
|
||||||
|
// License: LGPL-3.0-or-later
|
||||||
|
import * as React from 'react'
|
||||||
|
import * as RAT from "react-aria-tabpanel";
|
||||||
|
import {TabManager} from "./manager";
|
||||||
|
var PropTypes = require('prop-types');
|
||||||
|
|
||||||
|
var innerCreateManager = require('react-aria-tabpanel/lib/createManager');
|
||||||
|
var specialAssign = require('react-aria-tabpanel/lib/specialAssign');
|
||||||
|
|
||||||
|
interface AddManagerInterface {
|
||||||
|
manager?: TabManager
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
var checkedProps = {
|
||||||
|
children: PropTypes.node.isRequired,
|
||||||
|
activeTabId: PropTypes.string,
|
||||||
|
letterNavigation: PropTypes.bool,
|
||||||
|
onChange: PropTypes.func,
|
||||||
|
tag: PropTypes.string,
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Works just like the normal Wrapper but provides a tool for passing in our own TabManager
|
||||||
|
*/
|
||||||
|
export class ManagedWrapper extends RAT.Wrapper<AddManagerInterface>
|
||||||
|
{
|
||||||
|
manager: TabManager
|
||||||
|
constructor(props:RAT.WrapperProps & AddManagerInterface){
|
||||||
|
super(props)
|
||||||
|
|
||||||
|
if (props.manager)
|
||||||
|
this.manager = this.props.manager
|
||||||
|
}
|
||||||
|
|
||||||
|
componentWillMount(){
|
||||||
|
|
||||||
|
console.log('seomte')
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
var props = this.props;
|
||||||
|
var elProps = {};
|
||||||
|
specialAssign(elProps, props, checkedProps);
|
||||||
|
return React.createElement(props.tag, elProps, props.children);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
111
javascripts/src/components/common/wizard/Wizard.spec.tsx
Normal file
111
javascripts/src/components/common/wizard/Wizard.spec.tsx
Normal file
|
@ -0,0 +1,111 @@
|
||||||
|
// License: LGPL-3.0-or-later
|
||||||
|
import * as React from 'react';
|
||||||
|
import 'jest';
|
||||||
|
import * as Component from './Wizard'
|
||||||
|
import {WizardState, WizardTabPanelState} from "./wizard_state";
|
||||||
|
import {Form} from "mobx-react-form";
|
||||||
|
import {computed, observable, action} from 'mobx';
|
||||||
|
import {Wizard} from "./Wizard";
|
||||||
|
import {shallow} from 'enzyme';
|
||||||
|
import {WizardPanel} from "./WizardPanel";
|
||||||
|
import toJson from 'enzyme-to-json';
|
||||||
|
|
||||||
|
class MockableTabPanelState extends WizardTabPanelState
|
||||||
|
{
|
||||||
|
@observable
|
||||||
|
customIsValid: boolean
|
||||||
|
|
||||||
|
@action.bound
|
||||||
|
setValid(validity:boolean){
|
||||||
|
this.customIsValid = validity;
|
||||||
|
}
|
||||||
|
|
||||||
|
@computed
|
||||||
|
get isValid():boolean {
|
||||||
|
return this.customIsValid
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
class EasyWizardState extends WizardState{
|
||||||
|
constructor(){
|
||||||
|
super(MockableTabPanelState)
|
||||||
|
}
|
||||||
|
createForm(i: any): Form {
|
||||||
|
return new Form(i)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('Wizard', () => {
|
||||||
|
let data =
|
||||||
|
{
|
||||||
|
tab1: {
|
||||||
|
tabName: "Tab1",
|
||||||
|
label: "Label1",
|
||||||
|
subFormDef: {extra: "nothing" }
|
||||||
|
},
|
||||||
|
tab2: {
|
||||||
|
tabName: "Tab2",
|
||||||
|
label: "Label2",
|
||||||
|
subFormDef: {extra: "not" }
|
||||||
|
},
|
||||||
|
tab3: {
|
||||||
|
tabName: "Tab3",
|
||||||
|
label: "Label3",
|
||||||
|
subFormDef: {extra: "no3t" }
|
||||||
|
},
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
let state:EasyWizardState = null
|
||||||
|
let tab1: MockableTabPanelState, tab2: MockableTabPanelState, tab3 : MockableTabPanelState = null
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
state = new EasyWizardState()
|
||||||
|
state.addTab(data.tab1.tabName, data.tab1.label, data.tab1.subFormDef)
|
||||||
|
state.addTab(data.tab2.tabName, data.tab2.label, data.tab2.subFormDef)
|
||||||
|
state.addTab(data.tab3.tabName, data.tab3.label, data.tab3.subFormDef)
|
||||||
|
state.initialize()
|
||||||
|
tab1 = (state.tabsByName[data.tab1.tabName] as MockableTabPanelState)
|
||||||
|
tab1.setValid(true)
|
||||||
|
tab2 = (state.tabsByName[data.tab2.tabName] as MockableTabPanelState)
|
||||||
|
tab2.setValid(true)
|
||||||
|
tab3 = (state.tabsByName[data.tab3.tabName] as MockableTabPanelState)
|
||||||
|
})
|
||||||
|
|
||||||
|
function createWizard(disabledTabs:boolean) {
|
||||||
|
return <Wizard wizardState={state} disableTabs={disabledTabs}>
|
||||||
|
<WizardPanel tab={tab1} />
|
||||||
|
<WizardPanel tab={tab2} />
|
||||||
|
<WizardPanel tab={tab3} />
|
||||||
|
</Wizard>
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
test('Mounts the first item only', () => {
|
||||||
|
const tree = shallow(createWizard(false) )
|
||||||
|
let panels = tree.find(WizardPanel)
|
||||||
|
expect(panels.length).toBe(1)
|
||||||
|
expect(panels.first().props().tab).toBe(tab1)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('Mounts the second tab only', () => {
|
||||||
|
state.activateTab(tab2)
|
||||||
|
const tree = shallow(createWizard(false) )
|
||||||
|
let panels = tree.find(WizardPanel)
|
||||||
|
expect(panels.length).toBe(1)
|
||||||
|
expect(panels.first().props().tab).toBe(tab2)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('Mounts the third tab only', () => {
|
||||||
|
state.activateTab(tab2)
|
||||||
|
state.activateTab(tab3)
|
||||||
|
const tree = shallow(createWizard(false) )
|
||||||
|
let panels = tree.find(WizardPanel)
|
||||||
|
expect(panels.length).toBe(1)
|
||||||
|
expect(panels.first().props().tab).toBe(tab3)
|
||||||
|
})
|
||||||
|
|
||||||
|
})
|
43
javascripts/src/components/common/wizard/Wizard.tsx
Normal file
43
javascripts/src/components/common/wizard/Wizard.tsx
Normal file
|
@ -0,0 +1,43 @@
|
||||||
|
// License: LGPL-3.0-or-later
|
||||||
|
import * as React from 'react';
|
||||||
|
import {observer} from "mobx-react"
|
||||||
|
import WizardTabList from "./WizardTabList";
|
||||||
|
import {WizardState} from './wizard_state';
|
||||||
|
import {ManagedWrapper} from "./ManagedWrapper";
|
||||||
|
import {WizardTabPanelProps} from "./WizardPanel";
|
||||||
|
|
||||||
|
export interface WizardProps
|
||||||
|
{
|
||||||
|
|
||||||
|
wizardState: WizardState
|
||||||
|
disableTabs: boolean
|
||||||
|
children: Array<React.ReactElement<WizardTabPanelProps>>
|
||||||
|
}
|
||||||
|
|
||||||
|
@observer
|
||||||
|
export class Wizard extends React.Component<WizardProps, {}> {
|
||||||
|
|
||||||
|
render() {
|
||||||
|
return <ManagedWrapper onChange={this.props.wizardState.handleTabChange}
|
||||||
|
letterNavigation={true}
|
||||||
|
activeTabId={this.props.wizardState.activeTab.id}
|
||||||
|
tag="section"
|
||||||
|
style={{display: 'table'}} className="wizard-steps" manager={this.props.wizardState.manager}>
|
||||||
|
<WizardTabList wizardState={this.props.wizardState} disableTabs={this.props.disableTabs}>
|
||||||
|
</WizardTabList>
|
||||||
|
<div className="modal-body">
|
||||||
|
|
||||||
|
<form onSubmit={this.props.wizardState.form.onSubmit} >
|
||||||
|
|
||||||
|
{this.props.children.filter((i) =>
|
||||||
|
i.props.tab == this.props.wizardState.activeTab
|
||||||
|
)}
|
||||||
|
|
||||||
|
</form>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</ManagedWrapper>;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -0,0 +1,36 @@
|
||||||
|
// License: LGPL-3.0-or-later
|
||||||
|
import * as React from 'react';
|
||||||
|
import 'jest';
|
||||||
|
|
||||||
|
import * as Component from './WizardPanel'
|
||||||
|
|
||||||
|
import {Form, Field} from 'mobx-react-form';
|
||||||
|
import { shallow, render } from 'enzyme';
|
||||||
|
import {TabPanel, Wrapper} from 'react-aria-tabpanel'
|
||||||
|
import toJson from 'enzyme-to-json';
|
||||||
|
import {WizardState} from "./wizard_state";
|
||||||
|
const TestRenderer = require('react-test-renderer')
|
||||||
|
|
||||||
|
class EasyWizardState extends WizardState{
|
||||||
|
|
||||||
|
createForm(i: any): Form {
|
||||||
|
return new Form(i)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('WizardPanel', () => {
|
||||||
|
test('shallow render', () => {
|
||||||
|
let fields = [{name: 'fun', id: 'fun', label: 'alsofun'}]
|
||||||
|
const form = new Form({fields});
|
||||||
|
|
||||||
|
const ws = new EasyWizardState()
|
||||||
|
ws.addTab('something', 'something label',{} )
|
||||||
|
ws.initialize()
|
||||||
|
|
||||||
|
const tree = shallow(<Component.WizardPanel tab={ws.tabsByName['something']}><hr/></Component.WizardPanel>)
|
||||||
|
|
||||||
|
|
||||||
|
expect(toJson(tree)).toMatchSnapshot()
|
||||||
|
})
|
||||||
|
})
|
36
javascripts/src/components/common/wizard/WizardPanel.tsx
Normal file
36
javascripts/src/components/common/wizard/WizardPanel.tsx
Normal file
|
@ -0,0 +1,36 @@
|
||||||
|
// License: LGPL-3.0-or-later
|
||||||
|
import * as React from 'react';
|
||||||
|
import { TabPanel } from "react-aria-tabpanel";
|
||||||
|
import { observer } from 'mobx-react'
|
||||||
|
import { WizardTabPanelState} from './wizard_state';
|
||||||
|
import {computed} from 'mobx';
|
||||||
|
|
||||||
|
|
||||||
|
export interface WizardTabPanelProps {
|
||||||
|
tab: WizardTabPanelState
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface WizardPanelProps extends WizardTabPanelProps {
|
||||||
|
[props:string]:any
|
||||||
|
}
|
||||||
|
|
||||||
|
@observer
|
||||||
|
export class WizardPanel extends React.Component<WizardPanelProps, {}> {
|
||||||
|
@computed
|
||||||
|
get tab():WizardTabPanelState{
|
||||||
|
return this.props.tab
|
||||||
|
}
|
||||||
|
@computed
|
||||||
|
get isActive(){
|
||||||
|
return this.tab.active
|
||||||
|
}
|
||||||
|
render() {
|
||||||
|
|
||||||
|
let props = this.props.props ? this.props.props : {}
|
||||||
|
return <TabPanel tabId={this.tab.id} active={this.isActive}
|
||||||
|
{...props} className="wizard-step">
|
||||||
|
{this.props.children}
|
||||||
|
</TabPanel>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
49
javascripts/src/components/common/wizard/WizardTab.spec.tsx
Normal file
49
javascripts/src/components/common/wizard/WizardTab.spec.tsx
Normal file
|
@ -0,0 +1,49 @@
|
||||||
|
// License: LGPL-3.0-or-later
|
||||||
|
import * as React from 'react';
|
||||||
|
import 'jest';
|
||||||
|
import WizardTab from './WizardTab'
|
||||||
|
import { Tab } from 'react-aria-tabpanel';
|
||||||
|
import {shallowWithIntl} from "../../../lib/tests/helpers";
|
||||||
|
|
||||||
|
describe('WizardTab', () => {
|
||||||
|
function runTest(enabled:boolean, active:boolean, widthPercentage:number){
|
||||||
|
let tab = {active:active, enabled: enabled, label: "A label", id: "our_id"}
|
||||||
|
let result = shallowWithIntl(<WizardTab widthPercentage={widthPercentage} tab={tab}/>)
|
||||||
|
|
||||||
|
let ourWrapper = result.find(Tab).first()
|
||||||
|
expect(ourWrapper.prop('id')).toEqual("our_id")
|
||||||
|
let classes = ourWrapper.prop('className').split(' ')
|
||||||
|
expect(classes).toContain("wizard-index-label")
|
||||||
|
|
||||||
|
|
||||||
|
if (active){
|
||||||
|
expect(classes).toContain("is-current")
|
||||||
|
}
|
||||||
|
else{
|
||||||
|
expect(classes).not.toContain("is-current")
|
||||||
|
}
|
||||||
|
|
||||||
|
if (enabled){
|
||||||
|
expect(classes).toContain("is-accessible")
|
||||||
|
}
|
||||||
|
else{
|
||||||
|
expect(classes).not.toContain("is-accessible")
|
||||||
|
}
|
||||||
|
expect(ourWrapper.prop('style').width).toEqual(`${widthPercentage}%`)
|
||||||
|
expect(ourWrapper.prop('tag')).toEqual('span')
|
||||||
|
expect(ourWrapper.childAt(0).text()).toEqual('A label')
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
test('inactive, non-current tab displays', () =>{
|
||||||
|
runTest(false, false, 20)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('active, non-current tab displays', () =>{
|
||||||
|
runTest(false, true, 33.33333)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('active, current tab displays', () =>{
|
||||||
|
runTest(true, true, 50)
|
||||||
|
})
|
||||||
|
})
|
47
javascripts/src/components/common/wizard/WizardTab.tsx
Normal file
47
javascripts/src/components/common/wizard/WizardTab.tsx
Normal file
|
@ -0,0 +1,47 @@
|
||||||
|
// License: LGPL-3.0-or-later
|
||||||
|
import * as React from 'react';
|
||||||
|
import { Tab } from 'react-aria-tabpanel';
|
||||||
|
import {FormattedMessage, injectIntl, InjectedIntlProps} from 'react-intl';
|
||||||
|
import {observer} from 'mobx-react';
|
||||||
|
import {WizardTabPanelState} from "./wizard_state";
|
||||||
|
|
||||||
|
interface MiniTabInfo{
|
||||||
|
active:boolean
|
||||||
|
enabled:boolean
|
||||||
|
label:string
|
||||||
|
id: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface WizardTabProps
|
||||||
|
{
|
||||||
|
tab: MiniTabInfo
|
||||||
|
widthPercentage:number
|
||||||
|
style?: any
|
||||||
|
disableTabs?: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class WizardTab extends React.Component<WizardTabProps & InjectedIntlProps, {}> {
|
||||||
|
render() {
|
||||||
|
let percentageToString = this.props.widthPercentage.toString() + "%"
|
||||||
|
let style= {width: percentageToString}
|
||||||
|
|
||||||
|
|
||||||
|
let className = "wizard-index-label"
|
||||||
|
if (this.props.tab.active){
|
||||||
|
className += " is-current"
|
||||||
|
}
|
||||||
|
let disableOverrideTab = this.props.disableTabs
|
||||||
|
|
||||||
|
if (this.props.tab.enabled || disableOverrideTab){
|
||||||
|
className += " is-accessible"
|
||||||
|
}
|
||||||
|
|
||||||
|
return <Tab tag={'span'} active={this.props.tab.active} className={className} style={style} id={this.props.tab.id}>
|
||||||
|
|
||||||
|
{this.props.intl.formatMessage({id:this.props.tab.label})}
|
||||||
|
</Tab>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default injectIntl(observer(WizardTab))
|
26
javascripts/src/components/common/wizard/WizardTabList.tsx
Normal file
26
javascripts/src/components/common/wizard/WizardTabList.tsx
Normal file
|
@ -0,0 +1,26 @@
|
||||||
|
// License: LGPL-3.0-or-later
|
||||||
|
import * as React from 'react';
|
||||||
|
import WizardTab from './WizardTab';
|
||||||
|
import { TabList } from 'react-aria-tabpanel';
|
||||||
|
import {observer} from 'mobx-react';
|
||||||
|
import {WizardState} from "./wizard_state";
|
||||||
|
|
||||||
|
|
||||||
|
export interface WizardTabListProps
|
||||||
|
{
|
||||||
|
wizardState: WizardState
|
||||||
|
disableTabs: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
@observer
|
||||||
|
export default class WizardTabList extends React.Component<WizardTabListProps, {}> {
|
||||||
|
render() {
|
||||||
|
let widthOfTab = 100 / this.props.wizardState.panels.length
|
||||||
|
let output = this.props.wizardState.panels.map((i) => {
|
||||||
|
return <WizardTab tab={i} widthPercentage={widthOfTab} key={i.id + "key"} disableTabs={this.props.disableTabs}></WizardTab>})
|
||||||
|
return <TabList tag={"div"} className="wizard-index">
|
||||||
|
{output}
|
||||||
|
</TabList>;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -0,0 +1,12 @@
|
||||||
|
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||||
|
|
||||||
|
exports[`WizardPanel shallow render 1`] = `
|
||||||
|
<AriaTabPanel-TabPanel
|
||||||
|
active={true}
|
||||||
|
className="wizard-step"
|
||||||
|
tabId="tab1"
|
||||||
|
tag="div"
|
||||||
|
>
|
||||||
|
<hr />
|
||||||
|
</AriaTabPanel-TabPanel>
|
||||||
|
`;
|
120
javascripts/src/components/common/wizard/manager.ts
Normal file
120
javascripts/src/components/common/wizard/manager.ts
Normal file
|
@ -0,0 +1,120 @@
|
||||||
|
// License: ISC
|
||||||
|
// from https://github.com/davidtheclark/react-aria-tabpanel
|
||||||
|
var createFocusGroup = require('focus-group');
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Exact same as normal TabManager but includes a callback for verifying we can actually change to a
|
||||||
|
* new tab
|
||||||
|
*/
|
||||||
|
export class TabManager {
|
||||||
|
options: any
|
||||||
|
focusGroup: any
|
||||||
|
tabs: any
|
||||||
|
activeTabId: any
|
||||||
|
tabPanels: any
|
||||||
|
|
||||||
|
constructor(options: any) {
|
||||||
|
this.options = options;
|
||||||
|
|
||||||
|
var focusGroupOptions = {
|
||||||
|
wrap: true,
|
||||||
|
forwardArrows: ['down', 'right'],
|
||||||
|
backArrows: ['up', 'left'],
|
||||||
|
stringSearch: options.letterNavigation,
|
||||||
|
};
|
||||||
|
|
||||||
|
this.focusGroup = createFocusGroup(focusGroupOptions);
|
||||||
|
|
||||||
|
// These component references are added when the relevant components mount
|
||||||
|
this.tabs = [];
|
||||||
|
this.tabPanels = [];
|
||||||
|
|
||||||
|
this.activeTabId = options.activeTabId;
|
||||||
|
}
|
||||||
|
|
||||||
|
activate() {
|
||||||
|
this.focusGroup.activate();
|
||||||
|
};
|
||||||
|
|
||||||
|
memberStartsActive(tabId: any) {
|
||||||
|
if (this.activeTabId === tabId) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.activeTabId === undefined) {
|
||||||
|
this.activeTabId = tabId;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
|
||||||
|
registerTab(tabMember: any) {
|
||||||
|
if (tabMember.index === undefined) {
|
||||||
|
this.tabs.push(tabMember);
|
||||||
|
} else {
|
||||||
|
this.tabs.splice(tabMember.index, 0, tabMember);
|
||||||
|
}
|
||||||
|
|
||||||
|
var focusGroupMember = (tabMember.letterNavigationText) ? {
|
||||||
|
node: tabMember.node,
|
||||||
|
text: tabMember.letterNavigationText,
|
||||||
|
} : tabMember.node;
|
||||||
|
|
||||||
|
this.focusGroup.addMember(focusGroupMember, tabMember.index);
|
||||||
|
|
||||||
|
this.activateTab(this.activeTabId || tabMember.id);
|
||||||
|
};
|
||||||
|
|
||||||
|
registerTabPanel(tabPanelMember: any) {
|
||||||
|
this.tabPanels.push(tabPanelMember);
|
||||||
|
this.activateTab(this.activeTabId);
|
||||||
|
|
||||||
|
this.activateTab(this.activeTabId || tabPanelMember.tabId);
|
||||||
|
};
|
||||||
|
|
||||||
|
activateTab(nextActiveTabId: any) {
|
||||||
|
if (this.options.canChangeTo) {
|
||||||
|
if (!this.options.canChangeTo(nextActiveTabId)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
if (nextActiveTabId === this.activeTabId) return;
|
||||||
|
this.activeTabId = nextActiveTabId;
|
||||||
|
|
||||||
|
if (this.options.onChange) {
|
||||||
|
this.options.onChange(nextActiveTabId);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.tabPanels.forEach(function (tabPanelMember: any) {
|
||||||
|
tabPanelMember.update(nextActiveTabId === tabPanelMember.tabId);
|
||||||
|
});
|
||||||
|
this.tabs.forEach(function (tabMember: any) {
|
||||||
|
tabMember.update(nextActiveTabId === tabMember.id);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
handleTabFocus(focusedTabId: any) {
|
||||||
|
this.activateTab(focusedTabId);
|
||||||
|
};
|
||||||
|
|
||||||
|
focusTab(tabId: any) {
|
||||||
|
var tabMemberToFocus = this.tabs.find(function (tabMember: any) {
|
||||||
|
return tabMember.id === tabId;
|
||||||
|
});
|
||||||
|
if (!tabMemberToFocus) return;
|
||||||
|
tabMemberToFocus.node.focus();
|
||||||
|
};
|
||||||
|
|
||||||
|
destroy() {
|
||||||
|
this.focusGroup.deactivate();
|
||||||
|
};
|
||||||
|
|
||||||
|
getTabPanelId(tabId: any) {
|
||||||
|
return tabId + '-panel';
|
||||||
|
}
|
||||||
|
}
|
253
javascripts/src/components/common/wizard/wizard_state.spec.ts
Normal file
253
javascripts/src/components/common/wizard/wizard_state.spec.ts
Normal file
|
@ -0,0 +1,253 @@
|
||||||
|
// License: LGPL-3.0-or-later
|
||||||
|
import 'jest';
|
||||||
|
import {Form} from "mobx-react-form"
|
||||||
|
import {WizardState, WizardTabPanelState} from "./wizard_state";
|
||||||
|
import {computed, observable, action} from 'mobx';
|
||||||
|
class MockableTabPanelState extends WizardTabPanelState
|
||||||
|
{
|
||||||
|
@observable
|
||||||
|
customIsValid: boolean
|
||||||
|
|
||||||
|
@action.bound
|
||||||
|
setValid(validity:boolean){
|
||||||
|
this.customIsValid = validity;
|
||||||
|
}
|
||||||
|
|
||||||
|
@computed
|
||||||
|
get isValid():boolean {
|
||||||
|
return this.customIsValid
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
class EasyWizardState extends WizardState{
|
||||||
|
constructor(){
|
||||||
|
super(MockableTabPanelState)
|
||||||
|
}
|
||||||
|
createForm(i: any): Form {
|
||||||
|
return new Form(i)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
describe("WizardState", () =>{
|
||||||
|
let data =
|
||||||
|
{
|
||||||
|
tab1: {
|
||||||
|
tabName: "Tab1",
|
||||||
|
label: "Label1",
|
||||||
|
subFormDef: {extra: "nothing" }
|
||||||
|
},
|
||||||
|
tab2: {
|
||||||
|
tabName: "Tab2",
|
||||||
|
label: "Label2",
|
||||||
|
subFormDef: {extra: "not" }
|
||||||
|
},
|
||||||
|
tab3: {
|
||||||
|
tabName: "Tab3",
|
||||||
|
label: "Label3",
|
||||||
|
subFormDef: {extra: "no3t" }
|
||||||
|
},
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
it('adds tab properly', () =>{
|
||||||
|
let state = new EasyWizardState()
|
||||||
|
|
||||||
|
|
||||||
|
state.addTab(data.tab1.tabName, data.tab1.label, data.tab1.subFormDef)
|
||||||
|
state.initialize()
|
||||||
|
|
||||||
|
let tab = state.tabsByName[data.tab1.tabName]
|
||||||
|
expect(tab.tabName).toBe(data.tab1.tabName)
|
||||||
|
expect(tab.label).toBe(data.tab1.label)
|
||||||
|
expect(tab.form.extra).toBe(data.tab1.subFormDef.extra)
|
||||||
|
expect(tab.enabled).toBe(true)
|
||||||
|
expect(tab.previous).toBe(null)
|
||||||
|
expect(tab.next).toBe(null)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('prevents going to next if next isnt enabled', () =>{
|
||||||
|
let state = new EasyWizardState()
|
||||||
|
|
||||||
|
state.addTab(data.tab1.tabName, data.tab1.label, data.tab1.subFormDef)
|
||||||
|
state.addTab(data.tab2.tabName, data.tab2.label, data.tab2.subFormDef)
|
||||||
|
state.initialize()
|
||||||
|
|
||||||
|
expect(state.activeTab).toBe(state.tabsByName[data.tab1.tabName])
|
||||||
|
state.activateTab(state.tabsByName[data.tab2.tabName].id)
|
||||||
|
expect(state.activeTab).toBe(state.tabsByName[data.tab1.tabName])
|
||||||
|
expect(state.tabsByName[data.tab2.tabName].active).toBeFalsy()
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('go to next and back', () => {
|
||||||
|
let state = new EasyWizardState()
|
||||||
|
|
||||||
|
state.addTab(data.tab1.tabName, data.tab1.label, data.tab1.subFormDef)
|
||||||
|
state.addTab(data.tab2.tabName, data.tab2.label, data.tab2.subFormDef)
|
||||||
|
state.addTab(data.tab3.tabName, data.tab3.label, data.tab3.subFormDef)
|
||||||
|
state.initialize()
|
||||||
|
|
||||||
|
let tab1 = (state.tabsByName[data.tab1.tabName] as MockableTabPanelState)
|
||||||
|
tab1.setValid(true)
|
||||||
|
let tab2 = (state.tabsByName[data.tab2.tabName] as MockableTabPanelState)
|
||||||
|
tab2.setValid(true)
|
||||||
|
let tab3 = state.tabsByName[data.tab3.tabName]
|
||||||
|
it('go to next', () =>{
|
||||||
|
expect(state.nextTab).toBe(tab2)
|
||||||
|
expect(state.previousTab).toBeNull()
|
||||||
|
|
||||||
|
state.moveToNextTab()
|
||||||
|
|
||||||
|
expect(state.activeTab).toBe(tab2)
|
||||||
|
expect(state.manager.activeTabId).toBe(tab2.id)
|
||||||
|
expect(state.previousTab).toBe(tab1)
|
||||||
|
expect(state.nextTab).toBe(tab3)
|
||||||
|
|
||||||
|
expect(tab1.active).toBeFalsy()
|
||||||
|
expect(tab3.active).toBeFalsy()
|
||||||
|
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
describe('handle moving back to tabs when one is disabled', () => {
|
||||||
|
|
||||||
|
let state:EasyWizardState = null
|
||||||
|
let tab1: MockableTabPanelState, tab2: MockableTabPanelState, tab3 : MockableTabPanelState = null
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
state = new EasyWizardState()
|
||||||
|
state.addTab(data.tab1.tabName, data.tab1.label, data.tab1.subFormDef)
|
||||||
|
state.addTab(data.tab2.tabName, data.tab2.label, data.tab2.subFormDef)
|
||||||
|
state.addTab(data.tab3.tabName, data.tab3.label, data.tab3.subFormDef)
|
||||||
|
state.initialize()
|
||||||
|
tab1 = (state.tabsByName[data.tab1.tabName] as MockableTabPanelState)
|
||||||
|
tab1.setValid(true)
|
||||||
|
tab2 = (state.tabsByName[data.tab2.tabName] as MockableTabPanelState)
|
||||||
|
tab2.setValid(true)
|
||||||
|
tab3 = (state.tabsByName[data.tab3.tabName]as MockableTabPanelState)
|
||||||
|
})
|
||||||
|
|
||||||
|
it ('move back to previous tab if the current one is disabled', () =>{
|
||||||
|
|
||||||
|
state.activateTab(tab3)
|
||||||
|
|
||||||
|
expect(tab3.active).toBe(true)
|
||||||
|
expect(state.manager.activeTabId).toBe(tab3.id)
|
||||||
|
expect(tab1.active).toBe(false)
|
||||||
|
expect(tab2.active).toBe(false)
|
||||||
|
tab2.setValid(false)
|
||||||
|
|
||||||
|
expect(tab3.active).toBe(false)
|
||||||
|
expect(tab2.active).toBe(true)
|
||||||
|
expect(state.activeTab).toBe(tab2)
|
||||||
|
expect(state.manager.activeTabId).toBe(tab2.id)
|
||||||
|
|
||||||
|
})
|
||||||
|
|
||||||
|
it ('move back to first tab if all but that one is disabled', () =>{
|
||||||
|
state.activateTab(tab3)
|
||||||
|
|
||||||
|
expect(tab3.active).toBe(true)
|
||||||
|
expect(state.manager.activeTabId).toBe(tab3.id)
|
||||||
|
tab2.setValid(false)
|
||||||
|
tab1.setValid(false)
|
||||||
|
|
||||||
|
expect(tab3.active).toBe(false)
|
||||||
|
expect(tab2.active).toBe(false)
|
||||||
|
expect(tab1.active).toBe(true)
|
||||||
|
expect(state.activeTab).toBe(tab1)
|
||||||
|
expect(state.manager.activeTabId).toBe(tab1.id)
|
||||||
|
|
||||||
|
tab1.setValid(true)
|
||||||
|
expect(state.activeTab).toBe(tab1)
|
||||||
|
expect(state.manager.activeTabId).toBe(tab1.id)
|
||||||
|
|
||||||
|
tab1.setValid(false)
|
||||||
|
expect(state.activeTab).toBe(tab1)
|
||||||
|
expect(state.manager.activeTabId).toBe(tab1.id)
|
||||||
|
|
||||||
|
state.moveToNextTab()
|
||||||
|
expect(state.activeTab).toBe(tab1)
|
||||||
|
expect(state.manager.activeTabId).toBe(tab1.id)
|
||||||
|
|
||||||
|
tab2.setValid(true)
|
||||||
|
expect(state.activeTab).toBe(tab1)
|
||||||
|
expect(state.manager.activeTabId).toBe(tab1.id)
|
||||||
|
})
|
||||||
|
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
describe("before works properly", () =>{
|
||||||
|
let state:EasyWizardState = null
|
||||||
|
let tab1: MockableTabPanelState, tab2: MockableTabPanelState, tab3 : MockableTabPanelState = null
|
||||||
|
beforeEach(() => {
|
||||||
|
state = new EasyWizardState()
|
||||||
|
state.addTab(data.tab1.tabName, data.tab1.label, data.tab1.subFormDef)
|
||||||
|
|
||||||
|
tab1 = (state.tabsByName[data.tab1.tabName] as MockableTabPanelState)
|
||||||
|
})
|
||||||
|
|
||||||
|
it("handles before when nothing else in it",() =>{
|
||||||
|
|
||||||
|
let tab2 = new MockableTabPanelState()
|
||||||
|
state.initialize()
|
||||||
|
expect(tab1.before(tab2)).toBeFalsy()
|
||||||
|
})
|
||||||
|
|
||||||
|
it("handles before before When nothing else in it",() =>{
|
||||||
|
|
||||||
|
state.addTab(data.tab2.tabName, data.tab2.label, data.tab2.subFormDef)
|
||||||
|
state.initialize();
|
||||||
|
tab2 = (state.tabsByName[data.tab2.tabName] as MockableTabPanelState)
|
||||||
|
expect(tab1.before(tab2)).toBeTruthy()
|
||||||
|
expect(tab2.before(tab1)).toBeFalsy()
|
||||||
|
tab3 = new MockableTabPanelState()
|
||||||
|
|
||||||
|
expect(tab2.before(tab3)).toBeFalsy()
|
||||||
|
expect(tab3.before(tab2)).toBeFalsy()
|
||||||
|
})
|
||||||
|
|
||||||
|
})
|
||||||
|
|
||||||
|
describe("after works properly", () =>{
|
||||||
|
let state:EasyWizardState = null
|
||||||
|
let tab1: MockableTabPanelState, tab2: MockableTabPanelState, tab3 : MockableTabPanelState = null
|
||||||
|
beforeEach(() => {
|
||||||
|
state = new EasyWizardState()
|
||||||
|
state.addTab(data.tab1.tabName, data.tab1.label, data.tab1.subFormDef)
|
||||||
|
|
||||||
|
tab1 = (state.tabsByName[data.tab1.tabName] as MockableTabPanelState)
|
||||||
|
})
|
||||||
|
|
||||||
|
it("handles before when nothing else in it",() =>{
|
||||||
|
|
||||||
|
let tab2 = new MockableTabPanelState()
|
||||||
|
state.initialize()
|
||||||
|
expect(tab1.after(tab2)).toBeFalsy()
|
||||||
|
expect(tab2.after(tab1)).toBeFalsy()
|
||||||
|
})
|
||||||
|
|
||||||
|
it("handles before before When nothing else in it",() =>{
|
||||||
|
|
||||||
|
state.addTab(data.tab2.tabName, data.tab2.label, data.tab2.subFormDef)
|
||||||
|
state.initialize();
|
||||||
|
tab2 = (state.tabsByName[data.tab2.tabName] as MockableTabPanelState)
|
||||||
|
expect(tab2.after(tab1)).toBeTruthy()
|
||||||
|
expect(tab1.after(tab2)).toBeFalsy()
|
||||||
|
tab3 = new MockableTabPanelState()
|
||||||
|
|
||||||
|
expect(tab2.before(tab3)).toBeFalsy()
|
||||||
|
expect(tab3.before(tab2)).toBeFalsy()
|
||||||
|
})
|
||||||
|
|
||||||
|
})
|
||||||
|
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
|
310
javascripts/src/components/common/wizard/wizard_state.ts
Normal file
310
javascripts/src/components/common/wizard/wizard_state.ts
Normal file
|
@ -0,0 +1,310 @@
|
||||||
|
// License: LGPL-3.0-or-later
|
||||||
|
import {observable, action, computed, toJS, reaction, runInAction} from "mobx";
|
||||||
|
import {Field, Form, FieldDefinition, FieldHandlers, FieldHooks} from "mobx-react-form";
|
||||||
|
import _ = require("lodash");
|
||||||
|
import {TabManager} from "./manager"
|
||||||
|
import {Wizard} from "./Wizard";
|
||||||
|
|
||||||
|
interface SubFormDefinition {
|
||||||
|
related?: string[]
|
||||||
|
bindings?: any
|
||||||
|
options?: any
|
||||||
|
extra?: any
|
||||||
|
hooks?: FieldHooks
|
||||||
|
handlers?: FieldHandlers
|
||||||
|
fields?: Array<FieldDefinition>
|
||||||
|
}
|
||||||
|
|
||||||
|
export abstract class WizardState<PanelStateType extends WizardTabPanelState = WizardTabPanelState> {
|
||||||
|
panelType: { new(): PanelStateType }
|
||||||
|
|
||||||
|
constructor(panelType: { new(): PanelStateType } = null) {
|
||||||
|
this.panelType = panelType
|
||||||
|
}
|
||||||
|
|
||||||
|
@observable
|
||||||
|
lastRequestedTab: WizardTabPanelState
|
||||||
|
|
||||||
|
@observable panels = new Array<WizardTabPanelState>()
|
||||||
|
@observable form: Form
|
||||||
|
|
||||||
|
@observable manager: TabManager
|
||||||
|
|
||||||
|
abstract createForm(i: any): Form;
|
||||||
|
|
||||||
|
@action.bound
|
||||||
|
private createChildState(): WizardTabPanelState {
|
||||||
|
if (this.panelType)
|
||||||
|
return new this.panelType()
|
||||||
|
else
|
||||||
|
return new WizardTabPanelState()
|
||||||
|
}
|
||||||
|
|
||||||
|
@action.bound
|
||||||
|
addTab(tabName: string, label: string, tabFieldDefinition: SubFormDefinition): WizardTabPanelState {
|
||||||
|
|
||||||
|
var newTab = this.createChildState()
|
||||||
|
newTab.id = _.uniqueId('tab')
|
||||||
|
newTab.tabName = tabName
|
||||||
|
newTab.label = label
|
||||||
|
if (this.panels.length == 0) {
|
||||||
|
this.activeTab = newTab
|
||||||
|
}
|
||||||
|
newTab.parent = this
|
||||||
|
newTab.panelFormDefinition = tabFieldDefinition as FieldDefinition
|
||||||
|
this.panels.push(newTab)
|
||||||
|
|
||||||
|
if (!this.manager) {
|
||||||
|
this.manager = new TabManager({
|
||||||
|
onChange: this.handleTabChange, letterNavigation: true, activeTabId: this.activeTab.id,
|
||||||
|
canChangeTo: this.canChangeTo
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
reaction(() => this.lastConsistentlyEnabledTab, (data, react) => {
|
||||||
|
if (data.before(this.activeTab)) {
|
||||||
|
this.activateTab(data)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
return newTab;
|
||||||
|
}
|
||||||
|
|
||||||
|
@action.bound
|
||||||
|
initialize(): void {
|
||||||
|
if (this.panels.length > 0) {
|
||||||
|
//let's create the forms
|
||||||
|
let lastIndex = this.panels.length
|
||||||
|
for (let i = 0; i < lastIndex; i++) {
|
||||||
|
let ourPanel = this.panels[i]
|
||||||
|
|
||||||
|
if (!ourPanel.panelFormDefinition.hooks)
|
||||||
|
ourPanel.panelFormDefinition.hooks = {}
|
||||||
|
|
||||||
|
ourPanel.originalOnSuccessHook = toJS(ourPanel.panelFormDefinition.hooks['onSuccess'])
|
||||||
|
|
||||||
|
ourPanel.panelFormDefinition.hooks['onSuccess'] = this.onSuccessForPanel
|
||||||
|
|
||||||
|
/// this won't work because the hook is already replaced
|
||||||
|
if (ourPanel.panelFormDefinition.hooks)
|
||||||
|
ourPanel.originalOnErrorHook = ourPanel.panelFormDefinition.hooks['onError']
|
||||||
|
ourPanel.panelFormDefinition.hooks['onError'] = this.onErrorForPanel
|
||||||
|
|
||||||
|
ourPanel.panelFormDefinition.name = ourPanel.tabName
|
||||||
|
}
|
||||||
|
|
||||||
|
//we need to change these back to JS objects because they're likely observable and fieldDefinitions
|
||||||
|
// can't handle that
|
||||||
|
let fieldDefinition = toJS(this.panels.map((i) => toJS(i.panelFormDefinition)))
|
||||||
|
this.form = this.createForm({fields: fieldDefinition})
|
||||||
|
|
||||||
|
_.forEach(this.panels, (i) => {
|
||||||
|
//add the form to each panel
|
||||||
|
i.parentForm = this.form
|
||||||
|
i.form = this.form.$(i.tabName)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@computed
|
||||||
|
get tabsByName(): { [name: string]: WizardTabPanelState } {
|
||||||
|
return _.fromPairs(this.panels.map((i) => [i.tabName, i]));
|
||||||
|
}
|
||||||
|
|
||||||
|
@observable activeTab: WizardTabPanelState
|
||||||
|
|
||||||
|
activateTab(tab: WizardTabPanelState | string) {
|
||||||
|
let tabId: string = null
|
||||||
|
if (tab instanceof WizardTabPanelState) {
|
||||||
|
tabId = tab.id
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
tabId = tab;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.manager.activateTab(tabId)
|
||||||
|
}
|
||||||
|
|
||||||
|
@action.bound
|
||||||
|
handleTabChange(tabId: string): WizardTabPanelState {
|
||||||
|
let self = this
|
||||||
|
|
||||||
|
let tabInfo = _.find(self.panels, (i) => i.id == tabId)
|
||||||
|
if (tabInfo && tabInfo.enabled) {
|
||||||
|
this.activeTab = tabInfo
|
||||||
|
|
||||||
|
return self.activeTab === tabInfo ? tabInfo : null;
|
||||||
|
}
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
@action.bound
|
||||||
|
private canChangeTo(tabId: string): boolean {
|
||||||
|
|
||||||
|
let tab = _.find(this.panels, (i) => i.id == tabId)
|
||||||
|
return tab && tab.enabled
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
@action.bound
|
||||||
|
moveToNextTab() {
|
||||||
|
let self = this
|
||||||
|
|
||||||
|
if (this.nextTab) {
|
||||||
|
self.manager.activateTab(this.nextTab.id)
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@action.bound
|
||||||
|
onSuccessForPanel(a: Field): void {
|
||||||
|
|
||||||
|
if (this.activeTab.originalOnSuccessHook) {
|
||||||
|
this.activeTab.originalOnSuccessHook(a)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (a.submitting) {
|
||||||
|
if (this.nextTab)
|
||||||
|
this.moveToNextTab()
|
||||||
|
else
|
||||||
|
this.form.submit()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@action.bound
|
||||||
|
onErrorForPanel(a: Field): any {
|
||||||
|
if (this.activeTab.originalOnErrorHook) {
|
||||||
|
this.activeTab.originalOnErrorHook(a)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@computed
|
||||||
|
get firstDisabledTab(): WizardTabPanelState {
|
||||||
|
return _.find(this.panels, (i) => !i.enabled)
|
||||||
|
}
|
||||||
|
|
||||||
|
@computed
|
||||||
|
get lastConsistentlyEnabledTab(): WizardTabPanelState {
|
||||||
|
return this.firstDisabledTab ? this.firstDisabledTab.previous : _.last(this.panels)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@computed
|
||||||
|
get nextTab(): WizardTabPanelState {
|
||||||
|
return this.activeTab.next
|
||||||
|
}
|
||||||
|
|
||||||
|
@computed
|
||||||
|
get previousTab(): WizardTabPanelState {
|
||||||
|
return this.activeTab.previous
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class WizardTabPanelState {
|
||||||
|
@observable parent: WizardState
|
||||||
|
|
||||||
|
@observable parentForm: Form
|
||||||
|
|
||||||
|
@observable form: Field
|
||||||
|
|
||||||
|
|
||||||
|
@observable id: string
|
||||||
|
@observable tabName: string
|
||||||
|
@observable label: string
|
||||||
|
|
||||||
|
@observable originalOnSuccessHook: Function
|
||||||
|
@observable originalOnErrorHook: Function
|
||||||
|
|
||||||
|
panelFormDefinition: FieldDefinition
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Whether this tab's form is valid. We override this in a mock so we can manually set the validity
|
||||||
|
* via a simple function call
|
||||||
|
* @returns {boolean} true if this tab's form is valid, otherwise false
|
||||||
|
*/
|
||||||
|
@computed
|
||||||
|
get isValid(): boolean {
|
||||||
|
return this.form.isValid
|
||||||
|
}
|
||||||
|
|
||||||
|
@computed
|
||||||
|
get active(): boolean {
|
||||||
|
return this.parent.activeTab === this
|
||||||
|
}
|
||||||
|
|
||||||
|
@computed
|
||||||
|
get enabled(): boolean {
|
||||||
|
|
||||||
|
let previous = this.previous
|
||||||
|
let next = this.next
|
||||||
|
|
||||||
|
if (previous) {
|
||||||
|
let enabled = previous.enabled
|
||||||
|
let valid = previous.isValid;
|
||||||
|
return enabled && valid
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@computed
|
||||||
|
get previous(): WizardTabPanelState {
|
||||||
|
if (!this.parent || !this.parent.panels)
|
||||||
|
return null;
|
||||||
|
let index = _.findIndex(this.parent.panels, (i) => i == this)
|
||||||
|
if (index === null) {
|
||||||
|
// return null but we have a problem here
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
if (index === 0) {
|
||||||
|
// there is no previous one because we're first!
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.parent.panels[index - 1]
|
||||||
|
}
|
||||||
|
|
||||||
|
@computed
|
||||||
|
get next(): WizardTabPanelState {
|
||||||
|
if (!this.parent || !this.parent.panels)
|
||||||
|
return null;
|
||||||
|
|
||||||
|
let index = _.findIndex(this.parent.panels, (i) => i == this)
|
||||||
|
let panelLength = this.parent.panels.length
|
||||||
|
if (index === null) {
|
||||||
|
// return null but we have a problem here
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
if (index + 1 >= panelLength) {
|
||||||
|
//we have no advanced
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.parent.panels[index + 1]
|
||||||
|
}
|
||||||
|
|
||||||
|
before(tab: WizardTabPanelState): boolean {
|
||||||
|
let testItem: WizardTabPanelState = this
|
||||||
|
while (testItem.next != tab) {
|
||||||
|
if (!testItem.next)
|
||||||
|
return false;
|
||||||
|
testItem = testItem.next
|
||||||
|
}
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
after(tab: WizardTabPanelState): boolean {
|
||||||
|
let testItem: WizardTabPanelState = this
|
||||||
|
while (testItem.previous != tab) {
|
||||||
|
if (!testItem.previous)
|
||||||
|
return false;
|
||||||
|
testItem = testItem.previous
|
||||||
|
}
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,71 @@
|
||||||
|
// License: LGPL-3.0-or-later
|
||||||
|
import * as React from 'react';
|
||||||
|
import 'jest';
|
||||||
|
jest.useFakeTimers()
|
||||||
|
import NonprofitInfoForm, * as NIF from './NonprofitInfoForm'
|
||||||
|
import {runInAction} from 'mobx'
|
||||||
|
import {Field, FieldDefinition, Form} from "mobx-react-form";
|
||||||
|
import {mount, ReactWrapper} from 'enzyme'
|
||||||
|
import * as Component from "./NonprofitInfoPanel";
|
||||||
|
import {mountWithIntl} from "../../lib/tests/helpers";
|
||||||
|
import {HoudiniForm} from "../../lib/houdini_form";
|
||||||
|
|
||||||
|
|
||||||
|
function createSubFormInitialization(name:string, subfieldDefinitions:Array<FieldDefinition>): Array<FieldDefinition>{
|
||||||
|
let ret: FieldDefinition = {
|
||||||
|
name: name,
|
||||||
|
fields: subfieldDefinitions
|
||||||
|
}
|
||||||
|
return [ret]
|
||||||
|
}
|
||||||
|
describe('NonprofitInfoForm', () => {
|
||||||
|
let outerForm:HoudiniForm
|
||||||
|
let form:Field
|
||||||
|
let wrapper: ReactWrapper
|
||||||
|
|
||||||
|
test('pointless test for this to pass', () => {})
|
||||||
|
// beforeEach(() => {
|
||||||
|
// outerForm = new HoudiniForm({fields: createSubFormInitialization('none', NIF.FieldDefinitions)}, {
|
||||||
|
// validateOnInit: true,
|
||||||
|
// validateOnChange: true,
|
||||||
|
// retrieveOnlyDirtyValues: true,
|
||||||
|
// retrieveOnlyEnabledFields: true
|
||||||
|
// });
|
||||||
|
// form = outerForm.$('none')
|
||||||
|
// })
|
||||||
|
// afterEach(() => {
|
||||||
|
// wrapper.detach();
|
||||||
|
// })
|
||||||
|
// test('validations', async () => {
|
||||||
|
// wrapper = mountWithIntl(<NonprofitInfoForm form={form} buttonText={"none.none"}/>)
|
||||||
|
// let organization_name = form.$('organization_name')
|
||||||
|
// let city = form.$('city')
|
||||||
|
// let state = form.$('state')
|
||||||
|
//
|
||||||
|
// try {
|
||||||
|
// //await organization_name.validate()
|
||||||
|
// }
|
||||||
|
// catch(e){
|
||||||
|
// console.log(e)
|
||||||
|
// }
|
||||||
|
// wrapper.find(`#${organization_name.id}`).simulate('focus')
|
||||||
|
// wrapper.find(`#${organization_name.id}`).simulate('blur')
|
||||||
|
// wrapper.find(`#${state.id}`).simulate('click')
|
||||||
|
// organization_name.focus()
|
||||||
|
// state.focus()
|
||||||
|
//
|
||||||
|
// //jest.runTimersToTime(100000);
|
||||||
|
// try {
|
||||||
|
// await organization_name.validate()
|
||||||
|
// }
|
||||||
|
// catch(e){
|
||||||
|
// console.log(e)
|
||||||
|
// }
|
||||||
|
// expect(organization_name.error).toBe(false)
|
||||||
|
// expect(state.hasError).toBe(true)
|
||||||
|
// expect(city.hasError).toBe(true)
|
||||||
|
//
|
||||||
|
//
|
||||||
|
// console.log(wrapper.html())
|
||||||
|
// })
|
||||||
|
})
|
|
@ -0,0 +1,83 @@
|
||||||
|
// License: LGPL-3.0-or-later
|
||||||
|
import * as React from 'react';
|
||||||
|
import { observer } from 'mobx-react';
|
||||||
|
import {InjectedIntlProps, injectIntl} from 'react-intl';
|
||||||
|
import {Field, FieldDefinition} from "../../../../types/mobx-react-form";
|
||||||
|
import {BasicField} from "../common/fields";
|
||||||
|
import {ThreeColumnFields, TwoColumnFields} from "../common/layout";
|
||||||
|
import {Validations} from "../../lib/vjf_rules";
|
||||||
|
|
||||||
|
export interface NonprofitInfoFormProps
|
||||||
|
{
|
||||||
|
form:Field
|
||||||
|
buttonText:string
|
||||||
|
}
|
||||||
|
|
||||||
|
export const FieldDefinitions : Array<FieldDefinition> = [
|
||||||
|
{
|
||||||
|
name: 'organization_name',
|
||||||
|
label: 'registration.wizard.nonprofit.name',
|
||||||
|
type: 'text',
|
||||||
|
validators: [Validations.isFilled]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'website',
|
||||||
|
label: 'registration.wizard.nonprofit.website',
|
||||||
|
validators: [Validations.optional(Validations.isUrl)]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'org_email',
|
||||||
|
label: 'registration.wizard.nonprofit.email',
|
||||||
|
validators: [Validations.optional(Validations.isEmail)]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'org_phone',
|
||||||
|
label: 'registration.wizard.nonprofit.phone',
|
||||||
|
type: "tel"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'city',
|
||||||
|
label: 'registration.wizard.nonprofit.city',
|
||||||
|
validators: [Validations.isFilled]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'state',
|
||||||
|
label: 'registration.wizard.nonprofit.state',
|
||||||
|
type: 'text',
|
||||||
|
validators: [Validations.isFilled]
|
||||||
|
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'zip',
|
||||||
|
label: 'registration.wizard.nonprofit.zip',
|
||||||
|
validators: [Validations.isFilled]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
class NonprofitInfoForm extends React.Component<NonprofitInfoFormProps & InjectedIntlProps, {}> {
|
||||||
|
|
||||||
|
|
||||||
|
render() {
|
||||||
|
return <fieldset >
|
||||||
|
<BasicField field={this.props.form.$("organization_name")}/>
|
||||||
|
<BasicField field={this.props.form.$('website')}/>
|
||||||
|
<TwoColumnFields>
|
||||||
|
<BasicField field={this.props.form.$('org_email')}/>
|
||||||
|
<BasicField field={this.props.form.$('org_phone')}/>
|
||||||
|
</TwoColumnFields>
|
||||||
|
|
||||||
|
<ThreeColumnFields>
|
||||||
|
<BasicField field={this.props.form.$('city')}/>
|
||||||
|
<BasicField field={this.props.form.$('state')}/>
|
||||||
|
<BasicField field={this.props.form.$('zip')}/>
|
||||||
|
</ThreeColumnFields>
|
||||||
|
<button onClick={this.props.form.onSubmit} className="button" disabled={!this.props.form.isValid || this.props.form.submitting}>
|
||||||
|
{this.props.intl.formatMessage({id: this.props.buttonText})}</button>
|
||||||
|
</fieldset>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default injectIntl(observer(NonprofitInfoForm))
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,52 @@
|
||||||
|
// License: LGPL-3.0-or-later
|
||||||
|
import * as React from 'react';
|
||||||
|
import {WizardPanel, WizardPanelProps, WizardTabPanelProps} from "../common/wizard/WizardPanel";
|
||||||
|
|
||||||
|
import {Form, Field, FieldHooks} from 'mobx-react-form'
|
||||||
|
|
||||||
|
import { inject, observer } from 'mobx-react';
|
||||||
|
import { action, computed } from 'mobx';
|
||||||
|
import {WizardState, WizardTabPanelState} from "../common/wizard/wizard_state";
|
||||||
|
import {InjectedIntlProps, injectIntl, InjectedIntl} from 'react-intl';
|
||||||
|
import {ThreeColumnFields, TwoColumnFields} from "../common/layout";
|
||||||
|
|
||||||
|
import {BasicField} from "../common/fields";
|
||||||
|
import {Validations} from "../../lib/vjf_rules";
|
||||||
|
import NonprofitInfoForm from "./NonprofitInfoForm";
|
||||||
|
|
||||||
|
export interface NonprofitInfoPanelProps extends WizardTabPanelProps
|
||||||
|
{
|
||||||
|
buttonText:string
|
||||||
|
}
|
||||||
|
|
||||||
|
class NonprofitInfoPanel extends React.Component<NonprofitInfoPanelProps & InjectedIntlProps, {}> {
|
||||||
|
|
||||||
|
@computed
|
||||||
|
get wizardTab(): WizardTabPanelState {
|
||||||
|
return this.props.tab
|
||||||
|
}
|
||||||
|
|
||||||
|
@computed
|
||||||
|
get form():Field{
|
||||||
|
return this.wizardTab.form
|
||||||
|
}
|
||||||
|
@computed
|
||||||
|
get submit(){
|
||||||
|
return this.form.onSubmit
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
render() {
|
||||||
|
var self = this
|
||||||
|
return <WizardPanel
|
||||||
|
tab={this.wizardTab} key={this.wizardTab.tabName}
|
||||||
|
>
|
||||||
|
<NonprofitInfoForm form={this.form} buttonText={this.props.buttonText}/>
|
||||||
|
</WizardPanel>
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
export default injectIntl(observer(NonprofitInfoPanel))
|
|
@ -0,0 +1,26 @@
|
||||||
|
// License: LGPL-3.0-or-later
|
||||||
|
import * as React from 'react';
|
||||||
|
import RegistrationWizard from "./RegistrationWizard";
|
||||||
|
|
||||||
|
import {configure} from 'mobx'
|
||||||
|
import {observer} from 'mobx-react';
|
||||||
|
import {InjectedIntlProps, injectIntl, InjectedIntl, FormattedMessage} from 'react-intl';
|
||||||
|
|
||||||
|
|
||||||
|
export interface RegistrationPageProps
|
||||||
|
{
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
class RegistrationPage extends React.Component<RegistrationPageProps & InjectedIntlProps, {}> {
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
render() {
|
||||||
|
return <div className="container"><h1><FormattedMessage id="registration.get_started.header"/></h1><p><FormattedMessage id="registration.get_started.description"/></p><RegistrationWizard/></div>
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default injectIntl(observer(RegistrationPage))
|
||||||
|
|
|
@ -0,0 +1,200 @@
|
||||||
|
// License: LGPL-3.0-or-later
|
||||||
|
import * as React from 'react';
|
||||||
|
|
||||||
|
import {observer, inject} from 'mobx-react'
|
||||||
|
import NonprofitInfoPanel from './NonprofitInfoPanel'
|
||||||
|
import {action, observable, computed} from 'mobx';
|
||||||
|
import {Wizard} from '../common/wizard/Wizard'
|
||||||
|
|
||||||
|
import {Form} from 'mobx-react-form';
|
||||||
|
import {FormattedMessage, injectIntl, InjectedIntlProps} from 'react-intl';
|
||||||
|
import {WizardState} from "../common/wizard/wizard_state";
|
||||||
|
import UserInfoPanel, * as UserInfo from "./UserInfoPanel";
|
||||||
|
import {
|
||||||
|
NonprofitApi,
|
||||||
|
PostNonprofit,
|
||||||
|
ValidationErrorsException
|
||||||
|
} from "../../../api";
|
||||||
|
|
||||||
|
import {initializationDefinition} from "../../../../types/mobx-react-form";
|
||||||
|
import {ApiManager} from "../../lib/api_manager";
|
||||||
|
import {HoudiniForm, StaticFormToErrorAndBackConverter} from "../../lib/houdini_form";
|
||||||
|
import {WebUserSignInOut} from "../../lib/api/sign_in";
|
||||||
|
import {Validations} from "../../lib/vjf_rules";
|
||||||
|
import * as NonprofitInfoForm from "./NonprofitInfoForm";
|
||||||
|
import * as UserInfoForm from "./UserInfoForm";
|
||||||
|
|
||||||
|
export interface RegistrationWizardProps {
|
||||||
|
ApiManager?: ApiManager
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
export class RegistrationPageForm extends HoudiniForm {
|
||||||
|
converter: StaticFormToErrorAndBackConverter<PostNonprofit>
|
||||||
|
|
||||||
|
constructor(definition: initializationDefinition, options?: any) {
|
||||||
|
super(definition, options)
|
||||||
|
this.converter = new StaticFormToErrorAndBackConverter<PostNonprofit>(this.inputToForm)
|
||||||
|
}
|
||||||
|
|
||||||
|
nonprofitApi: NonprofitApi
|
||||||
|
signinApi: WebUserSignInOut
|
||||||
|
|
||||||
|
options() {
|
||||||
|
return {
|
||||||
|
validateOnInit: true,
|
||||||
|
validateOnChange: true,
|
||||||
|
retrieveOnlyDirtyValues: true,
|
||||||
|
retrieveOnlyEnabledFields: true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
inputToForm = {
|
||||||
|
'nonprofit[name]': 'nonprofitTab.organization_name',
|
||||||
|
'nonprofit[url]': 'nonprofitTab.website',
|
||||||
|
'nonprofit[email]': 'nonprofitTab.org_email',
|
||||||
|
'nonprofit[phone]': 'nonprofitTab.org_phone',
|
||||||
|
'nonprofit[city]': 'nonprofitTab.city',
|
||||||
|
'nonprofit[state_code]': 'nonprofitTab.state',
|
||||||
|
'nonprofit[zip_code]': 'nonprofitTab.zip',
|
||||||
|
'user[name]': 'userTab.name',
|
||||||
|
'user[email]': 'userTab.email',
|
||||||
|
'user[password]': 'userTab.password',
|
||||||
|
'user[password_confirmation]': 'userTab.password_confirmation'
|
||||||
|
}
|
||||||
|
|
||||||
|
hooks() {
|
||||||
|
return {
|
||||||
|
onSuccess: async (f: Form) => {
|
||||||
|
let input = this.converter.convertFormToObject(f)
|
||||||
|
|
||||||
|
|
||||||
|
try {
|
||||||
|
let r = await this.nonprofitApi.postNonprofit(input)
|
||||||
|
|
||||||
|
await this.signinApi.postLogin({email: input.user.email, password: input.user.password})
|
||||||
|
window.location.href = `/nonprofits/${r.id}/dashboard`
|
||||||
|
|
||||||
|
}
|
||||||
|
catch (e) {
|
||||||
|
console.log(e)
|
||||||
|
if (e instanceof ValidationErrorsException) {
|
||||||
|
this.converter.convertErrorToForm(e, f)
|
||||||
|
}
|
||||||
|
//set error to the form
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
class RegistrationWizardState extends WizardState {
|
||||||
|
@action.bound
|
||||||
|
createForm(i: any): Form {
|
||||||
|
return new RegistrationPageForm(i)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
export class InnerRegistrationWizard extends React.Component<RegistrationWizardProps & InjectedIntlProps, {}> {
|
||||||
|
|
||||||
|
constructor(props: RegistrationWizardProps & InjectedIntlProps) {
|
||||||
|
super(props)
|
||||||
|
|
||||||
|
this.setRegistrationWizardState()
|
||||||
|
this.createForm()
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@observable registrationWizardState: RegistrationWizardState
|
||||||
|
|
||||||
|
@computed
|
||||||
|
get form(): RegistrationPageForm {
|
||||||
|
return (this.registrationWizardState && this.registrationWizardState.form)as RegistrationPageForm
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@action.bound
|
||||||
|
setRegistrationWizardState() {
|
||||||
|
this.registrationWizardState = new RegistrationWizardState()
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@action.bound
|
||||||
|
createForm() {
|
||||||
|
this.registrationWizardState.addTab("nonprofitTab", 'registration.wizard.tabs.nonprofit', {
|
||||||
|
fields:
|
||||||
|
NonprofitInfoForm.FieldDefinitions,
|
||||||
|
hooks: {
|
||||||
|
onError: (f: any) => {
|
||||||
|
console.log(f)
|
||||||
|
},
|
||||||
|
onSuccess: (f: any) => {
|
||||||
|
console.log(f)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
this.registrationWizardState.addTab("userTab", 'registration.wizard.tabs.contact', {
|
||||||
|
fields:
|
||||||
|
UserInfoForm.FieldDefinitions,
|
||||||
|
hooks: {
|
||||||
|
onError: (f: any) => {
|
||||||
|
|
||||||
|
},
|
||||||
|
onSuccess: (f: any) => {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
this.registrationWizardState.initialize()
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
render() {
|
||||||
|
if (!this.form.nonprofitApi) {
|
||||||
|
this.form.nonprofitApi = this.props.ApiManager.get(NonprofitApi)
|
||||||
|
|
||||||
|
}
|
||||||
|
if(!this.form.signinApi){
|
||||||
|
this.form.signinApi = this.props.ApiManager.get(WebUserSignInOut)
|
||||||
|
}
|
||||||
|
|
||||||
|
//set up labels
|
||||||
|
let label: {[props:string]: string} = {
|
||||||
|
'nonprofitTab[organization_name]': "registration.wizard.nonprofit.name",
|
||||||
|
"nonprofitTab[website]": 'registration.wizard.nonprofit.website',
|
||||||
|
"nonprofitTab[org_email]": 'registration.wizard.nonprofit.email',
|
||||||
|
'nonprofitTab[org_phone]': 'registration.wizard.nonprofit.phone',
|
||||||
|
'nonprofitTab[city]': 'registration.wizard.nonprofit.city',
|
||||||
|
'nonprofitTab[state]': 'registration.wizard.nonprofit.state',
|
||||||
|
'nonprofitTab[zip]': 'registration.wizard.nonprofit.zip',
|
||||||
|
'userTab[name]': 'registration.wizard.contact.name',
|
||||||
|
'userTab[email]': 'registration.wizard.contact.email',
|
||||||
|
'userTab[password]': 'registration.wizard.contact.password',
|
||||||
|
'userTab[password_confirmation]': 'registration.wizard.contact.password_confirmation'
|
||||||
|
}
|
||||||
|
for (let key in label){
|
||||||
|
this.form.$(key).set('label', this.props.intl.formatMessage({id: label[key]}))
|
||||||
|
}
|
||||||
|
|
||||||
|
return <Wizard wizardState={this.registrationWizardState} disableTabs={this.form.submitting}>
|
||||||
|
<NonprofitInfoPanel tab={this.registrationWizardState.tabsByName['nonprofitTab']}
|
||||||
|
buttonText="registration.wizard.next"/>
|
||||||
|
|
||||||
|
<UserInfoPanel tab={this.registrationWizardState.tabsByName['userTab']}
|
||||||
|
buttonText="registration.wizard.save_and_finish"/>
|
||||||
|
</Wizard>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default injectIntl(
|
||||||
|
inject('ApiManager')
|
||||||
|
(observer( InnerRegistrationWizard)
|
||||||
|
)
|
||||||
|
)
|
|
@ -0,0 +1,66 @@
|
||||||
|
// License: LGPL-3.0-or-later
|
||||||
|
import * as React from 'react';
|
||||||
|
import { observer } from 'mobx-react';
|
||||||
|
import {InjectedIntlProps, injectIntl} from 'react-intl';
|
||||||
|
import {Validations} from "../../lib/vjf_rules";
|
||||||
|
import {Field, FieldDefinition} from "mobx-react-form";
|
||||||
|
import {TwoColumnFields} from "../common/layout";
|
||||||
|
import {BasicField} from "../common/fields";
|
||||||
|
|
||||||
|
export const FieldDefinitions : Array<FieldDefinition> = [
|
||||||
|
{
|
||||||
|
name: 'name',
|
||||||
|
label: 'registration.wizard.contact.name',
|
||||||
|
validators: [Validations.isFilled]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'email',
|
||||||
|
label: 'registration.wizard.contact.email',
|
||||||
|
validators: [Validations.isEmail]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'password',
|
||||||
|
label: 'registration.wizard.contact.password',
|
||||||
|
type: 'password',
|
||||||
|
validators: [Validations.isFilled],
|
||||||
|
related: ['userTab.password_confirmation']
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'password_confirmation',
|
||||||
|
label: 'registration.wizard.contact.password_confirmation',
|
||||||
|
type: 'password',
|
||||||
|
validators: [Validations.shouldBeEqualTo("userTab.password")]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
export interface UserInfoFormProps
|
||||||
|
{
|
||||||
|
form: Field
|
||||||
|
buttonText:string
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
class UserInfoForm extends React.Component<UserInfoFormProps & InjectedIntlProps, {}> {
|
||||||
|
render() {
|
||||||
|
return <fieldset>
|
||||||
|
<TwoColumnFields>
|
||||||
|
<BasicField field={this.props.form.$("name")}/>
|
||||||
|
<BasicField field={this.props.form.$('email')}/>
|
||||||
|
</TwoColumnFields>
|
||||||
|
|
||||||
|
<BasicField field={this.props.form.$('password')}/>
|
||||||
|
<BasicField field={this.props.form.$('password_confirmation')}/>
|
||||||
|
|
||||||
|
|
||||||
|
<button onClick={this.props.form.onSubmit} className="button" disabled={!this.props.form.isValid || this.props.form.submitting}>
|
||||||
|
{this.props.intl.formatMessage({id: this.props.buttonText})}
|
||||||
|
</button>
|
||||||
|
</fieldset>;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default injectIntl(observer(UserInfoForm))
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,54 @@
|
||||||
|
// License: LGPL-3.0-or-later
|
||||||
|
import * as React from 'react';
|
||||||
|
import {observer} from 'mobx-react';
|
||||||
|
import {InjectedIntlProps, injectIntl} from 'react-intl';
|
||||||
|
import {Field} from "mobx-react-form";
|
||||||
|
import {computed} from 'mobx';
|
||||||
|
import {WizardPanel, WizardTabPanelProps} from "../common/wizard/WizardPanel";
|
||||||
|
import {WizardTabPanelState} from "../common/wizard/wizard_state";
|
||||||
|
|
||||||
|
export interface UserInfoPanelProps extends WizardTabPanelProps {
|
||||||
|
buttonText: string
|
||||||
|
}
|
||||||
|
|
||||||
|
class UserInfoPanel extends React.Component<UserInfoPanelProps & InjectedIntlProps, {}> {
|
||||||
|
|
||||||
|
@computed
|
||||||
|
get wizardTab(): WizardTabPanelState {
|
||||||
|
return this.props.tab
|
||||||
|
}
|
||||||
|
|
||||||
|
@computed
|
||||||
|
get form():Field{
|
||||||
|
return this.wizardTab.form
|
||||||
|
}
|
||||||
|
|
||||||
|
@computed
|
||||||
|
get submit() {
|
||||||
|
return this.form.onSubmit
|
||||||
|
}
|
||||||
|
|
||||||
|
@computed
|
||||||
|
get tabName() {
|
||||||
|
return this.wizardTab.tabName;
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
let parentForm = this.form.container() || this.form.state.form
|
||||||
|
let submitting = parentForm.submitting
|
||||||
|
|
||||||
|
return <WizardPanel
|
||||||
|
tab={this.wizardTab} key={this.wizardTab.tabName}
|
||||||
|
>
|
||||||
|
|
||||||
|
|
||||||
|
</WizardPanel>;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
export default injectIntl(observer(UserInfoPanel))
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -1,10 +0,0 @@
|
||||||
// License: LGPL-3.0-or-later
|
|
||||||
import * as React from 'react';
|
|
||||||
import 'jest';
|
|
||||||
import * as Component from './registration_page'
|
|
||||||
|
|
||||||
describe('RegistrationPage', () => {
|
|
||||||
test('your test here', () => {
|
|
||||||
expect(false).toBe(true)
|
|
||||||
})
|
|
||||||
})
|
|
|
@ -1,14 +0,0 @@
|
||||||
// License: LGPL-3.0-or-later
|
|
||||||
import * as React from 'react';
|
|
||||||
|
|
||||||
export interface RegistrationPageProps
|
|
||||||
{
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
export default class RegistrationPage extends React.Component<RegistrationPageProps, {}> {
|
|
||||||
render() {
|
|
||||||
return <div></div>;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
88
javascripts/src/lib/api/sign_in.ts
Normal file
88
javascripts/src/lib/api/sign_in.ts
Normal file
|
@ -0,0 +1,88 @@
|
||||||
|
// License: LGPL-3.0-or-later
|
||||||
|
import * as $ from 'jquery';
|
||||||
|
|
||||||
|
import * as models from "../../../api/model/models";
|
||||||
|
import {Configuration} from "../../../api/configuration";
|
||||||
|
|
||||||
|
export class WebUserSignInOut {
|
||||||
|
protected basePath = '/';
|
||||||
|
public defaultHeaders: Array<string> = [];
|
||||||
|
public defaultExtraJQueryAjaxSettings?: JQueryAjaxSettings = null;
|
||||||
|
public configuration: Configuration = new Configuration();
|
||||||
|
|
||||||
|
constructor(basePath?: string, configuration?: Configuration, defaultExtraJQueryAjaxSettings?: JQueryAjaxSettings) {
|
||||||
|
if (basePath) {
|
||||||
|
this.basePath = basePath;
|
||||||
|
}
|
||||||
|
if (configuration) {
|
||||||
|
this.configuration = configuration;
|
||||||
|
}
|
||||||
|
if (defaultExtraJQueryAjaxSettings) {
|
||||||
|
this.defaultExtraJQueryAjaxSettings = defaultExtraJQueryAjaxSettings;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public postLogin(loginInfo: WebLoginModel, extraJQueryAjaxSettings?: JQueryAjaxSettings): Promise<any> {
|
||||||
|
let localVarPath = this.basePath + 'users/sign_in.json';
|
||||||
|
|
||||||
|
let queryParameters: any = {};
|
||||||
|
let headerParams: any = {};
|
||||||
|
// verify required parameter 'nonprofit' is not null or undefined
|
||||||
|
if (loginInfo === null || loginInfo === undefined) {
|
||||||
|
throw new Error('Required parameter nonprofit was null or undefined when calling postNonprofit.');
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
localVarPath = localVarPath + "?" + $.param(queryParameters);
|
||||||
|
// to determine the Content-Type header
|
||||||
|
let consumes: string[] = [
|
||||||
|
'application/json'
|
||||||
|
];
|
||||||
|
|
||||||
|
// to determine the Accept header
|
||||||
|
let produces: string[] = [
|
||||||
|
'application/json'
|
||||||
|
];
|
||||||
|
|
||||||
|
|
||||||
|
headerParams['Content-Type'] = 'application/json';
|
||||||
|
|
||||||
|
let requestOptions: JQueryAjaxSettings = {
|
||||||
|
url: localVarPath,
|
||||||
|
type: 'POST',
|
||||||
|
headers: headerParams,
|
||||||
|
processData: false
|
||||||
|
};
|
||||||
|
|
||||||
|
requestOptions.data = JSON.stringify({user:loginInfo});
|
||||||
|
if (headerParams['Content-Type']) {
|
||||||
|
requestOptions.contentType = headerParams['Content-Type'];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (extraJQueryAjaxSettings) {
|
||||||
|
requestOptions = (<any>Object).assign(requestOptions, extraJQueryAjaxSettings);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.defaultExtraJQueryAjaxSettings) {
|
||||||
|
requestOptions = (<any>Object).assign(requestOptions, this.defaultExtraJQueryAjaxSettings);
|
||||||
|
}
|
||||||
|
|
||||||
|
let dfd = $.Deferred();
|
||||||
|
$.ajax(requestOptions).then(
|
||||||
|
(data: any, textStatus: string, jqXHR: JQueryXHR) =>
|
||||||
|
dfd.resolve(jqXHR, data),
|
||||||
|
(xhr: JQueryXHR, textStatus: string, errorThrown: string) => {
|
||||||
|
|
||||||
|
|
||||||
|
dfd.reject(errorThrown)
|
||||||
|
|
||||||
|
}
|
||||||
|
);
|
||||||
|
return dfd.promise();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
interface WebLoginModel {
|
||||||
|
email:string
|
||||||
|
password:string
|
||||||
|
}
|
84
javascripts/src/lib/api_manager.spec.ts
Normal file
84
javascripts/src/lib/api_manager.spec.ts
Normal file
|
@ -0,0 +1,84 @@
|
||||||
|
// License: LGPL-3.0-or-later
|
||||||
|
import {ApiManager, ApiMissingException} from "./api_manager";
|
||||||
|
import 'jest';
|
||||||
|
describe('ApiManager', () => {
|
||||||
|
|
||||||
|
class A{}
|
||||||
|
class B{}
|
||||||
|
class C {}
|
||||||
|
var manager: ApiManager = null
|
||||||
|
describe('simple non-intercepted API', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
manager = new ApiManager([A, B])})
|
||||||
|
|
||||||
|
test('it errors when API is missing', () => {
|
||||||
|
expect(() =>{
|
||||||
|
let c = manager.get(C)
|
||||||
|
}
|
||||||
|
).toThrow(ApiMissingException)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('it gets API of type A', () => {
|
||||||
|
let a = manager.get(A)
|
||||||
|
expect(a).toBeInstanceOf(A)
|
||||||
|
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('handle interceptor', () => {
|
||||||
|
let interceptorValue0:boolean
|
||||||
|
let interceptor0 = () => {interceptorValue0 = true}
|
||||||
|
let interceptorValue1:boolean
|
||||||
|
let interceptor1 = () => {interceptorValue1 = true}
|
||||||
|
class A {
|
||||||
|
defaultExtraJQueryAjaxSettings?: JQuery.AjaxSettings
|
||||||
|
}
|
||||||
|
class B {
|
||||||
|
defaultExtraJQueryAjaxSettings?: JQuery.AjaxSettings
|
||||||
|
}
|
||||||
|
class C{}
|
||||||
|
beforeEach(() => {
|
||||||
|
|
||||||
|
interceptorValue0 = false
|
||||||
|
interceptorValue1 = false
|
||||||
|
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
test('returns A with no interceptor', () => {
|
||||||
|
manager = new ApiManager([A, B])
|
||||||
|
let a = manager.get(A)
|
||||||
|
|
||||||
|
expect(a).toBeInstanceOf(A)
|
||||||
|
expect(a.defaultExtraJQueryAjaxSettings).toBeUndefined()
|
||||||
|
expect(interceptorValue0).toBe(false)
|
||||||
|
expect(interceptorValue1).toBe(false)
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
test('returns B with proper interceptor0', () => {
|
||||||
|
manager = new ApiManager([A, B], interceptor0)
|
||||||
|
let b = manager.get(B)
|
||||||
|
expect(b).toBeInstanceOf(B)
|
||||||
|
b.defaultExtraJQueryAjaxSettings.beforeSend(null, null)
|
||||||
|
expect(interceptorValue0).toBe(true)
|
||||||
|
expect(interceptorValue1).toBe(false)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('returns A with two proper interceptors', () => {
|
||||||
|
manager = new ApiManager([A, B], interceptor1, interceptor0)
|
||||||
|
let a = manager.get(A)
|
||||||
|
expect(a).toBeInstanceOf(A)
|
||||||
|
a.defaultExtraJQueryAjaxSettings.beforeSend(null, null)
|
||||||
|
expect(interceptorValue0).toBe(true)
|
||||||
|
expect(interceptorValue1).toBe(true)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('returns error on invalid class', () => {
|
||||||
|
expect(() =>{
|
||||||
|
let c = manager.get(C)
|
||||||
|
}
|
||||||
|
).toThrow(ApiMissingException)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
78
javascripts/src/lib/api_manager.ts
Normal file
78
javascripts/src/lib/api_manager.ts
Normal file
|
@ -0,0 +1,78 @@
|
||||||
|
// License: LGPL-3.0-or-later
|
||||||
|
import * as _ from 'lodash'
|
||||||
|
|
||||||
|
import * as JQuery from 'jquery'
|
||||||
|
|
||||||
|
interface ApiWithSettings {
|
||||||
|
defaultExtraJQueryAjaxSettings?: JQuery.AjaxSettings
|
||||||
|
}
|
||||||
|
|
||||||
|
export type Interceptor = (this: any, jqXHR: JQuery.jqXHR, settings: JQuery.AjaxSettings<any>) => false | void
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A service locator for creating getting a prepared instance of the API
|
||||||
|
*/
|
||||||
|
export class ApiManager {
|
||||||
|
apis: any[] = []
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @param {{new(): ApiWithSettings}[]} apis a list of APIs. Normally this will be initialized
|
||||||
|
* by the Root component with the APIS const from the generated API folder
|
||||||
|
* @param {Interceptor[]} beforeSendInterceptors the interceptors that run before your XHR request is made.
|
||||||
|
*/
|
||||||
|
constructor(apis: { new(): ApiWithSettings }[], ...beforeSendInterceptors: Interceptor[]) {
|
||||||
|
_.forEach(apis, (i) => {
|
||||||
|
let newed = new i()
|
||||||
|
if (beforeSendInterceptors && beforeSendInterceptors.length > 0) {
|
||||||
|
let a: JQuery.AjaxSettings<any> = {
|
||||||
|
beforeSend: <any>((jqXHR:JQuery.jqXHR, settings:JQuery.AjaxSettings<any>) : false|void => {
|
||||||
|
_.forEach(beforeSendInterceptors, (i:Interceptor) => i(jqXHR, settings))
|
||||||
|
return
|
||||||
|
})
|
||||||
|
}
|
||||||
|
newed.defaultExtraJQueryAjaxSettings = a
|
||||||
|
}
|
||||||
|
|
||||||
|
this.apis.push(newed)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieves the Api instance for class you request
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* //returns the nonprofit API
|
||||||
|
* api.get(NonprofitApi)
|
||||||
|
* @param {{new(): T}} c class of the Api you'd like to use
|
||||||
|
* @throws ApiMissingException when you pass in a class which isn't in the list of managed APIs
|
||||||
|
* @returns {T} instance of the Api
|
||||||
|
*/
|
||||||
|
get<T>(c: { new(): T }): T {
|
||||||
|
let result = _.find(this.apis, (i) => {
|
||||||
|
return i instanceof c
|
||||||
|
})
|
||||||
|
if (result) {
|
||||||
|
return result as T
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new ApiMissingException(`No API of type ${c.toString()}`)
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* An error for when the class you requested from ApiManager is missing
|
||||||
|
*/
|
||||||
|
export class ApiMissingException implements Error {
|
||||||
|
constructor(message: string) {
|
||||||
|
this.message = message
|
||||||
|
}
|
||||||
|
|
||||||
|
message: string;
|
||||||
|
name: string;
|
||||||
|
stack: string;
|
||||||
|
|
||||||
|
}
|
4
javascripts/src/lib/apis.ts
Normal file
4
javascripts/src/lib/apis.ts
Normal file
|
@ -0,0 +1,4 @@
|
||||||
|
// License: LGPL-3.0-or-later
|
||||||
|
import {WebUserSignInOut} from "./api/sign_in";
|
||||||
|
|
||||||
|
export const APIS = [WebUserSignInOut]
|
11
javascripts/src/lib/csrf_interceptor.ts
Normal file
11
javascripts/src/lib/csrf_interceptor.ts
Normal file
|
@ -0,0 +1,11 @@
|
||||||
|
// License: LGPL-3.0-or-later
|
||||||
|
/**
|
||||||
|
* An Interceptor for ApiManager which adds the CSRF token to API calls
|
||||||
|
* @param {JQuery.jqXHR} jqXHR
|
||||||
|
* @param {JQuery.AjaxSettings<any>} settings
|
||||||
|
* @returns {false | void}
|
||||||
|
*/
|
||||||
|
export function CSRFInterceptor(this:any, jqXHR:JQuery.jqXHR, settings: JQuery.AjaxSettings<any>): false|void {
|
||||||
|
jqXHR.setRequestHeader('X-CSRF-Token', (<any>window)._csrf)
|
||||||
|
}
|
||||||
|
|
87
javascripts/src/lib/houdini_form.ts
Normal file
87
javascripts/src/lib/houdini_form.ts
Normal file
|
@ -0,0 +1,87 @@
|
||||||
|
// License: LGPL-3.0-or-later
|
||||||
|
import {Form} from "mobx-react-form";
|
||||||
|
import {action, runInAction} from 'mobx'
|
||||||
|
import validator = require("validator")
|
||||||
|
import * as _ from 'lodash'
|
||||||
|
import {ValidationErrorsException} from "../../api";
|
||||||
|
|
||||||
|
|
||||||
|
export class HoudiniForm extends Form {
|
||||||
|
plugins() {
|
||||||
|
return {
|
||||||
|
vjf: validator
|
||||||
|
|
||||||
|
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
interface PathToFormField {
|
||||||
|
[props: string]: string
|
||||||
|
}
|
||||||
|
|
||||||
|
type FormFieldToPath = PathToFormField
|
||||||
|
|
||||||
|
/**
|
||||||
|
* tool for converting between the form's data structure
|
||||||
|
* to the AJAX datastructure and properly assigning
|
||||||
|
* errors if AJAX request fails
|
||||||
|
* As an example for the, consider the the following form structure:
|
||||||
|
* {
|
||||||
|
* // a tab in a Wizard
|
||||||
|
* nonprofitTab: {
|
||||||
|
* organization_name: {some field info}
|
||||||
|
* }
|
||||||
|
* }
|
||||||
|
*
|
||||||
|
* We want to create a data structure for AJAX like so:
|
||||||
|
*
|
||||||
|
* In the database
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
export class StaticFormToErrorAndBackConverter<T> {
|
||||||
|
|
||||||
|
pathToForm: PathToFormField
|
||||||
|
formToPath: FormFieldToPath
|
||||||
|
|
||||||
|
|
||||||
|
constructor(pathToForm: PathToFormField) {
|
||||||
|
this.pathToForm = pathToForm
|
||||||
|
this.formToPath = _.invert(pathToForm)
|
||||||
|
}
|
||||||
|
|
||||||
|
convertFormToObject(form: Form): T {
|
||||||
|
let output = {}
|
||||||
|
for (let pathToFormKey in this.pathToForm) {
|
||||||
|
if (this.pathToForm.hasOwnProperty(pathToFormKey)) {
|
||||||
|
let formPath = this.pathToForm[pathToFormKey]
|
||||||
|
if (form.$(formPath).value && _.trim(form.$(formPath).value) !== "")
|
||||||
|
_.set(output, pathToFormKey, form.$(formPath).value)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
return output as T
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
@action.bound
|
||||||
|
convertErrorToForm(errorException: ValidationErrorsException, form: Form): void {
|
||||||
|
runInAction(() => {
|
||||||
|
_.forEach(errorException.item.errors, (error) => {
|
||||||
|
let message = error.messages.join(", ")
|
||||||
|
_.forEach(error.params, (p) => {
|
||||||
|
if (this.pathToForm[p])
|
||||||
|
form.$(this.pathToForm[p]).invalidate(message)
|
||||||
|
else {
|
||||||
|
console.warn(`We couldn't find a form element for path: "${p}"`)
|
||||||
|
}
|
||||||
|
|
||||||
|
})
|
||||||
|
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
37
javascripts/src/lib/regex.spec.ts
Normal file
37
javascripts/src/lib/regex.spec.ts
Normal file
|
@ -0,0 +1,37 @@
|
||||||
|
// License: LGPL-3.0-or-later
|
||||||
|
import * as Regex from './regex'
|
||||||
|
import 'jest';
|
||||||
|
|
||||||
|
|
||||||
|
describe('Regex.Email', () => {
|
||||||
|
test("rejects empty",() =>
|
||||||
|
expect(Regex.Email.test("")).toBeFalsy()
|
||||||
|
)
|
||||||
|
|
||||||
|
test("rejects blank",() =>
|
||||||
|
expect(Regex.Email.test(" ")).toBeFalsy()
|
||||||
|
)
|
||||||
|
|
||||||
|
test("rejects no before @ part",() =>
|
||||||
|
expect(Regex.Email.test("@h.n")).toBeFalsy()
|
||||||
|
)
|
||||||
|
test("rejects no after @ part",() =>
|
||||||
|
expect(Regex.Email.test("something@")).toBeFalsy()
|
||||||
|
)
|
||||||
|
|
||||||
|
test("rejects with space in before @ part",() =>
|
||||||
|
expect(Regex.Email.test("somethi ng@s.c")).toBeFalsy()
|
||||||
|
)
|
||||||
|
|
||||||
|
test("rejects with space in after @ part",() =>
|
||||||
|
expect(Regex.Email.test("something@s j.c")).toBeFalsy()
|
||||||
|
)
|
||||||
|
|
||||||
|
test("accepts basic email",() =>
|
||||||
|
expect(Regex.Email.test("something+f.3+3@s.com")).toBeTruthy()
|
||||||
|
)
|
||||||
|
|
||||||
|
test("accepts IDN and Unicode email",() =>
|
||||||
|
expect(Regex.Email.test("македонија+f.и+3@বাংলাদেশ.icom.museum")).toBeTruthy()
|
||||||
|
)
|
||||||
|
})
|
2
javascripts/src/lib/regex.ts
Normal file
2
javascripts/src/lib/regex.ts
Normal file
|
@ -0,0 +1,2 @@
|
||||||
|
// License: LGPL-3.0-or-later
|
||||||
|
export const Email = /^[^ ]+@[^ ]+\.[^ ]+$/i
|
125
javascripts/src/lib/tests/helpers.ts
Normal file
125
javascripts/src/lib/tests/helpers.ts
Normal file
|
@ -0,0 +1,125 @@
|
||||||
|
// License: LGPL-3.0-or-later
|
||||||
|
import * as React from 'react';
|
||||||
|
import { IntlProvider, intlShape } from 'react-intl';
|
||||||
|
import { mount, shallow, ShallowRendererProps, MountRendererProps, ShallowWrapper } from 'enzyme';
|
||||||
|
|
||||||
|
// Create the IntlProvider to retrieve context for wrapping around.
|
||||||
|
const intlProvider = new IntlProvider({ locale: 'en'}, {});
|
||||||
|
const { intl } = intlProvider.getChildContext();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* When using React-Intl `injectIntl` on components, props.intl is required.
|
||||||
|
*/
|
||||||
|
function nodeWithIntlProp(node:any) {
|
||||||
|
return React.cloneElement(node, { intl });
|
||||||
|
}
|
||||||
|
|
||||||
|
export function shallowWithIntl(node:any, options?:ShallowRendererProps) {
|
||||||
|
let context = {}
|
||||||
|
|
||||||
|
if (options ) {
|
||||||
|
context = options.context
|
||||||
|
|
||||||
|
}
|
||||||
|
return shallow(
|
||||||
|
nodeWithIntlProp(node),
|
||||||
|
{
|
||||||
|
...options,
|
||||||
|
context: (Object as any).assign({}, context, {intl})
|
||||||
|
}
|
||||||
|
).dive();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
export function mountWithIntl(node:any, options?:MountRendererProps) {
|
||||||
|
let context = {}
|
||||||
|
let additionalOptions:Array<any> = []
|
||||||
|
let childContextTypes = {}
|
||||||
|
|
||||||
|
if (options) {
|
||||||
|
context = options.context
|
||||||
|
childContextTypes = options.childContextTypes
|
||||||
|
}
|
||||||
|
return mount(
|
||||||
|
nodeWithIntlProp(node),
|
||||||
|
{
|
||||||
|
...options,
|
||||||
|
context:(Object as any).assign({},context, {intl}),
|
||||||
|
childContextTypes: (Object as any).assign({}, { intl: intlShape }, childContextTypes)
|
||||||
|
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
interface shallowUntilTargetProps {
|
||||||
|
maxTries?: number
|
||||||
|
shallowOptions?: ShallowRendererProps,
|
||||||
|
_shallow?: Function
|
||||||
|
}
|
||||||
|
|
||||||
|
/* from: https://github.com/mozilla/addons-frontend/blob/18f433f2199fb3d68109ef4d0a164ba1af37520a/tests/unit/helpers.js
|
||||||
|
* Repeatedly render a component tree using enzyme.shallow() until
|
||||||
|
* finding and rendering TargetComponent.
|
||||||
|
*
|
||||||
|
* This is useful for testing a component wrapped in one or more
|
||||||
|
* HOCs (higher order components).
|
||||||
|
*
|
||||||
|
* The `componentInstance` parameter is a React component instance.
|
||||||
|
* Example: <MyComponent {...props} />
|
||||||
|
*
|
||||||
|
* The `TargetComponent` parameter is the React class (or function) that
|
||||||
|
* you want to retrieve from the component tree.
|
||||||
|
*/
|
||||||
|
export function shallowUntilTarget<T>(componentInstance:React.ReactElement<any>, TargetComponent:{new(): T}, props:shallowUntilTargetProps): ShallowWrapper<T> {
|
||||||
|
if (!componentInstance) {
|
||||||
|
throw new Error('componentInstance parameter is required');
|
||||||
|
}
|
||||||
|
if (!TargetComponent) {
|
||||||
|
throw new Error('TargetComponent parameter is required');
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
let maxTries = props.maxTries || 10
|
||||||
|
let shallowOptions = props.shallowOptions || null
|
||||||
|
let _shallow = props._shallow || shallow
|
||||||
|
|
||||||
|
let root = _shallow(componentInstance, shallowOptions);
|
||||||
|
|
||||||
|
if (typeof root.type() === 'string') {
|
||||||
|
// If type() is a string then it's a DOM Node.
|
||||||
|
// If it were wrapped, it would be a React component.
|
||||||
|
throw new Error(
|
||||||
|
'Cannot unwrap this component because it is not wrapped');
|
||||||
|
}
|
||||||
|
|
||||||
|
for (let tries = 1; tries <= maxTries; tries++) {
|
||||||
|
if (root.is(TargetComponent)) {
|
||||||
|
// Now that we found the target component, render it.
|
||||||
|
return root.shallow(shallowOptions);
|
||||||
|
}
|
||||||
|
// Unwrap the next component in the hierarchy.
|
||||||
|
root = root.dive();
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new Error(`Could not find ${TargetComponent} in rendered
|
||||||
|
instance: ${componentInstance}; gave up after ${maxTries} tries`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function shallowUntilTargetWithIntl(node:any, TargetComponent:any, options?:ShallowRendererProps): ShallowWrapper<any> {
|
||||||
|
let context = {}
|
||||||
|
|
||||||
|
if (options ) {
|
||||||
|
context = options.context
|
||||||
|
|
||||||
|
}
|
||||||
|
return shallowUntilTarget(
|
||||||
|
nodeWithIntlProp(node),
|
||||||
|
TargetComponent,
|
||||||
|
{
|
||||||
|
shallowOptions: {
|
||||||
|
...options,
|
||||||
|
context: (Object as any).assign({}, context, {intl})
|
||||||
|
}})
|
||||||
|
}
|
57
javascripts/src/lib/vjf_rules.ts
Normal file
57
javascripts/src/lib/vjf_rules.ts
Normal file
|
@ -0,0 +1,57 @@
|
||||||
|
// License: LGPL-3.0-or-later
|
||||||
|
import * as Regex from './regex'
|
||||||
|
import {Field, Form} from "mobx-react-form";
|
||||||
|
|
||||||
|
|
||||||
|
interface ValidationInput {
|
||||||
|
field: Field
|
||||||
|
validator?: ValidatorJS.ValidatorStatic
|
||||||
|
form?: Form
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
interface StringBoolTuple extends Array<boolean|string>{0:boolean, 1:string}
|
||||||
|
|
||||||
|
interface Validation {
|
||||||
|
(input:ValidationInput): StringBoolTuple
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
export class Validations {
|
||||||
|
static isEmail({field}:ValidationInput) : StringBoolTuple {
|
||||||
|
return [field.value.match(Regex.Email) !== null,
|
||||||
|
`${field.label} is not a valid email`]
|
||||||
|
}
|
||||||
|
|
||||||
|
static shouldBeEqualTo (targetPath: string): Validation {
|
||||||
|
return ({field, form}:ValidationInput) => {
|
||||||
|
const fieldsAreEquals = (form.$(targetPath).value === field.value);
|
||||||
|
return [fieldsAreEquals, `${field.label} and ${form.$(targetPath).label} must be the same`]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static isUrl({field, validator}:ValidationInput):StringBoolTuple {
|
||||||
|
return [validator.isURL(field.value),
|
||||||
|
`${field.label} must be a valid URL`]
|
||||||
|
}
|
||||||
|
|
||||||
|
static isFilled({field, validator}:ValidationInput) :StringBoolTuple {
|
||||||
|
return [
|
||||||
|
!validator.isEmpty(field.value),
|
||||||
|
`${field.label} must be filled out`
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
static optional(validation:Validation) : Validation {
|
||||||
|
return ({field, form, validator}:ValidationInput) => {
|
||||||
|
if (!field.value || validator.isEmpty(field.value)){
|
||||||
|
return [true, ""]
|
||||||
|
}
|
||||||
|
else{
|
||||||
|
return validation({field: field, form: form, validator: validator})
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
|
@ -39,7 +39,7 @@ class CopyNamingAlgorithm
|
||||||
end
|
end
|
||||||
}
|
}
|
||||||
|
|
||||||
raise ArgumentError.new("It's not possible to generate a UNIQUE name using name_to_copy: #{name_to_copy} copy_addition: #{self.copy_addition} separator_before_copy_number: #{self.separator_before_copy_number} max_copy_num: #{self.max_copies} max_length: #{self.max_length}")
|
raise UnableToCreateNameCopyError.new("It's not possible to generate a UNIQUE name using name_to_copy: #{name_to_copy} copy_addition: #{self.copy_addition} separator_before_copy_number: #{self.separator_before_copy_number} max_copy_num: #{self.max_copies} max_length: #{self.max_length}")
|
||||||
end
|
end
|
||||||
|
|
||||||
def generate_name(name_to_copy, copy_num)
|
def generate_name(name_to_copy, copy_num)
|
||||||
|
@ -47,7 +47,7 @@ class CopyNamingAlgorithm
|
||||||
|
|
||||||
# is what_to_add longer than max length? If so, it's not possible to create a copy
|
# is what_to_add longer than max length? If so, it's not possible to create a copy
|
||||||
if (what_to_add.length > self.max_length)
|
if (what_to_add.length > self.max_length)
|
||||||
raise ArgumentError.new("It's not possible to generate a name using name_to_copy: #{name_to_copy} copy_addition: #{self.copy_addition} separator_before_copy_number: #{self.separator_before_copy_number} copy_num: #{copy_num} max_length: #{self.max_length}")
|
raise UnableToCreateNameCopyError.new("It's not possible to generate a name using name_to_copy: #{name_to_copy} copy_addition: #{self.copy_addition} separator_before_copy_number: #{self.separator_before_copy_number} copy_num: #{copy_num} max_length: #{self.max_length}")
|
||||||
end
|
end
|
||||||
max_length_for_name_to_copy = self.max_length - what_to_add.length
|
max_length_for_name_to_copy = self.max_length - what_to_add.length
|
||||||
name_to_copy[0..max_length_for_name_to_copy-1] + what_to_add
|
name_to_copy[0..max_length_for_name_to_copy-1] + what_to_add
|
||||||
|
@ -58,4 +58,8 @@ class CopyNamingAlgorithm
|
||||||
"%0#{number_of_digits}d" % unprefixed_copy_number
|
"%0#{number_of_digits}d" % unprefixed_copy_number
|
||||||
end
|
end
|
||||||
|
|
||||||
|
end
|
||||||
|
|
||||||
|
class UnableToCreateNameCopyError < ArgumentError
|
||||||
|
|
||||||
end
|
end
|
|
@ -1,7 +1,7 @@
|
||||||
# License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later
|
# License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later
|
||||||
module Email
|
module Email
|
||||||
|
|
||||||
Regex ||= /\A([^@\s]+)@((?:[-a-z0-9]+\.)+[a-z]{2,})\Z/i
|
Regex ||= /^[^ ]+@[^ ]+\.[^ ]+/i
|
||||||
PsqlRegex ||= '^[A-Za-z0-9._%-]+@[A-Za-z0-9.-]+[.][A-Za-z]+$'
|
#PsqlRegex ||= '^[A-Za-z0-9._%-]+@[A-Za-z0-9.-]+[.][A-Za-z]+$'
|
||||||
|
|
||||||
end
|
end
|
||||||
|
|
9
lib/generators/api/entity/USAGE
Normal file
9
lib/generators/api/entity/USAGE
Normal file
|
@ -0,0 +1,9 @@
|
||||||
|
Description:
|
||||||
|
Generates a new entity to be returned by the API
|
||||||
|
|
||||||
|
Example:
|
||||||
|
rails generate api:entity EntityName
|
||||||
|
|
||||||
|
This will create:
|
||||||
|
A new subclass of Grape::Entity called Houdini::V1::Entities::EntityName
|
||||||
|
in app/api/houdini/v1/entities/entity_name.rb
|
7
lib/generators/api/entity/entity_generator.rb
Normal file
7
lib/generators/api/entity/entity_generator.rb
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
# License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later
|
||||||
|
class Api::EntityGenerator < Rails::Generators::NamedBase
|
||||||
|
source_root File.expand_path('../templates', __FILE__)
|
||||||
|
def copy_to_entity
|
||||||
|
template 'entity.rb.erb', File.join("app/api/houdini/v1/entities", "#{name.underscore}.rb")
|
||||||
|
end
|
||||||
|
end
|
4
lib/generators/api/entity/templates/entity.rb.erb
Normal file
4
lib/generators/api/entity/templates/entity.rb.erb
Normal file
|
@ -0,0 +1,4 @@
|
||||||
|
# License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later
|
||||||
|
class Houdini::V1::Entities::<%= name.camelcase %> < Grape::Entity
|
||||||
|
|
||||||
|
end
|
10
lib/generators/api/resource/USAGE
Normal file
10
lib/generators/api/resource/USAGE
Normal file
|
@ -0,0 +1,10 @@
|
||||||
|
Description:
|
||||||
|
Creates a new resource for API usage
|
||||||
|
|
||||||
|
Example:
|
||||||
|
rails generate api:resource ResourceName
|
||||||
|
|
||||||
|
This will create:
|
||||||
|
* A new subclass of Grape::API called Houdini::V1::ResourceName
|
||||||
|
at app/api/houdini/v1/resource_name.rb
|
||||||
|
* Houdini::V1::ResourceName mounted to the api in app/api/houdini/v1/api.rb
|
10
lib/generators/api/resource/resource_generator.rb
Normal file
10
lib/generators/api/resource/resource_generator.rb
Normal file
|
@ -0,0 +1,10 @@
|
||||||
|
class Api::ResourceGenerator < Rails::Generators::NamedBase
|
||||||
|
source_root File.expand_path('../templates', __FILE__)
|
||||||
|
def copy_to_resource
|
||||||
|
template 'resource.rb.erb', File.join("app/api/houdini/v1", "#{name.underscore}.rb")
|
||||||
|
end
|
||||||
|
|
||||||
|
def add_to_root_api
|
||||||
|
inject_into_file "app/api/houdini/v1/api.rb", "\tmount Houdini::V1::#{ name.camelcase}\n", after: "class Houdini::V1::API < Grape::API\n"
|
||||||
|
end
|
||||||
|
end
|
3
lib/generators/api/resource/templates/resource.rb.erb
Normal file
3
lib/generators/api/resource/templates/resource.rb.erb
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
# License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later
|
||||||
|
class Houdini::V1::<%= name.camelcase %> < Grape::API
|
||||||
|
end
|
8
lib/generators/api/validator/USAGE
Normal file
8
lib/generators/api/validator/USAGE
Normal file
|
@ -0,0 +1,8 @@
|
||||||
|
Description:
|
||||||
|
Explain the generator
|
||||||
|
|
||||||
|
Example:
|
||||||
|
rails generate validator Thing
|
||||||
|
|
||||||
|
This will create:
|
||||||
|
what/will/it/create
|
6
lib/generators/api/validator/templates/validator.rb.erb
Normal file
6
lib/generators/api/validator/templates/validator.rb.erb
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
# License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later
|
||||||
|
class Houdini::V1::Validators::<%= name.camelcase %> < Grape::Validations::Base
|
||||||
|
def validate_param!(attr_name, params)
|
||||||
|
fail Grape::Exceptions::Validation, params: [@scope.full_name(attr_name)], message: 'MESSAGE'
|
||||||
|
end
|
||||||
|
end
|
15
lib/generators/api/validator/validator_generator.rb
Normal file
15
lib/generators/api/validator/validator_generator.rb
Normal file
|
@ -0,0 +1,15 @@
|
||||||
|
# License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later
|
||||||
|
class Api::ValidatorGenerator < Rails::Generators::NamedBase
|
||||||
|
source_root File.expand_path('../templates', __FILE__)
|
||||||
|
|
||||||
|
def copy_to_validators
|
||||||
|
post_api_part = File.join("houdini/v1/validators", "#{name.underscore}.rb")
|
||||||
|
output_file = File.join("app/api", post_api_part )
|
||||||
|
template 'validator.rb.erb', output_file
|
||||||
|
end
|
||||||
|
|
||||||
|
def add_to_root_validations
|
||||||
|
post_api_part = File.join("houdini/v1/validators", "#{name.underscore}")
|
||||||
|
append_to_file "app/api/houdini/v1/validations.rb", "\nrequire '#{post_api_part}'"
|
||||||
|
end
|
||||||
|
end
|
|
@ -3,7 +3,7 @@ class React::ComponentGenerator < Rails::Generators::NamedBase
|
||||||
source_root File.expand_path('../templates', __FILE__)
|
source_root File.expand_path('../templates', __FILE__)
|
||||||
|
|
||||||
def copy_file_to_component
|
def copy_file_to_component
|
||||||
template 'component.tsx.erb', File.join("javascripts/src/components", *(class_path + ["#{file_name.underscore}.tsx"]))
|
template 'component.tsx.erb', File.join("javascripts/src/components", *(class_path + ["#{file_name.camelize}.tsx"]))
|
||||||
template 'component.spec.ts.erb', File.join("javascripts/src/components", *(class_path + ["#{file_name.underscore}.spec.ts"]))
|
template 'component.spec.tsx.erb', File.join("javascripts/src/components", *(class_path + ["#{file_name.camelize}.spec.tsx"]))
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -1,10 +0,0 @@
|
||||||
// License: LGPL-3.0-or-later
|
|
||||||
import * as React from 'react';
|
|
||||||
import 'jest';
|
|
||||||
import * as Component from './<%= file_name.underscore%>'
|
|
||||||
|
|
||||||
describe('<%= file_name.camelize %>', () => {
|
|
||||||
test('your test here', () => {
|
|
||||||
expect(false).toBe(true)
|
|
||||||
})
|
|
||||||
})
|
|
|
@ -0,0 +1,10 @@
|
||||||
|
// License: LGPL-3.0-or-later
|
||||||
|
import * as React from 'react';
|
||||||
|
import 'jest';
|
||||||
|
import <%= file_name.camelize %> from './<%= file_name.camelize%>'
|
||||||
|
|
||||||
|
describe('<%= file_name.camelize %>', () => {
|
||||||
|
test('your test here', () => {
|
||||||
|
expect(false).toBe(true)
|
||||||
|
})
|
||||||
|
})
|
|
@ -1,14 +1,20 @@
|
||||||
// License: LGPL-3.0-or-later
|
// License: LGPL-3.0-or-later
|
||||||
import * as React from 'react';
|
import * as React from 'react';
|
||||||
|
import { observer } from 'mobx-react';
|
||||||
|
import {InjectedIntlProps, injectIntl} from 'react-intl';
|
||||||
|
|
||||||
export interface <%= file_name.camelize %>Props
|
export interface <%= file_name.camelize %>Props
|
||||||
{
|
{
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export default class <%= file_name.camelize %> extends React.Component<<%= file_name.camelize %>Props, {}> {
|
class <%= file_name.camelize %> extends React.Component<<%= file_name.camelize %>Props & InjectedIntlProps, {}> {
|
||||||
render() {
|
render() {
|
||||||
return <div></div>;
|
return <div></div>;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export default injectIntl(observer(<%= file_name.camelize %>))
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -3,7 +3,7 @@ import 'jest';
|
||||||
import * as Lib from './<%= file_name.underscore%>'
|
import * as Lib from './<%= file_name.underscore%>'
|
||||||
|
|
||||||
describe('<%= file_name.camelize %>', () => {
|
describe('<%= file_name.camelize %>', () => {
|
||||||
test('your test here', () => {
|
test('your test here', () => {
|
||||||
expect(false).toBe(true)
|
expect(false).toBe(true)
|
||||||
})
|
})
|
||||||
})
|
})
|
|
@ -1,5 +1,14 @@
|
||||||
// License: LGPL-3.0-or-later
|
// License: LGPL-3.0-or-later
|
||||||
|
|
||||||
// require a root component here. This will be treated as the root of a webpack package
|
// require a root component here. This will be treated as the root of a webpack package
|
||||||
|
import Root from "../src/components/common/Root"
|
||||||
|
import RegistrationPage from "../src/components/<%= file_name.underscore%>/<%= file_name.underscore%>"
|
||||||
|
|
||||||
import "../src/components/<%= file_name.underscore%>/<%= file_name.underscore%>"
|
import * as ReactDOM from 'react-dom'
|
||||||
|
import * as React from 'react'
|
||||||
|
|
||||||
|
function LoadReactPage(element:HTMLElement) {
|
||||||
|
ReactDOM.render(<Root><RegistrationPage/></Root>, element)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
(window as any).LoadReactPage = LoadReactPage
|
8
lib/generators/ts/declaration/USAGE
Normal file
8
lib/generators/ts/declaration/USAGE
Normal file
|
@ -0,0 +1,8 @@
|
||||||
|
Description:
|
||||||
|
Creates a new Typescript declaration file for modules which don't have them
|
||||||
|
|
||||||
|
Example:
|
||||||
|
rails generate ts:declaration module_name
|
||||||
|
|
||||||
|
This will create:
|
||||||
|
A new basic Typescript declaration file at 'types/module_name/index.d.ts'
|
7
lib/generators/ts/declaration/declaration_generator.rb
Normal file
7
lib/generators/ts/declaration/declaration_generator.rb
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
# License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later
|
||||||
|
class Ts::DeclarationGenerator < Rails::Generators::NamedBase
|
||||||
|
source_root File.expand_path('../templates', __FILE__)
|
||||||
|
def copy_template
|
||||||
|
template 'template.d.ts.erb', File.join("types", name, 'index.d.ts')
|
||||||
|
end
|
||||||
|
end
|
|
@ -0,0 +1,2 @@
|
||||||
|
// License: LGPL-3.0-or-later
|
||||||
|
declare module "<%= name %>"
|
32
lib/slug_nonprofit_naming_algorithm.rb
Normal file
32
lib/slug_nonprofit_naming_algorithm.rb
Normal file
|
@ -0,0 +1,32 @@
|
||||||
|
# License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later
|
||||||
|
class SlugNonprofitNamingAlgorithm < CopyNamingAlgorithm
|
||||||
|
|
||||||
|
attr_accessor :state_slug, :city_slug
|
||||||
|
|
||||||
|
def initialize( state_slug, city_slug)
|
||||||
|
@state_slug = state_slug
|
||||||
|
@city_slug = city_slug
|
||||||
|
end
|
||||||
|
|
||||||
|
def copy_addition
|
||||||
|
""
|
||||||
|
end
|
||||||
|
|
||||||
|
def max_copies
|
||||||
|
99
|
||||||
|
end
|
||||||
|
|
||||||
|
def separator_before_copy_number
|
||||||
|
'-'
|
||||||
|
end
|
||||||
|
|
||||||
|
def get_name_for_entity(name_entity)
|
||||||
|
name_entity.slug
|
||||||
|
end
|
||||||
|
|
||||||
|
def get_already_used_name_entities(base_name)
|
||||||
|
end_name = "\\-\\d{2}"
|
||||||
|
Nonprofit.method(:where).call('slug SIMILAR TO ? AND state_code_slug = ? AND city_slug = ?', base_name + end_name, @state_slug, @city_slug).select('slug')
|
||||||
|
end
|
||||||
|
|
||||||
|
end
|
45
lib/swagger-typescript-jquery/README.mustache
Normal file
45
lib/swagger-typescript-jquery/README.mustache
Normal file
|
@ -0,0 +1,45 @@
|
||||||
|
## {{npmName}}@{{npmVersion}}
|
||||||
|
|
||||||
|
This generator creates TypeScript/JavaScript client that utilizes [jQuery](https://jquery.com/). The generated Node module can be used in the following environments:
|
||||||
|
|
||||||
|
Environment
|
||||||
|
* Node.js
|
||||||
|
* Webpack
|
||||||
|
* Browserify
|
||||||
|
|
||||||
|
Language level
|
||||||
|
* ES5 - you must have a Promises/A+ library installed
|
||||||
|
* ES6
|
||||||
|
|
||||||
|
Module system
|
||||||
|
* CommonJS
|
||||||
|
* ES6 module system
|
||||||
|
|
||||||
|
It can be used in both TypeScript and JavaScript. In TypeScript, the definition should be automatically resolved via `package.json`. ([Reference](http://www.typescriptlang.org/docs/handbook/typings-for-npm-packages.html))
|
||||||
|
|
||||||
|
### Building
|
||||||
|
|
||||||
|
To build an compile the typescript sources to javascript use:
|
||||||
|
```
|
||||||
|
npm install
|
||||||
|
npm run build
|
||||||
|
```
|
||||||
|
|
||||||
|
### Publishing
|
||||||
|
|
||||||
|
First build the package then run ```npm publish```
|
||||||
|
|
||||||
|
### Consuming
|
||||||
|
|
||||||
|
navigate to the folder of your consuming project and run one of the following commands.
|
||||||
|
|
||||||
|
_published:_
|
||||||
|
|
||||||
|
```
|
||||||
|
npm install {{npmName}}@{{npmVersion}} --save
|
||||||
|
```
|
||||||
|
|
||||||
|
_unPublished (not recommended):_
|
||||||
|
|
||||||
|
```
|
||||||
|
npm install PATH_TO_GENERATED_PACKAGE --save
|
260
lib/swagger-typescript-jquery/api.mustache
Normal file
260
lib/swagger-typescript-jquery/api.mustache
Normal file
|
@ -0,0 +1,260 @@
|
||||||
|
{{>licenseInfo}}
|
||||||
|
|
||||||
|
{{#jqueryAlreadyImported}}
|
||||||
|
declare var $ : any;
|
||||||
|
{{/jqueryAlreadyImported}}
|
||||||
|
{{^jqueryAlreadyImported}}
|
||||||
|
import * as $ from 'jquery';
|
||||||
|
{{/jqueryAlreadyImported}}
|
||||||
|
import * as models from '../model/models';
|
||||||
|
import { COLLECTION_FORMATS } from '../variables';
|
||||||
|
import { Configuration } from '../configuration';
|
||||||
|
|
||||||
|
/* tslint:disable:no-unused-variable member-ordering */
|
||||||
|
|
||||||
|
{{#operations}}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
{{#description}}
|
||||||
|
/**
|
||||||
|
* {{&description}}
|
||||||
|
*/
|
||||||
|
{{/description}}
|
||||||
|
export class {{classname}} {
|
||||||
|
protected basePath = '{{{basePath}}}';
|
||||||
|
public defaultHeaders: Array<string> = [];
|
||||||
|
public defaultExtraJQueryAjaxSettings?: JQueryAjaxSettings = null;
|
||||||
|
public configuration: Configuration = new Configuration();
|
||||||
|
|
||||||
|
constructor(basePath?: string, configuration?: Configuration, defaultExtraJQueryAjaxSettings?: JQueryAjaxSettings) {
|
||||||
|
if (basePath) {
|
||||||
|
this.basePath = basePath;
|
||||||
|
}
|
||||||
|
if (configuration) {
|
||||||
|
this.configuration = configuration;
|
||||||
|
}
|
||||||
|
if (defaultExtraJQueryAjaxSettings) {
|
||||||
|
this.defaultExtraJQueryAjaxSettings = defaultExtraJQueryAjaxSettings;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private extendObj<T1, T2 extends T1>(objA: T2, objB: T2): T1|T2 {
|
||||||
|
for (let key in objB) {
|
||||||
|
if (objB.hasOwnProperty(key)) {
|
||||||
|
objA[key] = objB[key];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return objA;
|
||||||
|
}
|
||||||
|
|
||||||
|
{{#operation}}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {{¬es}}
|
||||||
|
{{#summary}}
|
||||||
|
* @summary {{&summary}}
|
||||||
|
{{/summary}}
|
||||||
|
{{#allParams}}
|
||||||
|
* @param {{paramName}} {{description}}
|
||||||
|
{{/allParams}}
|
||||||
|
*/
|
||||||
|
public {{nickname}}({{#allParams}}{{paramName}}{{^required}}?{{/required}}: {{{dataType}}}, {{/allParams}}extraJQueryAjaxSettings?: JQueryAjaxSettings): JQueryPromise<{{#returnType}}{{{returnType}}}{{/returnType}}{{^returnType}}any{{/returnType}} > {
|
||||||
|
let localVarPath = this.basePath + '{{{path}}}'{{#pathParams}}.replace('{' + '{{baseName}}' + '}', encodeURIComponent(String({{paramName}}))){{/pathParams}};
|
||||||
|
|
||||||
|
let queryParameters: any = {};
|
||||||
|
let headerParams: any = {};
|
||||||
|
{{#hasFormParams}}
|
||||||
|
let formParams = new FormData();
|
||||||
|
let reqHasFile = false;
|
||||||
|
|
||||||
|
{{/hasFormParams}}
|
||||||
|
{{#allParams}}
|
||||||
|
{{#required}}
|
||||||
|
// verify required parameter '{{paramName}}' is not null or undefined
|
||||||
|
if ({{paramName}} === null || {{paramName}} === undefined) {
|
||||||
|
throw new Error('Required parameter {{paramName}} was null or undefined when calling {{nickname}}.');
|
||||||
|
}
|
||||||
|
|
||||||
|
{{/required}}
|
||||||
|
{{/allParams}}
|
||||||
|
{{#queryParams}}
|
||||||
|
{{#isListContainer}}
|
||||||
|
if ({{paramName}}) {
|
||||||
|
{{#isCollectionFormatMulti}}
|
||||||
|
{{paramName}}.forEach((element: any) => {
|
||||||
|
queryParameters['{{baseName}}'].push(element);
|
||||||
|
});
|
||||||
|
{{/isCollectionFormatMulti}}
|
||||||
|
{{^isCollectionFormatMulti}}
|
||||||
|
queryParameters['{{baseName}}'] = {{paramName}}.join(COLLECTION_FORMATS['{{collectionFormat}}']);
|
||||||
|
{{/isCollectionFormatMulti}}
|
||||||
|
}
|
||||||
|
{{/isListContainer}}
|
||||||
|
{{^isListContainer}}
|
||||||
|
if ({{paramName}} !== null && {{paramName}} !== undefined) {
|
||||||
|
{{#isDateTime}}
|
||||||
|
queryParameters['{{baseName}}'] = {{paramName}}.toISOString();
|
||||||
|
{{/isDateTime}}
|
||||||
|
{{^isDateTime}}
|
||||||
|
{{#isDate}}
|
||||||
|
queryParameters['{{baseName}}'] = {{paramName}}.toISOString();
|
||||||
|
{{/isDate}}
|
||||||
|
{{^isDate}}
|
||||||
|
queryParameters['{{baseName}}'] = <string><any>{{paramName}};
|
||||||
|
{{/isDate}}
|
||||||
|
{{/isDateTime}}
|
||||||
|
}
|
||||||
|
{{/isListContainer}}
|
||||||
|
{{/queryParams}}
|
||||||
|
|
||||||
|
localVarPath = localVarPath + "?" + $.param(queryParameters);
|
||||||
|
{{#formParams}}
|
||||||
|
{{#isFile}}
|
||||||
|
reqHasFile = true;
|
||||||
|
formParams.append("{{baseName}}", {{paramName}});
|
||||||
|
{{/isFile}}
|
||||||
|
{{^isFile}}
|
||||||
|
{{#isListContainer}}
|
||||||
|
if ({{paramName}}) {
|
||||||
|
{{#isCollectionFormatMulti}}
|
||||||
|
{{paramName}}.forEach((element: any) => {
|
||||||
|
formParams.append('{{baseName}}', element);
|
||||||
|
});
|
||||||
|
{{/isCollectionFormatMulti}}
|
||||||
|
{{^isCollectionFormatMulti}}
|
||||||
|
formParams.append('{{baseName}}', {{paramName}}.join(COLLECTION_FORMATS['{{collectionFormat}}']));
|
||||||
|
{{/isCollectionFormatMulti}}
|
||||||
|
}
|
||||||
|
{{/isListContainer}}
|
||||||
|
{{^isListContainer}}
|
||||||
|
if ({{paramName}} !== null && {{paramName}} !== undefined) {
|
||||||
|
formParams.append('{{baseName}}', <any>{{paramName}});
|
||||||
|
}
|
||||||
|
{{/isListContainer}}
|
||||||
|
{{/isFile}}
|
||||||
|
{{/formParams}}
|
||||||
|
{{#headerParams}}
|
||||||
|
{{#isListContainer}}
|
||||||
|
if ({{paramName}}) {
|
||||||
|
headerParams['{{baseName}}'] = {{paramName}}.join(COLLECTION_FORMATS['{{collectionFormat}}']);
|
||||||
|
}
|
||||||
|
{{/isListContainer}}
|
||||||
|
{{^isListContainer}}
|
||||||
|
headerParams['{{baseName}}'] = String({{paramName}});
|
||||||
|
{{/isListContainer}}
|
||||||
|
|
||||||
|
{{/headerParams}}
|
||||||
|
// to determine the Content-Type header
|
||||||
|
let consumes: string[] = [
|
||||||
|
{{#consumes}}
|
||||||
|
'{{{mediaType}}}'{{#hasMore}}, {{/hasMore}}
|
||||||
|
{{/consumes}}
|
||||||
|
];
|
||||||
|
|
||||||
|
// to determine the Accept header
|
||||||
|
let produces: string[] = [
|
||||||
|
{{#produces}}
|
||||||
|
'{{{mediaType}}}'{{#hasMore}}, {{/hasMore}}
|
||||||
|
{{/produces}}
|
||||||
|
];
|
||||||
|
|
||||||
|
{{#authMethods}}
|
||||||
|
// authentication ({{name}}) required
|
||||||
|
{{#isApiKey}}
|
||||||
|
{{#isKeyInHeader}}
|
||||||
|
if (this.configuration.apiKey) {
|
||||||
|
headerParams['{{keyParamName}}'] = this.configuration.apiKey;
|
||||||
|
}
|
||||||
|
|
||||||
|
{{/isKeyInHeader}}
|
||||||
|
{{#isKeyInQuery}}
|
||||||
|
if (this.configuration.apiKey) {
|
||||||
|
queryParameters.set('{{keyParamName}}', this.configuration.apiKey);
|
||||||
|
}
|
||||||
|
|
||||||
|
{{/isKeyInQuery}}
|
||||||
|
{{/isApiKey}}
|
||||||
|
{{#isBasic}}
|
||||||
|
// http basic authentication required
|
||||||
|
if (this.configuration.username || this.configuration.password) {
|
||||||
|
headerParams['Authorization'] = 'Basic ' + btoa(this.configuration.username + ':' + this.configuration.password);
|
||||||
|
}
|
||||||
|
|
||||||
|
{{/isBasic}}
|
||||||
|
{{#isOAuth}}
|
||||||
|
// oauth required
|
||||||
|
if (this.configuration.accessToken) {
|
||||||
|
let accessToken = typeof this.configuration.accessToken === 'function'
|
||||||
|
? this.configuration.accessToken()
|
||||||
|
: this.configuration.accessToken;
|
||||||
|
headerParams['Authorization'] = 'Bearer ' + accessToken;
|
||||||
|
}
|
||||||
|
|
||||||
|
{{/isOAuth}}
|
||||||
|
{{/authMethods}}
|
||||||
|
{{#hasFormParams}}
|
||||||
|
if (!reqHasFile) {
|
||||||
|
headerParams['Content-Type'] = 'application/x-www-form-urlencoded';
|
||||||
|
}
|
||||||
|
|
||||||
|
{{/hasFormParams}}
|
||||||
|
|
||||||
|
{{#bodyParam}}
|
||||||
|
headerParams['Content-Type'] = 'application/json';
|
||||||
|
|
||||||
|
{{/bodyParam}}
|
||||||
|
let requestOptions: JQueryAjaxSettings = {
|
||||||
|
url: localVarPath,
|
||||||
|
type: '{{httpMethod}}',
|
||||||
|
headers: headerParams,
|
||||||
|
processData: false
|
||||||
|
};
|
||||||
|
|
||||||
|
{{#bodyParam}}
|
||||||
|
requestOptions.data = JSON.stringify({{paramName}});
|
||||||
|
{{/bodyParam}}
|
||||||
|
if (headerParams['Content-Type']) {
|
||||||
|
requestOptions.contentType = headerParams['Content-Type'];
|
||||||
|
}
|
||||||
|
{{#hasFormParams}}
|
||||||
|
requestOptions.data = formParams;
|
||||||
|
if (reqHasFile) {
|
||||||
|
requestOptions.contentType = false;
|
||||||
|
}
|
||||||
|
{{/hasFormParams}}
|
||||||
|
|
||||||
|
if (extraJQueryAjaxSettings) {
|
||||||
|
requestOptions = (<any>Object).assign(requestOptions, extraJQueryAjaxSettings);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.defaultExtraJQueryAjaxSettings) {
|
||||||
|
requestOptions = (<any>Object).assign(requestOptions, this.defaultExtraJQueryAjaxSettings);
|
||||||
|
}
|
||||||
|
|
||||||
|
let dfd = $.Deferred();
|
||||||
|
$.ajax(requestOptions).then(
|
||||||
|
(data: {{#returnType}}{{{returnType}}}{{/returnType}}{{^returnType}}any{{/returnType}}, textStatus: string, jqXHR: JQueryXHR) =>
|
||||||
|
dfd.resolve(jqXHR, data),
|
||||||
|
(xhr: JQueryXHR, textStatus: string, errorThrown: string) => {
|
||||||
|
if(false){}
|
||||||
|
{{#responses}}
|
||||||
|
else if (xhr.status == {{{code}}} && {{{code}}} >= 400)
|
||||||
|
{
|
||||||
|
dfd.reject(new {{dataType}}Exception(<{{dataType}}>xhr.responseJSON))
|
||||||
|
}
|
||||||
|
|
||||||
|
{{/responses}}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
|
||||||
|
dfd.reject(errorThrown)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
return dfd.promise();
|
||||||
|
}
|
||||||
|
|
||||||
|
{{/operation}}
|
||||||
|
}
|
||||||
|
{{/operations}}
|
9
lib/swagger-typescript-jquery/apis.mustache
Normal file
9
lib/swagger-typescript-jquery/apis.mustache
Normal file
|
@ -0,0 +1,9 @@
|
||||||
|
{{#apiInfo}}
|
||||||
|
{{#apis}}
|
||||||
|
{{#operations}}
|
||||||
|
export * from './{{ classFilename }}';
|
||||||
|
import { {{ classname }} } from './{{ classFilename }}';
|
||||||
|
{{/operations}}
|
||||||
|
{{/apis}}
|
||||||
|
export const APIS = [{{#apis}}{{#operations}}{{ classname }}{{/operations}}{{^-last}}, {{/-last}}{{/apis}}];
|
||||||
|
{{/apiInfo}}
|
6
lib/swagger-typescript-jquery/configuration.mustache
Normal file
6
lib/swagger-typescript-jquery/configuration.mustache
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
export class Configuration {
|
||||||
|
apiKey: string;
|
||||||
|
username: string;
|
||||||
|
password: string;
|
||||||
|
accessToken: string | (() => string);
|
||||||
|
}
|
52
lib/swagger-typescript-jquery/git_push.sh.mustache
Executable file
52
lib/swagger-typescript-jquery/git_push.sh.mustache
Executable file
|
@ -0,0 +1,52 @@
|
||||||
|
#!/bin/sh
|
||||||
|
# ref: https://help.github.com/articles/adding-an-existing-project-to-github-using-the-command-line/
|
||||||
|
#
|
||||||
|
# Usage example: /bin/sh ./git_push.sh wing328 swagger-petstore-perl "minor update"
|
||||||
|
|
||||||
|
git_user_id=$1
|
||||||
|
git_repo_id=$2
|
||||||
|
release_note=$3
|
||||||
|
|
||||||
|
if [ "$git_user_id" = "" ]; then
|
||||||
|
git_user_id="{{{gitUserId}}}"
|
||||||
|
echo "[INFO] No command line input provided. Set \$git_user_id to $git_user_id"
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ "$git_repo_id" = "" ]; then
|
||||||
|
git_repo_id="{{{gitRepoId}}}"
|
||||||
|
echo "[INFO] No command line input provided. Set \$git_repo_id to $git_repo_id"
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ "$release_note" = "" ]; then
|
||||||
|
release_note="{{{releaseNote}}}"
|
||||||
|
echo "[INFO] No command line input provided. Set \$release_note to $release_note"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Initialize the local directory as a Git repository
|
||||||
|
git init
|
||||||
|
|
||||||
|
# Adds the files in the local repository and stages them for commit.
|
||||||
|
git add .
|
||||||
|
|
||||||
|
# Commits the tracked changes and prepares them to be pushed to a remote repository.
|
||||||
|
git commit -m "$release_note"
|
||||||
|
|
||||||
|
# Sets the new remote
|
||||||
|
git_remote=`git remote`
|
||||||
|
if [ "$git_remote" = "" ]; then # git remote not defined
|
||||||
|
|
||||||
|
if [ "$GIT_TOKEN" = "" ]; then
|
||||||
|
echo "[INFO] \$GIT_TOKEN (environment variable) is not set. Using the git credential in your environment."
|
||||||
|
git remote add origin https://github.com/${git_user_id}/${git_repo_id}.git
|
||||||
|
else
|
||||||
|
git remote add origin https://${git_user_id}:${GIT_TOKEN}@github.com/${git_user_id}/${git_repo_id}.git
|
||||||
|
fi
|
||||||
|
|
||||||
|
fi
|
||||||
|
|
||||||
|
git pull origin master
|
||||||
|
|
||||||
|
# Pushes (Forces) the changes in the local repository up to the remote repository
|
||||||
|
echo "Git pushing to https://github.com/${git_user_id}/${git_repo_id}.git"
|
||||||
|
git push origin master 2>&1 | grep -v 'To https'
|
||||||
|
|
4
lib/swagger-typescript-jquery/index.mustache
Normal file
4
lib/swagger-typescript-jquery/index.mustache
Normal file
|
@ -0,0 +1,4 @@
|
||||||
|
export * from './api/api';
|
||||||
|
export * from './model/models';
|
||||||
|
export * from './variables';
|
||||||
|
export * from './configuration';
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue