diff --git a/assets/css/app.css b/assets/css/app.css index 779bb07..1b548dc 100644 --- a/assets/css/app.css +++ b/assets/css/app.css @@ -119,6 +119,15 @@ 100% { opacity: 0; } } +header.table_header { + border: 0 none; + display: flex; + flex: 1; + flex-direction: row; + justify-content: space-between; + align-items: center; +} + .sorted-asc:after { content: "\21E7"; float: right; @@ -127,4 +136,10 @@ .sorted-desc:after { content: "\21E9"; float: right; +} + +.unsorted:after { + content: "\21D5"; + float: right; + color: lightgray; } \ No newline at end of file diff --git a/lib/star_wars/pagination.ex b/lib/star_wars/pagination.ex new file mode 100644 index 0000000..730a9d5 --- /dev/null +++ b/lib/star_wars/pagination.ex @@ -0,0 +1,16 @@ +defmodule StarWars.Pagination do + import Ecto.Query + alias StarWars.Repo + + def query(query, page, page_size) do + if page_size > 0 do + query |> limit(^page_size) |> offset(^((page - 1) * page_size)) + else + query + end + end + + def page_count(query) do + Repo.one(from(t in subquery(query), select: count("*"))) + end +end diff --git a/lib/star_wars/planets.ex b/lib/star_wars/planets.ex index d052c1c..49d3ae3 100644 --- a/lib/star_wars/planets.ex +++ b/lib/star_wars/planets.ex @@ -3,9 +3,40 @@ defmodule StarWars.Planets do The Planets context. """ - alias StarWars.Repo + alias StarWars.{Repo, Pagination} alias StarWars.Planets.Planet + import Ecto.Query + @default_sort :name + + @type query_options() :: %{ + optional(:page) => number(), + optional(:page_size) => number(), + optional(:sort) => atom(), + optional(:sort_dir) => atom(), + optional(:filter) => atom(), + optional(:filter_value) => any() + } + + @spec count_planets() :: list(%Planet{}) + @spec count_planets(query_options()) :: list(%Planet{}) + @doc """ + returns the total count of records + + ## Examples + + iex> count_planets() + 2 + + """ + def count_planets(opts \\ %{}) do + Planet + |> filter(opts) + |> Pagination.page_count() + end + + @spec list_planets() :: list(%Planet{}) + @spec list_planets(query_options()) :: list(%Planet{}) @doc """ Returns the list of planets. @@ -15,10 +46,15 @@ defmodule StarWars.Planets do [%Planet{}, ...] """ - def list_planets do - Repo.all(Planet) + def list_planets(opts \\ %{}) do + Planet + |> filter(opts) + |> paginate(opts) + |> sort(opts) + |> Repo.all end + @spec get_planet!(number()) :: %Planet{} @doc """ Gets a single planet. @@ -35,6 +71,7 @@ defmodule StarWars.Planets do """ def get_planet!(id), do: Repo.get!(Planet, id) + @spec create_planet(map()) :: {:ok, %Planet{}} | {:error, %Ecto.Changeset{}} @doc """ Creates a planet. @@ -53,6 +90,7 @@ defmodule StarWars.Planets do |> Repo.insert() end + @spec update_planet(%Planet{}, map()) :: {:ok, %Planet{}} | {:error, %Ecto.Changeset{}} @doc """ Updates a planet. @@ -71,6 +109,7 @@ defmodule StarWars.Planets do |> Repo.update() end + @spec delete_planet(%Planet{}) :: {:ok, %Planet{}} | {:error, %Ecto.Changeset{}} @doc """ Deletes a planet. @@ -87,6 +126,7 @@ defmodule StarWars.Planets do Repo.delete(planet) end + @spec change_planet(%Planet{}, map()) :: %Ecto.Changeset{} @doc """ Returns an `%Ecto.Changeset{}` for tracking planet changes. @@ -99,4 +139,60 @@ defmodule StarWars.Planets do def change_planet(%Planet{} = planet, attrs \\ %{}) do Planet.changeset(planet, attrs) end + +# private + + # takes a query and opts and returns a query with the opts for page and page_size applied + defp paginate(q, opts) do + page_size = if Map.has_key?(opts, :page_size), do: opts.page_size, else: 0 + page = if Map.has_key?(opts, :page) do + if opts.page < 1, do: 1, else: opts.page + end + q |> Pagination.query(page, page_size) + end + + # takes a query and opts and returns a query with the opts for sort and sort_dir applied + defp sort(q, opts) do + fields = Planet.__schema__(:fields) + sort = if Map.has_key?(opts, :sort) do + if Enum.member?(fields, opts.sort), do: opts.sort, else: @default_sort + else + @default_sort + end + direction = if Map.has_key?(opts, :sort_dir) && opts.sort_dir == :desc, do: :desc, else: :asc + sort = if direction == :desc, do: [desc: sort], else: [asc: sort] + order_by(q, ^sort) + end + + # takes a query and opts and returns a query with the opts for sort and sort_dir applied + defp filter(q, opts) do + fields = Planet.__schema__(:fields) + filter = if Map.has_key?(opts, :filter) && Enum.member?(fields, opts.filter) do + opts.filter + else + :none + end + value = if Map.has_key?(opts, :filter_value) && String.trim(opts.filter_value) do + String.trim(opts.filter_value) + else + :none + end + value = if filter != :none && value != :none do + attrs = Map.new([{filter, value}]) + changeset = Ecto.Changeset.cast(%Planet{}, attrs, [filter]) + if changeset.valid?, do: Map.get(changeset.changes, filter), else: :none + else + :none + end + if value != :none do + if is_binary(value) do + where(q, [p], ilike(field(p, ^filter), ^"#{value}%")) + else + value = [filter, value] + where(q, ^value) + end + else + q + end + end end diff --git a/lib/star_wars_web/live/live_pagination.ex b/lib/star_wars_web/live/live_pagination.ex new file mode 100644 index 0000000..951a2c3 --- /dev/null +++ b/lib/star_wars_web/live/live_pagination.ex @@ -0,0 +1,40 @@ +defmodule LivePagination do + use StarWarsWeb, :live_component + + @default_opts %{ + page: 1, + page_size: 10, + page_size_control: "builtin", + } + + def update(assigns, socket) do + {:ok, assign(socket, Map.merge(@default_opts, assigns.pagination_options))} + end + + def render(assigns) do + Pagination.pages(Map.merge(assigns, %{myself: assigns.myself})) + end + + def handle_event("page", %{"page" => page}, socket) do + opts = socket.assigns + total_pages = ceil(opts.total_records / opts.page_size) + page = String.to_integer(page) + page = if page < 1 or page > total_pages, do: 1, else: page + if connected?(socket) do + send(self(), {:update_page, %{page: page}}) + else + IO.inspect("not connected") + end + {:noreply, socket |> assign(page: page)} + end + + def handle_event("page_size", %{"page_size" => %{"page_size" => page_size}}, socket) do + page_size = String.to_integer(page_size) + page_size = if page_size < 1, do: 10, else: page_size + if connected?(socket) do + IO.inspect(page_size, label: "page_size event") + send(self(), {:update_page_size, %{page_size: page_size}}) + end + {:noreply, socket |> assign(page_size: page_size, page: 1)} + end +end diff --git a/lib/star_wars_web/live/planet_live/index.ex b/lib/star_wars_web/live/planet_live/index.ex index 1fec790..0f42183 100644 --- a/lib/star_wars_web/live/planet_live/index.ex +++ b/lib/star_wars_web/live/planet_live/index.ex @@ -15,14 +15,53 @@ defmodule StarWarsWeb.PlanetLive.Index do {:noreply, apply_action(socket, socket.assigns.live_action, params)} end - defp apply_action(socket, :index, _params) do + @impl true + def handle_info({:update_page, %{page: page}}, socket) do + page_size = 10 # TODO don't hard code this + path = Routes.planet_index_path(socket, :index, %{page: page, page_size: page_size}) + IO.inspect(path, label: "update_page") + {:noreply, push_patch(socket, to: path)} + end + + @impl true + def handle_info({:update_page, %{sort: sort, sort_dir: sort_dir}}, socket) do + path = Routes.planet_index_path(socket, :index, %{sort: sort, sort_dir: sort_dir}) + IO.inspect(path, label: "update_page") + {:noreply, push_patch(socket, to: path)} + end + + @impl true + def handle_info(:update_page, socket) do + IO.inspect("found", label: "update_page") + {:noreply, socket |> assign(something: "whatever")} + end + + defp apply_action(socket, :index, params) do + params = validate_params(params) + IO.inspect(params) + opts = Map.merge(%{ + total_records: Planets.count_planets(params), + page_size_control: "builtin" + }, params) socket |> assign(:page_title, "Listing Planets") |> assign(:planet, nil) + |> assign(:pagination_options, opts) + |> assign(:planets, Planets.list_planets(params)) end + defp validate_params(params) do + valid_keys = [:page, :page_size, :filter, :filter_value, :sort, :sort_dir] + for {key, val} <- params, into: %{} do + val = cond do + Enum.member?(["page", "page_size"], key) -> String.to_integer(val) + Enum.member?(["sort", "sort_dir"], key) -> String.to_existing_atom(val) + end + {String.to_atom(key), val} + end + |> Map.filter(fn ({k, _v}) -> Enum.member?(valid_keys, k) end) + end def number_formatter(value) do Delimit.number_to_delimited(value, precision: 0) end - end diff --git a/lib/star_wars_web/live/planet_live/index.html.heex b/lib/star_wars_web/live/planet_live/index.html.heex index 798bdd5..d54bfd0 100644 --- a/lib/star_wars_web/live/planet_live/index.html.heex +++ b/lib/star_wars_web/live/planet_live/index.html.heex @@ -1,4 +1,11 @@ -

Listing Planets

+
+

Listing Planets

+ <.live_component + module={LivePagination} + id="pagination" + pagination_options={@pagination_options} + /> +
<.live_component module={SortableTable} diff --git a/lib/star_wars_web/live/table_sortable.ex b/lib/star_wars_web/live/table_sortable.ex index c083b5d..e4c11aa 100644 --- a/lib/star_wars_web/live/table_sortable.ex +++ b/lib/star_wars_web/live/table_sortable.ex @@ -21,13 +21,10 @@ defmodule SortableTable do %{sorted_col: sorted_col, sort_dir: sort_dir} = socket.assigns col = String.to_existing_atom(col) dir = calc_dir(col, sorted_col, sort_dir) - - content = socket.assigns.content - |> Enum.sort_by(&Map.get(&1, col), dir) + send(self(), {:update_page, %{sort: col, sort_dir: dir}}) {:noreply, socket |> assign( - content: content, sorted_col: col, sort_dir: dir ) diff --git a/lib/star_wars_web/pagination.ex b/lib/star_wars_web/pagination.ex new file mode 100644 index 0000000..474c77b --- /dev/null +++ b/lib/star_wars_web/pagination.ex @@ -0,0 +1,69 @@ +defmodule Pagination do + use Phoenix.Component + require IEx + + use Phoenix.HTML + + def prev_button(assigns) do + ~H""" + <%= if @page > 1 do %> + < + <% end %> + """ + end + + def next_button(assigns) do + ~H""" + <%= if @page < @total_pages do %> + > + <% end %> + """ + end + + def page_numbers(assigns) do + ~H""" + <%= for cur_page <- 1..@total_pages do %> + <%= if @page == cur_page do %> + <%= cur_page %> + <% else %> + <%= cur_page %> + <% end %> + <% end %> + """ + end + + def page_size_control(%{page_size_control: ctrl} = assigns) when is_binary(ctrl) and ctrl == "none", do: ~H""" + """ + + def page_size_control(%{page_size_control: ctrl} = assigns) when is_nil(ctrl), do: page_size_control(Map.merge(assigns, %{page_size_control: "builtin"})) + + def page_size_control(%{page_size_control: ctrl} = assigns) when is_function(ctrl), do: ctrl.(assigns) + + def page_size_control(%{page_size_control: ctrl} = assigns) when is_binary(ctrl) and ctrl == "builtin" do + types = %{page_size: :integer} + changeset = {%{}, types} + |> Ecto.Changeset.cast(%{page_size: assigns.page_size}, Map.keys(types)) + ~H""" + <.form let={f} for={changeset} as="page_size" phx-change="page_size" phx-target={@myself} > + Show + <%= select f, :page_size, [10, 20, 50, 100], selected: @page_size %> + per page + + """ + end + + def page_size_control(assigns), do: page_size_control(%{page_size: assigns.page_size, page_size_control: "builtin"}) + + def pages(%{total_records: total_records, page: page, page_size: page_size, page_size_control: page_size_control} = assigns) do + total_pages = ceil(total_records / page_size) + + ~H""" + + """ + end +end diff --git a/lib/star_wars_web/table.ex b/lib/star_wars_web/table.ex index 3a8ed8e..a4ccc4f 100644 --- a/lib/star_wars_web/table.ex +++ b/lib/star_wars_web/table.ex @@ -9,7 +9,7 @@ defmodule Table do if column == key do "sorted-#{direction}" else - "" + "unsorted" end end