Add search and status/role/type filters to People
Makes it practical to find people in larger orgs across GraphQL, MCP, CLI, and n8n, with page size raised to 100. Signed-off-by: Sacha Al Himdani <sacha@probo.com>
This commit is contained in:
@@ -1019,6 +1019,13 @@
|
|||||||
"contractor": "Contractor",
|
"contractor": "Contractor",
|
||||||
"serviceAccount": "Service account"
|
"serviceAccount": "Service account"
|
||||||
},
|
},
|
||||||
|
"membershipRole": {
|
||||||
|
"owner": "Owner",
|
||||||
|
"admin": "Admin",
|
||||||
|
"viewer": "Viewer",
|
||||||
|
"auditor": "Auditor",
|
||||||
|
"employee": "Employee"
|
||||||
|
},
|
||||||
"registryStatus": {
|
"registryStatus": {
|
||||||
"open": "Open",
|
"open": "Open",
|
||||||
"inProgress": "In Progress",
|
"inProgress": "In Progress",
|
||||||
@@ -1351,20 +1358,20 @@
|
|||||||
"addPersonDialog": { "title": "Add Person" },
|
"addPersonDialog": { "title": "Add Person" },
|
||||||
"peopleList": {
|
"peopleList": {
|
||||||
"columns": { "name": "Name", "status": "Status", "email": "Email", "role": "Role", "createdOn": "Created on" },
|
"columns": { "name": "Name", "status": "Status", "email": "Email", "role": "Role", "createdOn": "Created on" },
|
||||||
"empty": "No people"
|
"empty": "No people",
|
||||||
|
"searchPlaceholder": "Search people...",
|
||||||
|
"filters": { "allStatuses": "All statuses", "active": "Active", "inactive": "Inactive", "allRoles": "All roles", "allTypes": "All types" }
|
||||||
},
|
},
|
||||||
"peopleListItem": {
|
"peopleListItem": {
|
||||||
"messages": { "invitationSent": "Invitation sent successfully", "roleUpdated": "Role updated successfully", "archived": "Person archived successfully", "removed": "Person removed successfully" },
|
"messages": { "invitationSent": "Invitation sent successfully", "roleUpdated": "Role updated successfully", "archived": "Person archived successfully", "removed": "Person removed successfully" },
|
||||||
"errors": { "sendInvitation": "Failed to send invitation", "updateRole": "Failed to update role", "archive": "Failed to archive person", "remove": "Failed to remove person" },
|
"errors": { "sendInvitation": "Failed to send invitation", "updateRole": "Failed to update role", "archive": "Failed to archive person", "remove": "Failed to remove person" },
|
||||||
"confirmations": { "sendActivationEmail": "Send the activation email to {{name}}?", "archive": "Are you sure you want to archive {{name}}?", "remove": "Are you sure you want to remove {{name}}?" },
|
"confirmations": { "sendActivationEmail": "Send the activation email to {{name}}?", "archive": "Are you sure you want to archive {{name}}?", "remove": "Are you sure you want to remove {{name}}?" },
|
||||||
"roles": { "owner": "Owner", "admin": "Admin", "viewer": "Viewer", "auditor": "Auditor", "employee": "Employee" },
|
|
||||||
"actions": { "send": "Send", "sendActivationMail": "Send activation mail", "resendActivationMail": "Resend activation mail", "archivePerson": "Archive person", "removePerson": "Remove person" }
|
"actions": { "send": "Send", "sendActivationMail": "Send activation mail", "resendActivationMail": "Resend activation mail", "archivePerson": "Archive person", "removePerson": "Remove person" }
|
||||||
},
|
},
|
||||||
"personForm": {
|
"personForm": {
|
||||||
"messages": { "created": "Person created successfully.", "updated": "Person updated successfully." },
|
"messages": { "created": "Person created successfully.", "updated": "Person updated successfully." },
|
||||||
"errors": { "create": "Failed to create person", "update": "Failed to update person" },
|
"errors": { "create": "Failed to create person", "update": "Failed to update person" },
|
||||||
"fields": { "fullName": "Full name *", "emailAddress": "Email Address *", "role": "Role *", "type": "Type", "position": "Position", "positionPlaceholder": "e.g. CEO, CFO, etc.", "contractStartDate": "Contract start date", "contractEndDate": "Contract end date" },
|
"fields": { "fullName": "Full name *", "emailAddress": "Email Address *", "role": "Role *", "type": "Type", "position": "Position", "positionPlaceholder": "e.g. CEO, CFO, etc.", "contractStartDate": "Contract start date", "contractEndDate": "Contract end date" },
|
||||||
"roles": { "owner": "Owner", "admin": "Admin", "viewer": "Viewer", "auditor": "Auditor", "employee": "Employee" },
|
|
||||||
"roleDescriptions": { "owner": "Full access to everything", "admin": "Full access except organization setup and API keys", "viewer": "Read-only access", "auditor": "Read-only access without settings and tasks", "employee": "Access to employee page" },
|
"roleDescriptions": { "owner": "Full access to everything", "admin": "Full access except organization setup and API keys", "viewer": "Read-only access", "auditor": "Read-only access without settings and tasks", "employee": "Access to employee page" },
|
||||||
"kinds": { "EMPLOYEE": "Employee", "CONTRACTOR": "Contractor", "SERVICE_ACCOUNT": "Service account" },
|
"kinds": { "EMPLOYEE": "Employee", "CONTRACTOR": "Contractor", "SERVICE_ACCOUNT": "Service account" },
|
||||||
"actions": { "update": "Update", "create": "Create" }
|
"actions": { "update": "Update", "create": "Create" }
|
||||||
|
|||||||
@@ -1676,6 +1676,13 @@
|
|||||||
"contractor": "Prestataire",
|
"contractor": "Prestataire",
|
||||||
"serviceAccount": "Compte de service"
|
"serviceAccount": "Compte de service"
|
||||||
},
|
},
|
||||||
|
"membershipRole": {
|
||||||
|
"owner": "Propriétaire",
|
||||||
|
"admin": "Administrateur",
|
||||||
|
"viewer": "Lecteur",
|
||||||
|
"auditor": "Auditeur",
|
||||||
|
"employee": "Employé"
|
||||||
|
},
|
||||||
"registryStatus": {
|
"registryStatus": {
|
||||||
"open": "Ouvert",
|
"open": "Ouvert",
|
||||||
"inProgress": "En cours",
|
"inProgress": "En cours",
|
||||||
@@ -2419,7 +2426,15 @@
|
|||||||
"role": "Rôle",
|
"role": "Rôle",
|
||||||
"createdOn": "Créé le"
|
"createdOn": "Créé le"
|
||||||
},
|
},
|
||||||
"empty": "Aucune personne"
|
"empty": "Aucune personne",
|
||||||
|
"searchPlaceholder": "Rechercher des personnes...",
|
||||||
|
"filters": {
|
||||||
|
"allStatuses": "Tous les statuts",
|
||||||
|
"active": "Actif",
|
||||||
|
"inactive": "Inactif",
|
||||||
|
"allRoles": "Tous les rôles",
|
||||||
|
"allTypes": "Tous les types"
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"peopleListItem": {
|
"peopleListItem": {
|
||||||
"messages": {
|
"messages": {
|
||||||
@@ -2439,13 +2454,6 @@
|
|||||||
"archive": "Voulez-vous vraiment archiver {{name}} ?",
|
"archive": "Voulez-vous vraiment archiver {{name}} ?",
|
||||||
"remove": "Voulez-vous vraiment supprimer {{name}} ?"
|
"remove": "Voulez-vous vraiment supprimer {{name}} ?"
|
||||||
},
|
},
|
||||||
"roles": {
|
|
||||||
"owner": "Propriétaire",
|
|
||||||
"admin": "Administrateur",
|
|
||||||
"viewer": "Lecteur",
|
|
||||||
"auditor": "Auditeur",
|
|
||||||
"employee": "Employé"
|
|
||||||
},
|
|
||||||
"actions": {
|
"actions": {
|
||||||
"send": "Envoyer",
|
"send": "Envoyer",
|
||||||
"sendActivationMail": "Envoyer l’e-mail d’activation",
|
"sendActivationMail": "Envoyer l’e-mail d’activation",
|
||||||
@@ -2473,13 +2481,6 @@
|
|||||||
"contractStartDate": "Date de début de contrat",
|
"contractStartDate": "Date de début de contrat",
|
||||||
"contractEndDate": "Date de fin de contrat"
|
"contractEndDate": "Date de fin de contrat"
|
||||||
},
|
},
|
||||||
"roles": {
|
|
||||||
"owner": "Propriétaire",
|
|
||||||
"admin": "Administrateur",
|
|
||||||
"viewer": "Lecteur",
|
|
||||||
"auditor": "Auditeur",
|
|
||||||
"employee": "Employé"
|
|
||||||
},
|
|
||||||
"roleDescriptions": {
|
"roleDescriptions": {
|
||||||
"owner": "Accès complet à tout",
|
"owner": "Accès complet à tout",
|
||||||
"admin": "Accès complet sauf la configuration de l’organisation et les clés API",
|
"admin": "Accès complet sauf la configuration de l’organisation et les clés API",
|
||||||
|
|||||||
@@ -37,7 +37,7 @@ export const peoplePageQuery = graphql`
|
|||||||
... on Organization {
|
... on Organization {
|
||||||
canCreateUser: permission(action: "iam:membership-profile:create", attributes: { target_role: "VIEWER" })
|
canCreateUser: permission(action: "iam:membership-profile:create", attributes: { target_role: "VIEWER" })
|
||||||
...PeopleListFragment
|
...PeopleListFragment
|
||||||
@arguments(first: 20, order: { direction: ASC, field: FULL_NAME })
|
@arguments(first: 100, order: { direction: ASC, field: FULL_NAME })
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -55,7 +55,10 @@ export function PeoplePage(props: {
|
|||||||
ConnectionHandler.getConnectionID(
|
ConnectionHandler.getConnectionID(
|
||||||
organizationId,
|
organizationId,
|
||||||
"PeopleListFragment_profiles",
|
"PeopleListFragment_profiles",
|
||||||
{ orderBy: { direction: "ASC", field: "FULL_NAME" } },
|
{
|
||||||
|
orderBy: { direction: "ASC", field: "FULL_NAME" },
|
||||||
|
filter: null,
|
||||||
|
},
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@@ -18,30 +18,51 @@
|
|||||||
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
// SOFTWARE.
|
// SOFTWARE.
|
||||||
|
|
||||||
import { getAssignableRoles } from "@probo/helpers";
|
import { getAssignableRoles, getMembershipRoles, peopleRoles } from "@probo/helpers";
|
||||||
import { Tbody, Td, Th, Thead, Tr } from "@probo/ui";
|
import {
|
||||||
|
IconMagnifyingGlass,
|
||||||
|
Input,
|
||||||
|
Option,
|
||||||
|
Select,
|
||||||
|
Tbody,
|
||||||
|
Td,
|
||||||
|
Th,
|
||||||
|
Thead,
|
||||||
|
Tr,
|
||||||
|
} from "@probo/ui";
|
||||||
import type { ComponentProps } from "react";
|
import type { ComponentProps } from "react";
|
||||||
import { use } from "react";
|
import { use, useCallback, useEffect, useState, useTransition } from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { ConnectionHandler, graphql, usePaginationFragment } from "react-relay";
|
import { graphql, usePaginationFragment } from "react-relay";
|
||||||
|
import { useDebounceCallback } from "usehooks-ts";
|
||||||
|
|
||||||
import type { PeopleListFragment$key } from "#/__generated__/iam/PeopleListFragment.graphql";
|
import type { PeopleListFragment$key } from "#/__generated__/iam/PeopleListFragment.graphql";
|
||||||
import type { PeopleListFragment_RefetchQuery } from "#/__generated__/iam/PeopleListFragment_RefetchQuery.graphql";
|
import type {
|
||||||
|
MembershipRole,
|
||||||
|
PeopleListFragment_RefetchQuery,
|
||||||
|
ProfileOrderField,
|
||||||
|
ProfileState,
|
||||||
|
} from "#/__generated__/iam/PeopleListFragment_RefetchQuery.graphql";
|
||||||
import { type Order, SortableTable, SortableTh } from "#/components/SortableTable";
|
import { type Order, SortableTable, SortableTh } from "#/components/SortableTable";
|
||||||
import { useOrganizationId } from "#/hooks/useOrganizationId";
|
|
||||||
import { CurrentUser } from "#/providers/CurrentUser";
|
import { CurrentUser } from "#/providers/CurrentUser";
|
||||||
|
|
||||||
import { PeopleListItem } from "./PeopleListItem";
|
import { PeopleListItem } from "./PeopleListItem";
|
||||||
|
|
||||||
|
const PAGE_SIZE = 100;
|
||||||
|
const SEARCH_DEBOUNCE_MS = 300;
|
||||||
|
|
||||||
|
type PeopleKind = (typeof peopleRoles)[number];
|
||||||
|
|
||||||
const fragment = graphql`
|
const fragment = graphql`
|
||||||
fragment PeopleListFragment on Organization
|
fragment PeopleListFragment on Organization
|
||||||
@refetchable(queryName: "PeopleListFragment_RefetchQuery")
|
@refetchable(queryName: "PeopleListFragment_RefetchQuery")
|
||||||
@argumentDefinitions(
|
@argumentDefinitions(
|
||||||
first: { type: "Int", defaultValue: 20 }
|
first: { type: "Int", defaultValue: 100 }
|
||||||
order: {
|
order: {
|
||||||
type: "ProfileOrder"
|
type: "ProfileOrder"
|
||||||
defaultValue: { direction: ASC, field: FULL_NAME }
|
defaultValue: { direction: ASC, field: FULL_NAME }
|
||||||
}
|
}
|
||||||
|
filter: { type: "ProfileFilter", defaultValue: null }
|
||||||
after: { type: "CursorKey", defaultValue: null }
|
after: { type: "CursorKey", defaultValue: null }
|
||||||
before: { type: "CursorKey", defaultValue: null }
|
before: { type: "CursorKey", defaultValue: null }
|
||||||
last: { type: "Int", defaultValue: null }
|
last: { type: "Int", defaultValue: null }
|
||||||
@@ -52,7 +73,8 @@ const fragment = graphql`
|
|||||||
last: $last
|
last: $last
|
||||||
before: $before
|
before: $before
|
||||||
orderBy: $order
|
orderBy: $order
|
||||||
) @connection(key: "PeopleListFragment_profiles", filters: ["orderBy"]) @required(action: THROW) {
|
filter: $filter
|
||||||
|
) @connection(key: "PeopleListFragment_profiles", filters: ["orderBy", "filter"]) @required(action: THROW) {
|
||||||
__id
|
__id
|
||||||
totalCount
|
totalCount
|
||||||
edges @required(action: THROW) {
|
edges @required(action: THROW) {
|
||||||
@@ -65,45 +87,182 @@ const fragment = graphql`
|
|||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
type PeopleFilter = {
|
||||||
|
query: string | null;
|
||||||
|
state: ProfileState | null;
|
||||||
|
role: MembershipRole | null;
|
||||||
|
kind: string | null;
|
||||||
|
};
|
||||||
|
|
||||||
export function PeopleList(props: {
|
export function PeopleList(props: {
|
||||||
fKey: PeopleListFragment$key;
|
fKey: PeopleListFragment$key;
|
||||||
onConnectionIdChange: (connectionId: string) => void;
|
onConnectionIdChange: (connectionId: string) => void;
|
||||||
}) {
|
}) {
|
||||||
const { fKey, onConnectionIdChange } = props;
|
const { fKey, onConnectionIdChange } = props;
|
||||||
|
|
||||||
const organizationId = useOrganizationId();
|
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const { role } = use(CurrentUser);
|
const { role } = use(CurrentUser);
|
||||||
const canManageRoles = getAssignableRoles(role).length > 0;
|
const canManageRoles = getAssignableRoles(role).length > 0;
|
||||||
|
|
||||||
|
const [queryFilter, setQueryFilter] = useState<string | null>(null);
|
||||||
|
const [stateFilter, setStateFilter] = useState<ProfileState | null>(null);
|
||||||
|
const [roleFilter, setRoleFilter] = useState<MembershipRole | null>(null);
|
||||||
|
const [kindFilter, setKindFilter] = useState<string | null>(null);
|
||||||
|
const [order, setOrder] = useState<Order>({
|
||||||
|
direction: "ASC",
|
||||||
|
field: "FULL_NAME",
|
||||||
|
});
|
||||||
|
const [isPending, startTransition] = useTransition();
|
||||||
|
|
||||||
const peoplePagination = usePaginationFragment<
|
const peoplePagination = usePaginationFragment<
|
||||||
PeopleListFragment_RefetchQuery,
|
PeopleListFragment_RefetchQuery,
|
||||||
PeopleListFragment$key
|
PeopleListFragment$key
|
||||||
>(fragment, fKey);
|
>(fragment, fKey);
|
||||||
|
|
||||||
const refetchPeople = () => {
|
const connectionId = peoplePagination.data.profiles.__id;
|
||||||
peoplePagination.refetch({}, { fetchPolicy: "network-only" });
|
|
||||||
|
useEffect(() => {
|
||||||
|
onConnectionIdChange(connectionId);
|
||||||
|
}, [connectionId, onConnectionIdChange]);
|
||||||
|
|
||||||
|
const currentFilter = (overrides: Partial<PeopleFilter> = {}): PeopleFilter => ({
|
||||||
|
query: queryFilter,
|
||||||
|
state: stateFilter,
|
||||||
|
role: roleFilter,
|
||||||
|
kind: kindFilter,
|
||||||
|
...overrides,
|
||||||
|
});
|
||||||
|
|
||||||
|
const connectionFilter = (filter: PeopleFilter) => ({
|
||||||
|
query: filter.query,
|
||||||
|
state: filter.state,
|
||||||
|
role: filter.role,
|
||||||
|
kind: filter.kind,
|
||||||
|
contractEnded: null,
|
||||||
|
});
|
||||||
|
|
||||||
|
const refetchPeople = (overrides: Partial<PeopleFilter> = {}, nextOrder: Order = order) => {
|
||||||
|
const filter = currentFilter(overrides);
|
||||||
|
startTransition(() => {
|
||||||
|
peoplePagination.refetch(
|
||||||
|
{
|
||||||
|
order: {
|
||||||
|
direction: nextOrder.direction,
|
||||||
|
field: nextOrder.field as ProfileOrderField,
|
||||||
|
},
|
||||||
|
filter: connectionFilter(filter),
|
||||||
|
},
|
||||||
|
{ fetchPolicy: "network-only" },
|
||||||
|
);
|
||||||
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleOrderChange = (order: Order) => {
|
const debouncedRefetchQuery = useDebounceCallback(
|
||||||
onConnectionIdChange(
|
useCallback(
|
||||||
ConnectionHandler.getConnectionID(
|
(value: string) => {
|
||||||
organizationId,
|
const newQuery = value === "" ? null : value;
|
||||||
"PeopleListFragment_profiles",
|
startTransition(() => {
|
||||||
{ orderBy: order },
|
peoplePagination.refetch(
|
||||||
),
|
{
|
||||||
|
order: {
|
||||||
|
direction: order.direction,
|
||||||
|
field: order.field as ProfileOrderField,
|
||||||
|
},
|
||||||
|
filter: {
|
||||||
|
query: newQuery,
|
||||||
|
state: stateFilter,
|
||||||
|
role: roleFilter,
|
||||||
|
kind: kindFilter,
|
||||||
|
contractEnded: null,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{ fetchPolicy: "network-only" },
|
||||||
);
|
);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
[peoplePagination, order, stateFilter, roleFilter, kindFilter],
|
||||||
|
),
|
||||||
|
SEARCH_DEBOUNCE_MS,
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleQueryFilterChange = (value: string) => {
|
||||||
|
setQueryFilter(value === "" ? null : value);
|
||||||
|
debouncedRefetchQuery(value);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleStateFilterChange = (value: string) => {
|
||||||
|
const newState = value === "ALL" ? null : (value as ProfileState);
|
||||||
|
setStateFilter(newState);
|
||||||
|
refetchPeople({ state: newState });
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleRoleFilterChange = (value: string) => {
|
||||||
|
const newRole = value === "ALL" ? null : (value as MembershipRole);
|
||||||
|
setRoleFilter(newRole);
|
||||||
|
refetchPeople({ role: newRole });
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleKindFilterChange = (value: string) => {
|
||||||
|
const newKind = value === "ALL" ? null : value;
|
||||||
|
setKindFilter(newKind);
|
||||||
|
refetchPeople({ kind: newKind });
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleOrderChange = (nextOrder: Order) => {
|
||||||
|
setOrder(nextOrder);
|
||||||
|
};
|
||||||
|
|
||||||
|
const refetchWithFilters: ComponentProps<typeof SortableTable>["refetch"] = ({ order: nextOrder }) => {
|
||||||
|
setOrder(nextOrder);
|
||||||
|
refetchPeople({}, nextOrder);
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
<div className="space-y-4">
|
||||||
|
<div className="flex items-center gap-4">
|
||||||
|
<Input
|
||||||
|
icon={IconMagnifyingGlass}
|
||||||
|
placeholder={t("peopleList.searchPlaceholder")}
|
||||||
|
value={queryFilter ?? ""}
|
||||||
|
onValueChange={handleQueryFilterChange}
|
||||||
|
/>
|
||||||
|
<Select
|
||||||
|
value={stateFilter ?? "ALL"}
|
||||||
|
onValueChange={handleStateFilterChange}
|
||||||
|
>
|
||||||
|
<Option value="ALL">{t("peopleList.filters.allStatuses")}</Option>
|
||||||
|
<Option value="ACTIVE">{t("peopleList.filters.active")}</Option>
|
||||||
|
<Option value="INACTIVE">{t("peopleList.filters.inactive")}</Option>
|
||||||
|
</Select>
|
||||||
|
<Select
|
||||||
|
value={roleFilter ?? "ALL"}
|
||||||
|
onValueChange={handleRoleFilterChange}
|
||||||
|
>
|
||||||
|
<Option value="ALL">{t("peopleList.filters.allRoles")}</Option>
|
||||||
|
{getMembershipRoles(t).map(({ value, label }) => (
|
||||||
|
<Option key={value} value={value}>
|
||||||
|
{label}
|
||||||
|
</Option>
|
||||||
|
))}
|
||||||
|
</Select>
|
||||||
|
<Select
|
||||||
|
value={kindFilter ?? "ALL"}
|
||||||
|
onValueChange={handleKindFilterChange}
|
||||||
|
>
|
||||||
|
<Option value="ALL">{t("peopleList.filters.allTypes")}</Option>
|
||||||
|
{peopleRoles.map((kind: PeopleKind) => (
|
||||||
|
<Option key={kind} value={kind}>
|
||||||
|
{t(`personForm.kinds.${kind}`)}
|
||||||
|
</Option>
|
||||||
|
))}
|
||||||
|
</Select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className={isPending ? "opacity-50 pointer-events-none transition-opacity" : ""}>
|
||||||
<SortableTable
|
<SortableTable
|
||||||
{...peoplePagination}
|
{...peoplePagination}
|
||||||
refetch={
|
refetch={refetchWithFilters}
|
||||||
peoplePagination.refetch as ComponentProps<
|
pageSize={PAGE_SIZE}
|
||||||
typeof SortableTable
|
|
||||||
>["refetch"]
|
|
||||||
}
|
|
||||||
pageSize={20}
|
|
||||||
>
|
>
|
||||||
<Thead>
|
<Thead>
|
||||||
<Tr>
|
<Tr>
|
||||||
@@ -127,14 +286,16 @@ export function PeopleList(props: {
|
|||||||
: (
|
: (
|
||||||
peoplePagination.data.profiles.edges.map(({ node: profile }) => (
|
peoplePagination.data.profiles.edges.map(({ node: profile }) => (
|
||||||
<PeopleListItem
|
<PeopleListItem
|
||||||
connectionId={peoplePagination.data.profiles.__id}
|
connectionId={connectionId}
|
||||||
key={profile.id}
|
key={profile.id}
|
||||||
fKey={profile}
|
fKey={profile}
|
||||||
onRefetch={refetchPeople}
|
onRefetch={() => refetchPeople()}
|
||||||
/>
|
/>
|
||||||
))
|
))
|
||||||
)}
|
)}
|
||||||
</Tbody>
|
</Tbody>
|
||||||
</SortableTable>
|
</SortableTable>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -18,7 +18,7 @@
|
|||||||
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
// SOFTWARE.
|
// SOFTWARE.
|
||||||
|
|
||||||
import { getAssignableRoles } from "@probo/helpers";
|
import { getAssignableRoles, getMembershipRoles } from "@probo/helpers";
|
||||||
import { dateFormat } from "@probo/i18n";
|
import { dateFormat } from "@probo/i18n";
|
||||||
import {
|
import {
|
||||||
ActionDropdown,
|
ActionDropdown,
|
||||||
@@ -288,21 +288,13 @@ export function PeopleListItem(props: {
|
|||||||
value={profile.membership.role}
|
value={profile.membership.role}
|
||||||
onValueChange={role => void handleUpdateRole(role)}
|
onValueChange={role => void handleUpdateRole(role)}
|
||||||
>
|
>
|
||||||
{roleOptions.includes("OWNER") && (
|
{getMembershipRoles(t)
|
||||||
<Option value="OWNER">{t("peopleListItem.roles.owner")}</Option>
|
.filter(({ value }) => roleOptions.includes(value))
|
||||||
)}
|
.map(({ value, label }) => (
|
||||||
{roleOptions.includes("ADMIN") && (
|
<Option key={value} value={value}>
|
||||||
<Option value="ADMIN">{t("peopleListItem.roles.admin")}</Option>
|
{label}
|
||||||
)}
|
</Option>
|
||||||
{roleOptions.includes("VIEWER") && (
|
))}
|
||||||
<Option value="VIEWER">{t("peopleListItem.roles.viewer")}</Option>
|
|
||||||
)}
|
|
||||||
{roleOptions.includes("AUDITOR") && (
|
|
||||||
<Option value="AUDITOR">{t("peopleListItem.roles.auditor")}</Option>
|
|
||||||
)}
|
|
||||||
{roleOptions.includes("EMPLOYEE") && (
|
|
||||||
<Option value="EMPLOYEE">{t("peopleListItem.roles.employee")}</Option>
|
|
||||||
)}
|
|
||||||
</Select>
|
</Select>
|
||||||
</Td>
|
</Td>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -18,8 +18,7 @@
|
|||||||
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
// SOFTWARE.
|
// SOFTWARE.
|
||||||
|
|
||||||
import { formatDatetime, getAssignableRoles, peopleRoles } from "@probo/helpers";
|
import { formatDatetime, getAssignableRoles, getMembershipRoles, peopleRoles, roles } from "@probo/helpers";
|
||||||
import { roles } from "@probo/helpers/src/roles";
|
|
||||||
import { Button, Field, Input, Option } from "@probo/ui";
|
import { Button, Field, Input, Option } from "@probo/ui";
|
||||||
import { use } from "react";
|
import { use } from "react";
|
||||||
import { useWatch } from "react-hook-form";
|
import { useWatch } from "react-hook-form";
|
||||||
@@ -202,21 +201,13 @@ export function PersonForm(props: {
|
|||||||
label={t("personForm.fields.role")}
|
label={t("personForm.fields.role")}
|
||||||
disabled={disabled || !!id}
|
disabled={disabled || !!id}
|
||||||
>
|
>
|
||||||
{availableRoles.includes("OWNER") && (
|
{getMembershipRoles(t)
|
||||||
<Option value="OWNER">{t("personForm.roles.owner")}</Option>
|
.filter(({ value }) => availableRoles.includes(value))
|
||||||
)}
|
.map(({ value, label }) => (
|
||||||
{availableRoles.includes("ADMIN") && (
|
<Option key={value} value={value}>
|
||||||
<Option value="ADMIN">{t("personForm.roles.admin")}</Option>
|
{label}
|
||||||
)}
|
</Option>
|
||||||
{availableRoles.includes("VIEWER") && (
|
))}
|
||||||
<Option value="VIEWER">{t("personForm.roles.viewer")}</Option>
|
|
||||||
)}
|
|
||||||
{availableRoles.includes("AUDITOR") && (
|
|
||||||
<Option value="AUDITOR">{t("personForm.roles.auditor")}</Option>
|
|
||||||
)}
|
|
||||||
{availableRoles.includes("EMPLOYEE") && (
|
|
||||||
<Option value="EMPLOYEE">{t("personForm.roles.employee")}</Option>
|
|
||||||
)}
|
|
||||||
</ControlledField>
|
</ControlledField>
|
||||||
|
|
||||||
<div className="mt-4 space-y-2 text-sm text-txt-tertiary">
|
<div className="mt-4 space-y-2 text-sm text-txt-tertiary">
|
||||||
|
|||||||
@@ -120,7 +120,13 @@ export {
|
|||||||
export { getTrackerTypeBadge, getTrackerSourceBadge } from "./tracker";
|
export { getTrackerTypeBadge, getTrackerSourceBadge } from "./tracker";
|
||||||
export { detectSocialName } from "./socialUrl";
|
export { detectSocialName } from "./socialUrl";
|
||||||
export { formatError, type GraphQLError } from "./error";
|
export { formatError, type GraphQLError } from "./error";
|
||||||
export { Role, roles, getAssignableRoles } from "./roles";
|
export {
|
||||||
|
Role,
|
||||||
|
roles,
|
||||||
|
getAssignableRoles,
|
||||||
|
getMembershipRole,
|
||||||
|
getMembershipRoles,
|
||||||
|
} from "./roles";
|
||||||
export {
|
export {
|
||||||
getCompliancePortalDocumentAccessStatusBadgeVariant,
|
getCompliancePortalDocumentAccessStatusBadgeVariant,
|
||||||
getCompliancePortalDocumentAccessStatusLabel,
|
getCompliancePortalDocumentAccessStatusLabel,
|
||||||
|
|||||||
@@ -18,6 +18,8 @@
|
|||||||
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
// SOFTWARE.
|
// SOFTWARE.
|
||||||
|
|
||||||
|
type Translator = (s: string) => string;
|
||||||
|
|
||||||
export const Role = {
|
export const Role = {
|
||||||
OWNER: "OWNER",
|
OWNER: "OWNER",
|
||||||
ADMIN: "ADMIN",
|
ADMIN: "ADMIN",
|
||||||
@@ -46,3 +48,45 @@ export function getAssignableRoles(currentRole: Role): Role[] {
|
|||||||
|
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function getMembershipRoles(t: Translator) {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
value: Role.OWNER,
|
||||||
|
label: t("helpers.membershipRole.owner"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: Role.ADMIN,
|
||||||
|
label: t("helpers.membershipRole.admin"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: Role.VIEWER,
|
||||||
|
label: t("helpers.membershipRole.viewer"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: Role.AUDITOR,
|
||||||
|
label: t("helpers.membershipRole.auditor"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: Role.EMPLOYEE,
|
||||||
|
label: t("helpers.membershipRole.employee"),
|
||||||
|
},
|
||||||
|
] as const;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getMembershipRole(t: Translator, role?: string): string {
|
||||||
|
switch (role) {
|
||||||
|
case Role.OWNER:
|
||||||
|
return t("helpers.membershipRole.owner");
|
||||||
|
case Role.ADMIN:
|
||||||
|
return t("helpers.membershipRole.admin");
|
||||||
|
case Role.VIEWER:
|
||||||
|
return t("helpers.membershipRole.viewer");
|
||||||
|
case Role.AUDITOR:
|
||||||
|
return t("helpers.membershipRole.auditor");
|
||||||
|
case Role.EMPLOYEE:
|
||||||
|
return t("helpers.membershipRole.employee");
|
||||||
|
default:
|
||||||
|
return t("helpers.common.unknown");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -71,6 +71,77 @@ export const description: INodeProperties[] = [
|
|||||||
default: 50,
|
default: 50,
|
||||||
description: 'Max number of results to return',
|
description: 'Max number of results to return',
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
displayName: 'Search',
|
||||||
|
name: 'query',
|
||||||
|
type: 'string',
|
||||||
|
displayOptions: {
|
||||||
|
show: {
|
||||||
|
resource: ['user'],
|
||||||
|
operation: ['listUsers'],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
default: '',
|
||||||
|
description: 'Search users by full name or email address',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
displayName: 'State',
|
||||||
|
name: 'state',
|
||||||
|
type: 'options',
|
||||||
|
displayOptions: {
|
||||||
|
show: {
|
||||||
|
resource: ['user'],
|
||||||
|
operation: ['listUsers'],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
options: [
|
||||||
|
{ name: 'Active', value: 'ACTIVE' },
|
||||||
|
{ name: 'All', value: '' },
|
||||||
|
{ name: 'Inactive', value: 'INACTIVE' },
|
||||||
|
],
|
||||||
|
default: '',
|
||||||
|
description: 'Filter by profile state',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
displayName: 'Role',
|
||||||
|
name: 'role',
|
||||||
|
type: 'options',
|
||||||
|
displayOptions: {
|
||||||
|
show: {
|
||||||
|
resource: ['user'],
|
||||||
|
operation: ['listUsers'],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
options: [
|
||||||
|
{ name: 'Admin', value: 'ADMIN' },
|
||||||
|
{ name: 'All', value: '' },
|
||||||
|
{ name: 'Auditor', value: 'AUDITOR' },
|
||||||
|
{ name: 'Employee', value: 'EMPLOYEE' },
|
||||||
|
{ name: 'Owner', value: 'OWNER' },
|
||||||
|
{ name: 'Viewer', value: 'VIEWER' },
|
||||||
|
],
|
||||||
|
default: '',
|
||||||
|
description: 'Filter by membership role',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
displayName: 'Type',
|
||||||
|
name: 'kind',
|
||||||
|
type: 'options',
|
||||||
|
displayOptions: {
|
||||||
|
show: {
|
||||||
|
resource: ['user'],
|
||||||
|
operation: ['listUsers'],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
options: [
|
||||||
|
{ name: 'All', value: '' },
|
||||||
|
{ name: 'Contractor', value: 'CONTRACTOR' },
|
||||||
|
{ name: 'Employee', value: 'EMPLOYEE' },
|
||||||
|
{ name: 'Service Account', value: 'SERVICE_ACCOUNT' },
|
||||||
|
],
|
||||||
|
default: '',
|
||||||
|
description: 'Filter by profile kind',
|
||||||
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
export async function execute(
|
export async function execute(
|
||||||
@@ -80,12 +151,16 @@ export async function execute(
|
|||||||
const organizationId = this.getNodeParameter('organizationId', itemIndex) as string;
|
const organizationId = this.getNodeParameter('organizationId', itemIndex) as string;
|
||||||
const returnAll = this.getNodeParameter('returnAll', itemIndex) as boolean;
|
const returnAll = this.getNodeParameter('returnAll', itemIndex) as boolean;
|
||||||
const limit = this.getNodeParameter('limit', itemIndex, 50) as number;
|
const limit = this.getNodeParameter('limit', itemIndex, 50) as number;
|
||||||
|
const query = this.getNodeParameter('query', itemIndex, '') as string;
|
||||||
|
const state = this.getNodeParameter('state', itemIndex, '') as string;
|
||||||
|
const role = this.getNodeParameter('role', itemIndex, '') as string;
|
||||||
|
const kind = this.getNodeParameter('kind', itemIndex, '') as string;
|
||||||
|
|
||||||
const query = `
|
const gqlQuery = `
|
||||||
query ListUsers($organizationId: ID!, $first: Int, $after: CursorKey, $orderBy: ProfileOrder) {
|
query ListUsers($organizationId: ID!, $first: Int, $after: CursorKey, $orderBy: ProfileOrder, $filter: ProfileFilter) {
|
||||||
node(id: $organizationId) {
|
node(id: $organizationId) {
|
||||||
... on Organization {
|
... on Organization {
|
||||||
profiles(first: $first, after: $after, orderBy: $orderBy) {
|
profiles(first: $first, after: $after, orderBy: $orderBy, filter: $filter) {
|
||||||
edges {
|
edges {
|
||||||
node {
|
node {
|
||||||
id
|
id
|
||||||
@@ -114,10 +189,27 @@ export async function execute(
|
|||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
const filter: IDataObject = {};
|
||||||
|
if (query) {
|
||||||
|
filter.query = query;
|
||||||
|
}
|
||||||
|
if (state) {
|
||||||
|
filter.state = state;
|
||||||
|
}
|
||||||
|
if (role) {
|
||||||
|
filter.role = role;
|
||||||
|
}
|
||||||
|
if (kind) {
|
||||||
|
filter.kind = kind;
|
||||||
|
}
|
||||||
|
|
||||||
const users = await proboConnectApiRequestAllItems.call(
|
const users = await proboConnectApiRequestAllItems.call(
|
||||||
this,
|
this,
|
||||||
query,
|
gqlQuery,
|
||||||
{ organizationId },
|
{
|
||||||
|
organizationId,
|
||||||
|
...(Object.keys(filter).length > 0 ? { filter } : {}),
|
||||||
|
},
|
||||||
(response: IDataObject) => {
|
(response: IDataObject) => {
|
||||||
const data = response?.data as IDataObject | undefined;
|
const data = response?.data as IDataObject | undefined;
|
||||||
const node = data?.node as IDataObject | undefined;
|
const node = data?.node as IDataObject | undefined;
|
||||||
|
|||||||
@@ -73,6 +73,9 @@ func NewCmdList(f *cmdutil.Factory) *cobra.Command {
|
|||||||
flagOrderDir string
|
flagOrderDir string
|
||||||
flagContractEnded string
|
flagContractEnded string
|
||||||
flagState string
|
flagState string
|
||||||
|
flagFilter string
|
||||||
|
flagRole string
|
||||||
|
flagKind string
|
||||||
flagOutput *string
|
flagOutput *string
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -86,6 +89,15 @@ func NewCmdList(f *cmdutil.Factory) *cobra.Command {
|
|||||||
# List only active users
|
# List only active users
|
||||||
prb user ls --state ACTIVE
|
prb user ls --state ACTIVE
|
||||||
|
|
||||||
|
# Search users by name or email
|
||||||
|
prb user ls --filter alice
|
||||||
|
|
||||||
|
# List only admins
|
||||||
|
prb user ls --role ADMIN
|
||||||
|
|
||||||
|
# Filter by type
|
||||||
|
prb user ls --kind EMPLOYEE
|
||||||
|
|
||||||
# List users whose contract has ended
|
# List users whose contract has ended
|
||||||
prb user ls --contract-ended true`,
|
prb user ls --contract-ended true`,
|
||||||
Args: cobra.NoArgs,
|
Args: cobra.NoArgs,
|
||||||
@@ -153,6 +165,26 @@ func NewCmdList(f *cmdutil.Factory) *cobra.Command {
|
|||||||
filter["state"] = flagState
|
filter["state"] = flagState
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if flagFilter != "" {
|
||||||
|
filter["query"] = flagFilter
|
||||||
|
}
|
||||||
|
|
||||||
|
if flagRole != "" {
|
||||||
|
if err := cmdutil.ValidateEnum("role", flagRole, []string{"OWNER", "ADMIN", "VIEWER", "AUDITOR", "EMPLOYEE"}); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
filter["role"] = flagRole
|
||||||
|
}
|
||||||
|
|
||||||
|
if flagKind != "" {
|
||||||
|
if err := cmdutil.ValidateEnum("kind", flagKind, []string{"EMPLOYEE", "CONTRACTOR", "SERVICE_ACCOUNT"}); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
filter["kind"] = flagKind
|
||||||
|
}
|
||||||
|
|
||||||
if len(filter) > 0 {
|
if len(filter) > 0 {
|
||||||
variables["filter"] = filter
|
variables["filter"] = filter
|
||||||
}
|
}
|
||||||
@@ -242,6 +274,9 @@ func NewCmdList(f *cmdutil.Factory) *cobra.Command {
|
|||||||
cmd.Flags().StringVar(&flagOrderDir, "order-direction", "DESC", "Sort direction (ASC, DESC)")
|
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(&flagContractEnded, "contract-ended", "", "Filter by contract status (true or false)")
|
||||||
cmd.Flags().StringVar(&flagState, "state", "", "Filter by profile state (ACTIVE or INACTIVE)")
|
cmd.Flags().StringVar(&flagState, "state", "", "Filter by profile state (ACTIVE or INACTIVE)")
|
||||||
|
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)")
|
||||||
flagOutput = cmdutil.AddOutputFlag(cmd)
|
flagOutput = cmdutil.AddOutputFlag(cmd)
|
||||||
|
|
||||||
return cmd
|
return cmd
|
||||||
|
|||||||
@@ -21,6 +21,7 @@
|
|||||||
package coredata
|
package coredata
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/jackc/pgx/v5"
|
"github.com/jackc/pgx/v5"
|
||||||
@@ -38,6 +39,9 @@ type (
|
|||||||
externalID *string
|
externalID *string
|
||||||
state *ProfileState
|
state *ProfileState
|
||||||
source *ProfileSource
|
source *ProfileSource
|
||||||
|
query *string
|
||||||
|
role *MembershipRole
|
||||||
|
kind *string
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -95,7 +99,45 @@ func (f *MembershipProfileFilter) Source() *ProfileSource {
|
|||||||
return f.source
|
return f.source
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (f *MembershipProfileFilter) WithQuery(query *string) *MembershipProfileFilter {
|
||||||
|
f.query = query
|
||||||
|
return f
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *MembershipProfileFilter) Query() *string {
|
||||||
|
return f.query
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *MembershipProfileFilter) WithRole(role MembershipRole) *MembershipProfileFilter {
|
||||||
|
f.role = &role
|
||||||
|
return f
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *MembershipProfileFilter) Role() *MembershipRole {
|
||||||
|
return f.role
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *MembershipProfileFilter) WithKind(kind *string) *MembershipProfileFilter {
|
||||||
|
f.kind = kind
|
||||||
|
return f
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *MembershipProfileFilter) Kind() *string {
|
||||||
|
return f.kind
|
||||||
|
}
|
||||||
|
|
||||||
|
func escapeLikePattern(s string) string {
|
||||||
|
return strings.NewReplacer(`\`, `\\`, `%`, `\%`, `_`, `\_`).Replace(s)
|
||||||
|
}
|
||||||
|
|
||||||
func (f *MembershipProfileFilter) SQLArguments() pgx.StrictNamedArgs {
|
func (f *MembershipProfileFilter) SQLArguments() pgx.StrictNamedArgs {
|
||||||
|
var filterQuery *string
|
||||||
|
|
||||||
|
if f.query != nil && *f.query != "" {
|
||||||
|
escaped := escapeLikePattern(*f.query)
|
||||||
|
filterQuery = &escaped
|
||||||
|
}
|
||||||
|
|
||||||
return pgx.StrictNamedArgs{
|
return pgx.StrictNamedArgs{
|
||||||
"filter_email": f.email,
|
"filter_email": f.email,
|
||||||
"filter_user_name": f.userName,
|
"filter_user_name": f.userName,
|
||||||
@@ -106,6 +148,9 @@ func (f *MembershipProfileFilter) SQLArguments() pgx.StrictNamedArgs {
|
|||||||
"current_date": f.currentDate,
|
"current_date": f.currentDate,
|
||||||
"filter_state": f.state,
|
"filter_state": f.state,
|
||||||
"filter_source": f.source,
|
"filter_source": f.source,
|
||||||
|
"filter_query": filterQuery,
|
||||||
|
"filter_role": f.role,
|
||||||
|
"filter_kind": f.kind,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -173,5 +218,36 @@ AND (
|
|||||||
ELSE TRUE
|
ELSE TRUE
|
||||||
END
|
END
|
||||||
)
|
)
|
||||||
|
AND (
|
||||||
|
CASE
|
||||||
|
WHEN @filter_query::text IS NOT NULL AND @filter_query::text <> '' THEN
|
||||||
|
(
|
||||||
|
p.full_name ILIKE '%' || @filter_query || '%' ESCAPE '\'
|
||||||
|
OR i.email_address ILIKE '%' || @filter_query || '%' ESCAPE '\'
|
||||||
|
OR p.position ILIKE '%' || @filter_query || '%' ESCAPE '\'
|
||||||
|
)
|
||||||
|
ELSE TRUE
|
||||||
|
END
|
||||||
|
)
|
||||||
|
AND (
|
||||||
|
CASE
|
||||||
|
WHEN @filter_role::authz_role IS NOT NULL THEN
|
||||||
|
p.identity_id IN (
|
||||||
|
SELECT identity_id
|
||||||
|
FROM iam_memberships
|
||||||
|
WHERE
|
||||||
|
organization_id = p.organization_id
|
||||||
|
AND role = @filter_role::authz_role
|
||||||
|
)
|
||||||
|
ELSE TRUE
|
||||||
|
END
|
||||||
|
)
|
||||||
|
AND (
|
||||||
|
CASE
|
||||||
|
WHEN @filter_kind::text IS NOT NULL AND @filter_kind::text <> '' THEN
|
||||||
|
p.kind = @filter_kind::text
|
||||||
|
ELSE TRUE
|
||||||
|
END
|
||||||
|
)
|
||||||
`
|
`
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ type Organization implements Node {
|
|||||||
last: Int
|
last: Int
|
||||||
before: CursorKey
|
before: CursorKey
|
||||||
orderBy: ProfileOrder
|
orderBy: ProfileOrder
|
||||||
|
filter: ProfileFilter
|
||||||
): ProfileConnection @goField(forceResolver: true)
|
): ProfileConnection @goField(forceResolver: true)
|
||||||
|
|
||||||
samlConfigurations(
|
samlConfigurations(
|
||||||
|
|||||||
@@ -71,6 +71,9 @@ enum ProfileOrderField
|
|||||||
input ProfileFilter {
|
input ProfileFilter {
|
||||||
contractEnded: Boolean
|
contractEnded: Boolean
|
||||||
state: ProfileState
|
state: ProfileState
|
||||||
|
query: String
|
||||||
|
role: MembershipRole
|
||||||
|
kind: String
|
||||||
}
|
}
|
||||||
|
|
||||||
input ProfileOrder
|
input ProfileOrder
|
||||||
|
|||||||
@@ -31,9 +31,22 @@ 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.ContractEnded).WithMembership()
|
filters = coredata.NewMembershipProfileFilter(filter.ContractEnded).WithMembership()
|
||||||
|
|
||||||
if filter.State != nil {
|
if filter.State != nil {
|
||||||
filters.WithState(*filter.State)
|
filters.WithState(*filter.State)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if filter.Query != nil {
|
||||||
|
filters.WithQuery(filter.Query)
|
||||||
|
}
|
||||||
|
|
||||||
|
if filter.Role != nil {
|
||||||
|
filters.WithRole(*filter.Role)
|
||||||
|
}
|
||||||
|
|
||||||
|
if filter.Kind != nil {
|
||||||
|
filters.WithKind(filter.Kind)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if gqlutils.OnlyTotalCountSelected(ctx) {
|
if gqlutils.OnlyTotalCountSelected(ctx) {
|
||||||
|
|||||||
@@ -179,18 +179,37 @@ func (r *organizationResolver) HorizontalLogo(ctx context.Context, obj *types.Or
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Profiles is the resolver for the profiles field.
|
// Profiles is the resolver for the profiles field.
|
||||||
func (r *organizationResolver) Profiles(ctx context.Context, obj *types.Organization, first *int, after *page.CursorKey, last *int, before *page.CursorKey, orderBy *types.ProfileOrderBy) (*types.ProfileConnection, error) {
|
func (r *organizationResolver) Profiles(ctx context.Context, obj *types.Organization, first *int, after *page.CursorKey, last *int, before *page.CursorKey, orderBy *types.ProfileOrderBy, filter *types.ProfileFilter) (*types.ProfileConnection, error) {
|
||||||
if _, err := r.authorize(ctx, obj.ID, iam.ActionMembershipProfileList); err != nil {
|
if _, err := r.authorize(ctx, obj.ID, iam.ActionMembershipProfileList); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
filter := coredata.NewMembershipProfileFilter(nil).WithMembership()
|
filters := coredata.NewMembershipProfileFilter(nil).WithMembership()
|
||||||
|
if filter != nil {
|
||||||
|
filters = coredata.NewMembershipProfileFilter(filter.ContractEnded).WithMembership()
|
||||||
|
|
||||||
|
if filter.State != nil {
|
||||||
|
filters.WithState(*filter.State)
|
||||||
|
}
|
||||||
|
|
||||||
|
if filter.Query != nil {
|
||||||
|
filters.WithQuery(filter.Query)
|
||||||
|
}
|
||||||
|
|
||||||
|
if filter.Role != nil {
|
||||||
|
filters.WithRole(*filter.Role)
|
||||||
|
}
|
||||||
|
|
||||||
|
if filter.Kind != nil {
|
||||||
|
filters.WithKind(filter.Kind)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if gqlutils.OnlyTotalCountSelected(ctx) {
|
if gqlutils.OnlyTotalCountSelected(ctx) {
|
||||||
return &types.ProfileConnection{
|
return &types.ProfileConnection{
|
||||||
Resolver: r,
|
Resolver: r,
|
||||||
ParentID: obj.ID,
|
ParentID: obj.ID,
|
||||||
Filters: filter,
|
Filters: filters,
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -207,13 +226,13 @@ func (r *organizationResolver) Profiles(ctx context.Context, obj *types.Organiza
|
|||||||
|
|
||||||
cursor := cursor.NewCursor(first, after, last, before, pageOrderBy)
|
cursor := cursor.NewCursor(first, after, last, before, pageOrderBy)
|
||||||
|
|
||||||
page, err := r.iam.OrganizationService.ListProfiles(ctx, obj.ID, cursor, filter)
|
page, err := r.iam.OrganizationService.ListProfiles(ctx, obj.ID, cursor, filters)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
r.logger.ErrorCtx(ctx, "cannot list profiles", log.Error(err))
|
r.logger.ErrorCtx(ctx, "cannot list profiles", log.Error(err))
|
||||||
return nil, gqlutils.Internal(ctx)
|
return nil, gqlutils.Internal(ctx)
|
||||||
}
|
}
|
||||||
|
|
||||||
return types.NewProfileConnection(page, r, obj.ID, filter), nil
|
return types.NewProfileConnection(page, r, obj.ID, filters), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// SamlConfigurations is the resolver for the samlConfigurations field.
|
// SamlConfigurations is the resolver for the samlConfigurations field.
|
||||||
|
|||||||
@@ -14,6 +14,17 @@ enum ProfileState
|
|||||||
@goEnum(value: "go.probo.inc/probo/pkg/coredata.ProfileStateInactive")
|
@goEnum(value: "go.probo.inc/probo/pkg/coredata.ProfileStateInactive")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
enum MembershipRole
|
||||||
|
@goModel(model: "go.probo.inc/probo/pkg/coredata.MembershipRole") {
|
||||||
|
OWNER @goEnum(value: "go.probo.inc/probo/pkg/coredata.MembershipRoleOwner")
|
||||||
|
ADMIN @goEnum(value: "go.probo.inc/probo/pkg/coredata.MembershipRoleAdmin")
|
||||||
|
EMPLOYEE
|
||||||
|
@goEnum(value: "go.probo.inc/probo/pkg/coredata.MembershipRoleEmployee")
|
||||||
|
VIEWER @goEnum(value: "go.probo.inc/probo/pkg/coredata.MembershipRoleViewer")
|
||||||
|
AUDITOR
|
||||||
|
@goEnum(value: "go.probo.inc/probo/pkg/coredata.MembershipRoleAuditor")
|
||||||
|
}
|
||||||
|
|
||||||
type Profile implements Node {
|
type Profile implements Node {
|
||||||
id: ID!
|
id: ID!
|
||||||
fullName: String!
|
fullName: String!
|
||||||
@@ -58,6 +69,9 @@ input ProfileOrder
|
|||||||
input ProfileFilter {
|
input ProfileFilter {
|
||||||
contractEnded: Boolean
|
contractEnded: Boolean
|
||||||
state: ProfileState
|
state: ProfileState
|
||||||
|
query: String
|
||||||
|
role: MembershipRole
|
||||||
|
kind: String
|
||||||
}
|
}
|
||||||
|
|
||||||
type ProfileConnection
|
type ProfileConnection
|
||||||
|
|||||||
@@ -118,9 +118,22 @@ 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.ContractEnded).WithMembership()
|
filters = coredata.NewMembershipProfileFilter(filter.ContractEnded).WithMembership()
|
||||||
|
|
||||||
if filter.State != nil {
|
if filter.State != nil {
|
||||||
filters.WithState(*filter.State)
|
filters.WithState(*filter.State)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if filter.Query != nil {
|
||||||
|
filters.WithQuery(filter.Query)
|
||||||
|
}
|
||||||
|
|
||||||
|
if filter.Role != nil {
|
||||||
|
filters.WithRole(*filter.Role)
|
||||||
|
}
|
||||||
|
|
||||||
|
if filter.Kind != nil {
|
||||||
|
filters.WithKind(filter.Kind)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pageOrderBy := page.OrderBy[coredata.MembershipProfileOrderField]{
|
pageOrderBy := page.OrderBy[coredata.MembershipProfileOrderField]{
|
||||||
|
|||||||
@@ -2771,9 +2771,23 @@ 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.ContractEnded).WithMembership()
|
filter = coredata.NewMembershipProfileFilter(input.Filter.ContractEnded).WithMembership()
|
||||||
|
|
||||||
if input.Filter.State != nil {
|
if input.Filter.State != nil {
|
||||||
filter.WithState(*input.Filter.State)
|
filter.WithState(*input.Filter.State)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if input.Filter.Query != nil {
|
||||||
|
filter.WithQuery(input.Filter.Query)
|
||||||
|
}
|
||||||
|
|
||||||
|
if input.Filter.Role != nil {
|
||||||
|
filter.WithRole(*input.Filter.Role)
|
||||||
|
}
|
||||||
|
|
||||||
|
if input.Filter.Kind != nil {
|
||||||
|
kind := string(*input.Filter.Kind)
|
||||||
|
filter.WithKind(&kind)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pageResult, err := r.iamSvc.OrganizationService.ListProfiles(ctx, input.OrganizationID, cursor, filter)
|
pageResult, err := r.iamSvc.OrganizationService.ListProfiles(ctx, input.OrganizationID, cursor, filter)
|
||||||
|
|||||||
@@ -188,6 +188,13 @@ components:
|
|||||||
- INACTIVE
|
- INACTIVE
|
||||||
go.probo.inc/mcpgen/type: go.probo.inc/probo/pkg/coredata.ProfileState
|
go.probo.inc/mcpgen/type: go.probo.inc/probo/pkg/coredata.ProfileState
|
||||||
|
|
||||||
|
ProfileKind:
|
||||||
|
type: string
|
||||||
|
enum:
|
||||||
|
- EMPLOYEE
|
||||||
|
- CONTRACTOR
|
||||||
|
- SERVICE_ACCOUNT
|
||||||
|
|
||||||
ProfileSource:
|
ProfileSource:
|
||||||
type: string
|
type: string
|
||||||
enum:
|
enum:
|
||||||
@@ -572,6 +579,15 @@ components:
|
|||||||
state:
|
state:
|
||||||
$ref: "#/components/schemas/ProfileState"
|
$ref: "#/components/schemas/ProfileState"
|
||||||
description: Filter by profile state (ACTIVE or INACTIVE)
|
description: Filter by profile state (ACTIVE or INACTIVE)
|
||||||
|
query:
|
||||||
|
type: string
|
||||||
|
description: Search by full name, email address, or position
|
||||||
|
role:
|
||||||
|
$ref: "#/components/schemas/MembershipRole"
|
||||||
|
description: Filter by membership role
|
||||||
|
kind:
|
||||||
|
$ref: "#/components/schemas/ProfileKind"
|
||||||
|
description: Filter by profile kind
|
||||||
|
|
||||||
ListUsersOutput:
|
ListUsersOutput:
|
||||||
type: object
|
type: object
|
||||||
|
|||||||
Reference in New Issue
Block a user