- Primary Group
-
-
- {groups.map(({ id, name }) => (
-
- ))}
-
-
- );
-}
diff --git a/apps/web/src/users/components/UserDetail.tsx b/apps/web/src/users/components/UserDetail.tsx
index c4618e4e8..fc919a484 100644
--- a/apps/web/src/users/components/UserDetail.tsx
+++ b/apps/web/src/users/components/UserDetail.tsx
@@ -6,7 +6,6 @@ import { CircleAlert, ShieldUserIcon } from "lucide-react";
import Label from "@/base/Label";
import Handle from "./Handle";
import Password from "./Password";
-import PrimaryGroup from "./PrimaryGroup";
import { UserActivationBanner } from "./UserActivationBanner";
import UserAdministratorRole from "./UserAdministratorRole";
import UserGroups from "./UserGroups";
@@ -79,8 +78,11 @@ export default function UserDetail({ userId }: UserDetailProps) {
-
-
+
diff --git a/apps/web/src/users/components/UserGroup.tsx b/apps/web/src/users/components/UserGroup.tsx
deleted file mode 100644
index b880a06aa..000000000
--- a/apps/web/src/users/components/UserGroup.tsx
+++ /dev/null
@@ -1,31 +0,0 @@
-import Checkbox from "@base/Checkbox";
-import SelectBoxGroupSection from "@base/SelectBoxGroupSection";
-
-export type UserGroupTypes = {
- /** The group unique id */
- id: string | number;
-
- /** The group name */
- name: string;
-
- /** Whether the group is selected */
- toggled: boolean;
-
- /** A callback function to handle the selection of a group */
- onClick: (id: string | number) => void;
-};
-
-/**
- * A condensed user group for use in a list of user groups
- */
-export function UserGroup({ id, name, toggled, onClick }: UserGroupTypes) {
- return (
- onClick(id)}
- className="capitalize select-none"
- >
-
-
- );
-}
diff --git a/apps/web/src/users/components/UserGroups.tsx b/apps/web/src/users/components/UserGroups.tsx
index cc25f73a0..f8285f895 100644
--- a/apps/web/src/users/components/UserGroups.tsx
+++ b/apps/web/src/users/components/UserGroups.tsx
@@ -1,74 +1,189 @@
import { useUpdateUser } from "@administration/queries";
+import { useFuse } from "@app/fuse";
import BoxGroup from "@base/BoxGroup";
-import InputLabel from "@base/InputLabel";
+import BoxGroupSection from "@base/BoxGroupSection";
+import ComboBox from "@base/ComboBox";
+import Icon from "@base/Icon";
+import Link from "@base/Link";
import LoadingPlaceholder from "@base/LoadingPlaceholder";
-import NoneFoundSection from "@base/NoneFoundSection";
import QueryError from "@base/QueryError";
+import { RadioGroup, RadioGroupItem } from "@base/RadioGroup";
import { useListGroups } from "@groups/queries";
import type { GroupMinimal } from "@groups/types";
-import { xor } from "es-toolkit/array";
-import { useId } from "react";
-import { UserGroup } from "./UserGroup";
+import { X } from "lucide-react";
-type UserGroupsType = {
- /** The groups associated with the user */
+/** A stable empty fallback so `useFuse` doesn't reset its term while loading */
+const NO_GROUPS: GroupMinimal[] = [];
+
+type UserGroupsProps = {
+ /** The groups the user is a member of */
memberGroups: GroupMinimal[];
+ /** The user's primary group, or null if none */
+ primaryGroup: GroupMinimal | null;
+
/** The unique user id */
userId: number;
};
/**
- * A list of user groups
+ * Manages a user's group membership and primary group.
+ *
+ * A single-select combobox adds groups to the membership list. Each member is
+ * a row in a radio group that selects the primary group, with a button to
+ * revoke membership.
*/
-export default function UserGroups({ memberGroups, userId }: UserGroupsType) {
+export default function UserGroups({
+ memberGroups,
+ primaryGroup,
+ userId,
+}: UserGroupsProps) {
const { data, isPending, isError } = useListGroups();
const mutation = useUpdateUser();
- const labelId = useId();
- if (isError && !data) {
- return ;
- }
+ const [results, term, setTerm] = useFuse(data ?? NO_GROUPS, [
+ "name",
+ ]);
- if (isPending) {
- return ;
+ const memberIds = new Set(memberGroups.map((group) => group.id));
+ const availableGroups = results.filter((group) => !memberIds.has(group.id));
+
+ function addGroup(id: number) {
+ mutation.mutate({
+ userId,
+ update: { groups: [...memberIds, id] },
+ });
}
- function handleEdit(groupId: number) {
+ function removeGroup(id: number) {
mutation.mutate({
userId,
update: {
- groups: xor(
- memberGroups.map((g) => g.id),
- [groupId],
- ),
+ groups: [...memberIds].filter((memberId) => memberId !== id),
+ ...(primaryGroup?.id === id ? { primary_group: null } : {}),
},
});
}
- return (
-