feat(elixir): robust sort in component list
This commit is contained in:
@@ -23,12 +23,16 @@ defmodule ComponentsElixirWeb.ComponentsLive do
|
||||
|> assign(:storage_locations, storage_locations)
|
||||
|> assign(:stats, stats)
|
||||
|> assign(:search, "")
|
||||
|> assign(:sort_criteria, "all_not_id")
|
||||
|> assign(:sort_criteria, "name_asc")
|
||||
|> assign(:selected_category, nil)
|
||||
|> assign(:offset, 0)
|
||||
|> assign(:components, [])
|
||||
|> assign(:has_more, false)
|
||||
|> assign(:loading, false)
|
||||
|> assign(:sort_freeze_until, nil)
|
||||
|> assign(:interacting_with, nil)
|
||||
|> assign(:sort_freeze_timer, nil)
|
||||
|> assign(:sort_frozen, false)
|
||||
|> assign(:show_add_form, false)
|
||||
|> assign(:show_edit_form, false)
|
||||
|> assign(:editing_component, nil)
|
||||
@@ -48,7 +52,7 @@ defmodule ComponentsElixirWeb.ComponentsLive do
|
||||
@impl true
|
||||
def handle_params(params, _uri, socket) do
|
||||
search = Map.get(params, "search", "")
|
||||
criteria = Map.get(params, "criteria", "all_not_id")
|
||||
criteria = Map.get(params, "criteria", "name_asc")
|
||||
|
||||
{:noreply,
|
||||
socket
|
||||
@@ -114,10 +118,36 @@ defmodule ComponentsElixirWeb.ComponentsLive do
|
||||
|
||||
case Inventory.increment_component_count(component) do
|
||||
{:ok, _updated_component} ->
|
||||
{:noreply,
|
||||
socket
|
||||
|> put_flash(:info, "Count updated")
|
||||
|> load_components()}
|
||||
# Only apply sort freeze for dynamic sorting criteria
|
||||
should_freeze = socket.assigns.sort_criteria in ["count_asc", "count_desc", "updated_at_asc", "updated_at_desc"]
|
||||
|
||||
if should_freeze do
|
||||
# Cancel any existing timer
|
||||
if socket.assigns.sort_freeze_timer do
|
||||
Process.cancel_timer(socket.assigns.sort_freeze_timer)
|
||||
end
|
||||
|
||||
# Set sort freeze for 3 seconds and mark component as interacting
|
||||
freeze_until = DateTime.add(DateTime.utc_now(), 3, :second)
|
||||
|
||||
# Set new timer to clear interaction state
|
||||
timer_ref = Process.send_after(self(), {:clear_interaction, id}, 3000)
|
||||
|
||||
{:noreply,
|
||||
socket
|
||||
|> put_flash(:info, "Count updated")
|
||||
|> assign(:sort_freeze_until, freeze_until)
|
||||
|> assign(:interacting_with, id)
|
||||
|> assign(:sort_freeze_timer, timer_ref)
|
||||
|> assign(:sort_frozen, true)
|
||||
|> load_components()}
|
||||
else
|
||||
# Normal behavior for stable sorts
|
||||
{:noreply,
|
||||
socket
|
||||
|> put_flash(:info, "Count updated")
|
||||
|> load_components()}
|
||||
end
|
||||
|
||||
{:error, _changeset} ->
|
||||
{:noreply, put_flash(socket, :error, "Failed to update count")}
|
||||
@@ -129,10 +159,36 @@ defmodule ComponentsElixirWeb.ComponentsLive do
|
||||
|
||||
case Inventory.decrement_component_count(component) do
|
||||
{:ok, _updated_component} ->
|
||||
{:noreply,
|
||||
socket
|
||||
|> put_flash(:info, "Count updated")
|
||||
|> load_components()}
|
||||
# Only apply sort freeze for dynamic sorting criteria
|
||||
should_freeze = socket.assigns.sort_criteria in ["count_asc", "count_desc", "updated_at_asc", "updated_at_desc"]
|
||||
|
||||
if should_freeze do
|
||||
# Cancel any existing timer
|
||||
if socket.assigns.sort_freeze_timer do
|
||||
Process.cancel_timer(socket.assigns.sort_freeze_timer)
|
||||
end
|
||||
|
||||
# Set sort freeze for 3 seconds and mark component as interacting
|
||||
freeze_until = DateTime.add(DateTime.utc_now(), 3, :second)
|
||||
|
||||
# Set new timer to clear interaction state
|
||||
timer_ref = Process.send_after(self(), {:clear_interaction, id}, 3000)
|
||||
|
||||
{:noreply,
|
||||
socket
|
||||
|> put_flash(:info, "Count updated")
|
||||
|> assign(:sort_freeze_until, freeze_until)
|
||||
|> assign(:interacting_with, id)
|
||||
|> assign(:sort_freeze_timer, timer_ref)
|
||||
|> assign(:sort_frozen, true)
|
||||
|> load_components()}
|
||||
else
|
||||
# Normal behavior for stable sorts
|
||||
{:noreply,
|
||||
socket
|
||||
|> put_flash(:info, "Count updated")
|
||||
|> load_components()}
|
||||
end
|
||||
|
||||
{:error, _changeset} ->
|
||||
{:noreply, put_flash(socket, :error, "Failed to update count")}
|
||||
@@ -270,35 +326,78 @@ defmodule ComponentsElixirWeb.ComponentsLive do
|
||||
end
|
||||
end
|
||||
|
||||
@impl true
|
||||
def handle_info({:clear_interaction, component_id}, socket) do
|
||||
# Only clear if this timer is for the currently interacting component
|
||||
if socket.assigns.interacting_with == component_id do
|
||||
# Clear interaction state and allow sorting to resume
|
||||
{:noreply,
|
||||
socket
|
||||
|> assign(:sort_freeze_until, nil)
|
||||
|> assign(:interacting_with, nil)
|
||||
|> assign(:sort_freeze_timer, nil)
|
||||
|> assign(:sort_frozen, false)
|
||||
|> load_components()}
|
||||
else
|
||||
# Ignore stale timer messages
|
||||
{:noreply, socket}
|
||||
end
|
||||
end
|
||||
|
||||
defp load_components(socket, opts \\ []) do
|
||||
append = Keyword.get(opts, :append, false)
|
||||
|
||||
filters = [
|
||||
search: socket.assigns.search,
|
||||
sort_criteria: socket.assigns.sort_criteria,
|
||||
category_id: socket.assigns.selected_category,
|
||||
limit: @items_per_page,
|
||||
offset: socket.assigns.offset
|
||||
]
|
||||
|> Enum.reject(fn
|
||||
{_, v} when is_nil(v) -> true
|
||||
{:search, v} when v == "" -> true
|
||||
{_, _} -> false
|
||||
end)
|
||||
# Check if sorting should be frozen
|
||||
now = DateTime.utc_now()
|
||||
should_reload = is_nil(socket.assigns.sort_freeze_until) ||
|
||||
DateTime.compare(now, socket.assigns.sort_freeze_until) != :lt
|
||||
|
||||
%{components: new_components, has_more: has_more} =
|
||||
Inventory.paginate_components(filters)
|
||||
if should_reload do
|
||||
# Normal loading - query database with current sort criteria
|
||||
filters = [
|
||||
search: socket.assigns.search,
|
||||
sort_criteria: socket.assigns.sort_criteria,
|
||||
category_id: socket.assigns.selected_category,
|
||||
limit: @items_per_page,
|
||||
offset: socket.assigns.offset
|
||||
]
|
||||
|> Enum.reject(fn
|
||||
{_, v} when is_nil(v) -> true
|
||||
{:search, v} when v == "" -> true
|
||||
{_, _} -> false
|
||||
end)
|
||||
|
||||
components =
|
||||
if append do
|
||||
socket.assigns.components ++ new_components
|
||||
%{components: new_components, has_more: has_more} =
|
||||
Inventory.paginate_components(filters)
|
||||
|
||||
components =
|
||||
if append do
|
||||
socket.assigns.components ++ new_components
|
||||
else
|
||||
new_components
|
||||
end
|
||||
|
||||
socket
|
||||
|> assign(:components, components)
|
||||
|> assign(:has_more, has_more)
|
||||
else
|
||||
# Frozen - just update the specific component in place without reordering
|
||||
if socket.assigns.interacting_with do
|
||||
updated_components =
|
||||
Enum.map(socket.assigns.components, fn component ->
|
||||
if to_string(component.id) == socket.assigns.interacting_with do
|
||||
# Reload this specific component to get updated count
|
||||
Inventory.get_component!(component.id)
|
||||
else
|
||||
component
|
||||
end
|
||||
end)
|
||||
|
||||
assign(socket, :components, updated_components)
|
||||
else
|
||||
new_components
|
||||
socket
|
||||
end
|
||||
|
||||
socket
|
||||
|> assign(:components, components)
|
||||
|> assign(:has_more, has_more)
|
||||
end
|
||||
end
|
||||
|
||||
defp build_query_params(socket, overrides) do
|
||||
@@ -421,24 +520,44 @@ defmodule ComponentsElixirWeb.ComponentsLive do
|
||||
</form>
|
||||
</div>
|
||||
<div>
|
||||
<form phx-change="sort_change">
|
||||
<form phx-change="sort_change" class="relative">
|
||||
<select
|
||||
name="sort_criteria"
|
||||
class="block w-full px-3 py-2 border border-base-300 rounded-md shadow-sm bg-base-100 text-base-content focus:outline-none focus:ring-primary focus:border-primary sm:text-sm"
|
||||
>
|
||||
<option value="all_not_id" selected={@sort_criteria == "all_not_id"}>
|
||||
All (without IDs)
|
||||
<option value="name_asc" selected={@sort_criteria == "name_asc"}>
|
||||
Name A-Z
|
||||
</option>
|
||||
<option value="all" selected={@sort_criteria == "all"}>All</option>
|
||||
<option value="name" selected={@sort_criteria == "name"}>Name</option>
|
||||
<option value="description" selected={@sort_criteria == "description"}>
|
||||
Description
|
||||
<option value="name_desc" selected={@sort_criteria == "name_desc"}>
|
||||
Name Z-A
|
||||
</option>
|
||||
<option value="id" selected={@sort_criteria == "id"}>ID</option>
|
||||
<option value="category_id" selected={@sort_criteria == "category_id"}>
|
||||
Category ID
|
||||
<option value="inserted_at_desc" selected={@sort_criteria == "inserted_at_desc"}>
|
||||
Entry Date (Newest)
|
||||
</option>
|
||||
<option value="inserted_at_asc" selected={@sort_criteria == "inserted_at_asc"}>
|
||||
Entry Date (Oldest)
|
||||
</option>
|
||||
<option value="updated_at_desc" selected={@sort_criteria == "updated_at_desc"}>
|
||||
Update Date (Newest)
|
||||
</option>
|
||||
<option value="updated_at_asc" selected={@sort_criteria == "updated_at_asc"}>
|
||||
Update Date (Oldest)
|
||||
</option>
|
||||
<option value="count_desc" selected={@sort_criteria == "count_desc"}>
|
||||
Count (Highest)
|
||||
</option>
|
||||
<option value="count_asc" selected={@sort_criteria == "count_asc"}>
|
||||
Count (Lowest)
|
||||
</option>
|
||||
</select>
|
||||
<%= if @sort_frozen do %>
|
||||
<div class="absolute -bottom-5 left-0 text-xs text-yellow-600 flex items-center transition-opacity duration-200 pointer-events-none">
|
||||
<svg class="w-3 h-3 mr-1" fill="currentColor" viewBox="0 0 20 20">
|
||||
<path d="M10 2L3 7v11h14V7l-7-5z"/>
|
||||
</svg>
|
||||
Sort temporarily frozen
|
||||
</div>
|
||||
<% end %>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
@@ -689,7 +808,11 @@ defmodule ComponentsElixirWeb.ComponentsLive do
|
||||
<div class="bg-base-100 shadow overflow-hidden sm:rounded-md">
|
||||
<ul class="divide-y divide-base-300" id="components-list" phx-update="replace">
|
||||
<%= for component <- @components do %>
|
||||
<li id={"component-#{component.id}"} class={"px-6 py-6 hover:bg-base-200 #{if @focused_component_id == component.id, do: "bg-base-50 border-l-4 border-primary", else: "cursor-pointer"}"} phx-click={if @focused_component_id != component.id, do: "toggle_focus", else: nil} phx-value-id={component.id}>
|
||||
<li id={"component-#{component.id}"} class={[
|
||||
"px-6 py-6 hover:bg-base-200 transition-all duration-200",
|
||||
if(@focused_component_id == component.id, do: "bg-base-50 border-l-4 border-primary", else: "cursor-pointer"),
|
||||
if(@interacting_with == to_string(component.id), do: "ring-2 ring-yellow-400 ring-opacity-50 bg-yellow-50", else: "")
|
||||
]} phx-click={if @focused_component_id != component.id, do: "toggle_focus", else: nil} phx-value-id={component.id}>
|
||||
<%= if @focused_component_id == component.id do %>
|
||||
<!-- Expanded/Focused View -->
|
||||
<div class="space-y-6">
|
||||
|
||||
Reference in New Issue
Block a user