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

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:
James Greenwood
2025-11-17 14:42:00 +00:00
commit cc4cc65950
41 changed files with 3890 additions and 0 deletions

View 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

View 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

View 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

View 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

View File

@@ -0,0 +1,5 @@
defmodule ActionRequestsDemo.Repo do
use Ecto.Repo,
otp_app: :action_requests_demo,
adapter: Ecto.Adapters.SQLite3
end