pg_rails 7.0.8.pre.alpha.52 → 7.0.8.pre.alpha.54

Sign up to get free protection for your applications and to get access to all the features.
Files changed (40) hide show
  1. checksums.yaml +4 -4
  2. data/pg_engine/app/controllers/admin/users_controller.rb +7 -3
  3. data/pg_engine/app/controllers/concerns/pg_engine/resource.rb +20 -14
  4. data/pg_engine/app/controllers/pg_engine/base_controller.rb +31 -26
  5. data/pg_engine/app/controllers/public/mensaje_contactos_controller.rb +6 -0
  6. data/pg_engine/app/controllers/users/registrations_controller.rb +1 -3
  7. data/pg_engine/app/decorators/pg_engine/base_decorator.rb +8 -3
  8. data/pg_engine/app/helpers/pg_engine/flash_helper.rb +1 -1
  9. data/pg_engine/app/helpers/pg_engine/index_helper.rb +2 -0
  10. data/pg_engine/app/helpers/pg_engine/print_helper.rb +0 -25
  11. data/pg_engine/app/models/pg_engine/base_record.rb +13 -0
  12. data/pg_engine/app/policies/user_policy.rb +15 -3
  13. data/pg_engine/app/policies/user_registration_policy.rb +26 -0
  14. data/pg_engine/app/views/admin/accounts/show.html.slim +1 -1
  15. data/pg_engine/app/views/admin/user_accounts/show.html.slim +1 -1
  16. data/pg_engine/app/views/admin/users/show.html.slim +5 -1
  17. data/pg_engine/app/views/public/mensaje_contactos/new.html.slim +3 -2
  18. data/pg_engine/config/initializers/active_admin.rb +7 -1
  19. data/pg_engine/config/initializers/devise.rb +2 -2
  20. data/pg_engine/config/locales/es.yml +4 -1
  21. data/pg_engine/config/simple_form/simple_form_bootstrap.rb +1 -1
  22. data/pg_engine/spec/controllers/admin/accounts_controller_spec.rb +14 -3
  23. data/pg_engine/spec/controllers/admin/user_accounts_controller_spec.rb +14 -3
  24. data/pg_engine/spec/controllers/admin/users_controller_spec.rb +14 -3
  25. data/pg_engine/spec/controllers/concerns/pg_engine/resource_helper_spec.rb +31 -6
  26. data/pg_engine/spec/controllers/pg_engine/base_controller_spec.rb +41 -2
  27. data/pg_engine/spec/features/destroy_spec.rb +72 -0
  28. data/pg_engine/spec/features/login_spec.rb +41 -0
  29. data/pg_engine/spec/features/signup_spec.rb +78 -0
  30. data/pg_layout/app/javascript/controllers/navbar_controller.js +9 -0
  31. data/pg_layout/app/views/devise/sessions/new.html.erb +0 -1
  32. data/pg_layout/app/views/pg_layout/_flash.html.slim +6 -3
  33. data/pg_layout/app/views/pg_layout/_navbar.html.erb +9 -3
  34. data/pg_layout/app/views/pg_layout/_sidebar.html.erb +4 -1
  35. data/pg_layout/app/views/pg_layout/_sidebar_mobile.html.erb +1 -1
  36. data/pg_layout/app/views/pg_layout/error.html.erb +11 -0
  37. data/pg_rails/lib/version.rb +1 -1
  38. data/pg_scaffold/lib/generators/pg_rspec/scaffold/templates/controller_spec.rb +14 -7
  39. data/pg_scaffold/lib/generators/pg_slim/templates/show.html.slim +1 -1
  40. metadata +7 -2
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 876fb2da0e2e5139b70ba574e13ea948c56033482c92735ddf35f6d3b6c2008f
4
- data.tar.gz: e14f3345126ab9ad158aef8d2869d392e257d323175ae53d620b98a68c7c6d08
3
+ metadata.gz: 8e9b803dcc4fc1155fb31efdeb33f3ab8f7b25f43db8abc04cd851c35f8f9db1
4
+ data.tar.gz: c99433c397a93742e57a71736f602f8f3e3b7c093e82792bcdbb3230f79beb55
5
5
  SHA512:
6
- metadata.gz: 295aae4c9c6c02b32f91d5da8c5089527efd9d64fbad58ed45471e048289ba90b3293cba4a83b7be0c5ad15cf55a0659de83105982734dddafba4f1b04307a31
7
- data.tar.gz: 462cd7d0875ad74e1a9d866da48022ff730d250f306ab7953f87edc9acb2a147f759c677655650d06f1a81aa0ec9e89ca69ea96c9e22025345d435377ef03511
6
+ metadata.gz: 534f5411925466cd4db3a5a0cb908c36f5212faceafd4a523e94f939d036f53b4db4ff5d52abed8675f4edc9eb783e2a17c900760a2f0ad6947ad9c01da7e87e
7
+ data.tar.gz: a116bda2dfc46a994cf7218364444d1488a8d4f4b285f74224766c82893dc545207f88a79dde19794662f27c9b79685ae63cd98974a099680a10b86e20f45a13
@@ -32,11 +32,15 @@ module Admin
32
32
 
33
33
  # :nocov:
34
34
  def login_as
35
- if dev_user_or_env?
36
- usuario = User.find(params[:id])
35
+ return unless dev_user_or_env?
36
+
37
+ usuario = User.find(params[:id])
38
+ if usuario.confirmed_at.present?
37
39
  sign_in(:user, usuario)
40
+ redirect_to after_sign_in_path_for(usuario)
41
+ else
42
+ go_back('No está confirmado')
38
43
  end
39
- redirect_to after_sign_in_path_for(usuario)
40
44
  end
41
45
  # :nocov:
42
46
 
@@ -49,8 +49,7 @@ module PgEngine
49
49
  end
50
50
 
51
51
  def destroy
52
- url = namespaced_path(@clase_modelo)
53
- pg_respond_destroy(instancia_modelo, url)
52
+ pg_respond_destroy(instancia_modelo, params[:redirect_to])
54
53
  end
55
54
  # End public endpoints
56
55
 
@@ -150,15 +149,21 @@ module PgEngine
150
149
 
151
150
  def pg_respond_destroy(model, redirect_url = nil)
152
151
  if destroy_model(model)
