From 3adc4e4e68af4e7e8ceef965d5f57c4600adfd22 Mon Sep 17 00:00:00 2001 From: emielvdveen Date: Thu, 30 Nov 2023 15:21:04 +0100 Subject: [PATCH 01/14] Refactored Project Item Form en Create Item Popup using Fabric and better signalling --- .../priv/gettext/en/LC_MESSAGES/eyra-enums.po | 6 +- .../gettext/en/LC_MESSAGES/eyra-project.po | 6 +- core/priv/gettext/en/LC_MESSAGES/eyra-ui.po | 4 - core/priv/gettext/eyra-enums.pot | 6 +- core/priv/gettext/eyra-ui.pot | 4 - .../priv/gettext/nl/LC_MESSAGES/eyra-enums.po | 6 +- .../gettext/nl/LC_MESSAGES/eyra-project.po | 8 +- core/priv/gettext/nl/LC_MESSAGES/eyra-ui.po | 4 - core/systems/project/_assembly.ex | 31 ++++--- core/systems/project/_presenter.ex | 2 +- core/systems/project/_public.ex | 33 ++++++- core/systems/project/_switch.ex | 15 ++- core/systems/project/create_item_popup.ex | 49 +++++----- core/systems/project/create_project_popup.ex | 4 +- core/systems/project/form.ex | 7 +- core/systems/project/item_form.ex | 92 ++++++++++++------- core/systems/project/item_templates.ex | 3 + core/systems/project/node_page.ex | 72 +++++++++++---- core/systems/project/node_page_builder.ex | 6 +- core/systems/project/overview_page.ex | 2 +- core/systems/project/templates.ex | 3 - core/test/systems/project/_assembly_test.exs | 6 +- 22 files changed, 224 insertions(+), 145 deletions(-) create mode 100644 core/systems/project/item_templates.ex delete mode 100644 core/systems/project/templates.ex diff --git a/core/priv/gettext/en/LC_MESSAGES/eyra-enums.po b/core/priv/gettext/en/LC_MESSAGES/eyra-enums.po index 1b2763b94..e122b6f82 100644 --- a/core/priv/gettext/en/LC_MESSAGES/eyra-enums.po +++ b/core/priv/gettext/en/LC_MESSAGES/eyra-enums.po @@ -323,15 +323,15 @@ msgid "organisation_types.team" msgstr "Department" #, elixir-autogen, elixir-format -msgid "project_templates.benchmark" +msgid "project_item_templates.benchmark" msgstr "Benchmark" #, elixir-autogen, elixir-format -msgid "project_templates.data_donation" +msgid "project_item_templates.data_donation" msgstr "Data Donation" #, elixir-autogen, elixir-format, fuzzy -msgid "project_templates.empty" +msgid "project_item_templates.empty" msgstr "Empty" #, elixir-autogen, elixir-format, fuzzy diff --git a/core/priv/gettext/en/LC_MESSAGES/eyra-project.po b/core/priv/gettext/en/LC_MESSAGES/eyra-project.po index 0c023225f..1287db2e2 100644 --- a/core/priv/gettext/en/LC_MESSAGES/eyra-project.po +++ b/core/priv/gettext/en/LC_MESSAGES/eyra-project.po @@ -117,7 +117,7 @@ msgstr "Online" #, elixir-autogen, elixir-format msgid "create.proceed.button" -msgstr "Proceed" +msgstr "Create" #, elixir-autogen, elixir-format msgid "create.title" @@ -133,7 +133,7 @@ msgstr "" #, elixir-autogen, elixir-format, fuzzy msgid "create.item.title" -msgstr "Select item type" +msgstr "Select item template" #, elixir-autogen, elixir-format, fuzzy msgid "item.form.name.label" @@ -141,7 +141,7 @@ msgstr "Name" #, elixir-autogen, elixir-format msgid "item.form.title" -msgstr "Project item" +msgstr "Item settings" #, elixir-autogen, elixir-format, fuzzy msgid "tabbar.item.support" diff --git a/core/priv/gettext/en/LC_MESSAGES/eyra-ui.po b/core/priv/gettext/en/LC_MESSAGES/eyra-ui.po index c360011d9..67f0d60b4 100644 --- a/core/priv/gettext/en/LC_MESSAGES/eyra-ui.po +++ b/core/priv/gettext/en/LC_MESSAGES/eyra-ui.po @@ -257,7 +257,3 @@ msgstr "Copy" #, elixir-autogen, elixir-format, fuzzy msgid "submit.button" msgstr "Save" - -#, elixir-autogen, elixir-format, fuzzy -msgid "save.button" -msgstr "Save" diff --git a/core/priv/gettext/eyra-enums.pot b/core/priv/gettext/eyra-enums.pot index 66b02b06b..3315d92b5 100644 --- a/core/priv/gettext/eyra-enums.pot +++ b/core/priv/gettext/eyra-enums.pot @@ -323,15 +323,15 @@ msgid "organisation_types.team" msgstr "" #, elixir-autogen, elixir-format -msgid "project_templates.benchmark" +msgid "project_item_templates.benchmark" msgstr "" #, elixir-autogen, elixir-format -msgid "project_templates.data_donation" +msgid "project_item_templates.data_donation" msgstr "" #, elixir-autogen, elixir-format -msgid "project_templates.empty" +msgid "project_item_templates.empty" msgstr "" #, elixir-autogen, elixir-format diff --git a/core/priv/gettext/eyra-ui.pot b/core/priv/gettext/eyra-ui.pot index 35fd6dbf6..1223c014d 100644 --- a/core/priv/gettext/eyra-ui.pot +++ b/core/priv/gettext/eyra-ui.pot @@ -257,7 +257,3 @@ msgstr "" #, elixir-autogen, elixir-format msgid "submit.button" msgstr "" - -#, elixir-autogen, elixir-format -msgid "save.button" -msgstr "" diff --git a/core/priv/gettext/nl/LC_MESSAGES/eyra-enums.po b/core/priv/gettext/nl/LC_MESSAGES/eyra-enums.po index 91b635ec6..2051b4cbe 100644 --- a/core/priv/gettext/nl/LC_MESSAGES/eyra-enums.po +++ b/core/priv/gettext/nl/LC_MESSAGES/eyra-enums.po @@ -323,15 +323,15 @@ msgid "organisation_types.team" msgstr "Afdeling" #, elixir-autogen, elixir-format -msgid "project_templates.benchmark" +msgid "project_item_templates.benchmark" msgstr "Benchmark" #, elixir-autogen, elixir-format -msgid "project_templates.data_donation" +msgid "project_item_templates.data_donation" msgstr "Data Donatie Studie" #, elixir-autogen, elixir-format, fuzzy -msgid "project_templates.empty" +msgid "project_item_templates.empty" msgstr "Leeg" #, elixir-autogen, elixir-format, fuzzy diff --git a/core/priv/gettext/nl/LC_MESSAGES/eyra-project.po b/core/priv/gettext/nl/LC_MESSAGES/eyra-project.po index 614c75f99..6811f6d0f 100644 --- a/core/priv/gettext/nl/LC_MESSAGES/eyra-project.po +++ b/core/priv/gettext/nl/LC_MESSAGES/eyra-project.po @@ -117,7 +117,7 @@ msgstr "Online" #, elixir-autogen, elixir-format msgid "create.proceed.button" -msgstr "Doorgaan" +msgstr "Maak aan" #, elixir-autogen, elixir-format msgid "create.title" @@ -125,7 +125,7 @@ msgstr "Selecteer template" #, elixir-autogen, elixir-format, fuzzy msgid "create.item.button" -msgstr "Terugtrekken" +msgstr "Trek terug" #, elixir-autogen, elixir-format msgid "create.item.button.short" @@ -133,7 +133,7 @@ msgstr "" #, elixir-autogen, elixir-format, fuzzy msgid "create.item.title" -msgstr "Kies type item" +msgstr "Kies item sjabloon" #, elixir-autogen, elixir-format, fuzzy msgid "item.form.name.label" @@ -141,7 +141,7 @@ msgstr "Naam" #, elixir-autogen, elixir-format msgid "item.form.title" -msgstr "Project item" +msgstr "Item instellingen" #, elixir-autogen, elixir-format, fuzzy msgid "tabbar.item.support" diff --git a/core/priv/gettext/nl/LC_MESSAGES/eyra-ui.po b/core/priv/gettext/nl/LC_MESSAGES/eyra-ui.po index b0c7950f2..2928996d8 100644 --- a/core/priv/gettext/nl/LC_MESSAGES/eyra-ui.po +++ b/core/priv/gettext/nl/LC_MESSAGES/eyra-ui.po @@ -257,7 +257,3 @@ msgstr "Kopieer" #, elixir-autogen, elixir-format, fuzzy msgid "submit.button" msgstr "Opslaan" - -#, elixir-autogen, elixir-format, fuzzy -msgid "save.button" -msgstr "Opslaan" diff --git a/core/systems/project/_assembly.ex b/core/systems/project/_assembly.ex index 9d98d5b03..5113f9a88 100644 --- a/core/systems/project/_assembly.ex +++ b/core/systems/project/_assembly.ex @@ -4,6 +4,7 @@ defmodule Systems.Project.Assembly do alias Ecto.Changeset import Ecto.Query, warn: false alias Frameworks.Utility.EctoHelper + alias Frameworks.Signal alias Core.Authorization alias Systems.{ @@ -19,11 +20,18 @@ defmodule Systems.Project.Assembly do |> Repo.delete_all() end - def delete(%Project.ItemModel{id: id}) do - from(item in Project.ItemModel, - where: item.id == ^id - ) - |> Repo.delete_all() + def delete(%Project.ItemModel{id: id} = item) do + items = + from(item in Project.ItemModel, + where: item.id == ^id + ) + + Multi.new() + |> Multi.delete_all(:items, items) + |> Multi.put(:project_item, item) + |> EctoHelper.run(:project_node, &load_node!/1) + |> Signal.Public.multi_dispatch({:project_node, :delete_item}) + |> Repo.transaction() end def create(name, user, :empty) do @@ -50,19 +58,20 @@ defmodule Systems.Project.Assembly do when is_binary(name) do Multi.new() |> Multi.insert( - :item, + :project_item, prepare_item(template, name) |> Changeset.put_assoc(:node, node) ) - |> EctoHelper.run(:node, &load_node!/1) + |> EctoHelper.run(:project_node, &load_node!/1) |> EctoHelper.run(:auth, &update_auth/2) |> EctoHelper.run(:path, &update_path/2) + |> Signal.Public.multi_dispatch({:project_node, :create_and_dispatch}) |> Repo.transaction() end # LOAD - defp load_node!(%{item: %{node_id: node_id}}) do + defp load_node!(%{project_item: %{node_id: node_id}}) do {:ok, Project.Public.get_node!(node_id, Project.NodeModel.preload_graph(:down))} end @@ -106,7 +115,7 @@ defmodule Systems.Project.Assembly do # PROJECT PATH def update_path(multi, %{project: project}), do: update_path(multi, project) - def update_path(multi, %{node: %{project_path: project_path} = node}), + def update_path(multi, %{project_node: %{project_path: project_path} = node}), do: update_path(multi, node, project_path) def update_path(multi, %Project.Model{id: id, root: root}) do @@ -165,8 +174,8 @@ defmodule Systems.Project.Assembly do # AUTHORIZATION def update_auth(multi, %{project: project}), do: update_auth(multi, project) - def update_auth(multi, %{node: node}), do: update_auth(multi, node) - def update_auth(multi, %{item: item}), do: update_auth(multi, item) + def update_auth(multi, %{project_node: node}), do: update_auth(multi, node) + def update_auth(multi, %{project_item: item}), do: update_auth(multi, item) def update_auth(multi, %Project.Model{} = project) do auth_tree = Project.Model.auth_tree(project) diff --git a/core/systems/project/_presenter.ex b/core/systems/project/_presenter.ex index 80da3f6a7..f972f0fad 100644 --- a/core/systems/project/_presenter.ex +++ b/core/systems/project/_presenter.ex @@ -4,7 +4,7 @@ defmodule Systems.Project.Presenter do alias Systems.Project @impl true - def view_model(Project.NodePage, %Project.NodeModel{} = node, assigns) do + def view_model(Project.NodePage, node, assigns) do Project.NodePageBuilder.view_model(node, assigns) end diff --git a/core/systems/project/_public.ex b/core/systems/project/_public.ex index 16113b1df..d2369f88a 100644 --- a/core/systems/project/_public.ex +++ b/core/systems/project/_public.ex @@ -3,7 +3,6 @@ defmodule Systems.Project.Public do import CoreWeb.Gettext alias Core.Repo - alias Core.Accounts.User alias Core.Authorization @@ -20,6 +19,14 @@ defmodule Systems.Project.Public do |> Repo.one!() end + def get_by_root(%Project.NodeModel{id: id}, preload \\ []) do + from(project in Project.Model, + where: project.root_id == ^id, + preload: ^preload + ) + |> Repo.one() + end + def get_node!(id, preload \\ []) do from(node in Project.NodeModel, where: node.id == ^id, @@ -235,3 +242,27 @@ defimpl Core.Persister, for: Systems.Project.Model do end end end + +defimpl Core.Persister, for: Systems.Project.ItemModel do + alias Frameworks.Utility.EctoHelper + alias Frameworks.Signal + alias Systems.Project + + def save(_project_item, changeset) do + result = + Ecto.Multi.new() + |> Core.Repo.multi_update(:project_item, changeset) + |> EctoHelper.run(:project_node, &load_node!/1) + |> Signal.Public.multi_dispatch({:project_node, :update}) + |> Core.Repo.transaction() + + case result do + {:ok, %{project_item: project_item}} -> {:ok, project_item} + _ -> {:error, changeset} + end + end + + defp load_node!(%{project_item: %{node_id: node_id}}) do + {:ok, Project.Public.get_node!(node_id, Project.NodeModel.preload_graph(:down))} + end +end diff --git a/core/systems/project/_switch.ex b/core/systems/project/_switch.ex index 3d18bb208..6e793246b 100644 --- a/core/systems/project/_switch.ex +++ b/core/systems/project/_switch.ex @@ -38,8 +38,15 @@ defmodule Systems.Project.Switch do end @impl true - def intercept({:project_item, _}, %{project_item: project_item}) do - update_pages(project_item) + def intercept({:project_node, _} = signal, %{project_node: project_node} = message) do + update_pages(project_node) + + if project = Project.Public.get_by_root(project_node) do + dispatch!( + {:project, signal}, + Map.merge(message, %{project: project}) + ) + end end @impl true @@ -52,9 +59,9 @@ defmodule Systems.Project.Switch do |> then(&dispatch!({:tool_ref, signal}, Map.merge(message, %{tool_ref: &1}))) end - defp update_pages(%Project.ItemModel{} = item) do + defp update_pages(%Project.NodeModel{} = node) do [Project.NodePage] - |> Enum.each(&update_page(&1, item)) + |> Enum.each(&update_page(&1, node)) end defp update_pages(%Project.Model{} = project) do diff --git a/core/systems/project/create_item_popup.ex b/core/systems/project/create_item_popup.ex index f336d5dc7..76e5387ec 100644 --- a/core/systems/project/create_item_popup.ex +++ b/core/systems/project/create_item_popup.ex @@ -1,5 +1,8 @@ defmodule Systems.Project.CreateItemPopup do - use CoreWeb, :live_component + use CoreWeb, :live_component_fabric + use Fabric.LiveComponent + + import CoreWeb.UI.Dialog alias Frameworks.Pixel.Selector @@ -24,13 +27,13 @@ defmodule Systems.Project.CreateItemPopup do # Initial Update @impl true - def update(%{id: id, node: node, target: target}, socket) do + def update(%{id: id, node: node}, socket) do title = dgettext("eyra-project", "create.item.title") { :ok, socket - |> assign(id: id, node: node, target: target, title: title) + |> assign(id: id, node: node, title: title) |> init_templates() |> init_buttons() } @@ -38,7 +41,7 @@ defmodule Systems.Project.CreateItemPopup do defp init_templates(socket) do selected_template = :empty - template_labels = Project.Templates.labels(selected_template) + template_labels = Project.ItemTemplates.labels(selected_template) socket |> assign(template_labels: template_labels, selected_template: selected_template) end @@ -69,21 +72,20 @@ defmodule Systems.Project.CreateItemPopup do ) do create_item(socket, selected_template) - {:noreply, socket |> close()} + {:noreply, socket |> finish()} end @impl true def handle_event("cancel", _, socket) do - {:noreply, socket |> close()} + {:noreply, socket |> finish()} end - defp close(%{assigns: %{target: target}} = socket) do - update_target(target, %{module: __MODULE__, action: :close}) - socket + defp finish(socket) do + socket |> send_event(:parent, "finish") end defp create_item(%{assigns: %{node: node}}, template) do - name = Project.Templates.translate(template) + name = Project.ItemTemplates.translate(template) Project.Assembly.create_item(template, name, node) end @@ -91,23 +93,16 @@ defmodule Systems.Project.CreateItemPopup do def render(assigns) do ~H"""
- <%= @title %> - <.spacing value="S" /> - <.live_component - module={Selector} - id={:template_selector} - items={@template_labels} - type={:radio} - optional?={false} - parent={%{type: __MODULE__, id: @id}} - /> - - <.spacing value="M" /> -
- <%= for button <- @buttons do %> - - <% end %> -
+ <.dialog {%{title: @title, buttons: @buttons}}> + <.live_component + module={Selector} + id={:template_selector} + items={@template_labels} + type={:radio} + optional?={false} + parent={%{type: __MODULE__, id: @id}} + /> +
""" end diff --git a/core/systems/project/create_project_popup.ex b/core/systems/project/create_project_popup.ex index 494d5e841..d253caddc 100644 --- a/core/systems/project/create_project_popup.ex +++ b/core/systems/project/create_project_popup.ex @@ -38,7 +38,7 @@ defmodule Systems.Project.CreatePopup do defp init_templates(socket) do selected_template = :empty - template_labels = Project.Templates.labels(selected_template) + template_labels = Project.ItemTemplates.labels(selected_template) socket |> assign(template_labels: template_labels, selected_template: selected_template) end @@ -87,7 +87,7 @@ defmodule Systems.Project.CreatePopup do end defp create_project(%{assigns: %{user: user}}, template) do - name = Project.Templates.translate(template) + name = Project.ItemTemplates.translate(template) Project.Assembly.create(name, user, template) end diff --git a/core/systems/project/form.ex b/core/systems/project/form.ex index fa3cacd15..48f144c6a 100644 --- a/core/systems/project/form.ex +++ b/core/systems/project/form.ex @@ -44,11 +44,6 @@ defmodule Systems.Project.Form do end # Handle Events - @impl true - def handle_event("close", _params, socket) do - send(self(), %{module: __MODULE__, action: :close}) - {:noreply, socket} - end @impl true def handle_event("change", %{"model" => attrs}, %{assigns: %{project: project}} = socket) do @@ -93,7 +88,7 @@ defmodule Systems.Project.Form do ~H"""
<.dialog {%{title: @title, buttons: @buttons}}> -
+
<.form id={@id} :let={form} for={@changeset} phx-change="change" phx-target={@myself} > <.text_input form={form} field={:name} label_text={dgettext("eyra-project", "form.name.label")} /> diff --git a/core/systems/project/item_form.ex b/core/systems/project/item_form.ex index d15e249c7..dbdecf4e6 100644 --- a/core/systems/project/item_form.ex +++ b/core/systems/project/item_form.ex @@ -1,5 +1,8 @@ defmodule Systems.Project.ItemForm do - use CoreWeb.LiveForm + use CoreWeb.LiveForm, :fabric + use Fabric.LiveComponent + + import CoreWeb.UI.Dialog alias Systems.{ Project @@ -8,69 +11,88 @@ defmodule Systems.Project.ItemForm do # Handle initial update @impl true def update( - %{id: id, entity: item, target: target}, + %{id: id, item: item}, socket ) do changeset = Project.ItemModel.changeset(item, %{}) - close_button = %{ - action: %{type: :send, event: "close"}, - face: %{type: :icon, icon: :close} - } - { :ok, socket |> assign( id: id, - entity: item, - target: target, - close_button: close_button, - changeset: changeset + item: item, + changeset: changeset, + show_errors: false ) + |> update_title() + |> update_buttons() } end - # Handle Events - @impl true - def handle_event("close", _params, socket) do - send(self(), %{module: __MODULE__, action: :close}) - {:noreply, socket} + defp update_title(socket) do + assign(socket, title: dgettext("eyra-project", "item.form.title")) + end + + defp update_buttons(%{assigns: %{myself: myself}} = socket) do + assign(socket, buttons: form_dialog_buttons(myself)) + end + + def handle_view_model_updated(socket) do + socket end + # Handle Events + @impl true - def handle_event("save", %{"item_model" => attrs}, %{assigns: %{entity: entity}} = socket) do + def handle_event("change", %{"item_model" => attrs}, %{assigns: %{item: item}} = socket) do + changeset = Project.ItemModel.changeset(item, attrs) + { :noreply, - socket - |> save(entity, attrs) + socket |> assign(changeset: changeset) } end - # Saving + @impl true + def handle_event("submit", _, socket) do + {:noreply, socket |> submit_form()} + end + + @impl true + def handle_event("cancel", _, socket) do + {:noreply, socket |> finish()} + end + + # Submit - def save(socket, entity, attrs) do - changeset = Project.ItemModel.changeset(entity, attrs) + defp submit_form(%{assigns: %{item: item, changeset: changeset}} = socket) do + case Core.Persister.save(item, changeset) do + {:ok, _} -> + socket |> finish() - socket - |> save(changeset) + {:error, changeset} -> + socket + |> assign(show_errors: true) + |> assign(changeset: changeset) + end + end + + defp finish(socket) do + socket |> send_event(:parent, "finish") end @impl true def render(assigns) do ~H"""
-
-
- <%= dgettext("eyra-project", "item.form.title") %> -
-
- -
- - <.form id={@id} :let={form} for={@changeset} phx-change="save" phx-target={@myself} > - <.text_input form={form} field={:name} label_text={dgettext("eyra-project", "item.form.name.label")} /> - + <.dialog {%{title: @title, buttons: @buttons}}> +
+ <.form id={@id} :let={form} for={@changeset} phx-change="change" phx-target={@myself} > + <.text_input form={form} field={:name} label_text={dgettext("eyra-project", "item.form.name.label")} /> + +
+
""" end diff --git a/core/systems/project/item_templates.ex b/core/systems/project/item_templates.ex new file mode 100644 index 000000000..c76a31666 --- /dev/null +++ b/core/systems/project/item_templates.ex @@ -0,0 +1,3 @@ +defmodule Systems.Project.ItemTemplates do + use Core.Enums.Base, {:project_item_templates, [:benchmark, :data_donation]} +end diff --git a/core/systems/project/node_page.ex b/core/systems/project/node_page.ex index 8ee1ec5f4..57ea9e3b1 100644 --- a/core/systems/project/node_page.ex +++ b/core/systems/project/node_page.ex @@ -1,5 +1,7 @@ defmodule Systems.Project.NodePage do - use CoreWeb, :live_view + use CoreWeb, :live_view_fabric + use Fabric.LiveView, CoreWeb.Layouts + use CoreWeb.Layouts.Workspace.Component, :projects use CoreWeb.UI.PlainDialog use Systems.Observatory.Public @@ -12,6 +14,7 @@ defmodule Systems.Project.NodePage do Project } + @impl true def mount(%{"id" => id}, _session, socket) do model = Project.Public.get_node!(String.to_integer(id), Project.NodeModel.preload_graph(:down)) @@ -19,27 +22,55 @@ defmodule Systems.Project.NodePage do { :ok, socket - |> assign(model: model, popup: nil) + |> assign( + model: model, + popup: nil, + focussed_item: nil + ) |> observe_view_model() |> update_menus() } end + def handle_view_model_updated(socket) do + socket |> update_menus() + end + def handle_auto_save_done(socket) do socket |> update_menus() end + # Childs + + @impl true + def compose(:project_item_form, %{focussed_item: item}) do + %{ + module: Project.ItemForm, + params: %{item: item} + } + end + + @impl true + def compose(:create_item_popup, %{vm: %{node: node}}) do + %{ + module: Project.CreateItemPopup, + params: %{node: node} + } + end + + # Events + @impl true def handle_event("edit", %{"item" => item_id}, socket) do item = Project.Public.get_item!(String.to_integer(item_id)) - popup = %{ - module: Project.ItemForm, - entity: item, - target: self() + { + :noreply, + socket + |> assign(focussed_item: item) + |> compose_child(:project_item_form) + |> show_popup(:project_item_form) } - - {:noreply, assign(socket, popup: popup)} end @impl true @@ -55,16 +86,12 @@ defmodule Systems.Project.NodePage do end @impl true - def handle_event("create_item", _params, %{assigns: %{vm: %{node: node}}} = socket) do - popup = %{ - module: Project.CreateItemPopup, - target: self(), - node: node - } - + def handle_event("create_item", _params, socket) do { :noreply, - socket |> assign(popup: popup) + socket + |> compose_child(:create_item_popup) + |> show_popup(:create_item_popup) } end @@ -80,12 +107,17 @@ defmodule Systems.Project.NodePage do end @impl true - def handle_info(%{module: _, action: :close}, socket) do + def handle_event("show_popup", %{ref: %{id: id, module: module}, params: params}, socket) do + popup = %{module: module, params: Map.put(params, :id, id)} + {:noreply, socket |> assign(popup: popup)} + end + + @impl true + def handle_event("finish", _, socket) do { :noreply, socket |> assign(popup: nil) - |> update_view_model() } end @@ -110,8 +142,8 @@ defmodule Systems.Project.NodePage do <%= if @popup do %> <.popup> -
- <.live_component id={:node_page_popup} module={@popup.module} {@popup} /> +
+ <.live_component id={:node_page_popup} module={@popup.module} {@popup.params} />
<% end %> diff --git a/core/systems/project/node_page_builder.ex b/core/systems/project/node_page_builder.ex index 5963052c5..b856027de 100644 --- a/core/systems/project/node_page_builder.ex +++ b/core/systems/project/node_page_builder.ex @@ -6,13 +6,11 @@ defmodule Systems.Project.NodePageBuilder do } def view_model( - %{ + %Project.NodeModel{ id: id - }, + } = node, assigns ) do - node = Project.Public.get_node!(id, Project.NodeModel.preload_graph(:down)) - 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 85790fa7d..cec22345d 100644 --- a/core/systems/project/overview_page.ex +++ b/core/systems/project/overview_page.ex @@ -228,7 +228,7 @@ defmodule Systems.Project.OverviewPage do <%= if @popup do %> <.popup> -
+
<.live_component id={:project_overview_popup} module={@popup.module} {@popup.props} />
diff --git a/core/systems/project/templates.ex b/core/systems/project/templates.ex deleted file mode 100644 index 45ffd1c69..000000000 --- a/core/systems/project/templates.ex +++ /dev/null @@ -1,3 +0,0 @@ -defmodule Systems.Project.Templates do - use Core.Enums.Base, {:project_templates, [:empty, :benchmark, :data_donation]} -end diff --git a/core/test/systems/project/_assembly_test.exs b/core/test/systems/project/_assembly_test.exs index 38da768f3..23971acb0 100644 --- a/core/test/systems/project/_assembly_test.exs +++ b/core/test/systems/project/_assembly_test.exs @@ -10,7 +10,7 @@ defmodule Systems.Project.AssemblyTest do item_name = "Item" - {:ok, %{item: %{id: id}}} = Project.Assembly.create_item(:benchmark, item_name, root) + {:ok, %{project_item: %{id: id}}} = Project.Assembly.create_item(:benchmark, item_name, root) item = Project.Public.get_item!(id, Project.ItemModel.preload_graph(:down)) assert %Systems.Project.ItemModel{ @@ -44,7 +44,9 @@ defmodule Systems.Project.AssemblyTest do item_name = "Item" - {:ok, %{item: %{id: id}}} = Project.Assembly.create_item(:data_donation, item_name, root) + {:ok, %{project_item: %{id: id}}} = + Project.Assembly.create_item(:data_donation, item_name, root) + item = Project.Public.get_item!(id, Project.ItemModel.preload_graph(:down)) assert %{ From 27a72b9f9d6adffe4bfd66d70ed3ef2e527813b0 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 30 Nov 2023 20:42:22 +0000 Subject: [PATCH 02/14] Bump docker/metadata-action Bumps [docker/metadata-action](https://github.com/docker/metadata-action) from 2a4836ac76fe8f5d0ee3a0d89aa12a80cc552ad3 to e6428a5c4e294a61438ed7f43155db912025b6b3. - [Release notes](https://github.com/docker/metadata-action/releases) - [Commits](https://github.com/docker/metadata-action/compare/2a4836ac76fe8f5d0ee3a0d89aa12a80cc552ad3...e6428a5c4e294a61438ed7f43155db912025b6b3) --- updated-dependencies: - dependency-name: docker/metadata-action dependency-type: direct:production ... Signed-off-by: dependabot[bot] --- .github/workflows/docker_release.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/docker_release.yml b/.github/workflows/docker_release.yml index a75410976..4e494b045 100644 --- a/.github/workflows/docker_release.yml +++ b/.github/workflows/docker_release.yml @@ -37,7 +37,7 @@ jobs: - name: Extract metadata (tags, labels) for Docker id: meta - uses: docker/metadata-action@2a4836ac76fe8f5d0ee3a0d89aa12a80cc552ad3 + uses: docker/metadata-action@e6428a5c4e294a61438ed7f43155db912025b6b3 with: images: | ${{env.REGISTRY}}/eyra/${{github.event.inputs.bundle}} From 07a721788ff653ce2077d1fdef5114d397fc66e9 Mon Sep 17 00:00:00 2001 From: emielvdveen Date: Sun, 3 Dec 2023 12:11:45 +0100 Subject: [PATCH 03/14] Fixed Wysiwyg switching combined with realtime updates --- core/assets/js/wysiwyg.js | 71 +++++++++++++++++-- core/frameworks/pixel/components/form.ex | 30 ++++---- .../pixel/components/radio_group.ex | 47 ++++++++++++ core/frameworks/pixel/components/selector.ex | 13 +++- core/frameworks/pixel/components/switch.ex | 39 ++++------ core/systems/assignment/gdpr_form.ex | 17 ++--- core/systems/consent/revision_form.ex | 39 +++++++++- core/systems/consent/wysiwyg_test_form.ex | 38 ++++++++++ 8 files changed, 235 insertions(+), 59 deletions(-) create mode 100644 core/frameworks/pixel/components/radio_group.ex create mode 100644 core/systems/consent/wysiwyg_test_form.ex diff --git a/core/assets/js/wysiwyg.js b/core/assets/js/wysiwyg.js index e300c2e30..f20ba48f0 100644 --- a/core/assets/js/wysiwyg.js +++ b/core/assets/js/wysiwyg.js @@ -2,9 +2,72 @@ import Trix from "trix"; export const Wysiwyg = { mounted() { - const element = document.querySelector("trix-editor"); - element.editor.element.addEventListener("trix-change", (e) => { - element.dispatchEvent(new Event("input", {bubbles: true})) + console.log("[Wysiwyg] Mounted"); + this.init(); + this.insertTextArea(this.html); + this.upsert_editor(this.visible); + }, + updated() { + console.log("[Wysiwyg] Updated"); + if (!this.el.parentNode.classList.contains("border-primary")) { + console.log("[Wysiwyg] Reset"); + this.init(); + this.insertTextArea(this.html); + this.upsert_editor(this.visible); + } + }, + init() { + this.id = this.el.dataset.id; + this.name = this.el.dataset.name; + this.target = this.el.dataset.target; + this.html = this.el.dataset.html; + this.visible = this.el.dataset.visible != undefined; + this.locked = this.el.dataset.locked != undefined; + }, + insertTextArea(html) { + if (this.textarea != undefined) { + this.textarea.remove(); + } + + this.textarea = document.createElement("textarea"); + this.textarea.setAttribute("id", this.id); + this.textarea.setAttribute("name", this.name); + this.textarea.setAttribute("phx-target", this.target); + this.textarea.setAttribute("phx-debounce", "1000"); + this.textarea.classList.add("hidden"); + this.textarea.value = html; + this.el.appendChild(this.textarea); + }, + upsert_editor(visible) { + if (visible) { + this.removeEditor(); + this.insertEditor(); + } else { + this.removeEditor(); + } + }, + insertEditor() { + this.editor = document.createElement("trix-editor"); + this.editor.setAttribute("input", this.textarea.id); + this.editor.classList.add("min-h-wysiwyg-editor"); + this.editor.classList.add("max-h-wysiwyg-editor"); + this.editor.classList.add("overflow-y-scroll"); + this.editor.setAttribute("phx-debounce", "1000"); + + this.container = document.createElement("div"); + this.container.appendChild(this.editor); + this.el.appendChild(this.container); + this.editor.addEventListener("trix-change", (e) => { + this.editor.dispatchEvent(new Event("input", { bubbles: true })); }); }, -}; \ No newline at end of file + removeEditor() { + if (this.container != undefined) { + while (this.container.lastElementChild) { + this.container.removeChild(this.container.lastElementChild); + } + this.container.remove(); + this.container = undefined; + } + }, +}; diff --git a/core/frameworks/pixel/components/form.ex b/core/frameworks/pixel/components/form.ex index 90cc61a8c..5bf95badb 100644 --- a/core/frameworks/pixel/components/form.ex +++ b/core/frameworks/pixel/components/form.ex @@ -389,6 +389,7 @@ defmodule Frameworks.Pixel.Form do attr(:debounce, :string, default: "1000") attr(:min_height, :string, default: "min-h-wysiwyg-editor") attr(:max_height, :string, default: "max-h-wysiwyg-editor") + attr(:visible, :boolean, default: true) def wysiwyg_area(%{form: form, field: field} = assigns) do errors = guarded_errors(form, field) @@ -415,6 +416,7 @@ defmodule Frameworks.Pixel.Form do }) ~H""" +
<.field field={@field_id} label_text={@label_text} @@ -423,17 +425,7 @@ defmodule Frameworks.Pixel.Form do errors={@errors} extra_space={false} > -
- - <%= @field_value %> - +
+
""" end diff --git a/core/frameworks/pixel/components/radio_group.ex b/core/frameworks/pixel/components/radio_group.ex new file mode 100644 index 000000000..82f519089 --- /dev/null +++ b/core/frameworks/pixel/components/radio_group.ex @@ -0,0 +1,47 @@ +defmodule Frameworks.Pixel.RadioGroup do + use CoreWeb, :live_component_fabric + use Fabric.LiveComponent + + @impl true + def update(%{items: items}, socket) do + active_item = items |> Enum.find(& &1.active) + form = to_form(%{"radio-group" => "#{active_item.id}"}) + + { + :ok, + socket + |> assign(items: items, form: form) + } + end + + @impl true + def handle_event("change", %{"radio-group" => status}, socket) do + {:noreply, + socket |> send_event(:parent, "update", %{status: String.to_existing_atom(status)})} + end + + @impl true + def render(assigns) do + ~H""" +
+ <.form id={"#{@id}_form"} for={@form} phx-change="change" phx-target={@myself}> +
+ <%= for item <- @items do %> + + <% end %> +
+ +
+ """ + end +end diff --git a/core/frameworks/pixel/components/selector.ex b/core/frameworks/pixel/components/selector.ex index 2208ac0a0..86ce2653e 100644 --- a/core/frameworks/pixel/components/selector.ex +++ b/core/frameworks/pixel/components/selector.ex @@ -31,6 +31,15 @@ defmodule Frameworks.Pixel.Selector do } end + @impl true + def update(%{items: new_items}, %{assigns: %{items: _items}} = socket) do + { + :ok, + socket + |> assign(current_items: new_items) + } + end + @impl true def update( %{ @@ -213,7 +222,7 @@ defmodule Frameworks.Pixel.Selector.Item do }) ~H""" -
+
+ """ end diff --git a/core/frameworks/pixel/components/switch.ex b/core/frameworks/pixel/components/switch.ex index f1483d7a1..2f38be94d 100644 --- a/core/frameworks/pixel/components/switch.ex +++ b/core/frameworks/pixel/components/switch.ex @@ -4,12 +4,6 @@ defmodule Frameworks.Pixel.Switch do alias Frameworks.Pixel - # Handle Selector Update - @impl true - def update(%{active_item_id: status, selector_id: :selector}, socket) do - {:ok, socket |> update_status(status)} - end - @impl true def update( %{id: id, on_text: on_text, off_text: off_text, opt_in?: opt_in?, status: status}, @@ -25,24 +19,12 @@ defmodule Frameworks.Pixel.Switch do opt_in?: opt_in?, status: status ) - |> compose_child(:selector) + |> compose_child(:radio_group) } end - defp update_status(%{assigns: %{status: status}} = socket, new_status) - when status != new_status do - socket - |> assign(status: new_status) - |> send_event(:parent, "switch", %{status: new_status}) - end - - defp update_status(socket, _status) do - socket - end - @impl true - def compose(:selector, %{ - id: id, + def compose(:radio_group, %{ on_text: on_text, off_text: off_text, opt_in?: opt_in?, @@ -59,17 +41,22 @@ defmodule Frameworks.Pixel.Switch do end %{ - module: Pixel.Selector, + module: Pixel.RadioGroup, params: %{ - grid_options: "flex flex-row gap-8", - items: items, - type: :radio, - optional?: false, - parent: %{id: id, type: __MODULE__} + items: items } } end + @impl true + def handle_event("update", %{status: status}, socket) do + { + :noreply, + socket + |> send_event(:parent, "update", %{status: status}) + } + end + @impl true def render(assigns) do ~H""" diff --git a/core/systems/assignment/gdpr_form.ex b/core/systems/assignment/gdpr_form.ex index dd659b881..317698ad2 100644 --- a/core/systems/assignment/gdpr_form.ex +++ b/core/systems/assignment/gdpr_form.ex @@ -40,7 +40,10 @@ defmodule Systems.Assignment.GdprForm do @impl true def compose(:consent_revision_form, %{entity: %{consent_agreement: nil}}) do - nil + %{ + module: Consent.RevisionForm, + params: %{entity: nil} + } end @impl true @@ -55,28 +58,26 @@ defmodule Systems.Assignment.GdprForm do @impl true def handle_event( - "switch", + "update", %{status: :on}, %{assigns: %{entity: %{auth_node: auth_node} = assignment}} = socket ) do - consent_agreement = Consent.Public.prepare_agreement(auth_node: auth_node) - Assignment.Public.update_consent_agreement(assignment, consent_agreement) + consent_agreement = Consent.Public.prepare_agreement(auth_node) + {:ok, _} = Assignment.Public.update_consent_agreement(assignment, consent_agreement) { :noreply, socket - |> compose_child(:consent_revision_form) } end @impl true - def handle_event("switch", %{status: :off}, %{assigns: %{entity: assignment}} = socket) do - Assignment.Public.update_consent_agreement(assignment, nil) + def handle_event("update", %{status: :off}, %{assigns: %{entity: assignment}} = socket) do + {:ok, _} = Assignment.Public.update_consent_agreement(assignment, nil) { :noreply, socket - |> hide_child(:consent_revision_form) } end diff --git a/core/systems/consent/revision_form.ex b/core/systems/consent/revision_form.ex index cad4c5035..e17ed811c 100644 --- a/core/systems/consent/revision_form.ex +++ b/core/systems/consent/revision_form.ex @@ -16,6 +16,23 @@ defmodule Systems.Consent.RevisionForm do |> assign( id: id, entity: entity, + visible: true, + form: form + ) + } + end + + @impl true + def update(%{id: id, entity: nil}, socket) do + form = to_form(%{"source" => "?"}) + + { + :ok, + socket + |> assign( + id: id, + entity: nil, + visible: false, form: form ) } @@ -37,19 +54,36 @@ defmodule Systems.Consent.RevisionForm do } end + @impl true + def handle_event( + "save", + _, + %{assigns: %{entity: nil}} = socket + ) do + { + :noreply, + socket + } + end + # Saving def save(socket, entity, attrs) do changeset = Consent.RevisionModel.changeset(entity, attrs) + auto_save_begin(socket) + case Core.Persister.save(entity, changeset) do {:ok, entity} -> socket |> assign(entity: entity) |> flash_persister_saved() + |> auto_save_end() {:error, changeset} -> - socket |> handle_save_errors(changeset) + socket + |> handle_save_errors(changeset) + |> auto_save_end() end end @@ -70,7 +104,8 @@ defmodule Systems.Consent.RevisionForm do ~H"""
<.form id="agreement_form" :let={form} for={@form} phx-change="save" phx-target={@myself} > - <.wysiwyg_area form={form} field={:source} /> + + <.wysiwyg_area form={form} field={:source} visible={@visible}/>
""" diff --git a/core/systems/consent/wysiwyg_test_form.ex b/core/systems/consent/wysiwyg_test_form.ex new file mode 100644 index 000000000..03b187a03 --- /dev/null +++ b/core/systems/consent/wysiwyg_test_form.ex @@ -0,0 +1,38 @@ +defmodule Systems.Consent.WysiwygTestForm do + use CoreWeb.LiveForm, :fabric + use Fabric.LiveComponent + + @impl true + def update(%{x: x}, socket) do + { + :ok, + socket |> assign(x: x) |> update_visible() + } + end + + defp update_visible(%{assigns: %{x: nil}} = socket) do + socket |> assign(visible: false) + end + + defp update_visible(socket) do + socket |> assign(visible: true) + end + + @impl true + def render(assigns) do + ~H""" +
+
+
+
+
+ """ + end +end From e3f6392a2ebbfa38e82f184f5a6b343525caeae0 Mon Sep 17 00:00:00 2001 From: emielvdveen Date: Sun, 3 Dec 2023 15:10:57 +0100 Subject: [PATCH 04/14] Implemented user content storage on S3 iso local FS --- core/config/dev.exs | 9 +++ core/config/runtime.exs | 12 +++- core/config/test.exs | 4 ++ core/lib/core_web/endpoint.ex | 2 + core/systems/assignment/info_form.ex | 10 ++- core/systems/content/_private.ex | 7 ++ core/systems/content/_public.ex | 12 ++++ core/systems/content/local_fs.ex | 38 +++++++++++ core/systems/content/plug.ex | 33 ++++++++++ core/systems/content/s3.ex | 59 +++++++++++++++++ core/test/systems/content/hello.svg | 12 ++++ core/test/systems/content/local_fs_test.exs | 31 +++++++++ core/test/systems/content/plug_test.exs | 62 ++++++++++++++++++ core/test/systems/content/s3_test.exs | 71 +++++++++++++++++++++ core/test/systems/next_wide.svg | 12 ++++ 15 files changed, 369 insertions(+), 5 deletions(-) create mode 100644 core/systems/content/_private.ex create mode 100644 core/systems/content/local_fs.ex create mode 100644 core/systems/content/plug.ex create mode 100644 core/systems/content/s3.ex create mode 100644 core/test/systems/content/hello.svg create mode 100644 core/test/systems/content/local_fs_test.exs create mode 100644 core/test/systems/content/plug_test.exs create mode 100644 core/test/systems/content/s3_test.exs create mode 100644 core/test/systems/next_wide.svg diff --git a/core/config/dev.exs b/core/config/dev.exs index 8ff960e77..8ea779f8a 100644 --- a/core/config/dev.exs +++ b/core/config/dev.exs @@ -85,6 +85,15 @@ config :core, # access_key_id: "my_access_key", # secret_access_key: "a_super_secret" +config :core, :content, + backend: Systems.Content.LocalFS, + local_fs_root_path: + File.cwd!() + |> Path.join("priv") + |> Path.join("static") + |> Path.join("content") + |> tap(&File.mkdir_p!/1) + config :core, :feldspar, backend: Systems.Feldspar.LocalFS, local_fs_root_path: diff --git a/core/config/runtime.exs b/core/config/runtime.exs index 50bc8e900..9ca024712 100644 --- a/core/config/runtime.exs +++ b/core/config/runtime.exs @@ -123,13 +123,19 @@ if config_env() == :prod do dsn: System.get_env("SENTRY_DSN"), environment_name: System.get_env("RELEASE_ENV") || "prod" + config :core, :content, + backend: Systems.Content.S3, + bucket: System.get_env("PUBLIC_S3_BUCKET"), + public_url: System.get_env("PUBLIC_S3_URL"), + prefix: System.get_env("CONTENT_S3_PREFIX", nil) + config :core, :feldspar, backend: Systems.Feldspar.S3, - bucket: System.get_env("FELDSPAR_S3_BUCKET"), - prefix: System.get_env("FELDSPAR_S3_PREFIX", nil), + bucket: System.get_env("PUBLIC_S3_BUCKET"), # The public URL must point to the root's (bucket) publicly accessible URL. # It should have a policy that allows anonymous users to read all files. - public_url: System.get_env("FELDSPAR_S3_PUBLIC_URL") + public_url: System.get_env("PUBLIC_S3_URL"), + prefix: System.get_env("FELDSPAR_S3_PREFIX", nil) config :core, :dist_hosts, diff --git a/core/config/test.exs b/core/config/test.exs index b1b09b6a7..e4ff51ca2 100644 --- a/core/config/test.exs +++ b/core/config/test.exs @@ -55,6 +55,10 @@ config :core, :bundle, :next config :core, :banking_backend, Systems.Banking.Dummy +config :core, :content, + backend: Systems.Content.LocalFS, + local_fs_root_path: "/tmp" + config :core, :feldspar, backend: Systems.Feldspar.LocalFS, local_fs_root_path: "/tmp" diff --git a/core/lib/core_web/endpoint.ex b/core/lib/core_web/endpoint.ex index 2c7a050f8..299e82db0 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 # The session will be stored in the cookie and signed, @@ -34,6 +35,7 @@ defmodule CoreWeb.Endpoint do ) end + Systems.Content.Plug.setup() Systems.Feldspar.Plug.setup() # Serve at "/" the static files from "priv/static" directory. diff --git a/core/systems/assignment/info_form.ex b/core/systems/assignment/info_form.ex index e207685bf..d01388156 100644 --- a/core/systems/assignment/info_form.ex +++ b/core/systems/assignment/info_form.ex @@ -11,13 +11,19 @@ defmodule Systems.Assignment.InfoForm do alias CoreWeb.UI.ImageCatalogPicker alias Systems.Assignment + alias Systems.Content @impl true def process_file( %{assigns: %{entity: entity}} = socket, - {local_relative_path, _local_full_path, _remote_file} + {_local_relative_path, local_full_path, _remote_file} ) do - save(socket, entity, :auto_save, %{logo_url: local_relative_path}) + logo_url = + Content.Public.store(local_full_path) + |> Content.Public.get_public_url() + + socket + |> save(entity, :auto_save, %{logo_url: logo_url}) end @impl true diff --git a/core/systems/content/_private.ex b/core/systems/content/_private.ex new file mode 100644 index 000000000..72ff5ad97 --- /dev/null +++ b/core/systems/content/_private.ex @@ -0,0 +1,7 @@ +defmodule Systems.Content.Private do + def get_backend do + :core + |> Application.fetch_env!(:content) + |> Access.fetch!(:backend) + end +end diff --git a/core/systems/content/_public.ex b/core/systems/content/_public.ex index 5c56c8135..6fd06056a 100644 --- a/core/systems/content/_public.ex +++ b/core/systems/content/_public.ex @@ -1,12 +1,24 @@ defmodule Systems.Content.Public do import Ecto.Query, warn: false + alias Systems.{ + Content + } + alias Core.Repo alias Ecto.Multi alias Systems.Content.TextItemModel, as: TextItem alias Systems.Content.TextBundleModel, as: TextBundle + def store(file) do + Content.Private.get_backend().store(file) + end + + def get_public_url(id) do + Content.Private.get_backend().get_public_url(id) + end + def get_text_item!(id, preload \\ []) do from(t in TextItem, preload: ^preload) |> Repo.get!(id) diff --git a/core/systems/content/local_fs.ex b/core/systems/content/local_fs.ex new file mode 100644 index 000000000..131ed0db1 --- /dev/null +++ b/core/systems/content/local_fs.ex @@ -0,0 +1,38 @@ +defmodule Systems.Content.LocalFS do + alias CoreWeb.Endpoint + + def store(tmp_path) do + uuid = Ecto.UUID.generate() + extname = Path.extname(tmp_path) + id = "#{uuid}#{extname}" + path = get_path(id) + File.cp!(tmp_path, path) + id + end + + def storage_path(id) do + get_path(id) + end + + def get_public_url(id) do + "#{Endpoint.url()}/#{public_path()}/#{id}" + end + + def remove(id) do + with {:ok, _} <- File.rm_rf(get_path(id)) do + :ok + end + end + + defp get_path(id) do + Path.join(get_root_path(), id) + end + + def get_root_path do + :core + |> Application.get_env(:content, []) + |> Access.fetch!(:local_fs_root_path) + end + + def public_path, do: "/content" +end diff --git a/core/systems/content/plug.ex b/core/systems/content/plug.ex new file mode 100644 index 000000000..207165b12 --- /dev/null +++ b/core/systems/content/plug.ex @@ -0,0 +1,33 @@ +defmodule Systems.Content.Plug do + @behaviour Plug + + defmacro setup() do + quote do + plug(Systems.Content.Plug, at: Systems.Content.LocalFS.public_path()) + end + end + + @impl true + def init(opts) do + opts + # Ensure that init works, from will be set dynamically later on + |> Keyword.put(:from, {nil, nil}) + |> Plug.Static.init() + end + + @impl true + def call( + conn, + options + ) do + call(Systems.Content.Private.get_backend(), conn, options) + end + + def call(Systems.Content.LocalFS, conn, options) do + root_path = Systems.Content.LocalFS.get_root_path() + options = Map.put(options, :from, root_path) + Plug.Static.call(conn, options) + end + + def call(_, conn, _options), do: conn +end diff --git a/core/systems/content/s3.ex b/core/systems/content/s3.ex new file mode 100644 index 000000000..c4fbd8ab1 --- /dev/null +++ b/core/systems/content/s3.ex @@ -0,0 +1,59 @@ +defmodule Systems.Content.S3 do + alias ExAws.S3 + + def store(file) do + bucket = Access.fetch!(s3_settings(), :bucket) + uuid = Ecto.UUID.generate() + extname = Path.extname(file) + id = "#{uuid}#{extname}" + + upload_file(file, id, bucket) + id + end + + def remove(id) do + bucket = Access.fetch!(s3_settings(), :bucket) + object_key = "#{object_key(id)}" + + S3.delete_object(bucket, object_key) + |> backend().request!() + end + + def get_public_url(id) do + settings = s3_settings() + public_url = Access.get(settings, :public_url) + "#{public_url}/#{object_key(id)}" + end + + defp upload_file(file, id, bucket) do + {:ok, data} = File.read(file) + object_key = "#{object_key(id)}" + + S3.put_object( + bucket, + object_key, + data, + content_type: content_type(file) + ) + |> backend().request!() + end + + defp content_type(name), do: MIME.from_path(name) + + defp object_key(id) do + prefix = Access.get(s3_settings(), :prefix, nil) + + [prefix, id] + |> Enum.filter(&(&1 != nil)) + |> Enum.join("/") + end + + defp s3_settings do + Application.fetch_env!(:core, :content) + end + + defp backend do + # Allow mocking + Access.get(s3_settings(), :s3_backend, ExAws) + end +end diff --git a/core/test/systems/content/hello.svg b/core/test/systems/content/hello.svg new file mode 100644 index 000000000..3f9ef5122 --- /dev/null +++ b/core/test/systems/content/hello.svg @@ -0,0 +1,12 @@ + + + + + + + + + + + + diff --git a/core/test/systems/content/local_fs_test.exs b/core/test/systems/content/local_fs_test.exs new file mode 100644 index 000000000..440cfbed2 --- /dev/null +++ b/core/test/systems/content/local_fs_test.exs @@ -0,0 +1,31 @@ +defmodule Systems.Content.LocalFSTest do + use ExUnit.Case, async: true + + alias Systems.Content.LocalFS + + describe "store/1" do + test "extracts stores file on disk" do + id = LocalFS.store(Path.join(__DIR__, "hello.svg")) + path = LocalFS.storage_path(id) + assert File.exists?(path) + end + end + + describe "get_public_url/1" do + test "returns URL" do + id = Ecto.UUID.generate() + url = LocalFS.get_public_url(id) + uri = URI.parse(url) + assert String.contains?(uri.path, id) + end + end + + describe "remove/1" do + test "removes folder" do + id = LocalFS.store(Path.join(__DIR__, "hello.svg")) + path = LocalFS.storage_path(id) + assert :ok == LocalFS.remove(id) + refute File.exists?(path) + end + end +end diff --git a/core/test/systems/content/plug_test.exs b/core/test/systems/content/plug_test.exs new file mode 100644 index 000000000..fa4f6dd7e --- /dev/null +++ b/core/test/systems/content/plug_test.exs @@ -0,0 +1,62 @@ +defmodule Systems.Content.PlugTest do + use ExUnit.Case + use Plug.Test + + require Systems.Content.Plug + alias Systems.Content.Plug + + setup do + conf = Application.get_env(:core, :content, []) + + on_exit(fn -> + Application.put_env(:core, :content, conf) + end) + + folder_name = "temp_#{:crypto.strong_rand_bytes(16) |> Base.encode16()}" + + tmp_dir = + System.tmp_dir() + |> Path.join(folder_name) + + File.mkdir!(tmp_dir) + + on_exit(fn -> + File.rm_rf!(tmp_dir) + end) + + conf = + conf + |> Keyword.put(:backend, Systems.Content.LocalFS) + |> Keyword.put(:local_fs_root_path, tmp_dir) + + Application.put_env( + :core, + :content, + conf + ) + + {:ok, tmp_dir: tmp_dir, app_conf: conf} + end + + test "call with LocalFS backend serves static content", %{tmp_dir: tmp_dir} do + tmp_dir + |> Path.join("plug_test.txt") + |> File.write("hello world!") + + opts = Plug.init(at: "/content") + conn = Plug.call(conn(:get, "/content/plug_test.txt"), opts) + assert "hello world!" == conn.resp_body + end + + test "call with other backends doesn't serve static content", %{app_conf: conf} do + Application.put_env( + :core, + :feldspar, + Keyword.put(conf, :backend, Systems.Content.FakeBackend) + ) + + opts = Plug.init(at: "/txt") + conn = Plug.call(conn(:get, "/txt/plug_test.txt"), opts) + assert nil == conn.resp_body + end +end diff --git a/core/test/systems/content/s3_test.exs b/core/test/systems/content/s3_test.exs new file mode 100644 index 000000000..4d899b507 --- /dev/null +++ b/core/test/systems/content/s3_test.exs @@ -0,0 +1,71 @@ +defmodule Systems.Content.S3Test do + use ExUnit.Case, async: true + import Mox + alias Systems.Content.S3 + + setup :verify_on_exit! + + setup do + initial_config = Application.get_env(:core, :content) + + Application.put_env(:core, :content, + backend: Systems.Content.S3, + bucket: "test-bucket", + public_url: "http://example.com", + s3_backend: MockAws + ) + + on_exit(fn -> + Application.put_env(:core, :content, initial_config) + end) + + :ok + end + + describe "store/1" do + test "stores file on disk" do + expect(MockAws, :request!, fn args -> + assert %ExAws.Operation.S3{ + bucket: "test-bucket", + http_method: :put, + resource: "", + params: %{}, + headers: %{"content-type" => "image/svg+xml"}, + service: :s3 + } = args + end) + + id = S3.store(Path.join(__DIR__, "hello.svg")) + assert is_binary(id) + refute id == "" + end + end + + describe "get_public_url/1 (without prefix)" do + test "returns URL" do + id = Ecto.UUID.generate() + url = S3.get_public_url(id) + assert "http://example.com/#{id}" == url + end + end + + describe "remove/1" do + test "removes file" do + id = Ecto.UUID.generate() + + expect(MockAws, :request!, 1, fn args -> + args + end) + + assert %ExAws.Operation.S3{ + bucket: "test-bucket", + http_method: :delete, + body: "", + resource: "", + params: %{}, + headers: %{}, + service: :s3 + } = S3.remove(id) + end + end +end diff --git a/core/test/systems/next_wide.svg b/core/test/systems/next_wide.svg new file mode 100644 index 000000000..3f9ef5122 --- /dev/null +++ b/core/test/systems/next_wide.svg @@ -0,0 +1,12 @@ + + + + + + + + + + + + From 7f9b5fc4ccad0f842d9013a15901d628f66184c9 Mon Sep 17 00:00:00 2001 From: emielvdveen Date: Sun, 3 Dec 2023 15:44:59 +0100 Subject: [PATCH 05/14] Fixed radio button by replacing css ring with outline --- core/frameworks/pixel/components/radio_group.ex | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/frameworks/pixel/components/radio_group.ex b/core/frameworks/pixel/components/radio_group.ex index 82f519089..903d731a2 100644 --- a/core/frameworks/pixel/components/radio_group.ex +++ b/core/frameworks/pixel/components/radio_group.ex @@ -34,7 +34,7 @@ defmodule Frameworks.Pixel.RadioGroup do type="radio" name="radio-group" checked={item.active} - class="cursor-pointer appearance-none w-3 h-3 rounded-full ring-2 ring-offset-4 ring-grey3 checked:bg-primary checked:ring-primary" + class="cursor-pointer appearance-none w-3 h-3 rounded-full outline outline-2 outline-offset-4 outline-grey3 checked:bg-primary checked:outline-primary" />
<%= item.value %>
From 499e04380fdb73837853c7de54797bf60a3d4b44 Mon Sep 17 00:00:00 2001 From: emielvdveen Date: Sun, 3 Dec 2023 16:26:57 +0100 Subject: [PATCH 06/14] Fix for drop-shadow on trix toolbar on production --- core/assets/tailwind.config.js | 1 + 1 file changed, 1 insertion(+) diff --git a/core/assets/tailwind.config.js b/core/assets/tailwind.config.js index 2dc3c04cc..b6db63a04 100644 --- a/core/assets/tailwind.config.js +++ b/core/assets/tailwind.config.js @@ -8,6 +8,7 @@ module.exports = { "./js/**/*.js", ], safelist: [ + "drop-shadow-lg", "drop-shadow-2xl", "text-bold", "text-pre", From 783c6905ba71aa233929758f011e9dd1eb84c54f Mon Sep 17 00:00:00 2001 From: emielvdveen Date: Sun, 3 Dec 2023 16:34:21 +0100 Subject: [PATCH 07/14] Fix for drop-shadow on trix toolbar by replacing it with a simple border --- core/assets/css/app.css | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/core/assets/css/app.css b/core/assets/css/app.css index 946e624c7..47a5c4968 100644 --- a/core/assets/css/app.css +++ b/core/assets/css/app.css @@ -3,7 +3,7 @@ @tailwind components; trix-toolbar { - @apply mb-4 drop-shadow-lg h-8; + @apply mb-4 h-8; } trix-toolbar .trix-button-row { @@ -15,7 +15,7 @@ trix-toolbar .trix-button-row { } trix-toolbar .trix-button-group { - @apply flex rounded-lg overflow-hidden h-full; + @apply flex border-2 border-grey4 rounded-lg overflow-hidden h-full; } trix-toolbar .trix-button-group:not(:first-child) { margin-left: 1.5vw; From abaf3c532715d476368094956ba05510f0f3f0db Mon Sep 17 00:00:00 2001 From: emielvdveen Date: Sun, 3 Dec 2023 18:22:31 +0100 Subject: [PATCH 08/14] Fixed firing premature radio group update --- .../pixel/components/radio_group.ex | 19 ++++++++++++++++--- 1 file changed, 16 insertions(+), 3 deletions(-) diff --git a/core/frameworks/pixel/components/radio_group.ex b/core/frameworks/pixel/components/radio_group.ex index 903d731a2..d04e6efea 100644 --- a/core/frameworks/pixel/components/radio_group.ex +++ b/core/frameworks/pixel/components/radio_group.ex @@ -15,9 +15,22 @@ defmodule Frameworks.Pixel.RadioGroup do end @impl true - def handle_event("change", %{"radio-group" => status}, socket) do - {:noreply, - socket |> send_event(:parent, "update", %{status: String.to_existing_atom(status)})} + def handle_event( + "change", + %{"radio-group" => status}, + %{assigns: %{form: %{source: %{"radio-group" => current_status}}}} = socket + ) + when current_status != status do + { + :noreply, + socket |> send_event(:parent, "update", %{status: String.to_existing_atom(status)}) + } + end + + @impl true + def handle_event("change", _, socket) do + # ignore change, this happens always the first time rendering + {:noreply, socket} end @impl true From 12b5d658d2234999e7eefac189becaa7a75b5fd8 Mon Sep 17 00:00:00 2001 From: emielvdveen Date: Sun, 3 Dec 2023 18:23:59 +0100 Subject: [PATCH 09/14] Reset signal loggin to debug --- core/frameworks/signal/_public.ex | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/frameworks/signal/_public.ex b/core/frameworks/signal/_public.ex index b319fe5a4..27c76bf81 100644 --- a/core/frameworks/signal/_public.ex +++ b/core/frameworks/signal/_public.ex @@ -20,7 +20,7 @@ defmodule Frameworks.Signal.Public do ] def dispatch(signal, message) do - Logger.warn("SIGNAL: " <> pretty_print(signal) <> " => " <> pretty_print(Map.keys(message))) + Logger.debug("SIGNAL: " <> pretty_print(signal) <> " => " <> pretty_print(Map.keys(message))) for handler <- signal_handlers() do handler.intercept(signal, message) From fa99fa0ae238c22286f0598499968b128805bc14 Mon Sep 17 00:00:00 2001 From: emielvdveen Date: Sun, 3 Dec 2023 18:34:47 +0100 Subject: [PATCH 10/14] Replaced title of Assignment content page with Template name --- core/priv/gettext/en/LC_MESSAGES/eyra-enums.po | 8 ++++---- core/priv/gettext/eyra-enums.pot | 8 ++++---- core/priv/gettext/nl/LC_MESSAGES/eyra-enums.po | 8 ++++---- core/systems/assignment/content_page_builder.ex | 13 +++++++++++-- core/systems/assignment/templates.ex | 2 +- 5 files changed, 24 insertions(+), 15 deletions(-) diff --git a/core/priv/gettext/en/LC_MESSAGES/eyra-enums.po b/core/priv/gettext/en/LC_MESSAGES/eyra-enums.po index e122b6f82..ac2f0b183 100644 --- a/core/priv/gettext/en/LC_MESSAGES/eyra-enums.po +++ b/core/priv/gettext/en/LC_MESSAGES/eyra-enums.po @@ -330,10 +330,6 @@ msgstr "Benchmark" msgid "project_item_templates.data_donation" msgstr "Data Donation" -#, elixir-autogen, elixir-format, fuzzy -msgid "project_item_templates.empty" -msgstr "Empty" - #, elixir-autogen, elixir-format, fuzzy msgid "templates.data_donation" msgstr "Data donation" @@ -373,3 +369,7 @@ msgstr "I&O Research" #, elixir-autogen, elixir-format msgid "external_panel_ids.liss" msgstr "LISS" + +#, elixir-autogen, elixir-format, fuzzy +msgid "templates.benchmark" +msgstr "Benchmark" diff --git a/core/priv/gettext/eyra-enums.pot b/core/priv/gettext/eyra-enums.pot index 3315d92b5..6306733c6 100644 --- a/core/priv/gettext/eyra-enums.pot +++ b/core/priv/gettext/eyra-enums.pot @@ -330,10 +330,6 @@ msgstr "" msgid "project_item_templates.data_donation" msgstr "" -#, elixir-autogen, elixir-format -msgid "project_item_templates.empty" -msgstr "" - #, elixir-autogen, elixir-format msgid "templates.data_donation" msgstr "" @@ -373,3 +369,7 @@ msgstr "" #, elixir-autogen, elixir-format msgid "external_panel_ids.liss" msgstr "" + +#, elixir-autogen, elixir-format +msgid "templates.benchmark" +msgstr "" diff --git a/core/priv/gettext/nl/LC_MESSAGES/eyra-enums.po b/core/priv/gettext/nl/LC_MESSAGES/eyra-enums.po index 2051b4cbe..d364496a2 100644 --- a/core/priv/gettext/nl/LC_MESSAGES/eyra-enums.po +++ b/core/priv/gettext/nl/LC_MESSAGES/eyra-enums.po @@ -330,10 +330,6 @@ msgstr "Benchmark" msgid "project_item_templates.data_donation" msgstr "Data Donatie Studie" -#, elixir-autogen, elixir-format, fuzzy -msgid "project_item_templates.empty" -msgstr "Leeg" - #, elixir-autogen, elixir-format, fuzzy msgid "templates.data_donation" msgstr "Data donatie" @@ -373,3 +369,7 @@ msgstr "I&O Research" #, elixir-autogen, elixir-format msgid "external_panel_ids.liss" msgstr "LISS" + +#, elixir-autogen, elixir-format, fuzzy +msgid "templates.benchmark" +msgstr "Benchmark" diff --git a/core/systems/assignment/content_page_builder.ex b/core/systems/assignment/content_page_builder.ex index ed6fe8151..f32417c4f 100644 --- a/core/systems/assignment/content_page_builder.ex +++ b/core/systems/assignment/content_page_builder.ex @@ -11,9 +11,10 @@ defmodule Systems.Assignment.ContentPageBuilder do } def view_model( - %{id: id} = assignment, + %{id: id, special: special} = assignment, assigns ) do + title = get_title(special) show_errors = show_errors(assignment, assigns) tabs = create_tabs(assignment, show_errors, assigns) action_map = action_map(assignment) @@ -21,13 +22,21 @@ defmodule Systems.Assignment.ContentPageBuilder do %{ id: id, - title: dgettext("eyra-assignment", "content.title"), + title: title, tabs: tabs, actions: actions, show_errors: show_errors } end + defp get_title(nil) do + dgettext("eyra-assignment", "content.title") + end + + defp get_title(special) do + Assignment.Templates.translate(special) + end + defp show_errors(_, _) do # concept? = status == :concept # publish_clicked or not concept? diff --git a/core/systems/assignment/templates.ex b/core/systems/assignment/templates.ex index e6a611439..928790988 100644 --- a/core/systems/assignment/templates.ex +++ b/core/systems/assignment/templates.ex @@ -3,5 +3,5 @@ defmodule Systems.Assignment.Templates do Defines different templates used by Systems.Assignment.Assembly to initialize specials. """ use Core.Enums.Base, - {:templates, [:online, :lab, :data_donation]} + {:templates, [:online, :lab, :data_donation, :benchmark]} end From 93a4bcd9b65bca8770d7fe46c8fac79ab93abd64 Mon Sep 17 00:00:00 2001 From: emielvdveen Date: Mon, 4 Dec 2023 00:02:22 +0100 Subject: [PATCH 11/14] Added new default Unsplash image --- core/lib/core/image_helpers.ex | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/lib/core/image_helpers.ex b/core/lib/core/image_helpers.ex index 92db49191..d26c7cc4a 100644 --- a/core/lib/core/image_helpers.ex +++ b/core/lib/core/image_helpers.ex @@ -1,5 +1,5 @@ defmodule Core.ImageHelpers do - @default_image_id "raw_url=https%3A%2F%2Fimages.unsplash.com%2Fphoto-1447433819943-74a20887a81e%3Fixid%3DMnwyMTY0MzZ8MHwxfHNlYXJjaHw1OXx8c3BhY2V8ZW58MHx8fHwxNjIxNzU2Njc3%26ixlib%3Drb-1.2.1&username=nasa&name=NASA&blur_hash=LMG%40%7DcK%2CBX9Ec%5BxwoOrpEmtSi%7Ct6" + @default_image_id "raw_url=https%3A%2F%2Fimages.unsplash.com%2Fphoto-1620121478247-ec786b9be2fa%3Fixid%3DM3w1MzYyOTF8MHwxfGFsbHx8fHx8fHx8fDE3MDE2NDIxNDl8%26ixlib%3Drb-4.0.3&username=ricvath&name=Richard%20Horvath&blur_hash=La3n%7Dpo_kObWi%3DZ~a2bKVXWFa%2Aoe" def catalog, do: Application.get_env(:core, :image_catalog) From 9004288e70dea492c35b5eca8b07fde9cbe91e77 Mon Sep 17 00:00:00 2001 From: emielvdveen Date: Mon, 4 Dec 2023 11:33:36 +0100 Subject: [PATCH 12/14] Fixed blurry image on project cards --- core/systems/project/item_model.ex | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/systems/project/item_model.ex b/core/systems/project/item_model.ex index 4f784d745..09e3b730b 100644 --- a/core/systems/project/item_model.ex +++ b/core/systems/project/item_model.ex @@ -100,7 +100,7 @@ defmodule Systems.Project.ItemModel do {Project.NodePage, :item_card}, _user ) do - image_info = ImageHelpers.get_image_info(image_id, 120, 115) + image_info = ImageHelpers.get_image_info(image_id, 400, 200) tags = get_card_tags(assignment) path = ~p"/assignment/#{assignment_id}/content" From fab9c7289e91cb6aa7dc243646cf615658411ae1 Mon Sep 17 00:00:00 2001 From: emielvdveen Date: Mon, 4 Dec 2023 19:37:26 +0100 Subject: [PATCH 13/14] Integrated Yoda storage --- core/.dialyzer_ignore.exs | 5 +- core/frameworks/fabric.ex | 98 ++++++++------ core/frameworks/fabric/html.ex | 4 +- core/frameworks/fabric/live_component.ex | 4 +- core/frameworks/pixel/components/form.ex | 4 + .../pixel/components/radio_group.ex | 10 +- core/frameworks/signal/_public.ex | 4 +- .../gettext/en/LC_MESSAGES/eyra-storage.po | 2 +- core/systems/assignment/_public.ex | 8 ++ core/systems/assignment/connection_view.ex | 1 - .../assignment/connection_view_panel.ex | 1 + .../assignment/connector_popup_storage.ex | 70 +++++----- core/systems/assignment/connector_view.ex | 47 +++---- core/systems/assignment/crew_page.ex | 4 + core/systems/assignment/crew_work_view.ex | 6 +- core/systems/assignment/gdpr_form.ex | 4 +- .../assignment/onboarding_consent_view.ex | 2 +- core/systems/assignment/settings_view.ex | 8 +- core/systems/storage/endpoint_form.ex | 120 +++++++++--------- core/systems/storage/endpoint_form_helper.ex | 15 ++- core/systems/storage/yoda/backend.ex | 34 ++++- core/systems/storage/yoda/client.ex | 47 +++++++ core/systems/storage/yoda/endpoint_form.ex | 6 +- .../core_web/image_catalog_picker_test.exs | 2 +- core/test/frameworks/fabric/factories.ex | 2 +- core/test/frameworks/fabric/live_view_mock.ex | 4 +- core/test/frameworks/fabric/test.exs | 36 +++++- 27 files changed, 346 insertions(+), 202 deletions(-) create mode 100644 core/systems/storage/yoda/client.ex diff --git a/core/.dialyzer_ignore.exs b/core/.dialyzer_ignore.exs index b590c6889..b5e4dee2e 100644 --- a/core/.dialyzer_ignore.exs +++ b/core/.dialyzer_ignore.exs @@ -1,5 +1,8 @@ [ # https://github.com/phoenixframework/phoenix/issues/5437 {"systems/benchmark/export_controller.ex", :no_return}, - {"systems/benchmark/export_controller.ex", :call} + {"systems/benchmark/export_controller.ex", :call}, + # issue with HTTPPoison not supporting HTTP method :mkcol + {"systems/storage/yoda/client.ex", :no_return}, + {"systems/storage/yoda/client.ex", :call} ] diff --git a/core/frameworks/fabric.ex b/core/frameworks/fabric.ex index 5a0e42c57..2495ad93b 100644 --- a/core/frameworks/fabric.ex +++ b/core/frameworks/fabric.ex @@ -26,25 +26,25 @@ defmodule Fabric do Phoenix.Component.assign(assigns, element_id, element) end - def update_child(context, child_id) when is_atom(child_id) do - if exists?(context, child_id) do - compose_child(context, child_id) + def update_child(context, child_name) when is_atom(child_name) do + if exists?(context, child_name) do + compose_child(context, child_name) else context end end - def compose_child(%Phoenix.LiveView.Socket{assigns: assigns} = socket, child_id) - when is_atom(child_id) do - %Phoenix.LiveView.Socket{socket | assigns: compose_child(assigns, child_id)} + def compose_child(%Phoenix.LiveView.Socket{assigns: assigns} = socket, child_name) + when is_atom(child_name) do + %Phoenix.LiveView.Socket{socket | assigns: compose_child(assigns, child_name)} end - def compose_child(%{fabric: fabric} = assigns, child_id) when is_atom(child_id) do + def compose_child(%{fabric: fabric} = assigns, child_name) when is_atom(child_name) do fabric = - if child = prepare_child(fabric, child_id, compose(child_id, assigns)) do + if child = prepare_child(fabric, child_name, compose(child_name, assigns)) do add_child(fabric, child) else - remove_child(fabric, child_id) + remove_child(fabric, child_name) end Phoenix.Component.assign(assigns, fabric: fabric) @@ -59,29 +59,46 @@ defmodule Fabric do end end + # Child id + + def child_id(%Fabric.Model{self: self}, child_name), do: child_id(self, child_name) + + def child_id(%{pid: pid}, child_name) do + pid_string = + pid + |> :erlang.pid_to_list() + |> to_string() + |> then(&("PID" <> &1)) + + child_id(pid_string, child_name) + end + + def child_id(%{id: id}, child_name), do: child_id(id, child_name) + def child_id(context, child_name), do: "#{child_name}->#{context}" # Prepare - def prepare_child(context, child_id, %{module: module, params: params}) do - prepare_child(context, child_id, module, params) + def prepare_child(context, child_name, %{module: module, params: params}) do + prepare_child(context, child_name, module, params) end def prepare_child(_context, _child_id, _), do: nil def prepare_child( %Phoenix.LiveView.Socket{assigns: assigns}, - child_id, + child_name, module, params ) do - prepare_child(assigns, child_id, module, params) + prepare_child(assigns, child_name, module, params) end - def prepare_child(%{fabric: fabric}, child_id, module, params) do - prepare_child(fabric, child_id, module, params) + def prepare_child(%{fabric: fabric}, child_name, module, params) do + prepare_child(fabric, child_name, module, params) end - def prepare_child(%Fabric.Model{self: self}, child_id, module, params) do - child_ref = %Fabric.LiveComponent.RefModel{id: child_id, module: module} + def prepare_child(%Fabric.Model{self: self}, child_name, module, params) do + child_id = child_id(self, child_name) + child_ref = %Fabric.LiveComponent.RefModel{id: child_id, name: child_name, module: module} child_fabric = %Fabric.Model{parent: self, self: child_ref, children: nil} params = Map.put(params, :fabric, child_fabric) %Fabric.LiveComponent.Model{ref: child_ref, params: params} @@ -104,20 +121,20 @@ defmodule Fabric do # CRUD - def get_child(%Phoenix.LiveView.Socket{assigns: %{fabric: fabric}}, child_id) do - get_child(fabric, child_id) + def get_child(%Phoenix.LiveView.Socket{assigns: %{fabric: fabric}}, child_name) do + get_child(fabric, child_name) end - def get_child(%{fabric: fabric}, child_id) do - get_child(fabric, child_id) + def get_child(%{fabric: fabric}, child_name) do + get_child(fabric, child_name) end - def get_child(%Fabric.Model{children: children}, child_id) do - Enum.find(List.wrap(children), &(&1.ref.id == child_id)) + def get_child(%Fabric.Model{children: children}, child_name) do + Enum.find(List.wrap(children), &(&1.ref.name == child_name)) end - def exists?(context, child_id) do - get_child(context, child_id) != nil + def exists?(context, child_name) do + get_child(context, child_name) != nil end def new_fabric(%Phoenix.LiveView.Socket{} = socket) do @@ -159,16 +176,16 @@ defmodule Fabric do ) end - def hide_child(%Phoenix.LiveView.Socket{assigns: assigns} = socket, child_id) do - %Phoenix.LiveView.Socket{socket | assigns: hide_child(assigns, child_id)} + def hide_child(%Phoenix.LiveView.Socket{assigns: assigns} = socket, child_name) do + %Phoenix.LiveView.Socket{socket | assigns: hide_child(assigns, child_name)} end - def hide_child(%{fabric: fabric} = assigns, child_id) do - Phoenix.Component.assign(assigns, fabric: remove_child(fabric, child_id)) + def hide_child(%{fabric: fabric} = assigns, child_name) do + Phoenix.Component.assign(assigns, fabric: remove_child(fabric, child_name)) end - def show_popup(context, child_id) when is_atom(child_id) do - child = get_child(context, child_id) + def show_popup(context, child_name) when is_atom(child_name) do + child = get_child(context, child_name) show_popup(context, child) end @@ -184,13 +201,13 @@ defmodule Fabric do Phoenix.Component.assign(assigns, fabric: add_child(fabric, child)) end - def hide_popup(%Phoenix.LiveView.Socket{assigns: assigns} = socket, child_id) do - %Phoenix.LiveView.Socket{socket | assigns: hide_popup(assigns, child_id)} + def hide_popup(%Phoenix.LiveView.Socket{assigns: assigns} = socket, child_name) do + %Phoenix.LiveView.Socket{socket | assigns: hide_popup(assigns, child_name)} end - def hide_popup(%{fabric: fabric} = assigns, child_id) do + def hide_popup(%{fabric: fabric} = assigns, child_name) do send_event(fabric, :root, "hide_popup") - Phoenix.Component.assign(assigns, fabric: remove_child(fabric, child_id)) + Phoenix.Component.assign(assigns, fabric: remove_child(fabric, child_name)) end def add_child(%Fabric.Model{children: nil} = fabric, %Fabric.LiveComponent.Model{} = child) do @@ -210,8 +227,11 @@ defmodule Fabric do def remove_child(%Fabric.Model{} = fabric, nil), do: fabric - def remove_child(%Fabric.Model{children: children} = fabric, child_id) do - %Fabric.Model{fabric | children: Enum.filter(List.wrap(children), &(&1.ref.id != child_id))} + def remove_child(%Fabric.Model{children: children} = fabric, child_name) do + %Fabric.Model{ + fabric + | children: Enum.filter(List.wrap(children), &(&1.ref.name != child_name)) + } end # Flow @@ -266,8 +286,8 @@ defmodule Fabric do raise "Sending event '#{name}' to empty flow" end - def send_event(%Fabric.Model{} = fabric, child_id, name, payload) do - if child = get_child(fabric, child_id) do + def send_event(%Fabric.Model{} = fabric, child_name, name, payload) do + if child = get_child(fabric, child_name) do send_event(child.ref, %{name: name, payload: payload}) end end diff --git a/core/frameworks/fabric/html.ex b/core/frameworks/fabric/html.ex index f1da296e4..28fa5b7d8 100644 --- a/core/frameworks/fabric/html.ex +++ b/core/frameworks/fabric/html.ex @@ -1,14 +1,14 @@ defmodule Fabric.Html do use Phoenix.Component - attr(:id, :any, required: true) + attr(:name, :any, required: true) attr(:fabric, :map, required: true) slot(:header) slot(:footer) def child(assigns) do ~H""" - <%= if child = Fabric.get_child(@fabric, @id) do %> + <%= if child = Fabric.get_child(@fabric, @name) do %> <%= render_slot(@header) %> <.live_child {Map.from_struct(child)} /> <%= render_slot(@footer) %> diff --git a/core/frameworks/fabric/live_component.ex b/core/frameworks/fabric/live_component.ex index b6bca98cc..b50d9676d 100644 --- a/core/frameworks/fabric/live_component.ex +++ b/core/frameworks/fabric/live_component.ex @@ -1,7 +1,7 @@ defmodule Fabric.LiveComponent do defmodule RefModel do - @type t :: %__MODULE__{id: atom() | binary(), module: atom()} - defstruct [:id, :module] + @type t :: %__MODULE__{id: atom() | binary(), name: atom() | binary(), module: atom()} + defstruct [:id, :name, :module] end defmodule Model do diff --git a/core/frameworks/pixel/components/form.ex b/core/frameworks/pixel/components/form.ex index 5bf95badb..9161322b5 100644 --- a/core/frameworks/pixel/components/form.ex +++ b/core/frameworks/pixel/components/form.ex @@ -244,6 +244,7 @@ defmodule Frameworks.Pixel.Form do attr(:label_color, :string, default: "text-grey1") attr(:background, :atom, default: :light) attr(:reserve_error_space, :boolean, default: true) + attr(:debounce, :string, default: "1000") def url_input(assigns) do ~H""" @@ -255,6 +256,7 @@ defmodule Frameworks.Pixel.Form do label_color={@label_color} background={@background} reserve_error_space={@reserve_error_space} + debounce={@debounce} type="url" /> """ @@ -266,6 +268,7 @@ defmodule Frameworks.Pixel.Form do attr(:label_color, :string, default: "text-grey1") attr(:background, :atom, default: :light) attr(:reserve_error_space, :boolean, default: true) + attr(:debounce, :string, default: "1000") def password_input(assigns) do ~H""" @@ -276,6 +279,7 @@ defmodule Frameworks.Pixel.Form do label_color={@label_color} background={@background} reserve_error_space={@reserve_error_space} + debounce={@debounce} type="password" /> """ diff --git a/core/frameworks/pixel/components/radio_group.ex b/core/frameworks/pixel/components/radio_group.ex index d04e6efea..1b53d9f9e 100644 --- a/core/frameworks/pixel/components/radio_group.ex +++ b/core/frameworks/pixel/components/radio_group.ex @@ -4,8 +4,14 @@ defmodule Frameworks.Pixel.RadioGroup do @impl true def update(%{items: items}, socket) do - active_item = items |> Enum.find(& &1.active) - form = to_form(%{"radio-group" => "#{active_item.id}"}) + value = + if active_item = items |> Enum.find(& &1.active) do + "#{active_item.id}" + else + "" + end + + form = to_form(%{"radio-group" => value}) { :ok, diff --git a/core/frameworks/signal/_public.ex b/core/frameworks/signal/_public.ex index 27c76bf81..b1bc5dd1e 100644 --- a/core/frameworks/signal/_public.ex +++ b/core/frameworks/signal/_public.ex @@ -20,7 +20,7 @@ defmodule Frameworks.Signal.Public do ] def dispatch(signal, message) do - Logger.debug("SIGNAL: " <> pretty_print(signal) <> " => " <> pretty_print(Map.keys(message))) + Logger.warn("SIGNAL: " <> pretty_print(signal) <> " => " <> pretty_print(Map.keys(message))) for handler <- signal_handlers() do handler.intercept(signal, message) @@ -42,7 +42,7 @@ defmodule Frameworks.Signal.Public do def multi_dispatch(multi, signal, message \\ %{}) when is_map(message) do Ecto.Multi.run(multi, :dispatch_signal, fn _, updates -> :ok = dispatch(signal, Map.merge(updates, message)) - {:ok, nil} + {:ok, message} end) end diff --git a/core/priv/gettext/en/LC_MESSAGES/eyra-storage.po b/core/priv/gettext/en/LC_MESSAGES/eyra-storage.po index c7758c9e8..5f786a069 100644 --- a/core/priv/gettext/en/LC_MESSAGES/eyra-storage.po +++ b/core/priv/gettext/en/LC_MESSAGES/eyra-storage.po @@ -53,7 +53,7 @@ msgstr "Password" #, elixir-autogen, elixir-format msgid "yoda.url.label" -msgstr "Portal" +msgstr "Folder url" #, elixir-autogen, elixir-format msgid "yoda.user.label" diff --git a/core/systems/assignment/_public.ex b/core/systems/assignment/_public.ex index 5c7d8115a..5584a1678 100644 --- a/core/systems/assignment/_public.ex +++ b/core/systems/assignment/_public.ex @@ -247,6 +247,14 @@ defmodule Systems.Assignment.Public do Core.Persister.save(assignment, changeset) end + def update_storage_endpoint(assignment, storage_endpoint) do + changeset = + Assignment.Model.changeset(assignment, %{}) + |> Ecto.Changeset.put_assoc(:storage_endpoint, storage_endpoint) + + Core.Persister.save(assignment, changeset) + end + def is_owner?(assignment, user) do Core.Authorization.user_has_role?(user, assignment, :owner) end diff --git a/core/systems/assignment/connection_view.ex b/core/systems/assignment/connection_view.ex index db9528f24..3e4891994 100644 --- a/core/systems/assignment/connection_view.ex +++ b/core/systems/assignment/connection_view.ex @@ -112,7 +112,6 @@ defmodule Systems.Assignment.ConnectionView do <% end %>
- <.spacing value="S" /> <.live_component {@special_view} />
diff --git a/core/systems/assignment/connection_view_panel.ex b/core/systems/assignment/connection_view_panel.ex index 27176856a..ed0ccb4ca 100644 --- a/core/systems/assignment/connection_view_panel.ex +++ b/core/systems/assignment/connection_view_panel.ex @@ -74,6 +74,7 @@ defmodule Systems.Assignment.ConnectionViewPanel do def render(assigns) do ~H"""
+ <.spacing value="S" /> <%= if @annotation do %> <% end %> diff --git a/core/systems/assignment/connector_popup_storage.ex b/core/systems/assignment/connector_popup_storage.ex index 62909c467..8b7257b4f 100644 --- a/core/systems/assignment/connector_popup_storage.ex +++ b/core/systems/assignment/connector_popup_storage.ex @@ -5,11 +5,10 @@ defmodule Systems.Assignment.ConnectorPopupStorage do import CoreWeb.UI.Dialog alias Systems.{ + Assignment, Storage } - @endpoint_form_key :endpoint_form - @impl true def update(%{id: id, entity: assignment}, socket) do { @@ -23,7 +22,7 @@ defmodule Systems.Assignment.ConnectorPopupStorage do |> update_text() |> update_buttons() |> update_storage_endpoint() - |> update_storage_endpoint_form() + |> compose_child(:storage_endpoint_form) } end @@ -62,70 +61,65 @@ defmodule Systems.Assignment.ConnectorPopupStorage do assign(socket, storage_endpoint: storage_endpoint) end - defp update_storage_endpoint_form(%{assigns: %{storage_endpoint: storage_endpoint}} = socket) do - child = - prepare_child(socket, @endpoint_form_key, Storage.EndpointForm, %{ + @impl true + def compose(:storage_endpoint_form, %{storage_endpoint: storage_endpoint}) do + %{ + module: Storage.EndpointForm, + params: %{ endpoint: storage_endpoint - }) - - show_child(socket, child) + } + } end @impl true - def handle_event("connect_storage", _payload, socket) do + def handle_event( + "update", + %{source: %{name: :storage_endpoint_form}, changeset: changeset}, + socket + ) do { :noreply, - socket |> commit_form() + socket |> assign(endpoint_changeset: changeset) } end @impl true - def handle_event("cancel", _payload, socket) do - { - :noreply, - socket |> cancel_popup() - } + def handle_event("connect_storage", _payload, socket) do + {:noreply, socket |> connect()} end @impl true - def handle_event("update", %{source: %{id: @endpoint_form_key}, changeset: changeset}, socket) do - { - :noreply, - socket |> assign(endpoint_changeset: changeset) - } + def handle_event("cancel", _payload, socket) do + {:noreply, socket |> cancel_popup()} + end + + defp cancel_popup(socket) do + socket |> send_event(:parent, "cancel") end - defp commit_form(%{assigns: %{endpoint_changeset: nil}} = socket) do + defp connect(%{assigns: %{endpoint_changeset: nil}} = socket) do socket end - defp commit_form(%{assigns: %{endpoint_changeset: endpoint_changeset}} = socket) do - case Ecto.Changeset.apply_action(endpoint_changeset, :update) do - {:ok, endpoint} -> + defp connect(%{assigns: %{endpoint_changeset: endpoint_changeset, entity: entity}} = socket) do + case Assignment.Public.update_storage_endpoint(entity, endpoint_changeset) do + {:ok, assignment} -> socket - |> assign(endpoint: endpoint) - |> finish() + |> assign(entity: assignment) + |> send_event(:parent, "finish", %{connection: %{endpoint: assignment.storage_endpoint}}) - {:error, _} -> + {:error, _changeset} -> socket - |> send_event(@endpoint_form_key, "show_errors") + |> send_event(:storage_endpoint_form, "show_errors") end end - defp cancel_popup(socket) do - socket |> send_event(:parent, "cancel") - end - - defp finish(%{assigns: %{endpoint: endpoint}} = socket) do - socket |> send_event(:parent, "finish", %{connection: endpoint}) - end - @impl true def render(assigns) do ~H"""
<.dialog {%{title: @title, text: @text, buttons: @buttons}}> - <.child id={:endpoint_form} fabric={@fabric}/> + <.child name={:storage_endpoint_form} fabric={@fabric}/>
""" diff --git a/core/systems/assignment/connector_view.ex b/core/systems/assignment/connector_view.ex index 2a984b7dc..c41fec2e3 100644 --- a/core/systems/assignment/connector_view.ex +++ b/core/systems/assignment/connector_view.ex @@ -28,7 +28,7 @@ defmodule Systems.Assignment.ConnectorView do uri_origin: uri_origin ) |> update_connect_button() - |> update_connection_view() + |> compose_child(:connection_view) } end @@ -41,30 +41,27 @@ defmodule Systems.Assignment.ConnectorView do assign(socket, connect_button: connect_button) end - defp update_connection_view(%{assigns: %{connection: nil}} = socket) do - assign(socket, connection_view: nil) + @impl true + def compose(:connection_view, %{connection: nil}) do + nil end - defp update_connection_view( - %{ - assigns: %{ - id: id, - type: type, - connection: connection, - assignment: assignment, - uri_origin: uri_origin - } - } = socket - ) do - child = - prepare_child(socket, "#{id}_connection_view", Assignment.ConnectionView, %{ + @impl true + def compose(:connection_view, %{ + type: type, + connection: connection, + assignment: assignment, + uri_origin: uri_origin + }) do + %{ + module: Assignment.ConnectionView, + params: %{ assignment: assignment, connection: connection, type: type, uri_origin: uri_origin - }) - - show_child(socket, child) + } + } end @impl true @@ -98,13 +95,17 @@ defmodule Systems.Assignment.ConnectorView do end @impl true - def handle_event("finish", %{source: %{id: :connector_popup}, connection: _connection}, socket) do + def handle_event( + "finish", + %{source: %{name: :connector_popup}, connection: _connection}, + socket + ) do hide_popup(socket, :connector_popup) {:noreply, socket} end @impl true - def handle_event("cancel", %{source: %{id: :connector_popup}}, socket) do + def handle_event("cancel", %{source: %{name: :connector_popup}}, socket) do hide_popup(socket, :connector_popup) {:noreply, socket} end @@ -113,8 +114,8 @@ defmodule Systems.Assignment.ConnectorView do def render(assigns) do ~H"""
- <%= if get_child(@fabric, "#{@id}_connection_view") do %> - <.child id={"#{@id}_connection_view"} fabric={@fabric}/> + <%= if get_child(@fabric, :connection_view) do %> + <.stack fabric={@fabric}/> <% else %> <.wrap> diff --git a/core/systems/assignment/crew_page.ex b/core/systems/assignment/crew_page.ex index 898cca247..f88fa0d6a 100644 --- a/core/systems/assignment/crew_page.ex +++ b/core/systems/assignment/crew_page.ex @@ -68,6 +68,10 @@ defmodule Systems.Assignment.CrewPage do |> assign(image_info: image_info) end + defp update_image_info(socket) do + socket + end + defp update_panel_info(socket, %{"panel_info" => panel_info}) do assign(socket, panel_info: panel_info) end diff --git a/core/systems/assignment/crew_work_view.ex b/core/systems/assignment/crew_work_view.ex index 62f7590dc..3c6205c87 100644 --- a/core/systems/assignment/crew_work_view.ex +++ b/core/systems/assignment/crew_work_view.ex @@ -191,17 +191,17 @@ defmodule Systems.Assignment.CrewWorkView do ~H"""
<%= if exists?(@fabric, :tool_ref_view) do %> - <.child id={:tool_ref_view} fabric={@fabric} /> + <.child name={:tool_ref_view} fabric={@fabric} /> <% else %> <%= if exists?(@fabric, :work_list_view) do %>
- <.child id={:work_list_view} fabric={@fabric} /> + <.child name={:work_list_view} fabric={@fabric} />
<% end %>
- <.child id={:start_view} fabric={@fabric} /> + <.child name={:start_view} fabric={@fabric} />
<% end %>
diff --git a/core/systems/assignment/gdpr_form.ex b/core/systems/assignment/gdpr_form.ex index 317698ad2..a5300f62a 100644 --- a/core/systems/assignment/gdpr_form.ex +++ b/core/systems/assignment/gdpr_form.ex @@ -85,9 +85,9 @@ defmodule Systems.Assignment.GdprForm do def render(assigns) do ~H"""
- <.child id={:switch} fabric={@fabric} /> + <.child name={:switch} fabric={@fabric} /> <.spacing value="S" /> - <.child id={:consent_revision_form} fabric={@fabric} /> + <.child name={:consent_revision_form} fabric={@fabric} />
""" end diff --git a/core/systems/assignment/onboarding_consent_view.ex b/core/systems/assignment/onboarding_consent_view.ex index 963c4bece..f8577a969 100644 --- a/core/systems/assignment/onboarding_consent_view.ex +++ b/core/systems/assignment/onboarding_consent_view.ex @@ -42,7 +42,7 @@ defmodule Systems.Assignment.OnboardingConsentView do <%= dgettext("eyra-assignment", "onboarding.consent.title") %> - <.child id={:clickwrap_view} fabric={@fabric} /> + <.child name={:clickwrap_view} fabric={@fabric} />
""" diff --git a/core/systems/assignment/settings_view.ex b/core/systems/assignment/settings_view.ex index 1cc27325b..7896c8b92 100644 --- a/core/systems/assignment/settings_view.ex +++ b/core/systems/assignment/settings_view.ex @@ -96,13 +96,13 @@ defmodule Systems.Assignment.SettingsView do <%= dgettext("eyra-assignment", "settings.title") %> <.spacing value="L" /> - <.child id={:info} fabric={@fabric} > + <.child name={:info} fabric={@fabric} > <:footer> <.spacing value="L" /> - <.child id={:consent} fabric={@fabric} > + <.child name={:consent} fabric={@fabric} > <:header> <%= dgettext("eyra-assignment", "settings.consent.title") %> <%= dgettext("eyra-assignment", "settings.consent.body") %> @@ -113,7 +113,7 @@ defmodule Systems.Assignment.SettingsView do - <.child id={:panel_connector} fabric={@fabric}> + <.child name={:panel_connector} fabric={@fabric}> <:header> <%= dgettext("eyra-assignment", "settings.panel.title") %> <%= dgettext("eyra-assignment", "settings.panel.body") %> @@ -124,7 +124,7 @@ defmodule Systems.Assignment.SettingsView do - <.child id={:storage_connector} fabric={@fabric}> + <.child name={:storage_connector} fabric={@fabric}> <:header> <%= dgettext("eyra-assignment", "settings.data_storage.title") %> <%= dgettext("eyra-assignment", "settings.data_storage.body") %> diff --git a/core/systems/storage/endpoint_form.ex b/core/systems/storage/endpoint_form.ex index 7ae59a8bd..1189b9e63 100644 --- a/core/systems/storage/endpoint_form.ex +++ b/core/systems/storage/endpoint_form.ex @@ -3,35 +3,12 @@ defmodule Systems.Storage.EndpointForm do use Fabric.LiveComponent alias Frameworks.Concept - alias Frameworks.Pixel.Selector + alias Frameworks.Pixel alias Systems.{ Storage } - @special_form_key :storage_endpoint_special_form - - # Handle Selector Update - @impl true - def update( - %{active_item_id: special_type, selector_id: :special_type_selector}, - socket - ) do - special = Storage.Private.build_special(special_type) - - { - :ok, - socket - |> assign( - special_type: special_type, - special_changeset: nil, - special: special - ) - |> update_special_form() - } - end - - # Handle initial update @impl true def update( %{id: id, endpoint: endpoint}, @@ -45,54 +22,60 @@ defmodule Systems.Storage.EndpointForm do endpoint: endpoint ) |> update_special_type() - |> update_type_selector() |> update_special() - |> update_special_form() + |> update_special_title() + |> compose_child(:type_selector) + |> compose_child(:special_form) } end defp update_special_type(%{assigns: %{endpoint: endpoint}} = socket) do - special_type = Storage.EndpointModel.special_field_id(endpoint) + special_type = Storage.EndpointModel.special_field(endpoint) assign(socket, special_type: special_type) end - defp update_type_selector(%{assigns: %{id: id, special_type: special_type}} = socket) do - items = Storage.ServiceIds.labels(special_type, Storage.Private.allowed_service_ids()) - - type_selector = %{ - module: Selector, - id: :special_type_selector, - grid_options: "flex flex-row gap-4", - items: items, - type: :radio, - parent: %{type: __MODULE__, id: id} - } - - assign(socket, type_selector: type_selector) - end - defp update_special(%{assigns: %{endpoint: endpoint}} = socket) do special = Storage.EndpointModel.special(endpoint) assign(socket, special: special) end - defp update_special_form(%{assigns: %{special_type: nil}} = socket) do + defp update_special_title(%{assigns: %{special_type: nil}} = socket) do socket - |> assign(@special_form_key, nil) - |> assign(special_form_title: nil) + |> assign(special_title: nil) end - defp update_special_form(%{assigns: %{special_type: special_type, special: special}} = socket) do - special_form_title = Storage.ServiceIds.translate(special_type) - - child = - prepare_child(socket, @special_form_key, Concept.ContentModel.form(special), %{ - model: special - }) + defp update_special_title(%{assigns: %{special_type: special_type}} = socket) do + special_title = Storage.ServiceIds.translate(special_type) socket - |> replace_child(child) - |> assign(special_form_title: special_form_title) + |> assign(special_title: special_title) + end + + @impl true + def compose(:type_selector, %{special_type: special_type}) do + items = Storage.ServiceIds.labels(special_type, Storage.Private.allowed_service_ids()) + + %{ + module: Pixel.RadioGroup, + params: %{ + items: items + } + } + end + + @impl true + def compose(:special_form, %{special: nil}) do + nil + end + + @impl true + def compose(:special_form, %{special: special}) do + %{ + module: Concept.ContentModel.form(special), + params: %{ + model: special + } + } end defp update_changeset(%{assigns: %{special_changeset: nil}} = socket) do @@ -115,7 +98,24 @@ defmodule Systems.Storage.EndpointForm do end @impl true - def handle_event("update", %{source: %{id: @special_form_key}, changeset: changeset}, socket) do + def handle_event("update", %{source: %{name: :type_selector}, status: special_type}, socket) do + special = Storage.Private.build_special(special_type) + + { + :noreply, + socket + |> assign( + special_type: special_type, + special_changeset: nil, + special: special + ) + |> update_special_title() + |> compose_child(:special_form) + } + end + + @impl true + def handle_event("update", %{source: %{name: :special_form}, changeset: changeset}, socket) do { :noreply, socket @@ -126,7 +126,7 @@ defmodule Systems.Storage.EndpointForm do @impl true def handle_event("show_errors", _payload, socket) do - {:noreply, socket |> send_event(@special_form_key, "show_errors")} + {:noreply, socket |> send_event(:special_form, "show_errors")} end @impl true @@ -136,13 +136,13 @@ defmodule Systems.Storage.EndpointForm do <%= dgettext("eyra-storage", "endpoint_form.type.label") %> <.spacing value="XS" />
- <.live_component {@type_selector} /> + <.child name={:type_selector} fabric={@fabric} />
- <%= if get_child(@fabric, :storage_endpoint_special_form) do %> + <%= if get_child(@fabric, :special_form) do %> <.spacing value="L" /> - <%= @special_form_title %> + <%= @special_title %> <.spacing value="XS" /> - <.child id={:storage_endpoint_special_form} fabric={@fabric} /> + <.child name={:special_form} fabric={@fabric} /> <% end %>
""" diff --git a/core/systems/storage/endpoint_form_helper.ex b/core/systems/storage/endpoint_form_helper.ex index fc88da0a1..b00a693bb 100644 --- a/core/systems/storage/endpoint_form_helper.ex +++ b/core/systems/storage/endpoint_form_helper.ex @@ -36,15 +36,24 @@ defmodule Systems.Storage.EndpointForm.Helper do end @impl true - def handle_event("show_errors", _payload, socket) do - {:noreply, socket |> assign(show_errors: true)} + def handle_event("show_errors", _payload, %{assigns: %{changeset: changeset}} = socket) do + { + :noreply, + socket |> assign(show_errors: true) + } end defp update_changeset(%{assigns: %{id: id, model: model, attrs: attrs}} = socket) do changeset = Model.changeset(model, attrs) |> Model.validate() - |> Map.put(:action, :update) + + changeset = + if model.id do + Map.put(changeset, :action, :update) + else + Map.put(changeset, :action, :insert) + end socket |> assign(:changeset, changeset) diff --git a/core/systems/storage/yoda/backend.ex b/core/systems/storage/yoda/backend.ex index 4df7a7a45..37ec9c0b5 100644 --- a/core/systems/storage/yoda/backend.ex +++ b/core/systems/storage/yoda/backend.ex @@ -1,14 +1,38 @@ defmodule Systems.Storage.Yoda.Backend do @behaviour Systems.Storage.Backend + alias Systems.Storage.Yoda + require Logger def store( - _endpoint, - _panel_info, - _data, - _meta_data + %{ + "user" => username, + "password" => password, + "url" => yoda_url + } = _endpoint, + %{ + "participant" => participant + } = _panel_info, + data, + %{ + "key" => key + } = _meta_data ) do - Logger.warn("Yoda backend not implemented yet") + folder = "participant-#{participant}" + folder_url = url([yoda_url, folder]) + + file = "#{key}.json" + file_url = url([yoda_url, folder, file]) + + with {:ok, false} <- Yoda.Client.has_resource?(username, password, folder_url) do + {:ok, _} = Yoda.Client.create_folder(username, password, folder_url) + end + + {:ok, _} = Yoda.Client.upload_file(username, password, file_url, data) + end + + defp url(components) do + Enum.join(components, "/") end end diff --git a/core/systems/storage/yoda/client.ex b/core/systems/storage/yoda/client.ex new file mode 100644 index 000000000..fbdf4016a --- /dev/null +++ b/core/systems/storage/yoda/client.ex @@ -0,0 +1,47 @@ +defmodule Systems.Storage.Yoda.Client do + alias Frameworks.Utility.HTTPClient + require Logger + + def upload_file(username, password, file_url, body) do + headers = headers(username, password) + http_request(:put, file_url, body, headers) + end + + def create_folder(username, password, folder_url) do + headers = headers(username, password) + http_request(:mkcol, folder_url, "", headers) + end + + def has_resource?(username, password, resource_url) do + headers = headers(username, password) + + case http_request(:head, resource_url, "", headers) do + {:ok, %HTTPoison.Response{status_code: 200}} -> + {:ok, true} + + {:ok, %HTTPoison.Response{}} -> + {:ok, false} + + {:error, error} -> + {:error, "Request failed: #{inspect(error)}"} + end + end + + defp headers(username, password) do + [ + {"Content-type", "application/json"}, + {"Authorization", "Basic " <> Base.encode64("#{username}:#{password}")} + ] + end + + defp http_request(method, url, body, headers, options \\ []) do + case HTTPClient.request(method, url, body, headers, options) do + {:error, error} -> + Logger.error("Yoda request failed: #{inspect(error)}") + {:error, error} + + {:ok, response} -> + {:ok, response} + end + end +end diff --git a/core/systems/storage/yoda/endpoint_form.ex b/core/systems/storage/yoda/endpoint_form.ex index 6b18e501c..7b5251a67 100644 --- a/core/systems/storage/yoda/endpoint_form.ex +++ b/core/systems/storage/yoda/endpoint_form.ex @@ -6,9 +6,9 @@ defmodule Systems.Storage.Yoda.EndpointForm do ~H"""
<.form id={"#{@id}_yoda_endpoint_form"} :let={form} for={@changeset} phx-change="save" phx-target={@myself}> - <.text_input form={form} field={:url} label_text={dgettext("eyra-storage", "yoda.url.label")} /> - <.text_input form={form} field={:user} label_text={dgettext("eyra-storage", "yoda.user.label")} /> - <.password_input form={form} field={:password} label_text={dgettext("eyra-storage", "yoda.password.label")} /> + <.text_input form={form} field={:url} label_text={dgettext("eyra-storage", "yoda.url.label")} debounce="0" placeholder="https:////"/> + <.text_input form={form} field={:user} label_text={dgettext("eyra-storage", "yoda.user.label")} debounce="0" /> + <.password_input form={form} field={:password} label_text={dgettext("eyra-storage", "yoda.password.label")} debounce="0" />
""" diff --git a/core/test/core_web/image_catalog_picker_test.exs b/core/test/core_web/image_catalog_picker_test.exs index 8ad722acb..cdb1db6d9 100644 --- a/core/test/core_web/image_catalog_picker_test.exs +++ b/core/test/core_web/image_catalog_picker_test.exs @@ -29,7 +29,7 @@ defmodule CoreWeb.UI.ImageCatalogPicker.Test.View do def render(assigns) do ~H"""
- <.child id={:image_picker} fabric={@fabric} /> + <.child name={:image_picker} fabric={@fabric} />
""" end diff --git a/core/test/frameworks/fabric/factories.ex b/core/test/frameworks/fabric/factories.ex index 26804ea66..3a6e289b4 100644 --- a/core/test/frameworks/fabric/factories.ex +++ b/core/test/frameworks/fabric/factories.ex @@ -12,7 +12,7 @@ defmodule Fabric.Factories do end def create_child(id, module \\ Fabric.LiveComponentMock, params \\ %{}) do - ref = %Fabric.LiveComponent.RefModel{id: id, module: module} + ref = %Fabric.LiveComponent.RefModel{id: id, name: id, module: module} fabric = create_fabric(ref) params = Map.put(params, :fabric, fabric) %Fabric.LiveComponent.Model{ref: ref, params: params} diff --git a/core/test/frameworks/fabric/live_view_mock.ex b/core/test/frameworks/fabric/live_view_mock.ex index e085af4dd..6bee323be 100644 --- a/core/test/frameworks/fabric/live_view_mock.ex +++ b/core/test/frameworks/fabric/live_view_mock.ex @@ -24,8 +24,8 @@ defmodule Fabric.LiveViewMock do @impl true def render(assigns) do ~H""" - <.child id={:child_a} fabric={@fabric} /> - <.child id={:child_b} fabric={@fabric} /> + <.child name={:child_a} fabric={@fabric} /> + <.child name={:child_b} fabric={@fabric} /> """ end end diff --git a/core/test/frameworks/fabric/test.exs b/core/test/frameworks/fabric/test.exs index f38fdcbe6..619cd8e5e 100644 --- a/core/test/frameworks/fabric/test.exs +++ b/core/test/frameworks/fabric/test.exs @@ -35,7 +35,11 @@ defmodule Fabric.Test do |> Fabric.prepare_child(:child, Fabric.LiveComponentMock, %{}) assert %Fabric.LiveComponent.Model{ - ref: %Fabric.LiveComponent.RefModel{id: :child, module: Fabric.LiveComponentMock}, + ref: %Fabric.LiveComponent.RefModel{ + id: :child, + name: :child, + module: Fabric.LiveComponentMock + }, params: %{ fabric: %Fabric.Model{ parent: nil, @@ -55,7 +59,11 @@ defmodule Fabric.Test do child = Fabric.prepare_child(fabric, :child, Fabric.LiveComponentMock, %{}) assert %Fabric.LiveComponent.Model{ - ref: %Fabric.LiveComponent.RefModel{id: :child, module: Fabric.LiveComponentMock}, + ref: %Fabric.LiveComponent.RefModel{ + id: :child, + name: :child, + module: Fabric.LiveComponentMock + }, params: %{ fabric: %Fabric.Model{ parent: nil, @@ -81,7 +89,11 @@ defmodule Fabric.Test do |> Fabric.get_child(:child) assert %Fabric.LiveComponent.Model{ - ref: %Fabric.LiveComponent.RefModel{id: :child, module: Fabric.LiveComponentMock}, + ref: %Fabric.LiveComponent.RefModel{ + id: :child, + name: :child, + module: Fabric.LiveComponentMock + }, params: %{ fabric: %Fabric.Model{ parent: nil, @@ -101,7 +113,11 @@ defmodule Fabric.Test do |> Fabric.get_child(:child) assert %Fabric.LiveComponent.Model{ - ref: %Fabric.LiveComponent.RefModel{id: :child, module: Fabric.LiveComponentMock}, + ref: %Fabric.LiveComponent.RefModel{ + id: :child, + name: :child, + module: Fabric.LiveComponentMock + }, params: %{ fabric: %Fabric.Model{ parent: nil, @@ -311,11 +327,19 @@ defmodule Fabric.Test do params: %{ fabric: %Fabric.Model{ parent: nil, - self: %Fabric.LiveComponent.RefModel{id: :child, module: Fabric.LiveComponentMock}, + self: %Fabric.LiveComponent.RefModel{ + id: :child, + name: :child, + module: Fabric.LiveComponentMock + }, children: nil } }, - ref: %Fabric.LiveComponent.RefModel{id: :child, module: Fabric.LiveComponentMock} + ref: %Fabric.LiveComponent.RefModel{ + id: :child, + name: :child, + module: Fabric.LiveComponentMock + } } } } From bb0647508ff2dc56ac9ba32b2d4c07f545dc3210 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 4 Dec 2023 20:40:27 +0000 Subject: [PATCH 14/14] Bump docker/metadata-action from 5.2.0 to 5.3.0 Bumps [docker/metadata-action](https://github.com/docker/metadata-action) from 5.2.0 to 5.3.0. - [Release notes](https://github.com/docker/metadata-action/releases) - [Commits](https://github.com/docker/metadata-action/compare/e6428a5c4e294a61438ed7f43155db912025b6b3...31cebacef4805868f9ce9a0cb03ee36c32df2ac4) --- updated-dependencies: - dependency-name: docker/metadata-action dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- .github/workflows/docker_release.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/docker_release.yml b/.github/workflows/docker_release.yml index 4e494b045..272e8554f 100644 --- a/.github/workflows/docker_release.yml +++ b/.github/workflows/docker_release.yml @@ -37,7 +37,7 @@ jobs: - name: Extract metadata (tags, labels) for Docker id: meta - uses: docker/metadata-action@e6428a5c4e294a61438ed7f43155db912025b6b3 + uses: docker/metadata-action@31cebacef4805868f9ce9a0cb03ee36c32df2ac4 with: images: | ${{env.REGISTRY}}/eyra/${{github.event.inputs.bundle}}