mirror of
https://github.com/documenso/documenso.git
synced 2026-07-27 02:15:05 +10:00
fix: paginate and search member/group pickers (#2768)
This commit is contained in:
@@ -34,7 +34,6 @@ import {
|
||||
FormMessage,
|
||||
} from '@documenso/ui/primitives/form/form';
|
||||
import { Input } from '@documenso/ui/primitives/input';
|
||||
import { MultiSelectCombobox } from '@documenso/ui/primitives/multi-select-combobox';
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
@@ -44,6 +43,11 @@ import {
|
||||
} from '@documenso/ui/primitives/select';
|
||||
import { useToast } from '@documenso/ui/primitives/use-toast';
|
||||
|
||||
import {
|
||||
type OrganisationMemberOption,
|
||||
OrganisationMembersMultiSelectCombobox,
|
||||
} from '~/components/general/organisation-members-multiselect-combobox';
|
||||
|
||||
export type OrganisationGroupCreateDialogProps = {
|
||||
trigger?: React.ReactNode;
|
||||
} & Omit<DialogPrimitive.DialogProps, 'children'>;
|
||||
@@ -64,6 +68,7 @@ export const OrganisationGroupCreateDialog = ({
|
||||
const { toast } = useToast();
|
||||
|
||||
const [open, setOpen] = useState(false);
|
||||
const [selectedMembers, setSelectedMembers] = useState<OrganisationMemberOption[]>([]);
|
||||
const organisation = useCurrentOrganisation();
|
||||
|
||||
const form = useForm({
|
||||
@@ -77,13 +82,6 @@ export const OrganisationGroupCreateDialog = ({
|
||||
|
||||
const { mutateAsync: createOrganisationGroup } = trpc.organisation.group.create.useMutation();
|
||||
|
||||
const { data: membersFindResult, isLoading: isLoadingMembers } =
|
||||
trpc.organisation.member.find.useQuery({
|
||||
organisationId: organisation.id,
|
||||
});
|
||||
|
||||
const members = membersFindResult?.data ?? [];
|
||||
|
||||
const onFormSubmit = async ({
|
||||
name,
|
||||
organisationRole,
|
||||
@@ -119,6 +117,7 @@ export const OrganisationGroupCreateDialog = ({
|
||||
|
||||
useEffect(() => {
|
||||
form.reset();
|
||||
setSelectedMembers([]);
|
||||
}, [open, form]);
|
||||
|
||||
return (
|
||||
@@ -178,7 +177,7 @@ export const OrganisationGroupCreateDialog = ({
|
||||
</FormLabel>
|
||||
<FormControl>
|
||||
<Select {...field} onValueChange={field.onChange}>
|
||||
<SelectTrigger className="text-muted-foreground w-full">
|
||||
<SelectTrigger className="w-full text-muted-foreground">
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
|
||||
@@ -209,16 +208,15 @@ export const OrganisationGroupCreateDialog = ({
|
||||
</FormLabel>
|
||||
|
||||
<FormControl>
|
||||
<MultiSelectCombobox
|
||||
options={members.map((member) => ({
|
||||
label: member.name,
|
||||
value: member.id,
|
||||
}))}
|
||||
loading={isLoadingMembers}
|
||||
selectedValues={field.value}
|
||||
onChange={field.onChange}
|
||||
className="bg-background w-full"
|
||||
emptySelectionPlaceholder={t`Select members`}
|
||||
<OrganisationMembersMultiSelectCombobox
|
||||
organisationId={organisation.id}
|
||||
selectedMembers={selectedMembers}
|
||||
onChange={(members) => {
|
||||
setSelectedMembers(members);
|
||||
field.onChange(members.map((member) => member.id));
|
||||
}}
|
||||
className="w-full bg-background"
|
||||
dataTestId="group-members-picker"
|
||||
/>
|
||||
</FormControl>
|
||||
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { useEffect, useMemo, useState } from 'react';
|
||||
import { useEffect, useState } from 'react';
|
||||
|
||||
import { zodResolver } from '@hookform/resolvers/zod';
|
||||
import { Trans, useLingui } from '@lingui/react/macro';
|
||||
import { OrganisationGroupType, TeamMemberRole } from '@prisma/client';
|
||||
import { TeamMemberRole } from '@prisma/client';
|
||||
import type * as DialogPrimitive from '@radix-ui/react-dialog';
|
||||
import { useForm } from 'react-hook-form';
|
||||
import { match } from 'ts-pattern';
|
||||
@@ -31,7 +31,6 @@ import {
|
||||
FormMessage,
|
||||
} from '@documenso/ui/primitives/form/form';
|
||||
import { Input } from '@documenso/ui/primitives/input';
|
||||
import { MultiSelectCombobox } from '@documenso/ui/primitives/multi-select-combobox';
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
@@ -41,6 +40,10 @@ import {
|
||||
} from '@documenso/ui/primitives/select';
|
||||
import { useToast } from '@documenso/ui/primitives/use-toast';
|
||||
|
||||
import {
|
||||
type OrganisationGroupOption,
|
||||
OrganisationGroupsMultiSelectCombobox,
|
||||
} from '~/components/general/organisation-groups-multiselect-combobox';
|
||||
import { useCurrentTeam } from '~/providers/team';
|
||||
|
||||
export type TeamGroupCreateDialogProps = Omit<DialogPrimitive.DialogProps, 'children'>;
|
||||
@@ -59,6 +62,7 @@ type TAddTeamMembersFormSchema = z.infer<typeof ZAddTeamMembersFormSchema>;
|
||||
export const TeamGroupCreateDialog = ({ ...props }: TeamGroupCreateDialogProps) => {
|
||||
const [open, setOpen] = useState(false);
|
||||
const [step, setStep] = useState<'SELECT' | 'ROLES'>('SELECT');
|
||||
const [selectedGroups, setSelectedGroups] = useState<OrganisationGroupOption[]>([]);
|
||||
|
||||
const { t } = useLingui();
|
||||
const { toast } = useToast();
|
||||
@@ -74,26 +78,6 @@ export const TeamGroupCreateDialog = ({ ...props }: TeamGroupCreateDialogProps)
|
||||
|
||||
const { mutateAsync: createTeamGroups } = trpc.team.group.createMany.useMutation();
|
||||
|
||||
const organisationGroupQuery = trpc.organisation.group.find.useQuery({
|
||||
organisationId: team.organisationId,
|
||||
perPage: 100, // Won't really work if they somehow have more than 100 groups.
|
||||
types: [OrganisationGroupType.CUSTOM],
|
||||
});
|
||||
|
||||
const teamGroupQuery = trpc.team.group.find.useQuery({
|
||||
teamId: team.id,
|
||||
perPage: 100, // Won't really work if they somehow have more than 100 groups.
|
||||
});
|
||||
|
||||
const avaliableOrganisationGroups = useMemo(() => {
|
||||
const organisationGroups = organisationGroupQuery.data?.data ?? [];
|
||||
const teamGroups = teamGroupQuery.data?.data ?? [];
|
||||
|
||||
return organisationGroups.filter(
|
||||
(group) => !teamGroups.some((teamGroup) => teamGroup.organisationGroupId === group.id),
|
||||
);
|
||||
}, [organisationGroupQuery, teamGroupQuery]);
|
||||
|
||||
const onFormSubmit = async ({ groups }: TAddTeamMembersFormSchema) => {
|
||||
try {
|
||||
await createTeamGroups({
|
||||
@@ -121,6 +105,7 @@ export const TeamGroupCreateDialog = ({ ...props }: TeamGroupCreateDialogProps)
|
||||
if (!open) {
|
||||
form.reset();
|
||||
setStep('SELECT');
|
||||
setSelectedGroups([]);
|
||||
}
|
||||
}, [open, form]);
|
||||
|
||||
@@ -178,28 +163,24 @@ export const TeamGroupCreateDialog = ({ ...props }: TeamGroupCreateDialogProps)
|
||||
</FormLabel>
|
||||
|
||||
<FormControl>
|
||||
<MultiSelectCombobox
|
||||
options={avaliableOrganisationGroups.map((group) => ({
|
||||
label: group.name ?? group.organisationRole,
|
||||
value: group.id,
|
||||
}))}
|
||||
loading={organisationGroupQuery.isLoading || teamGroupQuery.isLoading}
|
||||
selectedValues={field.value.map(
|
||||
({ organisationGroupId }) => organisationGroupId,
|
||||
)}
|
||||
onChange={(value) => {
|
||||
<OrganisationGroupsMultiSelectCombobox
|
||||
organisationId={team.organisationId}
|
||||
selectedGroups={selectedGroups}
|
||||
excludeTeamId={team.id}
|
||||
onChange={(groups) => {
|
||||
setSelectedGroups(groups);
|
||||
field.onChange(
|
||||
value.map((organisationGroupId) => ({
|
||||
organisationGroupId,
|
||||
groups.map((group) => ({
|
||||
organisationGroupId: group.id,
|
||||
teamRole:
|
||||
field.value.find(
|
||||
(value) => value.organisationGroupId === organisationGroupId,
|
||||
(value) => value.organisationGroupId === group.id,
|
||||
)?.teamRole || TeamMemberRole.MEMBER,
|
||||
})),
|
||||
);
|
||||
}}
|
||||
className="bg-background w-full"
|
||||
emptySelectionPlaceholder={t`Select groups`}
|
||||
className="w-full bg-background"
|
||||
dataTestId="team-groups-picker"
|
||||
/>
|
||||
</FormControl>
|
||||
|
||||
@@ -243,9 +224,8 @@ export const TeamGroupCreateDialog = ({ ...props }: TeamGroupCreateDialogProps)
|
||||
readOnly
|
||||
className="bg-background"
|
||||
value={
|
||||
avaliableOrganisationGroups.find(
|
||||
({ id }) => id === group.organisationGroupId,
|
||||
)?.name || t`Untitled Group`
|
||||
selectedGroups.find(({ id }) => id === group.organisationGroupId)
|
||||
?.name || t`Untitled Group`
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useEffect, useMemo, useRef, useState } from 'react';
|
||||
import { useEffect, useRef, useState } from 'react';
|
||||
|
||||
import { zodResolver } from '@hookform/resolvers/zod';
|
||||
import { Trans, useLingui } from '@lingui/react/macro';
|
||||
@@ -36,7 +36,6 @@ import {
|
||||
FormMessage,
|
||||
} from '@documenso/ui/primitives/form/form';
|
||||
import { Input } from '@documenso/ui/primitives/input';
|
||||
import { MultiSelectCombobox } from '@documenso/ui/primitives/multi-select-combobox';
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
@@ -48,6 +47,10 @@ import { Tooltip, TooltipContent, TooltipTrigger } from '@documenso/ui/primitive
|
||||
import { useToast } from '@documenso/ui/primitives/use-toast';
|
||||
|
||||
import { OrganisationMemberInviteDialog } from '~/components/dialogs/organisation-member-invite-dialog';
|
||||
import {
|
||||
type OrganisationMemberOption,
|
||||
OrganisationMembersMultiSelectCombobox,
|
||||
} from '~/components/general/organisation-members-multiselect-combobox';
|
||||
import { useCurrentTeam } from '~/providers/team';
|
||||
|
||||
export type TeamMemberCreateDialogProps = {
|
||||
@@ -69,6 +72,7 @@ export const TeamMemberCreateDialog = ({ trigger, ...props }: TeamMemberCreateDi
|
||||
const [open, setOpen] = useState(false);
|
||||
const [step, setStep] = useState<'SELECT' | 'MEMBERS'>('SELECT');
|
||||
const [inviteDialogOpen, setInviteDialogOpen] = useState(false);
|
||||
const [selectedMembers, setSelectedMembers] = useState<OrganisationMemberOption[]>([]);
|
||||
const prevInviteDialogOpenRef = useRef(false);
|
||||
|
||||
const { t } = useLingui();
|
||||
@@ -92,25 +96,16 @@ export const TeamMemberCreateDialog = ({ trigger, ...props }: TeamMemberCreateDi
|
||||
|
||||
const { mutateAsync: createTeamMembers } = trpc.team.member.createMany.useMutation();
|
||||
|
||||
const organisationMemberQuery = trpc.organisation.member.find.useQuery({
|
||||
// Lightweight count-only query for org members not already on this team.
|
||||
// Powers the "no available members" empty state.
|
||||
const availableMemberCountQuery = trpc.organisation.member.find.useQuery({
|
||||
organisationId: team.organisationId,
|
||||
perPage: 1,
|
||||
excludeTeamId: team.id,
|
||||
});
|
||||
|
||||
const teamMemberQuery = trpc.team.member.find.useQuery({
|
||||
teamId: team.id,
|
||||
});
|
||||
|
||||
const avaliableOrganisationMembers = useMemo(() => {
|
||||
const organisationMembers = organisationMemberQuery.data?.data ?? [];
|
||||
const teamMembers = teamMemberQuery.data?.data ?? [];
|
||||
|
||||
return organisationMembers.filter(
|
||||
(member) => !teamMembers.some((teamMember) => teamMember.id === member.id),
|
||||
);
|
||||
}, [organisationMemberQuery, teamMemberQuery]);
|
||||
|
||||
const hasNoAvailableMembers =
|
||||
!organisationMemberQuery.isLoading && avaliableOrganisationMembers.length === 0;
|
||||
!availableMemberCountQuery.isLoading && (availableMemberCountQuery.data?.count ?? 0) === 0;
|
||||
|
||||
const onFormSubmit = async ({ members }: TAddTeamMembersFormSchema) => {
|
||||
if (members.length === 0) {
|
||||
@@ -159,6 +154,7 @@ export const TeamMemberCreateDialog = ({ trigger, ...props }: TeamMemberCreateDi
|
||||
form.reset();
|
||||
setStep('SELECT');
|
||||
setInviteDialogOpen(false);
|
||||
setSelectedMembers([]);
|
||||
}
|
||||
}, [open, form]);
|
||||
|
||||
@@ -296,29 +292,24 @@ export const TeamMemberCreateDialog = ({ trigger, ...props }: TeamMemberCreateDi
|
||||
)}
|
||||
</div>
|
||||
) : (
|
||||
<MultiSelectCombobox
|
||||
options={avaliableOrganisationMembers.map((member) => ({
|
||||
label: member.name,
|
||||
value: member.id,
|
||||
}))}
|
||||
loading={organisationMemberQuery.isLoading}
|
||||
selectedValues={field.value.map(
|
||||
(member) => member.organisationMemberId,
|
||||
)}
|
||||
onChange={(value) => {
|
||||
<OrganisationMembersMultiSelectCombobox
|
||||
organisationId={team.organisationId}
|
||||
selectedMembers={selectedMembers}
|
||||
excludeTeamId={team.id}
|
||||
onChange={(members) => {
|
||||
setSelectedMembers(members);
|
||||
field.onChange(
|
||||
value.map((organisationMemberId) => ({
|
||||
organisationMemberId,
|
||||
members.map((member) => ({
|
||||
organisationMemberId: member.id,
|
||||
teamRole:
|
||||
field.value.find(
|
||||
(member) =>
|
||||
member.organisationMemberId === organisationMemberId,
|
||||
(entry) => entry.organisationMemberId === member.id,
|
||||
)?.teamRole || TeamMemberRole.MEMBER,
|
||||
})),
|
||||
);
|
||||
}}
|
||||
className="w-full bg-background"
|
||||
emptySelectionPlaceholder={t`Select members`}
|
||||
dataTestId="team-members-picker"
|
||||
/>
|
||||
)}
|
||||
</FormControl>
|
||||
@@ -394,9 +385,8 @@ export const TeamMemberCreateDialog = ({ trigger, ...props }: TeamMemberCreateDi
|
||||
readOnly
|
||||
className="bg-background"
|
||||
value={
|
||||
organisationMemberQuery.data?.data.find(
|
||||
({ id }) => id === member.organisationMemberId,
|
||||
)?.name || ''
|
||||
selectedMembers.find(({ id }) => id === member.organisationMemberId)
|
||||
?.name || ''
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user