Compare commits

...

10 Commits

Author SHA1 Message Date
Joe Au-Yeung 7a7571d688
Merge branch 'feat/organizations' into feat/org-app-install 2023-05-30 05:41:01 +09:00
Sean Brydon 90c8057e7d Add orgid to install/uninstall 2023-05-29 11:06:44 +01:00
Sean Brydon 6d673da128 App install dropdown for orgs 2023-05-29 10:59:50 +01:00
Leo Giovanetti fd539ad76c Merge branch 'main' into feat/organizations 2023-05-28 18:44:45 -03:00
Sean Brydon 375961dc8a AppInstallButtonBase 2023-05-25 17:15:18 +01:00
Leo Giovanetti 582079d32c Merge branch 'main' into feat/organizations 2023-05-25 12:30:15 -03:00
Leo Giovanetti c2951d3126 Merge branch 'main' into feat/organizations 2023-05-25 10:15:09 -03:00
Leo Giovanetti d58b5f0204 Merge branch 'main' into feat/organizations 2023-05-22 15:06:15 -03:00
Leo Giovanetti 65d6b7e9f3 Adding feature flag 2023-05-19 11:37:10 -03:00
Leo Giovanetti 93aad0a8f8 Initial commit 2023-05-19 10:30:10 -03:00
17 changed files with 238 additions and 24 deletions

View File

