Merge branch 'feat/organizations' into feat/organizations-banner

This commit is contained in:
Leo Giovanetti 2023-05-31 10:58:53 -03:00
commit d636cf7b42
29 changed files with 443 additions and 126 deletions

View File

@ -27,8 +27,6 @@ jobs:
- uses: ./.github/actions/env-read-file
- uses: ./.github/actions/cache-db
- uses: ./.github/actions/cache-build
- name: Seed Test DB
run: yarn db-seed
- name: Run Tests
run: yarn e2e --shard=${{ matrix.shard }}/${{ strategy.job-total }}
env:

View File

@ -66,6 +66,12 @@ jobs:
uses: ./.github/workflows/production-build.yml
secrets: inherit
build-without-database:
name: Production build (without database)
needs: env
uses: ./.github/workflows/production-build-without-database.yml
secrets: inherit
e2e:
name: E2E tests
needs: [changes, lint, build]

View File

@ -0,0 +1,16 @@
name: Production Build (without database)
on:
workflow_call:
jobs:
build:
name: Build
runs-on: ubuntu-latest
timeout-minutes: 30
steps:
- uses: actions/checkout@v3
- uses: ./.github/actions/dangerous-git-checkout
- uses: ./.github/actions/yarn-install
- uses: ./.github/actions/env-read-file
- uses: ./.github/actions/cache-build

View File