152
+ msg = "#{model.model_name.human} #{model.gender == 'f' ? 'borrada' : 'borrado'}"
153
153
  respond_to do |format|
154
- format.html do
155
- if redirect_url.present?
156
- redirect_to redirect_url, notice: 'Elemento borrado.', status: :see_other
157
- else
158
- redirect_back(fallback_location: root_path, notice: 'Elemento borrado.', status: 303)
154
+ if redirect_url.present?
155
+ format.html do
156
+ redirect_to redirect_url, notice: msg, status: :see_other
157
+ end
158
+ else
159
+ format.turbo_stream do
160
+ render turbo_stream: turbo_stream.remove(model)
161
+ end
162
+ format.html do
163
+ redirect_back(fallback_location: root_path, notice: msg, status: 303)
159
164
  end
165
+ format.json { head :no_content }
160
166
  end
161
- format.json { head :no_content }
162
167
  end
163
168
  else
164
169
  respond_to do |format|
@@ -168,11 +173,7 @@ module PgEngine
168
173
  render destroy_error_details_view
169
174
  else
170
175
  flash[:alert] = @error_message
171
- # if redirect_url.present?
172
- # redirect_to redirect_url
173
- # else
174
176
  redirect_back(fallback_location: root_path, status: 303)
175
- # end
176
177
  end
177
178
  end
178
179
  format.json { render json: { error: @error_message }, status: :unprocessable_entity }
@@ -273,7 +274,8 @@ module PgEngine
273
274
 
274
275
  def do_sort(scope, field, direction)
275
276
  # TODO: restringir ciertos campos?
276
- unless scope.model.column_names.include? field.to_s
277
+ unless scope.model.column_names.include?(field.to_s) ||
278
+ scope.model.respond_to?("order_by_#{field}")
277
279
  PgLogger.warn("No existe el campo \"#{field}\"", :warn)
278
280
  return scope
279
281
  end
@@ -281,7 +283,11 @@ module PgEngine
281
283
  PgLogger.warn("Direction not valid: \"#{direction}\"", :warn)
282
284
  return scope
283
285
  end
284
- scope = scope.order(field => direction)
286
+ scope = if scope.model.respond_to? "order_by_#{field}"
287
+ scope.send "order_by_#{field}", direction
288
+ else
289
+ scope.order(field => direction)
290
+ end
285
291
  instance_variable_set(:@field, field)
286
292
  instance_variable_set(:@direction, direction)
287
293
  scope
@@ -22,12 +22,36 @@ module PgEngine
22
22
 
23
23
  protect_from_forgery with: :exception
24
24
 
25
- rescue_from PrintHelper::FechaInvalidaError, with: :fecha_invalida
25
+ rescue_from PgEngine::Error, with: :internal_error
26
26
  rescue_from Pundit::NotAuthorizedError, with: :not_authorized
27
27
  rescue_from Redirect do |e|
28
28
  redirect_to e.url
29
29
  end
30
30
 
31
+ def internal_error(error)
32
+ pg_err error
33
+
34
+ @error_msg = <<~HTML.html_safe # rubocop:disable Rails/OutputSafety
35
+ <div>
36
+ Ocurrió algo inesperado
37
+ <br>
38
+ Por favor, intentá nuevamente
39
+ <br>
40
+ o <a class="text-decoration-underline" href="#{new_public_mensaje_contacto_path}">ponete en contacto</a> y pronto lo resolveremos
41
+ </div>
42
+ HTML
43
+
44
+ respond_to do |format|
45
+ format.html do
46
+ render 'pg_layout/error', layout: 'pg_layout/containerized'
47
+ end
48
+ format.turbo_stream do
49
+ flash.now[:critical] = @error_msg
50
+ render turbo_stream: (turbo_stream.remove_all('.modal') + render_turbo_stream_flash_messages)
51
+ end
52
+ end
53
+ end
54
+
31
55
  before_action do
32
56
  Current.user = current_user
33
57
  end
@@ -47,8 +71,8 @@ module PgEngine
47
71
  layout 'pg_layout/base'
48
72
 
49
73
  # Los flash_types resultantes serían:
50
- # [:alert, :notice, :warning, :success]
51
- add_flash_types :warning, :success
74
+ # [:critical, :alert, :notice, :warning, :success]
75
+ add_flash_types :critical, :warning, :success
52
76
 
53
77
  before_action do
54
78
  console if dev_user_or_env? && (params[:show_web_console] || params[:wc])
@@ -76,39 +100,20 @@ module PgEngine
76
100
 
77
101
  protected
78
102
 
79
- # TODO: ver qué pasa en producción
80
- # def default_url_options(options = {})
81
- # if Rails.env.production?
82
- # options.merge(protocol: 'https')
83
- # else
84
- # options
85
- # end
86
- # end
87
-
88
- # TODO!: ver qué onda esto, tiene sentido acá?
89
- def fecha_invalida
90
- respond_to do |format|
91
- format.json do
92
- render json: { error: 'Formato de fecha inválido' },
93
- status: :unprocessable_entity
94
- end
95
- format.html { go_back('Formato de fecha inválido') }
96
- end
97
- end
98
-
99
103
  def not_authorized
100
104
  respond_to do |format|
101
105
  format.json do
102
- render json: { error: 'Not authorized' },
106
+ render json: { error: 'Acceso no autorizado' },
103
107
  status: :unprocessable_entity
104
108
  end
109
+ # TODO: responder a turbo_stream
105
110
  format.html do
106
111
  if request.path == root_path
107
112
  # TODO!: renderear un 500.html y pg_err
108
113
  sign_out(Current.user) if Current.user.present?
109
- render plain: 'Not authorized'
114
+ render plain: 'Acceso no autorizado'
110
115
  else
111
- go_back('Not authorized')
116
+ go_back('Acceso no autorizado')
112
117
  end
113
118
  end
114
119
  end
@@ -14,7 +14,13 @@ module Public
14
14
 
15
15
  layout 'pg_layout/container_logo'
16
16
 
17
+ def new; end
18
+
17
19
  def create
20
+ if Current.user.present?
21
+ @mensaje_contacto.email = Current.user.email
22
+ @mensaje_contacto.nombre = Current.user.nombre_completo
23
+ end
18
24
  if @mensaje_contacto.save
19
25
  render turbo_stream: turbo_stream.update('mensaje_contacto', partial: 'gracias')
20
26
  else
