Filter trust center subprocessors server-side

Subprocessor filtering for the compliance portal happens in the backend
rather than the client. Add a SubprocessorFilter (query, category,
country) to the trust API's subprocessors connection, thread it through
the resolver and service, and extend the coredata ThirdParty filter with
category equality and country array membership. The connection stores the
filter so totalCount reflects the filtered set. Add e2e coverage for the
new filtering.

On the frontend, convert the page to a refetchable fragment whose filter
arguments are driven by URL-persisted, debounced toolbar state (category
and region selects plus a search field), populate the dropdowns from an
unfiltered facet selection, and offer to clear filters from the empty
state.

Signed-off-by: Émile Ré <emile@probo.com>
This commit is contained in:
Émile Ré
2026-06-30 14:40:50 +02:00
parent f1f4c93104
commit c9b74d6de0
20 changed files with 623 additions and 31 deletions

View File

@@ -1284,7 +1284,7 @@ func (r *organizationResolver) ThirdParties(ctx context.Context, obj *types.Orga
query = filter.Query
}
thirdPartyFilter := coredata.NewThirdPartyFilter(nil, level, query)
thirdPartyFilter := coredata.NewThirdPartyFilter(nil, level, query, nil, nil)
page, err := r.probo.ThirdParties.ListForOrganizationID(ctx, scope, obj.ID, cursor, thirdPartyFilter)
if err != nil {

View File

@@ -75,7 +75,7 @@ func (r *Resolver) ListThirdPartiesTool(ctx context.Context, req *mcp.CallToolRe
cursor := types.NewCursor(input.Size, input.Cursor, pageOrderBy)
thirdPartyFilter := coredata.NewThirdPartyFilter(nil, input.Level, nil)
thirdPartyFilter := coredata.NewThirdPartyFilter(nil, input.Level, nil, nil, nil)
page, err := prb.ThirdParties.ListForOrganizationID(ctx, scope, input.OrganizationID, cursor, thirdPartyFilter)
if err != nil {

View File

@@ -30,6 +30,7 @@ type TrustCenter implements Node {
after: CursorKey
last: Int
before: CursorKey
filter: SubprocessorFilter
): SubprocessorConnection! @goField(forceResolver: true)
references(
@@ -245,6 +246,12 @@ type Subprocessor implements Node @nda {
countries: [CountryCode!]!
}
input SubprocessorFilter {
query: String
category: SubprocessorCategory
country: CountryCode
}
type SubprocessorConnection
@goModel(
model: "go.probo.inc/probo/pkg/server/api/trust/v1/types.SubprocessorConnection"

View File

@@ -670,7 +670,7 @@ func (r *subprocessorConnectionResolver) TotalCount(ctx context.Context, obj *ty
switch obj.Resolver.(type) {
case *trustCenterResolver:
count, err := trustService.ThirdParties.CountForTrustCenterId(ctx, scope, obj.ParentID)
count, err := trustService.ThirdParties.CountForTrustCenterId(ctx, scope, obj.ParentID, obj.Filter)
if err != nil {
r.logger.ErrorCtx(ctx, "cannot count subprocessors", log.Error(err))
return 0, gqlutils.Internal(ctx)
@@ -798,7 +798,7 @@ func (r *trustCenterResolver) Audits(ctx context.Context, obj *types.TrustCenter
}
// Subprocessors is the resolver for the subprocessors field.
func (r *trustCenterResolver) Subprocessors(ctx context.Context, obj *types.TrustCenter, first *int, after *page.CursorKey, last *int, before *page.CursorKey) (*types.SubprocessorConnection, error) {
func (r *trustCenterResolver) Subprocessors(ctx context.Context, obj *types.TrustCenter, first *int, after *page.CursorKey, last *int, before *page.CursorKey, filter *types.SubprocessorFilter) (*types.SubprocessorConnection, error) {
compliancePage := compliancepage.CompliancePageFromContext(ctx)
scope := coredata.NewScopeFromObjectID(compliancePage.OrganizationID)
trustService := r.trust
@@ -808,13 +808,27 @@ func (r *trustCenterResolver) Subprocessors(ctx context.Context, obj *types.Trus
}
cursor := types.NewCursor(first, after, last, before, pageOrderBy)
thirdPartyPage, err := trustService.ThirdParties.ListForOrganizationId(ctx, scope, obj.Organization.ID, cursor)
var (
query *string
category *coredata.ThirdPartyCategory
country *coredata.CountryCode
)
if filter != nil {
query = filter.Query
category = filter.Category
country = filter.Country
}
showOnTrustCenter := true
thirdPartyFilter := coredata.NewThirdPartyFilter(&showOnTrustCenter, nil, query, category, country)
thirdPartyPage, err := trustService.ThirdParties.ListForOrganizationId(ctx, scope, obj.Organization.ID, cursor, thirdPartyFilter)
if err != nil {
r.logger.ErrorCtx(ctx, "cannot list subprocessors", log.Error(err))
return nil, gqlutils.Internal(ctx)
}
return types.NewSubprocessorConnection(thirdPartyPage, r, obj.ID), nil
return types.NewSubprocessorConnection(thirdPartyPage, r, obj.ID, thirdPartyFilter), nil
}
// References is the resolver for the references field.

View File

@@ -28,6 +28,7 @@ type (
Resolver any
ParentID gid.GID
Filter *coredata.ThirdPartyFilter
}
)
@@ -35,6 +36,7 @@ func NewSubprocessorConnection(
p *page.Page[*coredata.ThirdParty, coredata.ThirdPartyOrderField],
parentType any,
parentID gid.GID,
filter *coredata.ThirdPartyFilter,
) *SubprocessorConnection {
edges := make([]*SubprocessorEdge, len(p.Data))
for i, thirdParty := range p.Data {
@@ -47,6 +49,7 @@ func NewSubprocessorConnection(
Resolver: parentType,
ParentID: parentID,
Filter: filter,
}
}