Skip to content

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_configs table + module (app/graphql/landing_page_configs/).
  • A LandingPage enum 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 SETTINGS access manages them).
  • Validation of the config JSON 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

  • Modelapp/graphql/landing_page_configs/models/landing_page_config.py (CoreBaseModel, HasPrimaryKey, HasCreatedAt, HasCreatedBy).
  • Enumapp/graphql/landing_page_configs/strawberry/landing_page.py (LandingPage, a @strawberry.enum IntEnum stored via IntEnum(LandingPage)).
  • Input.../strawberry/landing_page_config_input.py: a single LandingPageConfigInput(BaseInputGQL[LandingPageConfig]) shared by create and update, with to_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]): inherited add for create, an update that loads + merges, and list_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 on Path.SETTINGS).
  • Migrationalembic/versions/20260603_add_landing_page_configs.py.
  • Model registration — added to load_models.py.
  • Teststests/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 createLandingPageConfig with the active landingPage, a name, optional description, and the current filter payload as config (any JSON; the backend stores it verbatim).
  • Update a saved view: call updateLandingPageConfig with the same input shape plus the existing id. Update is a full replace of name/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). Omit landingPage to get all.
  • Apply a view: read config back and re-hydrate the landing-page filter UI.
  • Delete: deleteLandingPageConfig(id).
  • Saved configs are shared across the tenant; createdById is 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_only flag 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.