@ -1,6 +1,6 @@
import { z } from "zod";
import { _BookingModel as Booking, _AttendeeModel, _UserModel } from "@calcom/prisma/zod";
import { _BookingModel as Booking, _AttendeeModel, _UserModel, _PaymentModel } from "@calcom/prisma/zod";
import { extendedBookingCreateBody, iso8601 } from "@calcom/prisma/zod-utils";
import { schemaQueryUserId } from "./shared/queryUserId";
@ -49,6 +49,15 @@ export const schemaBookingReadPublic = Booking.extend({
locale: true,
})
.optional(),
payment: z
.array(
_PaymentModel.pick({
id: true,
success: true,
paymentOption: true,
})
)
.optional(),
}).pick({
id: true,
userId: true,
@ -61,6 +70,7 @@ export const schemaBookingReadPublic = Booking.extend({
timeZone: true,
attendees: true,
user: true,
payment: true,
metadata: true,
status: true,
responses: true,

View File

@ -0,0 +1,20 @@
import { withValidation } from "next-validations";
import { z } from "zod";
import { baseApiParams } from "./baseApiParams";
// Extracted out as utility function so can be reused
// at different endpoints that require this validation.
export const schemaQueryAttendeeEmail = baseApiParams.extend({
attendeeEmail: z.string().email(),
});
export const schemaQuerySingleOrMultipleAttendeeEmails = z.object({
attendeeEmail: z.union([z.string().email(), z.array(z.string().email())]).optional(),
});
export const withValidQueryAttendeeEmail = withValidation({
schema: schemaQueryAttendeeEmail,
type: "Zod",
mode: "query",
});

View File

@ -32,40 +32,65 @@ import { schemaQueryIdParseInt } from "~/lib/validations/shared/queryIdTransform
* content:
* application/json:
* schema:
* $ref: "#/components/schemas/ArrayOfBookings"
* $ref: "#/components/schemas/Booking"
* examples:
* bookings:
* value: [
* booking:
* value:
* {
* "id": 1,
* "description": "Meeting with John",
* "eventTypeId": 2,
* "uid": "a1b2c3d4-e5f6-7890-g1h2-i3j4k5l6m7n8",
* "title": "Business Meeting",
* "startTime": "2023-04-20T10:00:00.000Z",
* "endTime": "2023-04-20T11:00:00.000Z",
* "timeZone": "Europe/London",
* "attendees": [
* {
* "email": "example@cal.com",
* "name": "John Doe",
* "timeZone": "Europe/London",
* "booking": {
* "id": 91,
* "userId": 5,
* "description": "",
* "eventTypeId": 7,
* "uid": "bFJeNb2uX8ANpT3JL5EfXw",
* "title": "60min between Pro Example and John Doe",
* "startTime": "2023-05-25T09:30:00.000Z",
* "endTime": "2023-05-25T10:30:00.000Z",
* "attendees": [
* {
* "email": "john.doe@example.com",
* "name": "John Doe",
* "timeZone": "Asia/Kolkata",
* "locale": "en"
* }
* ],
* "user": {
* "email": "pro@example.com",
* "name": "Pro Example",
* "timeZone": "Asia/Kolkata",
* "locale": "en"
* },
* "payment": [
* {
* "id": 1,
* "success": true,
* "paymentOption": "ON_BOOKING"
* }
* ],
* "metadata": {},
* "status": "ACCEPTED",
* "responses": {
* "email": "john.doe@example.com",
* "name": "John Doe",
* "location": {
* "optionValue": "",
* "value": "inPerson"
* }
* }
* ]
* }
* }
* ]
* 401:
* description: Authorization information is missing or invalid.
* 404:
* description: Booking was not found
*/
export async function getHandler(req: NextApiRequest) {
const { prisma, query } = req;
const { id } = schemaQueryIdParseInt.parse(query);
const booking = await prisma.booking.findUnique({
where: { id },
include: { attendees: true, user: true },
include: { attendees: true, user: true, payment: true },
});
return { booking: schemaBookingReadPublic.parse(booking) };
}

View File

@ -5,6 +5,7 @@ import { HttpError } from "@calcom/lib/http-error";
import { defaultResponder } from "@calcom/lib/server";
import { schemaBookingReadPublic } from "~/lib/validations/booking";
import { schemaQuerySingleOrMultipleAttendeeEmails } from "~/lib/validations/shared/queryAttendeeEmail";
import { schemaQuerySingleOrMultipleUserIds } from "~/lib/validations/shared/queryUserId";
/**
@ -31,6 +32,19 @@ import { schemaQuerySingleOrMultipleUserIds } from "~/lib/validations/shared/que
* items:
* type: integer
* example: [2, 3, 4]
* - in: query
* name: attendeeEmails
* required: false
* schema:
* oneOf:
* - type: string
* format: email
* example: john.doe@example.com
* - type: array
* items:
* type: string
* format: email
* example: [john.doe@example.com, jane.doe@example.com]
* operationId: listBookings
* tags:
* - bookings
@ -45,22 +59,47 @@ import { schemaQuerySingleOrMultipleUserIds } from "~/lib/validations/shared/que
* bookings:
* value: [
* {
* "id": 1,
* "description": "Meeting with John",
* "eventTypeId": 2,
* "uid": "a1b2c3d4-e5f6-7890-g1h2-i3j4k5l6m7n8",
* "title": "Business Meeting",
* "startTime": "2023-04-20T10:00:00.000Z",
* "endTime": "2023-04-20T11:00:00.000Z",
* "timeZone": "Europe/London",
* "attendees": [
* {
* "email": "example@cal.com",
* "name": "John Doe",
* "timeZone": "Europe/London",
* "booking": {
* "id": 91,
* "userId": 5,
* "description": "",
* "eventTypeId": 7,
* "uid": "bFJeNb2uX8ANpT3JL5EfXw",
* "title": "60min between Pro Example and John Doe",
* "startTime": "2023-05-25T09:30:00.000Z",
* "endTime": "2023-05-25T10:30:00.000Z",
* "attendees": [
* {
* "email": "john.doe@example.com",
* "name": "John Doe",
* "timeZone": "Asia/Kolkata",
* "locale": "en"
* }
* ],
* "user": {
* "email": "pro@example.com",
* "name": "Pro Example",
* "timeZone": "Asia/Kolkata",
* "locale": "en"
* },
* "payment": [
* {
* "id": 1,
* "success": true,
* "paymentOption": "ON_BOOKING"
* }
* ],
* "metadata": {},
* "status": "ACCEPTED",
* "responses": {
* "email": "john.doe@example.com",
* "name": "John Doe",
* "location": {
* "optionValue": "",
* "value": "inPerson"
* }
* }
* ]
* }
* }
* ]
* 401:
@ -69,26 +108,42 @@ import { schemaQuerySingleOrMultipleUserIds } from "~/lib/validations/shared/que
* description: No bookings were found
*/
async function handler(req: NextApiRequest) {
const { userId, isAdmin, prisma } = req;
const args: Prisma.BookingFindManyArgs = {};
args.include = {
attendees: true,
user: true,
};
/** Only admins can query other users */
if (isAdmin && req.query.userId) {
const query = schemaQuerySingleOrMultipleUserIds.parse(req.query);
const userIds = Array.isArray(query.userId) ? query.userId : [query.userId || userId];
const users = await prisma.user.findMany({
where: { id: { in: userIds } },
select: { email: true },
});
const userEmails = users.map((u) => u.email);
args.where = {
/**
* Constructs the WHERE clause for Prisma booking findMany operation.
*
* @param userId - The ID of the user making the request. This is used to filter bookings where the user is either the host or an attendee.
* @param attendeeEmails - An array of emails provided in the request for filtering bookings by attendee emails, used in case of Admin calls.
* @param userIds - An array of user IDs to be included in the filter. Defaults to an empty array, and an array of user IDs in case of Admin call containing it.
* @param userEmails - An array of user emails to be included in the filter if it is an Admin call and contains userId in query parameter. Defaults to an empty array.
*
* @returns An object that represents the WHERE clause for the findMany/findUnique operation.
*/
function buildWhereClause(
userId: number,
attendeeEmails: string[],
userIds: number[] = [],
userEmails: string[] = []
) {
const filterByAttendeeEmails = attendeeEmails.length > 0;
const userFilter = userIds.length > 0 ? { userId: { in: userIds } } : { userId };
let whereClause = {};
if (filterByAttendeeEmails) {
whereClause = {
AND: [
userFilter,
{
attendees: {
some: {
email: { in: attendeeEmails },
},
},
},
],
};
} else {
whereClause = {
OR: [
{ userId: { in: userIds } },
userFilter,
{
attendees: {
some: {
@ -98,7 +153,45 @@ async function handler(req: NextApiRequest) {
},
],
};
} else if (!isAdmin) {
}
return {
...whereClause,
};
}
async function handler(req: NextApiRequest) {
const { userId, isAdmin, prisma } = req;
const args: Prisma.BookingFindManyArgs = {};
args.include = {
attendees: true,
user: true,
payment: true,
};
const queryFilterForAttendeeEmails = schemaQuerySingleOrMultipleAttendeeEmails.parse(req.query);
const attendeeEmails = Array.isArray(queryFilterForAttendeeEmails.attendeeEmail)
? queryFilterForAttendeeEmails.attendeeEmail
: typeof queryFilterForAttendeeEmails.attendeeEmail === "string"
? [queryFilterForAttendeeEmails.attendeeEmail]
: [];
const filterByAttendeeEmails = attendeeEmails.length > 0;
/** Only admins can query other users */
if (isAdmin) {
if (req.query.userId) {
const query = schemaQuerySingleOrMultipleUserIds.parse(req.query);
const userIds = Array.isArray(query.userId) ? query.userId : [query.userId || userId];
const users = await prisma.user.findMany({
where: { id: { in: userIds } },
select: { email: true },
});
const userEmails = users.map((u) => u.email);
args.where = buildWhereClause(userId, attendeeEmails, userIds, userEmails);
} else if (filterByAttendeeEmails) {
args.where = buildWhereClause(userId, attendeeEmails, [], []);
}
} else {
const user = await prisma.user.findUnique({
where: { id: userId },
select: {
@ -108,20 +201,7 @@ async function handler(req: NextApiRequest) {
if (!user) {
throw new HttpError({ message: "User not found", statusCode: 500 });
}
args.where = {
OR: [
{
userId,
},
{
attendees: {
some: {
email: user.email,
},
},
},
],
};
args.where = buildWhereClause(userId, attendeeEmails, [], []);
}
const data = await prisma.booking.findMany(args);
return { bookings: data.map((booking) => schemaBookingReadPublic.parse(booking)) };

View File

@ -98,6 +98,25 @@ const swaggerHandler = withSwagger({
},
},
},
payment: {
type: Array,
items: {
properties: {
id: {
type: "number",
example: 1,
},
success: {
type: "boolean",
example: true,
},
paymentOption: {
type: "string",
example: "ON_BOOKING",
},
},
},
},
},
},
},

View File

@ -118,7 +118,7 @@ async function getTeamLogos(subdomain: string) {
}
// load from DB
const { default: prisma } = await import("@calcom/prisma");
const team = await prisma.team.findUnique({
const team = await prisma.team.findFirst({
where: {
slug: subdomain,
},

View File

@ -29,7 +29,7 @@ async function getIdentityData(req: NextApiRequest) {
};
}
if (teamname) {
const team = await prisma.team.findUnique({
const team = await prisma.team.findFirst({
where: { slug: teamname },
select: { logo: true },
});

View File

@ -1,3 +1,4 @@
import { Prisma } from "@prisma/client";
import fs from "fs";
import matter from "gray-matter";
import MarkdownIt from "markdown-it";
@ -88,8 +89,18 @@ function SingleAppPage(props: inferSSRProps<typeof getStaticProps>) {
}
export const getStaticPaths: GetStaticPaths<{ slug: string }> = async () => {
const appStore = await prisma.app.findMany({ select: { slug: true } });
const paths = appStore.map(({ slug }) => ({ params: { slug } }));
let paths: { params: { slug: string } }[] = [];
try {
const appStore = await prisma.app.findMany({ select: { slug: true } });
paths = appStore.map(({ slug }) => ({ params: { slug } }));
} catch (e: unknown) {
if (e instanceof Prisma.PrismaClientInitializationError) {
// Database is not available at build time, but that's ok we fall back to resolving paths on demand
} else {
throw e;
}
}
return {
paths,

View File

@ -1,3 +1,4 @@
import { Prisma } from "@prisma/client";
import type { GetStaticPropsContext, InferGetStaticPropsType } from "next";
import Link from "next/link";
import { useRouter } from "next/router";
@ -54,6 +55,20 @@ Apps.PageWrapper = PageWrapper;
export const getStaticPaths = async () => {
const paths = Object.keys(AppCategories);
try {
await prisma.$queryRaw`SELECT 1`;
} catch (e: unknown) {
if (e instanceof Prisma.PrismaClientInitializationError) {
// Database is not available at build time. Make sure we fall back to building these pages on demand
return {
paths: [],
fallback: "blocking",
};
} else {
throw e;
}
}
return {
paths: paths.map((category) => ({ params: { category } })),
fallback: false,

View File

@ -1,15 +1,19 @@
import type { InferGetStaticPropsType } from "next";
import type { GetServerSidePropsContext } from "next";
import Link from "next/link";
import { getAppRegistry } from "@calcom/app-store/_appRegistry";
import { getAppRegistry, getAppRegistryWithCredentials } from "@calcom/app-store/_appRegistry";
import { getServerSession } from "@calcom/features/auth/lib/getServerSession";
import Shell from "@calcom/features/shell/Shell";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import type { inferSSRProps } from "@calcom/types/inferSSRProps";
import { SkeletonText } from "@calcom/ui";
import { ArrowLeft, ArrowRight } from "@calcom/ui/components/icon";
import PageWrapper from "@components/PageWrapper";
export default function Apps({ categories }: InferGetStaticPropsType<typeof getStaticProps>) {
import { ssrInit } from "@server/lib/ssr";
export default function Apps({ categories }: inferSSRProps<typeof getServerSideProps>) {
const { t, isLocaleReady } = useLocale();
return (
@ -47,8 +51,20 @@ export default function Apps({ categories }: InferGetStaticPropsType<typeof getS
Apps.PageWrapper = PageWrapper;
export const getStaticProps = async () => {
const appStore = await getAppRegistry();
export const getServerSideProps = async (context: GetServerSidePropsContext) => {
const { req, res } = context;
const ssr = await ssrInit(context);
const session = await getServerSession({ req, res });
let appStore;
if (session?.user?.id) {
appStore = await getAppRegistryWithCredentials(session.user.id);
} else {
appStore = await getAppRegistry();
}
const categories = appStore.reduce((c, app) => {
for (const category of app.categories) {
c[category] = c[category] ? c[category] + 1 : 1;
@ -59,6 +75,7 @@ export const getStaticProps = async () => {
return {
props: {
categories: Object.entries(categories).map(([name, count]) => ({ name, count })),
trpcState: ssr.dehydrate(),
},
};
};

View File

@ -26,6 +26,8 @@
"rejection_confirmation": "Recusar a reserva",
"manage_this_event": "Gerenciar este evento",
"invite_team_member": "Convidar membro da equipe",
"invite_team_individual_segment": "Convite individual",
"invite_team_bulk_segment": "Importação em massa",
"invite_team_notifcation_badge": "Con.",
"your_event_has_been_scheduled": "A sua reunião foi agendada",
"your_event_has_been_scheduled_recurring": "Seu evento recorrente foi agendado",
@ -222,6 +224,7 @@
"go_back_login": "Voltar à página de login",
"error_during_login": "Ocorreu um erro ao iniciar sessão. Volte a tela de login e tente novamente.",
"request_password_reset": "Solicitar redefinição da senha",
"send_invite": "Enviar convite",
"forgot_password": "Esqueceu a senha?",
"forgot": "Esqueceu?",
"done": "Concluído",
@ -237,6 +240,8 @@
"set_availability": "Definir disponibilidade",
"continue_without_calendar": "Continuar sem calendário",
"connect_your_calendar": "Conectar seu calendário",
"connect_your_video_app": "Conecte seus aplicativos de vídeo",
"connect_your_video_app_instructions": "Conecte seus aplicativos de vídeo para usá-los nos seus tipos de evento.",
"connect_your_calendar_instructions": "Conecte o seu calendário para verificar automaticamente as horas ocupadas e os novos eventos à medida em que são agendados.",
"set_up_later": "Configurar mais tarde",
"current_time": "Hora atual",
@ -367,6 +372,7 @@
"create_webhook": "Criar Webhook",
"booking_cancelled": "Reserva Cancelada",
"booking_rescheduled": "Reserva Reagendada",
"recording_ready": "Link para download de gravação pronto",
"booking_created": "Reserva Criada",
"meeting_ended": "Reunião encerrada",
"form_submitted": "Formulário enviado",
@ -465,6 +471,7 @@
"friday": "Sexta-feira",
"saturday": "Sábado",
"sunday": "Domingo",
"all_booked_today": "Tudo reservado.",
"slots_load_fail": "Não foi possível carregar os horários disponíveis.",
"additional_guests": "+ Participantes Adicionais",
"your_name": "Seu nome",
@ -752,6 +759,10 @@
"new_event_type_to_book_description": "Crie um novo tipo de evento para as pessoas agendarem horários.",
"length": "Comprimento",
"minimum_booking_notice": "Aviso mínimo",
"offset_toggle": "Deslocar horas de início",
"offset_toggle_description": "Desloca na quantidade de minutos especificada as alocações de tempo mostradas aos reservantes",
"offset_start": "Deslocar em",
"offset_start_description": "ex.: serão mostrados espaços de tempo aos seus reservantes à(s) {{ adjustedTime }} em vez de {{ originalTime }}",
"slot_interval": "Intervalos de tempo",
"slot_interval_default": "Usar a duração do evento (padrão)",
"delete_event_type": "Excluir tipo de evento?",
@ -904,6 +915,7 @@
"duplicate": "Duplicar",
"offer_seats": "Oferecer assentos",
"offer_seats_description": "Oferecer assentos para reserva. Isto desativa automaticamente reservas de convidados e confirmações.",
"seats_available_one": "Assento disponível",
"seats_available_other": "Assentos disponíveis",
"number_of_seats": "Número de assentos por reserva",
"enter_number_of_seats": "Insira a quantidade de assentos",
@ -1038,6 +1050,7 @@
"event_cancelled_trigger": "quando o evento é cancelado",
"new_event_trigger": "quando um novo evento for reservado",
"email_host_action": "enviar e-mail para o host",
"email_attendee_action": "enviar e-mail aos participantes",
"sms_attendee_action": "enviar SMS para participante",
"sms_number_action": "enviar SMS para um número específico",
"workflows": "Fluxos de trabalho",
@ -1190,6 +1203,7 @@
"create_workflow": "Criar fluxo de trabalho",
"do_this": "Faça isso",
"turn_off": "Desligar",
"turn_on": "Ligar",
"settings_updated_successfully": "Configurações atualizadas com sucesso",
"error_updating_settings": "Erro ao atualizar configurações",
"personal_cal_url": "Meu URL pessoal da {{appName}}",
@ -1246,6 +1260,7 @@
"calendars_description": "Configure como seus tipos de evento interagem com seus calendários",
"appearance_description": "Gerencie as configurações da aparência da sua reserva",
"conferencing_description": "Gerencie seus aplicativos de videoconferência para suas reuniões",
"add_conferencing_app": "Adicionar aplicativo de conferência",
"password_description": "Gerencie as configurações para as senhas da sua conta",
"2fa_description": "Gerencie as configurações para as senhas da sua conta",
"we_just_need_basic_info": "Precisamos apenas de algumas informações básicas para configurar seu perfil.",
@ -1620,6 +1635,7 @@
"email_user_cta": "Ver convite",
"email_no_user_invite_heading": "Você precisa ter recebido um convite para ingressar em uma equipe de {{appName}}",
"email_no_user_invite_subheading": "Você recebeu um convite de {{invitedBy}} para ingressar em sua equipe em {{appName}}. {{appName}} é um agendador que concilia eventos e permite que sua equipe agende reuniões sem precisar trocar e-mails.",
"email_user_invite_subheading": "Você recebeu um convite de {{invitedBy}} para ingressar na equipe \"{{teamName}}\" em {{appName}}. {{appName}} é um agendador que concilia eventos e permite que sua equipe agende reuniões sem precisar trocar e-mails.",
"email_no_user_invite_steps_intro": "Orientaremos ao longo de alguns passos rápidos para que você comece logo a agendar eventos com sua equipe sem preocupação.",
"email_no_user_step_one": "Escolha seu nome de usuário",
"email_no_user_step_two": "Conecte com sua conta de calendário",
@ -1695,6 +1711,15 @@
"spot_popular_event_types_description": "Veja quais tipos de eventos estão recebendo a maioria dos cliques e reservas",
"no_responses_yet": "Sem respostas por enquanto",
"this_will_be_the_placeholder": "Este será o espaço reservado",
"error_booking_event": "Ocorreu um erro ao reservar o evento, atualize a página e tente novamente",
"timeslot_missing_title": "Nenhuma alocação de tempo selecionada",
"timeslot_missing_description": "Selecione uma alocação de tempo para reservar o evento.",
"timeslot_missing_cta": "Selecione a alocação de tempo",
"switch_monthly": "Trocar para visualização mensal",
"switch_weekly": "Trocar para visualização semanal",
"switch_multiday": "Trocar para visualização diária",
"num_locations": "{{num}} opções de local",
"select_on_next_step": "Selecione na próxima etapa",
"this_meeting_has_not_started_yet": "Esta reunião ainda não começou",
"this_app_requires_connected_account": "{{appName}} requer uma conta de {{dependencyName}} conectada",
"connect_app": "Conectar {{dependencyName}}",
@ -1724,6 +1749,7 @@
"locked_apps_description": "Os membros poderão ver os aplicativos ativos, mas não poderão editar as configurações",
"locked_webhooks_description": "Os membros poderão ver os webhooks ativos, mas não poderão editar as configurações",
"locked_workflows_description": "Os membros poderão ver os fluxos de trabalho ativos, mas não poderão editar as configurações",
"locked_by_admin": "Bloqueado pelo administrador da equipe",
"app_not_connected": "Você não conectou uma conta do {{appName}}.",
"connect_now": "Conectar agora",
"managed_event_dialog_confirm_button_one": "Substituir e notificar {{count}} membro",
@ -1781,5 +1807,29 @@
"seats_and_no_show_fee_error": "Neste momento, não é possível habilitar assentos e cobrar taxa de não comparecimento",
"complete_your_booking": "Conclua sua reserva",
"complete_your_booking_subject": "Conclua sua reserva: {{title}} à(s) {{date}}",
"email_invite_team": "{{email}} recebeu um convite"
"confirm_your_details": "Confirme suas informações",
"currency_string": "{{amount, currency}}",
"charge_card_dialog_body": "Você está prestes a cobrar {{amount, currency}} do participante. Tem certeza de que deseja continuar?",
"charge_attendee": "Cobrar {{amount, currency}} do participante",
"payment_app_commission": "Solicitar pagamento ({{paymentFeePercentage}}% + {{fee, currency}} de comissão a cada transação)",
"email_invite_team": "{{email}} recebeu um convite",
"email_invite_team_bulk": "{{userCount}} usuários foram convidados",
"error_collecting_card": "Erro ao coletar cartão",
"image_size_limit_exceed": "A imagem carregada não deve ultrapassar o limite de 5 MB",
"inline_embed": "Incorporar embutido",
"load_inline_content": "Carrega seu tipo de evento embutido diretamente no conteúdo restante do site.",
"floating_pop_up_button": "Botão pop-up flutuante",
"floating_button_trigger_modal": "Insere um botão flutuante no seu site para ativar um modal com seu tipo de evento.",
"pop_up_element_click": "Pop-up ao clicar em elemento",
"open_dialog_with_element_click": "Abra uma caixa de diálogo quando alguém clicar em um elemento.",
"need_help_embedding": "Precisa de ajuda? Confira nosso guia para incorporar o Cal no Wix, Squarespace ou WordPress. Consulte as perguntas frequentes ou explore opções de incorporação avançadas.",
"book_my_cal": "Reservar meu Cal",
"invite_as": "Convidar como",
"form_updated_successfully": "Formulário atualizado com êxito.",
"email_not_cal_member_cta": "Ingresse na sua equipe",
"disable_attendees_confirmation_emails": "Desative os e-mails de confirmação padrão para participantes",
"disable_attendees_confirmation_emails_description": "Pelo menos um fluxo de trabalho está ativo neste tipo de evento, que envia um e-mail aos participantes quando o evento for reservado.",
"disable_host_confirmation_emails": "Desative e-mails de confirmação padrão para hosts",
"disable_host_confirmation_emails_description": "Pelo menos um fluxo de trabalho está ativo neste tipo de evento, que envia um e-mail aos hosts quando o evento for reservado.",
"add_an_override": "Adicionar uma substituição"
}

View File

@ -471,7 +471,7 @@
"friday": "П’ятниця",
"saturday": "Субота",
"sunday": "Неділя",
"all_booked_today": "Усі заброньовані.",
"all_booked_today": "Усе заброньовано.",
"slots_load_fail": "Не вдалося завантажити доступні часові вікна.",
"additional_guests": "Додати гостей",
"your_name": "Ваше ім’я",
@ -762,7 +762,7 @@
"offset_toggle": "Змістити час початку",
"offset_toggle_description": "Проміжки часу для зміщення (у хвилинах), показані користувачам, які здійснюють бронювання",
"offset_start": "Зміщення на",
"offset_start_description": "наприклад, вони бачитимуть часові проміжки о {{ adjustedTime }} замість {{ originalTime }}",
"offset_start_description": "наприклад, користувачі, які здійснюють бронювання, бачитимуть часові проміжки о {{ adjustedTime }} замість {{ originalTime }}",
"slot_interval": "Інтервали між бронюваннями",
"slot_interval_default": "Використовувати тривалість заходу (за замовчуванням)",
"delete_event_type": "Видалити тип заходу?",
@ -915,7 +915,7 @@
"duplicate": "Дублювати",
"offer_seats": "Запропонувати місця",
"offer_seats_description": "Пропонуйте місця під час бронювання (при цьому вимикається гостьовий режим і бронювання з підтвердженням).",
"seats_available_one": "Доступне місце",
"seats_available_one": "Є доступне місце",
"seats_available_other": "Доступні місця",
"number_of_seats": "Кількість місць на одне бронювання",
"enter_number_of_seats": "Введіть кількість місць",
@ -1050,7 +1050,7 @@
"event_cancelled_trigger": "у момент скасування заходу",
"new_event_trigger": "у момент бронювання нового заходу",
"email_host_action": "надсилати електронний лист ведучому",
"email_attendee_action": "надсилати ел. лист учасникам",
"email_attendee_action": "надсилати ел. листи учасникам",
"sms_attendee_action": "надсилати SMS учаснику",
"sms_number_action": "надсилати SMS на певний номер",
"workflows": "Робочі процеси",
@ -1260,7 +1260,7 @@
"calendars_description": "Налаштуйте, як типи заходів мають взаємодіяти з вашими календарями",
"appearance_description": "Налаштуйте варіанти оформлення свого бронювання",
"conferencing_description": "Керуйте додатками для відеоконференцій і нарад",
"add_conferencing_app": "Додати застосунок Conferencing",
"add_conferencing_app": "Додати застосунок для проведення конференцій",
"password_description": "Налаштуйте параметри паролів облікових записів",
"2fa_description": "Налаштуйте параметри паролів облікових записів",
"we_just_need_basic_info": "Щоб налаштувати ваш профіль, нам потрібні базові дані.",
@ -1810,8 +1810,8 @@
"confirm_your_details": "Підтвердьте ваші дані",
"currency_string": "{{amount, currency}}",
"charge_card_dialog_body": "Ви збираєтеся списати кошти з рахунку учасника: {{amount, currency}}. Бажаєте продовжити?",
"charge_attendee": "Списати з учасника {{amount, currency}}",
"payment_app_commission": "Потрібна оплата ({{paymentFeePercentage}}% + {{fee, currency}} комісія за транзакцію)",
"charge_attendee": "Списати з рахунку учасника {{amount, currency}}",
"payment_app_commission": "Потрібна оплата ({{paymentFeePercentage}}% + комісія за транзакцію в розмірі {{fee, currency}})",
"email_invite_team": "{{email}} запрошено",
"email_invite_team_bulk": "Запрошено стільки користувачів: {{userCount}}",
"error_collecting_card": "Не вдалося забрати картку",
@ -1821,20 +1821,20 @@
"floating_pop_up_button": "Спливаюча кнопка",
"floating_button_trigger_modal": "Додає спливаючу кнопку на ваш сайт, яка викликає модальне вікно з вашим типом заходу.",
"pop_up_element_click": "Спливаюче вікно після натискання на елемент",
"open_dialog_with_element_click": "Відкриває ваш діалог з Cal, коли хтось натискає елемент.",
"open_dialog_with_element_click": "Відкриває ваш діалог із Cal, коли хтось натискає елемент.",
"need_help_embedding": "Потрібна допомога? Перегляньте наші посібники з вбудовування Cal на Wix, Squarespace або WordPress, ознайомтеся з поширеними запитаннями або розширеними параметрами вбудовування.",
"book_my_cal": "Забронювати мій Cal",
"invite_as": "Запросити як",
"form_updated_successfully": "Форму оновлено.",
"email_not_cal_member_cta": "Приєднатися до команди",
"disable_attendees_confirmation_emails": "Вимкнути листи підтвердження за замовчуванням для учасників",
"disable_attendees_confirmation_emails_description": "Принаймні один робочий процес активний у цьому типі заходів, який надсилає лист учасникам у разі бронювання події.",
"disable_attendees_confirmation_emails_description": "Принаймні один робочий процес активний у цьому типі заходів, який надсилає електронний лист учасникам у разі бронювання заходу.",
"disable_host_confirmation_emails": "Вимкнути листи підтвердження за замовчуванням для ведучого",
"disable_host_confirmation_emails_description": "Принаймні один робочий процес активний у цьому типі заходів, який надсилає лист ведучому в разі бронювання події.",
"disable_host_confirmation_emails_description": "Принаймні один робочий процес активний у цьому типі заходів, який надсилає електронний лист ведучому в разі бронювання заходу.",
"add_an_override": "Додати перевизначення",
"import_from_google_workspace": "Імпортувати користувачів із Google Workspace",
"connect_google_workspace": "Підключити Google Workspace",
"google_workspace_admin_tooltip": "Ви повинні бути адміністратором Workspace, щоб використовувати цю функцію",
"first_event_type_webhook_description": "Створіть свій перший вебгук для цього типу заходу",
"first_event_type_webhook_description": "Створіть свій перший вебгук для цього типу заходів",
"create_for": "Створити для"
}

View File

@ -372,6 +372,7 @@
"create_webhook": "Tạo Webhook",
"booking_cancelled": "Lịch hẹn đã bị hủy",
"booking_rescheduled": "Lịch hẹn đã được đổi",
"recording_ready": "Đã sẵn sàng liên kết tải xuống bản ghi",
"booking_created": "Lịch hẹn đã được tạo",
"meeting_ended": "Cuộc họp đã kết thúc",
"form_submitted": "Biểu mẫu đã được gửi",
@ -470,6 +471,7 @@
"friday": "Thứ Sáu",
"saturday": "Thứ Bảy",
"sunday": "Chủ Nhật",
"all_booked_today": "Tất cả đã được đặt chỗ.",
"slots_load_fail": "Không thể tải thời gian có thể đặt lịch.",
"additional_guests": "Thêm khách",
"your_name": "Tên của bạn",
@ -757,6 +759,10 @@
"new_event_type_to_book_description": "Tạo một loại sự kiện mới để mọi người đặt thời gian.",
"length": "Thời gian",
"minimum_booking_notice": "Thời gian đệm tối thiểu",
"offset_toggle": "Thời gian bắt đầu được bù lại",
"offset_toggle_description": "Suất thời gian bù lại được hiển thị cho những người đặt lịch theo số phút cụ thể",
"offset_start": "Bù lại",
"offset_start_description": "ví dụ cái này sẽ hiển thị những suất thời gian cho người đặt lịch vào lúc {{ adjustedTime }} thay vì {{ originalTime }}",
"slot_interval": "Khoảng thời gian giữa các lịch hẹn",
"slot_interval_default": "Sử dụng độ dài sự kiện (mặc định)",
"delete_event_type": "Xóa loại sự kiện?",
@ -909,6 +915,7 @@
"duplicate": "Sao chép",
"offer_seats": "Cung cấp ghế ngồi",
"offer_seats_description": "Cung cấp ghế để đặt lịch. Việc này sẽ tự động vô hiệu hoá đặt lịch của khách & đặt lịch tham gia.",
"seats_available_one": "Số ghế trống",
"seats_available_other": "Số ghế trống",
"number_of_seats": "Số ghế cho mỗi lần đặt",
"enter_number_of_seats": "Điền vào số lượng ghế",
@ -1043,6 +1050,7 @@
"event_cancelled_trigger": "khi sự kiện bị huỷ",
"new_event_trigger": "khi sự kiện mới được đặt lịch hẹn",
"email_host_action": "gửi email đến chủ sự kiện",
"email_attendee_action": "gửi email đến người tham gia",
"sms_attendee_action": "gửi SMS đến người tham gia",
"sms_number_action": "gửi SMS đến một số cụ thể",
"workflows": "Tiến độ công việc",
@ -1195,6 +1203,7 @@
"create_workflow": "Tạo một tiến độ công việc",
"do_this": "Làm cái này",
"turn_off": "Tắt",
"turn_on": "Bật",
"settings_updated_successfully": "Cập nhật thành công phần cài đặt",
"error_updating_settings": "Lỗi khi cập nhật cài đặt",
"personal_cal_url": "URL {{appName}} cá nhân của tôi",
@ -1251,6 +1260,7 @@
"calendars_description": "Cấu hình cách các loại sự kiện của bạn tương tác với lịch của bạn",
"appearance_description": "Quản lí cài đặt cho giao diện lịch hẹn của bạn",
"conferencing_description": "Quản lí ứng dụng hội nghị video dùng cho các cuộc họp",
"add_conferencing_app": "Thêm ứng dụng hội nghị",
"password_description": "Quản lí cài đặt cho mật khẩu tài khoản",
"2fa_description": "Quản lí cài đặt cho mật khẩu tài khoản",
"we_just_need_basic_info": "Chúng tôi chỉ cần một số thông tin cơ bản để thiết lập hồ sơ của bạn.",
@ -1625,6 +1635,7 @@
"email_user_cta": "Xem lời mời",
"email_no_user_invite_heading": "Bạn đã được mời gia nhập nhóm trên {{appName}}",
"email_no_user_invite_subheading": "{{invitedBy}} đã mời bạn gia nhập nhóm của họ trên {{appName}}. {{appName}} là công cụ lên lịch sắp xếp sự kiện cho phép bạn và nhóm bạn lên lịch các cuộc gặp mà không cần trao đổi email nhiều.",
"email_user_invite_subheading": "{{invitedBy}} đã mời bạn gia nhập nhóm '{{teamName}}' của họ trên {{appName}}. {{appName}} là công cụ lên lịch sắp xếp sự kiện cho phép bạn và nhóm bạn lên lịch các cuộc gặp mà không cần trao đổi email nhiều.",
"email_no_user_invite_steps_intro": "Chúng tôi sẽ hướng dẫn cho bạn qua vài bước nhỏ và bạn sẽ tận hưởng được ngay cảm giác thoải mái không căng thẳng trong việc lên lịch cùng nhóm của mình.",
"email_no_user_step_one": "Chọn tên người dùng của bạn",
"email_no_user_step_two": "Kết nối tài khoản lịch của bạn",
@ -1700,6 +1711,15 @@
"spot_popular_event_types_description": "Xem loại sự kiện nào của bạn nhận được nhiều lần nhấp chuột và nhiều lịch hẹn nhất",
"no_responses_yet": "Chưa có hồi đáp",
"this_will_be_the_placeholder": "Đây sẽ là mã giữ chỗ",
"error_booking_event": "Có lỗi khi đặt lịch sự kiện, hãy làm mới trang rồi thử lại",
"timeslot_missing_title": "Chưa chọn suất thời gian nào",
"timeslot_missing_description": "Vui lòng chọn một suất thời gian để đặt lịch sự kiện.",
"timeslot_missing_cta": "Chọn suất thời gian",
"switch_monthly": "Chuyển sang giao diện tháng",
"switch_weekly": "Chuyển sang giao diện tuần",
"switch_multiday": "Chuyển sang giao diện ngày",
"num_locations": "{{num}} lựa chọn vị trí",
"select_on_next_step": "Chọn ở bước kế tiếp",
"this_meeting_has_not_started_yet": "Cuộc hẹn này chưa được bắt đầu",
"this_app_requires_connected_account": "{{appName}} cần có một tài khoản {{dependencyName}} đã kết nối",
"connect_app": "Kết nối {{dependencyName}}",
@ -1729,6 +1749,7 @@
"locked_apps_description": "Các thành viên có thể thấy được những ứng dụng đang hoạt động nhưng không thể sửa bất kỳ thiết lập nào của ứng dụng",
"locked_webhooks_description": "Các thành viên có thể thấy được những webhook đang hoạt động nhưng không thể sửa bất kỳ thiết lập nào của webhook",
"locked_workflows_description": "Các thành viên có thể thấy được những tiến độ công việc đang hoạt động nhưng không thể sửa bất kỳ thiết lập nào của tiến độ công việc",
"locked_by_admin": "Bị khoá bởi quản trị viên của nhóm",
"app_not_connected": "Bạn không có tài khoản {{appName}} đã được kết nối.",
"connect_now": "Kết nối ngay",
"managed_event_dialog_confirm_button_one": "Thay thế & thông báo cho {{count}} thành viên",
@ -1786,5 +1807,34 @@
"seats_and_no_show_fee_error": "Hiện không thể kích hoạt chỗ ngồi và tính khoản phí vắng mặt",
"complete_your_booking": "Hoàn thành lịch hẹn của bạn",
"complete_your_booking_subject": "Hoàn thành lịch hẹn của bạn: {{title}} vào {{date}}",
"email_invite_team": "{{email}} đã được mời"
"confirm_your_details": "Xác nhận các chi tiết của bạn",
"currency_string": "{{amount, currency}}",
"charge_card_dialog_body": "Bạn sắp sửa thu phí người tham gia một khoản {{amount, currency}}. Bạn có chắc chắn muốn tiếp tục?",
"charge_attendee": "Thu phí người tham gia một khoản {{amount, currency}}",
"payment_app_commission": "Yêu cầu thanh toán ({{paymentFeePercentage}}% + {{fee, currency}} hoa hồng cho mỗi giao dịch)",
"email_invite_team": "{{email}} đã được mời",
"email_invite_team_bulk": "{{userCount}} người dùng đã được mời",
"error_collecting_card": "Lỗi khi thu thập thẻ",
"image_size_limit_exceed": "Ảnh tải lên không được vượt quá giới hạn kích cỡ 5mb",
"inline_embed": "Nội tuyến đã nhúng",
"load_inline_content": "Tải trực tiếp loại sự kiện của bạn ở dạng nội tuyến với nội dung khác trên website của bạn.",
"floating_pop_up_button": "Nút popup dạng nổi",
"floating_button_trigger_modal": "Đặt một nút dạng nổi trên website giúp kích hoạt chế độ với loại sự kiện này.",
"pop_up_element_click": "Popup thông qua nhấp chuột trong phần tử",
"open_dialog_with_element_click": "Mở hộp thoại Cal khi ai đó nhấp vào một phần tử.",
"need_help_embedding": "Cần trợ giúp? Xem hướng dẫn của chúng tôi dành cho việc nhúng Cal trên Wix, Squarespace hoặc WordPress, xem qua những câu hỏi thường gặp của chúng tôi, hoặc khám phá thêm các tuỳ chọn nhúng nâng cao.",
"book_my_cal": "Đặt lịch Cal của tôi",
"invite_as": "Mời ở vai trò",
"form_updated_successfully": "Đã cập nhật biểu mẫu thành công.",
"email_not_cal_member_cta": "Gia nhập nhóm của bạn",
"disable_attendees_confirmation_emails": "Vô hiệu hoá những email xác nhận mặc định dành cho người tham dự",
"disable_attendees_confirmation_emails_description": "Ít nhất một tiến độ công việc đang hoạt động ở loại sự kiện này, và tiến độ này sẽ gửi email đến những người tham dự khi sự kiện này được đặt lịch.",
"disable_host_confirmation_emails": "Vô hiệu hoá những email xác nhận mặc định dành cho chủ toạ",
"disable_host_confirmation_emails_description": "Ít nhất một tiến độ công việc đang hoạt động ở loại sự kiện này, và tiến độ này sẽ gửi email đến chủ toạ khi sự kiện này được đặt lịch.",
"add_an_override": "Thêm ngày ghi đè",
"import_from_google_workspace": "Nhập người dùng từ Google Workspace",
"connect_google_workspace": "Kết nối Google Workspace",
"google_workspace_admin_tooltip": "Bạn phải là một Quản trị viên của Workspace mới dùng được tính năng này",
"first_event_type_webhook_description": "Tạo webhook đầu tiên của bạn cho loại sự kiện này",
"create_for": "Tạo cho"
}

View File

@ -1817,9 +1817,12 @@
"error_collecting_card": "收集卡时出错",
"image_size_limit_exceed": "上传的图像不应超过 5mb 大小限制",
"inline_embed": "内联嵌入",
"load_inline_content": "将您的活动类型直接与您的其他网站内容内联加载。",
"floating_pop_up_button": "浮动弹出式按钮",
"floating_button_trigger_modal": "在您的网站上放置一个浮动按钮,用于根据您的活动类型触发模式。",
"pop_up_element_click": "通过点击元素弹出",
"open_dialog_with_element_click": "当有人点击元素时,打开您的 Cal 对话框。",
"need_help_embedding": "需要帮助?看一下在 Wix、Squarespace 或 WordPress 上嵌入 Cal 的指南,查看常见问题,或浏览高级嵌入选项。",
"book_my_cal": "预约我的 Cal",
"invite_as": "邀请为",
"form_updated_successfully": "表单更新成功。",

View File

@ -103,7 +103,7 @@ const BookerComponent = ({
since that's not a valid option, so it would set the layout to null.
*/}
{!isMobile && (
<div className="[&>div]:bg-muted fixed top-2 right-3 z-10">
<div className="[&>div]:bg-default fixed top-2 right-3 z-10">
<ToggleGroup
onValueChange={onLayoutToggle}
defaultValue={layout}
@ -133,7 +133,7 @@ const BookerComponent = ({
className={classNames(
// Sets booker size css variables for the size of all the columns.
...getBookerSizeClassNames(layout, bookerState),
"bg-muted grid max-w-full auto-rows-fr items-start overflow-clip dark:[color-scheme:dark] sm:transition-[width] sm:duration-300 sm:motion-reduce:transition-none md:flex-row",
"bg-default grid max-w-full auto-rows-fr items-start overflow-clip dark:[color-scheme:dark] sm:transition-[width] sm:duration-300 sm:motion-reduce:transition-none md:flex-row",
layout === "small_calendar" && "border-subtle rounded-md border"
)}>
<AnimatePresence>

View File

@ -35,7 +35,12 @@ export const EventMeta = () => {
{!isLoading && !!event && (
<m.div {...fadeInUp} layout transition={{ ...fadeInUp.transition, delay: 0.3 }}>
<EventMembers schedulingType={event.schedulingType} users={event.users} profile={event.profile} />
<EventTitle className="mt-2 mb-8">{event?.title}</EventTitle>
<EventTitle className="my-2">{event?.title}</EventTitle>
{event.description && (
<EventMetaBlock contentClassName="mb-8 break-words max-w-full max-h-[180px] scroll-bar pr-4">
<div dangerouslySetInnerHTML={{ __html: event.description }} />
</EventMetaBlock>
)}
<div className="space-y-4 font-medium">
{rescheduleBooking && (
<EventMetaBlock icon={Calendar}>

View File

@ -11,7 +11,7 @@ export const LargeCalendar = () => {
);
return (
<div className="bg-muted flex h-full w-full flex-col items-center justify-center">
<div className="bg-default flex h-full w-full flex-col items-center justify-center">
Something big is coming...
<br />
<button

View File

@ -2,7 +2,7 @@ import { useLocale } from "@calcom/lib/hooks/useLocale";
const UnAvailableMessage = ({ children, title }: { children: React.ReactNode; title: string }) => (
<div className="mx-auto w-full max-w-2xl">
<div className="bg-muted border-subtle overflow-hidden rounded-lg border p-10">
<div className="border-subtle bg-default overflow-hidden rounded-lg border p-10">
<h2 className="font-cal mb-4 text-3xl">{title}</h2>
{children}
</div>

View File

@ -110,9 +110,9 @@ export const AvailableTimes = ({
};
export const AvailableTimesSkeleton = () => (
<div className="mt-8 flex h-full w-[20%] flex-col only:w-full">
{/* Random number of elements between 1 and 10. */}
{Array.from({ length: Math.floor(Math.random() * 10) + 1 }).map((_, i) => (
<div className="mt-8 flex w-[20%] flex-col only:w-full">
{/* Random number of elements between 1 and 6. */}
{Array.from({ length: Math.floor(Math.random() * 6) + 1 }).map((_, i) => (
<SkeletonText className="mb-4 h-6 w-full" key={i} />
))}
</div>

View File

@ -44,7 +44,6 @@ interface EventMetaProps {
* Default order in which the event details will be rendered.
*/
const defaultEventDetailsBlocks = [
EventDetailBlocks.DESCRIPTION,
EventDetailBlocks.REQUIRES_CONFIRMATION,
EventDetailBlocks.DURATION,
EventDetailBlocks.OCCURENCES,
@ -112,16 +111,6 @@ export const EventDetails = ({ event, blocks = defaultEventDetailsBlocks }: Even
}
switch (block) {
case EventDetailBlocks.DESCRIPTION:
if (!event.description) return null;
return (
<EventMetaBlock
key={block}
contentClassName="break-words max-w-full max-h-[180px] scroll-bar pr-4">
<div dangerouslySetInnerHTML={{ __html: event.description }} />
</EventMetaBlock>
);
case EventDetailBlocks.DURATION:
return (
<EventMetaBlock key={block} icon={Clock}>

View File

@ -1600,7 +1600,7 @@ async function handler(
async function createBooking() {
if (originalRescheduledBooking) {
evt.title = originalRescheduledBooking?.title || evt.title;
evt.description = originalRescheduledBooking?.description || evt.additionalNotes;
evt.description = originalRescheduledBooking?.description || evt.description;
evt.location = originalRescheduledBooking?.location || evt.location;
}

View File

@ -8,7 +8,6 @@ export type PublicEvent = NonNullable<RouterOutputs["viewer"]["public"]["event"]
export type ValidationErrors<T extends object> = { key: FieldPath<T>; error: ErrorOption }[];
export enum EventDetailBlocks {
DESCRIPTION,
// Includes duration select when event has multiple durations.
DURATION,
LOCATION,

View File

@ -146,7 +146,7 @@ const Days = ({
<div key={`e-${idx}`} />
) : props.isLoading ? (
<button
className=" bg-muted text-muted opcaity-50 absolute top-0 left-0 right-0 bottom-0 mx-auto flex w-full items-center justify-center rounded-sm border-transparent text-center font-medium"
className="bg-muted text-muted absolute top-0 left-0 right-0 bottom-0 mx-auto flex w-full items-center justify-center rounded-sm border-transparent text-center font-medium opacity-50"
key={`e-${idx}`}
disabled>
<SkeletonText className="h-4 w-5" />

View File

@ -1,10 +1,14 @@
/*
Warnings:
- A unique constraint covering the columns `[slug,parentId]` on the table `Team` will be added. If there are existing duplicate values, this will fail.
- 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 "Team_slug_key";
-- DropIndex
DROP INDEX "users_email_idx";
@ -17,6 +21,9 @@ ALTER TABLE "Team" ADD COLUMN "parentId" INTEGER;
-- AlterTable
ALTER TABLE "users" ADD COLUMN "organizationId" INTEGER;
-- CreateIndex
CREATE UNIQUE INDEX "Team_slug_parentId_key" ON "Team"("slug", "parentId");
-- CreateIndex
CREATE UNIQUE INDEX "users_email_username_key" ON "users"("email", "username");
@ -29,12 +36,7 @@ ALTER TABLE "users" ADD CONSTRAINT "users_organizationId_fkey" FOREIGN KEY ("org
-- 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;
-- FeatureFlags
INSERT INTO "Feature" (slug, enabled, description, "type")
VALUES ('organizations', true, 'Manage organizations with multiple teams', 'OPERATIONAL')
ON CONFLICT (slug) DO NOTHING;

View File

@ -246,7 +246,7 @@ model Team {
/// @zod.min(1)
name String
/// @zod.min(1)
slug String? @unique
slug String?
logo String?
appLogo String?
appIconLogo String?
@ -268,6 +268,8 @@ model Team {
children Team[] @relation("organization")
orgUsers User[] @relation("scope")
webhooks Webhook[]
@@unique([slug, parentId])
}
enum MembershipRole {

View File

@ -52,7 +52,7 @@ const Skeleton = <T extends keyof JSX.IntrinsicElements | React.FC>({
<Component
className={classNames(
loading
? classNames("font-size-0 bg-subtle animate-pulse rounded-md text-transparent", loadingClassName)
? classNames("font-size-0 bg-emphasis animate-pulse rounded-md text-transparent", loadingClassName)
: "",
className
)}
@ -69,7 +69,7 @@ const SkeletonText: React.FC<SkeletonBaseProps & { invisible?: boolean }> = ({
return (
<span
className={classNames(
`font-size-0 bg-subtle inline-block animate-pulse rounded-md empty:before:inline-block empty:before:content-['']`,
`font-size-0 bg-emphasis inline-block animate-pulse rounded-md empty:before:inline-block empty:before:content-['']`,
className,
invisible ? "invisible" : ""
)}