Skip to content

Commit

Permalink
Feature/589 setup modal (#598)
Browse files Browse the repository at this point in the history
* implement redirect to modal after edit button has been clicked

* use yarn as manager

* setup stimulus and hotwire

* try to add modal

* modal works

* revert back to npm

* clean up

* clean up

* clean up

* use new crud controller

* Add create functionality

* first working version of create

* start implementing show error

* implement showing error messages when submiting invalid data

* ingore unnecessary test for this feature

* correct rubocop offenses

* use different icon to create skill and correct template error

* style modal

* fix bug of reopening modal when cancel button was clicked

* adjust styling of modal

* only include children in category dropdown and change label of options

* adjust styling of danger alerts

* sort category options by the title of their parent

---------

Co-authored-by: megli2 <[email protected]>
Co-authored-by: Yanick Minder <[email protected]>
  • Loading branch information
3 people authored Feb 16, 2024
1 parent a24e862 commit 0c61fab
Show file tree
Hide file tree
Showing 27 changed files with 773 additions and 170 deletions.
2 changes: 2 additions & 0 deletions Gemfile
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,8 @@ gem 'rest-client'
gem 'seed-fu'
gem 'sentry-raven'
gem 'sprockets-rails'
gem 'stimulus-rails'
gem 'turbo-rails'

group :metrics do
gem 'brakeman'
Expand Down
8 changes: 8 additions & 0 deletions Gemfile.lock
Original file line number Diff line number Diff line change
Expand Up @@ -392,10 +392,16 @@ GEM
activesupport (>= 5.2)
sprockets (>= 3.0.0)
ssrf_filter (1.1.2)
stimulus-rails (1.3.3)
railties (>= 6.0.0)
temple (0.10.3)
thor (1.3.0)
tilt (2.3.0)
timeout (0.4.1)
turbo-rails (1.5.0)
actionpack (>= 6.0.0)
activejob (>= 6.0.0)
railties (>= 6.0.0)
tzinfo (2.0.6)
concurrent-ruby (~> 1.0)
unf (0.1.4)
Expand Down Expand Up @@ -471,6 +477,8 @@ DEPENDENCIES
spring
spring-watcher-listen (~> 2.0.0)
sprockets-rails
stimulus-rails
turbo-rails
tzinfo-data
webdrivers

Expand Down
4 changes: 4 additions & 0 deletions app/assets/images/pencil-square.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
3 changes: 3 additions & 0 deletions app/assets/images/plus-lg.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
6 changes: 5 additions & 1 deletion app/assets/stylesheets/styles.scss
Original file line number Diff line number Diff line change
@@ -1 +1,5 @@
@import "bootstrap";
@import "bootstrap";

.modal-dialog {
max-width: 60%;
}
283 changes: 205 additions & 78 deletions app/controllers/crud_controller.rb
Original file line number Diff line number Diff line change
@@ -1,130 +1,257 @@
# frozen_string_literal: true

# A generic controller to display, create, update and destroy entries of a certain model class.
# Abstract controller providing basic CRUD actions.
#
# Some enhancements were made to ease extensibility.
# The current model entry is available in the view as an instance variable
# named after the +model_class+ or in the helper method +entry+.
# Several protected helper methods are there to be (optionally) overriden by
# subclasses.
# With the help of additional callbacks, it is possible to hook into the
# action procedures without overriding the entire method.
class CrudController < ListController
class_attribute :permitted_attrs, :nested_models, :permitted_relationships

# GET /users/1
def show(_options = {})
@entry = fetch_entry
class_attribute :permitted_attrs

# Defines before and after callback hooks for create, update, save and
# destroy actions.
define_model_callbacks :create, :update, :save, :destroy

# Defines before callbacks for the render actions. A virtual callback
# unifiying render_new and render_edit, called render_form, is defined
# further down.
define_render_callbacks :show, :new, :edit

before_action :entry, only: %i[show new edit update destroy]

helper_method :entry, :full_entry_label

############## ACTIONS ############################################

# GET /entries/1
# GET /entries/1.json
#
# Show one entry of this model.
def show; end

# GET /entries/new
# GET /entries/new.json
#
# Display a form to create a new entry of this model.
def new
assign_attributes if params[model_identifier]
end

# POST /users
def create(options = {})
build_entry
if entry.save
render_entry({ status: :created,
location: entry_url }
.merge(options[:render_options] || {}))
else
render_errors
# GET /entries/1/edit
#
# Display a form to edit an exisiting entry of this model.
def edit; end

# POST /entries
# POST /entries.json
#
# Create a new entry of this model from the passed params.
# There are before and after create callbacks to hook into the action.
#
# To customize the response for certain formats, you may overwrite
# this action and call super with a block that gets the format and
# success parameters. Calling a format action (e.g. format.html)
# in the given block will take precedence over the one defined here.
#
# Specify a :location option if you wish to do a custom redirect.
def create(**options, &block)
model_class.transaction do
assign_attributes
created = with_callbacks(:create, :save) { entry.save }
respond(created,
**options.reverse_merge(status: :created, render_on_failure: :new),
&block)
raise ActiveRecord::Rollback unless created
end
end

# PATCH/PUT /users/1
def update(options = {})
entry.attributes = model_params
if entry.save
render_entry(options[:render_options])
else
render_errors
# PUT /entries/1
# PUT /entries/1.json
#
# Update an existing entry of this model from the passed params.
# There are before and after update callbacks to hook into the action.
#
# To customize the response for certain formats, you may overwrite
# this action and call super with a block that gets the format and
# success parameters. Calling a format action (e.g. format.html)
# in the given block will take precedence over the one defined here.
#
# Specify a :location option if you wish to do a custom redirect.
def update(**options, &block)
model_class.transaction do
assign_attributes
updated = with_callbacks(:update, :save) { entry.save }
respond(updated,
**options.merge(status: :ok, render_on_failure: :edit),
&block)
raise ActiveRecord::Rollback unless updated
end
end

# DELETE /users/1
def destroy(_options = {})
if entry.destroy
head :no_content
else
render_errors
# DELETE /entries/1
# DELETE /entries/1.json
#
# Destroy an existing entry of this model.
# There are before and after destroy callbacks to hook into the action.
#
# To customize the response for certain formats, you may overwrite
# this action and call super with a block that gets format and
# success parameters. Calling a format action (e.g. format.html)
# in the given block will take precedence over the one defined here.
#
# Specify a :location option if you wish to do a custom redirect.
def destroy(**options, &block)
model_class.transaction do
destroyed = run_callbacks(:destroy) { entry.destroy }
respond(destroyed,
**options.merge(status: :no_content),
&block)
raise ActiveRecord::Rollback unless destroyed
end
end

private

############# CUSTOMIZABLE HELPER METHODS ##############################

# Main accessor method for the handled model entry.
def entry
instance_variable_get(:"@#{ivar_name}") ||
instance_variable_set(:"@#{ivar_name}", fetch_entry)
model_ivar_get || model_ivar_set(params[:id] ? find_entry : build_entry)
end

def fetch_entry
model_scope.find(params.fetch(:id))
# Creates a new model entry.
def build_entry
model_scope.new
end

def build_entry
instance_variable_set(:"@#{ivar_name}", model_scope.new(model_params))
# Sets an existing model entry from the given id.
def find_entry
model_scope.find(params[:id])
end

# Assigns the attributes from the params to the model entry.
def assign_attributes
entry.attributes = model_params
end

def render_entry(options = {})
render({ json: entry,
serializer: model_serializer,
root: model_root_key }
.merge(render_options)
.merge(options || {}))
# The form params for this model.
def model_params
params.require(model_identifier).permit(permitted_attrs)
end

def render_errors
render json: entry, status: :unprocessable_entity,
adapter: :json_api, serializer: ActiveModel::Serializer::ErrorSerializer
# Path of the index page to return to.
def index_path
polymorphic_path(path_args(model_class), returning: true)
end

def entry_url
send("#{self.class.name.underscore
.gsub(/_controller$/, '')
.gsub(/\//, '_').singularize}_url", entry)
# Path of the show page.
def show_path
path_args(entry)
end

# Only allow a trusted parameter "white list" through.
def model_params
attrs = params[:data][:attributes].permit(permitted_attrs)
attrs = map_relationships(attrs)
AttributeDeserializer.new(attrs, nested_models: nested_models).run
def respond(success, **options)
respond_to do |format|
yield(format, success) if block_given?
if success
render_on_success(format, **options)
else
render_on_error(format, **options)
end
end
end

def map_relationships(attrs)
relationships = params[:data][:relationships]
def render_on_success(format, **options)
format.html { redirect_on_success(**options) }
format.json { render_success_json(options[:status]) }
end

return attrs if relationships.blank?
def render_on_error(format, **options)
format.turbo_stream { render options[:render_on_failure], status: options[:status] }
format.html { render_or_redirect_on_failure(**options) }
format.json { render_failure_json }
end

relationships.each do |model_name, data|
attrs[relationship_param_name(model_name)] = relationship_ids(data, model_name)
# If the option :render_on_failure is given, render the corresponding
# template, otherwise redirect.
def render_or_redirect_on_failure(**options)
if options[:render_on_failure]
render options[:render_on_failure], status: options[:status]
else
redirect_on_failure(**options)
end
attrs
end

def relationship_param_name(model_name)
permitted_param?("#{model_name}_id") ? "#{model_name}_id" : "#{model_name.singularize}_ids"
# Perform a redirect after a successfull operation and set a flash notice.
def redirect_on_success(**options)
location = options[:location] ||
(entry.destroyed? ? index_path : show_path)
flash[:notice] ||= flash_message(:success)
redirect_to location
end

def relationship_ids(model_data, name)
return unless model_data[:data]
return model_data[:data][:id] unless model_data[:data].is_a?(Array)
# Perform a redirect after a failed operation and set a flash alert.
def redirect_on_failure(**options)
location = options[:location] ||
request.env['HTTP_REFERER'].presence ||
index_path
flash[:alert] ||= error_messages.presence || flash_message(:failure)
redirect_to location
end

if permitted_relationship?(name)
model_data[:data].collect do |e|
e[:id]
end
# Render the show json with the given status or :no_content
def render_success_json(status)
if status == :no_content
head :no_content
else
render :show, status: status, location: show_path
end
end

def permitted_param?(attribute_name)
permitted_attrs.map(&:to_s).include?(attribute_name)
# Render a json with the errors.
def render_failure_json
render json: entry.errors, status: :unprocessable_entity
end

def permitted_relationship?(attribute_name)
permitted_relationships.map(&:to_s).include?(attribute_name)
# Get an I18n flash message.
# Uses the key {controller_name}.{action_name}.flash.{state}
# or crud.{action_name}.flash.{state} as fallback.
def flash_message(state)
scope = "#{action_name}.flash.#{state}"
keys = [:"#{controller_name}.#{scope}_html",
:"#{controller_name}.#{scope}",
:"crud.#{scope}_html",
:"crud.#{scope}"]
I18n.t(keys.shift, model: full_entry_label, default: keys)
end

def ivar_name
model_class.model_name.param_key
# A label for the current entry, including the model name.
def full_entry_label
# rubocop:disable Rails/OutputSafety
"#{models_label(plural: false)} <i>#{ERB::Util.h(entry)}</i>".html_safe
# rubocop:enable Rails/OutputSafety
end

def get_asset_path(filename)
manifest_file = Rails.application.assets_manifest.assets[filename]
if manifest_file
File.join(Rails.application.assets_manifest.directory, manifest_file)
else
Rails.application.assets&.[](filename)&.filename
# Html safe error messages of the current entry.
def error_messages
# rubocop:disable Rails/OutputSafety
escaped = entry.errors.full_messages.map { |m| ERB::Util.html_escape(m) }
escaped.join('<br/>').html_safe
# rubocop:enable Rails/OutputSafety
end

# Class methods for CrudActions.
class << self
# Convenience callback to apply a callback on both form actions
# (new and edit).
def before_render_form(*methods)
before_render_new(*methods)
before_render_edit(*methods)
end
end

end
Loading

0 comments on commit 0c61fab

Please sign in to comment.