Collection Views
Collection Views
Covers the collection view feature: querying pages by type (query_pages_by_type), persisting view preferences
(save_collection_view, get_collection_view), switching between Table, List, and Gallery display modes, configuring
column visibility, and verifying that property values appear correctly in query results. This spec is P2 because
collection views are a derived query surface over the type and property systems — they are important for the power-user
structured-data workflow but do not directly gate data integrity.
The collection view system consists of two domain types: CollectionView (persisted view preferences — mode, column
config, keyed by type_slug) and CollectionViewRow (query result — page metadata plus a
HashMap<String, serde_json::Value> of property values from frontmatter). Three Tauri commands are tested:
query_pages_by_type, get_collection_view, and save_collection_view. The default ViewMode is Table.
Preconditions
- HTTP bridge running on port 9990
- A workspace initialized via
initialize_workspacebefore each scenario - Bridge shim injected via
playwright.config.ts - At least one user-defined type and several pages assigned to that type must be created before collection queries can return meaningful data
Scenarios
Seed: seed.spec.ts
1. Query pages by type in Table mode (default)
query_pages_by_type returns all pages assigned to the given type as CollectionViewRow records ordered by title.
Steps:
- Call
create_typewithname = "Character". Capturetype_id. - Call
create_pagewithtitle = "Aria". Assign the Character type viaassign_type_to_page. - Call
create_pagewithtitle = "Brann". Assign the Character type. - Call
create_pagewithtitle = "Celeste". Assign the Character type. - Call
query_pages_by_typewithtype_slug = "character",offset = 0,limit = 50.
Expected: The returned array contains exactly 3 entries. Titles are "Aria", "Brann", "Celeste" (ordered
alphabetically by title, ascending). Each row has page_id, slug, title, page_type, and a properties map (may
be empty if no frontmatter is set).
2. Collection view rows include property values from frontmatter
When a page assigned to a type has frontmatter values for properties linked to that type, those values appear in
CollectionViewRow.properties.
Steps:
- Call
create_typewithname = "Creature". Capturetype_id. - Call
create_propertywithname = "CR",value_type = "number". Captureproperty_id. - Call
add_property_to_typewithtype_idandproperty_id. - Call
create_pagewithtitle = "Owlbear". Capturepage_id. - Call
assign_type_to_pagewithpage_idandtype_id. - Call
set_property_valuewithpage_id,property_slug = "cr",value = 3. - Call
query_pages_by_typewithtype_slug = "creature",offset = 0,limit = 50.
Expected: The result contains one row with title = "Owlbear". The properties map on that row contains the key
"cr" with value 3 (JSON number). Other pages in the workspace that are not assigned the Creature type do not appear
in the result.
3. Empty collection — no pages assigned to the type
Querying a type that has no pages assigned to it returns an empty array, not an error.
Steps:
- Call
create_typewithname = "Myth". - Call
query_pages_by_typewithtype_slug = "myth",offset = 0,limit = 50.
Expected: The call succeeds and returns an empty array []. No error is thrown. The workspace is unaffected.
4. Pagination — offset and limit are respected
query_pages_by_type applies offset and limit at the database level.
Steps:
- Call
create_typewithname = "Faction". Capturetype_id. - Create 5 pages titled “Faction 1” through “Faction 5”. Assign the Faction type to each.
- Call
query_pages_by_typewithtype_slug = "faction",offset = 0,limit = 2. Capture result A. - Call
query_pages_by_typewithtype_slug = "faction",offset = 2,limit = 2. Capture result B. - Call
query_pages_by_typewithtype_slug = "faction",offset = 4,limit = 2. Capture result C.
Expected: Result A contains 2 rows. Result B contains 2 rows (different from A). Result C contains 1 row (the last page). The union of all rows across A, B, and C equals the full set of 5 faction pages with no duplicates.
5. Save and retrieve a Table-mode collection view config
save_collection_view persists mode and column configuration. get_collection_view retrieves it.
Steps:
- Call
create_typewithname = "Item". - Call
create_propertywithname = "Weight",value_type = "number". Captureproperty_id. - Construct a
CollectionViewobject withtype_slug = "item",view_mode = "table", andcolumn_config = [{ property_id, visible: true, width: 150, sort_order: 0 }]. - Call
save_collection_viewwith the constructed view. - Call
get_collection_viewwithtype_slug = "item".
Expected: get_collection_view returns the saved CollectionView with view_mode = "table" and column_config
containing one entry for the Weight property. The type_slug, visible, width, and sort_order fields all match
what was saved.
6. Switch collection view to Gallery mode
Saving a view with view_mode = "gallery" replaces the previous configuration for that type.
Steps:
- Call
create_typewithname = "Artwork". - Call
save_collection_viewwithtype_slug = "artwork",view_mode = "table", emptycolumn_config. - Construct a new
CollectionViewwith the sametype_slug,view_mode = "gallery", emptycolumn_config. - Call
save_collection_viewwith the new view. - Call
get_collection_viewwithtype_slug = "artwork".
Expected: get_collection_view returns view_mode = "gallery". The previous “table” config is replaced. No
duplicate entries exist for “artwork” in the view storage.
7. Switch collection view to List mode
A view can be saved with view_mode = "list" and retrieved correctly.
Steps:
- Call
create_typewithname = "Note". - Call
save_collection_viewwithtype_slug = "note",view_mode = "list", emptycolumn_config. - Call
get_collection_viewwithtype_slug = "note".
Expected: get_collection_view returns a CollectionView with view_mode = "list". The type_slug matches
"note".
8. Get collection view for a type with no saved config returns null
Before any save_collection_view call, get_collection_view returns null (not an error).
Steps:
- Call
create_typewithname = "Relic". - Call
get_collection_viewwithtype_slug = "relic".
Expected: The call returns null (JSON null). No error is thrown. The absence of a saved config is the expected
initial state.
9. Column visibility configuration — hidden column is still persisted
A column can be marked as not visible (visible: false) and the configuration round-trips correctly.
Steps:
- Call
create_typewithname = "Quest". - Call
create_propertywithname = "Difficulty",value_type = "select". Captureprop_id_a. - Call
create_propertywithname = "Reward",value_type = "text". Captureprop_id_b. - Construct a
CollectionViewwithtype_slug = "quest",view_mode = "table", andcolumn_config:{ property_id: prop_id_a, visible: true, sort_order: 0 }{ property_id: prop_id_b, visible: false, sort_order: 1 }
- Call
save_collection_viewwith this config. - Call
get_collection_viewwithtype_slug = "quest".
Expected: The returned config contains two column entries. The Difficulty column has visible = true and
sort_order = 0. The Reward column has visible = false and sort_order = 1. Both are persisted even though one is
hidden.
10. Collection view rows for multiple types are isolated
Querying one type’s pages does not include pages assigned only to a different type.
Steps:
- Call
create_typewithname = "Hero". Capturehero_type_id. - Call
create_typewithname = "Villain". Capturevillain_type_id. - Call
create_pagewithtitle = "Knight". Assign the Hero type. - Call
create_pagewithtitle = "Sorcerer". Assign the Villain type. - Call
create_pagewithtitle = "Dual Role". Assign both the Hero and Villain types. - Call
query_pages_by_typewithtype_slug = "hero",offset = 0,limit = 50. - Call
query_pages_by_typewithtype_slug = "villain",offset = 0,limit = 50.
Expected: The Hero query returns 2 rows: “Knight” and “Dual Role”. The Villain query returns 2 rows: “Sorcerer” and “Dual Role”. “Knight” does not appear in the Villain results and “Sorcerer” does not appear in the Hero results.
11. Collection view config persists across workspace sessions
A saved view configuration survives workspace close and re-open.
Steps:
- Call
create_typewithname = "Chronicle". - Call
save_collection_viewwithtype_slug = "chronicle",view_mode = "gallery", emptycolumn_config. - Call
close_workspace(or equivalent restart). - Re-open the same workspace.
- Call
get_collection_viewwithtype_slug = "chronicle".
Expected: The returned CollectionView has view_mode = "gallery" and type_slug = "chronicle". The configuration
persisted across the session boundary without data loss.
12. Querying with an unknown type slug returns an empty array
A type slug that does not match any registered type returns no rows rather than an error.
Steps:
- Call
query_pages_by_typewithtype_slug = "nonexistent-type",offset = 0,limit = 50.
Expected: The call returns an empty array. No error is thrown. This behavior is consistent with querying a type that exists but has no assigned pages.
Test Data
| Key | Value | Notes |
|---|---|---|
| view_modes | table, list, gallery | All valid ViewMode variants; table is the default |
| default_offset | 0 | Standard starting offset for first-page queries |
| default_limit | 50 | Typical page size; should be large enough for most test scenarios |
| min_limit | 1 | Minimum limit for pagination boundary testing |
| type_slug_character | character | Derived slug for “Character” type (used in multi-scenario setup) |
| view_mode_table | table | Serializes as "table" in JSON (snake_case) |
| view_mode_gallery | gallery | Serializes as "gallery" in JSON |
| view_mode_list | list | Serializes as "list" in JSON |
Notes
query_pages_by_typeorders results bypages.title ASCat the database level. Test data page titles should be chosen to make alphabetical order predictable and testable.CollectionViewRow.propertiesis a flatHashMap<String, serde_json::Value>keyed by property slug, not property name. Tests that assert on property values must use the slug (e.g.,"cr"not"CR").save_collection_viewperforms an insert-or-replace (upsert) keyed ontype_slug. The previous config for a type is replaced entirely — there is no merge. Column configs that are omitted in a re-save are removed, not preserved.get_collection_viewreturnsnull(Option::None) when no config has been saved for the type slug, not a NotFound error. Test assertions must distinguish betweennulland an error response.SaveCollectionViewUseCaserequiresTypesWritecapability.QueryPagesByTypeUseCaserequires onlyPagesRead. Tests must ensure the bridge uses an owner guard (which has all capabilities).- The
column_configarray in aCollectionViewis only meaningful forTablemode. ForListandGallerymodes the column configuration is ignored by the UI but still round-trips through storage — saving non-emptycolumn_configwith a non-table mode is valid and is persisted. - The property values in
CollectionViewRow.propertiescome from thefrontmattertable. Pages that have been assigned a type but have not hadset_property_valuecalled will have an empty or partialpropertiesmap. This is expected — the query is non-blocking and does not require a value to be set. query_pages_by_typewithtype_slug = "page"ortype_slug = "folder"is valid and queries the system types. In a freshly initialized workspace this will return all pages or all folder pages respectively.
Was this page helpful?
Thanks for your feedback!