Compare commits
10 Commits
main
...
feat/org-a
Author | SHA1 | Date | |
---|---|---|---|
|
7a7571d688 | ||
|
90c8057e7d | ||
|
6d673da128 | ||
|
fd539ad76c | ||
|
375961dc8a | ||
|
582079d32c | ||
|
c2951d3126 | ||
|
d58b5f0204 | ||
|
65d6b7e9f3 | ||
|
93aad0a8f8 |
|
@ -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, "+"),
|
||||
|
|
|
@ -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,
|
||||
},
|
||||
|
|
|
@ -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 },
|
||||
});
|
||||
|
|
|
@ -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": {
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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
|
|
@ -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>
|
||||
);
|
||||
}
|
|
@ -10,5 +10,6 @@ export type AppFlags = {
|
|||
workflows: boolean;
|
||||
"v2-booking-page": boolean;
|
||||
"managed-event-types": boolean;
|
||||
organizations: boolean;
|
||||
"google-workspace-directory": boolean;
|
||||
};
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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 &&
|
||||
|
|
|
@ -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;
|
|
@ -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[]
|
||||
}
|
||||
|
||||
|
|
|
@ -142,7 +142,10 @@ async function seedAppData() {
|
|||
],
|
||||
user: {
|
||||
connect: {
|
||||
email_username: {
|
||||
username: "pro",
|
||||
email: "pro@example.com",
|
||||
},
|
||||
},
|
||||
},
|
||||
name: seededForm.name,
|
||||
|
|
|
@ -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,
|
||||
});
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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 ",
|
||||
},
|
||||
|
@ -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"
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
|
|
|
@ -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}
|
||||
/>
|
||||
|
|
Loading…
Reference in New Issue
Block a user