require_relative "base_generator" module Generators module Avo class EjectGenerator < BaseGenerator class_option :partial, desc: "The partial to eject. Example: ':logo', 'app/views/layouts/avo/application.html.erb'", type: :string, required: false class_option :component, desc: "The component to eject. Example: 'Avo::Index::TableRowComponent', 'avo/index/table_row_component'", type: :string, required: false class_option :controller, desc: "The controller to eject. Example: 'application_controller'", type: :string, required: false class_option :scope, desc: "The scope of the component. Example: 'users', 'admins'", type: :string, required: false class_option "field-components", desc: "The field components to eject. Example: 'trix', 'text'", type: :string, required: false class_option :view, desc: "The view of the component to eject when using --field-component option. Example: 'index', 'show'", type: :string, required: false source_root ::Avo::Engine.root namespace "avo:eject" TEMPLATES = { logo: "app/views/avo/partials/_logo.html.erb", head: "app/views/avo/partials/_head.html.erb", header: "app/views/avo/partials/_header.html.erb", footer: "app/views/avo/partials/_footer.html.erb", pre_head: "app/views/avo/partials/_pre_head.html.erb", scripts: "app/views/avo/partials/_scripts.html.erb", sidebar_extra: "app/views/avo/partials/_sidebar_extra.html.erb", profile_menu_extra: "app/views/avo/partials/_profile_menu_extra.html.erb", } def handle if options[:controller].present? eject_controller elsif options[:partial].present? eject_partial elsif options[:component].present? eject_component elsif options["field-components"].present? eject_field_components else say "Please specify a partial or a component to eject.\n" \ "Examples: rails g avo:eject --partial :logo\n" \ " rails g avo:eject --partial app/views/layouts/avo/application.html.erb\n" \ " rails g avo:eject --component Avo::Index::TableRowComponent\n" \ " rails g avo:eject --component avo/index/table_row_component\n" \ " rails g avo:eject --controller application_controller\n" \ " rails g avo:eject --field-components trix\n" \ " rails g avo:eject --field-components trix --scope users\n" \ " rails g avo:eject --field-components text --scope users --view edit\n" \ " rails g avo:eject --component Avo::Views::ResourceIndexComponent --scope users\n" \ " rails g avo:eject --component avo/views/resource_index_component --scope users", :yellow end end no_tasks do def path_to_sym(filename) template_id = filename.dup template_id[0] = "" template_id.to_sym end def path_exists?(path) path.present? && File.file?(::Avo::Engine.root.join(path)) end def dir_exists?(path) path.present? && Dir.exist?(::Avo::Engine.root.join(path)) end def eject(path, dest_path = nil, is_directory: false) method = is_directory ? :directory : :copy_file send method, ::Avo::Engine.root.join(path), ::Rails.root.join(dest_path || path) end def eject_partial if options[:partial].starts_with?(":") template_id = path_to_sym options[:partial] template_path = TEMPLATES[template_id] if path_exists? template_path return unless confirm_ejection_on template_path eject template_path else say("Failed to find the `#{template_id.to_sym}` template.", :yellow) end elsif path_exists? options[:partial] return unless confirm_ejection_on template_path eject options[:partial] else say("Failed to find the `#{options[:partial]}` template.", :yellow) end end def eject_component(component_to_eject = options[:component], confirmation: true) # Underscore the component name # Example: Avo::Views::ResourceIndexComponent => avo/views/resource_index_component component = component_to_eject.underscore # Get the component path for both, the rb and erb files rb, erb = if (component_constant = component_to_eject.safe_constantize) # If component is a constant, find the source location source_location = component_constant.source_location [source_location, source_location.gsub(".rb", ".html.erb")] else ["app/components/#{component}.rb", "app/components/#{component}.html.erb"] end # Return if one of the components doesn't exist if !path_exists?(rb) || !path_exists?(erb) return say("Failed to find the `#{component_to_eject}` component.", :yellow) end # Add the scope to the component if it's possible if add_scope? component component = if component.starts_with?("avo/views/") component.gsub("avo/views/", "avo/views/#{options[:scope].underscore}/") elsif component.starts_with?("avo/fields/") component.gsub("avo/fields/", "avo/fields/#{options[:scope].underscore}/") end added_scope = true end # Confirm the ejection if confirmation return if !confirm_ejection_on(component.camelize) end # Get the destination path for both, the rb and erb files dest_rb = "#{::Avo.configuration.view_component_path}/#{component}.rb" dest_erb = "#{::Avo.configuration.view_component_path}/#{component}.html.erb" # Eject the component eject rb, dest_rb eject erb, dest_erb # Remame the component class if scope was added # Example: Avo::Views::ResourceIndexComponent => Avo::Views::Admins::ResourceIndexComponent if added_scope [dest_rb, dest_erb].each do |path| if component.starts_with?("avo/views/") modified_content = File.read(path).gsub("Avo::Views::", "Avo::Views::#{options[:scope].camelize}::") elsif component.starts_with?("avo/fields/") && options["field-components"].present? modified_content = File.read(path).gsub("#{options["field-components"].camelize}Field", "#{options[:scope].camelize}::#{options["field-components"].camelize}Field") end File.open(path, "w") do |file| file.puts modified_content end end if component.starts_with?("avo/views/") say "You can now use this component on any resource by configuring the 'self.components' option.\n" \ " self.components = {\n" \ " #{component.split("/").last}: #{component.camelize}\n" \ " }", :green elsif component.starts_with?("avo/fields/") say "You can now use this component on any field by configuring the 'components' option.\n" \ " field :name, as: :#{options["field-components"]}, components: {\n" \ " #{component.split("/").last}: #{component.camelize}\n" \ " }", :green end end end def eject_field_components # Check if the field exists field_path = "lib/avo/fields/#{options["field-components"]}_field.rb" return say("Failed to find the `#{options["field-components"]}` field.", :yellow) if !path_exists?(field_path) # Eject single component if view is specified if options[:view].present? return eject_component "Avo::Fields::#{options["field-components"].camelize}Field::#{options[:view].camelize}Component" end # Check if the field components directory exist components_path = "app/components/avo/fields/#{options["field-components"]}_field" return say("Failed to find the `#{options["field-components"]}` field components.", :yellow) if !dir_exists?(components_path) # Build the destination path for the components directory add the scope destination_components_path = "#{::Avo.configuration.view_component_path}/#{components_path.gsub("app/components/", "")}" if options[:scope].present? destination_components_path = destination_components_path.gsub("avo/fields/", "avo/fields/#{options[:scope].underscore}/") end # Confirm the ejection confirm_ejection_on destination_components_path, is_directory: true # Eject the components directory eject components_path, destination_components_path, is_directory: true # Rename the component classes if scope was added if options[:scope].present? Dir.glob("#{destination_components_path}/*").each do |file| modified_content = File.read(file).gsub("#{options["field-components"].camelize}Field", "#{options[:scope].camelize}::#{options["field-components"].camelize}Field") File.open(file, "w") do |open_file| open_file.puts modified_content end end end end def eject_controller controller_file = "#{options[:controller].chomp("_controller")}_controller" controller_path = ::Avo::Engine.root.join("app/controllers/avo/#{controller_file}.rb") return say("Failed to find the `#{options[:controller]}.rb` file.", :yellow) if !path_exists?(controller_path) eject controller_path, ::Rails.root.join("app/controllers/avo/#{controller_file}.rb") end def confirm_ejection_on(path, is_directory: false) say("By ejecting the '#{path}'#{" directory" if is_directory} \033[1myou'll take on the responsibility for maintain it.", :yellow) yes?("Are you sure you want to eject the '#{path}'#{" directory" if is_directory}? [y/N]", :yellow) end def add_scope?(component) (component.starts_with?("avo/views/") || component.starts_with?("avo/fields/")) && options[:scope].present? end end end end end