diff --git a/CHANGELOG.md b/CHANGELOG.md index 995099a8..38f83dac 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,19 @@ # Changelog +## 0.1.0-rc.1 + +### Enhancements + * Added Shared Event Handlers which are global event handlers shared among all pages. + That's a simple model to work with where a layout, component, or multiple pages may share the same event handler, + for example a newsletter subscription form in a component called in a layout doesn't need to duplicate the same + event handler in all pages. + +### Breaking Changes + * Remove Page Event Handlers in favor of Shared Event Handlers. + With Shared Event Handlers, it doesn't make sense to have page event handlers unless overriding becomes a neccessity. + The data is automatically migrated in a best-effort way, duplicated event handler names (from multiple pages) are + consolidated into a single shared event handler. See the migration `V002` for more info. + ## 0.1.0-rc.0 (2024-08-02) ### Enhancements diff --git a/lib/beacon/boot.ex b/lib/beacon/boot.ex index c85cb656..208fbc33 100644 --- a/lib/beacon/boot.ex +++ b/lib/beacon/boot.ex @@ -55,7 +55,8 @@ defmodule Beacon.Boot do Task.Supervisor.async(task_supervisor, fn -> Beacon.Loader.reload_live_data_module(config.site) end), Task.Supervisor.async(task_supervisor, fn -> Beacon.Loader.reload_layouts_modules(config.site) end), Task.Supervisor.async(task_supervisor, fn -> Beacon.Loader.reload_error_page_module(config.site) end), - Task.Supervisor.async(task_supervisor, fn -> Beacon.Loader.reload_pages_modules(config.site, per_page: 20) end) + Task.Supervisor.async(task_supervisor, fn -> Beacon.Loader.reload_pages_modules(config.site, per_page: 20) end), + Task.Supervisor.async(task_supervisor, fn -> Beacon.Loader.reload_event_handlers_module(config.site) end) # TODO: load main pages (order_by: path, per_page: 10) to avoid SEO issues ] diff --git a/lib/beacon/content.ex b/lib/beacon/content.ex index 652d5f21..a25cd795 100644 --- a/lib/beacon/content.ex +++ b/lib/beacon/content.ex @@ -35,6 +35,7 @@ defmodule Beacon.Content do alias Beacon.Content.ComponentSlot alias Beacon.Content.ComponentSlotAttr alias Beacon.Content.ErrorPage + alias Beacon.Content.EventHandler alias Beacon.Content.Layout alias Beacon.Content.LayoutEvent alias Beacon.Content.LayoutSnapshot @@ -42,7 +43,6 @@ defmodule Beacon.Content do alias Beacon.Content.LiveDataAssign alias Beacon.Content.Page alias Beacon.Content.PageEvent - alias Beacon.Content.PageEventHandler alias Beacon.Content.PageField alias Beacon.Content.PageSnapshot alias Beacon.Content.PageVariant @@ -702,7 +702,7 @@ defmodule Beacon.Content do @doc false def create_page_snapshot(page, event) do - page = repo(page).preload(page, [:variants, :event_handlers]) + page = repo(page).preload(page, :variants) attrs = %{ "site" => page.site, @@ -1025,14 +1025,14 @@ defmodule Beacon.Content do defp extract_page_snapshot(%{schema_version: 1, page: %Page{} = page}) do page |> repo(page).reload() - |> repo(page).preload([:variants, :event_handlers], force: true) + |> repo(page).preload([:variants], force: true) |> maybe_add_leading_slash() end defp extract_page_snapshot(%{schema_version: 2, page: %Page{} = page}) do page |> repo(page).reload() - |> repo(page).preload([:variants, :event_handlers], force: true) + |> repo(page).preload([:variants], force: true) |> maybe_add_leading_slash() end @@ -3521,77 +3521,73 @@ defmodule Beacon.Content do ## Example - iex> change_page_event_handler(page_event_handler, %{name: "form-submit"}) - %Ecto.Changeset{data: %PageEventHandler{}} + iex> change_event_handler(event_handler, %{name: "form-submit"}) + %Ecto.Changeset{data: %EventHandler{}} """ - @doc type: :page_event_handlers - @spec change_page_event_handler(PageEventHandler.t(), map()) :: Changeset.t() - def change_page_event_handler(%PageEventHandler{} = event_handler, attrs \\ %{}) do - PageEventHandler.changeset(event_handler, attrs) + @doc type: :event_handlers + @spec change_event_handler(EventHandler.t(), map()) :: Changeset.t() + def change_event_handler(%EventHandler{} = event_handler, attrs \\ %{}) do + EventHandler.changeset(event_handler, attrs) end @doc """ - Creates a new page event handler and returns the page with updated `:event_handlers` association. + Lists all event handlers for a given Beacon site. """ - @doc type: :page_event_handlers - @spec create_event_handler_for_page(Page.t(), %{name: binary(), code: binary()}) :: {:ok, Page.t()} | {:error, Changeset.t()} - def create_event_handler_for_page(page, attrs) do - changeset = - page - |> Ecto.build_assoc(:event_handlers) - |> PageEventHandler.changeset(attrs) - |> validate_page_event_handler(page) - - transact(repo(page), fn -> - with {:ok, %PageEventHandler{}} <- repo(page).insert(changeset), - %Page{} = page <- repo(page).preload(page, :event_handlers, force: true), - %Page{} = page <- Lifecycle.Page.after_update_page(page) do - {:ok, page} - end - end) + @spec list_event_handlers(Site.t()) :: [EventHandler.t()] + def list_event_handlers(site) do + repo(site).all(from eh in EventHandler, where: [site: ^site]) end @doc """ - Updates a page event handler and returns the page with updated `:event_handlers` association. + Creates a new event handler. """ - @doc type: :page_event_handlers - @spec update_event_handler_for_page(Page.t(), PageEventHandler.t(), map()) :: {:ok, Page.t()} | {:error, Changeset.t()} - def update_event_handler_for_page(page, event_handler, attrs) do + @doc type: :event_handlers + @spec create_event_handler(%{name: binary(), code: binary(), site: Site.t()}) :: + {:ok, EventHandler.t()} | {:error, Changeset.t()} + def create_event_handler(attrs) do changeset = - event_handler - |> PageEventHandler.changeset(attrs) - |> validate_page_event_handler(page) + %EventHandler{} + |> EventHandler.changeset(attrs) + |> validate_event_handler() - transact(repo(page), fn -> - with {:ok, %PageEventHandler{}} <- repo(page).update(changeset), - %Page{} = page <- repo(page).preload(page, :event_handlers, force: true), - %Page{} = page <- Lifecycle.Page.after_update_page(page) do - {:ok, page} - end - end) + site = Changeset.get_field(changeset, :site) + + changeset + |> repo(site).insert() + |> tap(&maybe_broadcast_updated_content_event(&1, :event_handler)) + end + + @doc """ + Updates an event handler with the given attrs. + """ + @doc type: :event_handlers + @spec update_event_handler(EventHandler.t(), map()) :: {:ok, EventHandler.t()} | {:error, Changeset.t()} + def update_event_handler(event_handler, attrs) do + event_handler + |> EventHandler.changeset(attrs) + |> validate_event_handler() + |> repo(event_handler).update() + |> tap(&maybe_broadcast_updated_content_event(&1, :event_handler)) end - defp validate_page_event_handler(changeset, page) do + defp validate_event_handler(changeset) do code = Changeset.get_field(changeset, :code) - metadata = %Beacon.Template.LoadMetadata{site: page.site, path: page.path} variable_names = ["socket", "event_params"] imports = ["Phoenix.Socket"] - do_validate_template(changeset, :code, :elixir, code, metadata, variable_names, imports) + do_validate_template(changeset, :code, :elixir, code, nil, variable_names, imports) end @doc """ - Deletes a page event handler and returns the page with updated `:event_handlers` association. + Deletes an event handler. """ - @doc type: :page_event_handlers - @spec delete_event_handler_from_page(Page.t(), PageEventHandler.t()) :: {:ok, Page.t()} | {:error, Changeset.t()} - def delete_event_handler_from_page(page, event_handler) do - with {:ok, %PageEventHandler{}} <- repo(page).delete(event_handler), - %Page{} = page <- repo(page).preload(page, :event_handlers, force: true), - %Page{} = page <- Lifecycle.Page.after_update_page(page) do - {:ok, page} - end + @doc type: :event_handlers + @spec delete_event_handler(EventHandler.t()) :: {:ok, EventHandler.t()} | {:error, Changeset.t()} + def delete_event_handler(event_handler) do + event_handler + |> repo(event_handler).delete() + |> tap(&maybe_broadcast_updated_content_event(&1, :event_handler)) end # PAGE VARIANTS diff --git a/lib/beacon/content/page_event_handler.ex b/lib/beacon/content/event_handler.ex similarity index 82% rename from lib/beacon/content/page_event_handler.ex rename to lib/beacon/content/event_handler.ex index 5b290fa7..67990c7e 100644 --- a/lib/beacon/content/page_event_handler.ex +++ b/lib/beacon/content/event_handler.ex @@ -1,4 +1,4 @@ -defmodule Beacon.Content.PageEventHandler do +defmodule Beacon.Content.EventHandler do @moduledoc """ Beacon's representation of a LiveView [handle_event/3](https://hexdocs.pm/phoenix_live_view/Phoenix.LiveView.html#c:handle_event/3). @@ -17,31 +17,29 @@ defmodule Beacon.Content.PageEventHandler do import Ecto.Changeset - alias Beacon.Content.Page + alias Beacon.Types.Site alias Ecto.UUID @type t :: %__MODULE__{ id: UUID.t(), name: binary(), code: binary(), - page_id: UUID.t(), - page: Page.t(), + site: Site.t(), inserted_at: DateTime.t(), updated_at: DateTime.t() } - schema "beacon_page_event_handlers" do + schema "beacon_event_handlers" do field :name, :string field :code, :string - - belongs_to :page, Page + field :site, Site timestamps() end @doc false def changeset(%__MODULE__{} = event_handler, attrs) do - fields = ~w(name code)a + fields = ~w(name code site)a event_handler |> cast(attrs, fields) diff --git a/lib/beacon/content/live_data_assign.ex b/lib/beacon/content/live_data_assign.ex index e5763d91..004d8409 100644 --- a/lib/beacon/content/live_data_assign.ex +++ b/lib/beacon/content/live_data_assign.ex @@ -1,6 +1,6 @@ defmodule Beacon.Content.LiveDataAssign do @moduledoc """ - Dynamic key/value assigns to be used by `Beacon.Content.Page` templates and updated with `Beacon.Content.PageEventHandler`s. + Dynamic key/value assigns to be used by `Beacon.Content.Page` templates and updated with `Beacon.Content.EventHandler`s. LiveDataAssigns don't exist on their own, but exist as part of a `Beacon.Content.LiveData` struct. diff --git a/lib/beacon/content/page.ex b/lib/beacon/content/page.ex index b4f67919..ba199884 100644 --- a/lib/beacon/content/page.ex +++ b/lib/beacon/content/page.ex @@ -45,7 +45,6 @@ defmodule Beacon.Content.Page do belongs_to :layout, Content.Layout has_many :variants, Content.PageVariant - has_many :event_handlers, Content.PageEventHandler embeds_many :helpers, Helper diff --git a/lib/beacon/loader.ex b/lib/beacon/loader.ex index 846668ef..dd019bed 100644 --- a/lib/beacon/loader.ex +++ b/lib/beacon/loader.ex @@ -141,6 +141,10 @@ defmodule Beacon.Loader do Loader.Stylesheet.module_name(site) end + def fetch_event_handlers_module(site) do + Loader.EventHandlers.module_name(site) + end + def fetch_layouts_modules(site) do Enum.map(Content.list_published_layouts(site), fn layout -> fetch_layout_module(layout.site, layout.id) @@ -189,6 +193,10 @@ defmodule Beacon.Loader do GenServer.call(worker(site), :reload_stylesheet_module, @timeout) end + def reload_event_handlers_module(site) do + GenServer.call(worker(site), :reload_event_handlers_module, @timeout) + end + def reload_layouts_modules(site) do Enum.map(Content.list_published_layouts(site), &reload_layout_module(&1.site, &1.id)) end @@ -288,6 +296,11 @@ defmodule Beacon.Loader do {:noreply, config} end + def handle_info({:content_updated, :event_handler, %{site: site}}, config) do + reload_event_handlers_module(site) + {:noreply, config} + end + def handle_info(msg, config) do raise inspect(msg) Logger.warning("Beacon.Loader can not handle the message: #{inspect(msg)}") diff --git a/lib/beacon/loader/event_handlers.ex b/lib/beacon/loader/event_handlers.ex new file mode 100644 index 00000000..fdaebeeb --- /dev/null +++ b/lib/beacon/loader/event_handlers.ex @@ -0,0 +1,30 @@ +defmodule Beacon.Loader.EventHandlers do + @moduledoc false + alias Beacon.Loader + + def module_name(site), do: Loader.module_name(site, "EventHandlers") + + def build_ast(site, event_handlers) do + module = module_name(site) + functions = Enum.map(event_handlers, &build_fn/1) + + quote do + defmodule unquote(module) do + import Beacon.Web, only: [assign: 2, assign: 3, assign_new: 3] + + (unquote_splicing(functions)) + end + end + end + + defp build_fn(event_handler) do + %{site: site, name: name, code: code} = event_handler + Beacon.safe_code_check!(site, code) + + quote do + def handle_event(unquote(name), var!(event_params), var!(socket)) do + unquote(Code.string_to_quoted!(code)) + end + end + end +end diff --git a/lib/beacon/loader/page.ex b/lib/beacon/loader/page.ex index e8262cdf..9d5a934e 100644 --- a/lib/beacon/loader/page.ex +++ b/lib/beacon/loader/page.ex @@ -1,11 +1,12 @@ defmodule Beacon.Loader.Page do @moduledoc false - - require Logger + alias Beacon.Content alias Beacon.Lifecycle alias Beacon.Loader alias Beacon.Template.HEEx + require Logger + def module_name(site, page_id), do: Loader.module_name(site, "Page#{page_id}") def build_ast(site, page) do @@ -96,7 +97,7 @@ defmodule Beacon.Loader.Page do defp interpolate_raw_schema_record(schema, page) when is_map(schema) do render = fn key, value, page -> - case Beacon.Content.render_snippet(value, %{page: page, live_data: %{}}) do + case Content.render_snippet(value, %{page: page, live_data: %{}}) do {:ok, new_value} -> {key, new_value} @@ -124,10 +125,10 @@ defmodule Beacon.Loader.Page do end defp handle_event(page) do - %{site: site, event_handlers: event_handlers} = page + event_handlers = Content.list_event_handlers(page.site) Enum.map(event_handlers, fn event_handler -> - Beacon.safe_code_check!(site, event_handler.code) + Beacon.safe_code_check!(page.site, event_handler.code) quote do def handle_event(unquote(event_handler.name), var!(event_params), var!(socket)) do diff --git a/lib/beacon/loader/worker.ex b/lib/beacon/loader/worker.ex index fcb67466..e1b21a13 100644 --- a/lib/beacon/loader/worker.ex +++ b/lib/beacon/loader/worker.ex @@ -381,6 +381,14 @@ defmodule Beacon.Loader.Worker do stop(result, config) end + def handle_call(:reload_event_handlers_module, _from, config) do + %{site: site} = config + event_handlers = Content.list_event_handlers(site) + ast = Loader.EventHandlers.build_ast(site, event_handlers) + result = compile_module(site, ast, "event_handlers") + stop(result, config) + end + def handle_call({:reload_layout_module, layout_id}, _from, config) do %{site: site} = config layout = Beacon.Content.get_published_layout(site, layout_id) diff --git a/lib/beacon/migration.ex b/lib/beacon/migration.ex index 379240bb..f2042dc1 100644 --- a/lib/beacon/migration.ex +++ b/lib/beacon/migration.ex @@ -7,10 +7,10 @@ defmodule Beacon.Migration do To install Beacon, you'll need to generate an `Ecto.Migration` that wraps calls to `Beacon.Migration`: ``` - mix ecto.gen.migration create_beacon_tables + $ mix ecto.gen.migration create_beacon_tables ``` - Open the generated migration in your editor and either call or delegate to `up/0` and `down/0`: + Open the generated migration in your editor and either call or delegate to `up/1` and `down/1`: ```elixir defmodule MyApp.Repo.Migrations.CreateBeaconTables do @@ -23,30 +23,96 @@ defmodule Beacon.Migration do Then, run the migrations for your app to create the necessary Beacon tables in your database: ``` - mix ecto.migrate + $ mix ecto.migrate ``` - Note that `up/0` will always execute all migration steps from the initial version to the latest version, - and those migration are idempotent. + By calling `up()` with no arguments, this will execute all migration steps from the initial version to + the latest version. As new versions are released, you may need to repeat this process, by first + generating a new migration: - Check out the [your first site](https://hexdocs.pm/beacon/your-first-site.html) guide for a full example. + ``` + $ mix ecto.gen.migration upgrade_beacon_tables_to_v2 + ``` + Then in the generated migration, you could simply call `up()` again, because the migrations are + idempotent, but you can be safer and more efficient by specifying the migration version to execute: + + ```elixir + defmodule MyApp.Repo.Migrations.UpgradeBeaconTables do + use Ecto.Migration + def up, do: Beacon.Migration.up(version: 2) + def down, do: Beacon.Migration.down(version: 2) + end + ``` + + Now this migration will update to v2, but if rolled back, will only roll back the v2 changes, + leaving v1 tables in-place. + + To see this step within the larger context of installing Beacon, check out the [your first site](your-first-site.html) guide. """ - # TODO: `up/1` should execute all migrations from v001 up to `@latest` - @latest Beacon.Migrations.V001 + @initial_version 1 + @current_version 2 @doc """ - Run the `up` changes for all migrations between the initial version and the current version. + Upgrades Beacon database schemas. + + If a specific version number is provided, Beacon will only upgrade to that version. + Otherwise, it will bring you fully up-to-date with the current version. + + ## Example + + Run all migrations up to the current version: + + Beacon.Migration.up() + + Run migrations up to a specified version: + + Beacon.Migration.down(version: 2) + """ - def up do - @latest.up() + def up(opts \\ []) do + versions_to_run = + case opts[:version] do + nil -> @initial_version..@current_version//1 + version -> @initial_version..version//1 + end + + Enum.each(versions_to_run, fn version -> + padded = String.pad_leading("#{version}", 3, "0") + module = Module.concat([Beacon.Migrations, "V#{padded}"]) + module.up() + end) end @doc """ - Run the `down` changes for all migrations between the initial version and the current version. + Downgrades Beacon database schemas. + + If a specific version number is provided, Beacon will only downgrade to that version (inclusive). + Otherwise, it will completely uninstall Beacon from your app's database. + + ## Example + + Run all migrations from current version down to the first: + + Beacon.Migration.down() + + Run migrations down to and including a specified version: + + Beacon.Migration.down(version: 2) + """ - def down do - @latest.down() + def down(opts \\ []) do + versions_to_run = + case opts[:version] do + nil -> @current_version..@initial_version//-1 + version -> @current_version..version//-1 + end + + Enum.each(versions_to_run, fn version -> + padded = String.pad_leading("#{version}", 3, "0") + module = Module.concat([Beacon.Migrations, "V#{padded}"]) + module.down() + end) end end diff --git a/lib/beacon/migrations/v002.ex b/lib/beacon/migrations/v002.ex new file mode 100644 index 00000000..571f6082 --- /dev/null +++ b/lib/beacon/migrations/v002.ex @@ -0,0 +1,54 @@ +defmodule Beacon.Migrations.V002 do + @moduledoc false + use Ecto.Migration + + import Ecto.Query + + def up do + create_if_not_exists table(:beacon_event_handlers, primary_key: false) do + add :id, :binary_id, primary_key: true + add :name, :text, null: false + add :code, :text, null: false + add :site, :text, null: false + + timestamps(type: :utc_datetime_usec) + end + + flush() + + repo().all( + from(peh in "beacon_page_event_handlers", + join: p in "beacon_pages", + on: peh.page_id == p.id, + select: %{site: p.site, name: peh.name, code: peh.code}, + # distinct saves us memory in cases of duplicate code + distinct: true + ) + ) + # we still need to avoid duplicates where the code is different + |> Enum.group_by(&{&1.site, &1.name}, & &1.code) + |> Enum.map(fn {{site, name}, [code | _]} -> + now = DateTime.utc_now() + %{id: Ecto.UUID.generate() |> Ecto.UUID.dump!(), name: name, code: code, site: site, inserted_at: now, updated_at: now} + end) + |> then(&repo().insert_all("beacon_event_handlers", &1, [])) + + drop_if_exists table(:beacon_page_event_handlers) + end + + def down do + create_if_not_exists table(:beacon_page_event_handlers, primary_key: false) do + add :id, :binary_id, primary_key: true + add :name, :text, null: false + add :code, :text, null: false + + add :page_id, references(:beacon_pages, on_delete: :delete_all, type: :binary_id), null: false + + timestamps(type: :utc_datetime_usec) + end + + # global event handlers can't be converted back into page event handlers + + drop_if_exists table(:beacon_event_handlers) + end +end diff --git a/lib/beacon/web/beacon_assigns.ex b/lib/beacon/web/beacon_assigns.ex index 11eb99c6..f757fb49 100644 --- a/lib/beacon/web/beacon_assigns.ex +++ b/lib/beacon/web/beacon_assigns.ex @@ -39,6 +39,7 @@ defmodule Beacon.Web.BeaconAssigns do private: %{ page_module: nil, components_module: nil, + event_handlers_module: nil, live_data_keys: [], live_path: [] } @@ -65,6 +66,7 @@ defmodule Beacon.Web.BeaconAssigns do path_params = Beacon.Router.path_params(page.path, path_info) page_title = Beacon.Web.DataSource.page_title(site, page.id, live_data) components_module = Beacon.Loader.Components.module_name(site) + event_handlers_module = Beacon.Loader.EventHandlers.module_name(site) %__MODULE__{ site: page.site, @@ -74,6 +76,7 @@ defmodule Beacon.Web.BeaconAssigns do private: %{ page_module: page_module, components_module: components_module, + event_handlers_module: event_handlers_module, live_data_keys: Map.keys(live_data), live_path: path_info } diff --git a/lib/beacon/web/live/page_live.ex b/lib/beacon/web/live/page_live.ex index 242dc242..c4b481de 100644 --- a/lib/beacon/web/live/page_live.ex +++ b/lib/beacon/web/live/page_live.ex @@ -64,12 +64,12 @@ defmodule Beacon.Web.PageLive do end def handle_event(event_name, event_params, socket) do - %{beacon: %{private: %{page_module: page_module, live_path: live_path}}} = socket.assigns + %{page_module: page_module, live_path: live_path, event_handlers_module: event_handlers_module} = socket.assigns.beacon.private %{site: site, id: page_id} = Beacon.apply_mfa(page_module, :page_assigns, [[:site, :id]]) result = Beacon.apply_mfa( - page_module, + event_handlers_module, :handle_event, [event_name, event_params, socket], context: %{site: site, page_id: page_id, live_path: live_path} diff --git a/lib/errors.ex b/lib/errors.ex index 5ba00cde..7e666bce 100644 --- a/lib/errors.ex +++ b/lib/errors.ex @@ -83,7 +83,7 @@ end defmodule Beacon.Web.ServerError do @moduledoc """ - Raised when a `Beacon.Content.PageEventHandler` returns an invalid response. + Raised when a `Beacon.Content.EventHandler` returns an invalid response. If you're seeing this error, check the code in your site's event handlers, and ensure that each one returns `{:noreply, socket}`. diff --git a/mix.exs b/mix.exs index 1033cbb7..dd0c9c0a 100644 --- a/mix.exs +++ b/mix.exs @@ -129,7 +129,7 @@ defmodule Beacon.MixProject do "Functions: Stylesheets": &(&1[:type] == :stylesheets), "Functions: Components": &(&1[:type] == :components), "Functions: Snippets": &(&1[:type] == :snippets), - "Functions: Page Event Handlers": &(&1[:type] == :page_event_handlers), + "Functions: Event Handlers": &(&1[:type] == :event_handlers), "Functions: Error Pages": &(&1[:type] == :error_pages), "Functions: Live Data": &(&1[:type] == :live_data) ], @@ -164,6 +164,7 @@ defmodule Beacon.MixProject do Beacon.Content.ComponentSlot, Beacon.Content.ComponentSlotAttr, Beacon.Content.ErrorPage, + Beacon.Content.EventHandler, Beacon.Content.Layout, Beacon.Content.LayoutEvent, Beacon.Content.LayoutSnapshot, @@ -173,7 +174,6 @@ defmodule Beacon.MixProject do Beacon.Content.Page.Event, Beacon.Content.Page.Helper, Beacon.Content.PageEvent, - Beacon.Content.PageEventHandler, Beacon.Content.PageSnapshot, Beacon.Content.PageVariant, Beacon.Content.Stylesheet, diff --git a/test/beacon/content_test.exs b/test/beacon/content_test.exs index 49b1456a..c8a2db1e 100644 --- a/test/beacon/content_test.exs +++ b/test/beacon/content_test.exs @@ -6,13 +6,13 @@ defmodule Beacon.ContentTest do alias Beacon.Content alias Beacon.Content.Component alias Beacon.Content.ErrorPage + alias Beacon.Content.EventHandler alias Beacon.Content.Layout alias Beacon.Content.LayoutEvent alias Beacon.Content.LayoutSnapshot alias Beacon.Content.LiveData alias Beacon.Content.Page alias Beacon.Content.PageEvent - alias Beacon.Content.PageEventHandler alias Beacon.Content.PageSnapshot alias Beacon.Content.PageVariant alias Beacon.BeaconTest.Repo @@ -597,33 +597,29 @@ defmodule Beacon.ContentTest do end describe "event_handlers" do - test "create event handler OK" do - page = page_fixture() - attrs = %{name: "Foo", code: "{:noreply, socket}"} + test "list_event_handlers/1" do + event_handlers = for _ <- 1..3, do: event_handler_fixture(site: :my_site) - assert {:ok, %Page{event_handlers: [event_handler]}} = Content.create_event_handler_for_page(page, attrs) - assert %PageEventHandler{name: "Foo", code: "{:noreply, socket}"} = event_handler - end + result = Content.list_event_handlers(:my_site) - test "create triggers after_update_page lifecycle" do - page = page_fixture(site: :lifecycle_test) - attrs = %{name: "Foo", code: "{:noreply, socket}"} + assert Enum.sort(event_handlers) == Enum.sort(result) + end - {:ok, %Page{}} = Content.create_event_handler_for_page(page, attrs) + test "create event handler OK" do + attrs = %{name: "Foo", code: "{:noreply, socket}", site: :my_site} - assert_receive :lifecycle_after_update_page + assert {:ok, event_handler} = Content.create_event_handler(attrs) + assert %EventHandler{name: "Foo", code: "{:noreply, socket}"} = event_handler end test "create validates elixir code" do - page = page_fixture(%{format: :heex}) - - attrs = %{name: "test", code: "[1)"} - assert {:error, %{errors: [error]}} = Content.create_event_handler_for_page(page, attrs) + attrs = %{name: "test", code: "[1)", site: :my_site} + assert {:error, %{errors: [error]}} = Content.create_event_handler(attrs) {:code, {_, [compilation_error: compilation_error]}} = error assert compilation_error =~ "unexpected token: )" - attrs = %{name: "test", code: "if true, do false"} - assert {:error, %{errors: [error]}} = Content.create_event_handler_for_page(page, attrs) + attrs = %{name: "test", code: "if true, do false", site: :my_site} + assert {:error, %{errors: [error]}} = Content.create_event_handler(attrs) {:code, {_, [compilation_error: compilation_error]}} = error assert compilation_error =~ "unexpected reserved word: do" @@ -633,39 +629,28 @@ defmodule Beacon.ContentTest do {:noreply, assign(socket, res: res)} | - attrs = %{name: "test", code: code} - assert {:ok, _} = Content.create_event_handler_for_page(page, attrs) + attrs = %{name: "test", code: code, site: :my_site} + assert {:ok, _} = Content.create_event_handler(attrs) end test "update event handler OK" do - page = page_fixture(%{format: :heex}) - event_handler = page_event_handler_fixture(%{page: page}) + event_handler = event_handler_fixture() attrs = %{name: "Changed Name", code: "{:noreply, assign(socket, foo: :bar)}"} - assert {:ok, %Page{event_handlers: [updated_event_handler]}} = Content.update_event_handler_for_page(page, event_handler, attrs) - assert %PageEventHandler{name: "Changed Name", code: "{:noreply, assign(socket, foo: :bar)}"} = updated_event_handler - end - - test "update triggers after_update_page lifecycle" do - page = page_fixture(site: :lifecycle_test) - event_handler = page_event_handler_fixture(%{page: page}) - - {:ok, %Page{}} = Content.update_event_handler_for_page(page, event_handler, %{name: "Changed"}) - - assert_receive :lifecycle_after_update_page + assert {:ok, updated_event_handler} = Content.update_event_handler(event_handler, attrs) + assert %EventHandler{name: "Changed Name", code: "{:noreply, assign(socket, foo: :bar)}"} = updated_event_handler end test "update validates elixir code" do - page = page_fixture(%{format: :heex}) - page_event_handler = page_event_handler_fixture(%{page: page}) + event_handler = event_handler_fixture() attrs = %{code: "[1)"} - assert {:error, %{errors: [error]}} = Content.update_event_handler_for_page(page, page_event_handler, attrs) + assert {:error, %{errors: [error]}} = Content.update_event_handler(event_handler, attrs) {:code, {_, [compilation_error: compilation_error]}} = error assert compilation_error =~ "unexpected token: )" attrs = %{code: "if true, do false"} - assert {:error, %{errors: [error]}} = Content.update_event_handler_for_page(page, page_event_handler, attrs) + assert {:error, %{errors: [error]}} = Content.update_event_handler(event_handler, attrs) {:code, {_, [compilation_error: compilation_error]}} = error assert compilation_error =~ "unexpected reserved word: do" @@ -676,25 +661,13 @@ defmodule Beacon.ContentTest do | attrs = %{code: code} - assert {:ok, _} = Content.update_event_handler_for_page(page, page_event_handler, attrs) + assert {:ok, _} = Content.update_event_handler(event_handler, attrs) end test "delete event handler OK" do - page = page_fixture(%{format: :heex}) - event_handler_1 = page_event_handler_fixture(%{page: page}) - event_handler_2 = page_event_handler_fixture(%{page: page}) + %{id: id} = event_handler = event_handler_fixture() - assert {:ok, %Page{event_handlers: [^event_handler_2]}} = Content.delete_event_handler_from_page(page, event_handler_1) - assert {:ok, %Page{event_handlers: []}} = Content.delete_event_handler_from_page(page, event_handler_2) - end - - test "delete triggers after_update_page lifecycle" do - page = page_fixture(site: :lifecycle_test) - event_handler = page_event_handler_fixture(%{page: page}) - - {:ok, %Page{}} = Content.delete_event_handler_from_page(page, event_handler) - - assert_receive :lifecycle_after_update_page + assert {:ok, %{id: ^id}} = Content.delete_event_handler(event_handler) end end diff --git a/test/beacon/lifecycle/template_test.exs b/test/beacon/lifecycle/template_test.exs index b41fd0bd..ad799fba 100644 --- a/test/beacon/lifecycle/template_test.exs +++ b/test/beacon/lifecycle/template_test.exs @@ -16,7 +16,7 @@ defmodule Beacon.Lifecycle.TemplateTest do end test "render_template" do - page = published_page_fixture(site: "my_site") |> Repo.preload([:event_handlers, :variants]) + page = published_page_fixture(site: "my_site") |> Repo.preload(:variants) Beacon.Loader.reload_page_module(page.site, page.id) env = Beacon.Web.PageLive.make_env(:my_site) diff --git a/test/beacon/loader/page_test.exs b/test/beacon/loader/page_test.exs index 6bd0ce27..bb9231ce 100644 --- a/test/beacon/loader/page_test.exs +++ b/test/beacon/loader/page_test.exs @@ -69,7 +69,7 @@ defmodule Beacon.Loader.PageTest do describe "render" do test "render primary template" do - page = published_page_fixture(site: "my_site", path: "/1") |> Repo.preload([:event_handlers, :variants]) + page = published_page_fixture(site: "my_site", path: "/1") |> Repo.preload(:variants) {:ok, module} = Loader.reload_page_module(page.site, page.id) assert %Phoenix.LiveView.Rendered{static: ["
\n

