Skip to content
Documentation GitHub
Workspace

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_workspace before 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:

  1. Call create_type with name = "Character". Capture type_id.
  2. Call create_page with title = "Aria". Assign the Character type via assign_type_to_page.
  3. Call create_page with title = "Brann". Assign the Character type.
  4. Call create_page with title = "Celeste". Assign the Character type.
  5. Call query_pages_by_type with type_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:

  1. Call create_type with name = "Creature". Capture type_id.
  2. Call create_property with name = "CR", value_type = "number". Capture property_id.
  3. Call add_property_to_type with type_id and property_id.
  4. Call create_page with title = "Owlbear". Capture page_id.
  5. Call assign_type_to_page with page_id and type_id.
  6. Call set_property_value with page_id, property_slug = "cr", value = 3.
  7. Call query_pages_by_type with type_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:

  1. Call create_type with name = "Myth".
  2. Call query_pages_by_type with type_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:

  1. Call create_type with name = "Faction". Capture type_id.
  2. Create 5 pages titled “Faction 1” through “Faction 5”. Assign the Faction type to each.
  3. Call query_pages_by_type with type_slug = "faction", offset = 0, limit = 2. Capture result A.
  4. Call query_pages_by_type with type_slug = "faction", offset = 2, limit = 2. Capture result B.
  5. Call query_pages_by_type with type_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:

  1. Call create_type with name = "Item".
  2. Call create_property with name = "Weight", value_type = "number". Capture property_id.
  3. Construct a CollectionView object with type_slug = "item", view_mode = "table", and column_config = [{ property_id, visible: true, width: 150, sort_order: 0 }].
  4. Call save_collection_view with the constructed view.
  5. Call get_collection_view with type_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.

Saving a view with view_mode = "gallery" replaces the previous configuration for that type.

Steps:

  1. Call create_type with name = "Artwork".
  2. Call save_collection_view with type_slug = "artwork", view_mode = "table", empty column_config.
  3. Construct a new CollectionView with the same type_slug, view_mode = "gallery", empty column_config.
  4. Call save_collection_view with the new view.
  5. Call get_collection_view with type_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:

  1. Call create_type with name = "Note".
  2. Call save_collection_view with type_slug = "note", view_mode = "list", empty column_config.
  3. Call get_collection_view with type_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:

  1. Call create_type with name = "Relic".
  2. Call get_collection_view with type_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:

  1. Call create_type with name = "Quest".
  2. Call create_property with name = "Difficulty", value_type = "select". Capture prop_id_a.
  3. Call create_property with name = "Reward", value_type = "text". Capture prop_id_b.
  4. Construct a CollectionView with type_slug = "quest", view_mode = "table", and column_config:
    • { property_id: prop_id_a, visible: true, sort_order: 0 }
    • { property_id: prop_id_b, visible: false, sort_order: 1 }
  5. Call save_collection_view with this config.
  6. Call get_collection_view with type_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:

  1. Call create_type with name = "Hero". Capture hero_type_id.
  2. Call create_type with name = "Villain". Capture villain_type_id.
  3. Call create_page with title = "Knight". Assign the Hero type.
  4. Call create_page with title = "Sorcerer". Assign the Villain type.
  5. Call create_page with title = "Dual Role". Assign both the Hero and Villain types.
  6. Call query_pages_by_type with type_slug = "hero", offset = 0, limit = 50.
  7. Call query_pages_by_type with type_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:

  1. Call create_type with name = "Chronicle".
  2. Call save_collection_view with type_slug = "chronicle", view_mode = "gallery", empty column_config.
  3. Call close_workspace (or equivalent restart).
  4. Re-open the same workspace.
  5. Call get_collection_view with type_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:

  1. Call query_pages_by_type with type_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

KeyValueNotes
view_modestable, list, galleryAll valid ViewMode variants; table is the default
default_offset0Standard starting offset for first-page queries
default_limit50Typical page size; should be large enough for most test scenarios
min_limit1Minimum limit for pagination boundary testing
type_slug_charactercharacterDerived slug for “Character” type (used in multi-scenario setup)
view_mode_tabletableSerializes as "table" in JSON (snake_case)
view_mode_gallerygallerySerializes as "gallery" in JSON
view_mode_listlistSerializes as "list" in JSON

Notes

  • query_pages_by_type orders results by pages.title ASC at the database level. Test data page titles should be chosen to make alphabetical order predictable and testable.
  • CollectionViewRow.properties is a flat HashMap<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_view performs an insert-or-replace (upsert) keyed on type_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_view returns null (Option::None) when no config has been saved for the type slug, not a NotFound error. Test assertions must distinguish between null and an error response.
  • SaveCollectionViewUseCase requires TypesWrite capability. QueryPagesByTypeUseCase requires only PagesRead. Tests must ensure the bridge uses an owner guard (which has all capabilities).
  • The column_config array in a CollectionView is only meaningful for Table mode. For List and Gallery modes the column configuration is ignored by the UI but still round-trips through storage — saving non-empty column_config with a non-table mode is valid and is persisted.
  • The property values in CollectionViewRow.properties come from the frontmatter table. Pages that have been assigned a type but have not had set_property_value called will have an empty or partial properties map. This is expected — the query is non-blocking and does not require a value to be set.
  • query_pages_by_type with type_slug = "page" or type_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?