Add documents page to the compliance portal

Build the Trust Center documents page: a unified list of published
documents, uploaded files, and audit reports, grouped into category
sections. An All/Public/Private tab bar filters the list by trust
center visibility.

Expose that filter over the trust v1 API by adding a
TrustCenterVisibility enum and a shared TrustCenterVisibilityFilter
input, wiring it through the documents, audits, and trustCenterFiles
connections down to the existing coredata SQL filters. "All" keeps the
default public+private slice; the other tabs pin a single visibility.

Access controls are display-only for now (auth is handled separately):
authorized or public entries open their exported PDF via the export
mutations, requested entries show a pending state, and everything else
shows an inert Get Access affordance.

Add the v2 Tabs and Toaster kit components (Base UI headless) needed by
the page and mount a toast provider at the app root for mutation
feedback.

Signed-off-by: Émile Ré <emile@probo.com>
This commit is contained in:
Émile Ré
2026-07-15 18:33:46 +02:00
parent 3c3276a90b
commit 158861ccdd
41 changed files with 1589 additions and 43 deletions

View File

@@ -43,6 +43,11 @@ func NewAuditTrustCenterFilter() *AuditFilter {
}
}
func (f *AuditFilter) WithTrustCenterVisibilities(visibilities ...TrustCenterVisibility) *AuditFilter {
f.trustCenterVisibilities = visibilities
return f
}
func (f *AuditFilter) SQLArguments() pgx.NamedArgs {
args := pgx.NamedArgs{}

View File

@@ -63,6 +63,11 @@ func (f *DocumentFilter) WithPublished(published *bool) *DocumentFilter {
return f
}
func (f *DocumentFilter) WithTrustCenterVisibilities(visibilities ...TrustCenterVisibility) *DocumentFilter {
f.trustCenterVisibilities = visibilities
return f
}
func (f *DocumentFilter) WithEmployeeIdentityID(identityID *gid.GID, modes ...EmployeeFilterMode) *DocumentFilter {
f.employeeIdentityID = identityID
f.employeeFilterModes = modes

View File

@@ -16,6 +16,7 @@ type TrustCenter implements Node {
after: CursorKey
last: Int
before: CursorKey
filter: TrustCenterVisibilityFilter
): DocumentConnection! @goField(forceResolver: true)
audits(
@@ -23,6 +24,7 @@ type TrustCenter implements Node {
after: CursorKey
last: Int
before: CursorKey
filter: TrustCenterVisibilityFilter
): AuditConnection! @goField(forceResolver: true)
subprocessors(
@@ -50,6 +52,7 @@ type TrustCenter implements Node {
after: CursorKey
last: Int
before: CursorKey
filter: TrustCenterVisibilityFilter
): TrustCenterFileConnection! @goField(forceResolver: true)
complianceFrameworks(
@@ -95,6 +98,18 @@ enum DocumentType
)
}
enum TrustCenterVisibility
@goModel(model: "go.probo.inc/probo/pkg/coredata.TrustCenterVisibility") {
PRIVATE
@goEnum(value: "go.probo.inc/probo/pkg/coredata.TrustCenterVisibilityPrivate")
PUBLIC
@goEnum(value: "go.probo.inc/probo/pkg/coredata.TrustCenterVisibilityPublic")
}
input TrustCenterVisibilityFilter {
visibility: TrustCenterVisibility
}
type Document implements Node @nda {
id: ID!
title: String!

View File

@@ -758,7 +758,7 @@ func (r *trustCenterResolver) Organization(ctx context.Context, obj *types.Trust
}
// Documents is the resolver for the documents field.
func (r *trustCenterResolver) Documents(ctx context.Context, obj *types.TrustCenter, first *int, after *page.CursorKey, last *int, before *page.CursorKey) (*types.DocumentConnection, error) {
func (r *trustCenterResolver) Documents(ctx context.Context, obj *types.TrustCenter, first *int, after *page.CursorKey, last *int, before *page.CursorKey, filter *types.TrustCenterVisibilityFilter) (*types.DocumentConnection, error) {
compliancePage := compliancepage.CompliancePageFromContext(ctx)
scope := coredata.NewScopeFromObjectID(compliancePage.OrganizationID)
trustService := r.trust
@@ -768,7 +768,12 @@ func (r *trustCenterResolver) Documents(ctx context.Context, obj *types.TrustCen
}
cursor := types.NewCursor(first, after, last, before, pageOrderBy)
documentPage, err := trustService.Documents.ListForOrganizationId(ctx, scope, obj.Organization.ID, cursor)
documentFilter := coredata.NewDocumentTrustCenterFilter()
if filter != nil && filter.Visibility != nil {
documentFilter = documentFilter.WithTrustCenterVisibilities(*filter.Visibility)
}
documentPage, err := trustService.Documents.ListForOrganizationId(ctx, scope, obj.Organization.ID, cursor, documentFilter)
if err != nil {
r.logger.ErrorCtx(ctx, "cannot list public documents", log.Error(err))
return nil, gqlutils.Internal(ctx)
@@ -778,7 +783,7 @@ func (r *trustCenterResolver) Documents(ctx context.Context, obj *types.TrustCen
}
// Audits is the resolver for the audits field.
func (r *trustCenterResolver) Audits(ctx context.Context, obj *types.TrustCenter, first *int, after *page.CursorKey, last *int, before *page.CursorKey) (*types.AuditConnection, error) {
func (r *trustCenterResolver) Audits(ctx context.Context, obj *types.TrustCenter, first *int, after *page.CursorKey, last *int, before *page.CursorKey, filter *types.TrustCenterVisibilityFilter) (*types.AuditConnection, error) {
compliancePage := compliancepage.CompliancePageFromContext(ctx)
scope := coredata.NewScopeFromObjectID(compliancePage.OrganizationID)
trustService := r.trust
@@ -788,7 +793,12 @@ func (r *trustCenterResolver) Audits(ctx context.Context, obj *types.TrustCenter
}
cursor := types.NewCursor(first, after, last, before, pageOrderBy)
auditPage, err := trustService.Audits.ListForOrganizationId(ctx, scope, obj.Organization.ID, cursor)
auditFilter := coredata.NewAuditTrustCenterFilter()
if filter != nil && filter.Visibility != nil {
auditFilter = auditFilter.WithTrustCenterVisibilities(*filter.Visibility)
}
auditPage, err := trustService.Audits.ListForOrganizationId(ctx, scope, obj.Organization.ID, cursor, auditFilter)
if err != nil {
r.logger.ErrorCtx(ctx, "cannot list public audits", log.Error(err))
return nil, gqlutils.Internal(ctx)
@@ -888,7 +898,7 @@ func (r *trustCenterResolver) References(ctx context.Context, obj *types.TrustCe
}
// TrustCenterFiles is the resolver for the trustCenterFiles field.
func (r *trustCenterResolver) TrustCenterFiles(ctx context.Context, obj *types.TrustCenter, first *int, after *page.CursorKey, last *int, before *page.CursorKey) (*types.TrustCenterFileConnection, error) {
func (r *trustCenterResolver) TrustCenterFiles(ctx context.Context, obj *types.TrustCenter, first *int, after *page.CursorKey, last *int, before *page.CursorKey, filter *types.TrustCenterVisibilityFilter) (*types.TrustCenterFileConnection, error) {
compliancePage := compliancepage.CompliancePageFromContext(ctx)
scope := coredata.NewScopeFromObjectID(compliancePage.OrganizationID)
trustService := r.trust
@@ -898,14 +908,19 @@ func (r *trustCenterResolver) TrustCenterFiles(ctx context.Context, obj *types.T
}
cursor := types.NewCursor(first, after, last, before, pageOrderBy)
filter := coredata.NewTrustCenterFileFilter(
coredata.WithTrustCenterFileVisibilities(
coredata.TrustCenterVisibilityPublic,
coredata.TrustCenterVisibilityPrivate,
),
visibilities := []coredata.TrustCenterVisibility{
coredata.TrustCenterVisibilityPublic,
coredata.TrustCenterVisibilityPrivate,
}
if filter != nil && filter.Visibility != nil {
visibilities = []coredata.TrustCenterVisibility{*filter.Visibility}
}
fileFilter := coredata.NewTrustCenterFileFilter(
coredata.WithTrustCenterFileVisibilities(visibilities...),
)
trustCenterFilePage, err := trustService.TrustCenterFiles.ListForOrganizationId(ctx, scope, obj.Organization.ID, cursor, filter)
trustCenterFilePage, err := trustService.TrustCenterFiles.ListForOrganizationId(ctx, scope, obj.Organization.ID, cursor, fileFilter)
if err != nil {
r.logger.ErrorCtx(ctx, "cannot list public trust center files", log.Error(err))
return nil, gqlutils.Internal(ctx)

View File

@@ -88,14 +88,17 @@ func (s AuditService) ListForOrganizationId(
scope coredata.Scoper,
organizationID gid.GID,
cursor *page.Cursor[coredata.AuditOrderField],
filter *coredata.AuditFilter,
) (*page.Page[*coredata.Audit, coredata.AuditOrderField], error) {
var audits coredata.Audits
if filter == nil {
filter = coredata.NewAuditTrustCenterFilter()
}
err := s.svc.pg.WithConn(
ctx,
func(ctx context.Context, conn pg.Querier) error {
filter := coredata.NewAuditTrustCenterFilter()
err := audits.LoadByOrganizationID(ctx, conn, scope, organizationID, cursor, filter)
if err != nil {
return fmt.Errorf("cannot load audits: %w", err)

View File

@@ -271,7 +271,7 @@ func (s *Service) fetchDocumentIDs(ctx context.Context, scope coredata.Scoper, o
},
)
result, err := s.Documents.ListForOrganizationId(ctx, scope, orgID, cursor)
result, err := s.Documents.ListForOrganizationId(ctx, scope, orgID, cursor, nil)
if err != nil {
return nil, fmt.Errorf("cannot list documents: %w", err)
}
@@ -345,7 +345,7 @@ func (s *Service) fetchDocumentIDs(ctx context.Context, scope coredata.Scoper, o
},
)
result, err := s.Audits.ListForOrganizationId(ctx, scope, orgID, cursor)
result, err := s.Audits.ListForOrganizationId(ctx, scope, orgID, cursor, nil)
if err != nil {
return nil, fmt.Errorf("cannot list audits: %w", err)
}
@@ -454,7 +454,7 @@ func (s *Service) fetchDocuments(ctx context.Context, scope coredata.Scoper, org
},
)
result, err := s.Documents.ListForOrganizationId(ctx, scope, orgID, cursor)
result, err := s.Documents.ListForOrganizationId(ctx, scope, orgID, cursor, nil)
if err != nil {
return nil, fmt.Errorf("cannot list documents: %w", err)
}
@@ -500,7 +500,7 @@ func (s *Service) fetchAudits(ctx context.Context, scope coredata.Scoper, orgID
},
)
result, err := s.Audits.ListForOrganizationId(ctx, scope, orgID, cursor)
result, err := s.Audits.ListForOrganizationId(ctx, scope, orgID, cursor, nil)
if err != nil {
return nil, fmt.Errorf("cannot list audits: %w", err)
}

View File

@@ -55,14 +55,17 @@ func (s *DocumentService) ListForOrganizationId(
scope coredata.Scoper,
organizationID gid.GID,
cursor *page.Cursor[coredata.DocumentOrderField],
filter *coredata.DocumentFilter,
) (*page.Page[*coredata.Document, coredata.DocumentOrderField], error) {
var documents coredata.Documents
if filter == nil {
filter = coredata.NewDocumentTrustCenterFilter()
}
err := s.svc.pg.WithConn(
ctx,
func(ctx context.Context, conn pg.Querier) error {
filter := coredata.NewDocumentTrustCenterFilter()
if err := documents.LoadPublishedByOrganizationID(ctx, conn, scope, organizationID, cursor, filter); err != nil {
return fmt.Errorf("cannot load published documents: %w", err)
}