Make profile state filters multi-value

Address PR review feedback on the profile-state split: SAML sign-in now
activates a pending profile, deactivation counts owners against the
profile's own organization to close a last-owner bypass, the migration
leaves historical activated_at/deactivated_at NULL rather than
fabricating timestamps, and pending members are no longer rendered with
the deactivated (faded) styling.

Drop the single-value state filter in favor of the multi-value states
across the profile and signatory surfaces. Remove ProfileFilter.state
(only states[] remains) and convert the signatures profileState filter
to profileStates. Turn the console people filter, the CLI
"user list --state" flag, and the n8n listUsers and getAllSignatures
state inputs into multi-select controls, where an empty selection means
all states.

Signed-off-by: Émile Ré <emile@probo.com>
This commit is contained in:
Émile Ré
2026-07-28 18:16:52 +02:00
parent b10fc55b7f
commit eecf8a1d97
29 changed files with 88 additions and 106 deletions

View File

@@ -72,7 +72,7 @@ func NewCmdList(f *cmdutil.Factory) *cobra.Command {
flagOrder string
flagOrderDir string
flagContractEnded string
flagState string
flagState []string
flagFilter string
flagRole string
flagKind string
@@ -157,12 +157,14 @@ func NewCmdList(f *cmdutil.Factory) *cobra.Command {
filter["contractEnded"] = flagContractEnded == "true"
}
if flagState != "" {
if err := cmdutil.ValidateEnum("state", flagState, []string{"PENDING", "ACTIVE", "DEACTIVATED"}); err != nil {
return err
if len(flagState) > 0 {
for _, state := range flagState {
if err := cmdutil.ValidateEnum("state", state, []string{"PENDING", "ACTIVE", "DEACTIVATED"}); err != nil {
return err
}
}
filter["state"] = flagState
filter["states"] = flagState
}
if flagFilter != "" {
@@ -273,7 +275,7 @@ func NewCmdList(f *cmdutil.Factory) *cobra.Command {
cmd.Flags().StringVar(&flagOrder, "order-by", "", "Order by field (FULL_NAME, CREATED_AT, KIND)")
cmd.Flags().StringVar(&flagOrderDir, "order-direction", "DESC", "Sort direction (ASC, DESC)")
cmd.Flags().StringVar(&flagContractEnded, "contract-ended", "", "Filter by contract status (true or false)")
cmd.Flags().StringVar(&flagState, "state", "", "Filter by profile state (PENDING, ACTIVE, DEACTIVATED)")
cmd.Flags().StringSliceVar(&flagState, "state", nil, "Filter by profile state; repeat or comma-separate for multiple (PENDING, ACTIVE, DEACTIVATED)")
cmd.Flags().StringVarP(&flagFilter, "filter", "q", "", "Filter users by name or email search query")
cmd.Flags().StringVar(&flagRole, "role", "", "Filter by membership role (OWNER, ADMIN, VIEWER, AUDITOR, EMPLOYEE)")
cmd.Flags().StringVar(&flagKind, "kind", "", "Filter by profile kind (EMPLOYEE, CONTRACTOR, SERVICE_ACCOUNT)")

View File

@@ -28,15 +28,15 @@ type (
DocumentVersionSignatureFilter struct {
states DocumentVersionSignatureStates
activeContract *bool
profileState *ProfileState
profileStates ProfileStateValues
}
)
func NewDocumentVersionSignatureFilter(states []DocumentVersionSignatureState, activeContract *bool, profileState *ProfileState) *DocumentVersionSignatureFilter {
func NewDocumentVersionSignatureFilter(states []DocumentVersionSignatureState, activeContract *bool, profileStates []ProfileState) *DocumentVersionSignatureFilter {
return &DocumentVersionSignatureFilter{
states: DocumentVersionSignatureStates(states),
activeContract: activeContract,
profileState: profileState,
profileStates: ProfileStateValues(profileStates),
}
}
@@ -44,7 +44,7 @@ func (f *DocumentVersionSignatureFilter) SQLArguments() pgx.StrictNamedArgs {
return pgx.StrictNamedArgs{
"states": f.states,
"active_contract": f.activeContract,
"profile_state": f.profileState,
"profile_states": f.profileStates,
}
}
@@ -81,13 +81,13 @@ func (f *DocumentVersionSignatureFilter) SQLFragment() string {
END
AND
CASE
WHEN @profile_state::text IS NULL
WHEN @profile_states::membership_state[] IS NULL
THEN TRUE
ELSE EXISTS (
SELECT 1
FROM iam_membership_profiles p
WHERE p.id = signed_by_profile_id
AND p.state = @profile_state::membership_state
AND p.state = ANY(@profile_states::membership_state[])
)
END
)`

View File

@@ -81,11 +81,6 @@ func (f *MembershipProfileFilter) WithExternalID(externalID string) *MembershipP
return f
}
func (f *MembershipProfileFilter) WithState(state ProfileState) *MembershipProfileFilter {
f.states = ProfileStateValues{state}
return f
}
func (f *MembershipProfileFilter) WithStates(states ...ProfileState) *MembershipProfileFilter {
f.states = ProfileStateValues(states)
return f

View File

@@ -46,10 +46,3 @@ WHERE state = 'DEACTIVATED'
)
);
UPDATE iam_membership_profiles
SET activated_at = updated_at
WHERE state = 'ACTIVE';
UPDATE iam_membership_profiles
SET deactivated_at = NOW()
WHERE state = 'DEACTIVATED';

View File

@@ -405,7 +405,7 @@ func (s *OrganizationService) DeactivateUser(
if membership.Role == coredata.MembershipRoleOwner && profile.State == coredata.ProfileStateActive {
profiles := coredata.MembershipProfiles{}
count, err := profiles.CountActiveOwnerByOrganizationID(ctx, tx, scope, organizationID)
count, err := profiles.CountActiveOwnerByOrganizationID(ctx, tx, scope, profile.OrganizationID)
if err != nil {
return fmt.Errorf("cannot count active owners: %w", err)
}

View File

@@ -336,6 +336,10 @@ func (s *Service) HandleAssertion(
if profile.State == coredata.ProfileStateDeactivated {
return NewUserInactiveError(profile.ID)
}
if profile.State == coredata.ProfileStatePending {
profile.MarkActive(now)
}
}
if err := membership.LoadByIdentityIDAndOrganizationID(

View File

@@ -71,7 +71,6 @@ enum ProfileOrderField
input ProfileFilter {
contractEnded: Boolean
state: ProfileState
states: [ProfileState!]
query: String
role: MembershipRole

View File

@@ -36,10 +36,6 @@ func (r *identityResolver) Profiles(ctx context.Context, obj *types.Identity, fi
filters.WithStates(filter.States...)
}
if filter.State != nil {
filters.WithState(*filter.State)
}
if filter.Query != nil {
filters.WithQuery(filter.Query)
}

View File

@@ -192,10 +192,6 @@ func (r *organizationResolver) Profiles(ctx context.Context, obj *types.Organiza
filters.WithStates(filter.States...)
}
if filter.State != nil {
filters.WithState(*filter.State)
}
if filter.Query != nil {
filters.WithQuery(filter.Query)
}

View File

@@ -282,7 +282,7 @@ func (r *documentVersionResolver) Signatures(ctx context.Context, obj *types.Doc
var (
signatureStates []coredata.DocumentVersionSignatureState
activeContract *bool
profileState *coredata.ProfileState
profileStates []coredata.ProfileState
)
if filter != nil {
@@ -294,12 +294,12 @@ func (r *documentVersionResolver) Signatures(ctx context.Context, obj *types.Doc
activeContract = filter.ActiveContract
}
if filter.ProfileState != nil {
profileState = filter.ProfileState
if filter.ProfileStates != nil {
profileStates = filter.ProfileStates
}
}
signatureFilter := coredata.NewDocumentVersionSignatureFilter(signatureStates, activeContract, profileState)
signatureFilter := coredata.NewDocumentVersionSignatureFilter(signatureStates, activeContract, profileStates)
cursor := types.NewCursor(first, after, last, before, pageOrderBy)

View File

@@ -247,7 +247,7 @@ input DocumentVersionSignatureOrder {
input DocumentVersionSignatureFilter {
states: [DocumentVersionSignatureState!]
activeContract: Boolean
profileState: ProfileState
profileStates: [ProfileState!]
}
input DocumentVersionApprovalQuorumOrder {

View File

@@ -69,7 +69,6 @@ input ProfileOrder
input ProfileFilter {
contractEnded: Boolean
state: ProfileState
states: [ProfileState!]
query: String
role: MembershipRole

View File

@@ -123,10 +123,6 @@ func (r *organizationResolver) Profiles(ctx context.Context, obj *types.Organiza
filters.WithStates(filter.States...)
}
if filter.State != nil {
filters.WithState(*filter.State)
}
if filter.Query != nil {
filters.WithQuery(filter.Query)
}

View File

@@ -2401,7 +2401,7 @@ func (r *Resolver) ListDocumentVersionSignaturesTool(ctx context.Context, req *m
var (
signatureStates []coredata.DocumentVersionSignatureState
activeContract *bool
profileState *coredata.ProfileState
profileStates []coredata.ProfileState
)
if input.Filter != nil {
@@ -2413,12 +2413,12 @@ func (r *Resolver) ListDocumentVersionSignaturesTool(ctx context.Context, req *m
activeContract = input.Filter.ActiveContract
}
if input.Filter.ProfileState != nil {
profileState = input.Filter.ProfileState
if input.Filter.ProfileStates != nil {
profileStates = input.Filter.ProfileStates
}
}
signatureFilter := coredata.NewDocumentVersionSignatureFilter(signatureStates, activeContract, profileState)
signatureFilter := coredata.NewDocumentVersionSignatureFilter(signatureStates, activeContract, profileStates)
page, err := prb.Documents.ListSignatures(ctx, scope, input.DocumentVersionID, cursor, signatureFilter)
if err != nil {
@@ -2780,10 +2780,6 @@ func (r *Resolver) ListUsersTool(ctx context.Context, req *mcp.CallToolRequest,
filter.WithStates(input.Filter.States...)
}
if input.Filter.State != nil {
filter.WithState(*input.Filter.State)
}
if input.Filter.Query != nil {
filter.WithQuery(input.Filter.Query)
}

View File

@@ -577,9 +577,6 @@ components:
contract_ended:
type: boolean
description: Filter by contract status. True returns only users with ended contracts, false returns only users with active or no contract.
state:
$ref: "#/components/schemas/ProfileState"
description: Filter by profile state (PENDING, ACTIVE, or DEACTIVATED)
states:
type: array
items:
@@ -6421,9 +6418,11 @@ components:
active_contract:
type: boolean
description: Signatory contract status
profile_state:
$ref: "#/components/schemas/ProfileState"
description: Signatory profile state
profile_states:
type: array
items:
$ref: "#/components/schemas/ProfileState"
description: Signatory profile states
ListDocumentVersionSignaturesOutput:
type: object