@@ -1,9 +1,7 @@
1
1
  module Users
2
2
  class RegistrationsController < Devise::RegistrationsController
3
- # POST /resource
4
-
5
3
  before_action do
6
- authorize User
4
+ authorize resource, nil, policy_class: UserRegistrationPolicy
7
5
  end
8
6
 
9
7
  def create
@@ -31,12 +31,17 @@ module PgEngine
31
31
  end
32
32
  # rubocop:enable Style/MissingRespondToMissing
33
33
 
34
- def destroy_link(confirm_text: '¿Estás seguro?', klass: 'btn-light')
34
+ def destroy_link_redirect
35
+ destroy_link(redirect_to: helpers.url_for(target_index))
36
+ end
37
+
38
+ def destroy_link(confirm_text: '¿Estás seguro?', klass: 'btn-light', redirect_to: nil)
35
39
  return unless Pundit.policy!(Current.user, object).destroy?
36
40
 
37
41
  helpers.content_tag :span, rel: :tooltip, title: 'Eliminar' do
38
- helpers.link_to object_url, data: { 'turbo-confirm': confirm_text, 'turbo-method': :delete },
39
- class: "btn btn-sm #{klass}" do
42
+ helpers.link_to object_url + (redirect_to.present? ? "?redirect_to=#{redirect_to}" : ''),
43
+ data: { 'turbo-confirm': confirm_text, 'turbo-method': :delete },
44
+ class: "btn btn-sm #{klass}" do
40
45
  helpers.content_tag :span, nil, class: clase_icono('trash-fill')
41
46
  end
42
47
  end
@@ -14,7 +14,7 @@ module PgEngine
14
14
  case flash_type
15
15
  when 'notice'
16
16
  'info'
17
- when 'alert'
17
+ when 'critical', 'alert'
18
18
  'danger'
19
19
  when 'warning'
20
20
  'warning'
@@ -3,6 +3,8 @@
3
3
  module PgEngine
4
4
  module IndexHelper
5
5
  def encabezado(campo, options = {})
6
+ campo = campo.to_s.sub(/_f\z/, '')
7
+ campo = campo.to_s.sub(/_text\z/, '')
6
8
  clase = options[:clase] || @clase_modelo
7
9
  if options[:ordenable]
8
10
  field = controller.instance_variable_get(:@field)
@@ -4,9 +4,6 @@ module PgEngine
4
4
  module PrintHelper
5
5
  include ActionView::Helpers::NumberHelper
6
6
 
7
- class FechaInvalidaError < PgEngine::Error
8
- end
9
-
10
7
  def mostrar_con_link(objeto, options = {})
11
8
  return if objeto.blank?
12
9
 
@@ -42,42 +39,36 @@ module PgEngine
42
39
  end
43
40
 
44
41
  def dmy_time(date)
45
- date = parsear_tiempo(date) if date.is_a? String
46
42
  return if date.blank?
47
43
 
48
44
  date.strftime('%d/%m/%Y %H:%M')
49
45
  end
50
46
 
51
47
  def dmy(date)
52
- date = parsear_fecha(date) if date.is_a? String
53
48
  return if date.blank?
54
49
 
55
50
  date.strftime('%d/%m/%Y')
56
51
  end
57
52
 
58
53
  def ymd(date)
59
- date = parsear_fecha(date) if date.is_a? String
60
54
  return if date.blank?
61
55
 
62
56
  date.strftime('%Y/%m/%d')
63
57
  end
64
58
 
65
59
  def dmyg(date)
66
- date = parsear_fecha(date) if date.is_a? String
67
60
  return if date.blank?
68
61
 
69
62
  date.strftime('%d-%m-%Y')
70
63
  end
71
64
 
72
65
  def ymdg(date)
73
- date = parsear_fecha(date) if date.is_a? String
74
66
  return if date.blank?
75
67
 
76
68
  date.strftime('%Y-%m-%d')
77
69
  end
78
70
 
79
71
  def myg(date)
80
- date = parsear_fecha(date) if date.is_a? String
81
72
  return if date.blank?
82
73
 
83
74
  date.strftime('%m-%Y')
@@ -142,22 +133,6 @@ module PgEngine
142
133
  end
143
134
  end
144
135
 
145
- def parsear_tiempo(datetime)
146
- return nil if datetime.blank?
147
-
148
- DateTime.parse(datetime)
149
- rescue ArgumentError
150
- raise FechaInvalidaError, datetime
151
- end
152
-
153
- def parsear_fecha(date)
154
- return nil if date.blank?
155
-
156
- Date.parse(date)
157
- rescue ArgumentError
158
- raise FechaInvalidaError, date
159
- end
160
-
161
136
  def show_percentage(value)
162
137
  return if value.blank?
163
138
 
@@ -23,6 +23,10 @@ module PgEngine
23
23
  authorizable_ransackable_attributes
24
24
  end
25
25
 
26
+ def gender
27
+ self.class.model_name.human.downcase.ends_with?('a') ? 'f' : 'm'
28
+ end
29
+
26
30
  def self.nombre_plural
27
31
  model_name.human(count: 2)
28
32
  end
@@ -43,6 +47,15 @@ module PgEngine
43
47
  end
44
48
  end
45
49
 
50
+ # Para el dom_id (index.html)
51
+ def to_key
52
+ if respond_to? :hashid
53
+ [hashid]
54
+ else
55
+ super
56
+ end
57
+ end
58
+
46
59
  def to_s
47
60
  %i[nombre name].each do |campo|
48
61
  return "#{send(campo)} ##{to_param}" if try(campo).present?
@@ -13,7 +13,19 @@ class UserPolicy < ApplicationPolicy
13
13
  # end
14
14
  end
15
15
 
16
- def acceso_total?
17
- true
18
- end
16
+ # def puede_editar?
17
+ # acceso_total? && !record.readonly?
18
+ # end
19
+
20
+ # def puede_crear?
21
+ # acceso_total? || user.asesor?
22
+ # end
23
+
24
+ # def puede_borrar?
25
+ # acceso_total? && !record.readonly?
26
+ # end
27
+
28
+ # def acceso_total?
29
+ # user.developer?
30
+ # end
19
31
  end
