Rails app template

Bullet Train

Add Avo 3 to your Bullet Trains app blazing fast!

I was hand-rolling an admin dashboard for this community database of third-party RPG content. It was painful for them to use but this morning I just dropped Avo in and suddenly it's a dream!

Means I can focus on the user-facing stuff without worrying about neglecting the data entry people 🤣

John Bannister
John Bannister
Rails developer, Exlibris RPG
Run the following command to quickly apply this app template

Do you think this template needs changes? Submit a PR here.

Full template

# Template info:
#   Link: https://v3.avohq.io/templates/bullet-train
#   Repository: https://github.com/avo-hq/bullet-train-app-template

# List of all Avo files with path and contents
files = {"app/avo/cards/users_count.rb"=>"class Avo::Cards::UsersCount < Avo::Cards::MetricCard\n  self.id = \"users_count\"\n  self.label = \"Users count\"\n  self.description = \"Count of registered users\"\n\n  def query\n    result ::User.all.count\n  end\nend\n", "app/avo/dashboards/overview.rb"=>"class Avo::Dashboards::Overview < Avo::Dashboards::BaseDashboard\n  self.id = \"overview\"\n  self.name = \"Overview\"\n  self.grid_cols = 4\n\n  def cards\n    card Avo::Cards::UsersCount\n  end\nend\n", "app/avo/resources/invitation.rb"=>"class Avo::Resources::Invitation < Avo::BaseResource\n  self.title = :email\n  self.includes = []\n  self.search = {\n    query: -> { query.ransack(id_eq: params[:q], email_cont: params[:q], m: \"or\").result(distinct: false) },\n    item: -> {\n      {\n        title: record.title\n      }\n    }\n  }\n\n  def fields\n    field :id, as: :id\n    field :email, as: :text\n    field :uuid, as: :text\n    field :team, as: :belongs_to, searchable: true\n    field :from_membership, as: :belongs_to, searchable: true\n    field :membership, as: :has_one\n  end\nend\n", "app/avo/resources/membership.rb"=>"class Avo::Resources::Membership < Avo::BaseResource\n  self.title = :name\n  self.includes = []\n  self.search = {\n    query: -> { query.ransack(id_eq: params[:q], name_cont: params[:q], user_email_cont: params[:q], role_cont: params[:q], m: \"or\").result(distinct: false) },\n    item: -> {\n      {\n        title: record.name,\n        description: \"Membership of \#{team.name}\"\n      }\n    }\n  }\n\n  def fields\n    field :id, as: :id\n    field :name, as: :text, hide_on: :forms\n    field :user_profile_photo_id, as: :text\n    field :user_email, as: :text\n    field :added_by_id, as: :number\n    field :role_ids, as: :tags\n    field :user, as: :belongs_to, searchable: true\n    field :team, as: :belongs_to, searchable: true\n    field :invitation, as: :belongs_to, searchable: true\n    field :added_by, as: :belongs_to, searchable: true\n  end\nend\n", "app/avo/resources/team.rb"=>"class Avo::Resources::Team < Avo::BaseResource\n  self.title = :name\n  self.includes = []\n  self.search = {\n    query: -> { query.ransack(id_eq: params[:q], name_cont: params[:q], slug_cont: params[:q], m: \"or\").result(distinct: false) },\n    item: -> {\n      {\n        title: record.name\n      }\n    }\n  }\n\n  def fields\n    field :id, as: :id\n    field :name, as: :text\n    field :slug, as: :text\n    field :being_destroyed, as: :boolean, hide_on: :forms\n    field :time_zone, as: :select, options: -> { view_context.time_zone_options_for_select(Avo::Current.user.time_zone, nil, ActiveSupport::TimeZone) }\n    field :locale, as: :text\n    field :users, as: :has_many, through: :memberships\n    field :memberships, as: :has_many\n    field :invitations, as: :has_many\n  end\nend\n", "app/avo/resources/user.rb"=>"class Avo::Resources::User < Avo::BaseResource\n  self.title = :full_name\n  self.includes = []\n  self.search = {\n    query: -> { query.ransack(id_eq: params[:q], first_name_cont: params[:q], last_name_cont: params[:q], email_cont: params[:q], m: \"or\").result(distinct: false) },\n    item: -> {\n      {\n        title: record.full_name,\n      }\n    }\n  }\n\n  def fields\n    field :id, as: :id\n    field :email, as: :text\n    field :first_name, as: :text\n    field :last_name, as: :text\n    field :time_zone, as: :text\n    field :current_team, as: :belongs_to\n\n    field :teams, as: :has_many, through: :teams\n    field :memberships, as: :has_many\n  end\nend\n", "app/controllers/avo/invitations_controller.rb"=>"# This controller has been generated to enable Rails' resource routes.\n# More information on https://docs.avohq.io/3.0/controllers.html\nclass Avo::InvitationsController < Avo::ResourcesController\nend\n", "app/controllers/avo/memberships_controller.rb"=>"# This controller has been generated to enable Rails' resource routes.\n# More information on https://docs.avohq.io/3.0/controllers.html\nclass Avo::MembershipsController < Avo::ResourcesController\nend\n", "app/controllers/avo/teams_controller.rb"=>"# This controller has been generated to enable Rails' resource routes.\n# More information on https://docs.avohq.io/3.0/controllers.html\nclass Avo::TeamsController < Avo::ResourcesController\nend\n", "app/controllers/avo/users_controller.rb"=>"# This controller has been generated to enable Rails' resource routes.\n# More information on https://docs.avohq.io/3.0/controllers.html\nclass Avo::UsersController < Avo::ResourcesController\nend\n", "config/initializers/avo.rb"=>"# For more information regarding these settings check out our docs https://docs.avohq.io\nAvo.configure do |config|\n  ## == Routing ==\n  config.root_path = \"/admin/avo\"\n  # used only when you have custom `map` configuration in your config.ru\n  # config.prefix_path = \"/internal\"\n\n  # Where should the user be redirected when visting the `/avo` url\n  # config.home_path = nil\n\n  ## == Licensing ==\n  # config.license_key = ENV['AVO_LICENSE_KEY']\n\n  ## == Set the context ==\n  config.set_context do\n    # Return a context object that gets evaluated in Avo::ApplicationController\n  end\n\n  ## == Authentication ==\n  config.current_user_method = :current_user\n  # config.authenticate_with do\n  # end\n\n  ## == Authorization ==\n  # config.authorization_methods = {\n  #   index: 'index?',\n  #   show: 'show?',\n  #   edit: 'edit?',\n  #   new: 'new?',\n  #   update: 'update?',\n  #   create: 'create?',\n  #   destroy: 'destroy?',\n  #   search: 'search?',\n  # }\n  # config.raise_error_on_missing_policy = false\n  # config.authorization_client = :pundit\n\n  ## == Localization ==\n  # config.locale = 'en-US'\n\n  ## == Resource options ==\n  # config.resource_controls_placement = :right\n  # config.model_resource_mapping = {}\n  # config.default_view_type = :table\n  # config.per_page = 24\n  # config.per_page_steps = [12, 24, 48, 72]\n  # config.via_per_page = 8\n  config.id_links_to_resource = true\n  # config.cache_resources_on_index_view = true\n  ## permanent enable or disable cache_resource_filters, default value is false\n  # config.cache_resource_filters = false\n  ## provide a lambda to enable or disable cache_resource_filters per user/resource.\n  # config.cache_resource_filters = ->(current_user:, resource:) { current_user.cache_resource_filters?}\n\n  ## == Customization ==\n  config.app_name = -> { I18n.t \"application.name\" }\n  # config.timezone = 'UTC'\n  # config.currency = 'USD'\n  # config.hide_layout_when_printing = false\n  # config.full_width_container = false\n  # config.full_width_index_view = false\n  # config.search_debounce = 300\n  # config.view_component_path = \"app/components\"\n  # config.display_license_request_timeout_error = true\n  # config.disabled_features = []\n  # config.buttons_on_form_footers = true\n  # config.field_wrapper_layout = true\n\n  ## == Branding ==\n  # config.branding = {\n  #   colors: {\n  #     background: \"248 246 242\",\n  #     100 => \"#CEE7F8\",\n  #     400 => \"#399EE5\",\n  #     500 => \"#0886DE\",\n  #     600 => \"#066BB2\",\n  #   },\n  #   chart_colors: [\"#0B8AE2\", \"#34C683\", \"#2AB1EE\", \"#34C6A8\"],\n  #   logo: \"/avo-assets/logo.png\",\n  #   logomark: \"/avo-assets/logomark.png\",\n  #   placeholder: \"/avo-assets/placeholder.svg\",\n  #   favicon: \"/avo-assets/favicon.ico\"\n  # }\n\n  ## == Breadcrumbs ==\n  # config.display_breadcrumbs = true\n  # config.set_initial_breadcrumbs do\n  #   add_breadcrumb \"Home\", '/avo'\n  # end\n\n  ## == Menus ==\n  # config.main_menu = -> {\n  #   section \"Dashboards\", icon: \"dashboards\" do\n  #     all_dashboards\n  #   end\n\n  #   section \"Resources\", icon: \"resources\" do\n  #     all_resources\n  #   end\n\n  #   section \"Tools\", icon: \"tools\" do\n  #     all_tools\n  #   end\n  # }\n  # config.profile_menu = -> {\n  #   link \"Profile\", path: \"/avo/profile\", icon: \"user-circle\"\n  # }\nend\n"}

