Files
calendar/apps/web/modules/ee/organizations/other-team-members-view.tsx
Benny JooGitHubDevin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
404d46c991 refactor: Move trpc-dependent components from features to web [2] (#26420)
* fix

* move event types components to web

* update import paths

* mv apps components

* migrate form builder

* fix

* mv sso

* fix

* mv

* update import paths

* update import paths

* mv

* mv

* mv

* fix

* update Booker

* fix

* fix

* fix

* fix

* mv video

* mv embed components to web

* update import paths

* mv calendar weekly view components

* update import paths

* fix

* fixp

* fix

* fix

* fix

* fix: update FormBuilder imports to use @calcom/features/form-builder

Co-Authored-By: [email protected] <[email protected]>

* fix: update broken import paths after file migrations

Co-Authored-By: [email protected] <[email protected]>

* fix: correct import paths for platform atoms and moved components

Co-Authored-By: [email protected] <[email protected]>

* fix: apply CSS type fixes and add missing atoms exports

Co-Authored-By: [email protected] <[email protected]>

* fix: resolve type errors in test files after component migrations

Co-Authored-By: [email protected] <[email protected]>

* fix: resolve remaining type errors in test files

Co-Authored-By: [email protected] <[email protected]>

* fix

* migrate

* fix: resolve type errors in test and mock files

- Add missing bookingForm, bookerFormErrorRef, instantConnectCooldownMs to Booker.test.tsx bookings prop
- Add all required BookerEvent properties to event.mock.ts
- Add vi import from vitest to all mock files
- Fix date parameter types in packages/dayjs/__mocks__/index.ts
- Add verificationCode and setVerificationCode to test-utils.tsx mock store
- Remove children.type access in Section.tsx mock to fix type error
- Fix lint issues: remove unused React imports, use import type where needed, add return types
- Add biome-ignore comments for pre-existing lint warnings in test files

Co-Authored-By: [email protected] <[email protected]>

* migrate

* migrate

* migrate

* update import paths

* update import paths

* update import paths

* fix

* migrate data table

* migrate data table

* fix

* fix

* fix

* migrate insights components

* migrate insights components

* fix

* mv

* update import paths

* fix

* fix

* fix

* fix

* fix

* fix: resolve type errors in test mocks

- Booker.test.tsx: Add all required UseFormReturn methods to bookingForm mock
- event.mock.ts: Fix entity, subsetOfHosts, instantMeetingParameters, fieldTranslations, image types
- dayjs/__mocks__/index.ts: Use Object.assign for proper typing of mock properties
- Section.tsx: Change 'class' to 'className' in JSX with biome-ignore comment

Co-Authored-By: [email protected] <[email protected]>

* fix: add missing hasDataErrors and dataErrors to bookings.errors mock

Co-Authored-By: [email protected] <[email protected]>

* fix: add missing loadingStates properties to bookings mock

Co-Authored-By: [email protected] <[email protected]>

* fix: add missing slots properties (setTentativeSelectedTimeslots, tentativeSelectedTimeslots, slotReservationId)

Co-Authored-By: [email protected] <[email protected]>

* fix: update quickAvailabilityChecks to include utcEndIso and use valid status type

Co-Authored-By: [email protected] <[email protected]>

* fix: add missing bookerForm properties (formName, beforeVerifyEmail, formErrors)

Co-Authored-By: [email protected] <[email protected]>

* fix: add missing UseFormReturn properties to bookerForm.bookingForm mock

Co-Authored-By: [email protected] <[email protected]>

* fix: add hasFormErrors and formErrors to bookerForm.formErrors mock

Co-Authored-By: [email protected] <[email protected]>

* fix: add hasFormErrors and formErrors to bookerForm.errors mock

Co-Authored-By: [email protected] <[email protected]>

* fix: add missing isError property to mockEvent

Co-Authored-By: [email protected] <[email protected]>

* fix: use complete BookerEvent mock in Booker.test.tsx

Co-Authored-By: [email protected] <[email protected]>

* fix: use branded bookingFields type in event mock

Co-Authored-By: [email protected] <[email protected]>

* fix: add missing schedule mock properties (isError, isSuccess, isLoading, dataUpdatedAt)

Co-Authored-By: [email protected] <[email protected]>

* revert

* fix

* fix

* fix

* fix build

* fix

* fix

* fix

* fix: correct AddMembersWithSwitch test wrapper to use initial assignAllTeamMembers value

- Fixed test wrapper to initialize useState with componentProps.assignAllTeamMembers
  instead of hardcoded false, allowing tests to properly test different states
- Updated test expectations for ALL_TEAM_MEMBERS_ENABLED_AND_SEGMENT_NOT_APPLICABLE
  state to match actual component behavior (toggle should be present and checked)
- Fixed 'should show Segment when toggled on' test to start with assignAllTeamMembers: false
  to properly test the flow of enabling it
- Added explicit types to satisfy biome lint requirements

Co-Authored-By: [email protected] <[email protected]>

* fix: use JSX.Element instead of React.JSX.Element for type compatibility

Co-Authored-By: [email protected] <[email protected]>

---------

Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2026-01-05 12:53:51 -03:00

262 lines
8.2 KiB
TypeScript

"use client";
// import { debounce } from "lodash";
import { keepPreviousData } from "@tanstack/react-query";
import { useSession } from "next-auth/react";
import { useRouter } from "next/navigation";
import { useState, useEffect } from "react";
import { checkAdminOrOwner } from "@calcom/features/auth/lib/checkAdminOrOwner";
import MemberListItem from "~/ee/organizations/components/MemberListItem";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { useParamsWithFallback } from "@calcom/lib/hooks/useParamsWithFallback";
import { CreationSource } from "@calcom/prisma/enums";
import { trpc } from "@calcom/trpc/react";
import type { RouterOutputs } from "@calcom/trpc/react";
import { Button } from "@calcom/ui/components/button";
import { showToast } from "@calcom/ui/components/toast";
import { revalidateTeamsList } from "@calcom/web/app/(use-page-wrapper)/(main-nav)/teams/actions";
import MakeTeamPrivateSwitch from "~/ee/teams/components/MakeTeamPrivateSwitch";
import MemberInvitationModal from "~/ee/teams/components/MemberInvitationModal";
type Members = RouterOutputs["viewer"]["organizations"]["listOtherTeamMembers"]["rows"];
type Team = RouterOutputs["viewer"]["organizations"]["getOtherTeam"];
interface MembersListProps {
members: Members | undefined;
team: Team | undefined;
fetchNextPage: () => void;
hasNextPage: boolean | undefined;
isFetchingNextPage: boolean | undefined;
}
function MembersList(props: MembersListProps) {
const { t } = useLocale();
const { hasNextPage, members = [], team, fetchNextPage, isFetchingNextPage } = props;
return (
<div className="flex flex-col gap-y-3">
{members?.length && team ? (
<ul className="divide-subtle border-subtle divide-y rounded-md border">
{members.map((member) => {
return <MemberListItem key={member.id} member={member} />;
})}
</ul>
) : null}
{members?.length === 0 && (
<div className="flex flex-col items-center justify-center">
<p className="text-default text-sm font-bold">{t("no_members_found")}</p>
</div>
)}
<div className="text-default p-4 text-center">
<Button
color="minimal"
loading={isFetchingNextPage}
disabled={!hasNextPage}
onClick={() => fetchNextPage()}>
{hasNextPage ? t("load_more_results") : t("no_more_results")}
</Button>
</div>
</div>
);
}
export const memberInvitationModalRef = {
current: null as null | ((show: boolean) => void),
};
export const TeamMembersCTA = () => {
const { t } = useLocale();
const session = useSession();
const { data: currentOrg } = trpc.viewer.organizations.listCurrent.useQuery(undefined, {
enabled: !!session.data?.user?.org,
});
const isOrgAdminOrOwner = currentOrg && checkAdminOrOwner(currentOrg.user.role);
if (!isOrgAdminOrOwner) return null;
return (
<Button
type="button"
color="primary"
StartIcon="plus"
className="ml-auto"
onClick={() => memberInvitationModalRef.current?.(true)}
data-testid="new-member-button">
{t("add")}
</Button>
);
};
const MembersView = () => {
const { t, i18n } = useLocale();
const router = useRouter();
const params = useParamsWithFallback();
const teamId = Number(params.id);
const utils = trpc.useUtils();
// const [query, setQuery] = useState<string | undefined>("");
// const [queryToFetch, setQueryToFetch] = useState<string | undefined>("");
const limit = 20;
const [showMemberInvitationModal, setShowMemberInvitationModal] = useState<boolean>(false);
const {
data: team,
isPending: isTeamLoading,
error: otherTeamError,
} = trpc.viewer.organizations.getOtherTeam.useQuery(
{ teamId },
{
enabled: !Number.isNaN(teamId),
}
);
const { data: orgMembersNotInThisTeam, isPending: isOrgListLoading } =
trpc.viewer.organizations.getMembers.useQuery(
{
teamIdToExclude: teamId,
distinctUser: true,
},
{
enabled: !Number.isNaN(teamId),
}
);
const {
fetchNextPage,
isFetchingNextPage,
hasNextPage,
error: otherMembersError,
data,
} = trpc.viewer.organizations.listOtherTeamMembers.useInfiniteQuery(
{ teamId, limit },
{
enabled: !Number.isNaN(teamId),
getNextPageParam: (lastPage) => lastPage.nextCursor,
placeholderData: keepPreviousData,
}
);
useEffect(
function refactorMeWithoutEffect() {
if (otherMembersError || otherTeamError) {
router.replace("/enterprise");
}
},
[router, otherMembersError, otherTeamError]
);
useEffect(() => {
memberInvitationModalRef.current = setShowMemberInvitationModal;
return () => {
memberInvitationModalRef.current = null;
};
}, []);
const isPending = isTeamLoading || isOrgListLoading;
const inviteMemberMutation = trpc.viewer.teams.inviteMember.useMutation({
onSuccess: () => {
utils.viewer.organizations.getMembers.invalidate();
utils.viewer.organizations.listOtherTeams.invalidate();
utils.viewer.teams.list.invalidate();
revalidateTeamsList();
utils.viewer.organizations.listOtherTeamMembers.invalidate();
},
});
return (
<>
{!isPending && (
<>
<div>
<>
{/* Currently failing due to re render and loose focus */}
{/* <TextField
type="search"
autoComplete="false"
onChange={(e) => {
setQuery(e.target.value);
debouncedFunction(e.target.value);
}}
value={query}
placeholder={`${t("search")}...`}
/> */}
<MembersList
members={data?.pages?.flatMap((page) => page.rows) ?? []}
team={team}
fetchNextPage={fetchNextPage}
hasNextPage={hasNextPage}
isFetchingNextPage={isFetchingNextPage}
/>
</>
{team && (
<>
<hr className="border-subtle my-8 mt-6" />
<MakeTeamPrivateSwitch
teamId={team.id}
isPrivate={team.isPrivate}
disabled={false}
isOrg={false}
/>
</>
)}
</div>
{showMemberInvitationModal && team && (
<MemberInvitationModal
isPending={inviteMemberMutation.isPending}
isOpen={showMemberInvitationModal}
orgMembers={orgMembersNotInThisTeam}
teamId={team.id}
disableCopyLink={true}
onExit={() => setShowMemberInvitationModal(false)}
onSubmit={(values, resetFields) => {
inviteMemberMutation.mutate(
{
teamId,
language: i18n.language,
role: values.role,
usernameOrEmail: values.emailOrUsername,
creationSource: CreationSource.WEBAPP,
},
{
onSuccess: async (data) => {
await utils.viewer.teams.get.invalidate();
setShowMemberInvitationModal(false);
if (Array.isArray(data.usernameOrEmail)) {
showToast(
t("email_invite_team_bulk", {
userCount: data.numUsersInvited,
}),
"success"
);
resetFields();
} else {
showToast(
t("email_invite_team", {
email: data.usernameOrEmail,
}),
"success"
);
}
},
onError: (error) => {
showToast(error.message, "error");
},
}
);
}}
onSettingsOpen={() => {
setShowMemberInvitationModal(false);
}}
/>
)}
</>
)}
</>
);
};
export default MembersView;