breezy 0.11.0 → 0.16.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (43) hide show
  1. checksums.yaml +5 -5
  2. data/lib/breezy.rb +10 -15
  3. data/lib/breezy/helpers.rb +541 -11
  4. data/lib/breezy/redirection.rb +30 -0
  5. data/lib/breezy/xhr_headers.rb +0 -10
  6. data/lib/generators/rails/breezy_generator.rb +13 -6
  7. data/lib/generators/rails/templates/_form.json.props +13 -0
  8. data/lib/generators/rails/templates/controller.rb.tt +0 -3
  9. data/lib/generators/rails/templates/edit.json.props +14 -0
  10. data/lib/generators/rails/templates/{index.js.props → index.json.props} +1 -2
  11. data/lib/generators/rails/templates/new.json.props +15 -0
  12. data/lib/generators/rails/templates/{show.js.props → show.json.props} +0 -2
  13. data/lib/generators/rails/templates/web/edit.html.erb +7 -0
  14. data/lib/generators/rails/templates/web/edit.jsx +21 -28
  15. data/lib/generators/rails/templates/web/index.html.erb +7 -0
  16. data/lib/generators/rails/templates/web/index.jsx +10 -7
  17. data/lib/generators/rails/templates/web/new.html.erb +7 -0
  18. data/lib/generators/rails/templates/web/new.jsx +18 -25
  19. data/lib/generators/rails/templates/web/show.html.erb +7 -0
  20. data/lib/generators/rails/templates/web/show.jsx +3 -4
  21. data/lib/install/templates/web/action_creators.js +14 -12
  22. data/lib/install/templates/web/actions.js +4 -1
  23. data/lib/install/templates/web/application.js +172 -46
  24. data/lib/install/templates/web/application.json.props +26 -0
  25. data/lib/install/templates/web/application_visit.js +65 -0
  26. data/lib/install/templates/web/initializer.rb +1 -6
  27. data/lib/install/templates/web/reducer.js +62 -9
  28. data/lib/install/web.rb +18 -55
  29. data/lib/tasks/install.rake +11 -7
  30. data/test/helpers_test.rb +9 -17
  31. data/test/render_test.rb +25 -92
  32. data/test/test_helper.rb +2 -2
  33. metadata +31 -34
  34. data/app/views/breezy/response.html.erb +0 -0
  35. data/lib/breezy/configuration.rb +0 -35
  36. data/lib/breezy/render.rb +0 -37
  37. data/lib/generators/rails/templates/edit.js.props +0 -16
  38. data/lib/generators/rails/templates/new.js.props +0 -4
  39. data/lib/generators/rails/templates/web/base.jsx +0 -11
  40. data/lib/generators/rails/templates/web/form.jsx +0 -31
  41. data/lib/install/templates/web/babelrc +0 -35
  42. data/test/breezy_test.rb +0 -125
  43. data/test/configuration_test.rb +0 -36