@@ -0,0 +1,26 @@
1
+ # frozen_string_literal: true
2
+
3
+ class UserRegistrationPolicy
4
+ attr_reader :user, :record
5
+
6
+ def initialize(user, record)
7
+ @user = user
8
+ @record = record
9
+ end
10
+
11
+ def new?
12
+ create?
13
+ end
14
+
15
+ def create?
16
+ user.blank?
17
+ end
18
+
19
+ def edit?
20
+ update?
21
+ end
22
+
23
+ def update?
24
+ user == record
25
+ end
26
+ end
@@ -1,7 +1,7 @@
1
1
  - content_for :title do
2
2
  = @account.to_s
3
3
  - content_for :actions do
4
- = @account.destroy_link
4
+ = @account.destroy_link_redirect
5
5
  .ms-1
6
6
  = @account.edit_link
7
7
 
@@ -1,7 +1,7 @@
1
1
  - content_for :title do
2
2
  = @user_account.to_s
3
3
  - content_for :actions do
4
- = @user_account.destroy_link
4
+ = @user_account.destroy_link_redirect
5
5
  .ms-1
6
6
  = @user_account.edit_link
7
7
 
@@ -1,9 +1,13 @@
1
1
  - content_for :title do
2
2
  = @user.to_s
3
3
  - content_for :actions do
4
- = @user.destroy_link
4
+ = @user.destroy_link_redirect
5
5
  .ms-1
6
6
  = @user.edit_link
7
+ .ms-1
8
+ = link_to admin_login_as_path(id: @user.id), class: 'btn btn-light btn-sm' do
9
+ span.bi.bi-arrow-right
10
+ | Login as
7
11
 
8
12
  table.table.table-borderless.table-sm.w-auto.mb-0.m-3
9
13
  - atributos_para_mostrar.each do |att|
@@ -8,8 +8,9 @@
8
8
  = f.mensajes_de_error
9
9
 
10
10
  = hidden_field_tag :asociable, true if asociable
11
- = f.input :nombre, input_html: { style: 'max-width: 22em' }
12
- = f.input :email, input_html: { style: 'max-width: 23em' }
11
+ - unless user_signed_in?
12
+ = f.input :nombre, input_html: { style: 'max-width: 22em' }
13
+ = f.input :email, input_html: { style: 'max-width: 23em' }
13
14
  = f.input :telefono, hint: '(Opcional)', input_html: { style: 'max-width: 15em' }
14
15
  = f.input :mensaje, as: :text, input_html: { rows: 5 }
15
16
  .mt-2
@@ -1,3 +1,9 @@
1
+ class MyAdapter < ActiveAdmin::AuthorizationAdapter
2
+ def authorized?(action, subject = nil)
3
+ user.developer?
4
+ end
5
+ end
6
+
1
7
  ActiveAdmin.setup do |config|
2
8
  # == Site Title
3
9
  #
@@ -79,7 +85,7 @@ ActiveAdmin.setup do |config|
79
85
  # method in a before filter of all controller actions to
80
86
  # ensure that there is a user with proper rights. You can use
81
87
  # CanCanAdapter or make your own. Please refer to documentation.
82
- # config.authorization_adapter = ActiveAdmin::CanCanAdapter
88
+ config.authorization_adapter = MyAdapter
83
89
 
84
90
  # In case you prefer Pundit over other solutions you can here pass
85
91
  # the name of default policy class. This policy will be used in every
@@ -164,7 +164,7 @@ Devise.setup do |config|
164
164
 
165
165
  # ==> Configuration for :rememberable
166
166
  # The time the user will be remembered without asking for credentials again.
167
- # config.remember_for = 2.weeks
167
+ config.remember_for = 60.days
168
168
 
169
169
  # Invalidates all the remember me tokens when the user signs out.
170
170
  config.expire_all_remember_me_on_sign_out = true
@@ -188,7 +188,7 @@ Devise.setup do |config|
188
188
  # ==> Configuration for :timeoutable
189
189
  # The time you want to timeout the user session without activity. After this
190
190
  # time the user will be asked for credentials again. Default is 30 minutes.
191
- # config.timeout_in = 30.minutes
191
+ config.timeout_in = 14.days
192
192
 
193
193
  # ==> Configuration for :lockable
194
194
  # Defines which strategy will be used to lock an account.
@@ -77,7 +77,10 @@ es:
77
77
  sign_up: Crear una cuenta
78
78
  forgot_your_password: ¿Olvidaste tu contraseña?
79
79
  didn_t_receive_confirmation_instructions: ¿No recibiste las instrucciones para confirmar tu cuenta?
80
-
80
+ activerecord:
81
+ attributes:
82
+ user:
83
+ remember_me: Mantener sesión abierta
81
84
 
82
85
 
83
86
 
@@ -92,7 +92,7 @@ SimpleForm.setup do |config|
92
92
  config.wrappers :vertical_form, class: 'mb-3', &control_wrapper
93
93
 
94
94
  # vertical input for boolean
95
- config.wrappers :vertical_boolean, tag: 'fieldset', class: 'mb-3' do |b|
95
+ config.wrappers :vertical_boolean, tag: 'fieldset', class: 'mb-3 d-flex justify-content-center' do |b|
96
96
  b.use :html5
97
97
  b.optional :readonly
98
98
  b.wrapper :form_check_wrapper, class: 'form-check' do |bb|
@@ -184,10 +184,12 @@ RSpec.describe Admin::AccountsController do
184
184
 
185
185
  describe 'DELETE #destroy' do
186
186
  subject do
187
- delete :destroy, params: { id: account.to_param }
187
+ request.headers['Accept'] = 'text/vnd.turbo-stream.html,text/html'
188
+ delete :destroy, params: { id: account.to_param, redirect_to: redirect_url }
188
189
  end
189
190
 
190
191
  let!(:account) { create :account }
192
+ let(:redirect_url) { nil }
191
193
 
192
194
  it 'destroys the requested account' do
193
195
  expect { subject }.to change(Account.kept, :count).by(-1)
@@ -198,9 +200,18 @@ RSpec.describe Admin::AccountsController do
198
200
  expect(account.reload.discarded_at).to be_present
199
201
  end
200
202
 
201
- it 'redirects to the accounts list' do
203
+ it 'quita el elemento de la lista' do
202
204
  subject
203
- expect(response).to redirect_to(admin_accounts_url)
205
+ expect(response.body).to include('turbo-stream action="remove"')
206
+ end
207
+
208
+ context 'si hay redirect_to' do
209
+ let(:redirect_url) { admin_accounts_url }
210
+
211
+ it 'redirects to the accounts list' do
212
+ subject
213
+ expect(response).to redirect_to(admin_accounts_url)
214
+ end
204
215
  end
