Landing-Page Filter Configurations¶
Date: 2026-06-03
Problem / motivation¶
The frontend already sends rich filter payloads to every landing (list/table) page — see Landing-Page Filter Groups (OR/AND) and Landing-Page Array/Tag Filters. Until now there was no way to save and reuse those payloads. This feature persists a named, described filter configuration (stored opaquely as JSONB) tied to a specific landing page, so the frontend can offer reusable "saved views" per list page. Configurations are tenant-shared: any user in the tenant can list and apply them.
In scope¶
- A new
landing_page_configstable + module (app/graphql/landing_page_configs/). - A
LandingPageenum with one member per backend landing page. - Tenant-shared CRUD: create, update, list (optionally filtered by landing page), get-by-id, delete.
Out of scope¶
- Per-user privacy / sharing toggles (configs are tenant-shared by design).
- Restricting edit/delete to the creator (any user with
SETTINGSaccess manages them). - Validation of the
configJSON shape — it is stored opaquely; the frontend owns it.
Data model changes¶
New table landing_page_configs (alembic revision add_landing_page_configs,
down_revision = add_inv_log_source_type):
| column | type | notes |
|---|---|---|
id |
UUID | PK, gen_random_uuid() |
landing_page |
SMALLINT | LandingPage enum value; indexed (landing_page_idx) |
name |
VARCHAR(255) | display name |
description |
TEXT | nullable |
config |
JSONB | opaque frontend filter payload |
created_by_id |
UUID | FK → users.id (audit only) |
created_at |
TIMESTAMPTZ | now() |
updated_at |
TIMESTAMPTZ | now(), bumped on update |
Model: LandingPageConfig.
What's being implemented¶
- Model —
app/graphql/landing_page_configs/models/landing_page_config.py(CoreBaseModel, HasPrimaryKey, HasCreatedAt, HasCreatedBy). - Enum —
app/graphql/landing_page_configs/strawberry/landing_page.py(LandingPage, a@strawberry.enumIntEnum stored viaIntEnum(LandingPage)). - Input —
.../strawberry/landing_page_config_input.py: a singleLandingPageConfigInput(BaseInputGQL[LandingPageConfig])shared by create and update, withto_orm_model()(the quotes/orders convention). - Response —
.../strawberry/landing_page_config_response.py(LandingPageConfigResponse(DTOMixin)). - Repository —
.../repositories/landing_page_config_repository.py(AbstractRepository[LandingPageConfigInput, LandingPageConfig]): inheritedaddfor create, anupdatethat loads +merges, andlist_configs(landing_page). - Service —
.../services/landing_page_config_service.py. - Queries —
.../queries/landing_page_config_queries.py(ungated reads). - Mutations —
.../mutations/landing_page_config_mutations.py(gated onPath.SETTINGS). - Migration —
alembic/versions/20260603_add_landing_page_configs.py. - Model registration — added to
load_models.py. - Tests —
tests/graphql/landing_page_configs/(DB-backed repository test + mocked service test).
GraphQL surface¶
enum LandingPage { INVOICES SUPPLIER_INVOICES QUOTES ORDERS RECEIPTS PAYMENTS
CREDITS SUPPLIER_CREDITS CLIENTS SUPPLIERS SUPPLIER_QUOTES SUPPLIER_CATEGORIES
PURCHASE_ORDERS PURCHASE_REQUISITIONS EXPENSES EXPENSE_CATEGORIES ACCOUNTS JOURNALS
PROJECTS ITEMS ITEM_CATEGORIES TAGS INVENTORY_LOGS USERS SALESPERSONS FILES
APPOINTMENTS HEALTH_INSTITUTIONS APPOINTMENT_PAYMENTS }
type LandingPageConfig {
id: UUID!
landingPage: LandingPage!
name: String!
description: String
config: JSON!
createdById: UUID!
createdAt: datetime!
updatedAt: datetime!
}
input LandingPageConfigInput {
id: UUID = null # null = create; set = update
landingPage: LandingPage!
name: String!
config: JSON!
description: String
}
# Queries
landingPageConfigs(landingPage: LandingPage = null): [LandingPageConfig!]!
landingPageConfig(id: UUID!): LandingPageConfig
# Mutations (require SETTINGS path permission)
createLandingPageConfig(config: LandingPageConfigInput!): LandingPageConfig!
updateLandingPageConfig(config: LandingPageConfigInput!): LandingPageConfig!
deleteLandingPageConfig(id: UUID!): Boolean!
RBAC¶
No new Path/Resource enum member and no backfill. Reads (landingPageConfigs,
landingPageConfig) are open to any authenticated tenant user; write mutations are gated
behind the existing Path.SETTINGS.
Frontend contract¶
- Save the current view: call
createLandingPageConfigwith the activelandingPage, aname, optionaldescription, and the current filter payload asconfig(any JSON; the backend stores it verbatim). - Update a saved view: call
updateLandingPageConfigwith the same input shape plus the existingid. Update is a full replace ofname/description/config/landingPage— send the complete object, not a partial. - List views for a page:
landingPageConfigs(landingPage: INVOICES)returns the tenant's saved configs for that page (newest first). OmitlandingPageto get all. - Apply a view: read
configback and re-hydrate the landing-page filter UI. - Delete:
deleteLandingPageConfig(id). - Saved configs are shared across the tenant;
createdByIdis for display/audit only.
What shipped¶
Everything in "What's being implemented" landed. task all is green and 13 tests pass.
Future additions¶
- Per-user vs shared toggle — deferred; current scope is tenant-shared only. Add an
is_shared/owner_onlyflag if private saved views are requested. - Creator-only edit/delete — deferred; today any
SETTINGS-permitted user can manage any config. Revisit if tenants want stricter ownership. - Default / pinned view per page — deferred; would let a tenant mark one config as the default landing-page filter.