From 18fa99259e89e6dfafc143fb1ac709691bd9ce08 Mon Sep 17 00:00:00 2001 From: Stefan Wintermeyer Date: Tue, 17 Oct 2023 00:17:22 +0200 Subject: [PATCH] improvement: ash_phoenix.gen.html generator (#112) --- lib/mix/tasks/ash_phoenix.gen.html.ex | 121 ++++++++++++++++++ .../ash_phoenix.gen.html/controller.ex | 78 +++++++++++ .../ash_phoenix.gen.html/edit.html.heex | 8 ++ priv/templates/ash_phoenix.gen.html/html.ex | 5 + .../ash_phoenix.gen.html/index.html.heex | 25 ++++ .../ash_phoenix.gen.html/new.html.heex | 8 ++ .../resource_form.html.heex | 15 +++ .../ash_phoenix.gen.html/show.html.heex | 17 +++ 8 files changed, 277 insertions(+) create mode 100644 lib/mix/tasks/ash_phoenix.gen.html.ex create mode 100644 priv/templates/ash_phoenix.gen.html/controller.ex create mode 100644 priv/templates/ash_phoenix.gen.html/edit.html.heex create mode 100644 priv/templates/ash_phoenix.gen.html/html.ex create mode 100644 priv/templates/ash_phoenix.gen.html/index.html.heex create mode 100644 priv/templates/ash_phoenix.gen.html/new.html.heex create mode 100644 priv/templates/ash_phoenix.gen.html/resource_form.html.heex create mode 100644 priv/templates/ash_phoenix.gen.html/show.html.heex diff --git a/lib/mix/tasks/ash_phoenix.gen.html.ex b/lib/mix/tasks/ash_phoenix.gen.html.ex new file mode 100644 index 0000000..d7eb04b --- /dev/null +++ b/lib/mix/tasks/ash_phoenix.gen.html.ex @@ -0,0 +1,121 @@ +defmodule Mix.Tasks.AshPhoenix.Gen.Html do + use Mix.Task + + @shortdoc "Generates a controller and HTML views for an existing Ash resource." + + @moduledoc """ + This task renders .ex and .heex templates and copies them to specified directories. + + ## Arguments + + api The API (e.g. "Shop"). + resource The resource (e.g. "Product"). + plural The plural schema name (e.g. "products"). + + ## Example + + mix ash_phoenix.gen.html Shop Product products + """ + + def run([]) do + Mix.shell().info(""" + #{Mix.Task.shortdoc(__MODULE__)} + + #{Mix.Task.moduledoc(__MODULE__)} + """) + end + + def run(args) when length(args) == 3 do + Mix.Task.run("compile") + + [api, resource, plural] = args + singular = String.downcase(resource) + + opts = %{ + api: api, + resource: resource, + singular: singular, + plural: plural + } + + if Code.ensure_loaded?(resource_module(opts)) do + source_path = Application.app_dir(:ash_phoenix, "priv/templates/ash_phoenix.gen.html") + resource_html_dir = Macro.underscore(opts[:resource]) <> "_html" + + template_files(resource_html_dir, opts) + |> generate_files(assigns([:api, :resource, :singular, :plural], opts), source_path) + + print_shell_instructions(opts[:resource], opts[:plural]) + else + Mix.shell().info( + "The resource #{app_name()}.#{opts[:api]}.#{opts[:resource]} does not exist." + ) + end + end + + defp assigns(keys, opts) do + binding = Enum.map(keys, fn key -> {key, opts[key]} end) + binding = [{:route_prefix, Macro.underscore(opts[:plural])} | binding] + binding = [{:app_name, app_name()} | binding] + binding = [{:attributes, attributes(opts)} | binding] + Enum.into(binding, %{}) + end + + defp template_files(resource_html_dir, opts) do + app_web_path = "lib/#{Macro.underscore(app_name())}_web" + + %{ + "index.html.heex" => "#{app_web_path}/controllers/#{resource_html_dir}/index.html.heex", + "show.html.heex" => "#{app_web_path}/controllers/#{resource_html_dir}/show.html.heex", + "resource_form.html.heex" => + "#{app_web_path}/controllers/#{resource_html_dir}/#{Macro.underscore(opts[:resource])}_form.html.heex", + "new.html.heex" => "#{app_web_path}/controllers/#{resource_html_dir}/new.html.heex", + "edit.html.heex" => "#{app_web_path}/controllers/#{resource_html_dir}/edit.html.heex", + "controller.ex" => + "#{app_web_path}/controllers/#{Macro.underscore(opts[:resource])}_controller.ex", + "html.ex" => "#{app_web_path}/controllers/#{Macro.underscore(opts[:resource])}_html.ex" + } + end + + defp generate_files(template_files, assigns, source_path) do + Enum.each(template_files, fn {source_file, dest_file} -> + Mix.Generator.create_file( + dest_file, + EEx.eval_file("#{source_path}/#{source_file}", assigns: assigns) + ) + end) + end + + defp app_name do + app_name_atom = Mix.Project.config()[:app] + Macro.camelize(Atom.to_string(app_name_atom)) + end + + defp print_shell_instructions(resource, plural) do + Mix.shell().info(""" + + Add the resource to your browser scope in lib/#{Macro.underscore(resource)}_web/router.ex: + + resources "/#{plural}", #{resource}Controller + """) + end + + defp resource_module(opts) do + Module.concat(["#{app_name()}.#{opts[:api]}.#{opts[:resource]}"]) + end + + defp attributes(opts) do + resource_module(opts) + |> Ash.Resource.Info.attributes() + |> Enum.map(&attribute_map/1) + |> Enum.reject(&reject_attribute?/1) + end + + defp attribute_map(attr) do + %{name: attr.name, type: attr.type, writable?: attr.writable?, private?: attr.private?} + end + + defp reject_attribute?(%{name: :id, type: Ash.Type.UUID}), do: true + defp reject_attribute?(%{private?: true}), do: true + defp reject_attribute?(_), do: false +end diff --git a/priv/templates/ash_phoenix.gen.html/controller.ex b/priv/templates/ash_phoenix.gen.html/controller.ex new file mode 100644 index 0000000..69ac1fc --- /dev/null +++ b/priv/templates/ash_phoenix.gen.html/controller.ex @@ -0,0 +1,78 @@ +defmodule <%= @app_name %>Web.<%= @resource %>Controller do + use <%= @app_name %>Web, :controller + + alias <%= @app_name %>.<%= @api %>.<%= @resource %> + + def index(conn, _params) do + <%= @plural %> = <%= @resource %>.read!() + render(conn, :index, <%= @plural %>: <%= @plural %>) + end + + def new(conn, _params) do + render(conn, :new, form: create_form()) + end + + def create(conn, %{"<%= @singular %>" => <%= @singular %>_params}) do + <%= @singular %>_params + |> create_form() + |> AshPhoenix.Form.submit() + |> case do + {:ok, <%= @singular %>} -> + conn + |> put_flash(:info, "<%= @resource %> created successfully.") + |> redirect(to: ~p"/<%= @plural %>/#{<%= @singular %>}") + + {:error, form} -> + conn + |> put_flash(:error, "<%= @resource %> could not be created.") + |> render(:new, form: form) + end + end + + def show(conn, %{"id" => id}) do + <%= @singular %> = <%= @resource %>.by_id!(id) + render(conn, :show, <%= @singular %>: <%= @singular %>) + end + + def edit(conn, %{"id" => id}) do + <%= @singular %> = <%= @resource %>.by_id!(id) + + render(conn, :edit, <%= @singular %>: <%= @singular %>, form: update_form(<%= @singular %>)) + end + + def update(conn, %{"<%= @singular %>" => <%= @singular %>_params, "id" => id}) do + <%= @singular %> = <%= @resource %>.by_id!(id) + + <%= @singular %> + |> update_form(<%= @singular %>_params) + |> AshPhoenix.Form.submit() + |> case do + {:ok, <%= @singular %>} -> + conn + |> put_flash(:info, "<%= @resource %> updated successfully.") + |> redirect(to: ~p"/<%= @plural %>/#{<%= @singular %>}") + + {:error, form} -> + conn + |> put_flash(:error, "<%= @resource %> could not be updated.") + |> render(:edit, <%= @singular %>: <%= @singular %>, form: form) + end + end + + def delete(conn, %{"id" => id}) do + <%= @singular %> = <%= @resource %>.by_id!(id) + :ok = <%= @resource %>.destroy(<%= @singular %>) + + conn + |> put_flash(:info, "<%= @resource %> deleted successfully.") + |> redirect(to: ~p"/<%= @plural %>") + end + + defp create_form(params \\ nil) do + AshPhoenix.Form.for_create(<%= @resource %>, :create, as: "<%= @singular %>", api: <%= @app_name %>.<%= @api %>, params: params) + end + + defp update_form(<%= @singular %>, params \\ nil) do + AshPhoenix.Form.for_update(<%= @singular %>, :update, as: "<%= @singular %>", api: <%= @app_name %>.<%= @api %>, params: params) + end +end diff --git a/priv/templates/ash_phoenix.gen.html/edit.html.heex b/priv/templates/ash_phoenix.gen.html/edit.html.heex new file mode 100644 index 0000000..d7ccf5a --- /dev/null +++ b/priv/templates/ash_phoenix.gen.html/edit.html.heex @@ -0,0 +1,8 @@ +<.header> + Edit <%= @resource %> <%%= @<%= @singular %>.id %> + <:subtitle>Use this form to manage <%= @singular %> records in your database. + + +<.<%= @singular %>_form <%= @singular %>={@<%= @singular %>} form={@form} action={~p"/<%= @plural %>/#{@<%= @singular %>}"} /> + +<.back navigate={~p"/<%= @plural %>"}>Back to <%= @plural %> diff --git a/priv/templates/ash_phoenix.gen.html/html.ex b/priv/templates/ash_phoenix.gen.html/html.ex new file mode 100644 index 0000000..5c5d0fa --- /dev/null +++ b/priv/templates/ash_phoenix.gen.html/html.ex @@ -0,0 +1,5 @@ +defmodule <%= @app_name %>Web.<%= @resource %>HTML do + use <%= @app_name %>Web, :html + + embed_templates "<%= @singular %>_html/*" +end diff --git a/priv/templates/ash_phoenix.gen.html/index.html.heex b/priv/templates/ash_phoenix.gen.html/index.html.heex new file mode 100644 index 0000000..c968058 --- /dev/null +++ b/priv/templates/ash_phoenix.gen.html/index.html.heex @@ -0,0 +1,25 @@ +<.header> + <%= @resource %> Listing + <:actions> + <.link href={~p"/<%= @route_prefix %>/new"}> + <.button>New <%= @resource %> + + + + +<.table id="<%= @plural %>" rows={@<%= @plural %>} row_click={&JS.navigate(~p"/<%= @route_prefix %>/#{&1}")}> + <%= for attribute <- @attributes do %> + <:col :let={<%= @singular %>} label="<%= attribute.name %>"><%%= <%= @singular %>.<%= attribute.name %> %> + <% end %> + <:action :let={<%= @singular %>}> +
+ <.link navigate={~p"/<%= @route_prefix %>/#{<%= @singular %>}"}>Show +
+ <.link navigate={~p"/<%= @route_prefix %>/#{<%= @singular %>}/edit"}>Edit + + <:action :let={<%= @singular %>}> + <.link href={~p"/<%= @route_prefix %>/#{<%= @singular %>}"} method="delete" data-confirm="Are you sure?"> + Delete + + + diff --git a/priv/templates/ash_phoenix.gen.html/new.html.heex b/priv/templates/ash_phoenix.gen.html/new.html.heex new file mode 100644 index 0000000..9382cef --- /dev/null +++ b/priv/templates/ash_phoenix.gen.html/new.html.heex @@ -0,0 +1,8 @@ +<.header> + New <%= @resource %> + <:subtitle>Use this form to manage <%= @singular %> records in your database. + + +<.<%= @singular %>_form form={@form} action={~p"/<%= @plural %>/"} /> + +<.back navigate={~p"/products"}>Back to <%= @plural %> diff --git a/priv/templates/ash_phoenix.gen.html/resource_form.html.heex b/priv/templates/ash_phoenix.gen.html/resource_form.html.heex new file mode 100644 index 0000000..3c9879a --- /dev/null +++ b/priv/templates/ash_phoenix.gen.html/resource_form.html.heex @@ -0,0 +1,15 @@ +<.simple_form :let={f} for={@form} action={@action}> + <.error :if={@form.submitted_once?}> + Oops, something went wrong! Please check the errors below. + + <%= for attribute <- @attributes do %> + <%= if attribute.type in [Ash.Type.Integer] do %> + <.input field={f[:<%= attribute.name %>]} type="number" label="<%= attribute.name %>" /> + <% else %> + <.input field={f[:<%= attribute.name %>]} type="text" label="<%= attribute.name %>" /> + <% end %> + <% end %> + <:actions> + <.button>Save Product + + diff --git a/priv/templates/ash_phoenix.gen.html/show.html.heex b/priv/templates/ash_phoenix.gen.html/show.html.heex new file mode 100644 index 0000000..25310eb --- /dev/null +++ b/priv/templates/ash_phoenix.gen.html/show.html.heex @@ -0,0 +1,17 @@ +<.header> + <%= @resource %> <%%= @<%= @singular %>.id %> + <:subtitle>This is a <%= @singular %> record from your database. + <:actions> + <.link href={~p"/<%= @plural %>/#{@<%= @singular %>}/edit"}> + <.button>Edit <%= @singular %> + + + + +<.list> +<%= for attribute <- @attributes do %> + <:item title="<%= attribute.name %>"><%%= @<%= @singular %>.<%= attribute.name %> %> +<% end %> + + +<.back navigate={~p"/<%= @plural %>"}>Back to <%= @plural %>