205
216
  end
206
217
  end
@@ -172,18 +172,29 @@ RSpec.describe Admin::UserAccountsController do
172
172
 
173
173
  describe 'DELETE #destroy' do
174
174
  subject do
175
- delete :destroy, params: { id: user_account.to_param }
175
+ request.headers['Accept'] = 'text/vnd.turbo-stream.html,text/html'
176
+ delete :destroy, params: { id: user_account.to_param, redirect_to: redirect_url }
176
177
  end
177
178
 
178
179
  let!(:user_account) { create :user_account }
180
+ let(:redirect_url) { nil }
179
181
 
180
182
  it 'destroys the requested user_account' do
181
183
  expect { subject }.to change(UserAccount, :count).by(-1)
182
184
  end
183
185
 
184
- it 'redirects to the user_accounts list' do
186
+ it 'quita el elemento de la lista' do
185
187
  subject
186
- expect(response).to redirect_to(admin_user_accounts_url)
188
+ expect(response.body).to include('turbo-stream action="remove"')
189
+ end
190
+
191
+ context 'si hay redirect_to' do
192
+ let(:redirect_url) { admin_user_accounts_url }
193
+
194
+ it 'redirects to the user_accounts list' do
195
+ subject
196
+ expect(response).to redirect_to(admin_user_accounts_url)
197
+ end
187
198
  end
188
199
  end
189
200
  end
@@ -158,10 +158,12 @@ RSpec.describe Admin::UsersController do
158
158
 
159
159
  describe 'DELETE #destroy' do
160
160
  subject do
161
- delete :destroy, params: { id: user.to_param }
161
+ request.headers['Accept'] = 'text/vnd.turbo-stream.html,text/html'
162
+ delete :destroy, params: { id: user.to_param, redirect_to: redirect_url }
162
163
  end
163
164
 
164
165
  let!(:user) { create :user }
166
+ let(:redirect_url) { nil }
165
167
 
166
168
  it 'destroys the requested user' do
167
169
  expect { subject }.to change(User.kept, :count).by(-1)
@@ -172,9 +174,18 @@ RSpec.describe Admin::UsersController do
172
174
  expect(user.reload.discarded_at).to be_present
173
175
  end
174
176
 
175
- it 'redirects to the users list' do
177
+ it 'quita el elemento de la lista' do
176
178
  subject
177
- expect(response).to redirect_to(admin_users_url)
179
+ expect(response.body).to include('turbo-stream action="remove"')
180
+ end
181
+
182
+ context 'si hay redirect_to' do
183
+ let(:redirect_url) { admin_users_url }
184
+
185
+ it 'redirects to the users list' do
186
+ subject
187
+ expect(response).to redirect_to(admin_users_url)
188
+ end
178
189
  end
179
190
  end
180
191
  end
@@ -34,9 +34,9 @@ describe PgEngine::Resource do
34
34
  instancia.send(:do_sort, scope, param, direction)
35
35
  end
36
36
 
37
- let!(:categoria_de_cosa_ult) { create :categoria_de_cosa, nombre: 'Z' }
38
- let!(:categoria_de_cosa_pri) { create :categoria_de_cosa, nombre: 'a' }
39
- let(:scope) { CategoriaDeCosa.all }
37
+ let!(:cosa_ult) { create :cosa, nombre: 'Z' }
38
+ let!(:cosa_pri) { create :cosa, nombre: 'a' }
39
+ let(:scope) { Cosa.all }
40
40
  let(:param) { :nombre }
41
41
  let(:direction) { :desc }
42
42
 
@@ -44,7 +44,7 @@ describe PgEngine::Resource do
44
44
  let(:direction) { :asc }
45
45
 
46
46
  it do
47
- expect(subject.to_a).to eq [categoria_de_cosa_pri, categoria_de_cosa_ult]
47
+ expect(subject.to_a).to eq [cosa_pri, cosa_ult]
48
48
  end
49
49
  end
50
50
 
@@ -52,7 +52,7 @@ describe PgEngine::Resource do
52
52
  let(:direction) { :desc }
53
53
 
54
54
  it do
55
- expect(subject.to_a).to eq [categoria_de_cosa_ult, categoria_de_cosa_pri]
55
+ expect(subject.to_a).to eq [cosa_ult, cosa_pri]
56
56
  end
57
57
  end
58
58
 
@@ -60,7 +60,32 @@ describe PgEngine::Resource do
60
60
  let(:param) { :inexistente }
61
61
 
62
62
  it do
63
- expect(subject.to_a).to eq [categoria_de_cosa_ult, categoria_de_cosa_pri]
63
+ expect(subject.to_a).to eq [cosa_ult, cosa_pri]
64
+ end
65
+ end
66
+
67
+ context 'cuando ordeno por categoria' do
68
+ let(:param) { :categoria_de_cosa }
69
+
70
+ before do
71
+ cosa_pri.categoria_de_cosa.update_column(:nombre, 'a') # rubocop:disable Rails/SkipsModelValidations
72
+ cosa_ult.categoria_de_cosa.update_column(:nombre, 'z') # rubocop:disable Rails/SkipsModelValidations
73
+ end
74
+
75
+ context 'si es asc' do
76
+ let(:direction) { :asc }
77
+
78
+ it do
79
+ expect(subject.to_a).to eq [cosa_pri, cosa_ult]
80
+ end
81
+ end
82
+
83
+ context 'si es desc' do
84
+ let(:direction) { :desc }
85
+
86
+ it do
87
+ expect(subject.to_a).to eq [cosa_ult, cosa_pri]
88
+ end
64
89
  end
65
90
  end
66
91
  end
@@ -9,6 +9,10 @@ class DummyBaseController < PgEngine::BaseController
9
9
  raise Pundit::NotAuthorizedError
10
10
  end
11
11
 
12
+ def test_internal_error
13
+ raise PgEngine::Error
14
+ end
15
+
12
16
  def check_dev_user
13
17
  @dev_user_or_env = dev_user_or_env?
14
18
  @dev_user = dev_user?
@@ -20,6 +24,8 @@ end
20
24
  # rubocop:disable RSpec/FilePath
