Improve people list filters and invite state

Replace the cramped status checkboxes with a multi-select
dropdown (new DropdownCheckboxItem), and update the Relay
store to PENDING when an activation email is sent so the
row reflects the server-side state change immediately.

Signed-off-by: Émile Ré <emile@probo.com>
This commit is contained in:
Émile Ré
2026-07-29 10:22:32 +02:00
parent bb16be8d17
commit b3cf6d909b
4 changed files with 73 additions and 15 deletions

View File

@@ -20,7 +20,10 @@
import { getAssignableRoles, getMembershipRoles, peopleRoles } from "@probo/helpers"; import { getAssignableRoles, getMembershipRoles, peopleRoles } from "@probo/helpers";
import { import {
Checkbox, Button,
Dropdown,
DropdownCheckboxItem,
IconChevronDown,
IconMagnifyingGlass, IconMagnifyingGlass,
Input, Input,
Option, Option,
@@ -192,15 +195,21 @@ export function PeopleList(props: {
debouncedRefetchQuery(value); debouncedRefetchQuery(value);
}; };
const handleStateFilterToggle = (state: ProfileState) => { const handleStateFilterChange = (state: ProfileState, checked: boolean) => {
debouncedRefetchQuery.cancel(); debouncedRefetchQuery.cancel();
const newStates = statesFilter.includes(state) const newStates = checked
? statesFilter.filter(s => s !== state) ? [...statesFilter, state]
: [...statesFilter, state]; : statesFilter.filter(s => s !== state);
setStatesFilter(newStates); setStatesFilter(newStates);
refetchPeople({ states: newStates }); refetchPeople({ states: newStates });
}; };
const statusFilterLabel = statesFilter.length === 0
? t("peopleList.filters.allStatuses")
: statesFilter
.map(state => t(`peopleList.filters.${state.toLowerCase()}`))
.join(", ");
const handleRoleFilterChange = (value: string) => { const handleRoleFilterChange = (value: string) => {
debouncedRefetchQuery.cancel(); debouncedRefetchQuery.cancel();
const newRole = value === "ALL" ? null : (value as MembershipRole); const newRole = value === "ALL" ? null : (value as MembershipRole);
@@ -234,18 +243,24 @@ export function PeopleList(props: {
value={queryFilter ?? ""} value={queryFilter ?? ""}
onValueChange={handleQueryFilterChange} onValueChange={handleQueryFilterChange}
/> />
<div className="flex items-center gap-3"> <Dropdown
<span className="text-sm text-txt-secondary">{t("peopleList.filters.status")}</span> toggle={(
<Button variant="secondary" className="min-w-40 justify-between gap-2">
<span className="truncate">{statusFilterLabel}</span>
<IconChevronDown size={12} className="shrink-0" />
</Button>
)}
>
{PROFILE_STATES.map(state => ( {PROFILE_STATES.map(state => (
<label key={state} className="flex items-center gap-2 text-sm cursor-pointer"> <DropdownCheckboxItem
<Checkbox key={state}
checked={statesFilter.includes(state)} checked={statesFilter.includes(state)}
onChange={() => handleStateFilterToggle(state)} onCheckedChange={(checked: boolean) => handleStateFilterChange(state, checked)}
/> >
{t(`peopleList.filters.${state.toLowerCase()}`)} {t(`peopleList.filters.${state.toLowerCase()}`)}
</label> </DropdownCheckboxItem>
))} ))}
</div> </Dropdown>
<Select <Select
value={roleFilter ?? "ALL"} value={roleFilter ?? "ALL"}
onValueChange={handleRoleFilterChange} onValueChange={handleRoleFilterChange}

View File

@@ -191,6 +191,11 @@ export function PeopleListItem(props: {
}, },
connections: [profile.lastInvitation.__id], connections: [profile.lastInvitation.__id],
}, },
updater: (store) => {
// Inviting a deactivated user marks the profile pending server-side;
// the payload only returns the invitation edge, so update state here.
store.get(profile.id)?.setValue("PENDING", "state");
},
}); });
}, },
{ {

View File

@@ -24,7 +24,7 @@ import type { ComponentProps, FC, PropsWithChildren, ReactNode } from "react";
import { tv } from "tailwind-variants"; import { tv } from "tailwind-variants";
import { Button } from "../Button/Button"; import { Button } from "../Button/Button";
import { IconDotGrid1x3Horizontal } from "../Icons"; import { IconCheckmark1, IconDotGrid1x3Horizontal } from "../Icons";
import type { IconProps } from "../Icons/type"; import type { IconProps } from "../Icons/type";
type Props = PropsWithChildren<{ type Props = PropsWithChildren<{
@@ -138,3 +138,40 @@ export function DropdownItem({
</DropdownMenu.Item> </DropdownMenu.Item>
); );
} }
type DropdownCheckboxItemProps = PropsWithChildren<{
className?: string;
checked: boolean;
onCheckedChange: (checked: boolean) => void;
disabled?: boolean;
}>;
export function DropdownCheckboxItem({
className,
checked,
onCheckedChange,
disabled,
children,
}: DropdownCheckboxItemProps) {
return (
<DropdownMenu.CheckboxItem
checked={checked}
onCheckedChange={value => onCheckedChange(value === true)}
disabled={disabled}
className={clsx(dropdownItem(), "rounded-lg outline-none", className)}
>
<span
className={clsx(
"size-4 border border-border-mid relative rounded-sm flex items-center justify-center flex-none",
checked && "bg-accent text-invert",
disabled && "opacity-50",
)}
>
<DropdownMenu.ItemIndicator>
<IconCheckmark1 size={12} />
</DropdownMenu.ItemIndicator>
</span>
{children}
</DropdownMenu.CheckboxItem>
);
}

View File

@@ -43,6 +43,7 @@ export { Spinner } from "./Atoms/Spinner/Spinner";
export { export {
ActionDropdown, ActionDropdown,
Dropdown, Dropdown,
DropdownCheckboxItem,
DropdownItem, DropdownItem,
DropdownSeparator, DropdownSeparator,
} from "./Atoms/Dropdown/Dropdown"; } from "./Atoms/Dropdown/Dropdown";