Initial commit: Phoenix LiveView demo for interactive data tables with filtering, sorting, pagination, URL state, and progressive enhancement
All checks were successful
build / build (push) Successful in 11s
All checks were successful
build / build (push) Successful in 11s
Implements a fully-featured action requests table in a single LiveView module using Flop, Ecto, and SQLite. Includes: - Fuzzy search, status/assignment filters, column sorting, 15-per-page pagination - Real-time updates, bookmarkable URLs via `handle_params/3` - JS-disabled fallback with GET forms (no duplicate logic) - 1,000,000 seeded records, Tailwind + DaisyUI styling, light/dark themes - Comprehensive README with comparisons to Django+React/Rails+React stacks - 31 tests covering all scenarios Tech: Phoenix 1.8+, LiveView, Flop, Ecto, SQLite, Elixir 1.15+
This commit is contained in:
79
lib/action_requests_demo/action_requests.ex
Normal file
79
lib/action_requests_demo/action_requests.ex
Normal file
@@ -0,0 +1,79 @@
|
||||
defmodule ActionRequestsDemo.ActionRequests do
|
||||
import Ecto.Query
|
||||
alias ActionRequestsDemo.{Repo, ActionRequests.ActionRequest}
|
||||
|
||||
def list_action_requests(params, current_user_id \\ nil) do
|
||||
# Extract custom filters
|
||||
patient_name = get_in(params, ["patient_name"]) |> clean_param()
|
||||
status = get_in(params, ["status"]) |> clean_param()
|
||||
assignment = get_in(params, ["assignment"]) |> clean_param()
|
||||
|
||||
# Build base query with custom filters
|
||||
query =
|
||||
ActionRequest
|
||||
|> apply_patient_name_filter(patient_name)
|
||||
|> apply_status_filter(status)
|
||||
|> apply_assignment_filter(assignment, current_user_id)
|
||||
|
||||
# Use Flop for sorting and pagination only
|
||||
flop_params = Map.drop(params, ["patient_name", "status", "assignment"])
|
||||
|
||||
# Set default sort if not specified
|
||||
flop_params =
|
||||
if is_nil(flop_params["order_by"]) || flop_params["order_by"] == "" do
|
||||
flop_params
|
||||
|> Map.put("order_by", ["inserted_at"])
|
||||
|> Map.put("order_directions", ["desc"])
|
||||
else
|
||||
# Convert order_by and order_directions to arrays if they're strings
|
||||
flop_params
|
||||
|> maybe_listify("order_by")
|
||||
|> maybe_listify("order_directions")
|
||||
end
|
||||
|
||||
Flop.validate_and_run(query, flop_params, for: ActionRequest, repo: Repo)
|
||||
end
|
||||
|
||||
defp maybe_listify(params, key) do
|
||||
case params[key] do
|
||||
nil -> params
|
||||
"" -> Map.delete(params, key)
|
||||
val when is_binary(val) -> Map.put(params, key, [val])
|
||||
val when is_atom(val) -> Map.put(params, key, [val])
|
||||
val when is_list(val) -> params
|
||||
_ -> params
|
||||
end
|
||||
end
|
||||
|
||||
defp clean_param(nil), do: nil
|
||||
defp clean_param(""), do: nil
|
||||
defp clean_param("all"), do: nil
|
||||
defp clean_param(val), do: val
|
||||
|
||||
defp apply_patient_name_filter(query, nil), do: query
|
||||
|
||||
defp apply_patient_name_filter(query, name) do
|
||||
pattern = "%#{name}%"
|
||||
where(query, [a], like(a.patient_name, ^pattern))
|
||||
end
|
||||
|
||||
defp apply_status_filter(query, nil), do: query
|
||||
|
||||
defp apply_status_filter(query, status) do
|
||||
where(query, [a], a.status == ^status)
|
||||
end
|
||||
|
||||
defp apply_assignment_filter(query, nil, _), do: query
|
||||
|
||||
defp apply_assignment_filter(query, "mine", current_user_id) do
|
||||
where(query, [a], a.assigned_user_id == ^current_user_id)
|
||||
end
|
||||
|
||||
defp apply_assignment_filter(query, "assigned", _) do
|
||||
where(query, [a], not is_nil(a.assigned_user_id))
|
||||
end
|
||||
|
||||
defp apply_assignment_filter(query, "unassigned", _) do
|
||||
where(query, [a], is_nil(a.assigned_user_id))
|
||||
end
|
||||
end
|
||||
29
lib/action_requests_demo/action_requests/action_request.ex
Normal file
29
lib/action_requests_demo/action_requests/action_request.ex
Normal file
@@ -0,0 +1,29 @@
|
||||
defmodule ActionRequestsDemo.ActionRequests.ActionRequest do
|
||||
use Ecto.Schema
|
||||
import Ecto.Changeset
|
||||
|
||||
@derive {
|
||||
Flop.Schema,
|
||||
filterable: [:patient_name, :status, :assigned_user_id],
|
||||
sortable: [:patient_name, :status, :inserted_at, :delivery_scheduled_at],
|
||||
default_limit: 15
|
||||
}
|
||||
|
||||
@primary_key {:id, :binary_id, autogenerate: true}
|
||||
@foreign_key_type :binary_id
|
||||
schema "action_requests" do
|
||||
field :patient_name, :string
|
||||
field :status, :string
|
||||
field :assigned_user_id, :integer
|
||||
field :delivery_scheduled_at, :naive_datetime
|
||||
|
||||
timestamps(type: :utc_datetime)
|
||||
end
|
||||
|
||||
@doc false
|
||||
def changeset(action_request, attrs) do
|
||||
action_request
|
||||
|> cast(attrs, [:patient_name, :status, :assigned_user_id, :delivery_scheduled_at])
|
||||
|> validate_required([:patient_name, :status])
|
||||
end
|
||||
end
|
||||
34
lib/action_requests_demo/application.ex
Normal file
34
lib/action_requests_demo/application.ex
Normal file
@@ -0,0 +1,34 @@
|
||||
defmodule ActionRequestsDemo.Application do
|
||||
# See https://hexdocs.pm/elixir/Application.html
|
||||
# for more information on OTP Applications
|
||||
@moduledoc false
|
||||
|
||||
use Application
|
||||
|
||||
@impl true
|
||||
def start(_type, _args) do
|
||||
children = [
|
||||
ActionRequestsDemo.Repo,
|
||||
{Ecto.Migrator,
|
||||
repos: Application.fetch_env!(:action_requests_demo, :ecto_repos), skip: skip_migrations?()},
|
||||
{Phoenix.PubSub, name: ActionRequestsDemo.PubSub},
|
||||
ActionRequestsDemoWeb.Endpoint
|
||||
]
|
||||
|
||||
opts = [strategy: :one_for_one, name: ActionRequestsDemo.Supervisor]
|
||||
Supervisor.start_link(children, opts)
|
||||
end
|
||||
|
||||
# Tell Phoenix to update the endpoint configuration
|
||||
# whenever the application is updated.
|
||||
@impl true
|
||||
def config_change(changed, _new, removed) do
|
||||
ActionRequestsDemoWeb.Endpoint.config_change(changed, removed)
|
||||
:ok
|
||||
end
|
||||
|
||||
defp skip_migrations?() do
|
||||
# By default, sqlite migrations are run when using a release
|
||||
System.get_env("RELEASE_NAME") == nil
|
||||
end
|
||||
end
|
||||
70
lib/action_requests_demo/release.ex
Normal file
70
lib/action_requests_demo/release.ex
Normal file
@@ -0,0 +1,70 @@
|
||||
defmodule ActionRequestsDemo.Release do
|
||||
@moduledoc """
|
||||
Helpers for running tasks in a release environment.
|
||||
|
||||
This module is intended for commands invoked via:
|
||||
|
||||
bin/action_requests_demo eval "ActionRequestsDemo.Release.seed()"
|
||||
"""
|
||||
|
||||
@app :action_requests_demo
|
||||
|
||||
@doc """
|
||||
Run the database seeds script inside a release.
|
||||
|
||||
This is designed to be run against a *running* release (e.g. via
|
||||
`bin/action_requests_demo eval ...` inside your Docker container),
|
||||
so it does **not** try to start the application or endpoint again.
|
||||
It only ensures Faker is running, then evaluates the standard
|
||||
`priv/repo/seeds.exs` file, which uses `ActionRequestsDemo.Repo`.
|
||||
"""
|
||||
def seed do
|
||||
ensure_app_started(ActionRequestsDemo.Repo)
|
||||
ensure_app_started(:faker)
|
||||
|
||||
seeds_path = Application.app_dir(@app, "priv/repo/seeds.exs")
|
||||
Code.require_file(seeds_path)
|
||||
end
|
||||
|
||||
defp ensure_app_started(app_or_repo) do
|
||||
case Application.ensure_all_started(app_or_repo) do
|
||||
{:ok, _} ->
|
||||
:ok
|
||||
|
||||
{:error, {:already_started, _pid}} ->
|
||||
:ok
|
||||
|
||||
{:error, {:not_started, _}} ->
|
||||
start_repo(app_or_repo)
|
||||
|
||||
{:error, reason} ->
|
||||
raise "Could not start #{inspect(app_or_repo)}: #{inspect(reason)}"
|
||||
end
|
||||
rescue
|
||||
UndefinedFunctionError ->
|
||||
start_repo(app_or_repo)
|
||||
end
|
||||
|
||||
defp start_repo(repo) when is_atom(repo) do
|
||||
case repo.start_link() do
|
||||
{:ok, _pid} ->
|
||||
:ok
|
||||
|
||||
{:error, {:already_started, _pid}} ->
|
||||
:ok
|
||||
|
||||
other ->
|
||||
raise "Could not start #{inspect(repo)}: #{inspect(other)}"
|
||||
end
|
||||
end
|
||||
|
||||
defp start_repo(app) do
|
||||
case Application.ensure_all_started(app) do
|
||||
{:ok, _} -> :ok
|
||||
{:error, {:already_started, _}} -> :ok
|
||||
other -> raise "Could not start #{inspect(app)}: #{inspect(other)}"
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
5
lib/action_requests_demo/repo.ex
Normal file
5
lib/action_requests_demo/repo.ex
Normal file
@@ -0,0 +1,5 @@
|
||||
defmodule ActionRequestsDemo.Repo do
|
||||
use Ecto.Repo,
|
||||
otp_app: :action_requests_demo,
|
||||
adapter: Ecto.Adapters.SQLite3
|
||||
end
|
||||
Reference in New Issue
Block a user