Support for grape and onboarding via react

This commit is contained in:
Eric Schultz 2018-05-21 15:03:46 -05:00
parent 9c162d3f0d
commit 4c5b997d65
133 changed files with 14283 additions and 14420 deletions

12
.bootstraprc Normal file
View file

@ -0,0 +1,12 @@
{
"bootstrapVersion": 3,
"styleLoaders": ["style", "css", "sass"],
"extractStyles": true,
"styles": {
"mixins": true,
"grid": true,
"forms": true
},
"scripts": false
}

10
Gemfile
View file

@ -112,7 +112,7 @@ gem 'countries'
group :development do
gem 'traceroute'
gem 'debase'
gem 'ruby-debug-ide', '0.6.0'
gem 'ruby-debug-ide'
end
group :development, :test do
@ -155,3 +155,11 @@ gem 'foreman'
group :production do
gem 'rails_autoscale_agent'
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'

View file

@ -23,6 +23,24 @@ GIT
multi_json (~> 1.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
remote: https://rubygems.org/
specs:
@ -71,7 +89,11 @@ GEM
mail (> 2.2.5)
mime-types
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)
debug_inspector (>= 0.0.1)
bootsnap (1.1.7)
@ -95,6 +117,8 @@ GEM
simplecov
url
coderay (1.1.2)
coercible (1.0.0)
descendants_tracker (~> 0.0.1)
colorize (0.8.1)
concurrent-ruby (1.0.5)
config (1.7.0)
@ -115,7 +139,7 @@ GEM
database_cleaner (1.6.1)
debase (0.2.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)
deep_merge (1.2.1)
delayed_job (4.1.2)
@ -123,7 +147,9 @@ GEM
delayed_job_active_record (4.1.1)
activerecord (>= 3.0, < 5.1)
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)
orm_adapter (~> 0.1)
railties (>= 3.2.6, < 5)
@ -166,6 +192,7 @@ GEM
dry-equalizer (~> 0.2)
dry-logic (~> 0.4, >= 0.4.0)
dry-types (~> 0.12.0)
equalizer (0.0.11)
erubis (2.7.0)
execjs (2.5.2)
factory_bot (4.8.2)
@ -190,6 +217,23 @@ GEM
plissken
geocoder (1.2.11)
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)
concurrent-ruby (~> 1.0)
hashie (3.4.1)
@ -201,10 +245,12 @@ GEM
httparty (0.13.3)
json (~> 1.8)
multi_xml (>= 0.5.2)
i18n (0.8.6)
i18n (0.9.5)
concurrent-ruby (~> 1.0)
i18n-js (3.0.2)
i18n (~> 0.6, >= 0.6.6)
i18n_data (0.8.0)
ice_nine (0.11.2)
inflecto (0.0.2)
journey (1.0.4)
json (1.8.6)
@ -226,9 +272,12 @@ GEM
money (6.10.0)
i18n (>= 0.6.4, < 1.0)
msgpack (1.2.0)
multi_json (1.12.1)
multi_json (1.13.1)
multi_xml (0.5.5)
multipart-post (2.0.0)
mustermann (1.0.2)
mustermann-grape (1.0.0)
mustermann (~> 1.0.0)
nearest_time_zone (0.0.4)
andand
kdtree
@ -258,9 +307,11 @@ GEM
rabl (0.11.6)
activesupport (>= 2.3.14)
rack (1.4.7)
rack-accept (0.4.5)
rack (>= 0.4)
rack-attack (4.2.0)
rack
rack-cache (1.7.0)
rack-cache (1.7.2)
rack (>= 0.4)
rack-ssl (1.3.4)
rack
@ -292,7 +343,7 @@ GEM
rake (>= 0.8.7)
rdoc (~> 3.4)
thor (>= 0.14.6, < 2.0)
rake (12.0.0)
rake (12.3.1)
rdoc (3.12.2)
json (~> 1.4)
require_all (1.3.2)
@ -329,7 +380,7 @@ GEM
rspec-mocks (~> 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)
ruby-prof (0.15.9)
safe_yaml (1.0.4)
@ -359,7 +410,7 @@ GEM
test-unit (3.2.7)
power_assert
thor (0.19.4)
thread_safe (0.3.5)
thread_safe (0.3.6)
tilt (1.4.1)
timecop (0.7.3)
traceroute (0.5.0)
@ -367,7 +418,7 @@ GEM
treetop (1.4.15)
polyglot
polyglot (>= 0.3.1)
tzinfo (0.3.53)
tzinfo (0.3.54)
uglifier (2.7.1)
execjs (>= 0.3.0)
json (>= 1.8.0)
@ -377,7 +428,12 @@ GEM
unicode_utils (1.4.0)
url (0.3.2)
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)
webmock (1.21.0)
addressable (>= 2.3.6)
@ -418,6 +474,13 @@ DEPENDENCIES
foreman
fullcontact
geocoder
grape
grape-entity!
grape-swagger
grape-swagger-entity
grape_devise!
grape_logging
grape_url_validator
hamster
heroku-deflater
httparty
@ -446,7 +509,7 @@ DEPENDENCIES
roadie-rails
rspec
rspec-rails
ruby-debug-ide (= 0.6.0)
ruby-debug-ide
ruby-prof (= 0.15.9)
sass (= 3.2.19)
sass-rails (= 3.2.6)

5
app/api/houdini/api.rb Normal file
View 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
View 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

View 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

View 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

View 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

View 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

View 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

View 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

View 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

View 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'

View 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

View 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.

View file

