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",
|
||||
"serviceAccount": "Service account"
|
||||
},
|
||||
"membershipRole": {
|
||||
"owner": "Owner",
|
||||
"admin": "Admin",
|
||||
"viewer": "Viewer",
|
||||
"auditor": "Auditor",
|
||||
"employee": "Employee"
|
||||
},
|
||||
"registryStatus": {
|
||||
"open": "Open",
|
||||
"inProgress": "In Progress",
|
||||
@@ -1351,20 +1358,20 @@
|
||||
"addPersonDialog": { "title": "Add Person" },
|
||||
"peopleList": {
|
||||
"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": {
|
||||
"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" },
|
||||
"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" }
|
||||
},
|
||||
"personForm": {
|
||||
"messages": { "created": "Person created successfully.", "updated": "Person updated successfully." },
|
||||
"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" },
|
||||
"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" },
|
||||
"kinds": { "EMPLOYEE": "Employee", "CONTRACTOR": "Contractor", "SERVICE_ACCOUNT": "Service account" },
|
||||
"actions": { "update": "Update", "create": "Create" }
|
||||
|
||||
@@ -1676,6 +1676,13 @@
|
||||
"contractor": "Prestataire",
|
||||
"serviceAccount": "Compte de service"
|
||||
},
|
||||
"membershipRole": {
|
||||
"owner": "Propriétaire",
|
||||
"admin": "Administrateur",
|
||||
"viewer": "Lecteur",
|
||||
"auditor": "Auditeur",
|
||||
"employee": "Employé"
|
||||
},
|
||||
"registryStatus": {
|
||||
"open": "Ouvert",
|
||||
"inProgress": "En cours",
|
||||
@@ -2419,7 +2426,15 @@
|
||||
"role": "Rô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": {
|
||||
"messages": {
|
||||
@@ -2439,13 +2454,6 @@
|
||||
"archive": "Voulez-vous vraiment archiver {{name}} ?",
|
||||
"remove": "Voulez-vous vraiment supprimer {{name}} ?"
|
||||
},
|
||||
"roles": {
|
||||
"owner": "Propriétaire",
|
||||
"admin": "Administrateur",
|
||||
"viewer": "Lecteur",
|
||||
"auditor": "Auditeur",
|
||||
"employee": "Employé"
|
||||
},
|
||||
"actions": {
|
||||
"send": "Envoyer",
|
||||
"sendActivationMail": "Envoyer l’e-mail d’activation",
|
||||
@@ -2473,13 +2481,6 @@
|
||||
"contractStartDate": "Date de début de contrat",
|
||||
"contractEndDate": "Date de fin de contrat"
|
||||
},
|
||||
"roles": {
|
||||
"owner": "Propriétaire",
|
||||
"admin": "Administrateur",
|
||||
"viewer": "Lecteur",
|
||||
"auditor": "Auditeur",
|
||||
"employee": "Employé"
|
||||
},
|
||||
"roleDescriptions": {
|
||||
"owner": "Accès complet à tout",
|
||||
"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 {
|
||||
canCreateUser: permission(action: "iam:membership-profile:create", attributes: { target_role: "VIEWER" })
|
||||
...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(
|
||||
organizationId,
|
||||
"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
|
||||
// SOFTWARE.
|
||||
|
||||
import { getAssignableRoles } from "@probo/helpers";
|
||||
import { Tbody, Td, Th, Thead, Tr } from "@probo/ui";
|
||||
import { getAssignableRoles, getMembershipRoles, peopleRoles } from "@probo/helpers";
|
||||
import {
|
||||
IconMagnifyingGlass,
|
||||
Input,
|
||||
Option,
|
||||
Select,
|
||||
Tbody,
|
||||
Td,
|
||||
Th,
|
||||
Thead,
|
||||
Tr,
|
||||
} from "@probo/ui";
|
||||
import type { ComponentProps } from "react";
|
||||
import { use } from "react";
|
||||
import { use, useCallback, useEffect, useState, useTransition } from "react";
|
||||
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_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 { useOrganizationId } from "#/hooks/useOrganizationId";
|
||||
import { CurrentUser } from "#/providers/CurrentUser";
|
||||
|
||||
import { PeopleListItem } from "./PeopleListItem";
|
||||
|
||||
const PAGE_SIZE = 100;
|
||||
const SEARCH_DEBOUNCE_MS = 300;
|
||||
|
||||
type PeopleKind = (typeof peopleRoles)[number];
|
||||
|
||||
const fragment = graphql`
|
||||
fragment PeopleListFragment on Organization
|
||||
@refetchable(queryName: "PeopleListFragment_RefetchQuery")
|
||||
@argumentDefinitions(
|
||||
first: { type: "Int", defaultValue: 20 }
|
||||
first: { type: "Int", defaultValue: 100 }
|
||||
order: {
|
||||
type: "ProfileOrder"
|
||||
defaultValue: { direction: ASC, field: FULL_NAME }
|
||||
}
|
||||
filter: { type: "ProfileFilter", defaultValue: null }
|
||||
after: { type: "CursorKey", defaultValue: null }
|
||||
before: { type: "CursorKey", defaultValue: null }
|
||||
last: { type: "Int", defaultValue: null }
|
||||
@@ -52,7 +73,8 @@ const fragment = graphql`
|
||||
last: $last
|
||||
before: $before
|
||||
orderBy: $order
|
||||
) @connection(key: "PeopleListFragment_profiles", filters: ["orderBy"]) @required(action: THROW) {
|
||||
filter: $filter
|
||||
) @connection(key: "PeopleListFragment_profiles", filters: ["orderBy", "filter"]) @required(action: THROW) {
|
||||
__id
|
||||
totalCount
|
||||
edges @required(action: THROW) {
|
||||
@@ -65,76 +87,215 @@ const fragment = graphql`
|
||||
}
|
||||
`;
|
||||
|
||||
type PeopleFilter = {
|
||||
query: string | null;
|
||||
state: ProfileState | null;
|
||||
role: MembershipRole | null;
|
||||
kind: string | null;
|
||||
};
|
||||
|
||||
export function PeopleList(props: {
|
||||
fKey: PeopleListFragment$key;
|
||||
onConnectionIdChange: (connectionId: string) => void;
|
||||
}) {
|
||||
const { fKey, onConnectionIdChange } = props;
|
||||
|
||||
const organizationId = useOrganizationId();
|
||||
const { t } = useTranslation();
|
||||
const { role } = use(CurrentUser);
|
||||
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<
|
||||
PeopleListFragment_RefetchQuery,
|
||||
PeopleListFragment$key
|
||||
>(fragment, fKey);
|
||||
|
||||
const refetchPeople = () => {
|
||||
peoplePagination.refetch({}, { fetchPolicy: "network-only" });
|
||||
const connectionId = peoplePagination.data.profiles.__id;
|
||||
|
||||
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) => {
|
||||
onConnectionIdChange(
|
||||
ConnectionHandler.getConnectionID(
|
||||
organizationId,
|
||||
"PeopleListFragment_profiles",
|
||||
{ orderBy: order },
|
||||
),
|
||||
);
|
||||
const debouncedRefetchQuery = useDebounceCallback(
|
||||
useCallback(
|
||||
(value: string) => {
|
||||
const newQuery = value === "" ? null : value;
|
||||
startTransition(() => {
|
||||
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 (
|
||||
<SortableTable
|
||||
{...peoplePagination}
|
||||
refetch={
|
||||
peoplePagination.refetch as ComponentProps<
|
||||
typeof SortableTable
|
||||
>["refetch"]
|
||||
}
|
||||
pageSize={20}
|
||||
>
|
||||
<Thead>
|
||||
<Tr>
|
||||
<SortableTh field="FULL_NAME" onOrderChange={handleOrderChange}>{t("peopleList.columns.name")}</SortableTh>
|
||||
<SortableTh field="STATE">{t("peopleList.columns.status")}</SortableTh>
|
||||
<SortableTh field="EMAIL_ADDRESS" onOrderChange={handleOrderChange}>{t("peopleList.columns.email")}</SortableTh>
|
||||
{canManageRoles && <Th>{t("peopleList.columns.role")}</Th>}
|
||||
<SortableTh field="CREATED_AT" onOrderChange={handleOrderChange}>{t("peopleList.columns.createdOn")}</SortableTh>
|
||||
<Th></Th>
|
||||
</Tr>
|
||||
</Thead>
|
||||
<Tbody>
|
||||
{peoplePagination.data.profiles.totalCount === 0
|
||||
? (
|
||||
<Tr>
|
||||
<Td colSpan={7} className="text-center text-txt-secondary">
|
||||
{t("peopleList.empty")}
|
||||
</Td>
|
||||
</Tr>
|
||||
)
|
||||
: (
|
||||
peoplePagination.data.profiles.edges.map(({ node: profile }) => (
|
||||
<PeopleListItem
|
||||
connectionId={peoplePagination.data.profiles.__id}
|
||||
key={profile.id}
|
||||
fKey={profile}
|
||||
onRefetch={refetchPeople}
|
||||
/>
|
||||
))
|
||||
)}
|
||||
</Tbody>
|
||||
</SortableTable>
|
||||
<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
|
||||
{...peoplePagination}
|
||||
refetch={refetchWithFilters}
|
||||
pageSize={PAGE_SIZE}
|
||||
>
|
||||
<Thead>
|
||||
<Tr>
|
||||
<SortableTh field="FULL_NAME" onOrderChange={handleOrderChange}>{t("peopleList.columns.name")}</SortableTh>
|
||||
<SortableTh field="STATE">{t("peopleList.columns.status")}</SortableTh>
|
||||
<SortableTh field="EMAIL_ADDRESS" onOrderChange={handleOrderChange}>{t("peopleList.columns.email")}</SortableTh>
|
||||
{canManageRoles && <Th>{t("peopleList.columns.role")}</Th>}
|
||||
<SortableTh field="CREATED_AT" onOrderChange={handleOrderChange}>{t("peopleList.columns.createdOn")}</SortableTh>
|
||||
<Th></Th>
|
||||
</Tr>
|
||||
</Thead>
|
||||
<Tbody>
|
||||
{peoplePagination.data.profiles.totalCount === 0
|
||||
? (
|
||||
<Tr>
|
||||
<Td colSpan={7} className="text-center text-txt-secondary">
|
||||
{t("peopleList.empty")}
|
||||
</Td>
|
||||
</Tr>
|
||||
)
|
||||
: (
|
||||
peoplePagination.data.profiles.edges.map(({ node: profile }) => (
|
||||
<PeopleListItem
|
||||
connectionId={connectionId}
|
||||
key={profile.id}
|
||||
fKey={profile}
|
||||
onRefetch={() => refetchPeople()}
|
||||
/>
|
||||
))
|
||||
)}
|
||||
</Tbody>
|
||||
</SortableTable>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -18,7 +18,7 @@
|
||||
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
// SOFTWARE.
|
||||
|
||||
import { getAssignableRoles } from "@probo/helpers";
|
||||
import { getAssignableRoles, getMembershipRoles } from "@probo/helpers";
|
||||
import { dateFormat } from "@probo/i18n";
|
||||
import {
|
||||
ActionDropdown,
|
||||
@@ -288,21 +288,13 @@ export function PeopleListItem(props: {
|
||||
value={profile.membership.role}
|
||||
onValueChange={role => void handleUpdateRole(role)}
|
||||
>
|
||||
{roleOptions.includes("OWNER") && (
|
||||
<Option value="OWNER">{t("peopleListItem.roles.owner")}</Option>
|
||||
)}
|
||||
{roleOptions.includes("ADMIN") && (
|
||||
<Option value="ADMIN">{t("peopleListItem.roles.admin")}</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>
|
||||
)}
|
||||
{getMembershipRoles(t)
|
||||
.filter(({ value }) => roleOptions.includes(value))
|
||||
.map(({ value, label }) => (
|
||||
<Option key={value} value={value}>
|
||||
{label}
|
||||
</Option>
|
||||
))}
|
||||
</Select>
|
||||
</Td>
|
||||
)}
|
||||
|
||||
@@ -18,8 +18,7 @@
|
||||
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
// SOFTWARE.
|
||||
|
||||
import { formatDatetime, getAssignableRoles, peopleRoles } from "@probo/helpers";
|
||||
import { roles } from "@probo/helpers/src/roles";
|
||||
import { formatDatetime, getAssignableRoles, getMembershipRoles, peopleRoles, roles } from "@probo/helpers";
|
||||
import { Button, Field, Input, Option } from "@probo/ui";
|
||||
import { use } from "react";
|
||||
import { useWatch } from "react-hook-form";
|
||||
@@ -202,21 +201,13 @@ export function PersonForm(props: {
|
||||
label={t("personForm.fields.role")}
|
||||
disabled={disabled || !!id}
|
||||
>
|
||||
{availableRoles.includes("OWNER") && (
|
||||
<Option value="OWNER">{t("personForm.roles.owner")}</Option>
|
||||
)}
|
||||
{availableRoles.includes("ADMIN") && (
|
||||
<Option value="ADMIN">{t("personForm.roles.admin")}</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>
|
||||
)}
|
||||
{getMembershipRoles(t)
|
||||
.filter(({ value }) => availableRoles.includes(value))
|
||||
.map(({ value, label }) => (
|
||||
<Option key={value} value={value}>
|
||||
{label}
|
||||
</Option>
|
||||
))}
|
||||
</ControlledField>
|
||||
|
||||
<div className="mt-4 space-y-2 text-sm text-txt-tertiary">
|
||||
|
||||
Reference in New Issue
Block a user