21
25
  # rubocop:disable RSpec/SpecFilePathFormat
22
26
  describe DummyBaseController do
27
+ render_views
28
+
23
29
  describe 'PgEngine::BaseController::Redirect' do
24
30
  before { get :action_with_redirect }
25
31
 
@@ -28,6 +34,39 @@ describe DummyBaseController do
28
34
  end
29
35
  end
30
36
 
37
+ describe 'internal_error' do
38
+ subject do
39
+ get :test_internal_error
40
+ end
41
+
42
+ around do |example|
43
+ PgEngine::PgLogger.raise_errors = false
44
+ example.run
45
+ PgEngine::PgLogger.raise_errors = true
46
+ end
47
+
48
+ it do
49
+ subject
50
+ expect(response).to have_http_status(:ok)
51
+ expect(response.body).to include 'Ocurrió algo inesperado'
52
+ expect(response.content_type).to include 'text/html'
53
+ end
54
+
55
+ context 'cuando acepta turbo stream' do
56
+ before do
57
+ request.headers['Accept'] = 'text/vnd.turbo-stream.html,text/html'
58
+ end
59
+
60
+ it do
61
+ subject
62
+ expect(response).to have_http_status(:ok)
63
+ expect(response.content_type).to include 'text/vnd.turbo-stream.html'
64
+ expect(response.body).to include 'Ocurrió algo inesperado'
65
+ expect(response.body).to include '<turbo-stream action="remove" targets=".modal">'
66
+ end
67
+ end
68
+ end
69
+
31
70
  describe 'not_authorized' do
32
71
  subject do
33
72
  get :test_not_authorized
@@ -42,7 +81,7 @@ describe DummyBaseController do
42
81
  it do
43
82
  subject
44
83
  expect(response).to redirect_to root_path
45
- expect(flash[:alert]).to eq 'Not authorized'
84
+ expect(flash[:alert]).to eq 'Acceso no autorizado'
46
85
  expect(controller).to be_user_signed_in
47
86
  end
48
87
 
@@ -54,7 +93,7 @@ describe DummyBaseController do
54
93
  it do
55
94
  subject
56
95
  expect(response).to have_http_status(:ok)
57
- expect(response.body).to eq 'Not authorized'
96
+ expect(response.body).to eq 'Acceso no autorizado'
58
97
  expect(controller).not_to be_user_signed_in
59
98
  end
60
99
  end
@@ -0,0 +1,72 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'rails_helper'
4
+
5
+ describe 'Sign in', :js do
6
+ include ActionView::RecordIdentifier
7
+
8
+ shared_examples 'destroy from index' do
9
+ subject do
10
+ accept_confirm do
11
+ find("##{dom_id(cosa)} span[title=Eliminar] a").click
12
+ end
13
+ sleep 1
14
+ end
15
+
16
+ let(:user) { create :user, :developer }
17
+ let!(:cosa) { create :cosa }
18
+
19
+ before do
20
+ create_list :cosa, 5
21
+ login_as user
22
+ visit '/frontend/cosas'
23
+ end
24
+
25
+ it do
26
+ expect { subject }.to change { page.find_all('tbody tr').length }.from(6).to(5)
27
+ end
28
+ end
29
+
30
+ shared_examples 'destroy from show' do
31
+ subject do
32
+ accept_confirm do
33
+ find('.btn-toolbar span[title=Eliminar] a').click
34
+ end
35
+ sleep 1
36
+ end
37
+
38
+ let(:user) { create :user, :developer }
39
+ let!(:cosa) { create :cosa }
40
+
41
+ before do
42
+ login_as user
43
+ visit "/frontend/cosas/#{cosa.to_param}"
44
+ end
45
+
46
+ it do # rubocop:disable RSpec/MultipleExpectations
47
+ subject
48
+ expect(page).to have_current_path('/frontend/cosas')
49
+ expect(page).to have_text('Coso borrado')
50
+ end
51
+ end
52
+
53
+ # Capybara.drivers.keys
54
+ drivers = %i[
55
+ selenium_headless
56
+ selenium_chrome_headless
57
+ selenium_chrome_headless_notebook
58
+ selenium_chrome_headless_iphone
59
+ ]
60
+ # drivers = %i[selenium_chrome_headless_notebook]
61
+ # drivers = %i[selenium_chrome_debugger]
62
+ # drivers = %i[selenium]
63
+ # drivers = %i[selenium_chrome]
64
+ drivers = [ENV['DRIVER'].to_sym] if ENV['DRIVER'].present?
65
+
66
+ drivers.each do |driver|
67
+ context("with driver '#{driver}'", driver:) do
68
+ it_behaves_like 'destroy from index'
69
+ it_behaves_like 'destroy from show'
70
+ end
71
+ end
72
+ end
@@ -0,0 +1,41 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'rails_helper'
4
+
5
+ describe 'Sign in', :js do
6
+ shared_examples 'sign_in' do
7
+ subject do
8
+ visit '/users/sign_in'
9
+ fill_in 'user_email', with: user.email
10
+ fill_in 'user_password', with: password
11
+ find('input[type=submit]').click
12
+ end
13
+
14
+ let(:password) { 'pass1234' }
15
+ let!(:user) { create :user, password: }
16
+
17
+ it do
18
+ subject
19
+ expect(page).to have_text :all, 'No hay categorías de cosas aún'
20
+ end
21
+ end
22
+
23
+ # Capybara.drivers.keys
24
+ drivers = %i[
25
+ selenium_headless
26
+ selenium_chrome_headless
27
+ selenium_chrome_headless_notebook
28
+ selenium_chrome_headless_iphone
29
+ ]
30
+ # drivers = %i[selenium_chrome_headless_notebook]
31
+ # drivers = %i[selenium_chrome_debugger]
32
+ # drivers = %i[selenium]
33
+ # drivers = %i[selenium_chrome]
34
+ drivers = [ENV['DRIVER'].to_sym] if ENV['DRIVER'].present?
35
+
36
+ drivers.each do |driver|
37
+ context("with driver '#{driver}'", driver:) do
38
+ include_examples 'sign_in'
39
+ end
40
+ end
41
+ end
@@ -0,0 +1,78 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'rails_helper'
4
+
5
+ describe 'Al Registrarse', :js do
6
+ include ActiveJob::TestHelper
7
+
8
+ find_scroll = proc do |selector, options = {}|
9
+ elem = find(selector, **options.merge(visible: :all))
10
+ script = 'arguments[0].scrollIntoView({ behavior: "instant", block: "start", inline: "nearest" });'
11
+ page.execute_script(script, elem)
12
+ sleep 0.5
13
+ elem
14
+ end
15
+
16
+ shared_examples 'sign_up' do
17
+ subject do
18
+ perform_enqueued_jobs do
19
+ visit '/users/sign_up'
20
+ fill_in 'user_email', with: Faker::Internet.email
21
+ fill_in 'user_nombre', with: Faker::Name.name
22
+ fill_in 'user_apellido', with: Faker::Name.name
23
+ fill_in 'user_password', with: 'admin123'
24
+ fill_in 'user_password_confirmation', with: 'admin123'
25
+ instance_exec('input[type=submit]', &find_scroll).click
26
+ expect(page).to have_text('Se ha enviado un mensaje con un enlace')
27
+ end
28
+ end
29
+
30
+ it 'guarda el user' do
31
+ expect { subject }.to change(User, :count).by(1)
32
+ end
33
+ end
34
+
35
+ shared_examples 'edit user' do
36
+ subject do
37
+ fill_in 'user_nombre', with: 'despues'
38
+ fill_in 'user_current_password', with: password
39
+ instance_exec('input[type=submit]', &find_scroll).click
40
+ # find('').click
41
+ sleep 1
42
+ end
43
+
44
+ let(:password) { 'pass1234' }
45
+ let(:nombre) { 'antes' }
46
+ let!(:user) { create :user, password:, nombre: }
47
+
48
+ before do
49
+ login_as user
50
+ visit '/users/edit'
51
+ end
52
+
53
+ it do # rubocop:disable RSpec/MultipleExpectations
54
+ expect { subject }.to change { user.reload.nombre }.to('despues')
55
+ expect(page).to have_text('Tu cuenta se ha actualizado')
56
+ end
57
+ end
58
+
59
+ # Capybara.drivers.keys
60
+ drivers = %i[
61
+ selenium_headless
62
+ selenium_chrome_headless
63
+ selenium_chrome_headless_notebook
64
+ selenium_chrome_headless_iphone
65
+ ]
66
+ # drivers = %i[selenium_chrome_headless_notebook]
67
+ # drivers = %i[selenium_chrome_debugger]
68
+ # drivers = %i[selenium]
69
+ # drivers = %i[selenium_chrome]
70
+ drivers = [ENV['DRIVER'].to_sym] if ENV['DRIVER'].present?
71
+
72
+ drivers.each do |driver|
73
+ context("with driver '#{driver}'", driver:) do
74
+ it_behaves_like 'sign_up'
75
+ it_behaves_like 'edit user'
76
+ end
77
+ end
78
+ end
@@ -1,15 +1,24 @@
1
1
  import { Controller } from '@hotwired/stimulus'
