defmodule SelectoComponents.Views.Detail.Component do @doc """ Display results of a detail view """ import SelectoComponents.Components.Common import SelectoComponents.Components.SqlDebug alias SelectoComponents.EnhancedTable.Sorting use Phoenix.LiveComponent def mount(socket) do # Initialize column configuration # Will be populated in update/2 columns = [] socket = socket |> assign(:columns_config, init_columns_config(columns)) {:ok, socket} end def update(assigns, socket) do # Extract columns from selecto if available columns = if Map.has_key?(assigns, :selecto) do Map.get(assigns.selecto.set, :columns, []) |> Enum.map(fn col -> %{ id: col["field"], name: col["alias"] || col["field"], width: 150, min_width: 50, max_width: 500 } end) else [] end socket = socket |> assign(assigns) |> assign(:columns_config, init_columns_config(columns)) {:ok, socket} end def render(assigns) do ### Todo Deal with page changes without executing again....... # Check for execution error first if Map.get(assigns, :execution_error) do # Display the actual error message ~H"""
<%= if @execution_error do %> <% end %>
View cannot be displayed due to the query error shown above.
""" else # Check if we have valid query results and execution state case {assigns[:executed], assigns.query_results} do {false, _} -> # Query is being executed or hasn't been executed yet ~H"""
Loading view...
""" {true, nil} -> # Executed but no results - this is an error state ~H"""
No Results
Query executed but returned no results.
""" {true, {results, _fields, aliases}} -> # Valid results - proceed with normal rendering render_detail_view(assign(assigns, :processed_results, {results, aliases})) end end end defp render_detail_view(assigns) do {results, aliases} = assigns.processed_results # Ensure results are normalized to maps if they're lists normalized_results = if length(results) > 0 and is_list(hd(results)) do {_results, columns, _aliases} = assigns.query_results Enum.map(results, fn row -> Enum.zip(columns, row) |> Map.new() end) else results end page = assigns.view_meta.page per_page = assigns.view_meta.per_page show_start = page * per_page page_count = Float.ceil(Enum.count(normalized_results) / per_page) ### Use Selecto columns rather than aliases because a column can lead to more than one selection... assigns = assign(assigns, aliases: aliases, show_start: show_start, results: normalized_results, columns: Map.get(assigns.selecto.set, :columns, []), column_uuids: Map.get(assigns.selecto.set, :columns, []) |> Enum.map(fn c -> c["uuid"] end), max_pages: page_count ) ~H"""
<.sql_debug :if={Map.get(assigns, :sql)} sql={Map.get(assigns, :sql)} params={Map.get(assigns, :sql_params, [])} execution_time={Map.get(assigns, :execution_time)} />
<.sc_button :if={@view_meta.page > 0} type="button" phx-click="set_page" phx-value-page={@view_meta.page - 1} phx-target={@myself} > Prev Page
{Enum.count(@results)} Rows Found
<.sc_button :if={@view_meta.page < @max_pages} type="button" phx-click="set_page" phx-value-page={@view_meta.page + 1} phx-target={@myself} > Next Page
<%= for {alias, idx} <- Enum.with_index(@aliases) do %> <% column_field = Enum.at(@columns, idx)["field"] %> <% end %> <%!-- Add headers for subselect columns --%> <%= if Map.get(@view_meta, :subselect_configs, []) != [] do %> <%= for config <- Map.get(@view_meta, :subselect_configs, []) do %> <% end %> <% end %> <%!-- --%> <%= for {{resrow, actual_idx}, display_idx} <- Enum.slice(Enum.with_index(@results), @show_start, @view_meta.per_page) |> Enum.with_index() do %> <% # Process row data once at the beginning of the iteration %> <% resrow_list = cond do is_tuple(resrow) -> Tuple.to_list(resrow) is_list(resrow) -> resrow is_map(resrow) -> # If it's already a map, extract values in column order {_results, columns_from_query, _aliases} = @query_results Enum.map(columns_from_query, fn col -> Map.get(resrow, col) end) true -> [resrow] end %> <% row_data_by_uuid = Enum.zip(@column_uuids, resrow_list) |> Enum.into(%{}) %> <% # Also create a map by column name for subselects %> <% {_results, columns_from_query, _aliases} = @query_results %> <% row_data_by_column = Enum.zip(columns_from_query, resrow_list) |> Map.new() %> <%!-- Display regular columns --%> <%!-- Add subselect columns inline --%> <%= if Map.get(@view_meta, :subselect_configs, []) != [] do %> <%= for config <- Map.get(@view_meta, :subselect_configs, []) do %> <% data = Map.get(row_data_by_column, config.key, []) %> <% # Use actual_idx to ensure unique IDs %> <% unique_id = "page#{@view_meta.page}_idx#{actual_idx}_#{config.key}" %> <% end %> <% end %> <% end %>
# {Map.get(config, :title, config.key)}
{actual_idx + 1} <% def = Selecto.columns(@selecto)[col_conf["field"]] %> <%= case def do %> <% %{format: :component} = def -> %> {safe_render_component(def.component, %{ row: row_data_by_uuid[col_conf["uuid"]], config: col_conf })} <% %{format: :link} = def -> %> {safe_render_link(def.link_parts, row_data_by_uuid[col_conf["uuid"]])} <% _ -> %> {row_data_by_uuid[col_conf["uuid"]]} <% end %> <% # Parse the data here to ensure it's fresh %> <% parsed_data = SelectoComponents.Components.NestedTable.parse_subselect_data(data, config) %>
<%= if length(parsed_data) > 0 do %> <%= for key <- SelectoComponents.Components.NestedTable.get_data_keys(parsed_data) do %> <% end %> <%= for {item, _idx} <- Enum.with_index(parsed_data) do %> <%= for key <- SelectoComponents.Components.NestedTable.get_data_keys(parsed_data) do %> <% end %> <% end %>
{SelectoComponents.Components.NestedTable.humanize_key(key)}
{SelectoComponents.Components.NestedTable.format_value( Map.get(item, key, "") )}
<% else %>
No data
<% end %>
""" end def handle_event("sort_column", %{"column" => column} = params, socket) do multi = Map.get(params, "multi", "false") == "true" socket = Sorting.handle_sort_click(column, socket, multi) # Trigger re-execution with new sort send(self(), {:rerun_query_with_sort, socket.assigns.sort_by}) {:noreply, socket} end def handle_event("set_page", params, socket) do IO.puts("\n=== SET_PAGE EVENT RECEIVED IN DETAIL COMPONENT ===") IO.inspect(params, label: "Params") IO.inspect(socket.assigns.view_meta, label: "Current view_meta") new_page = String.to_integer(params["page"]) # Notify parent to update the page in the form params (parent is authoritative) send(self(), {:update_detail_page, new_page}) IO.puts("Sent update_detail_page to parent: #{new_page}") {:noreply, socket} end def handle_event("resize_column", %{"column_id" => _column_id, "width" => _width}, socket) do # Column resize handled by client-side JS {:noreply, socket} end def handle_event("reorder_columns", %{"columns" => _column_order}, socket) do # Column reorder handled by client-side JS {:noreply, socket} end def handle_event("reset_columns", _params, socket) do # Column reset handled by client-side JS {:noreply, socket} end def handle_event("viewport_change", _params, socket) do # Viewport change handled by client-side JS {:noreply, socket} end def handle_event("scroll_position", %{"top" => top, "left" => left}, socket) do socket = assign(socket, scroll_position: %{top: top, left: left}) {:noreply, socket} end def handle_event("virtual_scroll", _params, socket) do # Scroll handled by client-side JS {:noreply, socket} end def handle_event("row_height_changed", %{"index" => _index, "height" => _height}, socket) do # Row height update handled by client-side JS {:noreply, socket} end def handle_event("viewport_measured", %{"width" => _width, "height" => height}, socket) do virtual = Map.get(socket.assigns, :virtual_scroll, %{}) socket = assign(socket, virtual_scroll: Map.put(virtual, :viewport_height, height)) {:noreply, socket} end def handle_event("show_row_details", %{"row-index" => row_index}, socket) do index = String.to_integer(row_index) # Get results from processed_results if available, otherwise extract from query_results {results, aliases} = if Map.has_key?(socket.assigns, :processed_results) do socket.assigns.processed_results else case socket.assigns.query_results do {results, _columns, aliases} -> {results, aliases} _ -> {[], []} end end # Normalize results to maps if needed normalized_results = if length(results) > 0 and is_list(hd(results)) do {_results, columns, _aliases} = socket.assigns.query_results Enum.map(results, fn row -> Enum.zip(columns, row) |> Map.new() end) else results end record = Enum.at(normalized_results, index) # Send event to parent to show modal send( self(), {:show_detail_modal, %{ record: record, current_index: index, total_records: length(normalized_results), records: normalized_results, fields: aliases, related_data: build_related_data(record, socket) }} ) {:noreply, socket} end def handle_info({:load_more_data, _end_index}, socket) do # This would be implemented by the parent component to load more data send(self(), :load_more_virtual_data) {:noreply, socket} end # Helper function to initialize column configuration defp init_columns_config(columns) do Enum.map(columns, fn col -> %{ id: col, visible: true, width: "auto", locked: false } end) end # Helper function to build related data configuration defp build_related_data(_record, _socket) do # This would be configured based on the domain/schema relationships # For now, return empty map - parent component can override %{} end @doc """ JavaScript hooks for row click handling. """ def __hooks__() do %{ "RowClickable" => %{ mounted: """ // Add click handlers to all table rows this.el.querySelectorAll('tr[phx-click="show_row_details"]').forEach(row => { row.style.cursor = 'pointer'; row.addEventListener('mouseenter', () => { if (!row.classList.contains('bg-blue-50')) { row.dataset.originalBg = row.className; row.classList.add('bg-blue-50'); } }); row.addEventListener('mouseleave', () => { row.classList.remove('bg-blue-50'); if (row.dataset.originalBg) { row.className = row.dataset.originalBg; } }); }); """, updated: """ // Re-apply handlers after LiveView updates this.mounted(); """ } } end defp safe_render_component(component_fn, params) do try do component_fn.(params) rescue e -> error_html = """
Component Error:
#{inspect(e.__struct__)}: #{Exception.message(e)}
#{if Mix.env() == :dev do "
Debug Info
Row data: #{inspect(params.row)}
" else "" end}
""" Phoenix.HTML.raw(error_html) end end defp safe_render_link(link_parts_fn, row_data) do try do case link_parts_fn.(row_data) do {href, txt} -> escaped_txt = txt |> Phoenix.HTML.html_escape() |> Phoenix.HTML.safe_to_string() Phoenix.HTML.raw(""" #{escaped_txt} """) _ -> row_data end rescue e -> error_html = """
Link Error:
#{inspect(e.__struct__)}: #{Exception.message(e)}
""" Phoenix.HTML.raw(error_html) end end end