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:
@@ -1371,7 +1371,7 @@
|
||||
"columns": { "name": "Name", "status": "Status", "email": "Email", "role": "Role", "createdOn": "Created on" },
|
||||
"empty": "No people",
|
||||
"searchPlaceholder": "Search people...",
|
||||
"filters": { "allStatuses": "All statuses", "pending": "Pending", "active": "Active", "deactivated": "Deactivated", "allRoles": "All roles", "allTypes": "All types" }
|
||||
"filters": { "allStatuses": "All statuses", "status": "Status", "pending": "Pending", "active": "Active", "deactivated": "Deactivated", "allRoles": "All roles", "allTypes": "All types" }
|
||||
},
|
||||
"peopleListItem": {
|
||||
"messages": { "invitationSent": "Invitation sent successfully", "roleUpdated": "Role updated successfully", "deactivated": "Person deactivated successfully", "removed": "Person removed successfully" },
|
||||
|
||||
@@ -2458,6 +2458,7 @@
|
||||
"searchPlaceholder": "Rechercher des personnes...",
|
||||
"filters": {
|
||||
"allStatuses": "Tous les statuts",
|
||||
"status": "Statut",
|
||||
"pending": "En attente",
|
||||
"active": "Actif",
|
||||
"deactivated": "Désactivé",
|
||||
|
||||
@@ -43,7 +43,7 @@ export const enrollDevicePageQuery = graphql`
|
||||
profiles(
|
||||
first: 1000
|
||||
orderBy: { direction: ASC, field: ORGANIZATION_NAME }
|
||||
filter: { state: ACTIVE }
|
||||
filter: { states: [ACTIVE] }
|
||||
) @required(action: THROW) {
|
||||
edges @required(action: THROW) {
|
||||
node @required(action: THROW) {
|
||||
|
||||
@@ -41,7 +41,7 @@ export const membershipsPageQuery = graphql`
|
||||
profiles(
|
||||
first: 1000
|
||||
orderBy: { direction: ASC, field: ORGANIZATION_NAME }
|
||||
filter: { state: ACTIVE }
|
||||
filter: { states: [ACTIVE] }
|
||||
)
|
||||
@connection(key: "MembershipsPage_profiles")
|
||||
@required(action: THROW) {
|
||||
|
||||
@@ -34,7 +34,7 @@ export const membershipsDropdownMenuQuery = graphql`
|
||||
profiles(
|
||||
first: 1000
|
||||
orderBy: { direction: ASC, field: ORGANIZATION_NAME }
|
||||
filter: { state: ACTIVE }
|
||||
filter: { states: [ACTIVE] }
|
||||
) @required(action: THROW) {
|
||||
edges @required(action: THROW) {
|
||||
node @required(action: THROW) {
|
||||
|
||||
@@ -20,6 +20,7 @@
|
||||
|
||||
import { getAssignableRoles, getMembershipRoles, peopleRoles } from "@probo/helpers";
|
||||
import {
|
||||
Checkbox,
|
||||
IconMagnifyingGlass,
|
||||
Input,
|
||||
Option,
|
||||
@@ -89,11 +90,13 @@ const fragment = graphql`
|
||||
|
||||
type PeopleFilter = {
|
||||
query: string | null;
|
||||
state: ProfileState | null;
|
||||
states: ProfileState[];
|
||||
role: MembershipRole | null;
|
||||
kind: string | null;
|
||||
};
|
||||
|
||||
const PROFILE_STATES: ProfileState[] = ["PENDING", "ACTIVE", "DEACTIVATED"];
|
||||
|
||||
export function PeopleList(props: {
|
||||
fKey: PeopleListFragment$key;
|
||||
onConnectionIdChange: (connectionId: string) => void;
|
||||
@@ -105,7 +108,7 @@ export function PeopleList(props: {
|
||||
const canManageRoles = getAssignableRoles(role).length > 0;
|
||||
|
||||
const [queryFilter, setQueryFilter] = useState<string | null>(null);
|
||||
const [stateFilter, setStateFilter] = useState<ProfileState | null>(null);
|
||||
const [statesFilter, setStatesFilter] = useState<ProfileState[]>([]);
|
||||
const [roleFilter, setRoleFilter] = useState<MembershipRole | null>(null);
|
||||
const [kindFilter, setKindFilter] = useState<string | null>(null);
|
||||
const [order, setOrder] = useState<Order>({
|
||||
@@ -127,7 +130,7 @@ export function PeopleList(props: {
|
||||
|
||||
const currentFilter = (overrides: Partial<PeopleFilter> = {}): PeopleFilter => ({
|
||||
query: queryFilter,
|
||||
state: stateFilter,
|
||||
states: statesFilter,
|
||||
role: roleFilter,
|
||||
kind: kindFilter,
|
||||
...overrides,
|
||||
@@ -135,7 +138,7 @@ export function PeopleList(props: {
|
||||
|
||||
const connectionFilter = (filter: PeopleFilter) => ({
|
||||
query: filter.query,
|
||||
state: filter.state,
|
||||
states: filter.states.length > 0 ? filter.states : null,
|
||||
role: filter.role,
|
||||
kind: filter.kind,
|
||||
contractEnded: null,
|
||||
@@ -170,7 +173,7 @@ export function PeopleList(props: {
|
||||
},
|
||||
filter: {
|
||||
query: newQuery,
|
||||
state: stateFilter,
|
||||
states: statesFilter.length > 0 ? statesFilter : null,
|
||||
role: roleFilter,
|
||||
kind: kindFilter,
|
||||
contractEnded: null,
|
||||
@@ -180,7 +183,7 @@ export function PeopleList(props: {
|
||||
);
|
||||
});
|
||||
},
|
||||
[peoplePagination, order, stateFilter, roleFilter, kindFilter],
|
||||
[peoplePagination, order, statesFilter, roleFilter, kindFilter],
|
||||
),
|
||||
SEARCH_DEBOUNCE_MS,
|
||||
);
|
||||
@@ -190,10 +193,12 @@ export function PeopleList(props: {
|
||||
debouncedRefetchQuery(value);
|
||||
};
|
||||
|
||||
const handleStateFilterChange = (value: string) => {
|
||||
const newState = value === "ALL" ? null : (value as ProfileState);
|
||||
setStateFilter(newState);
|
||||
refetchPeople({ state: newState });
|
||||
const handleStateFilterToggle = (state: ProfileState) => {
|
||||
const newStates = statesFilter.includes(state)
|
||||
? statesFilter.filter(s => s !== state)
|
||||
: [...statesFilter, state];
|
||||
setStatesFilter(newStates);
|
||||
refetchPeople({ states: newStates });
|
||||
};
|
||||
|
||||
const handleRoleFilterChange = (value: string) => {
|
||||
@@ -226,15 +231,18 @@ export function PeopleList(props: {
|
||||
value={queryFilter ?? ""}
|
||||
onValueChange={handleQueryFilterChange}
|
||||
/>
|
||||
<Select
|
||||
value={stateFilter ?? "ALL"}
|
||||
onValueChange={handleStateFilterChange}
|
||||
>
|
||||
<Option value="ALL">{t("peopleList.filters.allStatuses")}</Option>
|
||||
<Option value="PENDING">{t("peopleList.filters.pending")}</Option>
|
||||
<Option value="ACTIVE">{t("peopleList.filters.active")}</Option>
|
||||
<Option value="DEACTIVATED">{t("peopleList.filters.deactivated")}</Option>
|
||||
</Select>
|
||||
<div className="flex items-center gap-3">
|
||||
<span className="text-sm text-txt-secondary">{t("peopleList.filters.status")}</span>
|
||||
{PROFILE_STATES.map(state => (
|
||||
<label key={state} className="flex items-center gap-2 text-sm cursor-pointer">
|
||||
<Checkbox
|
||||
checked={statesFilter.includes(state)}
|
||||
onChange={() => handleStateFilterToggle(state)}
|
||||
/>
|
||||
{t(`peopleList.filters.${state.toLowerCase()}`)}
|
||||
</label>
|
||||
))}
|
||||
</div>
|
||||
<Select
|
||||
value={roleFilter ?? "ALL"}
|
||||
onValueChange={handleRoleFilterChange}
|
||||
|
||||
@@ -146,7 +146,7 @@ export function PeopleListItem(props: {
|
||||
: [...availableRoles, profile.membership.role];
|
||||
|
||||
const isActive = profile.state === "ACTIVE";
|
||||
const isInactive = !isActive;
|
||||
const isInactive = profile.state === "DEACTIVATED";
|
||||
|
||||
const canSendActivationMail = !isActive && profile.source !== "SCIM" && profile.canInvite;
|
||||
const canDeactivate = profile.canDeactivate && profile.source !== "SCIM" && profile.state !== "DEACTIVATED";
|
||||
|
||||
@@ -46,10 +46,10 @@ export const documentLayoutQuery = graphql`
|
||||
...DocumentTitleFormFragment
|
||||
...DocumentActionsDropdown_versionFragment
|
||||
...DocumentDetailsCard_versionFragment
|
||||
signatures(first: 0 filter: { activeContract: true, profileState: ACTIVE }) {
|
||||
signatures(first: 0 filter: { activeContract: true, profileStates: [ACTIVE] }) {
|
||||
totalCount
|
||||
}
|
||||
signedSignatures: signatures(first: 0 filter: { states: [SIGNED], activeContract: true, profileState: ACTIVE }) {
|
||||
signedSignatures: signatures(first: 0 filter: { states: [SIGNED], activeContract: true, profileStates: [ACTIVE] }) {
|
||||
totalCount
|
||||
}
|
||||
approvalQuorums(first: 1, orderBy: { field: CREATED_AT, direction: DESC }) {
|
||||
@@ -91,10 +91,10 @@ export const documentLayoutQuery = graphql`
|
||||
...DocumentTitleFormFragment
|
||||
...DocumentActionsDropdown_versionFragment
|
||||
...DocumentDetailsCard_versionFragment
|
||||
signatures(first: 0 filter: { activeContract: true, profileState: ACTIVE }) {
|
||||
signatures(first: 0 filter: { activeContract: true, profileStates: [ACTIVE] }) {
|
||||
totalCount
|
||||
}
|
||||
signedSignatures: signatures(first: 0 filter: { states: [SIGNED], activeContract: true, profileState: ACTIVE }) {
|
||||
signedSignatures: signatures(first: 0 filter: { states: [SIGNED], activeContract: true, profileStates: [ACTIVE] }) {
|
||||
totalCount
|
||||
}
|
||||
approvalQuorums(first: 1, orderBy: { field: CREATED_AT, direction: DESC }) {
|
||||
|
||||
@@ -79,10 +79,10 @@ const fragment = graphql`
|
||||
}
|
||||
}
|
||||
}
|
||||
signatures(first: 0 filter: { activeContract: true, profileState: ACTIVE }) {
|
||||
signatures(first: 0 filter: { activeContract: true, profileStates: [ACTIVE] }) {
|
||||
totalCount
|
||||
}
|
||||
signedSignatures: signatures(first: 0 filter: { states: [SIGNED], activeContract: true, profileState: ACTIVE }) {
|
||||
signedSignatures: signatures(first: 0 filter: { states: [SIGNED], activeContract: true, profileStates: [ACTIVE] }) {
|
||||
totalCount
|
||||
}
|
||||
}
|
||||
|
||||
@@ -210,7 +210,7 @@ function PeopleList({
|
||||
signatureDocumentsDialogPeopleQuery,
|
||||
{
|
||||
organizationId,
|
||||
filter: { contractEnded: false, state: "ACTIVE" },
|
||||
filter: { contractEnded: false, states: ["ACTIVE"] },
|
||||
},
|
||||
);
|
||||
const {
|
||||
|
||||
@@ -33,7 +33,7 @@ export const documentSignaturesPageQuery = graphql`
|
||||
query DocumentSignaturesPageQuery($documentId: ID! $organizationId: ID! $versionId: ID! $versionSpecified: Boolean!) {
|
||||
organization: node(id: $organizationId) {
|
||||
__typename
|
||||
...DocumentSignatureList_peopleFragment @arguments(filter: { contractEnded: false, state: ACTIVE })
|
||||
...DocumentSignatureList_peopleFragment @arguments(filter: { contractEnded: false, states: [ACTIVE] })
|
||||
}
|
||||
# We use this on /documents/:documentId
|
||||
document: node(id: $documentId) @skip(if: $versionSpecified) {
|
||||
|
||||
@@ -35,7 +35,7 @@ const versionFragment = graphql`
|
||||
@argumentDefinitions(
|
||||
count: { type: "Int", defaultValue: 1000 }
|
||||
cursor: { type: "CursorKey" }
|
||||
signatureFilter: { type: "DocumentVersionSignatureFilter", defaultValue: { activeContract: true, profileState: ACTIVE } }
|
||||
signatureFilter: { type: "DocumentVersionSignatureFilter", defaultValue: { activeContract: true, profileStates: [ACTIVE] } }
|
||||
) {
|
||||
...DocumentSignaturePlaceholder_versionFragment
|
||||
signatures(first: $count, after: $cursor, filter: $signatureFilter)
|
||||
@@ -110,7 +110,7 @@ export function DocumentSignatureList(props: {
|
||||
|
||||
const filter = {
|
||||
activeContract: true,
|
||||
profileState: "ACTIVE" as const,
|
||||
profileStates: ["ACTIVE" as const],
|
||||
...(selectedStates.length > 0 ? { states: selectedStates } : {}),
|
||||
};
|
||||
|
||||
|
||||
Reference in New Issue
Block a user