@ -0,0 +1,6 @@
class OnboardController < ApplicationController
layout 'layouts/apified'
def index
end
end

View file

@ -1,13 +1,13 @@
# License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later
class Users::SessionsController < Devise::SessionsController
def create
def create
respond_to do |format|
format.html { super }
format.json {
warden.authenticate!(:scope => resource_name, :recall => "#{controller_path}#new")
render :status => 200, :json => { :status => "Success" }
}
}
end
end

View file

@ -0,0 +1,2 @@
module OnboardHelper
end

View file

@ -82,7 +82,7 @@ class Nonprofit < ActiveRecord::Base
validates :city, presence: true
validates :state_code, presence: 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
scope :vetted, -> {where(vetted: true)}
@ -140,9 +140,16 @@ class Nonprofit < ActiveRecord::Base
end
def set_slugs
self.slug = Format::Url.convert_to_slug self.name
self.city_slug = Format::Url.convert_to_slug self.city
self.state_code_slug = Format::Url.convert_to_slug self.state_code
unless (self.slug)
self.slug = Format::Url.convert_to_slug self.name
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
end

View 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>

View 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>

View file

@ -1,10 +1,10 @@
/* License: LGPL-3.0-or-later */
[class*="container"] {
margin-left: auto;
margin-right: auto;
}
/*[class*="container"] {*/
/*margin-left: auto;*/
/*margin-right: auto;*/
/*}*/
.container { max-width: 60rem; }
.container--medium { max-width: 50rem; }
.container--narrow { max-width: 40rem; }
/*.container { max-width: 60rem; }*/
/*.container--medium { max-width: 50rem; }*/
/*.container--narrow { max-width: 40rem; }*/

View file

@ -2,7 +2,7 @@
@import 'commons.css'; /* npm */
@import 'colors.css'; /* contains variables */
@import 'shadows.css'; /* contains variables */
@import 'typography.css';
/*@import 'typography.css';*/
@import 'icons.css';
@import 'containers.css';
@import 'buttons.css';

View file

@ -1,31 +1,31 @@
/* License: LGPL-3.0-or-later */
@font-face {
font-family: 'OpenSans';
src: url('/fonts/OpenSans/OpenSans-Regular.ttf') format('truetype');
font-family: 'Open Sans';
src: url('/fonts/Open_Sans/opensans-regular-webfont.ttf') format('truetype');
font-weight: 400;
font-style: normal;
}
@font-face {
font-family: 'OpenSans';
src: url('/fonts/OpenSans/OpenSans-Semibold.ttf') format('truetype');
font-family: 'Open Sans';
src: url('/fonts/Open_Sans/OpenSans-Semibold.ttf') format('truetype');
font-weight: 500;
font-style: normal;
}
@font-face {
font-family: 'OpenSans';
src: url('/fonts/OpenSans/OpenSans-Bold.ttf') format('truetype');
font-family: 'Open Sans';
src: url('/fonts/Open_Sans/opensans-bold-webfont.ttf') format('truetype');
font-weight: 600;
font-style: normal;
}
body {
font-family:
font-family:
OpenSans,
-apple-system,
BlinkMacSystemFont,
Segoe UI,
Helvetica Neue,
Helvetica,
OpenSans,
sans-serif;
line-height: 1.5;
color: var(--black);

View file

@ -16,6 +16,10 @@ module Commitchange
# Custom directories with classes and modules you want to be autoloadable.
# config.autoload_paths += %W(#{config.root}/extras)
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).
# :all can be used as a placeholder for all plugins not explicitly named.
# config.plugins = [ :exception_notification, :ssl_requirement, :all ]

View file

@ -8,13 +8,14 @@ Encoding.default_internal = Encoding::UTF_8
require 'dotenv'
Dotenv.load ".env"
@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}")
if Rails.env == 'test'
Settings.add_source!("./config/settings.test.yml")
else
Settings.add_source!("./config/#{ENV.fetch('ORG_NAME')}.yml")
Settings.add_source!("./config/#{ENV.fetch('ORG_NAME')}.#{Rails.env}.yml")
Settings.add_source!("./config/#{@org_name}.yml")
Settings.add_source!("./config/#{@org_name}.#{Rails.env}.yml")
end

View file

@ -39,4 +39,6 @@ Commitchange::Application.configure do
config.assets.debug = true
config.log_level = :debug
config.action_controller.allow_forgery_protection = false
end

View 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

View file

@ -139,3 +139,31 @@ en:
twitter: "Tweet"
twitter_message: "Join me in supporting"
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"

View file

@ -1,12 +1,15 @@
# License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later
Commitchange::Application.routes.draw do
mount Houdini::API => '/api'
if Rails.env == 'development'
get '/button_debug/embedded' => 'button_debug#embedded'
get '/button_debug/button' => 'button_debug#button'
get '/button_debug/embedded/:id' => 'button_debug#embedded'
get '/button_debug/button/:id' => 'button_debug#button'
end
end
get 'onboard' => 'onboard#index'
resources(:emails, {only: [:create]})
resources(:settings, {only: [:index]})
resources(:pricing, {only: [:index]})

View file

@ -48,7 +48,7 @@ page_editor:
editor: 'quill'
language: 'en'
available_locales: ['en']
available_locales: ['en', 'de']
intntl:
currencies: ["usd"]

View file

@ -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"

View 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

View file

@ -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()
})
})

View 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>;
}
}

View 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>
}
}

View file

@ -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()
})
})

View 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>
}
}

View file

@ -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>
`;

View file

@ -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 />`;

View 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>
}))

View 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>
})

View 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);
}
}

View 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>