diff --git a/.formatter.exs b/.formatter.exs index bafd8f8..f5dc12d 100644 --- a/.formatter.exs +++ b/.formatter.exs @@ -1,6 +1,6 @@ [ - import_deps: [:ecto, :phoenix], - inputs: ["*.{heex,ex,exs}", "priv/*/seeds.exs", "{config,lib,test}/**/*.{heex,ex,exs}"], + import_deps: [:ecto, :ecto_sql, :phoenix], + inputs: ["*.{heex,ex,exs}", "{config,lib,test}/**/*.{heex,ex,exs}", "priv/*/seeds.exs"], subdirectories: ["priv/*/migrations"], plugins: [Phoenix.LiveView.HTMLFormatter] ] diff --git a/lib/memex/accounts.ex b/lib/memex/accounts.ex index 56bcf7b..e9a9e14 100644 --- a/lib/memex/accounts.ex +++ b/lib/memex/accounts.ex @@ -374,8 +374,8 @@ defmodule Memex.Accounts do @doc """ Deletes the signed token with the given context. """ - @spec delete_session_token(token :: String.t()) :: :ok - def delete_session_token(token) do + @spec delete_user_session_token(token :: String.t()) :: :ok + def delete_user_session_token(token) do UserToken.token_and_context_query(token, "session") |> Repo.delete_all() :ok end diff --git a/lib/memex/accounts/email.ex b/lib/memex/accounts/email.ex index 3bd4cdd..810659f 100644 --- a/lib/memex/accounts/email.ex +++ b/lib/memex/accounts/email.ex @@ -7,10 +7,11 @@ defmodule Memex.Email do `lib/memex_web/templates/layout/email.txt.heex` for text emails. """ - use Phoenix.Swoosh, view: MemexWeb.EmailView, layout: {MemexWeb.LayoutView, :email} + import Swoosh.Email import MemexWeb.Gettext + import Phoenix.Template alias Memex.Accounts.User - alias MemexWeb.EmailView + alias MemexWeb.{EmailHTML, Layouts} @typedoc """ Represents an HTML and text body email that can be sent @@ -28,21 +29,33 @@ defmodule Memex.Email do def generate_email("welcome", user, %{"url" => url}) do user |> base_email(dgettext("emails", "Confirm your Memex account")) - |> render_body("confirm_email.html", %{user: user, url: url}) - |> text_body(EmailView.render("confirm_email.txt", %{user: user, url: url})) + |> html_email(:confirm_email_html, %{user: user, url: url}) + |> text_email(:confirm_email_text, %{user: user, url: url}) end def generate_email("reset_password", user, %{"url" => url}) do user |> base_email(dgettext("emails", "Reset your Memex password")) - |> render_body("reset_password.html", %{user: user, url: url}) - |> text_body(EmailView.render("reset_password.txt", %{user: user, url: url})) + |> html_email(:reset_password_html, %{user: user, url: url}) + |> text_email(:reset_password_text, %{user: user, url: url}) end def generate_email("update_email", user, %{"url" => url}) do user |> base_email(dgettext("emails", "Update your Memex email")) - |> render_body("update_email.html", %{user: user, url: url}) - |> text_body(EmailView.render("update_email.txt", %{user: user, url: url})) + |> html_email(:update_email_html, %{user: user, url: url}) + |> text_email(:update_email_text, %{user: user, url: url}) + end + + defp html_email(email, atom, assigns) do + heex = apply(EmailHTML, atom, [assigns]) + html = render_to_string(Layouts, "email_html", "html", email: email, inner_content: heex) + email |> html_body(html) + end + + defp text_email(email, atom, assigns) do + heex = apply(EmailHTML, atom, [assigns]) + text = render_to_string(Layouts, "email_text", "text", email: email, inner_content: heex) + email |> text_body(text) end end diff --git a/lib/memex/contexts/context.ex b/lib/memex/contexts/context.ex index 69e470e..3aa3e96 100644 --- a/lib/memex/contexts/context.ex +++ b/lib/memex/contexts/context.ex @@ -9,6 +9,7 @@ defmodule Memex.Contexts.Context do alias Ecto.{Changeset, UUID} alias Memex.{Accounts.User, Repo} + @derive {Phoenix.Param, key: :slug} @derive {Jason.Encoder, only: [ :slug, diff --git a/lib/memex/logger.ex b/lib/memex/logger.ex index 098bdfc..d9d9002 100644 --- a/lib/memex/logger.ex +++ b/lib/memex/logger.ex @@ -14,17 +14,17 @@ defmodule Memex.Logger do |> Map.put(:stacktrace, Exception.format_stacktrace(stacktrace)) |> pretty_encode() - Logger.error(meta.reason, data: data) + Logger.error("#{meta.reason} #{data}") end def handle_event([:oban, :job, :start], measure, meta, _config) do data = get_oban_job_data(meta, measure) |> pretty_encode() - Logger.info("Started oban job", data: data) + Logger.info("Started oban job: #{data}") end def handle_event([:oban, :job, :stop], measure, meta, _config) do data = get_oban_job_data(meta, measure) |> pretty_encode() - Logger.info("Finished oban job", data: data) + Logger.info("Finished oban job: #{data}") end def handle_event([:oban, :job, unhandled_event], measure, meta, _config) do @@ -33,7 +33,7 @@ defmodule Memex.Logger do |> Map.put(:event, unhandled_event) |> pretty_encode() - Logger.warning("Unhandled oban job event", data: data) + Logger.warning("Unhandled oban job event: #{data}") end def handle_event(unhandled_event, measure, meta, config) do @@ -45,7 +45,7 @@ defmodule Memex.Logger do config: config }) - Logger.warning("Unhandled telemetry event", data: data) + Logger.warning("Unhandled telemetry event: #{data}") end defp get_oban_job_data(%{job: job}, measure) do diff --git a/lib/memex/notes/note.ex b/lib/memex/notes/note.ex index 2c8bb3c..d52b8ea 100644 --- a/lib/memex/notes/note.ex +++ b/lib/memex/notes/note.ex @@ -8,6 +8,7 @@ defmodule Memex.Notes.Note do alias Ecto.{Changeset, UUID} alias Memex.{Accounts.User, Repo} + @derive {Phoenix.Param, key: :slug} @derive {Jason.Encoder, only: [ :slug, diff --git a/lib/memex/pipelines/pipeline.ex b/lib/memex/pipelines/pipeline.ex index 75b9676..15d1be4 100644 --- a/lib/memex/pipelines/pipeline.ex +++ b/lib/memex/pipelines/pipeline.ex @@ -8,6 +8,7 @@ defmodule Memex.Pipelines.Pipeline do alias Ecto.{Changeset, UUID} alias Memex.{Accounts.User, Pipelines.Steps.Step, Repo} + @derive {Phoenix.Param, key: :slug} @derive {Jason.Encoder, only: [ :slug, diff --git a/lib/memex_web.ex b/lib/memex_web.ex index 9b5a8c1..6a2b5e2 100644 --- a/lib/memex_web.ex +++ b/lib/memex_web.ex @@ -1,54 +1,61 @@ defmodule MemexWeb do @moduledoc """ The entrypoint for defining your web interface, such - as controllers, views, channels and so on. + as controllers, components, channels, and so on. This can be used in your application as: use MemexWeb, :controller - use MemexWeb, :view + use MemexWeb, :html - The definitions below will be executed for every view, - controller, etc, so keep them short and clean, focused + The definitions below will be executed for every controller, + component, etc, so keep them short and clean, focused on imports, uses and aliases. Do NOT define functions inside the quoted expressions - below. Instead, define any helper function in modules - and import those modules here. + below. Instead, define additional modules and import + those modules here. """ - def controller do - quote do - use Phoenix.Controller, namespace: MemexWeb + def static_paths, do: ~w(css js fonts images favicon.ico robots.txt) + def router do + quote do + use Phoenix.Router, helpers: false + + # Import common connection and controller functions to use in pipelines import Plug.Conn - import MemexWeb.Gettext - alias MemexWeb.Endpoint - alias MemexWeb.Router.Helpers, as: Routes + import Phoenix.Controller + import Phoenix.LiveView.Router end end - def view do + def channel do quote do - use Phoenix.View, - root: "lib/memex_web/templates", - namespace: MemexWeb + use Phoenix.Channel + end + end - # Import convenience functions from controllers - import Phoenix.Controller, - only: [get_flash: 1, get_flash: 2, view_module: 1, view_template: 1] + def controller do + quote do + use Phoenix.Controller, + formats: [:html, :json], + layouts: [html: MemexWeb.Layouts] - # Include shared imports and aliases for views - unquote(view_helpers()) + # credo:disable-for-next-line Credo.Check.Consistency.MultiAliasImportRequireUse + import Plug.Conn + import MemexWeb.Gettext + + unquote(verified_routes()) end end def live_view do quote do - use Phoenix.LiveView, layout: {MemexWeb.LayoutView, :live} + use Phoenix.LiveView, + layout: {MemexWeb.Layouts, :app} - on_mount MemexWeb.InitAssigns - unquote(view_helpers()) + unquote(html_helpers()) end end @@ -56,50 +63,46 @@ defmodule MemexWeb do quote do use Phoenix.LiveComponent - unquote(view_helpers()) + unquote(html_helpers()) end end - def component do + def html do quote do use Phoenix.Component - unquote(view_helpers()) + # Import convenience functions from controllers + import Phoenix.Controller, + only: [get_csrf_token: 0, view_module: 1, view_template: 1] + + # Include general helpers for rendering HTML + unquote(html_helpers()) end end - def router do + defp html_helpers do quote do - use Phoenix.Router - - import Phoenix.{Controller, LiveView.Router} - # credo:disable-for-next-line Credo.Check.Consistency.MultiAliasImportRequireUse - import Plug.Conn - end - end - - def channel do - quote do - use Phoenix.Channel - # credo:disable-for-next-line Credo.Check.Consistency.MultiAliasImportRequireUse - import MemexWeb.Gettext - end - end - - defp view_helpers do - quote do - # Use all HTML functionality (forms, tags, etc) # credo:disable-for-next-line Credo.Check.Consistency.MultiAliasImportRequireUse use Phoenix.HTML - # Import LiveView and .heex helpers (live_render, link, <.form>, etc) - # Import basic rendering functionality (render, render_layout, etc) - import Phoenix.{Component, View} - import MemexWeb.{ErrorHelpers, Gettext, CoreComponents, ViewHelpers} - # credo:disable-for-next-line Credo.Check.Consistency.MultiAliasImportRequireUse - alias MemexWeb.Endpoint - alias MemexWeb.Router.Helpers, as: Routes + import Phoenix.Component + import MemexWeb.{ErrorHelpers, Gettext, CoreComponents, HTMLHelpers} + + # Shortcut for generating JS commands + alias Phoenix.LiveView.JS + + # Routes generation with the ~p sigil + unquote(verified_routes()) + end + end + + def verified_routes do + quote do + use Phoenix.VerifiedRoutes, + endpoint: MemexWeb.Endpoint, + router: MemexWeb.Router, + statics: MemexWeb.static_paths() end end diff --git a/lib/memex_web/components/contexts_table_component.ex b/lib/memex_web/components/contexts_table_component.ex index 48b69ef..cdc1fce 100644 --- a/lib/memex_web/components/contexts_table_component.ex +++ b/lib/memex_web/components/contexts_table_component.ex @@ -88,11 +88,9 @@ defmodule MemexWeb.Components.ContextsTableComponent do @spec get_value_for_key(atom(), Context.t(), additional_data :: map()) :: any() | {any(), Rendered.t()} - defp get_value_for_key(:slug, %{slug: slug}, _additional_data) do - assigns = %{slug: slug} - + defp get_value_for_key(:slug, %{slug: slug} = assigns, _additional_data) do slug_block = ~H""" - <.link navigate={Routes.context_show_path(Endpoint, :show, @slug)} class="link"> + <.link navigate={~p"/context/#{@slug}"} class="link"> <%= @slug %> """ @@ -100,16 +98,10 @@ defmodule MemexWeb.Components.ContextsTableComponent do {slug, slug_block} end - defp get_value_for_key(:tags, %{tags: tags}, _additional_data) do - assigns = %{tags: tags} - + defp get_value_for_key(:tags, assigns, _additional_data) do ~H"""
- <.link - :for={tag <- @tags} - patch={Routes.context_index_path(Endpoint, :search, tag)} - class="link" - > + <.link :for={tag <- @tags} patch={~p"/contexts/#{tag}"} class="link"> <%= tag %>
diff --git a/lib/memex_web/components/core_components.ex b/lib/memex_web/components/core_components.ex index dd725e0..760cc7b 100644 --- a/lib/memex_web/components/core_components.ex +++ b/lib/memex_web/components/core_components.ex @@ -3,13 +3,13 @@ defmodule MemexWeb.CoreComponents do Provides core UI components. """ use Phoenix.Component - import MemexWeb.{Gettext, ViewHelpers} + use MemexWeb, :verified_routes + + import MemexWeb.{Gettext, HTMLHelpers} alias Memex.{Accounts, Accounts.Invite, Accounts.User} alias Memex.Contexts.Context alias Memex.Notes.Note alias Memex.Pipelines.Steps.Step - alias MemexWeb.{Endpoint, HomeLive} - alias MemexWeb.Router.Helpers, as: Routes alias Phoenix.HTML alias Phoenix.LiveView.JS @@ -31,13 +31,13 @@ defmodule MemexWeb.CoreComponents do ## Examples - <.modal return_to={Routes.<%= schema.singular %>_index_path(Endpoint, :index)}> + <.modal return_to={~p"/\#{<%= schema.plural %>}"}> <.live_component module={<%= 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} - return_to={Routes.<%= schema.singular %>_index_path(Endpoint, :index)} + return_to={~p"/\#{<%= schema.singular %>}"} <%= schema.singular %>: @<%= schema.singular %> /> @@ -167,7 +167,7 @@ defmodule MemexWeb.CoreComponents do link = HTML.Link.link( "[[#{slug}]]", - to: Routes.note_show_path(Endpoint, :show, slug), + to: ~p"/note/#{slug}", class: "link inline", data: [qa: "#{data_qa_prefix}-#{slug}"] ) diff --git a/lib/memex_web/components/core_components/invite_card.html.heex b/lib/memex_web/components/core_components/invite_card.html.heex index da0374a..cabf1f3 100644 --- a/lib/memex_web/components/core_components/invite_card.html.heex +++ b/lib/memex_web/components/core_components/invite_card.html.heex @@ -24,7 +24,7 @@ <% end %> <.qr_code - content={Routes.user_registration_url(Endpoint, :new, invite: @invite.token)} + content={url(MemexWeb.Endpoint, ~p"/users/register?invite=#{@invite.token}")} filename={@invite.name} /> @@ -38,7 +38,7 @@ class="mx-2 my-1 text-xs px-4 py-2 rounded-lg text-center break-all text-primary-400 bg-primary-800" phx-no-format - ><%= Routes.user_registration_url(Endpoint, :new, invite: @invite.token) %> + ><%= url(MemexWeb.Endpoint, ~p"/users/register?invite=#{@invite.token}") %> <%= if @code_actions, do: render_slot(@code_actions) %> diff --git a/lib/memex_web/components/core_components/topbar.html.heex b/lib/memex_web/components/core_components/topbar.html.heex index 26b7873..be2fb56 100644 --- a/lib/memex_web/components/core_components/topbar.html.heex +++ b/lib/memex_web/components/core_components/topbar.html.heex @@ -1,10 +1,7 @@