diff --git a/core/bundles/next/lib/layouts/workspace/menu_builder.ex b/core/bundles/next/lib/layouts/workspace/menu_builder.ex index dd338bff5..def0c7b97 100644 --- a/core/bundles/next/lib/layouts/workspace/menu_builder.ex +++ b/core/bundles/next/lib/layouts/workspace/menu_builder.ex @@ -40,6 +40,6 @@ defmodule Next.Layouts.Workspace.MenuBuilder do def include_map(user), do: %{ console: Authorization.can_access?(user, Next.Console.Page), - projects: Systems.Admin.Public.admin?(user) + projects: Systems.Admin.Public.admin?(user) or user.researcher } end diff --git a/core/lib/core/factories.ex b/core/lib/core/factories.ex index 6f417dd9c..5f247163f 100644 --- a/core/lib/core/factories.ex +++ b/core/lib/core/factories.ex @@ -251,7 +251,15 @@ defmodule Core.Factories do end def build(:project_node) do - build(:project_node, %{name: Faker.Lorem.word(), project_path: [1, 2]}) + build(:project_node, %{name: Faker.Lorem.word(), project_path: []}) + end + + def build(:project_item) do + build(:project_item, %{name: Faker.Lorem.word(), project_path: []}) + end + + def build(:tool_ref) do + build(:tool_ref, %{}) end def build(:auth_node, %{} = attributes) do @@ -379,6 +387,34 @@ defmodule Core.Factories do |> struct!(attributes) end + def build(:project_item, %{} = attributes) do + {node, attributes} = Map.pop(attributes, :node, build(:project_node)) + {tool_ref, attributes} = Map.pop(attributes, :tool_ref, build(:tool_ref)) + + %Project.ItemModel{ + node: node, + tool_ref: tool_ref + } + |> struct!(attributes) + end + + def build(:tool_ref, %{} = attributes) do + {item, attributes} = Map.pop(attributes, :item, build(:project_item)) + {survey_tool, attributes} = Map.pop(attributes, :survey_tool, nil) + {lab_tool, attributes} = Map.pop(attributes, :lab_tool, nil) + {data_donation_tool, attributes} = Map.pop(attributes, :data_donation_tool, nil) + {benchmark_tool, attributes} = Map.pop(attributes, :benchmark_tool, nil) + + %Project.ToolRefModel{ + item: item, + survey_tool: survey_tool, + lab_tool: lab_tool, + data_donation_tool: data_donation_tool, + benchmark_tool: benchmark_tool + } + |> struct!(attributes) + end + def build(:assignment, %{} = attributes) do {auth_node, attributes} = Map.pop(attributes, :auth_node, build(:auth_node)) {budget, attributes} = Map.pop(attributes, :budget, build(:budget)) diff --git a/core/priv/gettext/en/LC_MESSAGES/eyra-enums.po b/core/priv/gettext/en/LC_MESSAGES/eyra-enums.po index afff9ba8d..28119c9cd 100644 --- a/core/priv/gettext/en/LC_MESSAGES/eyra-enums.po +++ b/core/priv/gettext/en/LC_MESSAGES/eyra-enums.po @@ -340,4 +340,4 @@ msgstr "Benchmark" #, elixir-autogen, elixir-format, fuzzy msgid "project_tools.data_donation" -msgstr "Data Donation Study" +msgstr "Data Donation" diff --git a/core/priv/gettext/en/LC_MESSAGES/eyra-project.po b/core/priv/gettext/en/LC_MESSAGES/eyra-project.po index cb8ee9e2b..42408ff3a 100644 --- a/core/priv/gettext/en/LC_MESSAGES/eyra-project.po +++ b/core/priv/gettext/en/LC_MESSAGES/eyra-project.po @@ -190,3 +190,7 @@ msgstr "Retract" #, elixir-autogen, elixir-format msgid "create.item.button.short" msgstr "" + +#, elixir-autogen, elixir-format, fuzzy +msgid "create.item.title" +msgstr "Select item type" diff --git a/core/priv/gettext/eyra-project.pot b/core/priv/gettext/eyra-project.pot index 06f26afc1..09d4237ef 100644 --- a/core/priv/gettext/eyra-project.pot +++ b/core/priv/gettext/eyra-project.pot @@ -190,3 +190,7 @@ msgstr "" #, elixir-autogen, elixir-format msgid "create.item.button.short" msgstr "" + +#, elixir-autogen, elixir-format +msgid "create.item.title" +msgstr "" diff --git a/core/priv/gettext/nl/LC_MESSAGES/eyra-enums.po b/core/priv/gettext/nl/LC_MESSAGES/eyra-enums.po index 75a753519..ff6c0e8bc 100644 --- a/core/priv/gettext/nl/LC_MESSAGES/eyra-enums.po +++ b/core/priv/gettext/nl/LC_MESSAGES/eyra-enums.po @@ -340,4 +340,4 @@ msgstr "Benchmark" #, elixir-autogen, elixir-format, fuzzy msgid "project_tools.data_donation" -msgstr "Data Donatie Studie" +msgstr "Data Donatie" diff --git a/core/priv/gettext/nl/LC_MESSAGES/eyra-project.po b/core/priv/gettext/nl/LC_MESSAGES/eyra-project.po index 07a565320..3d41e2200 100644 --- a/core/priv/gettext/nl/LC_MESSAGES/eyra-project.po +++ b/core/priv/gettext/nl/LC_MESSAGES/eyra-project.po @@ -190,3 +190,7 @@ msgstr "Terugtrekken" #, elixir-autogen, elixir-format msgid "create.item.button.short" msgstr "" + +#, elixir-autogen, elixir-format, fuzzy +msgid "create.item.title" +msgstr "Kies type item" diff --git a/core/priv/repo/seeds.exs b/core/priv/repo/seeds.exs index 6afff9d93..e9078d4e2 100644 --- a/core/priv/repo/seeds.exs +++ b/core/priv/repo/seeds.exs @@ -24,7 +24,7 @@ images = [ "raw_url=https%3A%2F%2Fimages.unsplash.com%2Fphoto-1515378791036-0648a3ef77b2%3Fixid%3DMnwyMTY0MzZ8MHwxfHNlYXJjaHw4fHx3b3JrfGVufDB8fHx8MTYyMTc3NjgwOQ%26ixlib%3Drb-1.2.1&username=christinhumephoto&name=Christin+Hume&blur_hash=LMF%3B%3Dw0LAJR%25~A9uT0nNRjxaW%3DIo" ] -data_donation_promotions = +_data_donation_promotions = Enum.map(images, fn image -> %{ title: Faker.Lorem.sentence(), @@ -52,15 +52,15 @@ data_donation_promotions = } end) -Enum.each(data_donation_promotions, fn promotion -> - data = %{ - script: File.read!(Path.join([:code.priv_dir(:core), "repo", "script.py"])), - subject_count: 400 - # promotion: promotion - } +# Enum.each(data_donation_promotions, fn promotion -> +# data = %{ +# script: File.read!(Path.join([:code.priv_dir(:core), "repo", "script.py"])), +# subject_count: 400 +# # promotion: promotion +# } - Core.Factories.insert!(:data_donation_tool, data) -end) +# Core.Factories.insert!(:data_donation_tool, data) +# end) # campaigns = # Enum.map(data_donation_tools, fn data_donation_tool -> diff --git a/core/systems/project/_assembly.ex b/core/systems/project/_assembly.ex index ba2d77388..cf73be93b 100644 --- a/core/systems/project/_assembly.ex +++ b/core/systems/project/_assembly.ex @@ -48,7 +48,8 @@ defmodule Systems.Project.Assembly do |> Repo.transaction() end - def create_item(%Project.NodeModel{id: node_id} = node, tool_special) do + def create_item(name, %Project.NodeModel{id: node_id} = node, tool_special) + when is_binary(name) do project = from(p in Project.Model, where: p.root_id == ^node_id, preload: [:auth_node]) |> Repo.one!() @@ -59,11 +60,12 @@ defmodule Systems.Project.Assembly do end) |> prepare_tool(tool_special) |> Multi.insert(:tool_ref, fn %{tool: tool} -> - Project.Public.create_tool_ref(tool_special, tool) + key = String.to_existing_atom("#{tool_special}_tool") + Project.Public.create_tool_ref(key, tool) end) |> Multi.insert(:item, fn %{tool_ref: tool_ref} -> Project.Public.create_item( - %{name: "Item", project_path: [project.id, node_id]}, + %{name: name, project_path: [project.id, node_id]}, node, tool_ref ) @@ -120,14 +122,14 @@ defmodule Systems.Project.Assembly do end) end - defp prepare_tool(multi, :data_donation_tool) do + defp prepare_tool(multi, :data_donation) do multi |> Multi.insert(:tool, fn %{auth_node: auth_node} -> DataDonation.Public.create(%{subject_count: 0, director: :project}, auth_node) end) end - defp prepare_tool(multi, :benchmark_tool) do + defp prepare_tool(multi, :benchmark) do multi |> Multi.insert(:tool, fn %{auth_node: auth_node} -> Benchmark.Public.create(%{title: "", director: :project}, auth_node) diff --git a/core/systems/project/create_item_popup.ex b/core/systems/project/create_item_popup.ex new file mode 100644 index 000000000..89d24870b --- /dev/null +++ b/core/systems/project/create_item_popup.ex @@ -0,0 +1,114 @@ +defmodule Systems.Project.CreateItemPopup do + use CoreWeb, :live_component + + alias Frameworks.Pixel.Selector + + alias Systems.{ + Project + } + + # Handle Tool Type Selector Update + @impl true + def update( + %{active_item_id: active_item_id, selector_id: :tool_selector}, + %{assigns: %{tool_labels: tool_labels}} = socket + ) do + %{id: selected_tool} = Enum.find(tool_labels, &(&1.id == active_item_id)) + + { + :ok, + socket + |> assign(selected_tool: selected_tool) + } + end + + # Initial Update + @impl true + def update(%{id: id, node: node, target: target}, socket) do + title = dgettext("eyra-project", "create.item.title") + + { + :ok, + socket + |> assign(id: id, node: node, target: target, title: title) + |> init_tools() + |> init_buttons() + } + end + + defp init_tools(socket) do + selected_tool = :empty + tool_labels = Project.Tools.labels(selected_tool) + socket |> assign(tool_labels: tool_labels, selected_tool: selected_tool) + end + + defp init_buttons(%{assigns: %{myself: myself}} = socket) do + socket + |> assign( + buttons: [ + %{ + action: %{type: :send, event: "proceed", target: myself}, + face: %{ + type: :primary, + label: dgettext("eyra-project", "create.proceed.button") + } + }, + %{ + action: %{type: :send, event: "cancel", target: myself}, + face: %{type: :label, label: dgettext("eyra-ui", "cancel.button")} + } + ] + ) + end + + @impl true + def handle_event( + "proceed", + _, + %{assigns: %{selected_tool: selected_tool}} = socket + ) do + create_item(socket, selected_tool) + + {:noreply, socket |> close()} + end + + @impl true + def handle_event("cancel", _, socket) do + {:noreply, socket |> close()} + end + + defp close(%{assigns: %{target: target}} = socket) do + update_target(target, %{module: __MODULE__, action: :close}) + socket + end + + defp create_item(%{assigns: %{node: node}}, tool) do + name = Project.Tools.translate(tool) + Project.Assembly.create_item(name, node, tool) + end + + @impl true + def render(assigns) do + ~H""" +
+ <%= @title %> + <.spacing value="S" /> + <.live_component + module={Selector} + id={:tool_selector} + items={@tool_labels} + type={:radio} + optional?={false} + parent={%{type: __MODULE__, id: @id}} + /> + + <.spacing value="M" /> +
+ <%= for button <- @buttons do %> + + <% end %> +
+
+ """ + end +end diff --git a/core/systems/project/create_project_popup.ex b/core/systems/project/create_project_popup.ex index a3afb54da..494d5e841 100644 --- a/core/systems/project/create_project_popup.ex +++ b/core/systems/project/create_project_popup.ex @@ -96,7 +96,7 @@ defmodule Systems.Project.CreatePopup do ~H"""
<%= @title %> - <.spacing value="XS" /> + <.spacing value="S" /> <.live_component module={Selector} id={:template_selector} diff --git a/core/systems/project/item_form.ex b/core/systems/project/item_form.ex new file mode 100644 index 000000000..d15e249c7 --- /dev/null +++ b/core/systems/project/item_form.ex @@ -0,0 +1,77 @@ +defmodule Systems.Project.ItemForm do + use CoreWeb.LiveForm + + alias Systems.{ + Project + } + + # Handle initial update + @impl true + def update( + %{id: id, entity: item, target: target}, + 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 + ) + } + 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("save", %{"item_model" => attrs}, %{assigns: %{entity: entity}} = socket) do + { + :noreply, + socket + |> save(entity, attrs) + } + end + + # Saving + + def save(socket, entity, attrs) do + changeset = Project.ItemModel.changeset(entity, attrs) + + socket + |> save(changeset) + 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")} /> + +
+ """ + end +end diff --git a/core/systems/project/item_model.ex b/core/systems/project/item_model.ex index 23310d9c6..508456745 100644 --- a/core/systems/project/item_model.ex +++ b/core/systems/project/item_model.ex @@ -63,6 +63,11 @@ defmodule Systems.Project.ItemModel do tags = get_card_tags(tool) path = ~p"/project/item/#{id}/content" + edit = %{ + action: %{type: :send, event: "edit", item: id}, + face: %{type: :label, label: "Edit", wrap: true} + } + delete = %{ action: %{type: :send, event: "delete", item: id}, face: %{type: :icon, icon: :delete} @@ -76,7 +81,7 @@ defmodule Systems.Project.ItemModel do title: name, tags: tags, info: ["#{subject_count} participants | 0 donations"], - left_actions: [], + left_actions: [edit], right_actions: [delete] } end @@ -102,6 +107,11 @@ defmodule Systems.Project.ItemModel do path = ~p"/project/item/#{id}/content" label = get_label(status) + edit = %{ + action: %{type: :send, event: "edit", item: id}, + face: %{type: :label, label: "Edit", wrap: true} + } + delete = %{ action: %{type: :send, event: "delete", item: id}, face: %{type: :icon, icon: :delete} @@ -127,7 +137,7 @@ defmodule Systems.Project.ItemModel do title: name, tags: tags, info: [info_line_1], - left_actions: [], + left_actions: [edit], right_actions: [delete] } end diff --git a/core/systems/project/node_page.ex b/core/systems/project/node_page.ex index 12f2db212..ae19212c9 100644 --- a/core/systems/project/node_page.ex +++ b/core/systems/project/node_page.ex @@ -17,7 +17,7 @@ defmodule Systems.Project.NodePage do { :ok, socket - |> assign(model: model) + |> assign(model: model, popup: nil) |> observe_view_model() |> update_menus() } @@ -27,6 +27,19 @@ defmodule Systems.Project.NodePage do socket |> update_menus() end + @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, assign(socket, popup: popup)} + end + @impl true def handle_event("delete", %{"item" => item_id}, socket) do Project.Public.delete_item(String.to_integer(item_id)) @@ -39,6 +52,20 @@ 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 + } + + { + :noreply, + socket |> assign(popup: popup) + } + end + @impl true def handle_event( "card_clicked", @@ -50,6 +77,25 @@ defmodule Systems.Project.NodePage do {:noreply, push_redirect(socket, to: path)} end + @impl true + def handle_info(%{module: _, action: :close}, socket) do + { + :noreply, + socket + |> assign(popup: nil) + |> update_view_model() + } + end + + @impl true + def handle_info({:handle_auto_save_done, :node_page_popup}, socket) do + { + :noreply, + socket + |> update_view_model() + } + end + @doc """ ## Attributes - vm: Observed view model: title, node_cards, item_cards @@ -59,6 +105,15 @@ defmodule Systems.Project.NodePage do def render(assigns) do ~H""" <.workspace title={@vm.title} menus={@menus}> + + <%= if @popup do %> + <.popup> +
+ <.live_component id={:node_page_popup} module={@popup.module} {@popup} /> +
+ + <% end %> + <%= if Enum.count(@vm.node_cards) > 0 do %> diff --git a/core/systems/project/node_page_builder.ex b/core/systems/project/node_page_builder.ex index e174bf9bb..5963052c5 100644 --- a/core/systems/project/node_page_builder.ex +++ b/core/systems/project/node_page_builder.ex @@ -33,9 +33,8 @@ defmodule Systems.Project.NodePageBuilder do end defp to_item_cards(%{items: items}, assigns) do - Enum.map( - items, - &ViewModelBuilder.view_model(&1, {Project.NodePage, :item_card}, assigns) - ) + items + |> Enum.sort_by(& &1.inserted_at, {:asc, NaiveDateTime}) + |> Enum.map(&ViewModelBuilder.view_model(&1, {Project.NodePage, :item_card}, assigns)) end end diff --git a/core/test/systems/project/assembly_test.exs b/core/test/systems/project/assembly_test.exs index b73a8a98d..9e93947d1 100644 --- a/core/test/systems/project/assembly_test.exs +++ b/core/test/systems/project/assembly_test.exs @@ -1,11 +1,57 @@ defmodule Systems.Project.AssemblyTest do use Core.DataCase - # alias Systems.{ - # Project - # } + alias Systems.{ + Project + } - test "create_item/2" do - %{root: _root} = Factories.insert!(:project, %{name: "AAP"}) + test "create_item/3 create benchmark item" do + %{id: project_id, root: %{id: root_id} = root} = + Factories.insert!(:project, %{name: "Project"}) + + item_name = "Item" + + assert {:ok, + %{ + item: %Systems.Project.ItemModel{ + name: ^item_name, + project_path: [^project_id, ^root_id], + node_id: ^root_id, + tool_ref: %Systems.Project.ToolRefModel{ + survey_tool_id: nil, + lab_tool_id: nil, + data_donation_tool_id: nil, + benchmark_tool: %Systems.Benchmark.ToolModel{ + status: :concept, + director: :project + } + } + } + }} = Project.Assembly.create_item(item_name, root, :benchmark) + end + + test "create_item/3 create data donation item" do + %{id: project_id, root: %{id: root_id} = root} = + Factories.insert!(:project, %{name: "Project"}) + + item_name = "Item" + + assert {:ok, + %{ + item: %Systems.Project.ItemModel{ + name: ^item_name, + project_path: [^project_id, ^root_id], + node_id: ^root_id, + tool_ref: %Systems.Project.ToolRefModel{ + survey_tool_id: nil, + lab_tool_id: nil, + data_donation_tool: %Systems.DataDonation.ToolModel{ + status: :concept, + director: :project + }, + benchmark_tool_id: nil + } + } + }} = Project.Assembly.create_item(item_name, root, :data_donation) end end