diff --git a/lib/mv_web/components/layouts.ex b/lib/mv_web/components/layouts.ex index ba8ec67..f08c6ba 100644 --- a/lib/mv_web/components/layouts.ex +++ b/lib/mv_web/components/layouts.ex @@ -19,7 +19,7 @@ defmodule MvWeb.Layouts do

Content

- + """ attr :flash, :map, required: true, doc: "the map of flash messages" @@ -67,8 +67,8 @@ defmodule MvWeb.Layouts do -
-
+
+
{render_slot(@inner_block)}
diff --git a/lib/mv_web/components/table_components.ex b/lib/mv_web/components/table_components.ex new file mode 100644 index 0000000..ed94994 --- /dev/null +++ b/lib/mv_web/components/table_components.ex @@ -0,0 +1,44 @@ +defmodule MvWeb.TableComponents do + @moduledoc """ + TableComponents that can be used in tables as components (like a button for sorting, a filter...) + """ + use Phoenix.Component + import MvWeb.CoreComponents + use Gettext, backend: MvWeb.Gettext + + attr :field, :atom, required: true + attr :label, :string, required: true + attr :sort_field, :atom, default: nil + attr :sort_order, :atom, default: nil + + @doc """ + A sort button (with chevron icon) that can be used to sort a list of items + """ + def sort_button(assigns) do + ~H""" + + """ + end + + defp aria_sort(current_field, current_order, this_field) do + cond do + current_field != this_field -> "none" + current_order == :asc -> "ascending" + true -> "descending" + end + end +end diff --git a/lib/mv_web/live/member_live/index.ex b/lib/mv_web/live/member_live/index.ex index 1cff898..38a6f93 100644 --- a/lib/mv_web/live/member_live/index.ex +++ b/lib/mv_web/live/member_live/index.ex @@ -1,58 +1,19 @@ defmodule MvWeb.MemberLive.Index do use MvWeb, :live_view - - @impl true - def render(assigns) do - ~H""" - - <.header> - {gettext("Listing Members")} - <:actions> - <.button variant="primary" navigate={~p"/members/new"}> - <.icon name="hero-plus" /> {gettext("New Member")} - - - - - <.table - id="members" - rows={@streams.members} - row_click={fn {_id, member} -> JS.navigate(~p"/members/#{member}") end} - > - - <:col :let={{_id, member}} label={gettext("First Name")}>{member.first_name} - <:col :let={{_id, member}} label={gettext("Last Name")}>{member.last_name} - <:col :let={{_id, member}} label={gettext("Email")}>{member.email} - <:col :let={{_id, member}} label={gettext("City")}>{member.city} - <:col :let={{_id, member}} label={gettext("Join Date")}>{member.join_date} - - <:action :let={{_id, member}}> -
- <.link navigate={~p"/members/#{member}"}>{gettext("Show")} -
- - <.link navigate={~p"/members/#{member}/edit"}>{gettext("Edit")} - - - <:action :let={{id, member}}> - <.link - phx-click={JS.push("delete", value: %{id: member.id}) |> hide("##{id}")} - data-confirm={gettext("Are you sure?")} - > - {gettext("Delete")} - - - -
- """ - end + import MvWeb.TableComponents @impl true def mount(_params, _session, socket) do + members = Ash.read!(Mv.Membership.Member) + sorted = Enum.sort_by(members, & &1.first_name) + {:ok, socket |> assign(:page_title, gettext("Listing Members")) - |> stream(:members, Ash.read!(Mv.Membership.Member))} + |> assign(:sort_field, :first_name) + |> assign(:sort_order, :asc) + |> assign(:members, sorted) + |> assign(:selected_members, [])} end @impl true @@ -62,4 +23,69 @@ defmodule MvWeb.MemberLive.Index do {:noreply, stream_delete(socket, :members, member)} end + + @doc """ + Selects one member in the list of members + """ + @impl true + def handle_event("select_member", %{"id" => id}, socket) do + selected = + if id in socket.assigns.selected_members do + List.delete(socket.assigns.selected_members, id) + else + [id | socket.assigns.selected_members] + end + + {:noreply, assign(socket, :selected_members, selected)} + end + + @doc """ + Sorts the list of members according to a field, when you click on the column header + """ + @impl true + def handle_event("sort", %{"field" => field_str}, socket) do + members = socket.assigns.members + field = String.to_existing_atom(field_str) + + new_order = + if socket.assigns.sort_field == field do + toggle_order(socket.assigns.sort_order) + else + :asc + end + + sorted_members = + members + |> Enum.sort_by(&Map.get(&1, field), sort_fun(new_order)) + + {:noreply, + socket + |> assign(:sort_field, field) + |> assign(:sort_order, new_order) + |> assign(:members, sorted_members)} + end + + @doc """ + Selects all members in the list of members + """ + @impl true + def handle_event("select_all", _params, socket) do + members = socket.assigns.members + + all_ids = Enum.map(members, & &1.id) + + selected = + if Enum.sort(socket.assigns.selected_members) == Enum.sort(all_ids) do + [] + else + all_ids + end + + {:noreply, assign(socket, :selected_members, selected)} + end + + defp toggle_order(:asc), do: :desc + defp toggle_order(:desc), do: :asc + defp sort_fun(:asc), do: &<=/2 + defp sort_fun(:desc), do: &>=/2 end diff --git a/lib/mv_web/live/member_live/index.html.heex b/lib/mv_web/live/member_live/index.html.heex new file mode 100644 index 0000000..b873805 --- /dev/null +++ b/lib/mv_web/live/member_live/index.html.heex @@ -0,0 +1,80 @@ + + <.header> + {gettext("Listing Members")} + <:actions> + <.button variant="primary" navigate={~p"/members/new"}> + <.icon name="hero-plus" /> {gettext("New Member")} + + + + + <.table + id="members" + rows={@members} + row_click={fn member -> JS.navigate(~p"/members/#{member}") end} + > + + + <:col :let={member} label={ + ~H""" + <.input + type="checkbox" + name="select_all" + phx-click="select_all" + checked={Enum.sort(@selected_members) == Enum.map(@members, & &1.id) |> Enum.sort()} + aria-label={gettext("Select all members")} + role="checkbox" + /> + """ + }> + <.input + type="checkbox" + name={member.id} + phx-click="select_member" + phx-value-id={member.id} + checked={member.id in @selected_members} + phx-capture-click + phx-stop-propagation + aria-label={gettext("Select member")} + role="checkbox" + /> + + <:col + :let={member} + label= { + sort_button(%{ + field: :first_name, + label: gettext("Name"), + sort_field: @sort_field, + sort_order: @sort_order + }) + } + > + {member.first_name} {member.last_name} + + <:col :let={member} label={gettext("Email")}>{member.email} + <:col :let={member} label={gettext("Street")}>{member.street} + <:col :let={member} label={gettext("House Number")}>{member.house_number} + <:col :let={member} label={gettext("Postal Code")}>{member.postal_code} + <:col :let={member} label={gettext("City")}>{member.city} + <:col :let={member} label={gettext("Phone Number")}>{member.phone_number} + <:col :let={member} label={gettext("Join Date")}>{member.join_date} + + <:action :let={member}> +
+ <.link navigate={~p"/members/#{member}"}>{gettext("Show")} +
+ + <.link navigate={~p"/members/#{member}/edit"}>{gettext("Edit")} + + + <:action :let={member}> + <.link + phx-click={JS.push("delete", value: %{id: member.id}) |> hide("#row-#{member.id}")} + data-confirm={gettext("Are you sure?")} + > + {gettext("Delete")} + + + +