@@ -0,0 +1,26 @@
1
+ path = request.format.json? ? param_to_search_path(params[:bzq]) : nil
2
+
3
+ json.data(search: path) do
4
+ yield json
5
+ end
6
+
7
+ json.component_identifier local_assigns[:virtual_path_of_template]
8
+ json.defers json.deferred!
9
+ json.fragments json.fragments!
10
+ json.assets [
11
+ asset_pack_path('application.js'),
12
+ asset_path('application.css')
13
+ ]
14
+
15
+ if protect_against_forgery?
16
+ json.csrf_token form_authenticity_token
17
+ end
18
+
19
+ if path
20
+ json.action 'graft'
21
+ json.path search_path_to_camelized_param(path)
22
+ end
23
+
24
+ json.rendered_at Time.now.to_i
25
+ json.flash flash.to_h
26
+
@@ -0,0 +1,65 @@
1
+ import { visit, remote } from '@jho406/breezy/dist/action_creators'
2
+
3
+ export function buildVisitAndRemote(ref, store) {
4
+ const appRemote = (...args) => {
5
+ return store.dispatch(remote(...args))
6
+ }
7
+
8
+ const appVisit = (...args) => {
9
+ // Do something before
10
+ // e.g, show loading state, you can access the current pageKey
11
+ // via store.getState().breezy.currentPageKey
12
+ return store
13
+ .dispatch(visit(...args))
14
+ .then((meta) => {
15
+ // The assets fingerprints changed, instead of transitioning
16
+ // just go to the URL directly to retrieve new assets
17
+ if (meta.needsRefresh) {
18
+ window.location = meta.url
19
+ return
20
+ }
21
+
22
+ // There can only be one visit at a time, if `canNavigate` is false,
23
+ // then this request will be saved to the store but should be ignored
24
+ // for a more recent visit.
25
+ if (meta.canNavigate) {
26
+ ref.current.navigateTo(meta.pageKey, {
27
+ action: meta.suggestedAction,
28
+ })
29
+ }
30
+ })
31
+ .finally(() => {
32
+ // Do something after
33
+ // e.g, hide loading state, you can access the changed pageKey
34
+ // via getState().breezy.currentPageKey
35
+ })
36
+ .catch((err) => {
37
+ const response = err.response
38
+
39
+ if (!response) {
40
+ console.error(err)
41
+ return
42
+ }
43
+
44
+ if (response.ok) {
45
+ // err gets thrown, but if the response is ok,
46
+ // it must be an html body that
47
+ // breezy can't parse, just go to the location
48
+ window.location = response.url
49
+ } else {
50
+ if (response.status >= 400 && response.status < 500) {
51
+ window.location = '/400.html'
52
+ return
53
+ }
54
+
55
+ if (response.status >= 500) {
56
+ window.location = '/500.html'
57
+ return
58
+ }
59
+ }
60
+ })
61
+ }
62
+
63
+ return { visit: appVisit, remote: appRemote }
64
+ }
65
+
@@ -1,6 +1 @@
1
- require 'breezy_template/core_ext'
2
-
3
- Breezy.configure do |config|
4
- config.track_sprockets_assets = ['application.js', 'application.css']
5
- config.track_pack_assets = ['application.js']
6
- end
1
+ require 'props_template/core_ext'
@@ -1,17 +1,70 @@
1
+ // Example:
2
+ //
3
+ // import {
4
+ // CLEAR_FORM_ERRORS
5
+ // } from './actions'
6
+ // import produce from "immer"
7
+ //
8
+ // export default function (state = {}, action) {
9
+ // switch(action.type) {
10
+ // case CLEAR_FORM_ERRORS: {
11
+ // const {pageKey} = action.payload
12
+ //
13
+ // return produce(state, draft => {
14
+ // const currentPage = draft[pageKey]
15
+ // delete currentPage.errors
16
+ // })
17
+ // }
18
+ // default:
19
+ // return state
20
+ // }
21
+ // }
22
+
1
23
  import {
2
- CLEAR_FORM_ERRORS
24
+ REHYDRATE,
3
25
  } from './actions'
4
- import produce from "immer"
5
26
 
