From d664d027beb45580a0ce1172c4e8cedb4a5550b9 Mon Sep 17 00:00:00 2001 From: Jason Crossfield Date: Fri, 16 Oct 2020 00:29:39 -0400 Subject: [PATCH 1/7] Initial check-in of phx.gen.live.slime.ex based on phx.gen.live.ex from phoenix repo. --- lib/mix/tasks/phx.gen.live.slime.ex | 225 ++++++++++++++++++++++++++++ 1 file changed, 225 insertions(+) create mode 100644 lib/mix/tasks/phx.gen.live.slime.ex diff --git a/lib/mix/tasks/phx.gen.live.slime.ex b/lib/mix/tasks/phx.gen.live.slime.ex new file mode 100644 index 0000000..2568827 --- /dev/null +++ b/lib/mix/tasks/phx.gen.live.slime.ex @@ -0,0 +1,225 @@ +defmodule Mix.Tasks.Phx.Gen.Live do + @shortdoc "Generates LiveView, templates, and context for a resource" + + @moduledoc """ + Generates LiveView, templates, and context for a resource. + + mix phx.gen.live Accounts User users name:string age:integer + + The first argument is the context module followed by the schema module + and its plural name (used as the schema table name). + + The context is an Elixir module that serves as an API boundary for + the given resource. A context often holds many related resources. + Therefore, if the context already exists, it will be augmented with + functions for the given resource. + + When this command is run for the first time, a `ModalComponent` and + `LiveHelpers` module will be created, along with the resource level + LiveViews and components, including an `IndexLive`, `ShowLive`, `FormComponent` + for the new resource. + + > Note: A resource may also be split + > over distinct contexts (such as `Accounts.User` and `Payments.User`). + + The schema is responsible for mapping the database fields into an + Elixir struct. It is followed by an optional list of attributes, + with their respective names and types. See `mix phx.gen.schema` + for more information on attributes. + + Overall, this generator will add the following files to `lib/`: + + * a context module in `lib/app/accounts.ex` for the accounts API + * a schema in `lib/app/accounts/user.ex`, with an `users` table + * a view in `lib/app_web/views/user_view.ex` + * a LiveView in `lib/app_web/live/user_live/show_live.ex` + * a LiveView in `lib/app_web/live/user_live/index_live.ex` + * a LiveComponent in `lib/app_web/live/user_live/form_component.ex` + * a LiveComponent in `lib/app_web/live/modal_component.ex` + * a helpers modules in `lib/app_web/live/live_helpers.ex` + + ## The context app + + A migration file for the repository and test files for the context and + controller features will also be generated. + + The location of the web files (LiveView's, views, templates, etc) in an + umbrella application will vary based on the `:context_app` config located + in your applications `:generators` configuration. When set, the Phoenix + generators will generate web files directly in your lib and test folders + since the application is assumed to be isolated to web specific functionality. + If `:context_app` is not set, the generators will place web related lib + and test files in a `web/` directory since the application is assumed + to be handling both web and domain specific functionality. + Example configuration: + + config :my_app_web, :generators, context_app: :my_app + + Alternatively, the `--context-app` option may be supplied to the generator: + + mix phx.gen.live Sales User users --context-app warehouse + + ## Web namespace + + By default, the controller and view will be namespaced by the schema name. + You can customize the web module namespace by passing the `--web` flag with a + module name, for example: + + mix phx.gen.live Sales User users --web Sales + + Which would generate a LiveViews inside `lib/app_web/live/sales/user_live/` and a + view at `lib/app_web/views/sales/user_view.ex`. + + ## Customising the context, schema, tables and migrations + + In some cases, you may wish to bootstrap HTML templates, LiveViews, + and tests, but leave internal implementation of the context or schema + to yourself. You can use the `--no-context` and `--no-schema` flags + for file generation control. + + You can also change the table name or configure the migrations to + use binary ids for primary keys, see `mix phx.gen.schema` for more + information. + """ + use Mix.Task + + alias Mix.Phoenix.{Context} + alias Mix.Tasks.Phx.Gen + + @doc false + def run(args) do + if Mix.Project.umbrella?() do + Mix.raise "mix phx.gen.live.slime must be invoked from within your *_web application root directory" + end + + {context, schema} = Gen.Context.build(args) + Gen.Context.prompt_for_code_injection(context) + + binding = [context: context, schema: schema, inputs: Gen.Html.inputs(schema)] + paths = Mix.Phoenix.generator_paths() + + prompt_for_conflicts(context) + + context + |> copy_new_files(binding, paths) + |> maybe_inject_helpers() + |> print_shell_instructions() + end + + defp prompt_for_conflicts(context) do + context + |> files_to_be_generated() + |> Kernel.++(context_files(context)) + |> Mix.Phoenix.prompt_for_conflicts() + end + defp context_files(%Context{generate?: true} = context) do + Gen.Context.files_to_be_generated(context) + end + defp context_files(%Context{generate?: false}) do + [] + end + + defp files_to_be_generated(%Context{schema: schema, context_app: context_app}) do + web_prefix = Mix.Phoenix.web_path(context_app) + test_prefix = Mix.Phoenix.web_test_path(context_app) + web_path = to_string(schema.web_path) + live_subdir = "#{schema.singular}_live" + + [ + {:eex, "show.ex", Path.join([web_prefix, "live", web_path, live_subdir, "show.ex"])}, + {:eex, "index.ex", Path.join([web_prefix, "live", web_path, live_subdir, "index.ex"])}, + {:eex, "form_component.ex", Path.join([web_prefix, "live", web_path, live_subdir, "form_component.ex"])}, + {:eex, "form_component.html.leex", Path.join([web_prefix, "live", web_path, live_subdir, "form_component.html.leex"])}, + {:eex, "index.html.leex", Path.join([web_prefix, "live", web_path, live_subdir, "index.html.leex"])}, + {:eex, "show.html.leex", Path.join([web_prefix, "live", web_path, live_subdir, "show.html.leex"])}, + {:eex, "live_test.exs", Path.join([test_prefix, "live", web_path, "#{schema.singular}_live_test.exs"])}, + {:new_eex, "modal_component.ex", Path.join([web_prefix, "live", "modal_component.ex"])}, + {:new_eex, "live_helpers.ex", Path.join([web_prefix, "live", "live_helpers.ex"])}, + ] + end + + defp copy_new_files(%Context{} = context, binding, paths) do + files = files_to_be_generated(context) + Mix.Phoenix.copy_from(paths, "priv/templates/phx.gen.live", binding, files) + if context.generate?, do: Gen.Context.copy_new_files(context, paths, binding) + + context + end + + defp maybe_inject_helpers(%Context{context_app: ctx_app} = context) do + web_prefix = Mix.Phoenix.web_path(ctx_app) + [lib_prefix, web_dir] = Path.split(web_prefix) + file_path = Path.join(lib_prefix, "#{web_dir}.ex") + file = File.read!(file_path) + inject = "import #{inspect(context.web_module)}.LiveHelpers" + + if String.contains?(file, inject) do + :ok + else + do_inject_helpers(context, file, file_path, inject) + end + + context + end + + defp do_inject_helpers(context, file, file_path, inject) do + Mix.shell().info([:green, "* injecting ", :reset, Path.relative_to_cwd(file_path)]) + + new_file = String.replace(file, "import Phoenix.LiveView.Helpers", "import Phoenix.LiveView.Helpers\n #{inject}") + if file != new_file do + File.write!(file_path, new_file) + else + Mix.shell().info """ + + Could not find Phoenix.LiveView.Helpers imported in #{file_path}. + + This typically happens because your application was not generated + with the --live flag: + + mix phx.new my_app --live + + Please make sure LiveView is installed and that #{inspect(context.web_module)} + defines both `live_view/0` and `live_component/0` functions, + and that both functions import #{inspect(context.web_module)}.LiveHelpers. + """ + end + end + + @doc false + def print_shell_instructions(%Context{schema: schema, context_app: ctx_app} = context) do + prefix = Module.concat(context.web_module, schema.web_namespace) + web_path = Mix.Phoenix.web_path(ctx_app) + + if schema.web_namespace do + Mix.shell().info """ + + Add the live routes to your #{schema.web_namespace} :browser scope in #{web_path}/router.ex: + + scope "/#{schema.web_path}", #{inspect prefix}, as: :#{schema.web_path} do + pipe_through :browser + ... + + #{for line <- live_route_instructions(schema), do: " #{line}"} + end + """ + else + Mix.shell().info """ + + Add the live routes to your browser scope in #{Mix.Phoenix.web_path(ctx_app)}/router.ex: + + #{for line <- live_route_instructions(schema), do: " #{line}"} + """ + end + if context.generate?, do: Gen.Context.print_shell_instructions(context) + end + + defp live_route_instructions(schema) do + [ + ~s|live "/#{schema.plural}", #{inspect(schema.alias)}Live.Index, :index\n|, + ~s|live "/#{schema.plural}/new", #{inspect(schema.alias)}Live.Index, :new\n|, + ~s|live "/#{schema.plural}/:id/edit", #{inspect(schema.alias)}Live.Index, :edit\n\n|, + ~s|live "/#{schema.plural}/:id", #{inspect(schema.alias)}Live.Show, :show\n|, + ~s|live "/#{schema.plural}/:id/show/edit", #{inspect(schema.alias)}Live.Show, :edit| + ] + end +end From 333fc72d1d400c9ab67a5fd8ad22d38f0a8d4293 Mon Sep 17 00:00:00 2001 From: Jason Crossfield Date: Fri, 16 Oct 2020 02:46:49 -0400 Subject: [PATCH 2/7] Added phx.gen.live.slime task along with slim based templates for LiveView. --- lib/mix/tasks/phx.gen.live.slime.ex | 59 +++------- priv/templates/phx.gen.live/form_component.ex | 55 +++++++++ .../phx.gen.live/form_component.html.leex | 15 +++ priv/templates/phx.gen.live/index.ex | 46 ++++++++ priv/templates/phx.gen.live/index.html.leex | 30 +++++ priv/templates/phx.gen.live/live_helpers.ex | 23 ++++ priv/templates/phx.gen.live/live_test.exs | 110 ++++++++++++++++++ .../templates/phx.gen.live/modal_component.ex | 26 +++++ priv/templates/phx.gen.live/show.ex | 21 ++++ priv/templates/phx.gen.live/show.html.leex | 21 ++++ 10 files changed, 361 insertions(+), 45 deletions(-) create mode 100644 priv/templates/phx.gen.live/form_component.ex create mode 100644 priv/templates/phx.gen.live/form_component.html.leex create mode 100644 priv/templates/phx.gen.live/index.ex create mode 100644 priv/templates/phx.gen.live/index.html.leex create mode 100644 priv/templates/phx.gen.live/live_helpers.ex create mode 100644 priv/templates/phx.gen.live/live_test.exs create mode 100644 priv/templates/phx.gen.live/modal_component.ex create mode 100644 priv/templates/phx.gen.live/show.ex create mode 100644 priv/templates/phx.gen.live/show.html.leex diff --git a/lib/mix/tasks/phx.gen.live.slime.ex b/lib/mix/tasks/phx.gen.live.slime.ex index 2568827..a0ba7d7 100644 --- a/lib/mix/tasks/phx.gen.live.slime.ex +++ b/lib/mix/tasks/phx.gen.live.slime.ex @@ -1,4 +1,4 @@ -defmodule Mix.Tasks.Phx.Gen.Live do +defmodule Mix.Tasks.Phx.Gen.Live.Slime do @shortdoc "Generates LiveView, templates, and context for a resource" @moduledoc """ @@ -86,6 +86,9 @@ defmodule Mix.Tasks.Phx.Gen.Live do alias Mix.Phoenix.{Context} alias Mix.Tasks.Phx.Gen + import Mix.Tasks.Phx.Gen.Live, only: [print_shell_instructions: 1] + import Mix.Tasks.Phx.Gen.Html.Slime, only: [inputs: 1, label: 1, error: 1] + @doc false def run(args) do if Mix.Project.umbrella?() do @@ -112,30 +115,24 @@ defmodule Mix.Tasks.Phx.Gen.Live do |> Kernel.++(context_files(context)) |> Mix.Phoenix.prompt_for_conflicts() end + defp context_files(%Context{generate?: true} = context) do Gen.Context.files_to_be_generated(context) end + defp context_files(%Context{generate?: false}) do [] end - defp files_to_be_generated(%Context{schema: schema, context_app: context_app}) do - web_prefix = Mix.Phoenix.web_path(context_app) - test_prefix = Mix.Phoenix.web_test_path(context_app) - web_path = to_string(schema.web_path) - live_subdir = "#{schema.singular}_live" + @doc false + def files_to_be_generated(context) do + to_gen = Mix.Tasks.Phx.Gen.Live.files_to_be_generated(context) - [ - {:eex, "show.ex", Path.join([web_prefix, "live", web_path, live_subdir, "show.ex"])}, - {:eex, "index.ex", Path.join([web_prefix, "live", web_path, live_subdir, "index.ex"])}, - {:eex, "form_component.ex", Path.join([web_prefix, "live", web_path, live_subdir, "form_component.ex"])}, - {:eex, "form_component.html.leex", Path.join([web_prefix, "live", web_path, live_subdir, "form_component.html.leex"])}, - {:eex, "index.html.leex", Path.join([web_prefix, "live", web_path, live_subdir, "index.html.leex"])}, - {:eex, "show.html.leex", Path.join([web_prefix, "live", web_path, live_subdir, "show.html.leex"])}, - {:eex, "live_test.exs", Path.join([test_prefix, "live", web_path, "#{schema.singular}_live_test.exs"])}, - {:new_eex, "modal_component.ex", Path.join([web_prefix, "live", "modal_component.ex"])}, - {:new_eex, "live_helpers.ex", Path.join([web_prefix, "live", "live_helpers.ex"])}, - ] + new_extension = "slimleex" + + Enum.map(to_gen, fn {type, name, path} -> + {type, name, String.replace_suffix(path, "leex", new_extension)} + end) end defp copy_new_files(%Context{} = context, binding, paths) do @@ -185,34 +182,6 @@ defmodule Mix.Tasks.Phx.Gen.Live do end end - @doc false - def print_shell_instructions(%Context{schema: schema, context_app: ctx_app} = context) do - prefix = Module.concat(context.web_module, schema.web_namespace) - web_path = Mix.Phoenix.web_path(ctx_app) - - if schema.web_namespace do - Mix.shell().info """ - - Add the live routes to your #{schema.web_namespace} :browser scope in #{web_path}/router.ex: - - scope "/#{schema.web_path}", #{inspect prefix}, as: :#{schema.web_path} do - pipe_through :browser - ... - - #{for line <- live_route_instructions(schema), do: " #{line}"} - end - """ - else - Mix.shell().info """ - - Add the live routes to your browser scope in #{Mix.Phoenix.web_path(ctx_app)}/router.ex: - - #{for line <- live_route_instructions(schema), do: " #{line}"} - """ - end - if context.generate?, do: Gen.Context.print_shell_instructions(context) - end - defp live_route_instructions(schema) do [ ~s|live "/#{schema.plural}", #{inspect(schema.alias)}Live.Index, :index\n|, diff --git a/priv/templates/phx.gen.live/form_component.ex b/priv/templates/phx.gen.live/form_component.ex new file mode 100644 index 0000000..6957239 --- /dev/null +++ b/priv/templates/phx.gen.live/form_component.ex @@ -0,0 +1,55 @@ +defmodule <%= inspect context.web_module %>.<%= inspect Module.concat(schema.web_namespace, schema.alias) %>Live.FormComponent do + use <%= inspect context.web_module %>, :live_component + + alias <%= inspect context.module %> + + @impl true + def update(%{<%= schema.singular %>: <%= schema.singular %>} = assigns, socket) do + changeset = <%= inspect context.alias %>.change_<%= schema.singular %>(<%= schema.singular %>) + + {:ok, + socket + |> assign(assigns) + |> assign(:changeset, changeset)} + end + + @impl true + def handle_event("validate", %{"<%= schema.singular %>" => <%= schema.singular %>_params}, socket) do + changeset = + socket.assigns.<%= schema.singular %> + |> <%= inspect context.alias %>.change_<%= schema.singular %>(<%= schema.singular %>_params) + |> Map.put(:action, :validate) + + {:noreply, assign(socket, :changeset, changeset)} + end + + def handle_event("save", %{"<%= schema.singular %>" => <%= schema.singular %>_params}, socket) do + save_<%= schema.singular %>(socket, socket.assigns.action, <%= schema.singular %>_params) + end + + defp save_<%= schema.singular %>(socket, :edit, <%= schema.singular %>_params) do + case <%= inspect context.alias %>.update_<%= schema.singular %>(socket.assigns.<%= schema.singular %>, <%= schema.singular %>_params) do + {:ok, _<%= schema.singular %>} -> + {:noreply, + socket + |> put_flash(:info, "<%= schema.human_singular %> updated successfully") + |> push_redirect(to: socket.assigns.return_to)} + + {:error, %Ecto.Changeset{} = changeset} -> + {:noreply, assign(socket, :changeset, changeset)} + end + end + + defp save_<%= schema.singular %>(socket, :new, <%= schema.singular %>_params) do + case <%= inspect context.alias %>.create_<%= schema.singular %>(<%= schema.singular %>_params) do + {:ok, _<%= schema.singular %>} -> + {:noreply, + socket + |> put_flash(:info, "<%= schema.human_singular %> created successfully") + |> push_redirect(to: socket.assigns.return_to)} + + {:error, %Ecto.Changeset{} = changeset} -> + {:noreply, assign(socket, changeset: changeset)} + end + end +end diff --git a/priv/templates/phx.gen.live/form_component.html.leex b/priv/templates/phx.gen.live/form_component.html.leex new file mode 100644 index 0000000..2e5bb71 --- /dev/null +++ b/priv/templates/phx.gen.live/form_component.html.leex @@ -0,0 +1,15 @@ +h2 = @title + += f = form_for @changeset, "#", + id: "<%= schema.singular %>-form", + phx_target: @myself, + phx_change: "validate", + phx_submit: "save" + + <%= for {label, input, error} <- inputs, input do %> + <%= label %> + <%= input %> + <%= error %> + <% end %> + + = submit "Save", phx_disable_with: "Saving..." diff --git a/priv/templates/phx.gen.live/index.ex b/priv/templates/phx.gen.live/index.ex new file mode 100644 index 0000000..f46c53b --- /dev/null +++ b/priv/templates/phx.gen.live/index.ex @@ -0,0 +1,46 @@ +defmodule <%= inspect context.web_module %>.<%= inspect Module.concat(schema.web_namespace, schema.alias) %>Live.Index do + use <%= inspect context.web_module %>, :live_view + + alias <%= inspect context.module %> + alias <%= inspect schema.module %> + + @impl true + def mount(_params, _session, socket) do + {:ok, assign(socket, :<%= schema.collection %>, list_<%= schema.plural %>())} + end + + @impl true + def handle_params(params, _url, socket) do + {:noreply, apply_action(socket, socket.assigns.live_action, params)} + end + + defp apply_action(socket, :edit, %{"id" => id}) do + socket + |> assign(:page_title, "Edit <%= schema.human_singular %>") + |> assign(:<%= schema.singular %>, <%= inspect context.alias %>.get_<%= schema.singular %>!(id)) + end + + defp apply_action(socket, :new, _params) do + socket + |> assign(:page_title, "New <%= schema.human_singular %>") + |> assign(:<%= schema.singular %>, %<%= inspect schema.alias %>{}) + end + + defp apply_action(socket, :index, _params) do + socket + |> assign(:page_title, "Listing <%= schema.human_plural %>") + |> assign(:<%= schema.singular %>, nil) + end + + @impl true + def handle_event("delete", %{"id" => id}, socket) do + <%= schema.singular %> = <%= inspect context.alias %>.get_<%= schema.singular %>!(id) + {:ok, _} = <%= inspect context.alias %>.delete_<%= schema.singular %>(<%= schema.singular %>) + + {:noreply, assign(socket, :<%= schema.collection %>, list_<%=schema.plural %>())} + end + + defp list_<%= schema.plural %> do + <%= inspect context.alias %>.list_<%= schema.plural %>() + end +end diff --git a/priv/templates/phx.gen.live/index.html.leex b/priv/templates/phx.gen.live/index.html.leex new file mode 100644 index 0000000..4983001 --- /dev/null +++ b/priv/templates/phx.gen.live/index.html.leex @@ -0,0 +1,30 @@ +h1 Listing <%= schema.human_plural %> + += if @live_action in [:new, :edit] do + = live_modal @socket, <%= inspect context.web_module %>.<%= inspect Module.concat(schema.web_namespace, schema.alias) %>Live.FormComponent, + id: @<%= schema.singular %>.id || :new, + title: @page_title, + action: @live_action, + <%= schema.singular %>: @<%= schema.singular %>, + return_to: Routes.<%= schema.route_helper %>_index_path(@socket, :index) + +table + thead + tr + <%= for {k, _} <- schema.attrs do %> + th <%= Phoenix.Naming.humanize(Atom.to_string(k)) %> + <% end %> + th + tbody id="<%= schema.plural %>" + = for <%= schema.singular %> <- @<%= schema.collection %> do + tr id="<%= schema.singular %>-#{<%= schema.singular %>.id}" + <%= for {k, _} <- schema.attrs do %> + td = <%= schema.singular %>.<%= k %> + <% end %> + td + span = live_redirect "Show", to: Routes.<%= schema.route_helper %>_show_path(@socket, :show, <%= schema.singular %>) + span = live_patch "Edit", to: Routes.<%= schema.route_helper %>_index_path(@socket, :edit, <%= schema.singular %>) + span = link "Delete", to: "#", phx_click: "delete", phx_value_id: <%= schema.singular %>.id, data: [confirm: "Are you sure?"] + + +span = live_patch "New <%= schema.human_singular %>", to: Routes.<%= schema.route_helper %>_index_path(@socket, :new) diff --git a/priv/templates/phx.gen.live/live_helpers.ex b/priv/templates/phx.gen.live/live_helpers.ex new file mode 100644 index 0000000..91f0a5d --- /dev/null +++ b/priv/templates/phx.gen.live/live_helpers.ex @@ -0,0 +1,23 @@ +defmodule <%= inspect context.web_module %>.LiveHelpers do + import Phoenix.LiveView.Helpers + + @doc """ + Renders a component inside the `<%= inspect context.web_module %>.ModalComponent` component. + + The rendered modal receives a `:return_to` option to properly update + the URL when the modal is closed. + + ## Examples + + <%%= live_modal @socket, <%= inspect context.web_module %>.<%= inspect Module.concat(schema.web_namespace, schema.alias) %>Live.FormComponent, + id: @<%= schema.singular %>.id || :new, + action: @live_action, + <%= schema.singular %>: @<%= schema.singular %>, + return_to: Routes.<%= schema.singular %>_index_path(@socket, :index) %> + """ + def live_modal(socket, component, opts) do + path = Keyword.fetch!(opts, :return_to) + modal_opts = [id: :modal, return_to: path, component: component, opts: opts] + live_component(socket, <%= inspect context.web_module %>.ModalComponent, modal_opts) + end +end diff --git a/priv/templates/phx.gen.live/live_test.exs b/priv/templates/phx.gen.live/live_test.exs new file mode 100644 index 0000000..55110f3 --- /dev/null +++ b/priv/templates/phx.gen.live/live_test.exs @@ -0,0 +1,110 @@ +defmodule <%= inspect context.web_module %>.<%= inspect Module.concat(schema.web_namespace, schema.alias) %>LiveTest do + use <%= inspect context.web_module %>.ConnCase + + import Phoenix.LiveViewTest + import <%= inspect context.module %>Fixtures + + @create_attrs <%= Mix.Phoenix.to_text for {key, value} <- schema.params.create, into: %{}, do: {key, Mix.Phoenix.Schema.live_form_value(value)} %> + @update_attrs <%= Mix.Phoenix.to_text for {key, value} <- schema.params.update, into: %{}, do: {key, Mix.Phoenix.Schema.live_form_value(value)} %> + @invalid_attrs <%= Mix.Phoenix.to_text for {key, value} <- schema.params.create, into: %{}, do: {key, value |> Mix.Phoenix.Schema.live_form_value() |> Mix.Phoenix.Schema.invalid_form_value()} %> + + defp create_<%= schema.singular %>(_) do + <%= schema.singular %> = <%= schema.singular %>_fixture() + %{<%= schema.singular %>: <%= schema.singular %>} + end + + describe "Index" do + setup [:create_<%= schema.singular %>] + + test "lists all <%= schema.plural %>", <%= if schema.string_attr do %>%{conn: conn, <%= schema.singular %>: <%= schema.singular %>}<% else %>%{conn: conn}<% end %> do + {:ok, _index_live, html} = live(conn, Routes.<%= schema.route_helper %>_index_path(conn, :index)) + + assert html =~ "Listing <%= schema.human_plural %>"<%= if schema.string_attr do %> + assert html =~ <%= schema.singular %>.<%= schema.string_attr %><% end %> + end + + test "saves new <%= schema.singular %>", %{conn: conn} do + {:ok, index_live, _html} = live(conn, Routes.<%= schema.route_helper %>_index_path(conn, :index)) + + assert index_live |> element("a", "New <%= schema.human_singular %>") |> render_click() =~ + "New <%= schema.human_singular %>" + + assert_patch(index_live, Routes.<%= schema.route_helper %>_index_path(conn, :new)) + + assert index_live + |> form("#<%= schema.singular %>-form", <%= schema.singular %>: @invalid_attrs) + |> render_change() =~ "can't be blank" + + {:ok, _, html} = + index_live + |> form("#<%= schema.singular %>-form", <%= schema.singular %>: @create_attrs) + |> render_submit() + |> follow_redirect(conn, Routes.<%= schema.route_helper %>_index_path(conn, :index)) + + assert html =~ "<%= schema.human_singular %> created successfully"<%= if schema.string_attr do %> + assert html =~ "some <%= schema.string_attr %>"<% end %> + end + + test "updates <%= schema.singular %> in listing", %{conn: conn, <%= schema.singular %>: <%= schema.singular %>} do + {:ok, index_live, _html} = live(conn, Routes.<%= schema.route_helper %>_index_path(conn, :index)) + + assert index_live |> element("#<%= schema.singular %>-#{<%= schema.singular %>.id} a", "Edit") |> render_click() =~ + "Edit <%= schema.human_singular %>" + + assert_patch(index_live, Routes.<%= schema.route_helper %>_index_path(conn, :edit, <%= schema.singular %>)) + + assert index_live + |> form("#<%= schema.singular %>-form", <%= schema.singular %>: @invalid_attrs) + |> render_change() =~ "can't be blank" + + {:ok, _, html} = + index_live + |> form("#<%= schema.singular %>-form", <%= schema.singular %>: @update_attrs) + |> render_submit() + |> follow_redirect(conn, Routes.<%= schema.route_helper %>_index_path(conn, :index)) + + assert html =~ "<%= schema.human_singular %> updated successfully"<%= if schema.string_attr do %> + assert html =~ "some updated <%= schema.string_attr %>"<% end %> + end + + test "deletes <%= schema.singular %> in listing", %{conn: conn, <%= schema.singular %>: <%= schema.singular %>} do + {:ok, index_live, _html} = live(conn, Routes.<%= schema.route_helper %>_index_path(conn, :index)) + + assert index_live |> element("#<%= schema.singular %>-#{<%= schema.singular %>.id} a", "Delete") |> render_click() + refute has_element?(index_live, "#<%= schema.singular %>-#{<%= schema.singular %>.id}") + end + end + + describe "Show" do + setup [:create_<%= schema.singular %>] + + test "displays <%= schema.singular %>", %{conn: conn, <%= schema.singular %>: <%= schema.singular %>} do + {:ok, _show_live, html} = live(conn, Routes.<%= schema.route_helper %>_show_path(conn, :show, <%= schema.singular %>)) + + assert html =~ "Show <%= schema.human_singular %>"<%= if schema.string_attr do %> + assert html =~ <%= schema.singular %>.<%= schema.string_attr %><% end %> + end + + test "updates <%= schema.singular %> within modal", %{conn: conn, <%= schema.singular %>: <%= schema.singular %>} do + {:ok, show_live, _html} = live(conn, Routes.<%= schema.route_helper %>_show_path(conn, :show, <%= schema.singular %>)) + + assert show_live |> element("a", "Edit") |> render_click() =~ + "Edit <%= schema.human_singular %>" + + assert_patch(show_live, Routes.<%= schema.route_helper %>_show_path(conn, :edit, <%= schema.singular %>)) + + assert show_live + |> form("#<%= schema.singular %>-form", <%= schema.singular %>: @invalid_attrs) + |> render_change() =~ "can't be blank" + + {:ok, _, html} = + show_live + |> form("#<%= schema.singular %>-form", <%= schema.singular %>: @update_attrs) + |> render_submit() + |> follow_redirect(conn, Routes.<%= schema.route_helper %>_show_path(conn, :show, <%= schema.singular %>)) + + assert html =~ "<%= schema.human_singular %> updated successfully"<%= if schema.string_attr do %> + assert html =~ "some updated <%= schema.string_attr %>"<% end %> + end + end +end diff --git a/priv/templates/phx.gen.live/modal_component.ex b/priv/templates/phx.gen.live/modal_component.ex new file mode 100644 index 0000000..ab4301d --- /dev/null +++ b/priv/templates/phx.gen.live/modal_component.ex @@ -0,0 +1,26 @@ +defmodule <%= inspect context.web_module %>.ModalComponent do + use <%= inspect context.web_module %>, :live_component + + @impl true + def render(assigns) do + ~L""" +
+ +
+ <%%= live_patch raw("×"), to: @return_to, class: "phx-modal-close" %> + <%%= live_component @socket, @component, @opts %> +
+
+ """ + end + + @impl true + def handle_event("close", _, socket) do + {:noreply, push_patch(socket, to: socket.assigns.return_to)} + end +end diff --git a/priv/templates/phx.gen.live/show.ex b/priv/templates/phx.gen.live/show.ex new file mode 100644 index 0000000..b75bade --- /dev/null +++ b/priv/templates/phx.gen.live/show.ex @@ -0,0 +1,21 @@ +defmodule <%= inspect context.web_module %>.<%= inspect Module.concat(schema.web_namespace, schema.alias) %>Live.Show do + use <%= inspect context.web_module %>, :live_view + + alias <%= inspect context.module %> + + @impl true + def mount(_params, _session, socket) do + {:ok, socket} + end + + @impl true + def handle_params(%{"id" => id}, _, socket) do + {:noreply, + socket + |> assign(:page_title, page_title(socket.assigns.live_action)) + |> assign(:<%= schema.singular %>, <%= inspect context.alias %>.get_<%= schema.singular %>!(id))} + end + + defp page_title(:show), do: "Show <%= schema.human_singular %>" + defp page_title(:edit), do: "Edit <%= schema.human_singular %>" +end diff --git a/priv/templates/phx.gen.live/show.html.leex b/priv/templates/phx.gen.live/show.html.leex new file mode 100644 index 0000000..3a5847b --- /dev/null +++ b/priv/templates/phx.gen.live/show.html.leex @@ -0,0 +1,21 @@ +h1 Show <%= schema.human_singular %> + += if @live_action in [:edit] do + = live_modal @socket, <%= inspect context.web_module %>.<%= inspect Module.concat(schema.web_namespace, schema.alias) %>Live.FormComponent, + id: @<%= schema.singular %>.id, + title: @page_title, + action: @live_action, + <%= schema.singular %>: @<%= schema.singular %>, + return_to: Routes.<%= schema.route_helper %>_show_path(@socket, :show, @<%= schema.singular %>) + + +ul + + <%= for {k, _} <- schema.attrs do %> + li + strong <%= Phoenix.Naming.humanize(Atom.to_string(k)) %>: + = @<%= schema.singular %>.<%= k %> + <% end %> + +span = live_patch "Edit", to: Routes.<%= schema.route_helper %>_show_path(@socket, :edit, @<%= schema.singular %>), class: "button" +span = live_redirect "Back", to: Routes.<%= schema.route_helper %>_index_path(@socket, :index) From 6163947b4a75c1352a0803fd2ea7520ff018b81a Mon Sep 17 00:00:00 2001 From: Jason Crossfield Date: Wed, 18 Nov 2020 01:56:43 -0500 Subject: [PATCH 3/7] Updates and fixes for Mix.Tasks.Phx.Gen.Live.Slime module. --- lib/mix/tasks/phx.gen.live.slime.ex | 87 +++++++++++++++++++++++------ 1 file changed, 71 insertions(+), 16 deletions(-) diff --git a/lib/mix/tasks/phx.gen.live.slime.ex b/lib/mix/tasks/phx.gen.live.slime.ex index a0ba7d7..79e3412 100644 --- a/lib/mix/tasks/phx.gen.live.slime.ex +++ b/lib/mix/tasks/phx.gen.live.slime.ex @@ -83,23 +83,22 @@ defmodule Mix.Tasks.Phx.Gen.Live.Slime do """ use Mix.Task - alias Mix.Phoenix.{Context} + alias Mix.Phoenix.{Context,Schema} alias Mix.Tasks.Phx.Gen import Mix.Tasks.Phx.Gen.Live, only: [print_shell_instructions: 1] - import Mix.Tasks.Phx.Gen.Html.Slime, only: [inputs: 1, label: 1, error: 1] @doc false def run(args) do if Mix.Project.umbrella?() do - Mix.raise "mix phx.gen.live.slime must be invoked from within your *_web application root directory" + Mix.raise "mix phx.gen.live must be invoked from within your *_web application root directory" end {context, schema} = Gen.Context.build(args) Gen.Context.prompt_for_code_injection(context) - binding = [context: context, schema: schema, inputs: Gen.Html.inputs(schema)] - paths = Mix.Phoenix.generator_paths() + binding = [context: context, schema: schema, inputs: inputs(schema)] + paths = [".", :phoenix_slime, :phoenix] prompt_for_conflicts(context) @@ -115,29 +114,35 @@ defmodule Mix.Tasks.Phx.Gen.Live.Slime do |> Kernel.++(context_files(context)) |> Mix.Phoenix.prompt_for_conflicts() end - defp context_files(%Context{generate?: true} = context) do Gen.Context.files_to_be_generated(context) end - defp context_files(%Context{generate?: false}) do [] end - @doc false - def files_to_be_generated(context) do - to_gen = Mix.Tasks.Phx.Gen.Live.files_to_be_generated(context) - - new_extension = "slimleex" + defp files_to_be_generated(%Context{schema: schema, context_app: context_app}) do + web_prefix = Mix.Phoenix.web_path(context_app) + test_prefix = Mix.Phoenix.web_test_path(context_app) + web_path = to_string(schema.web_path) + live_subdir = "#{schema.singular}_live" - Enum.map(to_gen, fn {type, name, path} -> - {type, name, String.replace_suffix(path, "leex", new_extension)} - end) + [ + {:eex, "show.ex", Path.join([web_prefix, "live", web_path, live_subdir, "show.ex"])}, + {:eex, "index.ex", Path.join([web_prefix, "live", web_path, live_subdir, "index.ex"])}, + {:eex, "form_component.ex", Path.join([web_prefix, "live", web_path, live_subdir, "form_component.ex"])}, + {:eex, "form_component.html.leex", Path.join([web_prefix, "live", web_path, live_subdir, "form_component.html.slimleex"])}, + {:eex, "index.html.leex", Path.join([web_prefix, "live", web_path, live_subdir, "index.html.slimleex"])}, + {:eex, "show.html.leex", Path.join([web_prefix, "live", web_path, live_subdir, "show.html.slimleex"])}, + {:eex, "live_test.exs", Path.join([test_prefix, "live", web_path, "#{schema.singular}_live_test.exs"])}, + {:new_eex, "modal_component.ex", Path.join([web_prefix, "live", "modal_component.ex"])}, + {:new_eex, "live_helpers.ex", Path.join([web_prefix, "live", "live_helpers.ex"])}, + ] end defp copy_new_files(%Context{} = context, binding, paths) do files = files_to_be_generated(context) - Mix.Phoenix.copy_from(paths, "priv/templates/phx.gen.live", binding, files) + Mix.Phoenix.copy_from(paths, "priv/templates/phx.gen.live.slime", binding, files) if context.generate?, do: Gen.Context.copy_new_files(context, paths, binding) context @@ -191,4 +196,54 @@ defmodule Mix.Tasks.Phx.Gen.Live.Slime do ~s|live "/#{schema.plural}/:id/show/edit", #{inspect(schema.alias)}Live.Show, :edit| ] end + + defp inputs(%Schema{attrs: attrs}) do + Enum.map(attrs, fn + {_, {:array, _}} -> + {nil, nil, nil} + + {_, {:references, _}} -> + {nil, nil, nil} + + {key, :integer} -> + {label(key), ~s(= number_input f, #{inspect(key)}, class: "form-control", autocomplete: "off"), error(key)} + + {key, :float} -> + {label(key), ~s(= number_input f, #{inspect(key)}, step: "any", class: "form-control", autocomplete: "off"), + error(key)} + + {key, :decimal} -> + {label(key), ~s(= number_input f, #{inspect(key)}, step: "any", class: "form-control", autocomplete: "off"), + error(key)} + + {key, :boolean} -> + {label(key), ~s(= checkbox f, #{inspect(key)}, class: "form-control", autocomplete: "off"), error(key)} + + {key, :text} -> + {label(key), ~s(= textarea f, #{inspect(key)}, class: "form-control", autocomplete: "off"), error(key)} + + {key, :date} -> + {label(key), ~s(= date_select f, #{inspect(key)}, class: "form-control", autocomplete: "off"), error(key)} + + {key, :time} -> + {label(key), ~s(= time_select f, #{inspect(key)}, class: "form-control", autocomplete: "off"), error(key)} + + {key, :utc_datetime} -> + {label(key), ~s(= datetime_select f, #{inspect(key)}, class: "form-control", autocomplete: "off"), error(key)} + + {key, :naive_datetime} -> + {label(key), ~s(= datetime_select f, #{inspect(key)}, class: "form-control", autocomplete: "off"), error(key)} + + {key, _} -> + {label(key), ~s(= text_input f, #{inspect(key)}, class: "form-control", autocomplete: "off"), error(key)} + end) + end + + defp label(key) do + ~s(= label f, #{inspect(key)}, class: "control-label") + end + + defp error(field) do + ~s(= error_tag f, #{inspect(field)}) + end end From 07c5309ef9ce52dd6d082ac32a702005596fac6c Mon Sep 17 00:00:00 2001 From: Jason Crossfield Date: Wed, 18 Nov 2020 02:02:03 -0500 Subject: [PATCH 4/7] Move templates to new phx.gen.live.slime directory. --- .../{phx.gen.live => phx.gen.live.slime}/form_component.ex | 0 .../{phx.gen.live => phx.gen.live.slime}/form_component.html.leex | 0 priv/templates/{phx.gen.live => phx.gen.live.slime}/index.ex | 0 .../{phx.gen.live => phx.gen.live.slime}/index.html.leex | 0 .../{phx.gen.live => phx.gen.live.slime}/live_helpers.ex | 0 priv/templates/{phx.gen.live => phx.gen.live.slime}/live_test.exs | 0 .../{phx.gen.live => phx.gen.live.slime}/modal_component.ex | 0 priv/templates/{phx.gen.live => phx.gen.live.slime}/show.ex | 0 .../templates/{phx.gen.live => phx.gen.live.slime}/show.html.leex | 0 9 files changed, 0 insertions(+), 0 deletions(-) rename priv/templates/{phx.gen.live => phx.gen.live.slime}/form_component.ex (100%) rename priv/templates/{phx.gen.live => phx.gen.live.slime}/form_component.html.leex (100%) rename priv/templates/{phx.gen.live => phx.gen.live.slime}/index.ex (100%) rename priv/templates/{phx.gen.live => phx.gen.live.slime}/index.html.leex (100%) rename priv/templates/{phx.gen.live => phx.gen.live.slime}/live_helpers.ex (100%) rename priv/templates/{phx.gen.live => phx.gen.live.slime}/live_test.exs (100%) rename priv/templates/{phx.gen.live => phx.gen.live.slime}/modal_component.ex (100%) rename priv/templates/{phx.gen.live => phx.gen.live.slime}/show.ex (100%) rename priv/templates/{phx.gen.live => phx.gen.live.slime}/show.html.leex (100%) diff --git a/priv/templates/phx.gen.live/form_component.ex b/priv/templates/phx.gen.live.slime/form_component.ex similarity index 100% rename from priv/templates/phx.gen.live/form_component.ex rename to priv/templates/phx.gen.live.slime/form_component.ex diff --git a/priv/templates/phx.gen.live/form_component.html.leex b/priv/templates/phx.gen.live.slime/form_component.html.leex similarity index 100% rename from priv/templates/phx.gen.live/form_component.html.leex rename to priv/templates/phx.gen.live.slime/form_component.html.leex diff --git a/priv/templates/phx.gen.live/index.ex b/priv/templates/phx.gen.live.slime/index.ex similarity index 100% rename from priv/templates/phx.gen.live/index.ex rename to priv/templates/phx.gen.live.slime/index.ex diff --git a/priv/templates/phx.gen.live/index.html.leex b/priv/templates/phx.gen.live.slime/index.html.leex similarity index 100% rename from priv/templates/phx.gen.live/index.html.leex rename to priv/templates/phx.gen.live.slime/index.html.leex diff --git a/priv/templates/phx.gen.live/live_helpers.ex b/priv/templates/phx.gen.live.slime/live_helpers.ex similarity index 100% rename from priv/templates/phx.gen.live/live_helpers.ex rename to priv/templates/phx.gen.live.slime/live_helpers.ex diff --git a/priv/templates/phx.gen.live/live_test.exs b/priv/templates/phx.gen.live.slime/live_test.exs similarity index 100% rename from priv/templates/phx.gen.live/live_test.exs rename to priv/templates/phx.gen.live.slime/live_test.exs diff --git a/priv/templates/phx.gen.live/modal_component.ex b/priv/templates/phx.gen.live.slime/modal_component.ex similarity index 100% rename from priv/templates/phx.gen.live/modal_component.ex rename to priv/templates/phx.gen.live.slime/modal_component.ex diff --git a/priv/templates/phx.gen.live/show.ex b/priv/templates/phx.gen.live.slime/show.ex similarity index 100% rename from priv/templates/phx.gen.live/show.ex rename to priv/templates/phx.gen.live.slime/show.ex diff --git a/priv/templates/phx.gen.live/show.html.leex b/priv/templates/phx.gen.live.slime/show.html.leex similarity index 100% rename from priv/templates/phx.gen.live/show.html.leex rename to priv/templates/phx.gen.live.slime/show.html.leex From 13915a9a2657137f954c8f4c1346ec00a4f9b8d9 Mon Sep 17 00:00:00 2001 From: Jason Crossfield Date: Wed, 18 Nov 2020 02:06:04 -0500 Subject: [PATCH 5/7] Bug fix missing method call on Mix.Phoenix.Schema.live_form_value in live_test.exs live testing template. --- .../templates/phx.gen.live.slime/live_test.exs | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/priv/templates/phx.gen.live.slime/live_test.exs b/priv/templates/phx.gen.live.slime/live_test.exs index 55110f3..9fccb0c 100644 --- a/priv/templates/phx.gen.live.slime/live_test.exs +++ b/priv/templates/phx.gen.live.slime/live_test.exs @@ -2,21 +2,27 @@ defmodule <%= inspect context.web_module %>.<%= inspect Module.concat(schema.web use <%= inspect context.web_module %>.ConnCase import Phoenix.LiveViewTest - import <%= inspect context.module %>Fixtures - @create_attrs <%= Mix.Phoenix.to_text for {key, value} <- schema.params.create, into: %{}, do: {key, Mix.Phoenix.Schema.live_form_value(value)} %> - @update_attrs <%= Mix.Phoenix.to_text for {key, value} <- schema.params.update, into: %{}, do: {key, Mix.Phoenix.Schema.live_form_value(value)} %> - @invalid_attrs <%= Mix.Phoenix.to_text for {key, value} <- schema.params.create, into: %{}, do: {key, value |> Mix.Phoenix.Schema.live_form_value() |> Mix.Phoenix.Schema.invalid_form_value()} %> + alias <%= inspect context.module %> + + @create_attrs <%= inspect schema.params.create %> + @update_attrs <%= inspect schema.params.update %> + @invalid_attrs <%= inspect for {key, _} <- schema.params.create, into: %{}, do: {key, nil} %> + + defp fixture(:<%= schema.singular %>) do + {:ok, <%= schema.singular %>} = <%= inspect context.alias %>.create_<%= schema.singular %>(@create_attrs) + <%= schema.singular %> + end defp create_<%= schema.singular %>(_) do - <%= schema.singular %> = <%= schema.singular %>_fixture() + <%= schema.singular %> = fixture(:<%= schema.singular %>) %{<%= schema.singular %>: <%= schema.singular %>} end describe "Index" do setup [:create_<%= schema.singular %>] - test "lists all <%= schema.plural %>", <%= if schema.string_attr do %>%{conn: conn, <%= schema.singular %>: <%= schema.singular %>}<% else %>%{conn: conn}<% end %> do + test "lists all <%= schema.plural %>", %{conn: conn, <%= schema.singular %>: <%= schema.singular %>} do {:ok, _index_live, html} = live(conn, Routes.<%= schema.route_helper %>_index_path(conn, :index)) assert html =~ "Listing <%= schema.human_plural %>"<%= if schema.string_attr do %> From 22f935a52b99607802747ea8ce31c0d7862815a9 Mon Sep 17 00:00:00 2001 From: Jason Crossfield Date: Wed, 18 Nov 2020 02:12:13 -0500 Subject: [PATCH 6/7] Fix indentation issues on form_component and index live slime templates. --- .../phx.gen.live.slime/form_component.html.leex | 2 +- .../templates/phx.gen.live.slime/index.html.leex | 16 ++++++++-------- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/priv/templates/phx.gen.live.slime/form_component.html.leex b/priv/templates/phx.gen.live.slime/form_component.html.leex index 2e5bb71..76a69a0 100644 --- a/priv/templates/phx.gen.live.slime/form_component.html.leex +++ b/priv/templates/phx.gen.live.slime/form_component.html.leex @@ -12,4 +12,4 @@ h2 = @title <%= error %> <% end %> - = submit "Save", phx_disable_with: "Saving..." + = submit "Save", phx_disable_with: "Saving..." diff --git a/priv/templates/phx.gen.live.slime/index.html.leex b/priv/templates/phx.gen.live.slime/index.html.leex index 4983001..ee30474 100644 --- a/priv/templates/phx.gen.live.slime/index.html.leex +++ b/priv/templates/phx.gen.live.slime/index.html.leex @@ -17,14 +17,14 @@ table th tbody id="<%= schema.plural %>" = for <%= schema.singular %> <- @<%= schema.collection %> do - tr id="<%= schema.singular %>-#{<%= schema.singular %>.id}" - <%= for {k, _} <- schema.attrs do %> - td = <%= schema.singular %>.<%= k %> - <% end %> - td - span = live_redirect "Show", to: Routes.<%= schema.route_helper %>_show_path(@socket, :show, <%= schema.singular %>) - span = live_patch "Edit", to: Routes.<%= schema.route_helper %>_index_path(@socket, :edit, <%= schema.singular %>) - span = link "Delete", to: "#", phx_click: "delete", phx_value_id: <%= schema.singular %>.id, data: [confirm: "Are you sure?"] + tr id="<%= schema.singular %>-#{<%= schema.singular %>.id}" + <%= for {k, _} <- schema.attrs do %> + td = <%= schema.singular %>.<%= k %> + <% end %> + td + span = live_redirect "Show", to: Routes.<%= schema.route_helper %>_show_path(@socket, :show, <%= schema.singular %>) + span = live_patch "Edit", to: Routes.<%= schema.route_helper %>_index_path(@socket, :edit, <%= schema.singular %>) + span = link "Delete", to: "#", phx_click: "delete", phx_value_id: <%= schema.singular %>.id, data: [confirm: "Are you sure?"] span = live_patch "New <%= schema.human_singular %>", to: Routes.<%= schema.route_helper %>_index_path(@socket, :new) From da7afde3415ae8563d46db9dd98e23c98a7b223f Mon Sep 17 00:00:00 2001 From: Jason Crossfield Date: Wed, 18 Nov 2020 03:13:27 -0500 Subject: [PATCH 7/7] Bump phoenix package version to ~> 1.5 --- mix.exs | 2 +- mix.lock | 30 +++++++++++++++--------------- 2 files changed, 16 insertions(+), 16 deletions(-) diff --git a/mix.exs b/mix.exs index ac3d24f..650d46e 100644 --- a/mix.exs +++ b/mix.exs @@ -20,7 +20,7 @@ defmodule PhoenixSlime.Mixfile do def deps do [ - {:phoenix, "~> 1.4"}, + {:phoenix, "~> 1.5"}, {:phoenix_html, "~> 2.13"}, {:jason, "~> 1.0", optional: true}, {:slime, "~> 1.0"}, diff --git a/mix.lock b/mix.lock index b3775d9..144e5ad 100644 --- a/mix.lock +++ b/mix.lock @@ -1,17 +1,17 @@ %{ - "earmark": {:hex, :earmark, "1.4.3", "364ca2e9710f6bff494117dbbd53880d84bebb692dafc3a78eb50aa3183f2bfd", [:mix], [], "hexpm"}, - "ex_doc": {:hex, :ex_doc, "0.21.2", "caca5bc28ed7b3bdc0b662f8afe2bee1eedb5c3cf7b322feeeb7c6ebbde089d6", [:mix], [{:earmark, "~> 1.3.3 or ~> 1.4", [hex: :earmark, repo: "hexpm", optional: false]}, {:makeup_elixir, "~> 0.14", [hex: :makeup_elixir, repo: "hexpm", optional: false]}], "hexpm"}, - "jason": {:hex, :jason, "1.1.2", "b03dedea67a99223a2eaf9f1264ce37154564de899fd3d8b9a21b1a6fd64afe7", [:mix], [{:decimal, "~> 1.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm"}, - "makeup": {:hex, :makeup, "1.0.0", "671df94cf5a594b739ce03b0d0316aa64312cee2574b6a44becb83cd90fb05dc", [:mix], [{:nimble_parsec, "~> 0.5.0", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm"}, - "makeup_elixir": {:hex, :makeup_elixir, "0.14.0", "cf8b7c66ad1cff4c14679698d532f0b5d45a3968ffbcbfd590339cb57742f1ae", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}], "hexpm"}, - "mime": {:hex, :mime, "1.3.1", "30ce04ab3175b6ad0bdce0035cba77bba68b813d523d1aac73d9781b4d193cf8", [:mix], [], "hexpm"}, - "neotoma": {:hex, :neotoma, "1.7.3", "d8bd5404b73273989946e4f4f6d529e5c2088f5fa1ca790b4dbe81f4be408e61", [:rebar], [], "hexpm"}, - "nimble_parsec": {:hex, :nimble_parsec, "0.5.2", "1d71150d5293d703a9c38d4329da57d3935faed2031d64bc19e77b654ef2d177", [:mix], [], "hexpm"}, - "phoenix": {:hex, :phoenix, "1.4.11", "d112c862f6959f98e6e915c3b76c7a87ca3efd075850c8daa7c3c7a609014b0d", [:mix], [{:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:phoenix_pubsub, "~> 1.1", [hex: :phoenix_pubsub, repo: "hexpm", optional: false]}, {:plug, "~> 1.8.1 or ~> 1.9", [hex: :plug, repo: "hexpm", optional: false]}, {:plug_cowboy, "~> 1.0 or ~> 2.0", [hex: :plug_cowboy, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm"}, - "phoenix_html": {:hex, :phoenix_html, "2.13.3", "850e292ff6e204257f5f9c4c54a8cb1f6fbc16ed53d360c2b780a3d0ba333867", [:mix], [{:plug, "~> 1.5", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm"}, - "phoenix_pubsub": {:hex, :phoenix_pubsub, "1.1.2", "496c303bdf1b2e98a9d26e89af5bba3ab487ba3a3735f74bf1f4064d2a845a3e", [:mix], [], "hexpm"}, - "plug": {:hex, :plug, "1.8.3", "12d5f9796dc72e8ac9614e94bda5e51c4c028d0d428e9297650d09e15a684478", [:mix], [{:mime, "~> 1.0", [hex: :mime, repo: "hexpm", optional: false]}, {:plug_crypto, "~> 1.0", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4", [hex: :telemetry, repo: "hexpm", optional: true]}], "hexpm"}, - "plug_crypto": {:hex, :plug_crypto, "1.0.0", "18e49317d3fa343f24620ed22795ec29d4a5e602d52d1513ccea0b07d8ea7d4d", [:mix], [], "hexpm"}, - "slime": {:hex, :slime, "1.2.1", "71e036056051f0a6fae136af34eaa1322e8e11cdd2da3a56196fd31bca34dd49", [:mix], [{:neotoma, "~> 1.7", [hex: :neotoma, repo: "hexpm", optional: false]}], "hexpm"}, - "telemetry": {:hex, :telemetry, "0.4.1", "ae2718484892448a24470e6aa341bc847c3277bfb8d4e9289f7474d752c09c7f", [:rebar3], [], "hexpm"}, + "earmark": {:hex, :earmark, "1.4.3", "364ca2e9710f6bff494117dbbd53880d84bebb692dafc3a78eb50aa3183f2bfd", [:mix], [], "hexpm", "8cf8a291ebf1c7b9539e3cddb19e9cef066c2441b1640f13c34c1d3cfc825fec"}, + "ex_doc": {:hex, :ex_doc, "0.21.2", "caca5bc28ed7b3bdc0b662f8afe2bee1eedb5c3cf7b322feeeb7c6ebbde089d6", [:mix], [{:earmark, "~> 1.3.3 or ~> 1.4", [hex: :earmark, repo: "hexpm", optional: false]}, {:makeup_elixir, "~> 0.14", [hex: :makeup_elixir, repo: "hexpm", optional: false]}], "hexpm", "f1155337ae17ff7a1255217b4c1ceefcd1860b7ceb1a1874031e7a861b052e39"}, + "jason": {:hex, :jason, "1.2.2", "ba43e3f2709fd1aa1dce90aaabfd039d000469c05c56f0b8e31978e03fa39052", [:mix], [{:decimal, "~> 1.0 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "18a228f5f0058ee183f29f9eae0805c6e59d61c3b006760668d8d18ff0d12179"}, + "makeup": {:hex, :makeup, "1.0.0", "671df94cf5a594b739ce03b0d0316aa64312cee2574b6a44becb83cd90fb05dc", [:mix], [{:nimble_parsec, "~> 0.5.0", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "a10c6eb62cca416019663129699769f0c2ccf39428b3bb3c0cb38c718a0c186d"}, + "makeup_elixir": {:hex, :makeup_elixir, "0.14.0", "cf8b7c66ad1cff4c14679698d532f0b5d45a3968ffbcbfd590339cb57742f1ae", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}], "hexpm", "d4b316c7222a85bbaa2fd7c6e90e37e953257ad196dc229505137c5e505e9eff"}, + "mime": {:hex, :mime, "1.4.0", "5066f14944b470286146047d2f73518cf5cca82f8e4815cf35d196b58cf07c47", [:mix], [], "hexpm", "75fa42c4228ea9a23f70f123c74ba7cece6a03b1fd474fe13f6a7a85c6ea4ff6"}, + "neotoma": {:hex, :neotoma, "1.7.3", "d8bd5404b73273989946e4f4f6d529e5c2088f5fa1ca790b4dbe81f4be408e61", [:rebar], [], "hexpm", "2da322b9b1567ffa0706a7f30f6bbbde70835ae44a1050615f4b4a3d436e0f28"}, + "nimble_parsec": {:hex, :nimble_parsec, "0.5.2", "1d71150d5293d703a9c38d4329da57d3935faed2031d64bc19e77b654ef2d177", [:mix], [], "hexpm", "51aa192e0941313c394956718bdb1e59325874f88f45871cff90345b97f60bba"}, + "phoenix": {:hex, :phoenix, "1.5.6", "8298cdb4e0f943242ba8410780a6a69cbbe972fef199b341a36898dd751bdd66", [:mix], [{:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:phoenix_html, "~> 2.13", [hex: :phoenix_html, repo: "hexpm", optional: true]}, {:phoenix_pubsub, "~> 2.0", [hex: :phoenix_pubsub, repo: "hexpm", optional: false]}, {:plug, "~> 1.10", [hex: :plug, repo: "hexpm", optional: false]}, {:plug_cowboy, "~> 1.0 or ~> 2.2", [hex: :plug_cowboy, repo: "hexpm", optional: true]}, {:plug_crypto, "~> 1.1.2 or ~> 1.2", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "0dc4d39af1306b6aa5122729b0a95ca779e42c708c6fe7abbb3d336d5379e956"}, + "phoenix_html": {:hex, :phoenix_html, "2.13.3", "850e292ff6e204257f5f9c4c54a8cb1f6fbc16ed53d360c2b780a3d0ba333867", [:mix], [{:plug, "~> 1.5", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "8b01b3d6d39731ab18aa548d928b5796166d2500755f553725cfe967bafba7d9"}, + "phoenix_pubsub": {:hex, :phoenix_pubsub, "2.0.0", "a1ae76717bb168cdeb10ec9d92d1480fec99e3080f011402c0a2d68d47395ffb", [:mix], [], "hexpm", "c52d948c4f261577b9c6fa804be91884b381a7f8f18450c5045975435350f771"}, + "plug": {:hex, :plug, "1.11.0", "f17217525597628298998bc3baed9f8ea1fa3f1160aa9871aee6df47a6e4d38e", [:mix], [{:mime, "~> 1.0", [hex: :mime, repo: "hexpm", optional: false]}, {:plug_crypto, "~> 1.1.1 or ~> 1.2", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "2d9c633f0499f9dc5c2fd069161af4e2e7756890b81adcbb2ceaa074e8308876"}, + "plug_crypto": {:hex, :plug_crypto, "1.2.0", "1cb20793aa63a6c619dd18bb33d7a3aa94818e5fd39ad357051a67f26dfa2df6", [:mix], [], "hexpm", "a48b538ae8bf381ffac344520755f3007cc10bd8e90b240af98ea29b69683fc2"}, + "slime": {:hex, :slime, "1.2.1", "71e036056051f0a6fae136af34eaa1322e8e11cdd2da3a56196fd31bca34dd49", [:mix], [{:neotoma, "~> 1.7", [hex: :neotoma, repo: "hexpm", optional: false]}], "hexpm", "298568e64291fed4eb690be094f6c46400daa03b594bab34fcaa0167e139c263"}, + "telemetry": {:hex, :telemetry, "0.4.2", "2808c992455e08d6177322f14d3bdb6b625fbcfd233a73505870d8738a2f4599", [:rebar3], [], "hexpm", "2d1419bd9dda6a206d7b5852179511722e2b18812310d304620c7bd92a13fcef"}, }