Skip to content

Implement Row Seeker within Forms #4637

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Draft
wants to merge 58 commits into
base: form_maker
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
58 commits
Select commit Hold shift + click to select a range
8554902
Use a dropdown view for record selection
pavish Jun 13, 2025
4808acb
Merge branch 'develop' of https://github.com/mathesar-foundation/math…
pavish Jun 13, 2025
6d21860
Implement basic filtering
pavish Jun 13, 2025
e947517
Implement text search over summary
pavish Jun 16, 2025
88a0595
Merge branch 'develop' of https://github.com/mathesar-foundation/math…
pavish Jun 16, 2025
e345f06
Auto focus selected record
pavish Jun 17, 2025
c7c21b1
Add footer and pagination
pavish Jun 17, 2025
76b43f1
Add UX flow for new filter from drilldown menu
pavish Jun 17, 2025
539f79c
Show summary in the filter tags
pavish Jun 17, 2025
23498ab
Make basic text filtering possible
pavish Jun 17, 2025
dedce5c
Use row seeker in record selector input
pavish Jun 17, 2025
575b88e
Add list_by_record_summaries function defn to all mathesar objects table
pavish Jun 17, 2025
4e82ae1
Fix linting and remove expansion in basic mode
pavish Jun 18, 2025
4b4a68b
Switch mode back to complete
pavish Jun 18, 2025
fe16d66
Merge branch 'develop' into forms_minimal_1
seancolsen Jul 15, 2025
cc69869
Fix type errors
seancolsen Jul 15, 2025
d9ef86c
Remove row seeker features, clean up code
seancolsen Jul 15, 2025
fdda7d0
Merge branch 'form_maker' into row_seeker
seancolsen Jul 15, 2025
a220e05
WIP
seancolsen Jul 16, 2025
1f95968
Truncate row seeker result record summaries
seancolsen Jul 16, 2025
6673c51
Improve width of row seeker dropdown
seancolsen Jul 16, 2025
12b9aa8
Auto reposition row seeker as size changes
seancolsen Jul 16, 2025
e21fc83
Improve row seeker footer styling
seancolsen Jul 16, 2025
a2d4a0c
Improve row seeker loading state
seancolsen Jul 16, 2025
546f04a
Improve row seeker empty state
seancolsen Jul 16, 2025
b8ee807
Improve UX for indicating selection in row seeker
seancolsen Jul 16, 2025
04130b5
Merge branch 'form_maker' into row_seeker
seancolsen Jul 22, 2025
d707da9
Merge branch 'form_maker' into row_seeker
seancolsen Jul 22, 2025
ffc3d4c
Simplify list_by_record_summaries DB function
seancolsen Jul 23, 2025
1e3f43c
Move row seeker API to forms RPC namespace
seancolsen Jul 23, 2025
4b2db80
Adapt front end API types to forms API changes
seancolsen Jul 23, 2025
f549593
Revert prototyping changes to LinkedRecordCell
seancolsen Jul 23, 2025
47f6a1a
Begin transitioning row seeker to forms context
seancolsen Jul 23, 2025
17342f3
Continue transitioning row seeker to forms context
seancolsen Jul 23, 2025
7f24908
Use a factory to fix problem with context
seancolsen Jul 24, 2025
1fd4fd7
Fix race condition when opening nested selector
seancolsen Jul 24, 2025
a901b97
Merge branch 'form_maker' into row_seeker
seancolsen Jul 24, 2025
9cf8f2e
Linting
seancolsen Jul 24, 2025
9e1bb5b
Merge branch 'form_maker' into row_seeker
seancolsen Jul 24, 2025
79e8b16
Merge branch 'form_maker' into row_seeker
seancolsen Jul 24, 2025
b7dd595
Connect formToken and fieldKey to row seeker
seancolsen Jul 24, 2025
9fa2cc9
Make some properties reactive
seancolsen Jul 24, 2025
d1d5a40
Linting
seancolsen Jul 24, 2025
d8f6798
Fix value not being set in linked record input
seancolsen Jul 24, 2025
e6c802a
Display record summary in form input
seancolsen Jul 24, 2025
64faec4
Show previous value in row seeker
seancolsen Jul 24, 2025
64d462e
Re-open row seeker if value is cleared while open
seancolsen Jul 24, 2025
51a612e
Improve code docs
seancolsen Jul 24, 2025
6c52576
Linting
seancolsen Jul 24, 2025
15e7d8c
Fix JSON quoting in SQL tests
seancolsen Jul 24, 2025
36cc245
Improve handling of empty results
seancolsen Jul 24, 2025
0c03df5
Add SQL tests
seancolsen Jul 25, 2025
5b9e91e
Run prettier
seancolsen Jul 25, 2025
45447e5
Fix failing db_wrapper test
seancolsen Jul 25, 2025
063c368
Fix test_rpc_endpoint_expected_methods test
seancolsen Jul 25, 2025
368380c
Document new API
seancolsen Jul 25, 2025
9050fc2
Merge branch 'form_maker' into row_seeker
seancolsen Jul 25, 2025
4a8244c
Linting
seancolsen Jul 25, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 20 additions & 0 deletions db/records.py
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,26 @@ def search_records_from_table(
return result


def list_by_record_summaries(
conn,
table_oid,
limit=500,
offset=0,
search=None,
table_record_summary_templates=None,
):
result = db_conn.exec_msar_func(
conn,
'list_by_record_summaries',
table_oid,
limit,
offset,
search,
_json_or_none(table_record_summary_templates),
).fetchone()[0]
return result


def delete_records_from_table(conn, record_ids, table_oid):
"""
Delete records from table by id.
Expand Down
3 changes: 3 additions & 0 deletions db/sql/00_msar_all_objects_table.sql
Original file line number Diff line number Diff line change
Expand Up @@ -1143,6 +1143,9 @@ INSERT INTO msar.all_mathesar_objects VALUES
('msar', 'msar.list_column_privileges_for_current_role(regclass,smallint)', 'FUNCTION', NULL),
('msar', 'msar.list_database_privileges_for_current_role(oid)', 'FUNCTION', NULL),
('msar', 'msar.list_db_priv()', 'FUNCTION', NULL),
('msar', 'msar.list_by_record_summaries(oid,integer,integer,jsonb,text,jsonb,boolean,boolean)', 'FUNCTION', NULL),
('msar', 'msar.list_by_record_summaries(oid,integer,integer,text,jsonb,boolean)', 'FUNCTION', NULL),
('msar', 'msar.list_by_record_summaries(oid,integer,integer,text,jsonb)', 'FUNCTION', NULL),
('msar', 'msar.list_records_from_table(oid,integer,integer,jsonb,jsonb,jsonb,boolean)', 'FUNCTION', NULL),
('msar', 'msar.list_records_from_table(oid,integer,integer,jsonb,jsonb,jsonb,boolean,jsonb)', 'FUNCTION', NULL),
('msar', 'msar.list_roles()', 'FUNCTION', NULL),
Expand Down
43 changes: 43 additions & 0 deletions db/sql/05_msar.sql
Original file line number Diff line number Diff line change
Expand Up @@ -5627,6 +5627,49 @@ END;
$$ LANGUAGE plpgsql;


CREATE OR REPLACE FUNCTION msar.list_by_record_summaries(
tab_id oid,
limit_ integer,
offset_ integer,
search_ text DEFAULT NULL,
table_record_summary_templates jsonb DEFAULT NULL
) RETURNS jsonb
LANGUAGE plpgsql STABLE
AS $$
DECLARE
search_where_clause text := '';
final_sql text;
result_json jsonb;
BEGIN
IF search_ IS NOT NULL AND search_ <> '' THEN
search_where_clause := format(' WHERE summary ILIKE %L', '%'||search_||'%');
END IF;

final_sql := format(
$q$
WITH
all_record_summaries AS ( %1$s ),
filtered AS ( SELECT * FROM all_record_summaries %2$s ),
sorted AS ( SELECT * FROM filtered ORDER BY summary LIMIT %3$s OFFSET %4$s )
SELECT
json_build_object(
'count', (SELECT count(*) FROM filtered),
'results', coalesce(json_agg(sorted), '[]')
)
FROM sorted
$q$,
/* 1 */ msar.build_record_summary_query_for_table(tab_id, NULL, table_record_summary_templates),
/* 2 */ search_where_clause,
/* 3 */ limit_,
/* 4 */ offset_
);

EXECUTE final_sql INTO result_json;
RETURN result_json;
END;
$$;


CREATE OR REPLACE FUNCTION
msar.get_tab_col_info_map(tab_col_map jsonb)
RETURNS jsonb AS $$/*
Expand Down
96 changes: 90 additions & 6 deletions db/sql/test_sql_functions.sql
Original file line number Diff line number Diff line change
Expand Up @@ -4889,11 +4889,11 @@ BEGIN
rel_id := 'atable'::regclass::oid;
RETURN NEXT is(
msar.patch_record_in_table( rel_id, 2, '{"2": 10}'),
$p${
'{
"results": [{"1": 2, "2": 10, "3": "sdflfflsk", "4": null, "5": "[1, 2, 3, 4]"}],
"linked_record_summaries": null,
"record_summaries": null
}$p$
}'
);
END;
$$ LANGUAGE plpgsql;
Expand All @@ -4907,11 +4907,11 @@ BEGIN
rel_id := 'atable'::regclass::oid;
RETURN NEXT is(
msar.patch_record_in_table( rel_id, '2', '{"2": 10}'),
$p${
'{
"results": [{"1": 2, "2": 10, "3": "sdflfflsk", "4": null, "5": "[1, 2, 3, 4]"}],
"linked_record_summaries": null,
"record_summaries": null
}$p$
}'
);
END;
$$ LANGUAGE plpgsql;
Expand All @@ -4925,11 +4925,11 @@ BEGIN
rel_id := 'atable'::regclass::oid;
RETURN NEXT is(
msar.patch_record_in_table( rel_id, 2, '{"2": 10, "4": {"a": "json"}}'),
$p${
'{
"results": [{"1": 2, "2": 10, "3": "sdflfflsk", "4": "{\"a\": \"json\"}", "5": "[1, 2, 3, 4]"}],
"linked_record_summaries": null,
"record_summaries": null
}$p$
}'
);
END;
$$ LANGUAGE plpgsql;
Expand Down Expand Up @@ -6788,3 +6788,87 @@ BEGIN
);
END;
$$ LANGUAGE plpgsql;

-- msar.list_by_record_summaries --------------------------------------------------------------------

CREATE OR REPLACE FUNCTION test_list_by_record_summaries()
RETURNS SETOF TEXT AS $$
BEGIN
CREATE TABLE vehicles (
id int primary key,
name text,
wheel_count int
);

-- Empty table behavior
RETURN NEXT is(
msar.list_by_record_summaries('vehicles'::regclass, 10, 0),
'{"count": 0, "results": []}'
);

INSERT INTO vehicles VALUES
(1, 'Car', 4),
(2, 'Truck', 4),
(3, 'Unicycle', 1),
(4, 'Bicycle', 2),
(5, 'Tricycle', 3),
(6, 'Boat', 0),
(7, 'Semi', 18),
(8, 'Airplane', 10);

-- Basic test
RETURN NEXT is(
msar.list_by_record_summaries('vehicles'::regclass, 2, 0),
'{
"count": 8,
"results": [
{"key": 8, "summary": "Airplane"},
{"key": 4, "summary": "Bicycle"}
]
}'
);

-- Pagination
RETURN NEXT is(
msar.list_by_record_summaries('vehicles'::regclass, 3, 3),
'{
"count": 8,
"results": [
{"key": 1, "summary": "Car"},
{"key": 7, "summary": "Semi"},
{"key": 5, "summary": "Tricycle"}
]
}'
);

-- Search query
RETURN NEXT is(
msar.list_by_record_summaries('vehicles'::regclass, 2, 0, 'cycle'),
'{
"count": 3,
"results": [
{"key": 4, "summary": "Bicycle"},
{"key": 5, "summary": "Tricycle"}
]
}'
);

-- Empty search query
RETURN NEXT is(
msar.list_by_record_summaries('vehicles'::regclass, 2, 0, 'NOPE'),
'{"count": 0, "results": []}'
);

-- Search in custom record summary template
RETURN NEXT is(
msar.list_by_record_summaries(
'vehicles'::regclass,
2,
0,
'18 wheels',
format('{ "%s": [ [2], " with ", [3], " wheels" ] }', 'vehicles'::regclass::oid)::jsonb
),
'{"count": 1, "results": [{"key": 7, "summary": "Semi with 18 wheels"}]}'
);
END;
$$ LANGUAGE plpgsql;
10 changes: 9 additions & 1 deletion db/tests/wrapper/test_exec_wrappers.py
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,15 @@ def visit_Call(self, node):

@pytest.mark.parametrize("_,exec_sql_func_name,exec_sql_arg_count", find_exec_calls_in_project("db/"))
def test_db_wrapper(get_msar_func_names, _, exec_sql_func_name, exec_sql_arg_count):
"""Tests to make sure every SQL function is correctly wired up."""
"""
Tests to make sure every SQL function is correctly wired up.

If this test is failing, compare the number of arguments in your function
definitions across the following code locations:

- The source code of the function, as defined in the DB layer
- The python wrapper function that calls that DB layer function
"""
if exec_sql_func_name != 'drop_all_msar_objects':
assert exec_sql_func_name in get_msar_func_names.keys()
assert exec_sql_arg_count in get_msar_func_names[exec_sql_func_name]
Expand Down
3 changes: 3 additions & 0 deletions docs/docs/api/methods.md
Original file line number Diff line number Diff line change
Expand Up @@ -146,11 +146,14 @@
- add
- delete
- replace
- list_related_records
- FormInfo
- FieldInfo
- AddFormDef
- AddOrReplaceFieldDef
- ReplaceableFormDef
- ListRelatedRecordsResponse
- SummarizedRecordReference

## Records

Expand Down
90 changes: 88 additions & 2 deletions mathesar/rpc/forms.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,23 @@
"""
Classes and functions exposed to the RPC endpoint for managing forms.
"""
from typing import Optional, TypedDict, Literal
from typing import Optional, TypedDict, Literal, Any

from modernrpc.core import REQUEST_KEY

from db.records import list_by_record_summaries

from mathesar.rpc.decorators import mathesar_rpc_method
from mathesar.utils.forms import create_form, get_form, list_forms, delete_form, replace_form, get_form_source_info
from mathesar.utils.forms import (
create_form,
delete_form,
get_form_by_token,
get_form_source_info,
get_form,
list_forms,
replace_form,
)
from mathesar.utils.tables import get_table_record_summary_templates


class FieldInfo(TypedDict):
Expand Down Expand Up @@ -220,6 +231,37 @@ class ReplaceableFormDef(AddFormDef):
id: int


class SummarizedRecordReference(TypedDict):
"""
A summarized reference to a record, typically used in foreign key fields.

Attributes:
key: A unique identifier for the record.
summary: The record summary
"""
key: Any
summary: str


class ListRelatedRecordsResponse(TypedDict):
"""
Response for listing related records for a foreign key field.

Attributes:
count: The total number of records matching the criteria.
results: A list of summarized record references, each containing a key and a summary.
"""
count: int
results: list[SummarizedRecordReference]

@classmethod
def from_dict(cls, d):
return cls(
count=d["count"],
results=d["results"],
)


@mathesar_rpc_method(name="forms.add", auth="login")
def add(*, form_def: AddFormDef, **kwargs) -> FormInfo:
"""
Expand Down Expand Up @@ -309,3 +351,47 @@ def replace(*, new_form: ReplaceableFormDef, **kwargs) -> FormInfo:
user = kwargs.get(REQUEST_KEY).user
form_model = replace_form(new_form, user)
return FormInfo.from_model(form_model)


@mathesar_rpc_method(name="forms.list_related_records", auth="anonymous")
def list_related_records(
*,
form_token: str,
field_key: str,
limit: Optional[int] = None,
offset: Optional[int] = None,
search: Optional[str] = None,
**kwargs,
) -> ListRelatedRecordsResponse:
"""
List records for selection via the row seeker

Args:
form_token: The unique token of the form.
field_key: The key of the foreign key field for which to list related records.
limit: Optional limit on the number of records to return.
offset: Optional offset for pagination.
search: Optional search term to filter records.

Returns:
The requested records, along with some metadata.
"""

form = get_form_by_token(form_token)
database_id = form.database.id
form_field = form.fields.get(key=field_key)
if form_field.kind != "foreign_key":
raise ValueError(f"Field {field_key} is not a foreign key field.")

table_oid = form_field.related_table_oid

with form.connection as conn:
record_info = list_by_record_summaries(
conn,
table_oid,
limit=limit,
offset=offset,
search=search,
table_record_summary_templates=get_table_record_summary_templates(database_id),
)
return ListRelatedRecordsResponse.from_dict(record_info)
5 changes: 5 additions & 0 deletions mathesar/tests/rpc/test_endpoints.py
Original file line number Diff line number Diff line change
Expand Up @@ -283,6 +283,11 @@
"forms.replace",
[user_is_authenticated]
),
(
forms.list_related_records,
"forms.list_related_records",
[]
),
(
records.add,
"records.add",
Expand Down
4 changes: 4 additions & 0 deletions mathesar/utils/forms.py
Original file line number Diff line number Diff line change
Expand Up @@ -128,6 +128,10 @@ def get_form(form_id):
return form_model


def get_form_by_token(form_token):
return Form.objects.get(token=form_token)


def get_form_source_info(form_token):
form_model = Form.objects.get(token=form_token)
if form_model:
Expand Down
Loading
Loading