6
- export default function (state = {}, action) {
27
+ // The applicationPageReducer is for cross page reducers
28
+ // Its common to add to this. You'll typically have to pass a pageKey to the
29
+ // action payload to distinguish the current page
30
+ //
31
+ // The pageKey is passed through the props in your component. Access it like
32
+ // this: `this.props.pageKey` then dispatch it in an action
33
+ export const applicationPagesReducer = (state = {}, action) => {
34
+ switch(action.type) {
35
+ default:
36
+ return state
37
+ }
38
+ }
39
+
40
+ // The applicationRootReducer is for app wide reducers
41
+ // Its rare to be adding to this. Included out of the box ix a reducer for
42
+ // Redux Persist.
43
+ //
44
+ // The REHYDRATE reducer is generated by Breezy and is needed to persist state
45
+ // on any changes made to the initial state that gets injected into
46
+ // window.BREEZY_INITIAL_PAGE_STATE.
47
+ export const applicationRootReducer = (state = {}, action) => {
7
48
  switch(action.type) {
8
- case CLEAR_FORM_ERRORS: {
9
- const {pageKey} = action.payload
49
+ case REHYDRATE: {
50
+ if (action.payload) {
51
+ const {
52
+ pages: hydratedPages
53
+ } = action.payload
54
+ const { pages } = state
55
+ const nextPages = { ...pages, ...hydratedPages }
56
+
57
+ for (const key in pages) {
58
+ if (pages[key] && hydratedPages[key] &&
59
+ pages[key].renderedAt > hydratedPages[key].renderedAt) {
60
+ nextPages[key] = { ...pages[key] }
61
+ }
62
+ }
10
63
 
11
- return produce(state, draft => {
12
- const currentPage = draft[pageKey]
13
- delete currentPage.errors
14
- })
64
+ return { ...state, pages: nextPages }
65
+ } else {
66
+ return state
67
+ }
15
68
  }
16
69
  default:
17
70
  return state
@@ -1,24 +1,6 @@
1
1
  require "webpacker/configuration"
2
2
 
3
- babelrc = Rails.root.join(".babelrc")
4
-
5
- def append_js_tags
6
- app_html = 'app/views/layouts/application.html.erb'
7
- js_tag = <<-JS_TAG
8
-
9
- <script type="text/javascript">
10
- window.BREEZY_INITIAL_PAGE_STATE=<%= breezy_snippet %>;
11
- </script>
12
- JS_TAG
13
-
14
- inject_into_file app_html, after: '<head>' do
15
- js_tag
16
- end
17
-
18
- inject_into_file app_html, after: '<body>' do
19
- "\n <div id='app'></div>"
20
- end
21
- end
3
+ babel_config = Rails.root.join("babel.config.js")
22
4
 
23
5
  def add_member_methods
24
6
  inject_into_file "app/models/application_record.rb", after: "class ApplicationRecord < ActiveRecord::Base\n" do
@@ -28,49 +10,26 @@ def add_member_methods
28
10
  end
29
11
 
30
12
  def self.member_by(attr, value)
31
- find_by(Hash[attr, val])
13
+ find_by(Hash[attr, value])
32
14
  end
33
-
34
15
  RUBY
35
16
  end
36
17
  end
37
18
 
38
-
39
- if File.exist?(babelrc)
40
- react_babelrc = JSON.parse(File.read(babelrc))
41
- react_babelrc["presets"] ||= []
42
- react_babelrc["plugins"] ||= []
43
-
44
- if !react_babelrc["presets"].include?("react")
45
- react_babelrc["presets"].push("react")
46
- say "Copying react preset to your .babelrc file"
47
-
48
- File.open(babelrc, "w") do |f|
49
- f.puts JSON.pretty_generate(react_babelrc)
50
- end
51
- end
52
-
53
- if !react_babelrc["plugins"].any?{|plugin| Array(plugin).include?("module-resolver")}
54
- react_babelrc["plugins"].push(["module-resolver", {
19
+ say "Copying module-resolver preset to your babel.config.js"
20
+ resolver_snippet = <<~JAVASCRIPT
21
+ [
22
+ require('babel-plugin-module-resolver').default, {
55
23
  "root": ["./app"],
56
24
  "alias": {
57
25
  "views": "./app/views",
58
26
  "components": "./app/components",
59
27
  "javascript": "./app/javascript"
60
28
  }
61
- }])
62
-
63
- say "Copying module-resolver preset to your .babelrc file"
64
-
65
- File.open(babelrc, "w") do |f|
66
- f.puts JSON.pretty_generate(react_babelrc)
67
- end
68
- end
69
-
70
- else
71
- say "Copying .babelrc to app root directory"
72
- copy_file "#{__dir__}/templates/web/babelrc", ".babelrc"
73
- end
29
+ }
30
+ ],
31
+ JAVASCRIPT
32
+ insert_into_file "babel.config.js", resolver_snippet, after: /plugins: \[\n/
74
33
 
75
34
  say "Copying application.js file to #{Webpacker.config.source_entry_path}"
76
35
  copy_file "#{__dir__}/templates/web/application.js", "#{Webpacker.config.source_entry_path}/application.js"
@@ -84,19 +43,23 @@ copy_file "#{__dir__}/templates/web/action_creators.js", "#{Webpacker.config.sou
84
43
  say "Copying actions.js file to #{Webpacker.config.source_entry_path}"
85
44
  copy_file "#{__dir__}/templates/web/actions.js", "#{Webpacker.config.source_entry_path}/actions.js"
86
45
 
46
+ say "Copying application_visit.js file to #{Webpacker.config.source_entry_path}"
47
+ copy_file "#{__dir__}/templates/web/application_visit.js", "#{Webpacker.config.source_entry_path}/application_visit.js"
48
+
87
49
  say "Copying Breezy initializer"
88
50
  copy_file "#{__dir__}/templates/web/initializer.rb", "config/initializers/breezy.rb"
89
51
 
90
- say "Appending js tags to your application.html.erb"
91
- append_js_tags
52
+ say "Copying application.json.props"
53
+ copy_file "#{__dir__}/templates/web/application.json.props", "app/views/layouts/application.json.props"
92
54
 
93
55
  say "Adding required member methods to ApplicationRecord"
94
56
  add_member_methods
95
57
 
96
58
  say "Installing React, Redux, and Breezy"
97
- run "yarn add babel-plugin-module-resolver babel-preset-react formik history prop-types react-redux redux-thunk redux reduce-reducers react react-dom immer @jho406/breezy --save"
59
+ run "yarn add babel-plugin-module-resolver babel-preset-react history@\"^4\" html-react-parser@\"^0.13\" prop-types react-redux redux-thunk redux redux-persist reduce-reducers react react-dom immer @jho406/breezy --save"
98
60
 
99
- say "Updating webpack paths to include .jsx file extension"
61
+ say "Updating webpack config to include .jsx file extension and resolved_paths"
100
62
  insert_into_file Webpacker.config.config_path, " - .jsx\n", after: /extensions:\n/
63
+ insert_into_file Webpacker.config.config_path, "'app/views', 'app/components'", after: /resolved_paths: \[/
101
64
 
102
65
  say "Webpacker now supports breezy.js 🎉", :green
@@ -10,6 +10,16 @@ namespace :breezy do
10
10
  end
11
11
  end
12
12
 
13
+ desc "Verifies if any version of react is in package.json"
14
+ task :verify_react do
15
+ package_json = JSON.parse(File.read(Rails.root.join("package.json")))
16
+
17
+ if package_json['dependencies']['react'].nil?
18
+ $stderr.puts "React not installed. Did you run `rails webpacker:install:react`?"
19
+ $stderr.puts "Exiting!" && exit!
20
+ end
21
+ end
22
+
13
23
  desc "Verifies webpacker has been installed"
14
24
  task "verify_webpacker" do
15
25
  begin
@@ -23,16 +33,10 @@ namespace :breezy do
23
33
 
24
34
  namespace :install do
25
35
  desc "Install everything needed for breezy web"
26
- task 'web' => ["breezy:verify_webpacker", "webpacker:verify_install"] do
36
+ task 'web' => ["breezy:verify_webpacker", "webpacker:verify_install", "breezy:verify_react"] do
27
37
  template = File.expand_path("../install/web.rb", __dir__)
28
38
  exec "#{RbConfig.ruby} ./bin/rails app:template LOCATION=#{template}"
29
39
  end
30
-
31
- desc "Install everything needed for breezy mobile"
32
- task 'mobile' => ["breezy:verify_yarn"] do
33
- template = File.expand_path("../install/mobile.rb", __dir__)
34
- exec "#{RbConfig.ruby} ./bin/rails app:template LOCATION=#{template}"
35
- end
36
40
  end
37
41
  end
38
42
 
@@ -2,30 +2,22 @@ require 'test_helper'
2
2
 
3
3
  class HelpersTest < ActiveSupport::TestCase
4
4
  include Breezy::Helpers
5
- attr_reader :request
6
5
 
7
- class Request
8
- attr_reader :params
9
- def initialize(params = {})
10
- @params = params
11
- end
12
- end
13
-
14
- test 'breezy_filter returns a valid bzq param' do
15
- @request = Request.new({:bzq => 'foo.bar.baz_baz'})
6
+ test 'clean_bzq returns nil if qry is nil' do
7
+ qry = nil
16
8
 
17
- assert_equal breezy_filter, 'foo.bar.baz_baz'
9
+ assert_nil param_to_search_path(qry)
18
10
  end
19
11
 
20
- test 'breezy_filter removes invalid bzq param chars' do
21
- @request = Request.new({:bzq => 'foo.bar/?)()-'})
12
+ test 'clean_bzq returns a refined qry' do
13
+ qry = 'foo...bar/?)()-'
22
14
 
23
- assert_equal breezy_filter, 'foo.bar'
15
+ assert_equal param_to_search_path(qry), ['foo', 'bar']
24
16
  end
25
17
 
26
- test 'breezy_filter return nil when no params are present' do
27
- @request = Request.new({})
18
+ test 'camelize_path' do
19
+ path = ['foo_bar', 'foo_bar=1', 'foo_baz_roo']
28
20
 
29
- assert_nil breezy_filter
21
+ assert_equal search_path_to_camelized_param(path), 'fooBar.fooBar=1.fooBazRoo'
30
22
  end
31
23
  end
@@ -4,14 +4,14 @@ class RenderController < TestController
4
4
  require 'action_view/testing/resolvers'
5
5
 
6
6
  append_view_path(ActionView::FixtureResolver.new(
7
- 'render/action.js.breezy' => 'json.author "john smith"',
8
- 'render/action.html.erb' => 'john smith',
9
- 'render/implied_render_with_breezy.js.breezy' => 'json.author "john smith"',
10
- 'render/implied_render_with_breezy.html.erb' => 'john smith',
7
+ 'render/simple_render_with_breezy.json.props' => 'json.author "john smith"',
8
+ 'render/simple_render_with_breezy_with_bad_layout.json.props' => 'json.author "john smith"',
9
+ 'layouts/application.json.props' => 'json.data {yield json}',
10
+ 'layouts/does_not_exist.html.erb' => '',
11
11
  'layouts/application.html.erb' => <<~HTML
12
12
  <html>
13
13
  <head>
14
- <script><%= breezy_snippet %></script>
14
+ <script><%= @initial_state.strip.html_safe %></script>
15
15
  </head>
16
16
  <body><%=yield%></body>
17
17
  </html>
@@ -20,21 +20,18 @@ class RenderController < TestController
20
20
 
21
21
  layout 'application'
22
22
 
23
- before_action :use_breezy, only: [:simple_render_with_breezy, :implied_render_with_breezy]
24
-
25
23
  def render_action
26
24
  render :action
27
25
  end
28
26
 
29
27
  def simple_render_with_breezy
30
- render :action
28
+ @initial_state = render_to_string(formats: [:json], layout: true)
29
+ render inline: '', layout: true
31
30
  end
32
31
 
33
- def implied_render_with_breezy
34
- end
35
-
36
- def render_action_with_breezy_false
37
- render :action
32
+ def simple_render_with_breezy_with_bad_layout
33
+ @initial_state = render_to_string(formats: [:json], layout: 'does_not_exist')
34
+ render inline: '', layout: true
38
35
  end
39
36
 
40
37
  def form_authenticity_token
@@ -47,102 +44,38 @@ class RenderTest < ActionController::TestCase
47
44
 
48
45
 
49
46
  setup do
50
- Breezy.configuration.track_sprockets_assets = ['app.js']
51
- Breezy.configuration.track_pack_assets = ['app.js']
52
- end
53
-
54
- teardown do
55
- Breezy.configuration.track_sprockets_assets = []
56
- Breezy.configuration.track_pack_assets = []
57
- end
58
-
59
- test "render action via get" do
60
- get :render_action
61
- assert_normal_render 'john smith'
47
+ if Rails.version >= '6'
48
+ # In rails 6, the fixture orders the templates based on their appearance in the handler
49
+ # This doesn't happen IRL, so I'm going to explicitly set the handler here.
50
+ #
51
+ # Note that the original is the following
52
+ # @controller.lookup_context.handlers = [:raw, :breezy, :erb, :js, :html, :builder, :ruby]
53
+ @controller.lookup_context.handlers = [:props, :erb]
54
+ end
62
55
  end
63
56
 
64
57
  test "simple render with breezy" do
65
58
  get :simple_render_with_breezy
66
- assert_breezy_html({author: "john smith"}, screen: 'render/action')
67
- end
68
-
69
- test "implied render with breezy" do
70
- get :implied_render_with_breezy
71
- assert_breezy_html({author: "john smith"}, screen: 'render/implied_render_with_breezy')
72
- end
73
-
74
- test "simple render with breezy via get js" do
75
- @request.accept = 'application/javascript'
76
- get :simple_render_with_breezy
77
- assert_breezy_js({author: "john smith"})
78
- end
79
-
80
- test "render action via xhr and get js" do
81
- @request.accept = 'application/javascript'
82
- get :simple_render_with_breezy, xhr: true
83
- assert_breezy_js({author: "john smith"})
84
- end
85
-
86
- test "render with breezy false" do
87
- get :render_action_with_breezy_false
88
- assert_normal_render("john smith")
89
- end
90
-
91
- test "render with breezy false via xhr get" do
92
- @request.accept = 'text/html'
93
- get :render_action_with_breezy_false, xhr: true
94
- assert_normal_render("john smith")
95
- end
96
-
97
- test "render action via xhr and put" do
98
- @request.accept = 'text/html'
99
- put :render_action, xhr: true
100
- assert_normal_render 'john smith'
101
- end
102
59
 
103
- private
104
-
105
- def assert_breezy_html(content, opts={})
106
60
  assert_response 200
107
-
108
61
  rendered = <<~HTML
109
62
  <html>
110
63
  <head>
111
- <script>(function(){var fragments={};var lastFragmentName;var lastFragmentPath;var cache={};var defers=[];return ({"data":#{content.to_json},"screen":"#{opts[:screen]}","fragments":fragments,"privateOpts":{"csrfToken":"secret","assets":["/app.js"],"lastFragmentName":lastFragmentName,"lastFragmentPath":lastFragmentPath,"defers":defers}});})();</script>
64
+ <script>{"data":{"author":"john smith"}}</script>
112
65
  </head>
113
66
  <body></body>
114
67
  </html>
115
68
  HTML
116
69
 
117
70
  assert_equal rendered, @response.body
118
- assert_equal 'text/html', @response.content_type
119
- end
120
-
121
- def assert_breezy_js(content)
122
- assert_response 200
123
- assert_equal '(function(){var fragments={};var lastFragmentName;var lastFragmentPath;var cache={};var defers=[];return ({"data":' + content.to_json + ',"screen":"render/action","fragments":fragments,"privateOpts":{"csrfToken":"secret","assets":["/app.js"],"lastFragmentName":lastFragmentName,"lastFragmentPath":lastFragmentPath,"defers":defers}});})()', @response.body
124
- assert_equal 'text/javascript', @response.content_type
125
- end
126
-
127
- def assert_breezy_replace_js(content)
128
- assert_response 200
129
- assert_equal 'Breezy.replace((function(){return ({"data":' + content.to_json + ',"csrfToken":"secret","assets":["/app.js"]});})());', @response.body
130
- assert_equal 'text/javascript', @response.content_type
71
+ assert_equal 'text/html', @response.media_type
131
72
  end
132
73
 
133
- def assert_normal_render(content)
134
- assert_response 200
135
-
136
- rendered = <<~HTML
137
- <html>
138
- <head>
139
- <script></script>
140
- </head>
141
- <body>#{content}</body>
142
- </html>
143
- HTML
74
+ test "simple render when the layout doesn't exist" do
75
+ err = assert_raise ActionView::MissingTemplate do |e|
76
+ get :simple_render_with_breezy_with_bad_layout
77
+ end
144
78
 
145
- assert_equal rendered, @response.body
146
- assert_equal 'text/html', @response.content_type
79
+ assert_equal(true, err.message.starts_with?('Missing template layouts/does_not_exist with {:locale=>[:en], :formats=>[:json], :variants=>[], :handlers=>[:props, :erb]}.'))
147
80
  end
148
81
  end