diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 6e0bb974e..574e46d51 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -27,7 +27,7 @@ jobs: - id: setup-elixir uses: erlef/setup-elixir@v1 with: - otp-version: "25.0.4" + otp-version: "25.3.2.7" elixir-version: "1.14.0" - name: Setup the Elixir project diff --git a/.tool-versions b/.tool-versions index b39e1c180..3fa6184d2 100644 --- a/.tool-versions +++ b/.tool-versions @@ -1,3 +1,3 @@ -erlang 25.0.4 +erlang 25.3.2.7 elixir 1.14.0-otp-25 nodejs 18.19.0 diff --git a/.vscode/settings.json b/.vscode/settings.json index d0f43fd34..a44241c74 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -23,5 +23,6 @@ "elixir": "html", "phoenix-heex": "html" }, - "editor.tabCompletion": "on" + "editor.tabCompletion": "on", + "elixirLS.mixEnv": "dev" } diff --git a/core/assets/js/app.js b/core/assets/js/app.js index 6e0bbafc4..3b895b357 100644 --- a/core/assets/js/app.js +++ b/core/assets/js/app.js @@ -18,7 +18,7 @@ import { decode } from "blurhash"; import { urlBase64ToUint8Array } from "./tools"; import { registerAPNSDeviceToken } from "./apns"; import "./100vh-fix"; -import { ViewportResize } from "./viewport_resize"; +import { Viewport } from "./viewport"; import { SidePanel } from "./side_panel"; import { Toggle } from "./toggle"; import { Cell } from "./cell"; @@ -108,7 +108,7 @@ let Hooks = { Tabbar, TabbarItem, TabbarFooterItem, - ViewportResize, + Viewport, Wysiwyg, AutoSubmit, Sticky, diff --git a/core/assets/js/viewport_resize.js b/core/assets/js/viewport.js similarity index 60% rename from core/assets/js/viewport_resize.js rename to core/assets/js/viewport.js index 25470bb95..19ee3b494 100644 --- a/core/assets/js/viewport_resize.js +++ b/core/assets/js/viewport.js @@ -2,19 +2,24 @@ import _ from "lodash"; let resizeHandler; -export const ViewportResize = { +export const Viewport = { mounted() { // Direct push of current window size to properly update view this.pushResizeEvent(); window.addEventListener("resize", (event) => { - this.pushResizeEvent(); + this.pushChangeEvent(); }); }, - pushResizeEvent() { - console.log("pushResizeEvent"); - this.pushEvent("viewport_resize", { + updated() { + console.log("[Viewport] updated"); + this.pushChangeEvent(); + }, + + pushChangeEvent() { + console.log("[Viewport] push update event"); + this.pushEvent("viewport_changed", { width: window.innerWidth, height: window.innerHeight, }); diff --git a/core/bundles/next/lib/account/signin_page.ex b/core/bundles/next/lib/account/signin_page.ex index c7eea8369..dd1636c5b 100644 --- a/core/bundles/next/lib/account/signin_page.ex +++ b/core/bundles/next/lib/account/signin_page.ex @@ -1,11 +1,19 @@ defmodule Next.Account.SigninPage do use CoreWeb, :live_view + + on_mount({CoreWeb.Live.Hook.Base, __MODULE__}) + on_mount({CoreWeb.Live.Hook.User, __MODULE__}) + on_mount({CoreWeb.Live.Hook.Uri, __MODULE__}) + on_mount({Frameworks.GreenLight.LiveHook, __MODULE__}) + on_mount({Frameworks.Fabric.LiveHook, __MODULE__}) + import CoreWeb.Layouts.Stripped.Html import CoreWeb.Layouts.Stripped.Composer + import CoreWeb.Menus + import Frameworks.Pixel.Line alias Frameworks.Pixel.Tabbar - alias Next.Account.SigninPageBuilder @impl true @@ -29,11 +37,16 @@ defmodule Next.Account.SigninPage do } end - defp update_view_model(socket) do + def update_view_model(socket) do vm = SigninPageBuilder.view_model(nil, socket.assigns) assign(socket, vm: vm) end + def update_menus(%{assigns: %{current_user: user, uri: uri}} = socket) do + menus = build_menus(stripped_menus_config(), user, uri) + assign(socket, menus: menus) + end + @impl true def render(assigns) do ~H""" diff --git a/core/bundles/self/lib/account/signin_page.ex b/core/bundles/self/lib/account/signin_page.ex index a0df80394..f91024e9e 100644 --- a/core/bundles/self/lib/account/signin_page.ex +++ b/core/bundles/self/lib/account/signin_page.ex @@ -1,11 +1,20 @@ defmodule Self.Account.SigninPage do use CoreWeb, :live_view + + on_mount({CoreWeb.Live.Hook.Base, __MODULE__}) + on_mount({CoreWeb.Live.Hook.User, __MODULE__}) + on_mount({CoreWeb.Live.Hook.Uri, __MODULE__}) + on_mount({Frameworks.GreenLight.LiveHook, __MODULE__}) + on_mount({Frameworks.Fabric.LiveHook, __MODULE__}) + import CoreWeb.Layouts.Stripped.Html import CoreWeb.Layouts.Stripped.Composer + import CoreWeb.Menus alias Systems.Account.User alias Systems.Account.UserForm + @impl true def mount(params, _session, socket) do require_feature(:password_sign_in) @@ -18,6 +27,11 @@ defmodule Self.Account.SigninPage do } end + def update_menus(%{assigns: %{current_user: user, uri: uri}} = socket) do + menus = build_menus(stripped_menus_config(), user, uri) + assign(socket, menus: menus) + end + defp update_form(%{assigns: %{email: nil}} = socket) do assign(socket, :form, to_form(%{})) end diff --git a/core/config/config.exs b/core/config/config.exs index 239bf135e..1f1d5aa59 100644 --- a/core/config/config.exs +++ b/core/config/config.exs @@ -42,11 +42,10 @@ config :plug, :statuses, %{ 404 => "Page not found" } -config :core, :branch, factory: Systems.Project.Public - config :core, CoreWeb.FileUploader, max_file_size: 100_000_000 config :core, + greenlight_auth_module: Core.Authorization, image_catalog: Core.ImageCatalog.Unsplash, banking_backend: Systems.Banking.Dummy diff --git a/core/frameworks/concept/branch.ex b/core/frameworks/concept/branch.ex index f14ebf678..c7161a7d6 100644 --- a/core/frameworks/concept/branch.ex +++ b/core/frameworks/concept/branch.ex @@ -1,31 +1,11 @@ -defmodule Frameworks.Concept.Branch do - defmodule Factory do - @type scope :: :self | :parent - @type leaf :: struct() - @callback name(leaf, scope) :: {:ok, binary()} | {:error, atom()} - @callback hierarchy(leaf) :: {:ok, list()} | {:error, atom()} - end +defprotocol Frameworks.Concept.Branch do + # FIXME: add possibility to resolve dependencies to leafs (siblings) - require Logger + @type scope :: :self | :parent - def name(leaf, scope, default) when is_struct(leaf) and is_binary(default) do - case factory().name(leaf, scope) do - {:ok, name} -> - name + @spec name(t, scope) :: binary + def name(_t, _scope) - {:error, error} -> - Logger.warn( - "[Branch] Error while fetching name: #{inspect(error)} for leaf #{inspect(leaf)}" - ) - - default - end - end - - def hierarchy(leaf) when is_struct(leaf) do - factory().hierarchy(leaf) - end - - defp factory, do: Access.get(settings(), :factory, nil) - defp settings, do: Application.fetch_env!(:core, :branch) + @spec hierarchy(t) :: list + def hierarchy(_t) end diff --git a/core/frameworks/concept/live_hook.ex b/core/frameworks/concept/live_hook.ex new file mode 100644 index 000000000..42910d522 --- /dev/null +++ b/core/frameworks/concept/live_hook.ex @@ -0,0 +1,28 @@ +defmodule Frameworks.Concept.LiveHook do + @type live_view_module :: atom() + @type params :: map() + @type session :: map() + @type socket :: Phoenix.LiveView.Socket.t() + + @callback on_mount(live_view_module(), params(), session(), socket()) :: + {:cont | :halt, socket()} + + defmacro __using__(_opts) do + quote do + @behaviour Frameworks.Concept.LiveHook + + import Phoenix.LiveView, + only: [attach_hook: 4, connected?: 1, get_connect_params: 1, redirect: 2] + + import Phoenix.Component, only: [assign: 2] + + def optional_apply(socket, live_view_module, function) do + Frameworks.Utility.Module.optional_apply(live_view_module, function, [socket], socket) + end + + def optional_apply(socket, live_view_module, function, args) when is_list(args) do + Frameworks.Utility.Module.optional_apply(live_view_module, function, args, socket) + end + end + end +end diff --git a/core/frameworks/fabric/live_hook.ex b/core/frameworks/fabric/live_hook.ex new file mode 100644 index 000000000..fd0af06ea --- /dev/null +++ b/core/frameworks/fabric/live_hook.ex @@ -0,0 +1,9 @@ +defmodule Frameworks.Fabric.LiveHook do + import Phoenix.Component, only: [assign: 2] + + def on_mount(_live_view_module, _params, _session, socket) do + self = %Fabric.LiveView.RefModel{pid: self()} + fabric = %Fabric.Model{parent: nil, self: self, children: nil} + {:cont, socket |> assign(fabric: fabric)} + end +end diff --git a/core/frameworks/fabric/live_view_mount_plug.ex b/core/frameworks/fabric/live_view_mount_plug.ex deleted file mode 100644 index aabe2fad5..000000000 --- a/core/frameworks/fabric/live_view_mount_plug.ex +++ /dev/null @@ -1,22 +0,0 @@ -defmodule Frameworks.Fabric.LiveViewMountPlug do - defmacro __using__(_) do - quote do - @before_compile Frameworks.Fabric.LiveViewMountPlug - end - end - - defmacro __before_compile__(_env) do - quote do - defoverridable mount: 3 - - @doc """ - Automatically assigns Fabric to the socket on mount - """ - def mount(params, session, socket) do - self = %Fabric.LiveView.RefModel{pid: self()} - fabric = %Fabric.Model{parent: nil, self: self, children: nil} - super(params, session, socket |> Phoenix.Component.assign(:fabric, fabric)) - end - end - end -end diff --git a/core/frameworks/green_light/_live_feature.ex b/core/frameworks/green_light/_live_feature.ex new file mode 100644 index 000000000..725e599cc --- /dev/null +++ b/core/frameworks/green_light/_live_feature.ex @@ -0,0 +1,23 @@ +defmodule Frameworks.GreenLight.LiveFeature do + @callback get_authorization_context( + Phoenix.LiveView.unsigned_params() | :not_mounted_at_router, + session :: map, + socket :: Phoenix.Socket.t() + ) :: integer | struct + + @optional_callbacks get_authorization_context: 3 + + defmacro __using__(_opts) do + quote do + @behaviour Frameworks.GreenLight.LiveFeature + + def mount(params, session, %{assigns: %{authorization_failed: true}} = socket) do + {:ok, socket} + end + + def render(%{authorization_failed: true}) do + raise Frameworks.GreenLight.AccessDeniedError, "Authorization failed for #{__MODULE__}" + end + end + end +end diff --git a/core/frameworks/green_light/_live_hook.ex b/core/frameworks/green_light/_live_hook.ex new file mode 100644 index 000000000..0e289d05a --- /dev/null +++ b/core/frameworks/green_light/_live_hook.ex @@ -0,0 +1,40 @@ +defmodule Frameworks.GreenLight.LiveHook do + @moduledoc """ + Live Hook that enables automatic authorization checks. + """ + use Frameworks.Concept.LiveHook + use CoreWeb, :verified_routes + require Logger + + @impl true + def on_mount(live_view_module, params, session, socket) do + if access_allowed?(live_view_module, params, session, socket) do + {:cont, socket} + else + {:halt, redirect(socket, to: ~p"/access_denied")} + end + end + + defp access_allowed?(live_view_module, params, session, socket) do + user = Map.get(socket.assigns, :current_user) + + if function_exported?(live_view_module, :get_authorization_context, 3) do + can_access? = + auth_module().can_access?( + user, + live_view_module.get_authorization_context(params, session, socket) + |> Core.Authorization.print_roles(), + live_view_module + ) + + Logger.notice("User #{user.id} can_access? #{live_view_module}: #{can_access?}") + can_access? + else + auth_module().can_access?(user, live_view_module) + end + end + + defp auth_module() do + Application.get_env(:core, :greenlight_auth_module) + end +end diff --git a/core/frameworks/green_light/live.ex b/core/frameworks/green_light/live.ex deleted file mode 100644 index 95d8fda5f..000000000 --- a/core/frameworks/green_light/live.ex +++ /dev/null @@ -1,61 +0,0 @@ -defmodule Frameworks.GreenLight.Live do - require Logger - - @moduledoc """ - The Live module enables automatic authorization checks for LiveViews. - """ - @callback get_authorization_context( - Phoenix.LiveView.unsigned_params() | :not_mounted_at_router, - session :: map, - socket :: Phoenix.Socket.t() - ) :: integer | struct - @optional_callbacks get_authorization_context: 3 - - defmacro __using__(auth_module) do - quote do - @greenlight_authmodule unquote(auth_module) - @behaviour Frameworks.GreenLight.Live - @before_compile Frameworks.GreenLight.Live - import Phoenix.LiveView.Helpers - - def render(%{authorization_failed: true}) do - raise Frameworks.GreenLight.AccessDeniedError, "Authorization failed for #{__MODULE__}" - end - end - end - - defmacro __before_compile__(_env) do - quote do - if Module.defines?(__MODULE__, {:get_authorization_context, 3}) do - defp access_allowed?(params, session, socket) do - user = Map.get(socket.assigns, :current_user) - - can_access? = - @greenlight_authmodule.can_access?( - socket, - get_authorization_context(params, session, socket) - |> Core.Authorization.print_roles(), - __MODULE__ - ) - - Logger.notice("User #{user.id} can_access? #{__MODULE__}: #{can_access?}") - can_access? - end - else - defp access_allowed?(_params, session, socket) do - @greenlight_authmodule.can_access?(socket, __MODULE__) - end - end - - defoverridable mount: 3 - - def mount(params, session, socket) do - if access_allowed?(params, session, socket) do - super(params, session, socket) - else - {:ok, assign(socket, authorization_failed: true)} - end - end - end - end -end diff --git a/core/frameworks/pixel/components/tabbar.ex b/core/frameworks/pixel/components/tabbar.ex index f50ca4077..06aaf4415 100644 --- a/core/frameworks/pixel/components/tabbar.ex +++ b/core/frameworks/pixel/components/tabbar.ex @@ -28,17 +28,19 @@ defmodule Frameworks.Pixel.Tabbar do def container(assigns) do ~H""" -
- <%= if @size == :full do %> - <.container_full type={@type} tabs={@tabs} /> - <% end %> - <%= if @size == :wide do %> - <.container_wide type={@type} tabs={@tabs} /> - <% end %> - <%= if @size == :narrow do %> - <.container_narrow tabs={@tabs} /> - <% end %> -
+ <%= if Enum.count(@tabs) > 0 do %> +
+ <%= if @size == :full do %> + <.container_full type={@type} tabs={@tabs} /> + <% end %> + <%= if @size == :wide do %> + <.container_wide type={@type} tabs={@tabs} /> + <% end %> + <%= if @size == :narrow do %> + <.container_narrow tabs={@tabs} /> + <% end %> +
+ <% end %> """ end diff --git a/core/frameworks/utility/list.ex b/core/frameworks/utility/list.ex index 247c5731c..2b3b3ad98 100644 --- a/core/frameworks/utility/list.ex +++ b/core/frameworks/utility/list.ex @@ -1,9 +1,10 @@ defmodule Frameworks.Utility.List do - def append_if(list, element, true), do: append(list, element) + def append_if(list, term, true), do: append(list, term) def append_if(list, _element, _), do: list def append_if(list, nil), do: list - def append_if(list, element), do: append(list, element) + def append_if(list, term), do: append(list, term) + def append(list, closure) when is_function(closure, 0), do: list ++ [closure.()] def append(list, element), do: list ++ [element] def insert_at_every(list, every, fun) do diff --git a/core/frameworks/utility/module.ex b/core/frameworks/utility/module.ex index 636441cf6..f102f942a 100644 --- a/core/frameworks/utility/module.ex +++ b/core/frameworks/utility/module.ex @@ -1,4 +1,15 @@ defmodule Frameworks.Utility.Module do + def optional_apply(module, function, params, default \\ nil) + when is_atom(module) and is_atom(function) and is_list(params) do + if function_exported?(module, function, Enum.count(params)) do + "#{module}.#{function}/#{Enum.count(params)} called" + apply(module, function, params) + else + "#{module}.#{function}/#{Enum.count(params)} skipped" + default + end + end + def get(nil, _name), do: nil def get(system, name) when is_atom(system), diff --git a/core/lib/core/authorization.ex b/core/lib/core/authorization.ex index eea8da9ee..cb30098f9 100644 --- a/core/lib/core/authorization.ex +++ b/core/lib/core/authorization.ex @@ -62,7 +62,7 @@ defmodule Core.Authorization do grant_access(Systems.Pool.LandingPage, [:visitor, :member, :owner]) grant_access(Systems.Pool.ParticipantPage, [:creator]) grant_access(Systems.Pool.SubmissionPage, [:creator]) - grant_access(Systems.Project.NodePage, [:creator, :owner]) + grant_access(Systems.Project.NodePage, [:owner]) grant_access(Systems.Project.OverviewPage, [:admin, :creator]) grant_access(Systems.Promotion.LandingPage, [:visitor, :member]) grant_access(Systems.Storage.EndpointContentPage, [:owner]) diff --git a/core/lib/core_web.ex b/core/lib/core_web.ex index 944d72459..bbd2a389a 100644 --- a/core/lib/core_web.ex +++ b/core/lib/core_web.ex @@ -42,6 +42,8 @@ defmodule CoreWeb do import Phoenix.LiveView.Controller + plug(Systems.Project.BranchPlug) + # Routes generation with the ~p sigil unquote(verified_routes()) end @@ -95,11 +97,7 @@ defmodule CoreWeb do end end - def live_view() do - live_view(:base) - end - - def live_view(mount_plug_type) do + def live_view do quote do use Fabric.LiveView, CoreWeb.Layouts @@ -112,33 +110,20 @@ defmodule CoreWeb do unquote(component_helpers()) unquote(verified_routes()) - - unquote do - if mount_plug_type == :extended do - live_mount_plugs_extended() - else - live_mount_plugs() - end - end - end - end - - def live_mount_plugs do - quote do - use CoreWeb.LiveLocale - use CoreWeb.LiveTimezone - use CoreWeb.LiveRemoteIp - use Frameworks.Fabric.LiveViewMountPlug - use Frameworks.GreenLight.Live, Core.Authorization - use CoreWeb.LiveUser + unquote(live_features()) end end - def live_mount_plugs_extended do + def live_features do quote do - use Systems.Observatory.Public - use CoreWeb.LiveUri - unquote(live_mount_plugs()) + use Frameworks.GreenLight.LiveFeature + use Systems.Observatory.LiveFeature + use CoreWeb.Live.Feature.Viewport + use CoreWeb.Live.Feature.Uri + use CoreWeb.Live.Feature.Model + use CoreWeb.Live.Feature.Menus + use CoreWeb.Live.Feature.Tabbar + use CoreWeb.Live.Feature.Actions end end diff --git a/core/lib/core_web/controllers/error_controller.ex b/core/lib/core_web/controllers/error_controller.ex new file mode 100644 index 000000000..cefc74957 --- /dev/null +++ b/core/lib/core_web/controllers/error_controller.ex @@ -0,0 +1,10 @@ +defmodule CoreWeb.ErrorController do + use CoreWeb, :controller + + def access_denied(conn, _params) do + conn + |> put_status(:forbidden) + |> put_view(CoreWeb.ErrorHTML) + |> render(:"403") + end +end diff --git a/core/lib/core_web/controllers/error_html.ex b/core/lib/core_web/controllers/error_html.ex index e1eceadc4..ae60f88c9 100644 --- a/core/lib/core_web/controllers/error_html.ex +++ b/core/lib/core_web/controllers/error_html.ex @@ -2,6 +2,7 @@ defmodule CoreWeb.ErrorHTML do use CoreWeb, :html import CoreWeb.Layouts.Stripped.Html import CoreWeb.Layouts.Stripped.Composer + import CoreWeb.Menus defp body("403.html", status), do: dgettext("eyra-error", "403.body", status: status) defp body("404.html", status), do: dgettext("eyra-error", "404.body", status: status) @@ -19,7 +20,7 @@ defmodule CoreWeb.ErrorHTML do def render(template, assigns) do status = status(template) - menus = build_menus(Map.put(assigns, :active_menu_item, nil)) + menus = build_menus(stripped_menus_config(), nil, nil) assigns = Map.merge(assigns, %{ diff --git a/core/lib/core_web/controllers/layouts/error.html.heex b/core/lib/core_web/controllers/layouts/error.html.heex index ed6c40b45..8fb3531f0 100644 --- a/core/lib/core_web/controllers/layouts/error.html.heex +++ b/core/lib/core_web/controllers/layouts/error.html.heex @@ -7,7 +7,7 @@ - + - + diff --git a/core/lib/core_web/controllers/layouts/stripped/composer.ex b/core/lib/core_web/controllers/layouts/stripped/composer.ex index 717ba2cda..59671b21b 100644 --- a/core/lib/core_web/controllers/layouts/stripped/composer.ex +++ b/core/lib/core_web/controllers/layouts/stripped/composer.ex @@ -1,83 +1,35 @@ defmodule CoreWeb.Layouts.Stripped.Composer do - import Phoenix.Component - - @menus [ - :mobile_navbar, - :desktop_navbar - ] - - def builder, do: Application.fetch_env!(:core, :stripped_menu_builder) - - def build_menu(%{active_menu_item: active_menu_item} = assigns, type) do - builder().build_menu(assigns, type, active_menu_item) - end - - def build_menu(%{vm: %{active_menu_item: active_menu_item}} = assigns, type) do - builder().build_menu(assigns, type, active_menu_item) - end - - def build_menus(%{assigns: assigns} = socket) do - menus = build_menus(assigns) - socket |> assign(menus: menus) - end - - def build_menus(assigns) do - Enum.reduce(@menus, %{}, fn menu_id, acc -> - Map.put(acc, menu_id, build_menu(assigns, menu_id)) - end) - end - - def update_menus(socket) do - socket - |> build_menus() - end + def stripped_menus_config(), + do: { + :stripped_menu_builder, + [ + :mobile_navbar, + :desktop_navbar + ], + nil + } defmacro __using__(_) do quote do - @before_compile CoreWeb.Layouts.Stripped.Composer + def get_menus_config(), do: CoreWeb.Layouts.Stripped.Composer.stripped_menus_config() + + on_mount({CoreWeb.Live.Hook.Base, __MODULE__}) + on_mount({CoreWeb.Live.Hook.User, __MODULE__}) + on_mount({Frameworks.GreenLight.LiveHook, __MODULE__}) + on_mount({Frameworks.Fabric.LiveHook, __MODULE__}) + on_mount({CoreWeb.Live.Hook.RemoteIp, __MODULE__}) + on_mount({CoreWeb.Live.Hook.Timezone, __MODULE__}) + on_mount({CoreWeb.Live.Hook.Locale, __MODULE__}) + on_mount({CoreWeb.Live.Hook.Uri, __MODULE__}) + on_mount({CoreWeb.Live.Hook.Model, __MODULE__}) + on_mount({Systems.Project.LiveHook, __MODULE__}) + on_mount({Systems.Observatory.LiveHook, __MODULE__}) + on_mount({CoreWeb.Live.Hook.Menus, __MODULE__}) + use CoreWeb.UI.PlainDialog - import CoreWeb.Layouts.Stripped.Composer import CoreWeb.Layouts.Stripped.Html import Systems.Content.Html, only: [live_stripped: 1] end end - - defmacro __before_compile__(_env) do - quote do - defoverridable mount: 3 - - @impl true - def mount(params, session, %{assigns: %{authorization_failed: true}} = socket) do - {:ok, socket} - end - - @impl true - def mount(params, session, socket) do - {:ok, socket} = super(params, session, socket) - - { - :ok, - socket - |> assign(active_menu_item: nil) - |> update_menus() - } - end - - defoverridable handle_uri: 1 - - def handle_uri(socket) do - super(socket) - |> update_menus() - end - - defoverridable handle_view_model_updated: 1 - - @impl true - def handle_view_model_updated(socket) do - super(socket) - |> update_menus() - end - end - end end diff --git a/core/lib/core_web/controllers/layouts/website/composer.ex b/core/lib/core_web/controllers/layouts/website/composer.ex index e25941356..56a783d8f 100644 --- a/core/lib/core_web/controllers/layouts/website/composer.ex +++ b/core/lib/core_web/controllers/layouts/website/composer.ex @@ -1,18 +1,31 @@ defmodule CoreWeb.Layouts.Website.Composer do defmacro __using__(_) do quote do - use CoreWeb.LiveMenus, { - :website_menu_builder, - [ - :mobile_menu, - :mobile_navbar, - :desktop_navbar - ] - } + def get_menus_config(), + do: { + :website_menu_builder, + [ + :mobile_menu, + :mobile_navbar, + :desktop_navbar + ] + } + + on_mount({CoreWeb.Live.Hook.Base, __MODULE__}) + on_mount({CoreWeb.Live.Hook.User, __MODULE__}) + on_mount({Frameworks.GreenLight.LiveHook, __MODULE__}) + on_mount({Frameworks.Fabric.LiveHook, __MODULE__}) + on_mount({CoreWeb.Live.Hook.RemoteIp, __MODULE__}) + on_mount({CoreWeb.Live.Hook.Timezone, __MODULE__}) + on_mount({CoreWeb.Live.Hook.Locale, __MODULE__}) + on_mount({CoreWeb.Live.Hook.Uri, __MODULE__}) + on_mount({CoreWeb.Live.Hook.Model, __MODULE__}) + on_mount({Systems.Project.LiveHook, __MODULE__}) + on_mount({Systems.Observatory.LiveHook, __MODULE__}) + on_mount({CoreWeb.Live.Hook.Menus, __MODULE__}) use CoreWeb.UI.PlainDialog - import CoreWeb.Layouts.Website.Composer import CoreWeb.Layouts.Website.Html end end diff --git a/core/lib/core_web/controllers/layouts/workspace/composer.ex b/core/lib/core_web/controllers/layouts/workspace/composer.ex index 4eacd988d..8316ea346 100644 --- a/core/lib/core_web/controllers/layouts/workspace/composer.ex +++ b/core/lib/core_web/controllers/layouts/workspace/composer.ex @@ -1,19 +1,35 @@ defmodule CoreWeb.Layouts.Workspace.Composer do defmacro __using__(_opts) do quote do - use CoreWeb.LiveMenus, { - :workspace_menu_builder, - [ - :mobile_menu, - :mobile_navbar, - :desktop_menu, - :tablet_menu - ] - } + def get_menus_config(), + do: { + :workspace_menu_builder, + [ + :mobile_menu, + :mobile_navbar, + :desktop_menu, + :tablet_menu + ] + } + + on_mount({CoreWeb.Live.Hook.Base, __MODULE__}) + on_mount({CoreWeb.Live.Hook.User, __MODULE__}) + on_mount({Frameworks.GreenLight.LiveHook, __MODULE__}) + on_mount({Frameworks.Fabric.LiveHook, __MODULE__}) + on_mount({CoreWeb.Live.Hook.Viewport, __MODULE__}) + on_mount({CoreWeb.Live.Hook.RemoteIp, __MODULE__}) + on_mount({CoreWeb.Live.Hook.Timezone, __MODULE__}) + on_mount({CoreWeb.Live.Hook.Locale, __MODULE__}) + on_mount({CoreWeb.Live.Hook.Uri, __MODULE__}) + on_mount({CoreWeb.Live.Hook.Model, __MODULE__}) + on_mount({Systems.Project.LiveHook, __MODULE__}) + on_mount({Systems.Observatory.LiveHook, __MODULE__}) + on_mount({CoreWeb.Live.Hook.Menus, __MODULE__}) + on_mount({CoreWeb.Live.Hook.Tabbar, __MODULE__}) + on_mount({CoreWeb.Live.Hook.Actions, __MODULE__}) use CoreWeb.UI.PlainDialog - import CoreWeb.Layouts.Workspace.Composer import CoreWeb.Layouts.Workspace.Html @impl true diff --git a/core/lib/core_web/endpoint.ex b/core/lib/core_web/endpoint.ex index 0b8bcd2a7..a8adcbeec 100644 --- a/core/lib/core_web/endpoint.ex +++ b/core/lib/core_web/endpoint.ex @@ -1,5 +1,6 @@ defmodule CoreWeb.Endpoint do use Phoenix.Endpoint, otp_app: :core + require Systems.Content.Plug require Systems.Feldspar.Plug diff --git a/core/lib/core_web/live/fake_qualtrics.ex b/core/lib/core_web/live/fake_qualtrics.ex index 4d6218a6b..42dd8df46 100644 --- a/core/lib/core_web/live/fake_qualtrics.ex +++ b/core/lib/core_web/live/fake_qualtrics.ex @@ -6,6 +6,7 @@ defmodule CoreWeb.FakeQualtrics do alias Frameworks.Pixel.Text alias Frameworks.Pixel.Button + @impl true def mount(%{"re" => redirect_url}, _session, socket) do socket = socket diff --git a/core/lib/core_web/live/feature/actions.ex b/core/lib/core_web/live/feature/actions.ex new file mode 100644 index 000000000..5a30745a1 --- /dev/null +++ b/core/lib/core_web/live/feature/actions.ex @@ -0,0 +1,36 @@ +defmodule CoreWeb.Live.Feature.Actions do + alias CoreWeb.UI.Responsive.Breakpoint + + def create_actions(%{assigns: %{breakpoint: {:unknown, _}}} = _socket), do: [] + + def create_actions(%{assigns: %{vm: %{actions: actions}}} = socket) do + actions + |> Keyword.keys() + |> Enum.map(&create_action(Keyword.get(actions, &1), socket)) + |> Enum.filter(&(not is_nil(&1))) + end + + def create_actions(_socket), do: [] + + def create_action(action, %{assigns: %{breakpoint: breakpoint}}) do + Breakpoint.value(breakpoint, nil, + xs: %{0 => action.icon}, + md: %{40 => action.label, 100 => action.icon}, + lg: %{50 => action.label} + ) + end + + defmacro __using__(_opts \\ nil) do + quote do + import CoreWeb.Live.Feature.Actions + + # stubs, handled by Live Hooks + def handle_event("action_click", _, socket), do: {:noreply, socket} + def handle_info(:action_clicked, socket), do: {:noreply, socket} + + def update_actions(socket) do + assign(socket, actions: create_actions(socket)) + end + end + end +end diff --git a/core/lib/core_web/live/feature/menus.ex b/core/lib/core_web/live/feature/menus.ex new file mode 100644 index 000000000..14fef9b16 --- /dev/null +++ b/core/lib/core_web/live/feature/menus.ex @@ -0,0 +1,42 @@ +defmodule CoreWeb.Live.Feature.Menus do + @type menu_builder :: atom() + @type menus :: list(atom()) + @type active_menu_item :: atom() + + @callback get_menus_config() :: + {menu_builder(), menus()} | {menu_builder(), menus(), active_menu_item()} | nil + + defmacro __using__(_opts) do + quote do + @behaviour CoreWeb.Live.Feature.Menus + import Phoenix.Component, only: [assign: 2] + + @impl true + def get_menus_config(), do: nil + + defoverridable get_menus_config: 0 + + import Phoenix.Component + + def update_menus(%{assigns: %{authorization_failed: true}} = socket), do: socket + + def update_menus(%{assigns: %{menus_config: nil}} = socket), + do: socket |> assign(menus: nil) + + def update_menus( + %{assigns: %{menus_config: {menu_builder, menus}, active_menu_item: active_menu_item}} = + socket + ) do + update_menus(socket, menu_builder, menus, active_menu_item) + end + + def update_menus(socket, menu_builder, menus, active_menu_item) do + user = Map.get(socket.assigns, :current_user) + uri = Map.get(socket.assigns, :uri) + + menus = CoreWeb.Menus.build_menus(menu_builder, menus, active_menu_item, user, uri) + socket |> assign(menus: menus) + end + end + end +end diff --git a/core/lib/core_web/live/feature/model.ex b/core/lib/core_web/live/feature/model.ex new file mode 100644 index 000000000..58dc9bb64 --- /dev/null +++ b/core/lib/core_web/live/feature/model.ex @@ -0,0 +1,14 @@ +defmodule CoreWeb.Live.Feature.Model do + @callback get_model(map(), map(), Socket.t()) :: map | struct | nil + + defmacro __using__(_opts \\ nil) do + quote do + @behaviour CoreWeb.Live.Feature.Model + + @impl true + def get_model(_params, _session, _socket), do: nil + + defoverridable get_model: 3 + end + end +end diff --git a/core/lib/core_web/live/feature/tabbar.ex b/core/lib/core_web/live/feature/tabbar.ex new file mode 100644 index 000000000..c9e20cb17 --- /dev/null +++ b/core/lib/core_web/live/feature/tabbar.ex @@ -0,0 +1,17 @@ +defmodule CoreWeb.Live.Feature.Tabbar do + defmacro __using__(_opts \\ nil) do + quote do + alias CoreWeb.UI.Responsive.Breakpoint + + def tabbar_size({:unknown, _}), do: :unknown + def tabbar_size(bp), do: Breakpoint.value(bp, :narrow, sm: %{30 => :wide}) + + defoverridable tabbar_size: 1 + + def update_tabbar_size(%{assigns: %{breakpoint: breakpoint}} = socket) do + tabbar_size = tabbar_size(breakpoint) + socket |> assign(tabbar_size: tabbar_size) + end + end + end +end diff --git a/core/lib/core_web/live/feature/uri.ex b/core/lib/core_web/live/feature/uri.ex new file mode 100644 index 000000000..7e6967d86 --- /dev/null +++ b/core/lib/core_web/live/feature/uri.ex @@ -0,0 +1,14 @@ +defmodule CoreWeb.Live.Feature.Uri do + @callback handle_uri(Socket.t()) :: Socket.t() + + defmacro __using__(_opts \\ nil) do + quote do + @behaviour CoreWeb.Live.Feature.Uri + + @impl true + def handle_uri(socket), do: socket + + defoverridable handle_uri: 1 + end + end +end diff --git a/core/lib/core_web/live/feature/viewport.ex b/core/lib/core_web/live/feature/viewport.ex new file mode 100644 index 000000000..2e7b4e92c --- /dev/null +++ b/core/lib/core_web/live/feature/viewport.ex @@ -0,0 +1,20 @@ +defmodule CoreWeb.Live.Feature.Viewport do + @callback handle_resize(socket :: Socket.t()) :: Socket.t() + + defmacro __using__(_) do + quote do + @behaviour CoreWeb.Live.Feature.Viewport + + alias CoreWeb.UI.Responsive.Viewport + alias CoreWeb.UI.Responsive.Breakpoint + + # stubs, handled by Live Hook + def handle_event("viewport_changed", _, socket), do: {:noreply, socket} + def handle_info(:viewport_updated, socket), do: {:noreply, socket} + + # optional feature + def handle_resize(socket), do: socket + defoverridable handle_resize: 1 + end + end +end diff --git a/core/lib/core_web/live/hook/actions.ex b/core/lib/core_web/live/hook/actions.ex new file mode 100644 index 000000000..94dceb025 --- /dev/null +++ b/core/lib/core_web/live/hook/actions.ex @@ -0,0 +1,77 @@ +defmodule CoreWeb.Live.Hook.Actions do + use Frameworks.Concept.LiveHook + + @impl true + def on_mount(live_view_module, _params, _session, socket) do + { + :cont, + socket + |> update_actions(live_view_module) + |> handle_action_click(live_view_module) + |> handle_uri(live_view_module) + |> handle_view_model_updated(live_view_module) + |> handle_viewport_updated(live_view_module) + } + end + + defp handle_action_click(socket, live_view_module) do + attach_hook(socket, :tabbar_handle_action_click, :handle_event, fn + "action_click", %{"item" => action_id}, socket -> + {:cont, socket |> handle_action_click(live_view_module, action_id)} + + _, _, socket -> + {:cont, socket} + end) + end + + defp handle_uri(socket, live_view_module) do + attach_hook(socket, :actions_handle_uri, :handle_params, fn _params, _uri, socket -> + {:cont, socket |> update_actions(live_view_module)} + end) + end + + defp handle_view_model_updated(socket, live_view_module) do + attach_hook(socket, :actions_handle_view_model_updated, :handle_info, fn + :view_model_updated, socket -> + {:cont, socket |> update_actions(live_view_module)} + + _, socket -> + {:cont, socket} + end) + end + + defp handle_viewport_updated(socket, live_view_module) do + attach_hook(socket, :actions_viewport_updated, :handle_info, fn + :viewport_updated, socket -> + {:cont, socket |> update_actions(live_view_module)} + + _, socket -> + {:cont, socket} + end) + end + + def handle_action_click( + %{assigns: %{vm: %{actions: actions}}} = socket, + live_view_module, + action_id + ) + when is_binary(action_id) do + action_id = String.to_existing_atom(action_id) + action = Keyword.get(actions, action_id) + + socket + |> action.handle_click.() + |> update_view_model(live_view_module) + |> update_actions(live_view_module) + end + + def update_view_model(socket, live_view_module) do + socket + |> optional_apply(live_view_module, :update_view_model) + |> optional_apply(live_view_module, :handle_update_view_model) + end + + def update_actions(socket, live_view_module) do + optional_apply(socket, live_view_module, :update_actions) + end +end diff --git a/core/lib/core_web/live/hook/base.ex b/core/lib/core_web/live/hook/base.ex new file mode 100644 index 000000000..614a05148 --- /dev/null +++ b/core/lib/core_web/live/hook/base.ex @@ -0,0 +1,17 @@ +defmodule CoreWeb.Live.Hook.Base do + use Frameworks.Concept.LiveHook + + @impl true + def on_mount(live_view_module, _params, _session, socket) do + { + :cont, + socket + |> assign( + live_view_module: live_view_module, + popup: nil, + dialog: nil, + modal: nil + ) + } + end +end diff --git a/core/lib/core_web/live/hook/locale.ex b/core/lib/core_web/live/hook/locale.ex new file mode 100644 index 000000000..7bdf75994 --- /dev/null +++ b/core/lib/core_web/live/hook/locale.ex @@ -0,0 +1,23 @@ +defmodule CoreWeb.Live.Hook.Locale do + @moduledoc "A Live Hook that changes the locale of the current process" + + use Frameworks.Concept.LiveHook + + @impl true + def on_mount(_live_view_module, _params, _session, socket) do + {:cont, socket |> Phoenix.Component.assign(locale: CoreWeb.Live.Hook.Locale.get_locale())} + end + + def put_locale(locale) when is_atom(locale), do: put_locale(Atom.to_string(locale)) + + def put_locale(locale) do + CoreWeb.Cldr.put_locale(locale) + Gettext.put_locale(locale) + Gettext.put_locale(CoreWeb.Gettext, locale) + Gettext.put_locale(Timex.Gettext, locale) + end + + def get_locale() do + Gettext.get_locale() + end +end diff --git a/core/lib/core_web/live/hook/menus.ex b/core/lib/core_web/live/hook/menus.ex new file mode 100644 index 000000000..005ba2b20 --- /dev/null +++ b/core/lib/core_web/live/hook/menus.ex @@ -0,0 +1,70 @@ +defmodule CoreWeb.Live.Hook.Menus do + use Frameworks.Concept.LiveHook + + @impl true + def on_mount(live_view_module, _params, _session, socket) do + { + :cont, + socket + |> update_menu_config(live_view_module) + |> ensure_active_menu_item() + |> update_menus(live_view_module) + |> handle_uri(live_view_module) + |> handle_view_model_updated(live_view_module) + |> handle_viewport_updated(live_view_module) + } + end + + defp update_menu_config(socket, live_view_module) do + menus_config = live_view_module.get_menus_config() + assign(socket, menus_config: menus_config) + end + + def update_menus(socket, live_view_module) do + live_view_module.update_menus(socket) + end + + defp handle_uri(socket, live_view_module) do + attach_hook(socket, :menus_handle_uri, :handle_params, fn _params, _uri, socket -> + {:cont, socket |> update_menus(live_view_module)} + end) + end + + defp handle_view_model_updated(socket, live_view_module) do + attach_hook(socket, :menus_handle_view_model_updated, :handle_info, fn + :view_model_updated, socket -> + {:cont, socket |> update_menus(live_view_module)} + + _, socket -> + {:cont, socket} + end) + end + + defp handle_viewport_updated(socket, live_view_module) do + attach_hook(socket, :menus_viewport_updated, :handle_info, fn + :viewport_updated, socket -> + {:cont, socket |> update_menus(live_view_module)} + + _, socket -> + {:cont, socket} + end) + end + + defp ensure_active_menu_item( + %{assigns: %{menus_config: {menu_builder, menus, active_menu_item}}} = socket + ) do + socket |> assign(menus_config: {menu_builder, menus}, active_menu_item: active_menu_item) + end + + defp ensure_active_menu_item(%{assigns: %{vm: %{active_menu_item: active_menu_item}}} = socket) do + socket |> assign(active_menu_item: active_menu_item) + end + + defp ensure_active_menu_item(%{assigns: %{active_menu_item: _}} = socket) do + socket + end + + defp ensure_active_menu_item(socket) do + socket |> assign(active_menu_item: nil) + end +end diff --git a/core/lib/core_web/live/hook/model.ex b/core/lib/core_web/live/hook/model.ex new file mode 100644 index 000000000..ffb225e54 --- /dev/null +++ b/core/lib/core_web/live/hook/model.ex @@ -0,0 +1,16 @@ +defmodule CoreWeb.Live.Hook.Model do + @moduledoc "A Live Hook that injects the LiveView data model" + use Frameworks.Concept.LiveHook + + @impl true + def on_mount(live_view_module, params, session, socket) do + model = + Frameworks.Utility.Module.optional_apply(live_view_module, :get_model, [ + params, + session, + socket + ]) + + {:cont, socket |> assign(model: model)} + end +end diff --git a/core/lib/core_web/live/hook/remote_ip.ex b/core/lib/core_web/live/hook/remote_ip.ex new file mode 100644 index 000000000..6316a04d2 --- /dev/null +++ b/core/lib/core_web/live/hook/remote_ip.ex @@ -0,0 +1,23 @@ +defmodule CoreWeb.Live.Hook.RemoteIp do + @moduledoc "A Live Hook that injects the remote_ip from a session variable." + use Frameworks.Concept.LiveHook + + @impl true + def on_mount(_live_view_module, _params, %{"remote_ip" => remote_ip}, socket) do + {:cont, socket |> assign(remote_ip: remote_ip)} + end +end + +defmodule CoreWeb.Plug.RemoteIp do + @moduledoc "A Plug that sets a session variable to the current remote ip." + import Plug.Conn, only: [put_session: 3] + + def init(options) do + options + end + + def call(%{remote_ip: remote_ip} = conn, _opts) do + remote_ip = to_string(:inet_parse.ntoa(remote_ip)) + put_session(conn, :remote_ip, remote_ip) + end +end diff --git a/core/lib/core_web/live/hook/tabbar.ex b/core/lib/core_web/live/hook/tabbar.ex new file mode 100644 index 000000000..1572dec63 --- /dev/null +++ b/core/lib/core_web/live/hook/tabbar.ex @@ -0,0 +1,27 @@ +defmodule CoreWeb.Live.Hook.Tabbar do + use Frameworks.Concept.LiveHook + + @impl true + def on_mount(live_view_module, _params, _session, socket) do + { + :cont, + socket + |> update_tabbar_size(live_view_module) + |> handle_viewport_updated(live_view_module) + } + end + + defp handle_viewport_updated(socket, live_view_module) do + attach_hook(socket, :tabbar_viewport_updated, :handle_info, fn + :viewport_updated, socket -> + {:cont, socket |> update_tabbar_size(live_view_module)} + + _, socket -> + {:cont, socket} + end) + end + + defp update_tabbar_size(socket, live_view_module) do + optional_apply(socket, live_view_module, :update_tabbar_size) + end +end diff --git a/core/lib/core_web/live/hook/timezone.ex b/core/lib/core_web/live/hook/timezone.ex new file mode 100644 index 000000000..50b31b22c --- /dev/null +++ b/core/lib/core_web/live/hook/timezone.ex @@ -0,0 +1,14 @@ +defmodule CoreWeb.Live.Hook.Timezone do + use Frameworks.Concept.LiveHook + + @impl true + def on_mount(_live_view_module, _params, _session, socket) do + timezone = + case {connected?(socket), get_connect_params(socket)} do + {true, %{"timezone" => timezone}} -> timezone + _ -> "Europe/Amsterdam" + end + + {:cont, assign(socket, timezone: timezone)} + end +end diff --git a/core/lib/core_web/live/hook/uri.ex b/core/lib/core_web/live/hook/uri.ex new file mode 100644 index 000000000..371bd6c3c --- /dev/null +++ b/core/lib/core_web/live/hook/uri.ex @@ -0,0 +1,43 @@ +defmodule CoreWeb.Live.Hook.Uri do + @moduledoc "A LiveView helper that automatically sets the current locale from a session variable." + use Frameworks.Concept.LiveHook + + @impl true + def on_mount(live_view_module, _params, _session, socket) do + { + :cont, + socket + |> assign( + uri: nil, + uri_origin: nil, + uri_path: nil + ) + |> handle_uri(live_view_module) + } + end + + defp handle_uri(socket, live_view_module) do + attach_hook(socket, :handle_uri, :handle_params, fn params, uri, socket -> + parsed_uri = URI.parse(uri) + uri_origin = "#{parsed_uri.scheme}://#{parsed_uri.authority}" + + uri_path = + case parsed_uri.query do + nil -> parsed_uri.path + query -> "#{parsed_uri.path}?#{query}" + end + + socket = + assign(socket, + params: params, + uri: uri, + uri_origin: uri_origin, + uri_path: uri_path + ) + + socket = optional_apply(socket, live_view_module, :handle_uri) + + {:cont, socket} + end) + end +end diff --git a/core/lib/core_web/live/hook/user.ex b/core/lib/core_web/live/hook/user.ex new file mode 100644 index 000000000..4bcb8ffc7 --- /dev/null +++ b/core/lib/core_web/live/hook/user.ex @@ -0,0 +1,20 @@ +defmodule CoreWeb.Live.Hook.User do + @moduledoc """ + Live Hook that injects the current user. + """ + use Frameworks.Concept.LiveHook + alias Systems.Account + + @impl true + def on_mount(_live_view_module, _params, session, socket) do + {:cont, socket |> assign(current_user: current_user(session))} + end + + defp current_user(%{assigns: %{current_user: current_user}}), do: current_user + + defp current_user(%{"user_token" => user_token}) do + Account.Public.get_user_by_session_token(user_token) + end + + defp current_user(_), do: nil +end diff --git a/core/lib/core_web/live/hook/viewport.ex b/core/lib/core_web/live/hook/viewport.ex new file mode 100644 index 000000000..0079e9430 --- /dev/null +++ b/core/lib/core_web/live/hook/viewport.ex @@ -0,0 +1,41 @@ +defmodule CoreWeb.Live.Hook.Viewport do + @moduledoc """ + Live Hook that injects the current viewport and breakpoint. + """ + use Frameworks.Concept.LiveHook + + import CoreWeb.UI.Responsive.Breakpoint + import CoreWeb.UI.Responsive.Viewport + + @impl true + def on_mount(live_view_module, _params, _session, socket) do + { + :cont, + socket + |> assign_breakpoint() + |> handle_viewport_changed(live_view_module) + } + end + + defp handle_viewport_changed(socket, live_view_module) do + attach_hook(socket, :handle_viewport_changed, :handle_event, fn + "viewport_changed", new_viewport, socket -> + {:cont, socket |> update_viewport(live_view_module, new_viewport)} + + _, _, socket -> + {:cont, socket} + end) + end + + defp update_viewport(socket, live_view_module, new_viewport) do + new_breakpoint = breakpoint(new_viewport) + + send(self(), :viewport_updated) + + socket + |> assign(viewport: new_viewport) + |> assign(breakpoint: new_breakpoint) + |> optional_apply(live_view_module, :update_view_model) + |> optional_apply(live_view_module, :handle_resize) + end +end diff --git a/core/lib/core_web/live/menu/builder.ex b/core/lib/core_web/live/menu/builder.ex index 2c029fcd2..b65e42418 100644 --- a/core/lib/core_web/live/menu/builder.ex +++ b/core/lib/core_web/live/menu/builder.ex @@ -4,9 +4,10 @@ defmodule CoreWeb.Menu.Builder do @type active_item :: atom() @type menu :: map() @type user :: map() | nil + @type uri :: binary() | nil @type item :: atom() - @callback build_menu(socket, type, active_item) :: menu + @callback build_menu(type, active_item, user, uri) :: menu @callback include_map(user) :: map() alias Systems.Admin @@ -29,22 +30,20 @@ defmodule CoreWeb.Menu.Builder do import CoreWeb.Menu.Helpers @impl true - def build_menu(assigns, menu_id, active_item) do - builder = &build_item(assigns, menu_id, &1, active_item, @item_flags) + def build_menu(menu_id, active_item, user, uri) do + builder = &build_item(menu_id, &1, active_item, @item_flags, user, uri) primary = select_items(menu_id, @primary) secondary = select_items(menu_id, @secondary) %{ - home: build_home(assigns, menu_id, unquote(home), @home_flags), - primary: build(assigns, primary, builder), - secondary: build(assigns, secondary, builder) + home: build_home(menu_id, unquote(home), @home_flags, uri), + primary: build(primary, builder, user), + secondary: build(secondary, builder, user) } end - defp build(assigns, items, builder) do - user = Map.get(assigns, :current_user) - + defp build(items, builder, user) do include_map = Map.merge( unquote(__MODULE__).include_map(user), diff --git a/core/lib/core_web/live/menu/helpers.ex b/core/lib/core_web/live/menu/helpers.ex index 854e57399..f998e0821 100644 --- a/core/lib/core_web/live/menu/helpers.ex +++ b/core/lib/core_web/live/menu/helpers.ex @@ -10,24 +10,9 @@ defmodule CoreWeb.Menu.Helpers do require CoreWeb.Gettext - def home_item(menu_id, id, action, size) when is_atom(id) do - face = %{ - type: :menu_home, - icon: id, - size: size - } - - %{ - id: id, - menu_id: menu_id, - action: action, - face: face - } - end - - def build_home(assigns, menu_id, id, config) do + def build_home(menu_id, id, config, uri) do if Keyword.has_key?(config, menu_id) do - action = action(assigns, id) + action = action(id, uri) flags = select_flags(menu_id, id, config) size = @@ -43,6 +28,21 @@ defmodule CoreWeb.Menu.Helpers do end end + defp home_item(menu_id, id, action, size) when is_atom(id) do + face = %{ + type: :menu_home, + icon: id, + size: size + } + + %{ + id: id, + menu_id: menu_id, + action: action, + face: face + } + end + def menu_item(menu_id, id, active?, action, %{} = opts) when is_atom(id) do face = %{ type: :menu_item, @@ -60,22 +60,20 @@ defmodule CoreWeb.Menu.Helpers do } end - def menu_item(assigns, menu_id, id, active_item, flags) when is_list(flags) do + def menu_item(menu_id, id, active_item, flags, user, uri) when is_list(flags) do active? = id == active_item - action = action(assigns, id) - opts = opts(assigns, id, flags) + action = action(id, uri) + opts = opts(id, flags, user) menu_item(menu_id, id, active?, action, opts) end - def build_item(assigns, menu_id, id, active_item, config) do + def build_item(menu_id, id, active_item, config, user, uri) do flags = select_flags(menu_id, id, config) - menu_item(assigns, menu_id, id, active_item, flags) + menu_item(menu_id, id, active_item, flags, user, uri) end - defp opts(assigns, id, flags) when is_list(flags) do - user = Map.get(assigns, :current_user) - + defp opts(id, flags, user) when is_list(flags) do icon = if Enum.member?(flags, :icon) do icon(id) @@ -104,7 +102,9 @@ defmodule CoreWeb.Menu.Helpers do } end - def action(%{uri: uri} = assigns, :language) do + def action(:language, nil), do: action(:language, nil, "\\") + + def action(:language, uri) do parsed_uri = URI.parse(uri) redirect_url = @@ -113,14 +113,12 @@ defmodule CoreWeb.Menu.Helpers do query -> "#{parsed_uri.path}?#{query}" end - action(assigns, :language, redirect_url) + action(:language, uri, redirect_url) end - def action(assigns, :language), do: action(assigns, :language, "\\") - - def action(_assigns, id) when is_atom(id), do: ItemsProvider.action(id) + def action(id, _uri) when is_atom(id), do: ItemsProvider.action(id) - def action(_assigns, :language, redirect_url) when is_binary(redirect_url) do + def action(:language, _uri, redirect_url) when is_binary(redirect_url) do [locale] = supported_languages() %{ diff --git a/core/lib/core_web/live_locale.ex b/core/lib/core_web/live_locale.ex deleted file mode 100644 index 41a5c7f54..000000000 --- a/core/lib/core_web/live_locale.ex +++ /dev/null @@ -1,34 +0,0 @@ -defmodule CoreWeb.LiveLocale do - @moduledoc "A LiveView helper that changes the locale of the current process" - - def put_locale(locale) when is_atom(locale), do: put_locale(Atom.to_string(locale)) - - def put_locale(locale) do - CoreWeb.Cldr.put_locale(locale) - Gettext.put_locale(locale) - Gettext.put_locale(CoreWeb.Gettext, locale) - Gettext.put_locale(Timex.Gettext, locale) - end - - def get_locale() do - Gettext.get_locale() - end - - defmacro __using__(_opts \\ nil) do - quote do - @before_compile CoreWeb.LiveLocale - import CoreWeb.LiveLocale - end - end - - defmacro __before_compile__(_env) do - quote do - defoverridable mount: 3 - - @impl true - def mount(params, session, socket) do - super(params, session, socket |> assign(locale: CoreWeb.LiveLocale.get_locale())) - end - end - end -end diff --git a/core/lib/core_web/live_menus.ex b/core/lib/core_web/live_menus.ex deleted file mode 100644 index 1a6539a76..000000000 --- a/core/lib/core_web/live_menus.ex +++ /dev/null @@ -1,63 +0,0 @@ -defmodule CoreWeb.LiveMenus do - defmacro __using__({menu_builder, menus}) do - quote do - @before_compile CoreWeb.LiveMenus - import Phoenix.Component - - def builder, do: Application.fetch_env!(:core, unquote(menu_builder)) - - def build_menu(%{active_menu_item: active_menu_item} = assigns, type) do - builder().build_menu(assigns, type, active_menu_item) - end - - def build_menu(%{vm: %{active_menu_item: active_menu_item}} = assigns, type) do - builder().build_menu(assigns, type, active_menu_item) - end - - def build_menus(%{assigns: assigns} = socket) do - menus = build_menus(assigns) - socket |> assign(menus: menus) - end - - def build_menus(%{authorization_failed: true}), do: nil - - def build_menus(assigns) do - Enum.reduce(unquote(menus), %{}, fn menu_id, acc -> - Map.put(acc, menu_id, build_menu(assigns, menu_id)) - end) - end - - def update_menus(socket) do - socket - |> build_menus() - end - end - end - - defmacro __before_compile__(_env) do - quote do - defoverridable mount: 3 - - @impl true - def mount(params, session, socket) do - {:ok, socket} = super(params, session, socket) - {:ok, socket |> update_menus()} - end - - defoverridable handle_uri: 1 - - def handle_uri(socket) do - super(socket) - |> update_menus() - end - - defoverridable handle_view_model_updated: 1 - - @impl true - def handle_view_model_updated(socket) do - super(socket) - |> update_menus() - end - end - end -end diff --git a/core/lib/core_web/live_remote_ip.ex b/core/lib/core_web/live_remote_ip.ex deleted file mode 100644 index fe0d8d3b5..000000000 --- a/core/lib/core_web/live_remote_ip.ex +++ /dev/null @@ -1,33 +0,0 @@ -defmodule CoreWeb.Plug.LiveRemoteIp do - @moduledoc "A Plug that sets a session variable to the current remote ip." - import Plug.Conn, only: [put_session: 3] - - def init(options) do - options - end - - def call(%{remote_ip: remote_ip} = conn, _opts) do - remote_ip = to_string(:inet_parse.ntoa(remote_ip)) - put_session(conn, :remote_ip, remote_ip) - end -end - -defmodule CoreWeb.LiveRemoteIp do - @moduledoc "A LiveView helper that automatically sets the current remote_ip from a session variable." - - defmacro __using__(_opts \\ nil) do - quote do - @before_compile CoreWeb.LiveRemoteIp - end - end - - defmacro __before_compile__(_env) do - quote do - defoverridable mount: 3 - - def mount(params, %{"remote_ip" => remote_ip} = session, socket) do - super(params, session, socket |> assign(remote_ip: remote_ip)) - end - end - end -end diff --git a/core/lib/core_web/live_timezone.ex b/core/lib/core_web/live_timezone.ex deleted file mode 100644 index f19057daf..000000000 --- a/core/lib/core_web/live_timezone.ex +++ /dev/null @@ -1,31 +0,0 @@ -defmodule CoreWeb.LiveTimezone do - import Phoenix.LiveView, only: [connected?: 1, get_connect_params: 1] - - def update_timezone(socket, _session) do - timezone = - case {connected?(socket), get_connect_params(socket)} do - {true, %{"timezone" => timezone}} -> timezone - _ -> "Europe/Amsterdam" - end - - Phoenix.Component.assign(socket, timezone: timezone) - end - - defmacro __using__(_opts \\ nil) do - quote do - @before_compile CoreWeb.LiveTimezone - import CoreWeb.LiveTimezone - end - end - - defmacro __before_compile__(_env) do - quote do - defoverridable mount: 3 - - @impl true - def mount(params, session, socket) do - super(params, session, socket |> update_timezone(session)) - end - end - end -end diff --git a/core/lib/core_web/live_uri.ex b/core/lib/core_web/live_uri.ex deleted file mode 100644 index ccca92ae5..000000000 --- a/core/lib/core_web/live_uri.ex +++ /dev/null @@ -1,34 +0,0 @@ -defmodule CoreWeb.LiveUri do - @moduledoc "A LiveView helper that automatically sets the current locale from a session variable." - - @callback handle_uri(Socket.t()) :: Socket.t() - - defmacro __using__(_opts \\ nil) do - quote do - @behaviour CoreWeb.LiveUri - - import Phoenix.LiveView - - def handle_params(unsigned_params, uri, socket) do - parsed_uri = URI.parse(uri) - uri_origin = "#{parsed_uri.scheme}://#{parsed_uri.authority}" - - uri_path = - case parsed_uri.query do - nil -> parsed_uri.path - query -> "#{parsed_uri.path}?#{query}" - end - - { - :noreply, - socket - |> assign(:params, unsigned_params) - |> assign(:uri, uri) - |> assign(:uri_origin, uri_origin) - |> assign(:uri_path, uri_path) - |> handle_uri() - } - end - end - end -end diff --git a/core/lib/core_web/live_user.ex b/core/lib/core_web/live_user.ex deleted file mode 100644 index f061c49bd..000000000 --- a/core/lib/core_web/live_user.ex +++ /dev/null @@ -1,36 +0,0 @@ -defmodule CoreWeb.LiveUser do - alias Systems.Account - - @moduledoc """ - Automatically setup the current user in LiveViews. - """ - def current_user(%{assigns: %{current_user: current_user}}), do: current_user - - def current_user(%{"user_token" => user_token}) do - Account.Public.get_user_by_session_token(user_token) - end - - def current_user(_), do: nil - - defmacro __using__(_opts \\ nil) do - quote do - @before_compile CoreWeb.LiveUser - import CoreWeb.LiveUser - end - end - - defmacro __before_compile__(_env) do - quote do - defoverridable mount: 3 - - def mount(params, session, socket) do - super( - params, - session, - socket - |> Phoenix.Component.assign(current_user: current_user(session)) - ) - end - end - end -end diff --git a/core/lib/core_web/menus.ex b/core/lib/core_web/menus.ex new file mode 100644 index 000000000..ea65d67b8 --- /dev/null +++ b/core/lib/core_web/menus.ex @@ -0,0 +1,18 @@ +defmodule CoreWeb.Menus do + def build_menus({menu_builder, menus, active_menu_item}, user, uri), + do: build_menus(menu_builder, menus, active_menu_item, user, uri) + + def build_menus(menu_builder, menus, active_menu_item, user, uri) do + Enum.reduce(menus, %{}, fn menu_item, acc -> + Map.put(acc, menu_item, build_menu(menu_builder, menu_item, active_menu_item, user, uri)) + end) + end + + defp build_menu(menu_builder, menu_item, active_menu_item, user, uri) do + menu_builder_module(menu_builder).build_menu(menu_item, active_menu_item, user, uri) + end + + def menu_builder_module(menu_builder) do + Application.fetch_env!(:core, menu_builder) + end +end diff --git a/core/lib/core_web/routes.ex b/core/lib/core_web/routes.ex index ed7da0390..f1571c7cb 100644 --- a/core/lib/core_web/routes.ex +++ b/core/lib/core_web/routes.ex @@ -24,7 +24,7 @@ defmodule CoreWeb.Routes do ) plug(RemoteIp) - plug(CoreWeb.Plug.LiveRemoteIp) + plug(CoreWeb.Plug.RemoteIp) plug(:fetch_live_flash) plug(:fetch_meta_info) @@ -70,6 +70,11 @@ defmodule CoreWeb.Routes do require Systems.Routes Systems.Routes.routes() + scope "/", CoreWeb do + pipe_through(:browser_unprotected) + get("/access_denied", ErrorController, :access_denied) + end + scope "/", CoreWeb do pipe_through(:api) diff --git a/core/lib/core_web/ui/responsive/viewport.ex b/core/lib/core_web/ui/responsive/viewport.ex index 3dc30ca9c..9702571e9 100644 --- a/core/lib/core_web/ui/responsive/viewport.ex +++ b/core/lib/core_web/ui/responsive/viewport.ex @@ -1,31 +1,6 @@ defmodule CoreWeb.UI.Responsive.Viewport do - import Phoenix.Component - import CoreWeb.UI.Responsive.Breakpoint - - @callback handle_resize(socket :: Socket.t()) :: Socket.t() - - defmacro __using__(_) do - quote do - @behaviour CoreWeb.UI.Responsive.Viewport - - import CoreWeb.UI.Responsive.Viewport - import CoreWeb.UI.Responsive.Breakpoint - - alias CoreWeb.UI.Responsive.Breakpoint - - def handle_event("viewport_resize", new_viewport, socket) do - new_breakpoint = breakpoint(new_viewport) - - { - :noreply, - socket - |> assign(viewport: new_viewport) - |> assign(breakpoint: new_breakpoint) - |> handle_resize() - } - end - end - end + import Phoenix.Component, only: [assign: 2] + import CoreWeb.UI.Responsive.Breakpoint, only: [breakpoint: 1] def assign_viewport(%{private: %{connect_params: %{"viewport" => viewport}}} = socket) do assign(socket, viewport: viewport) diff --git a/core/mix.exs b/core/mix.exs index 149ba9d04..ea32d763b 100644 --- a/core/mix.exs +++ b/core/mix.exs @@ -10,6 +10,7 @@ defmodule Core.MixProject do elixirc_paths: elixirc_paths(Mix.env()), compilers: Mix.compilers(), start_permanent: Mix.env() == :prod, + consolidate_protocols: Mix.env() != :test, aliases: aliases(), deps: deps(), # The main page in the docs @@ -119,6 +120,8 @@ defmodule Core.MixProject do {:file_system, "~> 0.2", only: [:dev, :test]}, {:bypass, "~> 2.1", only: :test}, {:mox, "~> 1.0", only: :test}, + {:promox, "~> 0.1.0", only: :test}, + {:mock, "~> 0.3.0", only: :test}, {:progress_bar, "~> 2.0.1", only: [:dev, :test]}, {:phoenix_live_reload, "~> 1.3", only: :dev}, {:credo, "~> 1.6", only: [:dev, :test], runtime: false}, diff --git a/core/mix.lock b/core/mix.lock index 25a86613c..8c1412c14 100644 --- a/core/mix.lock +++ b/core/mix.lock @@ -68,10 +68,12 @@ "makeup": {:hex, :makeup, "1.1.0", "6b67c8bc2882a6b6a445859952a602afc1a41c2e08379ca057c0f525366fc3ca", [:mix], [{:nimble_parsec, "~> 1.2.2 or ~> 1.3", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "0a45ed501f4a8897f580eabf99a2e5234ea3e75a4373c8a52824f6e873be57a6"}, "makeup_elixir": {:hex, :makeup_elixir, "0.16.0", "f8c570a0d33f8039513fbccaf7108c5d750f47d8defd44088371191b76492b0b", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}, {:nimble_parsec, "~> 1.2.3", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "28b2cbdc13960a46ae9a8858c4bebdec3c9a6d7b4b9e7f4ed1502f8159f338e7"}, "makeup_erlang": {:hex, :makeup_erlang, "0.1.1", "3fcb7f09eb9d98dc4d208f49cc955a34218fc41ff6b84df7c75b3e6e533cc65f", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}], "hexpm", "174d0809e98a4ef0b3309256cbf97101c6ec01c4ab0b23e926a9e17df2077cbb"}, + "meck": {:hex, :meck, "0.9.2", "85ccbab053f1db86c7ca240e9fc718170ee5bda03810a6292b5306bf31bae5f5", [:rebar3], [], "hexpm", "81344f561357dc40a8344afa53767c32669153355b626ea9fcbc8da6b3045826"}, "metrics": {:hex, :metrics, "1.0.1", "25f094dea2cda98213cecc3aeff09e940299d950904393b2a29d191c346a8486", [:rebar3], [], "hexpm", "69b09adddc4f74a40716ae54d140f93beb0fb8978d8636eaded0c31b6f099f16"}, "mime": {:hex, :mime, "2.0.5", "dc34c8efd439abe6ae0343edbb8556f4d63f178594894720607772a041b04b02", [:mix], [], "hexpm", "da0d64a365c45bc9935cc5c8a7fc5e49a0e0f9932a761c55d6c52b142780a05c"}, "mimerl": {:hex, :mimerl, "1.3.0", "d0cd9fc04b9061f82490f6581e0128379830e78535e017f7780f37fea7545726", [:rebar3], [], "hexpm", "a1e15a50d1887217de95f0b9b0793e32853f7c258a5cd227650889b38839fe9d"}, "mint": {:hex, :mint, "1.4.2", "50330223429a6e1260b2ca5415f69b0ab086141bc76dc2fbf34d7c389a6675b2", [:mix], [{:castore, "~> 0.1.0", [hex: :castore, repo: "hexpm", optional: true]}, {:hpax, "~> 0.1.1", [hex: :hpax, repo: "hexpm", optional: false]}], "hexpm", "ce75a5bbcc59b4d7d8d70f8b2fc284b1751ffb35c7b6a6302b5192f8ab4ddd80"}, + "mock": {:hex, :mock, "0.3.8", "7046a306b71db2488ef54395eeb74df0a7f335a7caca4a3d3875d1fc81c884dd", [:mix], [{:meck, "~> 0.9.2", [hex: :meck, repo: "hexpm", optional: false]}], "hexpm", "7fa82364c97617d79bb7d15571193fc0c4fe5afd0c932cef09426b3ee6fe2022"}, "mox": {:hex, :mox, "1.0.2", "dc2057289ac478b35760ba74165b4b3f402f68803dd5aecd3bfd19c183815d64", [:mix], [], "hexpm", "f9864921b3aaf763c8741b5b8e6f908f44566f1e427b2630e89e9a73b981fef2"}, "nimble_options": {:hex, :nimble_options, "0.4.0", "c89babbab52221a24b8d1ff9e7d838be70f0d871be823165c94dd3418eea728f", [:mix], [], "hexpm", "e6701c1af326a11eea9634a3b1c62b475339ace9456c1a23ec3bc9a847bca02d"}, "nimble_parsec": {:hex, :nimble_parsec, "1.2.3", "244836e6e3f1200c7f30cb56733fd808744eca61fd182f731eac4af635cc6d0b", [:mix], [], "hexpm", "c8d789e39b9131acf7b99291e93dae60ab48ef14a7ee9d58c6964f59efb570b0"}, @@ -96,6 +98,7 @@ "poison": {:hex, :poison, "3.1.0", "d9eb636610e096f86f25d9a46f35a9facac35609a7591b3be3326e99a0484665", [:mix], [], "hexpm", "fec8660eb7733ee4117b85f55799fd3833eb769a6df71ccf8903e8dc5447cfce"}, "postgrex": {:hex, :postgrex, "0.16.5", "fcc4035cc90e23933c5d69a9cd686e329469446ef7abba2cf70f08e2c4b69810", [:mix], [{:connection, "~> 1.1", [hex: :connection, repo: "hexpm", optional: false]}, {:db_connection, "~> 2.1", [hex: :db_connection, repo: "hexpm", optional: false]}, {:decimal, "~> 1.5 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:table, "~> 0.1.0", [hex: :table, repo: "hexpm", optional: true]}], "hexpm", "edead639dc6e882618c01d8fc891214c481ab9a3788dfe38dd5e37fd1d5fb2e8"}, "progress_bar": {:hex, :progress_bar, "2.0.1", "7b40200112ae533d5adceb80ff75fbe66dc753bca5f6c55c073bfc122d71896d", [:mix], [{:decimal, "~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}], "hexpm", "2519eb58a2f149a3a094e729378256d8cb6d96a259ec94841bd69fdc71f18f87"}, + "promox": {:hex, :promox, "0.1.4", "6a833b8b954ecf534ad5ccc9c74550b636e996f433f75ced2cadfd1a15e10d0c", [:mix], [], "hexpm", "7e0a54fb183fb705f8711255d3f5beb3519284b69ccf468bed3dc9c1e661f455"}, "ranch": {:hex, :ranch, "1.8.0", "8c7a100a139fd57f17327b6413e4167ac559fbc04ca7448e9be9057311597a1d", [:make, :rebar3], [], "hexpm", "49fbcfd3682fab1f5d109351b61257676da1a2fdbe295904176d5e521a2ddfe5"}, "remote_ip": {:hex, :remote_ip, "1.1.0", "cb308841595d15df3f9073b7c39243a1dd6ca56e5020295cb012c76fbec50f2d", [:mix], [{:combine, "~> 0.10", [hex: :combine, repo: "hexpm", optional: false]}, {:plug, "~> 1.14", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "616ffdf66aaad6a72fc546dabf42eed87e2a99e97b09cbd92b10cc180d02ed74"}, "sentry": {:hex, :sentry, "8.1.0", "8d235b62fce5f8e067ea1644e30939405b71a5e1599d9529ff82899d11d03f2b", [:mix], [{:hackney, "~> 1.8", [hex: :hackney, repo: "hexpm", optional: true]}, {:jason, "~> 1.1", [hex: :jason, repo: "hexpm", optional: true]}, {:plug, "~> 1.6", [hex: :plug, repo: "hexpm", optional: true]}, {:plug_cowboy, "~> 2.3", [hex: :plug_cowboy, repo: "hexpm", optional: true]}], "hexpm", "f9fc7641ef61e885510f5e5963c2948b9de1de597c63f781e9d3d6c9c8681ab4"}, diff --git a/core/systems/account/await_confirmation.ex b/core/systems/account/await_confirmation.ex index 491dd6b3d..63792fa19 100644 --- a/core/systems/account/await_confirmation.ex +++ b/core/systems/account/await_confirmation.ex @@ -1,10 +1,19 @@ defmodule Systems.Account.AwaitConfirmation do use CoreWeb, :live_view + + on_mount({CoreWeb.Live.Hook.Base, __MODULE__}) + on_mount({CoreWeb.Live.Hook.User, __MODULE__}) + on_mount({CoreWeb.Live.Hook.Uri, __MODULE__}) + on_mount({Frameworks.GreenLight.LiveHook, __MODULE__}) + on_mount({Frameworks.Fabric.LiveHook, __MODULE__}) + import CoreWeb.Layouts.Stripped.Html import CoreWeb.Layouts.Stripped.Composer + import CoreWeb.Menus alias Frameworks.Pixel.Text + @impl true def mount(_params, _session, socket) do require_feature(:password_sign_in) @@ -16,6 +25,11 @@ defmodule Systems.Account.AwaitConfirmation do } end + def update_menus(%{assigns: %{current_user: user, uri: uri}} = socket) do + menus = build_menus(stripped_menus_config(), user, uri) + assign(socket, menus: menus) + end + # data(changeset, :any) @impl true def render(assigns) do diff --git a/core/systems/account/confirm_token.ex b/core/systems/account/confirm_token.ex index adbedefc6..85678ea16 100644 --- a/core/systems/account/confirm_token.ex +++ b/core/systems/account/confirm_token.ex @@ -1,19 +1,24 @@ defmodule Systems.Account.ConfirmToken do - @moduledoc """ - The home screen. - """ use CoreWeb, :live_view + + on_mount({CoreWeb.Live.Hook.Base, __MODULE__}) + on_mount({CoreWeb.Live.Hook.User, __MODULE__}) + on_mount({CoreWeb.Live.Hook.Uri, __MODULE__}) + on_mount({Frameworks.GreenLight.LiveHook, __MODULE__}) + on_mount({Frameworks.Fabric.LiveHook, __MODULE__}) + import CoreWeb.Layouts.Stripped.Html import CoreWeb.Layouts.Stripped.Composer - + import CoreWeb.Menus import Frameworks.Pixel.Form - alias Frameworks.Pixel.Button + alias Frameworks.Pixel.Button alias Systems.Account alias Systems.Account.User require Logger + @impl true def mount(%{"token" => token}, _session, socket) do require_feature(:password_sign_in) @@ -30,6 +35,11 @@ defmodule Systems.Account.ConfirmToken do } end + def update_menus(%{assigns: %{current_user: user, uri: uri}} = socket) do + menus = build_menus(stripped_menus_config(), user, uri) + assign(socket, menus: menus) + end + defp update_confirm_button(socket) do confirm_button = %{ action: %{type: :send, event: "confirm"}, diff --git a/core/systems/account/reset_password.ex b/core/systems/account/reset_password.ex index 2099aecc4..24a4d60b8 100644 --- a/core/systems/account/reset_password.ex +++ b/core/systems/account/reset_password.ex @@ -3,9 +3,14 @@ defmodule Systems.Account.ResetPassword do The home screen. """ use CoreWeb, :live_view + + on_mount({CoreWeb.Live.Hook.Base, __MODULE__}) + on_mount({CoreWeb.Live.Hook.User, __MODULE__}) + on_mount({CoreWeb.Live.Hook.Uri, __MODULE__}) + import CoreWeb.Layouts.Stripped.Html import CoreWeb.Layouts.Stripped.Composer - + import CoreWeb.Menus import Frameworks.Pixel.Form alias Systems.Account @@ -13,6 +18,7 @@ defmodule Systems.Account.ResetPassword do alias Frameworks.Pixel.Text alias Frameworks.Pixel.Button + @impl true def mount(_params, _session, socket) do { :ok, @@ -22,6 +28,11 @@ defmodule Systems.Account.ResetPassword do } end + def update_menus(%{assigns: %{current_user: user, uri: uri}} = socket) do + menus = build_menus(stripped_menus_config(), user, uri) + assign(socket, menus: menus) + end + @impl true def handle_event("reset-password", %{"user" => %{"email" => email}}, socket) do case User.valid_email_changeset(email) do diff --git a/core/systems/account/reset_password_token.ex b/core/systems/account/reset_password_token.ex index ce1a9dd84..720e57356 100644 --- a/core/systems/account/reset_password_token.ex +++ b/core/systems/account/reset_password_token.ex @@ -3,14 +3,22 @@ defmodule Systems.Account.ResetPasswordToken do The password reset token. """ use CoreWeb, :live_view + + on_mount({CoreWeb.Live.Hook.Base, __MODULE__}) + on_mount({CoreWeb.Live.Hook.User, __MODULE__}) + on_mount({CoreWeb.Live.Hook.Uri, __MODULE__}) + on_mount({Frameworks.GreenLight.LiveHook, __MODULE__}) + on_mount({Frameworks.Fabric.LiveHook, __MODULE__}) + import CoreWeb.Layouts.Stripped.Html import CoreWeb.Layouts.Stripped.Composer - + import CoreWeb.Menus import Frameworks.Pixel.Form alias Systems.Account alias Frameworks.Pixel.Button + @impl true def mount(%{"token" => token}, _session, socket) do if user = Account.Public.get_user_by_reset_password_token(token) do { @@ -31,6 +39,11 @@ defmodule Systems.Account.ResetPasswordToken do end end + def update_menus(%{assigns: %{current_user: user, uri: uri}} = socket) do + menus = build_menus(stripped_menus_config(), user, uri) + assign(socket, menus: menus) + end + @impl true def handle_event( "reset-password", diff --git a/core/systems/account/signup_page.ex b/core/systems/account/signup_page.ex index 2308b8e39..60bfaa3cf 100644 --- a/core/systems/account/signup_page.ex +++ b/core/systems/account/signup_page.ex @@ -3,13 +3,22 @@ defmodule Systems.Account.SignupPage do The home screen. """ use CoreWeb, :live_view + + on_mount({CoreWeb.Live.Hook.Base, __MODULE__}) + on_mount({CoreWeb.Live.Hook.User, __MODULE__}) + on_mount({CoreWeb.Live.Hook.Uri, __MODULE__}) + on_mount({Frameworks.GreenLight.LiveHook, __MODULE__}) + on_mount({Frameworks.Fabric.LiveHook, __MODULE__}) + import CoreWeb.Layouts.Stripped.Html import CoreWeb.Layouts.Stripped.Composer + import CoreWeb.Menus alias Systems.Account alias Systems.Account.UserForm alias Systems.Account.User + @impl true def mount(%{"user_type" => user_type}, _session, socket) do require_feature(:password_sign_in) creator? = user_type == "creator" @@ -27,6 +36,11 @@ defmodule Systems.Account.SignupPage do } end + def update_menus(%{assigns: %{current_user: user, uri: uri}} = socket) do + menus = build_menus(stripped_menus_config(), user, uri) + assign(socket, menus: menus) + end + @impl true def handle_event("signup", %{"user" => user_params}, %{assigns: %{creator?: creator?}} = socket) do user_params = Map.put(user_params, "creator", creator?) diff --git a/core/systems/account/user_profile_page.ex b/core/systems/account/user_profile_page.ex index 9e5d60a32..313c5d698 100644 --- a/core/systems/account/user_profile_page.ex +++ b/core/systems/account/user_profile_page.ex @@ -20,12 +20,6 @@ defmodule Systems.Account.UserProfilePage do } end - @impl true - def handle_view_model_updated(socket), do: socket - - @impl true - def handle_uri(socket), do: socket - @impl true def handle_event(_, _payload, socket) do {:noreply, socket} diff --git a/core/systems/admin/config_page.ex b/core/systems/admin/config_page.ex index 6aa7264dc..aac46e5cd 100644 --- a/core/systems/admin/config_page.ex +++ b/core/systems/admin/config_page.ex @@ -12,14 +12,6 @@ defmodule Systems.Admin.ConfigPage do {:ok, socket |> assign(initial_tab: initial_tab)} end - @impl true - def handle_view_model_updated(socket) do - socket - end - - @impl true - def handle_uri(socket), do: socket - @impl true def handle_event("change", _payload, socket) do {:noreply, socket} diff --git a/core/systems/admin/import_rewards_page.ex b/core/systems/admin/import_rewards_page.ex index f77d2744d..c37ac1900 100644 --- a/core/systems/admin/import_rewards_page.ex +++ b/core/systems/admin/import_rewards_page.ex @@ -38,6 +38,7 @@ defmodule Systems.Admin.ImportRewardsPage do Systems.Observatory.SingletonModel.instance() end + @impl true def mount(%{"back" => back}, _session, socket) do entity = %Admin.ImportRewardsModel{} @@ -94,12 +95,6 @@ defmodule Systems.Admin.ImportRewardsPage do } end - @impl true - def handle_view_model_updated(socket), do: socket - - @impl true - def handle_uri(socket), do: socket - @impl true def compose(:currency, %{currency_labels: items}) do %{ diff --git a/core/systems/admin/login_page.ex b/core/systems/admin/login_page.ex index 85f613964..2350f825a 100644 --- a/core/systems/admin/login_page.ex +++ b/core/systems/admin/login_page.ex @@ -1,12 +1,21 @@ defmodule Systems.Admin.LoginPage do use CoreWeb, :live_view + + on_mount({CoreWeb.Live.Hook.Base, __MODULE__}) + on_mount({CoreWeb.Live.Hook.User, __MODULE__}) + on_mount({CoreWeb.Live.Hook.Uri, __MODULE__}) + on_mount({Frameworks.GreenLight.LiveHook, __MODULE__}) + on_mount({Frameworks.Fabric.LiveHook, __MODULE__}) + import CoreWeb.Layouts.Stripped.Html import CoreWeb.Layouts.Stripped.Composer + import CoreWeb.Menus import Ecto.Query alias Core.Repo alias Systems.Account.User + @impl true def mount(_params, _session, socket) do { :ok, @@ -16,6 +25,11 @@ defmodule Systems.Admin.LoginPage do } end + def update_menus(%{assigns: %{current_user: user, uri: uri}} = socket) do + menus = build_menus(stripped_menus_config(), user, uri) + assign(socket, menus: menus) + end + # FIXME: Move this to Accounts defp list_users do from(u in User, order_by: {:asc, :email}) diff --git a/core/systems/advert/_switch.ex b/core/systems/advert/_switch.ex index a2bba122f..9b29c0774 100644 --- a/core/systems/advert/_switch.ex +++ b/core/systems/advert/_switch.ex @@ -4,7 +4,8 @@ defmodule Systems.Advert.Switch do alias Systems.{ Advert, Promotion, - Assignment + Assignment, + Home } @impl true @@ -56,6 +57,7 @@ defmodule Systems.Advert.Switch do update(Promotion.LandingPage, promotion_id, promotion, from_pid) update(Assignment.LandingPage, assignment_id, advert, from_pid) update(Advert.ContentPage, id, advert, from_pid) + update(Home.Page, :singleton, %{id: :singleton}, from_pid) end defp handle({_, _}, _), do: nil diff --git a/core/systems/advert/content_page.ex b/core/systems/advert/content_page.ex index b4c09cab1..abcaa2dbe 100644 --- a/core/systems/advert/content_page.ex +++ b/core/systems/advert/content_page.ex @@ -28,15 +28,6 @@ defmodule Systems.Advert.ContentPage do } end - @impl true - def handle_view_model_updated(socket), do: socket - - @impl true - def handle_resize(socket), do: socket - - @impl true - def handle_uri(socket), do: socket - @impl true def render(assigns) do ~H""" diff --git a/core/systems/advert/content_page_builder.ex b/core/systems/advert/content_page_builder.ex index bb9d49e23..fe270a59c 100644 --- a/core/systems/advert/content_page_builder.ex +++ b/core/systems/advert/content_page_builder.ex @@ -17,13 +17,13 @@ defmodule Systems.Advert.ContentPageBuilder do submission: submission, promotion: promotion } = advert, - assigns + %{branch: branch} = assigns ) do submitted? = Pool.SubmissionModel.submitted?(submission) show_errors = submitted? tabs = create_tabs(advert, show_errors, assigns) - breadcrumbs = create_breadcrumbs(advert) + breadcrumbs = Concept.Branch.hierarchy(branch) action_map = action_map(advert, assigns) actions = actions(advert, action_map) @@ -41,13 +41,6 @@ defmodule Systems.Advert.ContentPageBuilder do } end - defp create_breadcrumbs(advert) do - case Concept.Branch.hierarchy(advert) do - {:ok, hierarchy} -> hierarchy - {:error, _} -> nil - end - end - defp create_tabs(advert, show_errors, assigns) do advert |> get_tab_keys() diff --git a/core/systems/advert/funding_view.ex b/core/systems/advert/funding_view.ex index 26181c884..57f02473a 100644 --- a/core/systems/advert/funding_view.ex +++ b/core/systems/advert/funding_view.ex @@ -58,7 +58,7 @@ defmodule Systems.Advert.FundingView do changeset: changeset, selected_budget: budget, user: user, - locale: CoreWeb.LiveLocale.get_locale() + locale: CoreWeb.Live.Hook.Locale.get_locale() ) |> update_state() |> update_reward() diff --git a/core/systems/advert/promotion_landing_page_builder.ex b/core/systems/advert/promotion_landing_page_builder.ex index 853203ead..7a5d3e48d 100644 --- a/core/systems/advert/promotion_landing_page_builder.ex +++ b/core/systems/advert/promotion_landing_page_builder.ex @@ -26,7 +26,7 @@ defmodule Systems.Advert.PromotionLandingPageBuilder do ) do assignment |> Assignment.Model.language() - |> CoreWeb.LiveLocale.put_locale() + |> CoreWeb.Live.Hook.Locale.put_locale() extra = Map.take(promotion, [:image_id | Promotion.Model.plain_fields()]) icon_url = "/images/#{pool_name |> String.downcase()}-wide-dark.svg" diff --git a/core/systems/alliance/callback_page.ex b/core/systems/alliance/callback_page.ex index 5ee2ce3b3..1338fab11 100644 --- a/core/systems/alliance/callback_page.ex +++ b/core/systems/alliance/callback_page.ex @@ -34,12 +34,6 @@ defmodule Systems.Alliance.CallbackPage do } end - @impl true - def handle_view_model_updated(socket), do: socket - - @impl true - def handle_uri(socket), do: socket - defp activate_participant_task( %{assigns: %{vm: %{state: :participant}, model: model, current_user: user}} = socket ) do diff --git a/core/systems/alliance/content_page.ex b/core/systems/alliance/content_page.ex index 422adfec2..2d4f17a60 100644 --- a/core/systems/alliance/content_page.ex +++ b/core/systems/alliance/content_page.ex @@ -25,15 +25,6 @@ defmodule Systems.Alliance.ContentPage do } end - @impl true - def handle_view_model_updated(socket), do: socket - - @impl true - def handle_resize(socket), do: socket - - @impl true - def handle_uri(socket), do: socket - @impl true def render(assigns) do ~H""" diff --git a/core/systems/assignment/content_page.ex b/core/systems/assignment/content_page.ex index a63c4ef93..ed94d9798 100644 --- a/core/systems/assignment/content_page.ex +++ b/core/systems/assignment/content_page.ex @@ -36,13 +36,9 @@ defmodule Systems.Assignment.ContentPage do end @impl true - def handle_view_model_updated(socket), do: socket - - @impl true - def handle_resize(socket), do: socket - - @impl true - def handle_uri(socket), do: update_view_model(socket) + def handle_uri(socket) do + update_view_model(socket) + end @impl true def render(assigns) do diff --git a/core/systems/assignment/content_page_builder.ex b/core/systems/assignment/content_page_builder.ex index eb3d8aa68..4e2c7c698 100644 --- a/core/systems/assignment/content_page_builder.ex +++ b/core/systems/assignment/content_page_builder.ex @@ -26,12 +26,12 @@ defmodule Systems.Assignment.ContentPageBuilder do """ def view_model( %{id: id} = assignment, - assigns + %{branch: branch} = assigns ) do show_errors = false template = Assignment.Private.get_template(assignment) - breadcrumbs = create_breadcrumbs(assignment) + breadcrumbs = Concept.Branch.hierarchy(branch) tabs = create_tabs(assignment, template, show_errors, assigns) action_map = action_map(assignment) actions = actions(assignment, action_map) @@ -47,13 +47,6 @@ defmodule Systems.Assignment.ContentPageBuilder do } end - defp create_breadcrumbs(assignment) do - case Concept.Branch.hierarchy(assignment) do - {:ok, hierarchy} -> hierarchy - {:error, _} -> nil - end - end - defp action_map(assignment) do preview_url = Assignment.Private.get_preview_url(assignment) diff --git a/core/systems/assignment/crew_page.ex b/core/systems/assignment/crew_page.ex index 00d3e4b3b..ffc441e8b 100644 --- a/core/systems/assignment/crew_page.ex +++ b/core/systems/assignment/crew_page.ex @@ -1,7 +1,9 @@ defmodule Systems.Assignment.CrewPage do - use CoreWeb, {:live_view, :extended} + use CoreWeb, :live_view use CoreWeb.Layouts.Stripped.Composer - use CoreWeb.UI.Responsive.Viewport + + on_mount({CoreWeb.Live.Hook.Base, __MODULE__}) + on_mount({CoreWeb.Live.Hook.Viewport, __MODULE__}) require Logger @@ -30,7 +32,7 @@ defmodule Systems.Assignment.CrewPage do String.to_integer(id) |> Assignment.Public.get!([:info]) |> Assignment.Model.language() - |> CoreWeb.LiveLocale.put_locale() + |> CoreWeb.Live.Hook.Locale.put_locale() { :ok, @@ -41,7 +43,6 @@ defmodule Systems.Assignment.CrewPage do modal: nil, panel_form: nil ) - |> update_timezone(session) |> update_panel_info(session) |> update_image_info() |> signal_started() @@ -49,22 +50,17 @@ defmodule Systems.Assignment.CrewPage do } end - @impl true - def handle_uri(socket), do: socket - @impl true def handle_view_model_updated(socket) do socket |> update_flow() |> update_image_info() - |> update_menus() end @impl true def handle_resize(socket) do socket |> update_image_info() - |> update_menus() end def signal_started(%{assigns: %{vm: %{crew_member: crew_member}}} = socket) do @@ -235,7 +231,7 @@ defmodule Systems.Assignment.CrewPage do <% end %> -
+
<.flow fabric={@fabric} />
diff --git a/core/systems/budget/funding_page.ex b/core/systems/budget/funding_page.ex index 0544da5a1..a2a287c59 100644 --- a/core/systems/budget/funding_page.ex +++ b/core/systems/budget/funding_page.ex @@ -32,12 +32,6 @@ defmodule Systems.Budget.FundingPage do } end - @impl true - def handle_view_model_updated(socket), do: socket - - @impl true - def handle_uri(socket), do: socket - @impl true def compose(:create_budget_form, %{user: user, locale: locale}) do %{ diff --git a/core/systems/content/plug.ex b/core/systems/content/_plug.ex similarity index 100% rename from core/systems/content/plug.ex rename to core/systems/content/_plug.ex diff --git a/core/systems/content/composer.ex b/core/systems/content/composer.ex index e13122c28..13ec9a802 100644 --- a/core/systems/content/composer.ex +++ b/core/systems/content/composer.ex @@ -5,8 +5,7 @@ defmodule Systems.Content.Composer do def __using_live_website__ do quote do - @before_compile {Systems.Content.Composer, :__before_compile_live_website__} - use CoreWeb, {:live_view, :extended} + use CoreWeb, :live_view use CoreWeb.Layouts.Website.Composer use CoreWeb.LiveDefaults @@ -19,22 +18,9 @@ defmodule Systems.Content.Composer do end end - defmacro __before_compile_live_website__(_) do - quote do - defoverridable mount: 3 - - @impl true - def mount(params, session, socket) do - {:ok, socket} = super(params, session, socket) - {:ok, socket |> assign(popup: nil, dialog: nil, modal: nil)} - end - end - end - def __using_live_workspace__ do quote do - @before_compile {Systems.Content.Composer, :__before_compile_live_workspace__} - use CoreWeb, {:live_view, :extended} + use CoreWeb, :live_view use CoreWeb.Layouts.Workspace.Composer use CoreWeb.LiveDefaults @@ -57,18 +43,6 @@ defmodule Systems.Content.Composer do end end - defmacro __before_compile_live_workspace__(_) do - quote do - defoverridable mount: 3 - - @impl true - def mount(params, session, socket) do - {:ok, socket} = super(params, session, socket) - {:ok, socket |> assign(popup: nil, dialog: nil, modal: nil)} - end - end - end - def __using_tabbar_page__ do quote do use Systems.Content.Composer, :live_workspace @@ -77,104 +51,7 @@ defmodule Systems.Content.Composer do def __using_management_page__ do quote do - @before_compile {Systems.Content.Composer, :__before_compile_management_page__} - use Systems.Content.Composer, :live_workspace - use CoreWeb.UI.Responsive.Viewport - alias CoreWeb.UI.Responsive.Breakpoint - - def tabbar_size({:unknown, _}), do: :unknown - def tabbar_size(bp), do: Breakpoint.value(bp, :narrow, sm: %{30 => :wide}) - - def create_actions(%{assigns: %{breakpoint: {:unknown, _}}} = _socket), do: [] - - def create_actions(%{assigns: %{vm: %{actions: actions}}} = socket) do - actions - |> Keyword.keys() - |> Enum.map(&create_action(Keyword.get(actions, &1), socket)) - |> Enum.filter(&(not is_nil(&1))) - end - - def create_action(action, %{assigns: %{breakpoint: breakpoint}}) do - Breakpoint.value(breakpoint, nil, - xs: %{0 => action.icon}, - md: %{40 => action.label, 100 => action.icon}, - lg: %{50 => action.label} - ) - end - - def update_tabbar_size(%{assigns: %{breakpoint: breakpoint}} = socket) do - tabbar_size = tabbar_size(breakpoint) - socket |> assign(tabbar_size: tabbar_size) - end - - def update_actions(socket) do - assign(socket, actions: create_actions(socket)) - end - - @impl true - def handle_event( - "action_click", - %{"item" => action_id}, - %{assigns: %{vm: %{actions: actions}}} = socket - ) do - action_id = String.to_existing_atom(action_id) - action = Keyword.get(actions, action_id) - - { - :noreply, - socket - |> action.handle_click.() - |> update_view_model() - |> update_actions() - } - end - end - end - - defmacro __before_compile_management_page__(_) do - quote do - defoverridable mount: 3 - - @imple true - def mount(params, session, socket) do - {:ok, socket} = super(params, session, socket) - - { - :ok, - socket - |> assign_viewport() - |> assign_breakpoint() - |> update_actions() - |> update_tabbar_size() - } - end - - defoverridable handle_uri: 1 - - @impl true - def handle_uri(socket) do - super(socket) - |> update_actions() - end - - defoverridable handle_view_model_updated: 1 - - @impl true - def handle_view_model_updated(socket) do - super(socket) - |> update_actions() - end - - defoverridable handle_resize: 1 - - @impl true - def handle_resize(socket) do - super(socket) - |> update_tabbar_size() - |> update_actions() - |> update_menus() - end end end end diff --git a/core/systems/content/html.ex b/core/systems/content/html.ex index 542ba75e1..d3fb75559 100644 --- a/core/systems/content/html.ex +++ b/core/systems/content/html.ex @@ -130,7 +130,7 @@ defmodule Systems.Content.Html do def management_page(assigns) do ~H""" -
+
<.live_workspace title={@title} menus={@menus} modal={@modal} popup={@popup} dialog={@dialog}> <:top_bar> diff --git a/core/systems/desktop/page.ex b/core/systems/desktop/page.ex index 916747fe2..775c2018b 100644 --- a/core/systems/desktop/page.ex +++ b/core/systems/desktop/page.ex @@ -8,6 +8,7 @@ defmodule Systems.Desktop.Page do user end + @impl true def mount(_params, _session, socket) do { :ok, @@ -24,12 +25,6 @@ defmodule Systems.Desktop.Page do } end - @impl true - def handle_view_model_updated(socket), do: socket - - @impl true - def handle_uri(socket), do: socket - @impl true def render(assigns) do ~H""" diff --git a/core/systems/document/content_page.ex b/core/systems/document/content_page.ex index 4b04b52f8..70344b71e 100644 --- a/core/systems/document/content_page.ex +++ b/core/systems/document/content_page.ex @@ -25,15 +25,6 @@ defmodule Systems.Document.ContentPage do } end - @impl true - def handle_view_model_updated(socket), do: socket - - @impl true - def handle_resize(socket), do: socket - - @impl true - def handle_uri(socket), do: socket - @impl true def render(assigns) do ~H""" diff --git a/core/systems/feldspar/plug.ex b/core/systems/feldspar/_plug.ex similarity index 100% rename from core/systems/feldspar/plug.ex rename to core/systems/feldspar/_plug.ex diff --git a/core/systems/feldspar/app_page.ex b/core/systems/feldspar/app_page.ex index 3dabfcc74..88c99de60 100644 --- a/core/systems/feldspar/app_page.ex +++ b/core/systems/feldspar/app_page.ex @@ -1,7 +1,15 @@ defmodule Systems.Feldspar.AppPage do use CoreWeb, :live_view + + on_mount({CoreWeb.Live.Hook.Base, __MODULE__}) + on_mount({CoreWeb.Live.Hook.User, __MODULE__}) + on_mount({CoreWeb.Live.Hook.Uri, __MODULE__}) + on_mount({Frameworks.GreenLight.LiveHook, __MODULE__}) + on_mount({Frameworks.Fabric.LiveHook, __MODULE__}) + import CoreWeb.Layouts.Stripped.Html import CoreWeb.Layouts.Stripped.Composer + import CoreWeb.Menus require Logger @@ -26,6 +34,11 @@ defmodule Systems.Feldspar.AppPage do } end + def update_menus(%{assigns: %{current_user: user, uri: uri}} = socket) do + menus = build_menus(stripped_menus_config(), user, uri) + assign(socket, menus: menus) + end + @impl true def compose(:app_view, %{app_id: app_id, app_url: app_url}) do %{ @@ -33,7 +46,7 @@ defmodule Systems.Feldspar.AppPage do params: %{ key: "app_#{app_id}", url: app_url, - locale: CoreWeb.LiveLocale.get_locale() + locale: CoreWeb.Live.Hook.Locale.get_locale() } } end diff --git a/core/systems/feldspar/content_page.ex b/core/systems/feldspar/content_page.ex index a9fcf037a..22ffb2215 100644 --- a/core/systems/feldspar/content_page.ex +++ b/core/systems/feldspar/content_page.ex @@ -25,15 +25,6 @@ defmodule Systems.Feldspar.ContentPage do } end - @impl true - def handle_view_model_updated(socket), do: socket - - @impl true - def handle_resize(socket), do: socket - - @impl true - def handle_uri(socket), do: socket - @impl true def render(assigns) do ~H""" diff --git a/core/systems/graphite/leaderboard_content_page.ex b/core/systems/graphite/leaderboard_content_page.ex index 66857b089..0a7903d76 100644 --- a/core/systems/graphite/leaderboard_content_page.ex +++ b/core/systems/graphite/leaderboard_content_page.ex @@ -28,15 +28,6 @@ defmodule Systems.Graphite.LeaderboardContentPage do } end - @impl true - def handle_view_model_updated(socket), do: socket - - @impl true - def handle_resize(socket), do: socket - - @impl true - def handle_uri(socket), do: socket - @impl true def render(assigns) do ~H""" diff --git a/core/systems/graphite/leaderboard_content_page_builder.ex b/core/systems/graphite/leaderboard_content_page_builder.ex index b5bc01c07..0f3d948c3 100644 --- a/core/systems/graphite/leaderboard_content_page_builder.ex +++ b/core/systems/graphite/leaderboard_content_page_builder.ex @@ -7,10 +7,10 @@ defmodule Systems.Graphite.LeaderboardContentPageBuilder do alias Systems.Content alias Systems.Graphite - def view_model(%Graphite.LeaderboardModel{id: id} = leaderboard, assigns) do + def view_model(%Graphite.LeaderboardModel{id: id} = leaderboard, %{branch: branch} = assigns) do action_map = action_map(leaderboard, assigns) - breadcrumbs = create_breadcrumbs(leaderboard) + breadcrumbs = Concept.Branch.hierarchy(branch) tabs = create_tabs(leaderboard, false, assigns) %{ @@ -26,13 +26,6 @@ defmodule Systems.Graphite.LeaderboardContentPageBuilder do } end - defp create_breadcrumbs(leaderboard) do - case Concept.Branch.hierarchy(leaderboard) do - {:ok, hierarchy} -> hierarchy - {:error, _} -> nil - end - end - defp actions(%{status: :concept}, %{preview: preview, publish: publish}) do [preview: preview, publish: publish] end diff --git a/core/systems/graphite/leaderboard_page.ex b/core/systems/graphite/leaderboard_page.ex index edc3fd08d..e1a59d822 100644 --- a/core/systems/graphite/leaderboard_page.ex +++ b/core/systems/graphite/leaderboard_page.ex @@ -1,7 +1,9 @@ defmodule Systems.Graphite.LeaderboardPage do - use CoreWeb, {:live_view, :extended} + use CoreWeb, :live_view use CoreWeb.Layouts.Stripped.Composer - use CoreWeb.UI.Responsive.Viewport + + on_mount({CoreWeb.Live.Hook.Base, __MODULE__}) + on_mount({CoreWeb.Live.Hook.Viewport, __MODULE__}) alias Core.ImageHelpers alias Frameworks.Pixel.Card @@ -37,14 +39,10 @@ defmodule Systems.Graphite.LeaderboardPage do } end - @impl true - def handle_uri(socket), do: socket - @impl true def handle_resize(socket) do socket |> update_image_info() - |> update_menus() end @impl true @@ -87,7 +85,7 @@ defmodule Systems.Graphite.LeaderboardPage do @impl true def render(assigns) do ~H""" -
+
<.stripped menus={@menus}> <:header>
diff --git a/core/systems/graphite/tool_controller.ex b/core/systems/graphite/tool_controller.ex index 65662bf0e..e2416b743 100644 --- a/core/systems/graphite/tool_controller.ex +++ b/core/systems/graphite/tool_controller.ex @@ -2,7 +2,7 @@ defmodule Systems.Graphite.ToolController do use CoreWeb, :controller def ensure_spot(%{assigns: %{current_user: _user}} = conn, %{"id" => _id}) do - # TODO: PreRef + # FIXME: PreRef # id = String.to_integer(id) diff --git a/core/systems/home/_presenter.ex b/core/systems/home/_presenter.ex index e98d64e0c..118171308 100644 --- a/core/systems/home/_presenter.ex +++ b/core/systems/home/_presenter.ex @@ -2,7 +2,7 @@ defmodule Systems.Home.Presenter do @behaviour Frameworks.Concept.Presenter @impl true - def view_model(Systems.Home.Page, user, assigns) do - Systems.Home.PageBuilder.view_model(user, assigns) + def view_model(Systems.Home.Page, _, assigns) do + Systems.Home.PageBuilder.view_model(nil, assigns) end end diff --git a/core/systems/home/page.ex b/core/systems/home/page.ex index 408780c6a..d07226df6 100644 --- a/core/systems/home/page.ex +++ b/core/systems/home/page.ex @@ -4,17 +4,12 @@ defmodule Systems.Home.Page do alias Systems.Home alias Frameworks.Pixel.Hero - @impl true - def get_model(_params, _session, %{assigns: %{current_user: user}} = _socket) - when not is_nil(user) do - user - end - @impl true def get_model(_params, _session, _socket) do Systems.Observatory.SingletonModel.instance() end + @impl true def mount(_params, _session, socket) do { :ok, @@ -32,10 +27,10 @@ defmodule Systems.Home.Page do end @impl true - def handle_view_model_updated(socket), do: socket - - @impl true - def handle_uri(socket), do: socket + def handle_view_model_updated(socket) do + # FIXME: consider to move updates of childs to Fabric.LiveHook + socket |> update_child(:home_view) + end @impl true def render(assigns) do diff --git a/core/systems/home/page_builder.ex b/core/systems/home/page_builder.ex index 1dfd5c834..b396e2d0c 100644 --- a/core/systems/home/page_builder.ex +++ b/core/systems/home/page_builder.ex @@ -16,7 +16,7 @@ defmodule Systems.Home.PageBuilder do alias Systems.Pool alias Systems.Crew - def view_model(%Account.User{} = user, assigns) do + def view_model(_, %{current_user: user} = assigns) do panl? = panl_participant?(user) put_locale(user, panl?) @@ -58,11 +58,11 @@ defmodule Systems.Home.PageBuilder do end defp put_locale(%Systems.Account.User{creator: false}, true) do - CoreWeb.LiveLocale.put_locale("nl") + CoreWeb.Live.Hook.Locale.put_locale("nl") end defp put_locale(_, _) do - CoreWeb.LiveLocale.put_locale("en") + CoreWeb.Live.Hook.Locale.put_locale("en") end defp block_keys(%Account.User{}, opts) do diff --git a/core/systems/lab/content_page.ex b/core/systems/lab/content_page.ex index 5f96479cf..e55c7aea1 100644 --- a/core/systems/lab/content_page.ex +++ b/core/systems/lab/content_page.ex @@ -25,15 +25,6 @@ defmodule Systems.Lab.ContentPage do } end - @impl true - def handle_view_model_updated(socket), do: socket - - @impl true - def handle_resize(socket), do: socket - - @impl true - def handle_uri(socket), do: socket - @impl true def render(assigns) do ~H""" diff --git a/core/systems/lab/public_page.ex b/core/systems/lab/public_page.ex index 06607646a..4da9f9a2a 100644 --- a/core/systems/lab/public_page.ex +++ b/core/systems/lab/public_page.ex @@ -1,13 +1,15 @@ defmodule Systems.Lab.PublicPage do @moduledoc """ - The public promotion screen. + The public lab screen. """ use CoreWeb, :live_view - alias Systems.{ - Lab - } + alias Systems.Lab + on_mount({CoreWeb.Live.Hook.Base, __MODULE__}) + on_mount({CoreWeb.Live.Hook.User, __MODULE__}) + + @impl true def mount(%{"id" => id}, _session, %{assigns: %{current_user: user}} = socket) do tool = Lab.Public.get_tool!(id, [:time_slots]) diff --git a/core/systems/next_action/overview_page.ex b/core/systems/next_action/overview_page.ex index 71e0a14e6..80552dccd 100644 --- a/core/systems/next_action/overview_page.ex +++ b/core/systems/next_action/overview_page.ex @@ -20,14 +20,10 @@ defmodule Systems.NextAction.OverviewPage do } end - @impl true def handle_view_model_updated(socket) do refresh_next_actions(socket) end - @impl true - def handle_uri(socket), do: socket - def refresh_next_actions(%{assigns: %{current_user: user}} = socket) do assign( socket, diff --git a/core/systems/notification/overview_page.ex b/core/systems/notification/overview_page.ex index d9b3a24e0..4a60a82f6 100644 --- a/core/systems/notification/overview_page.ex +++ b/core/systems/notification/overview_page.ex @@ -2,6 +2,10 @@ defmodule Systems.Notification.OverviewPage do use CoreWeb, :live_view alias Systems.Notification + on_mount({CoreWeb.Live.Hook.Base, __MODULE__}) + on_mount({CoreWeb.Live.Hook.User, __MODULE__}) + + @impl true def mount(_params, _session, %{assigns: %{current_user: user}} = socket) do {:ok, socket |> assign(:notifications, Notification.Public.list(user))} end diff --git a/core/systems/observatory/_live_feature.ex b/core/systems/observatory/_live_feature.ex new file mode 100644 index 000000000..902aeec63 --- /dev/null +++ b/core/systems/observatory/_live_feature.ex @@ -0,0 +1,54 @@ +defmodule Systems.Observatory.LiveFeature do + @callback handle_view_model_updated(Socket.t()) :: Socket.t() + + defmacro __using__(_opts \\ []) do + quote do + @behaviour Systems.Observatory.LiveFeature + + @impl true + def handle_view_model_updated(socket), do: socket + + defoverridable handle_view_model_updated: 1 + + @presenter Frameworks.Concept.System.presenter(__MODULE__) + + import CoreWeb.Gettext + alias Systems.Observatory + + # Stubs for messages that are handled in Live Hooks + def handle_info(%Phoenix.Socket.Broadcast{}, socket), do: {:noreply, socket} + def handle_info(:view_model_updated, socket), do: {:noreply, socket} + + def observe_view_model(%{assigns: %{authorization_failed: true}} = socket) do + socket + end + + def observe_view_model(%{assigns: %{model: %{id: id} = model}} = socket) do + socket + |> Observatory.Public.observe([{__MODULE__, [id]}]) + |> Observatory.Public.update_view_model(__MODULE__, model, @presenter) + end + + def update_view_model(%{assigns: %{model: model}} = socket) do + socket + |> Observatory.Public.update_view_model(__MODULE__, model, @presenter) + end + + def put_info_flash(socket, from_pid) do + if from_pid == self() do + socket |> put_saved_info_flash() + else + socket |> put_updated_info_flash() + end + end + + def put_updated_info_flash(socket) do + socket |> Frameworks.Pixel.Flash.put_info("Updated") + end + + def put_saved_info_flash(socket) do + socket |> Frameworks.Pixel.Flash.put_info("Saved") + end + end + end +end diff --git a/core/systems/observatory/_live_hook.ex b/core/systems/observatory/_live_hook.ex new file mode 100644 index 000000000..8c48c578f --- /dev/null +++ b/core/systems/observatory/_live_hook.ex @@ -0,0 +1,56 @@ +defmodule Systems.Observatory.LiveHook do + use Frameworks.Concept.LiveHook + + require Logger + + @impl true + def on_mount(live_view_module, _params, session, socket) do + { + :cont, + socket + |> assign(session: session) + |> observe_view_model(live_view_module) + |> handle_auto_save_status(live_view_module) + |> handle_model_update(live_view_module) + } + end + + defp observe_view_model(socket, live_view_module) do + live_view_module.observe_view_model(socket) + end + + defp handle_auto_save_status(socket, _live_view_module) do + attach_hook(socket, :handle_auto_save_status, :handle_info, fn + %{auto_save: status}, socket -> + {:cont, socket |> assign(auto_save_status: status)} + + _, socket -> + {:cont, socket} + end) + end + + defp handle_model_update(socket, live_view_module) do + attach_hook(socket, :handle_model_update, :handle_info, fn + %{topic: _topic, payload: {_signal, %{model: model, from_pid: from_pid}}}, socket -> + {:cont, socket |> handle_model_update(live_view_module, model, from_pid)} + + %{topic: _topic, payload: {_signal, %{model: model}}}, socket -> + Logger.warn("Unknown sender, no from_pid provided") + {:cont, socket |> handle_model_update(live_view_module, model, nil)} + + _, socket -> + {:cont, socket} + end) + end + + defp handle_model_update(socket, live_view_module, model, from_pid) do + # Send message to other Live Hooks + send(self(), :view_model_updated) + + socket + |> assign(model: model) + |> optional_apply(live_view_module, :update_view_model) + |> optional_apply(live_view_module, :handle_view_model_updated) + |> optional_apply(live_view_module, :put_info_flash, [from_pid]) + end +end diff --git a/core/systems/observatory/_public.ex b/core/systems/observatory/_public.ex index 15c9283ab..a8e2635e3 100644 --- a/core/systems/observatory/_public.ex +++ b/core/systems/observatory/_public.ex @@ -1,9 +1,6 @@ defmodule Systems.Observatory.Public do alias CoreWeb.Endpoint - @callback get_model(map(), map(), Socket.t()) :: Socket.t() - @callback handle_view_model_updated(Socket.t()) :: Socket.t() - def subscribe(signal, key \\ []) do Endpoint.subscribe(topic_key(signal, key)) end @@ -43,121 +40,19 @@ defmodule Systems.Observatory.Public do def update_view_model(socket, page, model, presenter) do vm = get_view_model(socket, page, model, presenter) - socket - |> Phoenix.Component.assign(vm: vm) + Phoenix.Component.assign(socket, vm: vm) end - defp get_view_model(_socket, page, _model, nil) do + def get_view_model(_socket, page, _model, nil) do raise "No presenter available for #{page}" end - defp get_view_model( - %{assigns: assigns} = _socket, - page, - model, - presenter - ) do + def get_view_model( + %{assigns: assigns} = _socket, + page, + model, + presenter + ) do apply(presenter, :view_model, [page, model, assigns]) end - - defmacro __using__(_opts \\ []) do - quote do - @behaviour Systems.Observatory.Public - @before_compile Systems.Observatory.Public - @presenter Frameworks.Concept.System.presenter(__MODULE__) - - import CoreWeb.Gettext - alias Systems.Observatory.Public - - require Logger - - def handle_info(%{auto_save: status}, socket) do - { - :noreply, - socket |> assign(auto_save_status: status) - } - end - - def handle_info( - %{topic: _topic, payload: {signal, %{model: model, from_pid: from_pid}}} = payload, - socket - ) do - { - :noreply, - socket - |> assign(model: model) - |> update_view_model() - |> handle_view_model_updated() - |> put_info_flash(from_pid) - } - end - - def handle_info(%{topic: topic, payload: {signal, %{model: model}}} = payload, socket) do - Logger.warn("Unknown sender, no from_pid provided") - handle_info(%{topic: topic, payload: {signal, %{model: model, from_pid: nil}}}, socket) - end - - def observe_view_model(%{assigns: %{authorization_failed: true}} = socket) do - socket - end - - def observe_view_model(%{assigns: %{model: %{id: id} = model}} = socket) do - socket - |> Public.observe([{__MODULE__, [id]}]) - |> Public.update_view_model(__MODULE__, model, @presenter) - end - - def observe_event(%{assigns: %{model: %{id: id} = model}} = socket) do - socket - |> Public.observe([{__MODULE__, [id]}]) - |> Public.update_view_model(__MODULE__, model, @presenter) - end - - def update_view_model(%{assigns: %{model: model}} = socket) do - socket - |> Public.update_view_model(__MODULE__, model, @presenter) - end - - def put_info_flash(socket, from_pid) do - if from_pid == self() do - socket |> put_saved_info_flash() - else - socket |> put_updated_info_flash() - end - end - - def put_updated_info_flash(socket) do - socket |> Frameworks.Pixel.Flash.put_info("Updated") - end - - def put_saved_info_flash(socket) do - socket |> Frameworks.Pixel.Flash.put_info("Saved") - end - end - end - - defmacro __before_compile__(_env) do - quote do - defoverridable mount: 3 - - @impl true - def mount(params, session, socket) do - model = get_model(params, session, socket) - - super( - params, - session, - socket - |> assign(model: model, session: session) - |> observe_view_model() - ) - end - - defoverridable handle_view_model_updated: 1 - - def handle_view_model_updated(socket) do - super(socket) - end - end - end end diff --git a/core/systems/org/content_page.ex b/core/systems/org/content_page.ex index 338cbd722..99a4f13f1 100644 --- a/core/systems/org/content_page.ex +++ b/core/systems/org/content_page.ex @@ -23,15 +23,6 @@ defmodule Systems.Org.ContentPage do } end - @impl true - def handle_view_model_updated(socket), do: socket - - @impl true - def handle_resize(socket), do: socket - - @impl true - def handle_uri(socket), do: socket - @impl true def render(assigns) do ~H""" diff --git a/core/systems/pool/detail_page.ex b/core/systems/pool/detail_page.ex index a5cd7d58e..ebc95067a 100644 --- a/core/systems/pool/detail_page.ex +++ b/core/systems/pool/detail_page.ex @@ -3,7 +3,6 @@ defmodule Systems.Pool.DetailPage do The pool details screen. """ use Systems.Content.Composer, :live_workspace - use CoreWeb.UI.Responsive.Viewport alias Frameworks.Pixel.Tabbar alias Frameworks.Pixel.Navigation @@ -39,12 +38,6 @@ defmodule Systems.Pool.DetailPage do } end - @impl true - def handle_view_model_updated(socket), do: socket - - @impl true - def handle_uri(socket), do: socket - @impl true def handle_event("close_email_dialog", _, socket) do { @@ -94,12 +87,6 @@ defmodule Systems.Pool.DetailPage do {:noreply, socket} end - @impl true - def handle_resize(socket) do - socket - |> update_menus() - end - defp close_email_dialog(socket) do socket |> assign(email_dialog: nil) @@ -120,7 +107,7 @@ defmodule Systems.Pool.DetailPage do
<% end %> -
+
diff --git a/core/systems/pool/landing_page.ex b/core/systems/pool/landing_page.ex index bfa30e309..cb18b87db 100644 --- a/core/systems/pool/landing_page.ex +++ b/core/systems/pool/landing_page.ex @@ -17,12 +17,6 @@ defmodule Systems.Pool.LandingPage do {:ok, socket} end - @impl true - def handle_view_model_updated(socket), do: socket - - @impl true - def handle_uri(socket), do: socket - @impl true def handle_event("register", _, socket) do { diff --git a/core/systems/pool/participant_page.ex b/core/systems/pool/participant_page.ex index c40001468..aa61bff5b 100644 --- a/core/systems/pool/participant_page.ex +++ b/core/systems/pool/participant_page.ex @@ -18,12 +18,6 @@ defmodule Systems.Pool.ParticipantPage do {:ok, socket} end - @impl true - def handle_view_model_updated(socket), do: socket - - @impl true - def handle_uri(socket), do: socket - @impl true def render(assigns) do ~H""" diff --git a/core/systems/pool/submission_page.ex b/core/systems/pool/submission_page.ex index 537edc24b..e22436d60 100644 --- a/core/systems/pool/submission_page.ex +++ b/core/systems/pool/submission_page.ex @@ -25,12 +25,6 @@ defmodule Systems.Pool.SubmissionPage do {:ok, socket} end - @impl true - def handle_view_model_updated(socket), do: socket - - @impl true - def handle_uri(socket), do: socket - @impl true def handle_event("publish", _params, %{assigns: %{vm: %{submission: submission}}} = socket) do socket = diff --git a/core/systems/project/_live_hook.ex b/core/systems/project/_live_hook.ex new file mode 100644 index 000000000..384cab62a --- /dev/null +++ b/core/systems/project/_live_hook.ex @@ -0,0 +1,24 @@ +defmodule Systems.Project.LiveHook do + @moduledoc "A Live Hook that injects the correct Branch (Project.NodeModel) for each Leaf" + use Frameworks.Concept.LiveHook + + alias Frameworks.Concept + alias Systems.Project + + @impl true + def on_mount(_live_view_module, _params, _session, socket) do + branch = + with model <- Map.get(socket.assigns, :model, nil), + false <- model == nil, + false <- Concept.Leaf.impl_for(model) == nil, + item <- Project.Public.get_item_by(model), + false <- item == nil, + node <- Project.Public.get_node_by_item!(item) do + %Project.Branch{node_id: node.id, item_id: item.id} + else + _ -> nil + end + + {:cont, socket |> assign(branch: branch)} + end +end diff --git a/core/systems/project/_public.ex b/core/systems/project/_public.ex index 27cd64665..1e1314aac 100644 --- a/core/systems/project/_public.ex +++ b/core/systems/project/_public.ex @@ -1,6 +1,5 @@ defmodule Systems.Project.Public do use CoreWeb, :verified_routes - @behaviour Frameworks.Concept.Branch.Factory import CoreWeb.Gettext import Ecto.Query, warn: false @@ -11,7 +10,6 @@ defmodule Systems.Project.Public do alias Ecto.Multi alias Frameworks.Signal - alias Frameworks.Concept alias Systems.Account.User alias Systems.Advert @@ -21,57 +19,6 @@ defmodule Systems.Project.Public do alias Systems.Storage alias Systems.Workflow - @impl true - def name(:parent, %Systems.Storage.EndpointModel{} = model) do - case get_by_item_special(model) do - %{name: name} -> {:ok, name} - _ -> {:error, :not_found} - end - end - - @impl true - def name(:self, %Systems.Storage.EndpointModel{} = model) do - case get_item_by(model) do - %{name: name} -> {:ok, name} - _ -> {:error, :not_found} - end - end - - @impl true - def name(_, _), do: {:error, :not_supported} - - @impl true - def hierarchy(atom) do - if item = get_item_by(atom) do - breadcrumbs(item) - else - {:error, :unknown} - end - end - - def breadcrumbs(%Project.ItemModel{name: name} = item) do - special_path = "/#{Concept.Leaf.resource_id(item)}/content" - special_breadcrumb = %{label: name, path: special_path} - - {:ok, node_breadcrumbs} = - item - |> get_node_by_item!() - |> breadcrumbs() - - {:ok, node_breadcrumbs ++ [special_breadcrumb]} - end - - def breadcrumbs(%Project.NodeModel{} = node) do - project = get_by_root(node) - project_breadcrumb = %{label: project.name, path: "/project/node/#{node.id}"} - - {:ok, [projects_breadcrumb(), project_breadcrumb]} - end - - defp projects_breadcrumb() do - %{label: dgettext("eyra-project", "first.breadcrumb.label"), path: ~p"/project"} - end - def get!(id, preload \\ []) do from(project in Project.Model, where: project.id == ^id, diff --git a/core/systems/project/branch.ex b/core/systems/project/branch.ex new file mode 100644 index 000000000..c8e2fcef8 --- /dev/null +++ b/core/systems/project/branch.ex @@ -0,0 +1,58 @@ +defmodule Systems.Project.Branch do + use Ecto.Schema + @primary_key false + + embedded_schema do + field(:node_id, :integer) + field(:item_id, :integer) + end +end + +defimpl Frameworks.Concept.Branch, for: Systems.Project.Branch do + use CoreWeb, :verified_routes + import CoreWeb.Gettext + import Frameworks.Utility.List + alias Frameworks.Concept + alias Systems.Project + + def name(%Project.Branch{node_id: node_id}, :parent) do + %Project.Model{name: name} = + node_id + |> Project.Public.get_node!() + |> Project.Public.get_by_root() + + name + end + + def name(%Project.Branch{item_id: item_id}, :self) do + %Project.ItemModel{name: name} = Project.Public.get_item!(item_id) + name + end + + def hierarchy(%Project.Branch{node_id: node_id, item_id: item_id}) do + node_breadcrumb = fn -> + Project.Public.get_node!(node_id) |> breadcrumb() + end + + item_breadcrumb = fn -> + Project.Public.get_item!(item_id, Project.ItemModel.preload_graph(:down)) |> breadcrumb() + end + + [root_breadcrumb()] + |> append_if(node_breadcrumb, not is_nil(node_id)) + |> append_if(item_breadcrumb, not is_nil(item_id)) + end + + defp breadcrumb(%Project.ItemModel{name: name} = item) do + %{label: name, path: "/#{Concept.Leaf.resource_id(item)}/content"} + end + + defp breadcrumb(%Project.NodeModel{} = node) do + %Project.Model{name: name} = Project.Public.get_by_root(node) + %{label: name, path: "/project/node/#{node.id}"} + end + + defp root_breadcrumb() do + %{label: dgettext("eyra-project", "first.breadcrumb.label"), path: ~p"/project"} + end +end diff --git a/core/systems/project/branch_plug.ex b/core/systems/project/branch_plug.ex new file mode 100644 index 000000000..b689589b1 --- /dev/null +++ b/core/systems/project/branch_plug.ex @@ -0,0 +1,30 @@ +defmodule Systems.Project.BranchPlug do + @behaviour Plug + alias Frameworks.Concept + alias Systems.Project + alias Systems.Storage + + @impl true + def init(opts), do: opts + + @impl true + def call(%{request_path: request_path} = conn, _opts) do + branch = branch(Path.split(request_path)) + conn |> Plug.Conn.assign(:branch, branch) + end + + defp branch(["/", "storage", "endpoint", id | _]), do: branch(Storage.Public.get_endpoint!(id)) + + defp branch(%{} = leaf) do + with false <- Concept.Leaf.impl_for(leaf) == nil, + item <- Project.Public.get_item_by(leaf), + false <- item == nil, + node <- Project.Public.get_node_by_item!(item) do + %Project.Branch{node_id: node.id, item_id: item.id} + else + _ -> nil + end + end + + defp branch(_), do: nil +end diff --git a/core/systems/project/node_page.ex b/core/systems/project/node_page.ex index 13579f085..c41e4c469 100644 --- a/core/systems/project/node_page.ex +++ b/core/systems/project/node_page.ex @@ -7,6 +7,11 @@ defmodule Systems.Project.NodePage do alias Frameworks.Pixel.Breadcrumbs alias Systems.Project + @impl true + def get_authorization_context(params, session, socket) do + get_model(params, session, socket) + end + @impl true def get_model(%{"id" => id}, _session, _socket) do Project.Public.get_node!(String.to_integer(id), Project.NodeModel.preload_graph(:down)) @@ -88,12 +93,6 @@ defmodule Systems.Project.NodePage do {:noreply, socket |> hide_modal(modal_view)} end - @impl true - def handle_view_model_updated(socket), do: socket - - @impl true - def handle_uri(socket), do: socket - @impl true def render(assigns) do ~H""" diff --git a/core/systems/project/node_page_builder.ex b/core/systems/project/node_page_builder.ex index a4fa586bb..bf1e17ef4 100644 --- a/core/systems/project/node_page_builder.ex +++ b/core/systems/project/node_page_builder.ex @@ -2,7 +2,7 @@ defmodule Systems.Project.NodePageBuilder do use Core.FeatureFlags alias Frameworks.Utility.ViewModelBuilder - + alias Frameworks.Concept alias Systems.Project def view_model( @@ -11,7 +11,8 @@ defmodule Systems.Project.NodePageBuilder do } = node, assigns ) do - {:ok, breadcrumbs} = Project.Public.breadcrumbs(node) + branch = %Project.Branch{node_id: node.id} + breadcrumbs = Concept.Branch.hierarchy(branch) item_cards = to_item_cards(node, assigns) node_cards = to_node_cards(node, assigns) diff --git a/core/systems/project/overview_page.ex b/core/systems/project/overview_page.ex index 9f91cd134..71cdfb809 100644 --- a/core/systems/project/overview_page.ex +++ b/core/systems/project/overview_page.ex @@ -21,12 +21,8 @@ defmodule Systems.Project.OverviewPage do {:ok, socket} end - @impl true def handle_view_model_updated(socket), do: socket |> update_child(:people_page) - @impl true - def handle_uri(socket), do: socket - @impl true def compose(:project_form, %{active_project: project_id, vm: %{projects: projects}}) do project = Enum.find(projects, &(&1.id == String.to_integer(project_id))) diff --git a/core/systems/promotion/landing_page.ex b/core/systems/promotion/landing_page.ex index 490b0993e..ede471136 100644 --- a/core/systems/promotion/landing_page.ex +++ b/core/systems/promotion/landing_page.ex @@ -3,7 +3,9 @@ defmodule Systems.Promotion.LandingPage do The public promotion screen. """ use Systems.Content.Composer, :live_website - use CoreWeb.UI.Responsive.Viewport + + on_mount({CoreWeb.Live.Hook.Base, __MODULE__}) + on_mount({CoreWeb.Live.Hook.Viewport, __MODULE__}) import Systems.Promotion.BannerView @@ -42,14 +44,10 @@ defmodule Systems.Promotion.LandingPage do } end - @impl true def handle_view_model_updated(socket) do update_image_info(socket) end - @impl true - def handle_uri(socket), do: socket - @impl true def handle_resize(socket) do update_image_info(socket) @@ -117,7 +115,7 @@ defmodule Systems.Promotion.LandingPage do @impl true def render(assigns) do ~H""" -
+
<.live_website user={@current_user} user_agent={Browser.Ua.to_ua(@socket)} menus={@menus} modal={@modal} popup={@popup} dialog={@dialog}> <:hero>
diff --git a/core/systems/storage/controller.ex b/core/systems/storage/controller.ex index 4813fbc40..8166e0b71 100644 --- a/core/systems/storage/controller.ex +++ b/core/systems/storage/controller.ex @@ -2,19 +2,19 @@ defmodule Systems.Storage.Controller do alias CoreWeb.UI.Timestamp use CoreWeb, :controller - alias Frameworks.Concept.Branch + alias Frameworks.Concept alias Systems.Storage alias Systems.Rate - def export(conn, %{"id" => id}) do + def export(%{assigns: %{branch: branch}} = conn, %{"id" => id}) do if endpoint = Storage.Public.get_endpoint!( String.to_integer(id), Storage.EndpointModel.preload_graph(:down) ) do special = Storage.EndpointModel.special(endpoint) - branch_name = Branch.name(endpoint, :parent, "export") + branch_name = Concept.Branch.name(branch, :parent) export(conn, special, branch_name) else diff --git a/core/systems/storage/endpoint_content_page.ex b/core/systems/storage/endpoint_content_page.ex index ffbae673e..d906d1b24 100644 --- a/core/systems/storage/endpoint_content_page.ex +++ b/core/systems/storage/endpoint_content_page.ex @@ -28,7 +28,6 @@ defmodule Systems.Storage.EndpointContentPage do } end - @impl true def handle_view_model_updated(%{assigns: %{vm: vm}} = socket) do if tab = Enum.find(vm.tabs, &(&1.id == :data_view)) do Fabric.send_event(tab.child.ref, %{name: "update_files", payload: %{}}) @@ -37,9 +36,6 @@ defmodule Systems.Storage.EndpointContentPage do socket end - @impl true - def handle_resize(socket), do: socket - @impl true def handle_uri(socket), do: update_view_model(socket) diff --git a/core/systems/storage/endpoint_content_page_builder.ex b/core/systems/storage/endpoint_content_page_builder.ex index 25e507c45..b0c6cc467 100644 --- a/core/systems/storage/endpoint_content_page_builder.ex +++ b/core/systems/storage/endpoint_content_page_builder.ex @@ -8,12 +8,12 @@ defmodule Systems.Storage.EndpointContentPageBuilder do def view_model( %{id: id} = endpoint, - assigns + %{branch: branch} = assigns ) do show_errors = true - breadcrumbs = create_breadcrumbs(endpoint) + breadcrumbs = Concept.Branch.hierarchy(branch) tabs = create_tabs(endpoint, show_errors, assigns) - title = Concept.Branch.name(endpoint, :self, "Data") + title = Concept.Branch.name(branch, :self) %{ id: id, @@ -26,13 +26,6 @@ defmodule Systems.Storage.EndpointContentPageBuilder do } end - defp create_breadcrumbs(endpoint) do - case Concept.Branch.hierarchy(endpoint) do - {:ok, hierarchy} -> hierarchy - {:error, _} -> nil - end - end - defp get_tab_keys(endpoint) do special = Storage.EndpointModel.special_field(endpoint) @@ -79,10 +72,10 @@ defmodule Systems.Storage.EndpointContentPageBuilder do :data, endpoint, show_errors, - %{fabric: fabric, timezone: timezone} = _assigns + %{branch: branch, fabric: fabric, timezone: timezone} = _assigns ) do ready? = true - branch_name = Concept.Branch.name(endpoint, :parent, "Current") + branch_name = Concept.Branch.name(branch, :parent) child = Fabric.prepare_child(fabric, :data_view, Storage.EndpointDataView, %{ diff --git a/core/systems/support/helpdesk_page.ex b/core/systems/support/helpdesk_page.ex index cafa8cea0..b84b74b46 100644 --- a/core/systems/support/helpdesk_page.ex +++ b/core/systems/support/helpdesk_page.ex @@ -6,6 +6,7 @@ defmodule Systems.Support.HelpdeskPage do user end + @impl true def mount(_params, _session, socket) do {:ok, socket |> compose_child(:helpdesk_form)} end @@ -18,12 +19,6 @@ defmodule Systems.Support.HelpdeskPage do } end - @impl true - def handle_view_model_updated(socket), do: socket - - @impl true - def handle_uri(socket), do: socket - @impl true def render(assigns) do ~H""" diff --git a/core/systems/support/overview_page.ex b/core/systems/support/overview_page.ex index 3452594eb..36daaac01 100644 --- a/core/systems/support/overview_page.ex +++ b/core/systems/support/overview_page.ex @@ -17,12 +17,6 @@ defmodule Systems.Support.OverviewPage do } end - @impl true - def handle_view_model_updated(socket), do: socket - - @impl true - def handle_uri(socket), do: socket - @impl true def render(assigns) do ~H""" diff --git a/core/systems/support/ticket_page.ex b/core/systems/support/ticket_page.ex index 738e53d36..1c901416d 100644 --- a/core/systems/support/ticket_page.ex +++ b/core/systems/support/ticket_page.ex @@ -12,6 +12,7 @@ defmodule Systems.Support.TicketPage do Support.Public.get_ticket!(id) end + @impl true def mount(%{"id" => id}, _session, socket) do { :ok, @@ -20,12 +21,6 @@ defmodule Systems.Support.TicketPage do } end - @impl true - def handle_view_model_updated(socket), do: socket - - @impl true - def handle_uri(socket), do: socket - @impl true def handle_event("close_ticket", _params, %{assigns: %{id: id}} = socket) do Support.Public.close_ticket_by_id(id) diff --git a/core/systems/test/page.ex b/core/systems/test/page.ex index 8d9db4fb8..b410dfc59 100644 --- a/core/systems/test/page.ex +++ b/core/systems/test/page.ex @@ -2,7 +2,11 @@ defmodule Systems.Test.Page do @moduledoc """ The page for testing the view model observations """ - use Systems.Content.Composer, :live_workspace + use CoreWeb, :live_view + + on_mount({CoreWeb.Live.Hook.Base, __MODULE__}) + on_mount({CoreWeb.Live.Hook.Model, __MODULE__}) + on_mount({Systems.Observatory.LiveHook, __MODULE__}) alias Systems.Test @@ -13,14 +17,19 @@ defmodule Systems.Test.Page do @impl true def mount(_params, _session, socket) do - {:ok, socket |> assign(active_menu_item: nil)} + { + :ok, + socket + |> assign( + view_model_updated: 0, + active_menu_item: nil + ) + } end - @impl true - def handle_view_model_updated(socket), do: socket - - @impl true - def handle_uri(socket), do: socket + def handle_view_model_updated(%{assigns: %{view_model_updated: view_model_updated}} = socket) do + socket |> assign(view_model_updated: "#{view_model_updated + 1}") + end # data(model, :map) @impl true @@ -28,6 +37,7 @@ defmodule Systems.Test.Page do ~H"""
<%= @vm.title %>
<%= @vm.subtitle %>
+
view_model_updated: <%= @view_model_updated %>
""" end end diff --git a/core/test/systems/advert/content_page_test.exs b/core/test/systems/advert/content_page_test.exs index 001c726f6..c2879e248 100644 --- a/core/test/systems/advert/content_page_test.exs +++ b/core/test/systems/advert/content_page_test.exs @@ -2,8 +2,10 @@ defmodule Systems.Advert.ContentPageTest do use CoreWeb.ConnCase, async: true import Phoenix.ConnTest import Phoenix.LiveViewTest + import Phoenix.Component, only: [assign: 2] import ExUnit.Assertions + import Mock alias Systems.Advert @@ -11,18 +13,27 @@ defmodule Systems.Advert.ContentPageTest do setup [:login_as_creator] test "Default", %{conn: %{assigns: %{current_user: researcher}} = conn} do - %{id: id} = Advert.Factories.create_advert(researcher, :accepted, 1) + branch = + Promox.new() + |> Promox.stub(Frameworks.Concept.Branch, :hierarchy, fn _ -> [] end) - {:ok, _view, html} = live(conn, ~p"/advert/#{id}/content") + with_mock Systems.Project.LiveHook, + on_mount: fn _live_view_module, _params, _session, socket -> + {:cont, socket |> assign(branch: branch)} + end do + %{id: id} = Advert.Factories.create_advert(researcher, :accepted, 1) - assert html =~ "
" - assert html =~ "Settings" + {:ok, _view, html} = live(conn, ~p"/advert/#{id}/content") - assert html =~ "
" - assert html =~ "Criteria" + assert html =~ "
" + assert html =~ "Settings" - assert html =~ "
" - assert html =~ "Monitor" + assert html =~ "
" + assert html =~ "Criteria" + + assert html =~ "
" + assert html =~ "Monitor" + end end end end diff --git a/core/test/systems/observatory/view_model_observe_test.exs b/core/test/systems/observatory/view_model_observe_test.exs index 669191568..4b485b4ab 100644 --- a/core/test/systems/observatory/view_model_observe_test.exs +++ b/core/test/systems/observatory/view_model_observe_test.exs @@ -4,9 +4,7 @@ defmodule Systems.Observatory.ViewModelObserveTest do import Phoenix.LiveViewTest import Core.AuthTestHelpers - alias Systems.{ - Test - } + alias Systems.Test describe "View model roundtrip" do setup [:login_as_member] @@ -16,6 +14,7 @@ defmodule Systems.Observatory.ViewModelObserveTest do assert html =~ "John Doe" assert html =~ "Age: 56 - Works at: The Basement" + assert html =~ "view_model_updated: 0" end test "View model update", %{conn: conn} do @@ -24,7 +23,10 @@ defmodule Systems.Observatory.ViewModelObserveTest do model = Test.Public.get(1) Test.Public.update(model, %{age: 57}) - assert render(view) =~ "Age: 57 - Works at: The Basement" + html = render(view) + + assert html =~ "Age: 57 - Works at: The Basement" + assert html =~ "view_model_updated: 1" end end end diff --git a/core/test/systems/support/overview_page_test.exs b/core/test/systems/support/overview_page_test.exs index 4b394585b..d99cbb195 100644 --- a/core/test/systems/support/overview_page_test.exs +++ b/core/test/systems/support/overview_page_test.exs @@ -7,9 +7,7 @@ defmodule Systems.Support.OverviewPageTest do setup [:login_as_member] test "deny access to non-admin", %{conn: conn} do - assert_error_sent(403, fn -> - live(conn, ~p"/support/ticket") - end) + assert {:error, {:redirect, %{to: "/access_denied"}}} = live(conn, ~p"/support/ticket") end end diff --git a/core/test/systems/support/ticket_page_test.exs b/core/test/systems/support/ticket_page_test.exs index b3d427371..73ce53102 100644 --- a/core/test/systems/support/ticket_page_test.exs +++ b/core/test/systems/support/ticket_page_test.exs @@ -9,9 +9,8 @@ defmodule Systems.Support.TicketPageTest do test "deny access to non-admin", %{conn: conn} do ticket = Factories.insert!(:helpdesk_ticket) - assert_error_sent(403, fn -> - live(conn, ~p"/support/ticket/#{ticket.id}") - end) + assert {:error, {:redirect, %{to: "/access_denied"}}} = + live(conn, ~p"/support/ticket/#{ticket.id}") end end diff --git a/core/test/test_helper.exs b/core/test/test_helper.exs index 43a493d6b..0345f6488 100644 --- a/core/test/test_helper.exs +++ b/core/test/test_helper.exs @@ -1,3 +1,7 @@ +require Promox + +Promox.defmock(for: Frameworks.Concept.Branch) + ExUnit.start() Ecto.Adapters.SQL.Sandbox.mode(Core.Repo, :manual)