From 53853afa9fc1ca26317df0a682473318b47178f9 Mon Sep 17 00:00:00 2001 From: Etienne Lacoursiere Date: Tue, 3 Nov 2026 11:28:16 -0500 Subject: [PATCH 1/3] feat: searchable table columns via table_searchable_columns Adds a table_searchable_columns admin option. The listed columns get Cinder's search, and the search box stays visible even when the filters are hidden behind the Filters toggle. Co-Authored-By: Claude Opus 5.5 --- documentation/dsls/DSL-AshAdmin.Resource.md | 1 + .../components/resource/data_table.ex | 32 ++++++++++++- lib/ash_admin/resource/resource.ex | 9 ++++ lib/ash_admin/themes/ash_admin_theme.ex | 6 +++ .../resource/data_table_search_test.exs | 45 +++++++++++++++++++ test/support/resources/blog.ex | 6 +++ 6 files changed, 97 insertions(+), 2 deletions(-) create mode 100644 test/components/resource/data_table_search_test.exs diff --git a/documentation/dsls/DSL-AshAdmin.Resource.md b/documentation/dsls/DSL-AshAdmin.Resource.md index 2cba143e..8dfe51cc 100644 --- a/documentation/dsls/DSL-AshAdmin.Resource.md +++ b/documentation/dsls/DSL-AshAdmin.Resource.md @@ -35,6 +35,7 @@ Configure the admin dashboard for a given resource. | [`table_columns`](#admin-table_columns){: #admin-table_columns } | `list(atom)` | | The list of attributes to render on the table view. | | [`table_sortable_columns`](#admin-table_sortable_columns){: #admin-table_sortable_columns } | `list(atom)` | | The list of columns that can be sorted. If not specified, all columns are sortable. | | [`table_filterable_columns`](#admin-table_filterable_columns){: #admin-table_filterable_columns } | `list(atom)` | | The list of columns that can be filtered. If not specified, all columns are filterable. | +| [`table_searchable_columns`](#admin-table_searchable_columns){: #admin-table_searchable_columns } | `list(atom)` | | The list of columns that the table search box searches. A search box shows only when this is set. Each column must also be in the table columns. | | [`format_fields`](#admin-format_fields){: #admin-format_fields } | `list(any)` | | The list of fields and their formats represented as a MFA. For example: `updated_at: {Timex, :format!, ["{0D}-{0M}-{YYYY} {h12}:{m} {AM}"]}`. Datatable pages format all given fields. Show and Update pages format given read-only fields of types `Ash.Type.Date`, `Ash.Type.DateTime`, `Ash.Type.Time`, `Ash.Type.NaiveDatetime`, `Ash.Type.UtcDatetime` and `Ash.Type.UtcDatetimeUsec`. | | [`relationship_display_fields`](#admin-relationship_display_fields){: #admin-relationship_display_fields } | `list(atom)` | | The list of attributes to render when this resource is shown as a relationship on another resource's datatable. | | [`resource_group`](#admin-resource_group){: #admin-resource_group } | `atom` | | The group in the top resource dropdown that the resource appears in. | diff --git a/lib/ash_admin/components/resource/data_table.ex b/lib/ash_admin/components/resource/data_table.ex index 3297fb30..cf72fa43 100644 --- a/lib/ash_admin/components/resource/data_table.ex +++ b/lib/ash_admin/components/resource/data_table.ex @@ -105,7 +105,7 @@ defmodule AshAdmin.Components.Resource.DataTable do tenant={@tenant} page_size={@page_size} pagination={@pagination_mode} - show_filters={@show_filters} + show_filters={@show_filters || @has_search} query_opts={[authorize?: @authorizing]} theme={AshAdminTheme} > @@ -117,10 +117,31 @@ defmodule AshAdmin.Components.Resource.DataTable do label={to_name(field_name)} filter={filterable?(@resource, field_name)} sort={sortable?(@resource, field_name)} + search={searchable?(@resource, field_name)} > {render_field_value(record, field_name, assigns)} + + <:controls :let={controls}> +
+ + +
+ + <:col :let={record} :if={actions?(@resource)} label="">
@@ -330,12 +351,14 @@ defmodule AshAdmin.Components.Resource.DataTable do {page_size, pagination_mode} = pagination_config(socket.assigns.action) has_filters = AshAdmin.Resource.table_filterable_columns(socket.assigns.resource) != [] + has_search = AshAdmin.Resource.table_searchable_columns(socket.assigns.resource) != [] socket = assign(socket, page_size: page_size, pagination_mode: pagination_mode, - has_filters: has_filters + has_filters: has_filters, + has_search: has_search ) {:ok, @@ -568,6 +591,11 @@ defmodule AshAdmin.Components.Resource.DataTable do end end + # Check if a field is searched by the table search box + defp searchable?(resource, field_name) do + field_name in AshAdmin.Resource.table_searchable_columns(resource) + end + # Check if field is an actual resource attribute (not a relationship or calculated field) defp has_attribute?(resource, field_name) do Ash.Resource.Info.field(resource, field_name) != nil diff --git a/lib/ash_admin/resource/resource.ex b/lib/ash_admin/resource/resource.ex index 49b566a5..c659e196 100644 --- a/lib/ash_admin/resource/resource.ex +++ b/lib/ash_admin/resource/resource.ex @@ -94,6 +94,11 @@ defmodule AshAdmin.Resource do doc: "The list of columns that can be filtered. If not specified, all columns are filterable." ], + table_searchable_columns: [ + type: {:list, :atom}, + doc: + "The list of columns that the table search box searches. A search box shows only when this is set. Each column must also be in the table columns." + ], format_fields: [ type: {:list, :any}, doc: """ @@ -286,6 +291,10 @@ defmodule AshAdmin.Resource do Spark.Dsl.Extension.get_opt(resource, [:admin], :table_filterable_columns, nil, true) end + def table_searchable_columns(resource) do + Spark.Dsl.Extension.get_opt(resource, [:admin], :table_searchable_columns, [], true) + end + defp find_polymorphic_tables(resource, domains) do domains |> Enum.flat_map(&AshAdmin.Domain.show_resources/1) diff --git a/lib/ash_admin/themes/ash_admin_theme.ex b/lib/ash_admin/themes/ash_admin_theme.ex index fe23ce7b..abbe852c 100644 --- a/lib/ash_admin/themes/ash_admin_theme.ex +++ b/lib/ash_admin/themes/ash_admin_theme.ex @@ -93,6 +93,12 @@ defmodule AshAdmin.Themes.AshAdminTheme do set :filter_clear_button_class, "ml-2 text-slate-400 dark:text-slate-500 hover:text-slate-600 dark:hover:text-slate-300 text-sm" + # Search box - left padding leaves room for the magnifier icon + set :search_input_class, + "w-72 pl-9 pr-3 py-2 border border-slate-300 dark:border-slate-600 rounded text-sm bg-white dark:bg-slate-800 text-slate-900 dark:text-slate-100 focus:ring-1 focus:ring-slate-500 focus:border-slate-500" + + set :search_icon_class, "h-4 w-4 text-slate-400 dark:text-slate-500" + # Input styling - larger inputs with thicker borders to match table set :filter_text_input_class, "w-48 px-3 py-2 border border-slate-300 dark:border-slate-600 rounded text-sm bg-white dark:bg-slate-800 text-slate-900 dark:text-slate-100 focus:ring-1 focus:ring-slate-500 focus:border-slate-500" diff --git a/test/components/resource/data_table_search_test.exs b/test/components/resource/data_table_search_test.exs new file mode 100644 index 00000000..cd2dcdb9 --- /dev/null +++ b/test/components/resource/data_table_search_test.exs @@ -0,0 +1,45 @@ +# SPDX-FileCopyrightText: 2020 ash_admin contributors +# +# SPDX-License-Identifier: MIT + +defmodule AshAdmin.Test.Components.Resource.DataTableSearchTest do + use ExUnit.Case, async: false + + import Phoenix.ConnTest + import Phoenix.LiveViewTest + + @endpoint AshAdmin.Test.Endpoint + + setup do + Ash.create!(AshAdmin.Test.Blog, %{body: "Apple pie"}) + Ash.create!(AshAdmin.Test.Blog, %{body: "Banana bread"}) + + %{conn: build_conn()} + end + + test "the search box filters rows on the searchable columns", %{conn: conn} do + {:ok, view, _html} = live(conn, "/api/admin?domain=Domain&resource=Blog&action_type=read") + + html = render_async(view) + assert html =~ ~s(name="search") + assert html =~ "Apple pie" + assert html =~ "Banana bread" + + # No filterable columns, so there is no Filters button + refute has_element?(view, "button[phx-click=toggle_filters]") + + view + |> element("form[phx-change=filter_change]") + |> render_change(%{"search" => "banana"}) + + html = render_async(view) + refute html =~ "Apple pie" + assert html =~ "Banana bread" + end + + test "a table without searchable columns has no search box", %{conn: conn} do + {:ok, view, _html} = live(conn, "/api/admin?domain=Domain&resource=Post&action_type=read") + + refute render_async(view) =~ ~s(name="search") + end +end diff --git a/test/support/resources/blog.ex b/test/support/resources/blog.ex index b287a5d7..ee10b319 100644 --- a/test/support/resources/blog.ex +++ b/test/support/resources/blog.ex @@ -18,7 +18,13 @@ defmodule AshAdmin.Test.Blog do end end + actions do + defaults([:read, create: [:body]]) + end + admin do resource_group(:group_b) + table_searchable_columns([:body]) + table_filterable_columns([]) end end From 87f2dc7ba6f35e712465f0e54a2a04f3c30d2d25 Mon Sep 17 00:00:00 2001 From: Etienne Lacoursiere Date: Tue, 3 Nov 2026 11:49:18 -0500 Subject: [PATCH 2/3] feat: hide the table search with the filters The search box now shows with the filters when the Filters button is on. The button also shows when a resource has only searchable columns. Co-Authored-By: Claude Opus 5.5 --- .../components/resource/data_table.ex | 24 ++----------------- .../resource/data_table_search_test.exs | 20 ++++++++++++---- 2 files changed, 18 insertions(+), 26 deletions(-) diff --git a/lib/ash_admin/components/resource/data_table.ex b/lib/ash_admin/components/resource/data_table.ex index cf72fa43..08b295bf 100644 --- a/lib/ash_admin/components/resource/data_table.ex +++ b/lib/ash_admin/components/resource/data_table.ex @@ -85,7 +85,7 @@ defmodule AshAdmin.Components.Resource.DataTable do >
-
+