my_site#home

\n
"]} = module.render(%{}) end diff --git a/test/beacon_web/live/page_live_test.exs b/test/beacon_web/live/page_live_test.exs index 4ae7bd59..69109a5b 100644 --- a/test/beacon_web/live/page_live_test.exs +++ b/test/beacon_web/live/page_live_test.exs @@ -82,8 +82,8 @@ defmodule Beacon.Web.Live.PageLiveTest do ) _page_home_form_submit_handler = - page_event_handler_fixture(%{ - page: page_home, + event_handler_fixture(%{ + site: :my_site, name: "hello", code: """ {:noreply, assign(socket, :message, "Hello \#{event_params["greeting"]["name"]}!")} @@ -108,6 +108,7 @@ defmodule Beacon.Web.Live.PageLiveTest do Loader.reload_components_module(:my_site) Loader.reload_layouts_modules(:my_site) Loader.reload_pages_modules(:my_site) + Loader.reload_event_handlers_module(:my_site) [layout: layout] end @@ -181,7 +182,7 @@ defmodule Beacon.Web.Live.PageLiveTest do layout = published_layout_fixture() page = - [ + published_page_fixture( site: "my_site", layout_id: layout.id, path: "/page/meta-tag", @@ -192,9 +193,7 @@ defmodule Beacon.Web.Live.PageLiveTest do %{"property" => "og:url", "content" => "http://example.com{{ page.path }}"}, %{"property" => "og:image", "content" => "{{ live_data.image }}"} ] - ] - |> published_page_fixture() - |> Beacon.BeaconTest.Repo.preload(:event_handlers) + ) live_data = live_data_fixture(path: "/page/meta-tag") live_data_assign_fixture(live_data: live_data, format: :text, key: "image", value: "http://img.example.com") diff --git a/test/support/fixtures.ex b/test/support/fixtures.ex index 06926146..b52f7aa5 100644 --- a/test/support/fixtures.ex +++ b/test/support/fixtures.ex @@ -3,7 +3,7 @@ defmodule Beacon.Fixtures do alias Beacon.Content alias Beacon.Content.ErrorPage - alias Beacon.Content.PageEventHandler + alias Beacon.Content.EventHandler alias Beacon.Content.PageVariant alias Beacon.MediaLibrary alias Beacon.MediaLibrary.UploadMetadata @@ -171,27 +171,16 @@ defmodule Beacon.Fixtures do defp template_for(%{format: :heex} = _page), do: "
My Site
" defp template_for(%{format: :markdown} = _page), do: "# My site" - def page_event_handler_fixture(attrs \\ %{}) - - def page_event_handler_fixture(%{page: %Content.Page{} = page} = attrs), - do: page_event_handler_fixture(page, attrs) - - def page_event_handler_fixture(%{site: site, page_id: page_id} = attrs) do - site - |> Content.get_page!(page_id) - |> page_event_handler_fixture(attrs) - end - - defp page_event_handler_fixture(page, attrs) do + def event_handler_fixture(attrs \\ %{}) do full_attrs = %{ name: attrs[:name] || "Event Handler #{System.unique_integer([:positive])}", - code: attrs[:code] || "{:noreply, socket}" + code: attrs[:code] || "{:noreply, socket}", + site: attrs[:site] || :my_site } - page - |> Ecto.build_assoc(:event_handlers) - |> PageEventHandler.changeset(full_attrs) - |> repo(page).insert!() + %EventHandler{} + |> EventHandler.changeset(full_attrs) + |> repo(full_attrs.site).insert!() end def error_page_fixture(attrs \\ %{}) do diff --git a/test/test_helper.exs b/test/test_helper.exs index 95cc72b8..ceae09d9 100644 --- a/test/test_helper.exs +++ b/test/test_helper.exs @@ -159,28 +159,23 @@ Supervisor.start_link( ) # TODO: better control :booted default data when we introduce Beacon.Test functions -Beacon.BeaconTest.Repo.delete_all(Beacon.Content.Component) -Beacon.BeaconTest.Repo.delete_all(Beacon.Content.ErrorPage) -Beacon.BeaconTest.Repo.delete_all(Beacon.Content.Page) -Beacon.BeaconTest.Repo.delete_all(Beacon.Content.Layout) +Enum.each( + [ + Beacon.Content.Component, + Beacon.Content.ErrorPage, + Beacon.Content.Page, + Beacon.Content.Layout, + Beacon.Content.EventHandler + ], + &Beacon.BeaconTest.Repo.delete_all/1 +) # TODO: add hooks into Beacon.Testing to reload these shared/global modules -Beacon.Loader.reload_routes_module(:my_site) -Beacon.Loader.reload_routes_module(:not_booted) -Beacon.Loader.reload_routes_module(:booted) -Beacon.Loader.reload_routes_module(:s3_site) -Beacon.Loader.reload_routes_module(:data_source_test) -Beacon.Loader.reload_routes_module(:default_meta_tags_test) -Beacon.Loader.reload_routes_module(:lifecycle_test) -Beacon.Loader.reload_routes_module(:lifecycle_test_fail) -Beacon.Loader.reload_components_module(:my_site) -Beacon.Loader.reload_components_module(:not_booted) -Beacon.Loader.reload_components_module(:booted) -Beacon.Loader.reload_components_module(:s3_site) -Beacon.Loader.reload_components_module(:data_source_test) -Beacon.Loader.reload_components_module(:default_meta_tags_test) -Beacon.Loader.reload_components_module(:lifecycle_test) -Beacon.Loader.reload_components_module(:lifecycle_test_fail) +for site <- [:my_site, :not_booted, :s3_site, :data_source_test, :default_meta_tags_test, :lifecycle_test, :lifecycle_test_fail] do + Beacon.Loader.reload_routes_module(site) + Beacon.Loader.reload_components_module(site) + Beacon.Loader.reload_event_handlers_module(site) +end ExUnit.start(exclude: [:skip]) Ecto.Adapters.SQL.Sandbox.mode(Beacon.BeaconTest.Repo, :manual)