2
2
  import Cookies from './../utils/cookies'
3
+ import { fadeOut, fadeIn } from './../utils/utils'
3
4
 
4
5
  export default class extends Controller {
6
+ connect () {
7
+ if (document.getElementById('sidebar').classList.contains('opened')) {
8
+ document.querySelector('.navbar .navbar-brand').style.visibility = 'hidden'
9
+ }
10
+ }
11
+
5
12
  expandNavbar (e) {
6
13
  const icon = this.element.querySelector('i')
7
14
  if (document.getElementById('sidebar').classList.toggle('opened')) {
8
15
  icon.classList.add('bi-chevron-left')
9
16
  icon.classList.remove('bi-chevron-right')
17
+ fadeOut(document.querySelector('.navbar .navbar-brand'))
10
18
  } else {
11
19
  icon.classList.remove('bi-chevron-left')
12
20
  icon.classList.add('bi-chevron-right')
21
+ fadeIn(document.querySelector('.navbar .navbar-brand'))
13
22
  }
14
23
  const isOpened = document.getElementById('sidebar').classList.contains('opened')
15
24
  new Cookies().setCookie('navbar_expand', isOpened, 30)
@@ -10,7 +10,6 @@
10
10
  <%= f.input :password,
11
11
  required: false,
12
12
  input_html: { autocomplete: "current-password" } %>
13
- <%# TODO!: corregir style de checkbox %>
14
13
  <%#= f.input :remember_me, as: :boolean if devise_mapping.rememberable? %>
15
14
  </div>
16
15
 
@@ -7,14 +7,17 @@
7
7
  data-turbo-temporary="true"
8
8
  aria-live="assertive" aria-atomic="true" role="alert"
9
9
  ]
10
- - case flash_type_to_class(flash_type)
11
- - when 'danger'
10
+ - case flash_type
11
+ - when 'critical'
12
+ / .bi.bi-emoji-dizzy.me-3.fs-2
13
+ .bi.bi-exclamation-triangle-fill.me-3.fs-2
14
+ - when 'alert'
12
15
  .bi.bi-exclamation-triangle-fill.me-2
13
16
  - when 'warning'
14
17
  .bi.bi-exclamation-circle.me-2
15
18
  - when 'success'
16
19
  .bi.bi-check-lg.me-2
17
- - when 'info'
20
+ - when 'notice'
18
21
  .bi.bi-info-circle.me-2
19
22
  = message
20
23
  button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"
@@ -5,13 +5,19 @@
5
5
  <i class="bi <%= @navbar_chevron_class %>"></i>
6
6
  </button>
7
7
 
8
- <button class="btn btn-outline-light d-inline-block d-<%= @breakpoint_navbar_expand %>-none" type="button" data-bs-toggle="offcanvas" data-bs-target="#offcanvasExample" aria-controls="offcanvasExample">
9
- <i class="bi bi-list"></i>
10
- </button>
11
8
  <% end %>
12
9
  <% @navbar.extensiones.each do |extension| %>
13
10
  <%= extension %>
14
11
  <% end %>
15
12
  <%= @navbar.logo if @navbar.logo.present? %>
13
+ <button class="btn btn-outline-light d-inline-block d-<%= @breakpoint_navbar_expand %>-none" type="button" data-bs-toggle="offcanvas" data-bs-target="#offcanvasExample" aria-controls="offcanvasExample">
14
+ <i class="bi bi-list"></i>
15
+ </button>
16
16
  </div>
17
17
  </nav>
18
+
19
+ <style type="text/css" media="(max-width: 767px)">
20
+ .navbar .navbar-brand {
21
+ visibility:visible!important;
22
+ }
23
+ </style>
@@ -1,5 +1,8 @@
1
1
  <div id="sidebar" class="<%= @navbar_opened_class %> flex-shrink-0 d-none d-<%= @breakpoint_navbar_expand %>-block">
2
- <div class="mt-4">
2
+ <div class="mt-1">
3
+ <div class="m-3">
4
+ <%= @navbar.logo if @navbar.logo.present? %>
5
+ </div>
3
6
  <% if user_signed_in? %>
4
7
  <span class="ms-3 text-light"><%= Current.user %></span>
5
8
  <hr>
@@ -1,4 +1,4 @@
1
- <div class="offcanvas offcanvas-start" tabindex="-1" id="offcanvasExample" aria-labelledby="offcanvasExampleLabel">
1
+ <div class="offcanvas offcanvas-end" tabindex="-1" id="offcanvasExample" aria-labelledby="offcanvasExampleLabel">
2
2
  <div class="offcanvas-header" data-bs-theme="dark">
3
3
  <h5 class="offcanvas-title" id="offcanvasExampleLabel"></h5>
4
4
  <button type="button" class="btn-close" data-bs-dismiss="offcanvas" aria-label="Close"></button>
@@ -0,0 +1,11 @@
1
+ <div class="d-flex justify-content-around">
2
+ <div class="alert alert-danger d-flex align-items-center">
3
+ <div>
4
+ <span class="bi bi-emoji-dizzy fs-1 me-3"></span>
5
+ <%# <span class="bi bi-exclamation-triangle fs-1 me-3"></span> %>
6
+ </div>
7
+ <div>
8
+ <%= @error_msg %>
9
+ </div>
10
+ </div>
11
+ </div>
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module PgRails
4
- VERSION = '7.0.8-alpha.52'
4
+ VERSION = '7.0.8-alpha.54'
5
5
  end
@@ -241,14 +241,12 @@ RSpec.describe <%= controller_class_name %>Controller do
241
241
 
242
242
  describe 'DELETE #destroy' do
243
243
  subject do
244
- <% if Rails::VERSION::STRING < '5.0' -%>
245
- delete :destroy, { id: <%= file_name %>.to_param }
246
- <% else -%>
247
- delete :destroy, params: { id: <%= file_name %>.to_param }
248
- <% end -%>
244
+ request.headers['Accept'] = "text/vnd.turbo-stream.html,text/html"
245
+ delete :destroy, params: { id: <%= file_name %>.to_param, redirect_to: redirect_url }
249
246
  end
250
247
 
251
248
  let!(:<%= nombre_tabla_completo_singular %>) { create :<%= nombre_tabla_completo_singular %> }
249
+ let(:redirect_url) { nil }
252
250
 
253
251
  it 'destroys the requested <%= nombre_tabla_completo_singular %>' do
254
252
  <% if options[:discard] -%>
@@ -267,9 +265,18 @@ RSpec.describe <%= controller_class_name %>Controller do
267
265
  end
268
266
 
269
267
  <% end -%>
270
- it 'redirects to the <%= table_name %> list' do
268
+ it 'quita el elemento de la lista' do
271
269
  subject
272
- expect(response).to redirect_to(<%= index_helper %>_url)
270
+ expect(response.body).to include('turbo-stream action="remove"')
271
+ end
272
+
273
+ context 'si hay redirect_to' do
274
+ let(:redirect_url) { <%= index_helper %>_url }
275
+
276
+ it 'redirects to the <%= table_name %> list' do
277
+ subject
278
+ expect(response).to redirect_to(<%= index_helper %>_url)
279
+ end
273
280
  end
274
281
  end
275
282
  end
@@ -1,7 +1,7 @@
1
1
  - content_for :title do
2
2
  = @<%= singular_name %>.to_s
3
3
  - content_for :actions do
4
- = @<%= singular_name %>.destroy_link
4
+ = @<%= singular_name %>.destroy_link_redirect
5
5
  .ms-1
6
6
  = @<%= singular_name %>.edit_link
7
7
 
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: pg_rails
3
3
  version: !ruby/object:Gem::Version
4
- version: 7.0.8.pre.alpha.52
4
+ version: 7.0.8.pre.alpha.54
5
5
  platform: ruby
6
6
  authors:
7
7
  - Martín Rosso
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2024-04-28 00:00:00.000000000 Z
11
+ date: 2024-05-04 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: rails
@@ -890,6 +890,7 @@ files:
890
890
  - pg_engine/app/policies/pg_engine/application_policy.rb
891
891
  - pg_engine/app/policies/user_account_policy.rb
892
892
  - pg_engine/app/policies/user_policy.rb
893
+ - pg_engine/app/policies/user_registration_policy.rb
893
894
  - pg_engine/app/views/admin/accounts/_account.html.slim
894
895
  - pg_engine/app/views/admin/accounts/_form.html.slim
895
896
  - pg_engine/app/views/admin/accounts/edit.html.slim
@@ -956,6 +957,9 @@ files:
956
957
  - pg_engine/spec/factories/mensaje_contactos.rb
957
958
  - pg_engine/spec/factories/user_accounts.rb
958
959
  - pg_engine/spec/factories/users.rb
960
+ - pg_engine/spec/features/destroy_spec.rb
961
+ - pg_engine/spec/features/login_spec.rb
962
+ - pg_engine/spec/features/signup_spec.rb
959
963
  - pg_engine/spec/fixtures/test.pdf
960
964
  - pg_engine/spec/helpers/pg_engine/pg_rails_helper_spec.rb
961
965
  - pg_engine/spec/lib/pg_engine/error_helper_spec.rb
@@ -1013,6 +1017,7 @@ files:
1013
1017
  - pg_layout/app/views/pg_layout/_navbar.html.erb
1014
1018
  - pg_layout/app/views/pg_layout/_sidebar.html.erb
1015
1019
  - pg_layout/app/views/pg_layout/_sidebar_mobile.html.erb
1020
+ - pg_layout/app/views/pg_layout/error.html.erb
1016
1021
  - pg_layout/lib/pg_layout.rb
1017
1022
  - pg_layout/lib/pg_layout/engine.rb
1018
1023
  - pg_layout/spec/lib/navbar_spec.rb