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:
Peer Richelsen
2024-03-12 15:56:43 +00:00
committed by GitHub
co-authored by Udit Takkar Udit Takkar CarinaWolli
parent 0e110022fb
commit f2478ea7d3
10 changed files with 133 additions and 26 deletions
@@ -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;
+11 -3
View File
@@ -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,