defmodule Mix.Tasks.Admin.Install do @moduledoc """ Install ExAdmin supporting files. mix admin.install # Defaults: * assets - copy js, css, and image files * brunch - append instructions to brunch-config.js * config - add configuration to config/config.exs * dashboard - create a default dashboard * route - display instructions to add the admin routes ## Options: * --no-brunch - Write assets to priv/static instead of web/static/ * --no-assets - Skip the assets * --no-config - Skip the config * --no-dashboard - Skip the dashboard * --no-route - Skip the route instructions """ # @shortdoc "Install ExAdmin" use Mix.Task import Mix.ExAdmin.Utils import ExAdmin.Gettext @boolean_switchs ~w(assets config route dashboard brunch)a @switches Enum.map(@boolean_switchs, &({&1, :boolean})) defmodule Config do defstruct route: true, assets: true, dashboard: true, package_path: nil, config: true, brunch: true end def run(args) do do_run(args) end def do_run(args) do parse_args(args) |> do_install() end def do_install(config) do config |> check_project |> do_assets |> do_config |> do_dashboard |> do_route |> do_paging |> do_assets_instructions end def check_project(config) do config |> check_config |> check_assets end defp check_config(%{config: true} = config) do config_path = Path.join ~w(config config.exs) unless File.exists? config_path do Mix.raise """ Can't find #{config_path} """ end config end defp check_config(config), do: config defp check_assets(%{assets: true, brunch: true} = config) do unless File.exists? "brunch-config.js" do Mix.raise """ Can't find brunch-config.js """ end config end defp check_assets(%{assets: true} = config) do path = Path.join ~w(priv static) unless File.exists?(path) do Mix.raise """ Can't find #{path} """ end config end defp check_assets(config), do: config def do_assets(%Config{assets: true, brunch: true} = config) do base_path = Path.join(~w(priv static)) File.mkdir_p Path.join ~w{web static vendor} File.mkdir_p Path.join ~w{web static assets fonts} File.mkdir_p Path.join ~w{web static assets images ex_admin datepicker} status_msg("creating", "css files") ~w(admin_lte2.css admin_lte2.css.map active_admin.css.css active_admin.css.css.map) |> Enum.each(&(copy_vendor base_path, "css", &1)) status_msg("creating", "js files") ~w(jquery.min.js admin_lte2.js jquery.min.js.map admin_lte2.js.map) ++ ~w(ex_admin_common.js ex_admin_common.js.map) |> Enum.each(&(copy_vendor base_path, "js", &1)) copy_vendor_r(base_path, "fonts") copy_vendor_r(base_path, "images") case File.read "brunch-config.js" do {:ok, file} -> File.write! "brunch-config.js", file <> brunch_instructions error -> Mix.raise """ Could not open brunch-config.js file. #{inspect error} """ end config end def do_assets(%Config{assets: true} = config) do base = ~w(priv static) base_path = Path.join(base) Enum.each ~w(fonts css js), &(File.mkdir_p Path.join(base ++ [&1])) File.mkdir_p Path.join(~w{priv static images ex_admin datepicker}) File.mkdir_p Path.join(~w(web admin)) status_msg("creating", "css files") ~w(admin_lte2.css admin_lte2.css.map active_admin.css.css active_admin.css.css.map) |> Enum.each(&(copy_file base_path, "css", &1)) status_msg("creating", "js files") ~w(jquery.min.js admin_lte2.js jquery.min.js.map admin_lte2.js.map) ++ ~w(ex_admin_common.js ex_admin_common.js.map) |> Enum.each(&(copy_file base_path, "js", &1)) copy_r(base_path, "fonts") copy_r(base_path, "images") config end def do_assets(config) do config end def do_route(%Config{route: true} = config) do Mix.shell.info """ Add the admin routes to your web/router.ex: use ExAdmin.Router # your app's routes scope "/admin", ExAdmin do pipe_through :browser admin_routes end """ config end def do_route(config), do: config def do_config(%Config{config: true} = config) do status_msg("updating", "config/config.exs") dest_path = Path.join [File.cwd! | ~w(config)] dest_file_path = Path.join dest_path, "config.exs" source = File.read!(dest_file_path) "" |> config_xain(config, source) |> config_write(config, dest_file_path, source) end def do_config(config), do: config defp config_xain(append, _config, source) do unless String.contains? source, ":xain, :after_callback" do append <> """ config :xain, :after_callback, {Phoenix.HTML, :raw} """ else notice_msg("skipping", "xain config. It already exists.") append end end defp config_write("", config, _dest_file_path, _source), do: config defp config_write(append, config, dest_file_path, source) do File.write! dest_file_path, source <> "\n" <> append config end def do_dashboard(%Config{dashboard: true} = config) do dest_path = Path.join [File.cwd! | ~w(web admin)] dest_file_path = Path.join dest_path, "dashboard.ex" source = Path.join([config.package_path | ~w(priv templates admin.install dashboard.exs)] ) |> EEx.eval_file([base: get_module, title_txt: (gettext "Dashboard"), welcome_txt: (gettext "Welcome to ExAdmin. This is the default dashboard page."), add_txt: (gettext "To add dashboard sections, checkout 'web/admin/dashboards.ex'") ]) file = Path.join(~w(web admin dashboard.ex)) if File.exists?(file) do notice_msg "skipping", "#{file}. It already exists." else status_msg "creating", file File.mkdir_p dest_path File.write! dest_file_path, source dashboard_instructions end config end def do_dashboard(config), do: config def dashboard_instructions do base = get_module Mix.shell.info """ Remember to update your config file: config :ex_admin, repo: #{base}.Repo, module: #{base}, modules: [ #{base}.ExAdmin.Dashboard, ] """ end def do_paging(config) do base = get_module Mix.shell.info """ Add Scrivener paging to your Repo: defmodule #{base}.Repo do use Ecto.Repo, otp_app: :#{String.downcase base} use Scrivener, page_size: 10 # <--- add this end """ config end def do_assets_instructions(%{assets: true, brunch: true} = config) do Mix.shell.info """ Check the bottom of your brunch-config.js file. Instructions for adding the ExAdmin assets have been added. """ config end def do_assets_instructions(config), do: config defp copy_r(base_path, path) do File.cp_r Path.join([get_package_path, base_path, path]), Path.join([File.cwd!, base_path, path]) base_path end defp copy_file(base_path, path, file_name) do File.cp Path.join([get_package_path, base_path, path, file_name]), Path.join([File.cwd!, base_path, path, file_name]) base_path end defp copy_vendor(from_path, path, filename) do File.cp Path.join([get_package_path, from_path, path, filename]), Path.join([File.cwd!, "web", "static", "vendor", filename]) end defp copy_vendor_r(base_path, path) do File.cp_r Path.join([get_package_path, base_path, path]), Path.join([File.cwd!, "web", "static", "assets", path]) end def brunch_instructions do """ // To add the ExAdmin generated assets to your brunch build, do the following: // // Replace // // javascripts: { // joinTo: "js/app.js" // }, // // With // // javascripts: { // joinTo: { // "js/app.js": /^(web\\/static\\/js)|(node_modules)/, // "js/ex_admin_common.js": ["web/static/vendor/ex_admin_common.js"], // "js/admin_lte2.js": ["web/static/vendor/admin_lte2.js"], // "js/jquery.min.js": ["web/static/vendor/jquery.min.js"], // } // }, // // Replace // // stylesheets: { // joinTo: "css/app.css", // order: { // after: ["web/static/css/app.css"] // concat app.css last // } // }, // // With // // stylesheets: { // joinTo: { // "css/app.css": /^(web\\/static\\/css)/, // "css/admin_lte2.css": ["web/static/vendor/admin_lte2.css"], // "css/active_admin.css.css": ["web/static/vendor/active_admin.css.css"], // }, // order: { // after: ["web/static/css/app.css"] // concat app.css last // } // }, // """ end defp parse_args(args) do {opts, _values, _} = OptionParser.parse args, switches: @switches Enum.reduce opts, %Config{package_path: get_package_path}, fn {key, value}, config -> if key in Map.keys(config) do struct(config, [{key, value}]) else raise_option key end other, _config -> raise_option inspect(other) end end defp raise_option(option) do Mix.raise """ Invalid option --#{option} """ end end