@ -70,7 +70,7 @@ async function getUserPageProps(context: GetStaticPropsContext) {
const { type: slug, user: username } = paramsSchema.parse(context.params);
const ssg = await ssgInit(context);
const user = await prisma.user.findUnique({
const user = await prisma.user.findFirst({
where: {
/** TODO: We should standarize this */
username: username.toLowerCase().replace(/( |%20)/g, "+"),

View File

@ -18,7 +18,7 @@ export const getStaticProps: GetStaticProps<
{ user: string }
> = async (context) => {
const { user: username, month } = paramsSchema.parse(context.params);
const userWithCredentials = await prisma.user.findUnique({
const userWithCredentials = await prisma.user.findFirst({
where: {
username,
},

View File

@ -18,7 +18,7 @@ async function getIdentityData(req: NextApiRequest) {
const { username, teamname } = querySchema.parse(req.query);
if (username) {
const user = await prisma.user.findUnique({
const user = await prisma.user.findFirst({
where: { username },
select: { avatar: true, email: true },
});

View File

@ -6,9 +6,7 @@
"logo": "icon.svg",
"url": "https://cal.com/apps/mirotalk",
"variant": "conferencing",
"categories": [
"video"
],
"categories": ["video"],
"publisher": "Cal.com, Inc.",
"email": "support@cal.com",
"appData": {
@ -25,4 +23,4 @@
"__createdUsingCli": true,
"__template": "event-type-location-video-static",
"dirName": "mirotalk"
}
}

View File

@ -69,7 +69,7 @@ const providers: Provider[] = [
throw new Error(ErrorCode.InternalServerError);
}
const user = await prisma.user.findUnique({
const user = await prisma.user.findFirst({
where: {
email: credentials.email.toLowerCase(),
},
@ -605,7 +605,12 @@ export const AUTH_OPTIONS: AuthOptions = {
!existingUserWithEmail.username
) {
await prisma.user.update({
where: { email: existingUserWithEmail.email },
where: {
email_username: {
email: existingUserWithEmail.email,
username: existingUserWithEmail.username!,
},
},
data: {
// update the email to the IdP email
email: user.email,

View File

@ -0,0 +1,53 @@
import { Canvas, Meta, Story, ArgsTable } from "@storybook/addon-docs";
import { Examples, Example, Note, Title, CustomArgsTable } from "@calcom/storybook/components";
import { AppInstallButton } from "./AppInstallButton";
<Meta title="Organization/AppInstallButton" component={AppInstallButton} />
<Title title="AppInstallButton" suffix="Brief" subtitle="Version 2.0 — Last Update: 22 Aug 2022" />
export const USERS = [
{
id: "abcde",
orgId: "org1",
name: "John Doe",
avatarURL: "",
},
{
id: "abc",
name: "Cal.com Org",
avatarURL: "",
},
];
## Structure
<CustomArgsTable of={AppInstallButton} />
<Examples title="AppInstallButton style">
<Example title="Single User Not installed" >
<AppInstallButton
onInstall={(e) => console.log(e)}
onUninstall={(e) => console.log(e)}
users={[USERS[0]]}
/>
</Example>
<Example title="Single User Installed" >
<AppInstallButton
onInstall={(e) => console.log(e)}
onUninstall={(e) => console.log(e)}
users={[{ ...USERS[0], installed: true }]}
/>
</Example>
<Example title="Multi Users With one Installed" >
<AppInstallButton
onInstall={(e) => console.log(e)}
onUninstall={(e) => console.log(e)}
users={[{ ...USERS[0], installed: true },USERS[1]]}
/>
</Example>
</Examples>
## Usage

View File

@ -0,0 +1,95 @@
import { PlusIcon } from "lucide-react";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import type { ButtonProps } from "@calcom/ui";
import { Badge } from "@calcom/ui";
import { Avatar, DropdownMenuItem } from "@calcom/ui";
import { DropdownMenuContent, DropdownMenuLabel, DropdownMenuPortal } from "@calcom/ui";
import { DropdownMenuTrigger } from "@calcom/ui";
import { Button } from "@calcom/ui";
import { Dropdown } from "@calcom/ui";
type AppInstallButtonProps = {
onInstall: (userId: string, orgId?: string) => void;
onUninstall: (userId: string, orgId?: string) => void;
users: {
id: string;
orgId?: string; // present if org
name: string;
avatarUrl: string;
installed?: boolean;
}[];
} & ButtonProps;
export function AppInstallButton(props: AppInstallButtonProps) {
const { t } = useLocale();
const { users, onInstall, onUninstall } = props;
if (users.length === 0) return null;
if (users.length === 1) {
if (!users[0].installed) {
return (
<Button
onClick={() => onInstall(users[0].id, users[0].orgId)}
color="secondary"
size="sm"
StartIcon={PlusIcon}>
{t("install")}
</Button>
);
} else {
return (
<Button
onClick={() => onUninstall(users[0].id, users[0].orgId)}
color="secondary"
size="sm"
StartIcon={PlusIcon}>
{t("uninstall")}
</Button>
);
}
}
return (
<Dropdown>
<DropdownMenuTrigger asChild>
<Button color="secondary" size="sm" StartIcon={PlusIcon}>
{t("install")}
</Button>
</DropdownMenuTrigger>
<DropdownMenuPortal>
<DropdownMenuContent className="min-w-64">
<DropdownMenuLabel>{t("install_app_on")}</DropdownMenuLabel>
{users.map((user) => {
return (
<DropdownMenuItem
key={user.id}
className="flex h-9 flex-1 items-center space-x-2 pl-3"
onSelect={() => {
user.installed ? onUninstall(user.id, user.orgId) : onInstall(user.id, user.orgId);
}}>
<div className="h-5 w-5">
<Avatar
className="h-5 w-5"
size="sm"
alt={user.name}
imageSrc={user.avatarUrl}
gravatarFallbackMd5="hash"
asChild
/>
</div>
<div className="mr-auto text-sm font-medium leading-none">{user.name}</div>
{user.installed && (
<Badge size="sm" className="ml-auto">
{t("installed")}{" "}
</Badge>
)}
</DropdownMenuItem>
);
})}
</DropdownMenuContent>
</DropdownMenuPortal>
</Dropdown>
);
}

View File

@ -10,5 +10,6 @@ export type AppFlags = {
workflows: boolean;
"v2-booking-page": boolean;
"managed-event-types": boolean;
organizations: boolean;
"google-workspace-directory": boolean;
};

View File

@ -2,7 +2,9 @@ import { trpc } from "@calcom/trpc/react";
export function useFlags() {
const query = trpc.viewer.features.map.useQuery(undefined, {
initialData: process.env.NEXT_PUBLIC_IS_E2E ? { "managed-event-types": true, teams: true } : {},
initialData: process.env.NEXT_PUBLIC_IS_E2E
? { "managed-event-types": true, organizations: true, teams: true }
: {},
});
return query.data;
}

View File

@ -201,7 +201,7 @@ const SettingsSidebarContainer = ({
alt="User Avatar"
/>
)}
<p className="text-sm font-medium leading-5 truncate">{t(tab.name)}</p>
<p className="truncate text-sm font-medium leading-5">{t(tab.name)}</p>
</div>
</div>
<div className="my-3 space-y-0.5">
@ -226,7 +226,7 @@ const SettingsSidebarContainer = ({
{tab && tab.icon && (
<tab.icon className="h-[16px] w-[16px] stroke-[2px] ltr:mr-3 rtl:ml-3 md:mt-0" />
)}
<p className="text-sm font-medium leading-5 truncate">{t(tab.name)}</p>
<p className="truncate text-sm font-medium leading-5">{t(tab.name)}</p>
</div>
</Link>
{teams &&

View File

@ -0,0 +1,40 @@
/*
Warnings:
- A unique constraint covering the columns `[email,username]` on the table `users` will be added. If there are existing duplicate values, this will fail.
- A unique constraint covering the columns `[username,organizationId]` on the table `users` will be added. If there are existing duplicate values, this will fail.
*/
-- DropIndex
DROP INDEX "users_email_idx";
-- DropIndex
DROP INDEX "users_username_key";
-- AlterTable
ALTER TABLE "Team" ADD COLUMN "parentId" INTEGER;
-- AlterTable
ALTER TABLE "users" ADD COLUMN "organizationId" INTEGER;
-- CreateIndex
CREATE UNIQUE INDEX "users_email_username_key" ON "users"("email", "username");
-- CreateIndex
CREATE UNIQUE INDEX "users_username_organizationId_key" ON "users"("username", "organizationId");
-- AddForeignKey
ALTER TABLE "users" ADD CONSTRAINT "users_organizationId_fkey" FOREIGN KEY ("organizationId") REFERENCES "Team"("id") ON DELETE SET NULL ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "Team" ADD CONSTRAINT "Team_parentId_fkey" FOREIGN KEY ("parentId") REFERENCES "Team"("id") ON DELETE CASCADE ON UPDATE CASCADE;
INSERT INTO
"Feature" (slug, enabled, description, "type")
VALUES
(
'organizations',
true,
'Manage organizations with multiple teams',
'OPERATIONAL'
) ON CONFLICT (slug) DO NOTHING;

View File

@ -166,10 +166,10 @@ enum UserPermissionRole {
model User {
id Int @id @default(autoincrement())
username String? @unique
username String?
name String?
/// @zod.email()
email String @unique
email String
emailVerified DateTime?
password String?
bio String?
@ -225,8 +225,16 @@ model User {
routingForms App_RoutingForms_Form[] @relation("routing-form")
verifiedNumbers VerifiedNumber[]
hosts Host[]
organizationId Int?
organization Team? @relation("scope", fields: [organizationId], references: [id], onDelete: SetNull)
// Linking account code for orgs v2
//linkedByUserId Int?
//linkedBy User? @relation("linked_account", fields: [linkedByUserId], references: [id], onDelete: Cascade)
//linkedUsers User[] @relation("linked_account")*/
@@index([email])
@@unique([email])
@@unique([email, username])
@@unique([username, organizationId])
@@index([emailVerified])
@@index([identityProvider])
@@index([identityProviderId])
@ -255,6 +263,10 @@ model Team {
brandColor String @default("#292929")
darkBrandColor String @default("#fafafa")
verifiedNumbers VerifiedNumber[]
parentId Int?
parent Team? @relation("organization", fields: [parentId], references: [id], onDelete: Cascade)
children Team[] @relation("organization")
scopedMembers User[] @relation("scope")
webhooks Webhook[]
}

View File

@ -142,7 +142,10 @@ async function seedAppData() {
],
user: {
connect: {
username: "pro",
email_username: {
username: "pro",
email: "pro@example.com",
},
},
},
name: seededForm.name,

View File

@ -50,7 +50,7 @@ async function createUserAndEventType(opts: {
};
const user = await prisma.user.upsert({
where: { email: opts.user.email },
where: { email_username: { email: opts.user.email, username: opts.user.username } },
update: userData,
create: userData,
});

View File

@ -39,8 +39,8 @@ export function Avatar(props: AvatarProps) {
<AvatarPrimitive.Root
className={classNames(
"bg-emphasis item-center relative inline-flex aspect-square justify-center overflow-hidden rounded-full",
props.className,
sizesPropsBySize[size]
sizesPropsBySize[size],
props.className
)}>
<>
<AvatarPrimitive.Image

View File

@ -51,7 +51,7 @@ const buttonClasses = cva(
destructive: "",
},
size: {
sm: "px-3 py-2 leading-4 rounded-sm" /** For backwards compatibility */,
sm: "px-3 py-2 leading-4 rounded-md" /** For backwards compatibility */,
base: "h-9 px-4 py-2.5 ",
lg: "h-[36px] px-4 py-2.5 ",
},
@ -136,7 +136,7 @@ const buttonClasses = cva(
{
variant: "icon",
size: "sm",
className: "h-6 w-6 !p-1",
className: "h-6 w-6 !p-1 ",
},
{
variant: "fab",
@ -203,7 +203,8 @@ export const Button = forwardRef<HTMLAnchorElement | HTMLButtonElement, ButtonPr
<StartIcon
className={classNames(
variant === "icon" && "h-4 w-4",
variant === "button" && "h-4 w-4 stroke-[1.5px] ltr:-ml-1 ltr:mr-2 rtl:-mr-1 rtl:ml-2"
variant === "button" && "h-4 w-4 stroke-[1.5px] ltr:-ml-1 ltr:mr-2 rtl:-mr-1 rtl:ml-2",
size === "sm" && "ltr:mr-2 ltr:-ml-2 rtl:-mr-2 rtl:ml-2"
)}
/>
)}

View File

@ -40,7 +40,8 @@ export const DropdownMenuContent = forwardRef<HTMLDivElement, DropdownMenuConten
sideOffset={sideOffset}
className={classNames(
"shadow-dropdown w-50 bg-default border-subtle relative z-10 ml-1.5 origin-top-right rounded-md border text-sm",
"[&>*:first-child]:mt-1 [&>*:last-child]:mb-1"
"[&>*:first-child]:mt-1 [&>*:last-child]:mb-1",
props.className
)}
ref={forwardedRef}>
{children}
@ -59,7 +60,10 @@ type DropdownMenuItemProps = ComponentProps<(typeof DropdownMenuPrimitive)["Chec
export const DropdownMenuItem = forwardRef<HTMLDivElement, DropdownMenuItemProps>(
({ className = "", ...props }, forwardedRef) => (
<DropdownMenuPrimitive.Item
className={`focus:ring-brand-800 hover:bg-subtle hover:text-emphasis text-default text-sm ring-inset first-of-type:rounded-t-[inherit] last-of-type:rounded-b-[inherit] focus:outline-none focus:ring-1 ${className}`}
className={classNames(
`focus:ring-brand hover:bg-subtle hover:text-emphasis text-default text-sm ring-inset first-of-type:rounded-t-[inherit] last-of-type:rounded-b-[inherit] hover:cursor-pointer focus:outline-none focus:ring-1`,
className
)}
{...props}
ref={forwardedRef}
/>