feat: new privacy settings for orgs (#13693)
* moved privacy setting to new tab * i18n * added more privacy switches * fix: hide org teams and members * fix: use i18n * fix: privacy page * feat: also hide team mebers when org is private * chore: remove log * fix type error * chore: type err * chore: feedback * chore: type err --------- Co-authored-by: Udit Takkar <udit222001@gmail.com> Co-authored-by: Udit Takkar <53316345+Udit-takkar@users.noreply.github.com> Co-authored-by: CarinaWolli <wollencarina@gmail.com>
This commit is contained in:
co-authored by
Udit Takkar
Udit Takkar
CarinaWolli
parent
0e110022fb
commit
f2478ea7d3
@@ -90,6 +90,17 @@ export const getServerSideProps = async (context: GetServerSidePropsContext) =>
|
||||
},
|
||||
}),
|
||||
},
|
||||
include: {
|
||||
parent: {
|
||||
select: {
|
||||
id: true,
|
||||
slug: true,
|
||||
name: true,
|
||||
isPrivate: true,
|
||||
isOrganization: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
if (!unpublishedTeam) return { notFound: true } as const;
|
||||
@@ -103,19 +114,23 @@ export const getServerSideProps = async (context: GetServerSidePropsContext) =>
|
||||
} as const;
|
||||
}
|
||||
|
||||
const isTeamOrParentOrgPrivate = team.isPrivate || (team.parent?.isOrganization && team.parent?.isPrivate);
|
||||
|
||||
team.eventTypes =
|
||||
team.eventTypes?.map((type) => ({
|
||||
...type,
|
||||
users: type.users.map((user) => ({
|
||||
...user,
|
||||
avatar: `/${user.username}/avatar.png`,
|
||||
})),
|
||||
users: !isTeamOrParentOrgPrivate
|
||||
? type.users.map((user) => ({
|
||||
...user,
|
||||
avatar: `/${user.username}/avatar.png`,
|
||||
}))
|
||||
: [],
|
||||
descriptionAsSafeHTML: markdownToSafeHTML(type.description),
|
||||
})) ?? null;
|
||||
|
||||
const safeBio = markdownToSafeHTML(team.bio) || "";
|
||||
|
||||
const members = !team.isPrivate
|
||||
const members = !isTeamOrParentOrgPrivate
|
||||
? team.members.map((member) => {
|
||||
return {
|
||||
name: member.name,
|
||||
@@ -139,7 +154,13 @@ export const getServerSideProps = async (context: GetServerSidePropsContext) =>
|
||||
|
||||
return {
|
||||
props: {
|
||||
team: { ...serializableTeam, safeBio, members, metadata },
|
||||
team: {
|
||||
...serializableTeam,
|
||||
safeBio,
|
||||
members,
|
||||
metadata,
|
||||
children: isTeamOrParentOrgPrivate ? [] : team.children,
|
||||
},
|
||||
themeBasis: serializableTeam.slug,
|
||||
trpcState: ssr.dehydrate(),
|
||||
markdownStrippedBio,
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
import PrivacyView from "@calcom/features/ee/organizations/pages/settings/privacy";
|
||||
|
||||
import type { CalPageWrapper } from "@components/PageWrapper";
|
||||
import PageWrapper from "@components/PageWrapper";
|
||||
|
||||
const Page = PrivacyView as CalPageWrapper;
|
||||
Page.PageWrapper = PageWrapper;
|
||||
|
||||
export default Page;
|
||||
@@ -50,6 +50,8 @@ function TeamPage({
|
||||
const isBioEmpty = !team.bio || !team.bio.replace("<p><br></p>", "").length;
|
||||
const metadata = teamMetadataSchema.parse(team.metadata);
|
||||
|
||||
const teamOrOrgIsPrivate = team.isPrivate || (team?.parent?.isOrganization && team.parent?.isPrivate);
|
||||
|
||||
useEffect(() => {
|
||||
telemetry.event(
|
||||
telemetryEventTypes.pageView,
|
||||
@@ -194,11 +196,17 @@ function TeamPage({
|
||||
)}
|
||||
</div>
|
||||
{team.isOrganization ? (
|
||||
<SubTeams />
|
||||
!teamOrOrgIsPrivate ? (
|
||||
<SubTeams />
|
||||
) : (
|
||||
<div className="w-full text-center">
|
||||
<h2 className="text-emphasis font-semibold">{t("you_cannot_see_teams_of_org")}</h2>
|
||||
</div>
|
||||
)
|
||||
) : (
|
||||
<>
|
||||
{(showMembers.isOn || !team.eventTypes?.length) &&
|
||||
(team.isPrivate ? (
|
||||
(teamOrOrgIsPrivate ? (
|
||||
<div className="w-full text-center">
|
||||
<h2 data-testid="you-cannot-see-team-members" className="text-emphasis font-semibold">
|
||||
{t("you_cannot_see_team_members")}
|
||||
@@ -212,7 +220,7 @@ function TeamPage({
|
||||
<EventTypes eventTypes={team.eventTypes} />
|
||||
|
||||
{/* Hide "Book a team member button when team is private or hideBookATeamMember is true" */}
|
||||
{!team.hideBookATeamMember && !team.isPrivate && (
|
||||
{!team.hideBookATeamMember && !teamOrOrgIsPrivate && (
|
||||
<div>
|
||||
<div className="relative mt-12">
|
||||
<div className="absolute inset-0 flex items-center" aria-hidden="true">
|
||||
|
||||
@@ -1085,6 +1085,7 @@
|
||||
"make_team_private": "Make team private",
|
||||
"make_team_private_description": "Your team members won't be able to see other team members when this is turned on.",
|
||||
"you_cannot_see_team_members": "You cannot see all the team members of a private team.",
|
||||
"you_cannot_see_teams_of_org":"You cannot see teams of a private organization.",
|
||||
"allow_booker_to_select_duration": "Allow booker to select duration",
|
||||
"impersonate_user_tip": "All uses of this feature is audited.",
|
||||
"impersonating_user_warning": "Impersonating username \"{{user}}\".",
|
||||
@@ -1620,6 +1621,7 @@
|
||||
"test_routing": "Test Routing",
|
||||
"payment_app_disabled": "An admin has disabled a payment app",
|
||||
"edit_event_type": "Edit event type",
|
||||
"only_admin_can_see_members_of_org": "This Organization is private, and only the organization's admin or owner can view its members.",
|
||||
"collective_scheduling": "Collective Scheduling",
|
||||
"make_it_easy_to_book": "Make it easy to book your team when everyone is available.",
|
||||
"find_the_best_person": "Find the best person available and cycle through your team.",
|
||||
@@ -2297,6 +2299,8 @@
|
||||
"field_identifiers_as_variables": "Use field identifiers as variables for your custom event redirect",
|
||||
"field_identifiers_as_variables_with_example": "Use field identifiers as variables for your custom event redirect (e.g. {{variable}})",
|
||||
"account_already_linked": "Account is already linked",
|
||||
"privacy_organization_description": "Manage privacy settings for your organization",
|
||||
"privacy": "Privacy",
|
||||
"team_will_be_under_org": "New teams will be under your organization",
|
||||
"add_group_name": "Add group name",
|
||||
"group_name": "Group Name",
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
"use client";
|
||||
|
||||
import LicenseRequired from "@calcom/features/ee/common/components/LicenseRequired";
|
||||
import MakeTeamPrivateSwitch from "@calcom/features/ee/teams/components/MakeTeamPrivateSwitch";
|
||||
import { getLayout } from "@calcom/features/settings/layouts/SettingsLayout";
|
||||
import { UserListTable } from "@calcom/features/users/components/UserTable/UserListTable";
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
@@ -11,29 +10,24 @@ import { Meta } from "@calcom/ui";
|
||||
|
||||
const MembersView = () => {
|
||||
const { t } = useLocale();
|
||||
const { data: currentOrg } = trpc.viewer.organizations.listCurrent.useQuery();
|
||||
const { data: currentOrg, isPending } = trpc.viewer.organizations.listCurrent.useQuery();
|
||||
|
||||
const isInviteOpen = !currentOrg?.user.accepted;
|
||||
const isOrgAdminOrOwner =
|
||||
currentOrg &&
|
||||
(currentOrg.user.role === MembershipRole.OWNER || currentOrg.user.role === MembershipRole.ADMIN);
|
||||
|
||||
const canLoggedInUserSeeMembers =
|
||||
(currentOrg?.isPrivate && isOrgAdminOrOwner) || isOrgAdminOrOwner || !currentOrg?.isPrivate;
|
||||
|
||||
return (
|
||||
<LicenseRequired>
|
||||
<Meta title={t("organization_members")} description={t("organization_description")} />
|
||||
<div>
|
||||
{((currentOrg?.isPrivate && isOrgAdminOrOwner) || isOrgAdminOrOwner || !currentOrg?.isPrivate) && (
|
||||
<UserListTable />
|
||||
)}
|
||||
{currentOrg && isOrgAdminOrOwner && (
|
||||
<MakeTeamPrivateSwitch
|
||||
isOrg={true}
|
||||
teamId={currentOrg.id}
|
||||
isPrivate={currentOrg.isPrivate}
|
||||
disabled={isInviteOpen}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
<div>{!isPending && canLoggedInUserSeeMembers && <UserListTable />}</div>
|
||||
{!canLoggedInUserSeeMembers && (
|
||||
<div className="border-subtle rounded-xl border p-6">
|
||||
<h2 className="text-default">{t("only_admin_can_see_members_of_org")}</h2>
|
||||
</div>
|
||||
)}
|
||||
</LicenseRequired>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -0,0 +1,43 @@
|
||||
"use client";
|
||||
|
||||
import LicenseRequired from "@calcom/features/ee/common/components/LicenseRequired";
|
||||
import MakeTeamPrivateSwitch from "@calcom/features/ee/teams/components/MakeTeamPrivateSwitch";
|
||||
import { getLayout } from "@calcom/features/settings/layouts/SettingsLayout";
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
import { MembershipRole } from "@calcom/prisma/enums";
|
||||
import { trpc } from "@calcom/trpc/react";
|
||||
import { Meta } from "@calcom/ui";
|
||||
|
||||
const PrivacyView = () => {
|
||||
const { t } = useLocale();
|
||||
const { data: currentOrg } = trpc.viewer.organizations.listCurrent.useQuery();
|
||||
const isOrgAdminOrOwner =
|
||||
currentOrg &&
|
||||
(currentOrg.user.role === MembershipRole.OWNER || currentOrg.user.role === MembershipRole.ADMIN);
|
||||
const isInviteOpen = !currentOrg?.user.accepted;
|
||||
|
||||
const isDisabled = isInviteOpen || !isOrgAdminOrOwner;
|
||||
|
||||
if (!currentOrg) return null;
|
||||
|
||||
return (
|
||||
<LicenseRequired>
|
||||
<Meta
|
||||
borderInShellHeader={false}
|
||||
title={t("privacy")}
|
||||
description={t("privacy_organization_description")}
|
||||
/>
|
||||
<div>
|
||||
<MakeTeamPrivateSwitch
|
||||
isOrg={true}
|
||||
teamId={currentOrg.id}
|
||||
isPrivate={currentOrg.isPrivate}
|
||||
disabled={isDisabled}
|
||||
/>
|
||||
</div>
|
||||
</LicenseRequired>
|
||||
);
|
||||
};
|
||||
PrivacyView.getLayout = getLayout;
|
||||
|
||||
export default PrivacyView;
|
||||
@@ -92,6 +92,10 @@ const tabs: VerticalTabItemProps[] = [
|
||||
name: "members",
|
||||
href: "/settings/organizations/members",
|
||||
},
|
||||
{
|
||||
name: "privacy",
|
||||
href: "/settings/organizations/privacy",
|
||||
},
|
||||
{
|
||||
name: "appearance",
|
||||
href: "/settings/organizations/appearance",
|
||||
|
||||
@@ -99,6 +99,8 @@ export async function getTeamWithMembers(args: {
|
||||
id: true,
|
||||
slug: true,
|
||||
name: true,
|
||||
isPrivate: true,
|
||||
isOrganization: true,
|
||||
},
|
||||
},
|
||||
children: {
|
||||
|
||||
@@ -20,6 +20,16 @@ export const listMembersHandler = async ({ ctx, input }: GetOptions) => {
|
||||
throw new TRPCError({ code: "NOT_FOUND", message: "User is not part of any organization." });
|
||||
}
|
||||
|
||||
if (ctx.user.organization.isPrivate && !ctx.user.organization.isOrgAdmin) {
|
||||
return {
|
||||
canUserGetMembers: false,
|
||||
rows: [],
|
||||
meta: {
|
||||
totalRowCount: 0,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
const { cursor, limit } = input;
|
||||
|
||||
const getTotalMembers = await prisma.membership.count({
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { markdownToSafeHTML } from "@calcom/lib/markdownToSafeHTML";
|
||||
import { getTeamWithMembers } from "@calcom/lib/server/queries/teams";
|
||||
import { MembershipRole } from "@calcom/prisma/enums";
|
||||
|
||||
import { TRPCError } from "@trpc/server";
|
||||
|
||||
@@ -31,9 +32,20 @@ export const getHandler = async ({ ctx, input }: GetOptions) => {
|
||||
if (!membership) {
|
||||
throw new TRPCError({ code: "NOT_FOUND", message: "Not a member of this team." });
|
||||
}
|
||||
const { members, ...restTeam } = team;
|
||||
|
||||
// Hide Members of team when 1) Org is private and logged in user is not admin or owner
|
||||
// OR
|
||||
// 2)Team is private and logged in user is not admin or owner of team or Organization's admin or owner
|
||||
const hideMembers =
|
||||
(ctx.user.profile?.organization?.isPrivate && !ctx.user.organization?.isOrgAdmin) ||
|
||||
(team.isPrivate &&
|
||||
!(membership.role === MembershipRole.OWNER || membership.role === MembershipRole.ADMIN) &&
|
||||
!ctx.user.organization?.isOrgAdmin);
|
||||
|
||||
return {
|
||||
...team,
|
||||
...restTeam,
|
||||
members: hideMembers ? [] : members,
|
||||
safeBio: markdownToSafeHTML(team.bio),
|
||||
membership: {
|
||||
role: membership.role,
|
||||
|
||||
Reference in New Issue
Block a user