Add state to MCP profile and refactor contract filter

Add the profile state attribute (ACTIVE/INACTIVE) to the MCP
Profile schema so listUsers and getUser tools expose it, and
add a state filter to listUsers.

Rename excludeContractEnded to contractEnded across the entire
stack (MCP, GraphQL, CLI, frontend). The new boolean is two-way:
true returns only users with ended contracts, false returns only
users with active or no contract, and null returns all.

Signed-off-by: Émile Ré <emile@getprobo.com>
This commit is contained in:
Émile Ré
2026-04-24 11:45:52 +04:00
parent b7a28573f7
commit 272f63828f
16 changed files with 43 additions and 23 deletions

View File

@@ -65,7 +65,7 @@ function PeopleMultiSelectWithQuery<T extends FieldValues = FieldValues>(
) { ) {
const { __ } = useTranslate(); const { __ } = useTranslate();
const { name, organizationId, control, selectedPeople = [], placeholder } = props; const { name, organizationId, control, selectedPeople = [], placeholder } = props;
const people = usePeople(organizationId, { excludeContractEnded: true }); const people = usePeople(organizationId, { contractEnded: false });
const [isOpen, setIsOpen] = useState(false); const [isOpen, setIsOpen] = useState(false);
const allPeople = [...people]; const allPeople = [...people];

View File

@@ -61,7 +61,7 @@ function PeopleSelectWithQuery<TFieldValues extends FieldValues = FieldValues>(
) { ) {
const { __ } = useTranslate(); const { __ } = useTranslate();
const { name, organizationId, control } = props; const { name, organizationId, control } = props;
const people = usePeople(organizationId, { excludeContractEnded: true }); const people = usePeople(organizationId, { contractEnded: false });
return ( return (
<Controller <Controller

View File

@@ -31,7 +31,7 @@ export function PeopleCell(props: Props) {
query={peopleQuery} query={peopleQuery}
variables={{ variables={{
organizationId: props.organizationId, organizationId: props.organizationId,
filter: { excludeContractEnded: true }, filter: { contractEnded: false },
}} }}
items={data => items={data =>
data.organization?.profiles?.edges.map(edge => edge.node) ?? []} data.organization?.profiles?.edges.map(edge => edge.node) ?? []}

View File

@@ -49,13 +49,13 @@ export const peopleQuery = graphql`
*/ */
export function usePeople( export function usePeople(
organizationId: string, organizationId: string,
{ excludeContractEnded }: { excludeContractEnded?: boolean } = {}, { contractEnded }: { contractEnded?: boolean } = {},
) { ) {
const data = useLazyLoadQuery<PeopleGraphQuery>( const data = useLazyLoadQuery<PeopleGraphQuery>(
peopleQuery, peopleQuery,
{ {
organizationId: organizationId, organizationId: organizationId,
filter: excludeContractEnded ? { excludeContractEnded: true } : null, filter: contractEnded !== undefined ? { contractEnded } : null,
}, },
{ fetchPolicy: "network-only" }, { fetchPolicy: "network-only" },
); );

View File

@@ -196,7 +196,7 @@ function PeopleList({
signatureDocumentsDialogPeopleQuery, signatureDocumentsDialogPeopleQuery,
{ {
organizationId, organizationId,
filter: { excludeContractEnded: true }, filter: { contractEnded: false },
}, },
); );
const { const {

View File

@@ -27,7 +27,7 @@ export const documentSignaturesPageQuery = graphql`
query DocumentSignaturesPageQuery($documentId: ID! $organizationId: ID! $versionId: ID! $versionSpecified: Boolean!) { query DocumentSignaturesPageQuery($documentId: ID! $organizationId: ID! $versionId: ID! $versionSpecified: Boolean!) {
organization: node(id: $organizationId) { organization: node(id: $organizationId) {
__typename __typename
...DocumentSignatureList_peopleFragment @arguments(filter: { excludeContractEnded: true }) ...DocumentSignatureList_peopleFragment @arguments(filter: { contractEnded: false })
} }
# We use this on /documents/:documentId # We use this on /documents/:documentId
document: node(id: $documentId) @skip(if: $versionSpecified) { document: node(id: $documentId) @skip(if: $versionSpecified) {

View File

@@ -550,7 +550,7 @@ function OwnerFilterSelect({
onChange, onChange,
}: OwnerFilterSelectProps) { }: OwnerFilterSelectProps) {
const { __ } = useTranslate(); const { __ } = useTranslate();
const people = usePeople(organizationId, { excludeContractEnded: true }); const people = usePeople(organizationId, { contractEnded: false });
return ( return (
<Select value={value ?? "ALL"} onValueChange={onChange}> <Select value={value ?? "ALL"} onValueChange={onChange}>

View File

@@ -126,7 +126,7 @@ func NewCmdList(f *cmdutil.Factory) *cobra.Command {
if flagActive { if flagActive {
variables["filter"] = map[string]any{ variables["filter"] = map[string]any{
"excludeContractEnded": true, "contractEnded": false,
} }
} }
@@ -210,7 +210,7 @@ func NewCmdList(f *cmdutil.Factory) *cobra.Command {
cmd.Flags().IntVarP(&flagLimit, "limit", "L", 30, "Maximum number of users to list") cmd.Flags().IntVarP(&flagLimit, "limit", "L", 30, "Maximum number of users to list")
cmd.Flags().StringVar(&flagOrder, "order-by", "", "Order by field (FULL_NAME, CREATED_AT, KIND)") 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(&flagOrderDir, "order-direction", "DESC", "Sort direction (ASC, DESC)")
cmd.Flags().BoolVar(&flagActive, "active", false, "Exclude users whose contract has ended") cmd.Flags().BoolVar(&flagActive, "active", false, "Show only users with active or no contract")
flagOutput = cmdutil.AddOutputFlag(cmd) flagOutput = cmdutil.AddOutputFlag(cmd)
return cmd return cmd

View File

@@ -25,7 +25,7 @@ type (
MembershipProfileFilter struct { MembershipProfileFilter struct {
withMembership *bool withMembership *bool
withTrustCenterAccess *bool withTrustCenterAccess *bool
excludeContractEnded *bool contractEnded *bool
currentDate time.Time currentDate time.Time
email *mail.Addr email *mail.Addr
userName *string userName *string
@@ -35,10 +35,10 @@ type (
} }
) )
func NewMembershipProfileFilter(excludeContractEnded *bool) *MembershipProfileFilter { func NewMembershipProfileFilter(contractEnded *bool) *MembershipProfileFilter {
return &MembershipProfileFilter{ return &MembershipProfileFilter{
excludeContractEnded: excludeContractEnded, contractEnded: contractEnded,
currentDate: time.Now(), currentDate: time.Now(),
} }
} }
@@ -96,7 +96,7 @@ func (f *MembershipProfileFilter) SQLArguments() pgx.StrictNamedArgs {
"filter_external_id": f.externalID, "filter_external_id": f.externalID,
"with_membership": f.withMembership, "with_membership": f.withMembership,
"with_trust_center_access": f.withTrustCenterAccess, "with_trust_center_access": f.withTrustCenterAccess,
"exclude_contract_ended": f.excludeContractEnded, "contract_ended": f.contractEnded,
"current_date": f.currentDate, "current_date": f.currentDate,
"filter_state": f.state, "filter_state": f.state,
"filter_source": f.source, "filter_source": f.source,
@@ -132,7 +132,9 @@ AND (
) )
AND ( AND (
CASE CASE
WHEN @exclude_contract_ended::boolean IS NOT NULL AND @exclude_contract_ended::boolean = true THEN WHEN @contract_ended::boolean IS NOT NULL AND @contract_ended::boolean = true THEN
(p.contract_end_date IS NOT NULL AND p.contract_end_date < @current_date::date)
WHEN @contract_ended::boolean IS NOT NULL AND @contract_ended::boolean = false THEN
(p.contract_end_date IS NULL OR p.contract_end_date >= @current_date::date) (p.contract_end_date IS NULL OR p.contract_end_date >= @current_date::date)
ELSE TRUE ELSE TRUE
END END

View File

@@ -65,7 +65,7 @@ enum ProfileOrderField
} }
input ProfileFilter { input ProfileFilter {
excludeContractEnded: Boolean contractEnded: Boolean
state: ProfileState state: ProfileState
} }

View File

@@ -30,7 +30,7 @@ func (r *identityResolver) Profiles(ctx context.Context, obj *types.Identity, fi
filters := coredata.NewMembershipProfileFilter(nil).WithMembership() filters := coredata.NewMembershipProfileFilter(nil).WithMembership()
if filter != nil { if filter != nil {
filters = coredata.NewMembershipProfileFilter(filter.ExcludeContractEnded).WithMembership() filters = coredata.NewMembershipProfileFilter(filter.ContractEnded).WithMembership()
if filter.State != nil { if filter.State != nil {
filters.WithState(*filter.State) filters.WithState(*filter.State)
} }

View File

@@ -52,7 +52,7 @@ input ProfileOrder
} }
input ProfileFilter { input ProfileFilter {
excludeContractEnded: Boolean contractEnded: Boolean
} }
type ProfileConnection type ProfileConnection

View File

@@ -123,7 +123,7 @@ func (r *organizationResolver) Profiles(ctx context.Context, obj *types.Organiza
filters := coredata.NewMembershipProfileFilter(nil).WithMembership() filters := coredata.NewMembershipProfileFilter(nil).WithMembership()
if filter != nil { if filter != nil {
filters = coredata.NewMembershipProfileFilter(filter.ExcludeContractEnded).WithMembership() filters = coredata.NewMembershipProfileFilter(filter.ContractEnded).WithMembership()
} }
pageOrderBy := page.OrderBy[coredata.MembershipProfileOrderField]{ pageOrderBy := page.OrderBy[coredata.MembershipProfileOrderField]{

View File

@@ -2491,7 +2491,10 @@ func (r *Resolver) ListUsersTool(ctx context.Context, req *mcp.CallToolRequest,
filter := coredata.NewMembershipProfileFilter(nil).WithMembership() filter := coredata.NewMembershipProfileFilter(nil).WithMembership()
if input.Filter != nil { if input.Filter != nil {
filter = coredata.NewMembershipProfileFilter(input.Filter.ExcludeContractEnded).WithMembership() filter = coredata.NewMembershipProfileFilter(input.Filter.ContractEnded).WithMembership()
if input.Filter.State != nil {
filter.WithState(*input.Filter.State)
}
} }
pageResult, err := r.iamSvc.OrganizationService.ListProfiles(ctx, input.OrganizationID, cursor, filter) pageResult, err := r.iamSvc.OrganizationService.ListProfiles(ctx, input.OrganizationID, cursor, filter)

View File

@@ -180,6 +180,13 @@ components:
- AUDITOR - AUDITOR
go.probo.inc/mcpgen/type: go.probo.inc/probo/pkg/coredata.MembershipRole go.probo.inc/mcpgen/type: go.probo.inc/probo/pkg/coredata.MembershipRole
ProfileState:
type: string
enum:
- ACTIVE
- INACTIVE
go.probo.inc/mcpgen/type: go.probo.inc/probo/pkg/coredata.ProfileState
Profile: Profile:
type: object type: object
required: required:
@@ -189,6 +196,7 @@ components:
- email_address - email_address
- additional_email_addresses - additional_email_addresses
- kind - kind
- state
- created_at - created_at
- updated_at - updated_at
properties: properties:
@@ -214,6 +222,9 @@ components:
- string - string
- "null" - "null"
description: Profile kind description: Profile kind
state:
$ref: "#/components/schemas/ProfileState"
description: Profile state (ACTIVE or INACTIVE)
position: position:
type: type:
- string - string
@@ -260,9 +271,12 @@ components:
filter: filter:
type: object type: object
properties: properties:
exclude_contract_ended: contract_ended:
type: boolean type: boolean
description: Exclude users with ended contracts 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 (ACTIVE or INACTIVE)
ListUsersOutput: ListUsersOutput:
type: object type: object

View File

@@ -24,6 +24,7 @@ func NewProfile(p *coredata.MembershipProfile) *Profile {
EmailAddress: p.EmailAddress, EmailAddress: p.EmailAddress,
AdditionalEmailAddresses: p.AdditionalEmailAddresses, AdditionalEmailAddresses: p.AdditionalEmailAddresses,
Kind: p.Kind, Kind: p.Kind,
State: p.State,
Position: p.Position, Position: p.Position,
ContractStartDate: p.ContractStartDate, ContractStartDate: p.ContractStartDate,
ContractEndDate: p.ContractEndDate, ContractEndDate: p.ContractEndDate,