if ARGV.include? "--community-edition"
  edition = "community"
elsif ARGV.include? "--pro-edition"
  edition = "pro"
elsif ARGV.include? "--advanced-edition"
  edition = "advanced"
end

unless edition
  # === Fetch the Avo edition ===
  question = <<~QUESTION


    Which version of Avo would you like to install?
    1. Avo Community (default)
    2. Avo Pro
    3. Avo Advanced

    More information about version features here:
    https://avohq.io/pricing


  QUESTION
  puts question

  answer = ask("Which version of Avo would you like to install?", default: "1", limited_to: ["1", "2", "3"])

  edition = case answer
  when "1"
    "community"
  when "2"
    "pro"
  when "3"
    "advanced"
  end
end

# === Add gem to Gemfile ===
case edition
when "community"
  gem "avo", ">= 3.2.3"
when "pro"
  gem "avo-pro", source: "https://packager.dev/avo-hq"
when "advanced"
  gem "avo-advanced", source: "https://packager.dev/avo-hq"
end

gem "ransack"

# === Run bundle install ===
Bundler.with_unbundled_env { run "bundle install" }

file "config/routes/avo.rb", "# Avo admin panel
if defined?(Avo::Engine)
  authenticate :user, lambda { |u| u.developer? } do
    mount Avo::Engine, at: Avo.configuration.root_path
  end
end"

route "draw \"avo\""

# === Copy template files ===
files.each do |path, contents|
  file path, contents
end