Compare commits
No files matched your search
@@ -215,3 +215,8 @@ PROJECT_ID_VERCEL=
|
|||||||
TEAM_ID_VERCEL=
|
TEAM_ID_VERCEL=
|
||||||
# Get it from: https://vercel.com/account/tokens
|
# Get it from: https://vercel.com/account/tokens
|
||||||
AUTH_BEARER_TOKEN_VERCEL=
|
AUTH_BEARER_TOKEN_VERCEL=
|
||||||
|
|
||||||
|
# - APPLE CALENDAR
|
||||||
|
# Used for E2E tests on Apple Calendar
|
||||||
|
E2E_TEST_APPLE_CALENDAR_EMAIL=""
|
||||||
|
E2E_TEST_APPLE_CALENDAR_PASSWORD=""
|
||||||
@@ -5,16 +5,15 @@ on:
|
|||||||
types:
|
types:
|
||||||
- opened
|
- opened
|
||||||
|
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
label_on_pr:
|
label_on_pr:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
permissions:
|
permissions:
|
||||||
contents: none
|
contents: none
|
||||||
issues: read
|
issues: read
|
||||||
pull-requests: write
|
pull-requests: write
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Apply labels from linked issue to PR
|
- name: Apply labels from linked issue to PR
|
||||||
uses: actions/github-script@v5
|
uses: actions/github-script@v5
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
# .github/workflows/chromatic.yml
|
# .github/workflows/chromatic.yml
|
||||||
|
|
||||||
# Workflow name
|
# Workflow name
|
||||||
name: 'Chromatic'
|
name: "Chromatic"
|
||||||
|
|
||||||
# Event for the workflow
|
# Event for the workflow
|
||||||
on:
|
on:
|
||||||
|
|||||||
@@ -41,6 +41,8 @@ jobs:
|
|||||||
CALENDSO_ENCRYPTION_KEY: ${{ secrets.CI_CALENDSO_ENCRYPTION_KEY }}
|
CALENDSO_ENCRYPTION_KEY: ${{ secrets.CI_CALENDSO_ENCRYPTION_KEY }}
|
||||||
DATABASE_URL: ${{ secrets.CI_DATABASE_URL }}
|
DATABASE_URL: ${{ secrets.CI_DATABASE_URL }}
|
||||||
DEPLOYSENTINEL_API_KEY: ${{ secrets.DEPLOYSENTINEL_API_KEY }}
|
DEPLOYSENTINEL_API_KEY: ${{ secrets.DEPLOYSENTINEL_API_KEY }}
|
||||||
|
E2E_TEST_APPLE_CALENDAR_EMAIL: ${{ secrets.E2E_TEST_APPLE_CALENDAR_EMAIL }}
|
||||||
|
E2E_TEST_APPLE_CALENDAR_PASSWORD: ${{ secrets.E2E_TEST_APPLE_CALENDAR_PASSWORD }}
|
||||||
GOOGLE_API_CREDENTIALS: ${{ secrets.CI_GOOGLE_API_CREDENTIALS }}
|
GOOGLE_API_CREDENTIALS: ${{ secrets.CI_GOOGLE_API_CREDENTIALS }}
|
||||||
GOOGLE_LOGIN_ENABLED: ${{ vars.CI_GOOGLE_LOGIN_ENABLED }}
|
GOOGLE_LOGIN_ENABLED: ${{ vars.CI_GOOGLE_LOGIN_ENABLED }}
|
||||||
NEXTAUTH_SECRET: ${{ secrets.CI_NEXTAUTH_SECRET }}
|
NEXTAUTH_SECRET: ${{ secrets.CI_NEXTAUTH_SECRET }}
|
||||||
|
|||||||
@@ -38,6 +38,8 @@ jobs:
|
|||||||
CALENDSO_ENCRYPTION_KEY: ${{ secrets.CI_CALENDSO_ENCRYPTION_KEY }}
|
CALENDSO_ENCRYPTION_KEY: ${{ secrets.CI_CALENDSO_ENCRYPTION_KEY }}
|
||||||
DATABASE_URL: ${{ secrets.CI_DATABASE_URL }}
|
DATABASE_URL: ${{ secrets.CI_DATABASE_URL }}
|
||||||
DEPLOYSENTINEL_API_KEY: ${{ secrets.DEPLOYSENTINEL_API_KEY }}
|
DEPLOYSENTINEL_API_KEY: ${{ secrets.DEPLOYSENTINEL_API_KEY }}
|
||||||
|
E2E_TEST_APPLE_CALENDAR_EMAIL: ${{ secrets.E2E_TEST_APPLE_CALENDAR_EMAIL }}
|
||||||
|
E2E_TEST_APPLE_CALENDAR_PASSWORD: ${{ secrets.E2E_TEST_APPLE_CALENDAR_PASSWORD }}
|
||||||
GOOGLE_API_CREDENTIALS: ${{ secrets.CI_GOOGLE_API_CREDENTIALS }}
|
GOOGLE_API_CREDENTIALS: ${{ secrets.CI_GOOGLE_API_CREDENTIALS }}
|
||||||
GOOGLE_LOGIN_ENABLED: ${{ vars.CI_GOOGLE_LOGIN_ENABLED }}
|
GOOGLE_LOGIN_ENABLED: ${{ vars.CI_GOOGLE_LOGIN_ENABLED }}
|
||||||
NEXTAUTH_SECRET: ${{ secrets.CI_NEXTAUTH_SECRET }}
|
NEXTAUTH_SECRET: ${{ secrets.CI_NEXTAUTH_SECRET }}
|
||||||
|
|||||||
@@ -41,6 +41,8 @@ jobs:
|
|||||||
CALENDSO_ENCRYPTION_KEY: ${{ secrets.CI_CALENDSO_ENCRYPTION_KEY }}
|
CALENDSO_ENCRYPTION_KEY: ${{ secrets.CI_CALENDSO_ENCRYPTION_KEY }}
|
||||||
DATABASE_URL: ${{ secrets.CI_DATABASE_URL }}
|
DATABASE_URL: ${{ secrets.CI_DATABASE_URL }}
|
||||||
DEPLOYSENTINEL_API_KEY: ${{ secrets.DEPLOYSENTINEL_API_KEY }}
|
DEPLOYSENTINEL_API_KEY: ${{ secrets.DEPLOYSENTINEL_API_KEY }}
|
||||||
|
E2E_TEST_APPLE_CALENDAR_EMAIL: ${{ secrets.E2E_TEST_APPLE_CALENDAR_EMAIL }}
|
||||||
|
E2E_TEST_APPLE_CALENDAR_PASSWORD: ${{ secrets.E2E_TEST_APPLE_CALENDAR_PASSWORD }}
|
||||||
GOOGLE_API_CREDENTIALS: ${{ secrets.CI_GOOGLE_API_CREDENTIALS }}
|
GOOGLE_API_CREDENTIALS: ${{ secrets.CI_GOOGLE_API_CREDENTIALS }}
|
||||||
GOOGLE_LOGIN_ENABLED: ${{ vars.CI_GOOGLE_LOGIN_ENABLED }}
|
GOOGLE_LOGIN_ENABLED: ${{ vars.CI_GOOGLE_LOGIN_ENABLED }}
|
||||||
NEXTAUTH_SECRET: ${{ secrets.CI_NEXTAUTH_SECRET }}
|
NEXTAUTH_SECRET: ${{ secrets.CI_NEXTAUTH_SECRET }}
|
||||||
|
|||||||
@@ -40,11 +40,13 @@ jobs:
|
|||||||
CALENDSO_ENCRYPTION_KEY: ${{ secrets.CI_CALENDSO_ENCRYPTION_KEY }}
|
CALENDSO_ENCRYPTION_KEY: ${{ secrets.CI_CALENDSO_ENCRYPTION_KEY }}
|
||||||
DATABASE_URL: ${{ secrets.CI_DATABASE_URL }}
|
DATABASE_URL: ${{ secrets.CI_DATABASE_URL }}
|
||||||
DEPLOYSENTINEL_API_KEY: ${{ secrets.DEPLOYSENTINEL_API_KEY }}
|
DEPLOYSENTINEL_API_KEY: ${{ secrets.DEPLOYSENTINEL_API_KEY }}
|
||||||
|
E2E_TEST_APPLE_CALENDAR_EMAIL: ${{ secrets.E2E_TEST_APPLE_CALENDAR_EMAIL }}
|
||||||
|
E2E_TEST_APPLE_CALENDAR_PASSWORD: ${{ secrets.E2E_TEST_APPLE_CALENDAR_PASSWORD }}
|
||||||
GOOGLE_API_CREDENTIALS: ${{ secrets.CI_GOOGLE_API_CREDENTIALS }}
|
GOOGLE_API_CREDENTIALS: ${{ secrets.CI_GOOGLE_API_CREDENTIALS }}
|
||||||
EMAIL_SERVER_HOST: ${{ secrets.CI_EMAIL_SERVER_HOST }}
|
EMAIL_SERVER_HOST: ${{ secrets.CI_EMAIL_SERVER_HOST }}
|
||||||
EMAIL_SERVER_PORT: ${{ secrets.CI_EMAIL_SERVER_PORT }}
|
EMAIL_SERVER_PORT: ${{ secrets.CI_EMAIL_SERVER_PORT }}
|
||||||
EMAIL_SERVER_USER: ${{ secrets.CI_EMAIL_SERVER_USER }}
|
EMAIL_SERVER_USER: ${{ secrets.CI_EMAIL_SERVER_USER }}
|
||||||
EMAIL_SERVER_PASSWORD: ${{ secrets.CI_EMAIL_SERVER_PASSWORD}}
|
EMAIL_SERVER_PASSWORD: ${{ secrets.CI_EMAIL_SERVER_PASSWORD}}
|
||||||
GOOGLE_LOGIN_ENABLED: ${{ vars.CI_GOOGLE_LOGIN_ENABLED }}
|
GOOGLE_LOGIN_ENABLED: ${{ vars.CI_GOOGLE_LOGIN_ENABLED }}
|
||||||
NEXTAUTH_SECRET: ${{ secrets.CI_NEXTAUTH_SECRET }}
|
NEXTAUTH_SECRET: ${{ secrets.CI_NEXTAUTH_SECRET }}
|
||||||
NEXTAUTH_URL: ${{ secrets.CI_NEXTAUTH_URL }}
|
NEXTAUTH_URL: ${{ secrets.CI_NEXTAUTH_URL }}
|
||||||
|
|||||||
@@ -7,6 +7,8 @@ env:
|
|||||||
ALLOWED_HOSTNAMES: ${{ vars.CI_ALLOWED_HOSTNAMES }}
|
ALLOWED_HOSTNAMES: ${{ vars.CI_ALLOWED_HOSTNAMES }}
|
||||||
CALENDSO_ENCRYPTION_KEY: ${{ secrets.CI_CALENDSO_ENCRYPTION_KEY }}
|
CALENDSO_ENCRYPTION_KEY: ${{ secrets.CI_CALENDSO_ENCRYPTION_KEY }}
|
||||||
DATABASE_URL: ${{ secrets.CI_DATABASE_URL }}
|
DATABASE_URL: ${{ secrets.CI_DATABASE_URL }}
|
||||||
|
E2E_TEST_APPLE_CALENDAR_EMAIL: ${{ secrets.E2E_TEST_APPLE_CALENDAR_EMAIL }}
|
||||||
|
E2E_TEST_APPLE_CALENDAR_PASSWORD: ${{ secrets.E2E_TEST_APPLE_CALENDAR_PASSWORD }}
|
||||||
GOOGLE_API_CREDENTIALS: ${{ secrets.CI_GOOGLE_API_CREDENTIALS }}
|
GOOGLE_API_CREDENTIALS: ${{ secrets.CI_GOOGLE_API_CREDENTIALS }}
|
||||||
GOOGLE_LOGIN_ENABLED: ${{ vars.CI_GOOGLE_LOGIN_ENABLED }}
|
GOOGLE_LOGIN_ENABLED: ${{ vars.CI_GOOGLE_LOGIN_ENABLED }}
|
||||||
NEXTAUTH_SECRET: ${{ secrets.CI_NEXTAUTH_SECRET }}
|
NEXTAUTH_SECRET: ${{ secrets.CI_NEXTAUTH_SECRET }}
|
||||||
|
|||||||
@@ -7,6 +7,8 @@ env:
|
|||||||
ALLOWED_HOSTNAMES: ${{ vars.CI_ALLOWED_HOSTNAMES }}
|
ALLOWED_HOSTNAMES: ${{ vars.CI_ALLOWED_HOSTNAMES }}
|
||||||
CALENDSO_ENCRYPTION_KEY: ${{ secrets.CI_CALENDSO_ENCRYPTION_KEY }}
|
CALENDSO_ENCRYPTION_KEY: ${{ secrets.CI_CALENDSO_ENCRYPTION_KEY }}
|
||||||
DATABASE_URL: ${{ secrets.CI_DATABASE_URL }}
|
DATABASE_URL: ${{ secrets.CI_DATABASE_URL }}
|
||||||
|
E2E_TEST_APPLE_CALENDAR_EMAIL: ${{ secrets.E2E_TEST_APPLE_CALENDAR_EMAIL }}
|
||||||
|
E2E_TEST_APPLE_CALENDAR_PASSWORD: ${{ secrets.E2E_TEST_APPLE_CALENDAR_PASSWORD }}
|
||||||
GOOGLE_API_CREDENTIALS: ${{ secrets.CI_GOOGLE_API_CREDENTIALS }}
|
GOOGLE_API_CREDENTIALS: ${{ secrets.CI_GOOGLE_API_CREDENTIALS }}
|
||||||
GOOGLE_LOGIN_ENABLED: ${{ vars.CI_GOOGLE_LOGIN_ENABLED }}
|
GOOGLE_LOGIN_ENABLED: ${{ vars.CI_GOOGLE_LOGIN_ENABLED }}
|
||||||
NEXTAUTH_SECRET: ${{ secrets.CI_NEXTAUTH_SECRET }}
|
NEXTAUTH_SECRET: ${{ secrets.CI_NEXTAUTH_SECRET }}
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ on: # yamllint disable-line rule:truthy
|
|||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
inputs:
|
inputs:
|
||||||
RELEASE_TAG:
|
RELEASE_TAG:
|
||||||
description: 'v{Major}.{Minor}.{Patch}'
|
description: "v{Major}.{Minor}.{Patch}"
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
release:
|
release:
|
||||||
@@ -21,7 +21,7 @@ jobs:
|
|||||||
uses: actions/checkout@v3
|
uses: actions/checkout@v3
|
||||||
|
|
||||||
- name: "Determine tag"
|
- name: "Determine tag"
|
||||||
run: "echo \"RELEASE_TAG=${GITHUB_REF#refs/tags/}\" >> $GITHUB_ENV"
|
run: 'echo "RELEASE_TAG=${GITHUB_REF#refs/tags/}" >> $GITHUB_ENV'
|
||||||
|
|
||||||
- name: "Run remote release workflow"
|
- name: "Run remote release workflow"
|
||||||
uses: "actions/github-script@v6"
|
uses: "actions/github-script@v6"
|
||||||
|
|||||||
@@ -29,11 +29,11 @@ jobs:
|
|||||||
header: pr-title-lint-error
|
header: pr-title-lint-error
|
||||||
message: |
|
message: |
|
||||||
Hey there and thank you for opening this pull request! 👋🏼
|
Hey there and thank you for opening this pull request! 👋🏼
|
||||||
|
|
||||||
We require pull request titles to follow the [Conventional Commits specification](https://www.conventionalcommits.org/en/v1.0.0/) and it looks like your proposed title needs to be adjusted.
|
We require pull request titles to follow the [Conventional Commits specification](https://www.conventionalcommits.org/en/v1.0.0/) and it looks like your proposed title needs to be adjusted.
|
||||||
|
|
||||||
Details:
|
Details:
|
||||||
|
|
||||||
```
|
```
|
||||||
${{ steps.lint_pr_title.outputs.error_message }}
|
${{ steps.lint_pr_title.outputs.error_message }}
|
||||||
```
|
```
|
||||||
@@ -41,7 +41,7 @@ jobs:
|
|||||||
# Delete a previous comment when the issue has been resolved
|
# Delete a previous comment when the issue has been resolved
|
||||||
- if: ${{ steps.lint_pr_title.outputs.error_message == null }}
|
- if: ${{ steps.lint_pr_title.outputs.error_message == null }}
|
||||||
uses: marocchino/sticky-pull-request-comment@v2
|
uses: marocchino/sticky-pull-request-comment@v2
|
||||||
with:
|
with:
|
||||||
header: pr-title-lint-error
|
header: pr-title-lint-error
|
||||||
message: |
|
message: |
|
||||||
Thank you for following the naming conventions! 🙏
|
Thank you for following the naming conventions! 🙏
|
||||||
+1
-1
@@ -40,4 +40,4 @@ vscode:
|
|||||||
- bradlc.vscode-tailwindcss
|
- bradlc.vscode-tailwindcss
|
||||||
- ban.spellright
|
- ban.spellright
|
||||||
- stripe.vscode-stripe
|
- stripe.vscode-stripe
|
||||||
- Prisma.prisma
|
- Prisma.prisma
|
||||||
@@ -67,6 +67,7 @@ const schemaEventTypeCreateParams = z
|
|||||||
seatsPerTimeSlot: z.number().optional(),
|
seatsPerTimeSlot: z.number().optional(),
|
||||||
seatsShowAttendees: z.boolean().optional(),
|
seatsShowAttendees: z.boolean().optional(),
|
||||||
bookingFields: eventTypeBookingFields.optional(),
|
bookingFields: eventTypeBookingFields.optional(),
|
||||||
|
scheduleId: z.number().optional(),
|
||||||
})
|
})
|
||||||
.strict();
|
.strict();
|
||||||
|
|
||||||
@@ -85,6 +86,7 @@ const schemaEventTypeEditParams = z
|
|||||||
seatsPerTimeSlot: z.number().optional(),
|
seatsPerTimeSlot: z.number().optional(),
|
||||||
seatsShowAttendees: z.boolean().optional(),
|
seatsShowAttendees: z.boolean().optional(),
|
||||||
bookingFields: eventTypeBookingFields.optional(),
|
bookingFields: eventTypeBookingFields.optional(),
|
||||||
|
scheduleId: z.number().optional(),
|
||||||
})
|
})
|
||||||
.strict();
|
.strict();
|
||||||
|
|
||||||
@@ -98,6 +100,7 @@ export const schemaEventTypeReadPublic = EventType.pick({
|
|||||||
position: true,
|
position: true,
|
||||||
userId: true,
|
userId: true,
|
||||||
teamId: true,
|
teamId: true,
|
||||||
|
scheduleId: true,
|
||||||
eventName: true,
|
eventName: true,
|
||||||
timeZone: true,
|
timeZone: true,
|
||||||
periodType: true,
|
periodType: true,
|
||||||
|
|||||||
@@ -1,9 +1,11 @@
|
|||||||
import type { NextApiRequest } from "next";
|
import type { NextApiRequest } from "next";
|
||||||
|
|
||||||
|
import { HttpError } from "@calcom/lib/http-error";
|
||||||
import { defaultResponder } from "@calcom/lib/server";
|
import { defaultResponder } from "@calcom/lib/server";
|
||||||
|
|
||||||
import { schemaEventTypeReadPublic } from "~/lib/validations/event-type";
|
import { schemaEventTypeReadPublic } from "~/lib/validations/event-type";
|
||||||
import { schemaQueryIdParseInt } from "~/lib/validations/shared/queryIdTransformParseInt";
|
import { schemaQueryIdParseInt } from "~/lib/validations/shared/queryIdTransformParseInt";
|
||||||
|
import { checkPermissions as canAccessTeamEventOrThrow } from "~/pages/api/teams/[teamId]/_auth-middleware";
|
||||||
|
|
||||||
import getCalLink from "../_utils/getCalLink";
|
import getCalLink from "../_utils/getCalLink";
|
||||||
|
|
||||||
@@ -42,7 +44,8 @@ import getCalLink from "../_utils/getCalLink";
|
|||||||
export async function getHandler(req: NextApiRequest) {
|
export async function getHandler(req: NextApiRequest) {
|
||||||
const { prisma, query } = req;
|
const { prisma, query } = req;
|
||||||
const { id } = schemaQueryIdParseInt.parse(query);
|
const { id } = schemaQueryIdParseInt.parse(query);
|
||||||
const event_type = await prisma.eventType.findUnique({
|
|
||||||
|
const eventType = await prisma.eventType.findUnique({
|
||||||
where: { id },
|
where: { id },
|
||||||
include: {
|
include: {
|
||||||
customInputs: true,
|
customInputs: true,
|
||||||
@@ -51,10 +54,43 @@ export async function getHandler(req: NextApiRequest) {
|
|||||||
owner: { select: { username: true, id: true } },
|
owner: { select: { username: true, id: true } },
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
await checkPermissions(req, eventType);
|
||||||
|
|
||||||
const link = event_type ? getCalLink(event_type) : null;
|
const link = eventType ? getCalLink(eventType) : null;
|
||||||
|
// user.defaultScheduleId doesn't work the same for team events.
|
||||||
|
if (!eventType?.scheduleId && eventType?.userId && !eventType?.teamId) {
|
||||||
|
const user = await prisma.user.findUniqueOrThrow({
|
||||||
|
where: {
|
||||||
|
id: eventType.userId,
|
||||||
|
},
|
||||||
|
select: {
|
||||||
|
defaultScheduleId: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
eventType.scheduleId = user.defaultScheduleId;
|
||||||
|
}
|
||||||
|
|
||||||
return { event_type: schemaEventTypeReadPublic.parse({ ...event_type, link }) };
|
// TODO: eventType when not found should be a 404
|
||||||
|
// but API consumers may depend on the {} behaviour.
|
||||||
|
return { event_type: schemaEventTypeReadPublic.parse({ ...eventType, link }) };
|
||||||
|
}
|
||||||
|
|
||||||
|
type BaseEventTypeCheckPermissions = {
|
||||||
|
userId: number | null;
|
||||||
|
teamId: number | null;
|
||||||
|
};
|
||||||
|
|
||||||
|
async function checkPermissions<T extends BaseEventTypeCheckPermissions>(
|
||||||
|
req: NextApiRequest,
|
||||||
|
eventType: (T & Partial<Omit<T, keyof BaseEventTypeCheckPermissions>>) | null
|
||||||
|
) {
|
||||||
|
if (req.isAdmin) return true;
|
||||||
|
if (eventType?.teamId) {
|
||||||
|
req.query.teamId = String(eventType.teamId);
|
||||||
|
await canAccessTeamEventOrThrow(req, "MEMBER");
|
||||||
|
}
|
||||||
|
if (eventType?.userId === req.userId) return true; // is owner.
|
||||||
|
throw new HttpError({ statusCode: 403, message: "Forbidden" });
|
||||||
}
|
}
|
||||||
|
|
||||||
export default defaultResponder(getHandler);
|
export default defaultResponder(getHandler);
|
||||||
@@ -52,6 +52,9 @@ import checkTeamEventEditPermission from "../_utils/checkTeamEventEditPermission
|
|||||||
* slug:
|
* slug:
|
||||||
* type: string
|
* type: string
|
||||||
* description: Unique slug for the event type
|
* description: Unique slug for the event type
|
||||||
|
* scheduleId:
|
||||||
|
* type: number
|
||||||
|
* description: The ID of the schedule for this event type
|
||||||
* hosts:
|
* hosts:
|
||||||
* type: array
|
* type: array
|
||||||
* items:
|
* items:
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
import type { Prisma } from "@prisma/client";
|
|
||||||
import type { NextApiRequest } from "next";
|
import type { NextApiRequest } from "next";
|
||||||
|
|
||||||
import { HttpError } from "@calcom/lib/http-error";
|
import { HttpError } from "@calcom/lib/http-error";
|
||||||
import { defaultResponder } from "@calcom/lib/server";
|
import { defaultResponder } from "@calcom/lib/server";
|
||||||
|
import type { PrismaClient } from "@calcom/prisma/client";
|
||||||
|
|
||||||
import { schemaEventTypeReadPublic } from "~/lib/validations/event-type";
|
import { schemaEventTypeReadPublic } from "~/lib/validations/event-type";
|
||||||
import { schemaQuerySingleOrMultipleUserIds } from "~/lib/validations/shared/queryUserId";
|
import { schemaQuerySingleOrMultipleUserIds } from "~/lib/validations/shared/queryUserId";
|
||||||
@@ -35,20 +35,12 @@ import getCalLink from "./_utils/getCalLink";
|
|||||||
* description: No event types were found
|
* description: No event types were found
|
||||||
*/
|
*/
|
||||||
async function getHandler(req: NextApiRequest) {
|
async function getHandler(req: NextApiRequest) {
|
||||||
const { userId, isAdmin, prisma } = req;
|
const { userId, prisma } = req;
|
||||||
|
const userIds = req.query.userId ? extractUserIdsFromQuery(req) : [userId];
|
||||||
const args: Prisma.EventTypeFindManyArgs = {
|
|
||||||
where: { userId },
|
|
||||||
};
|
|
||||||
/** Only admins can query other users */
|
|
||||||
if (!isAdmin && req.query.userId) throw new HttpError({ statusCode: 401, message: "ADMIN required" });
|
|
||||||
if (isAdmin && req.query.userId) {
|
|
||||||
const query = schemaQuerySingleOrMultipleUserIds.parse(req.query);
|
|
||||||
const userIds = Array.isArray(query.userId) ? query.userId : [query.userId || userId];
|
|
||||||
args.where = { userId: { in: userIds } };
|
|
||||||
}
|
|
||||||
const data = await prisma.eventType.findMany({
|
const data = await prisma.eventType.findMany({
|
||||||
...args,
|
where: {
|
||||||
|
userId: { in: userIds },
|
||||||
|
},
|
||||||
include: {
|
include: {
|
||||||
customInputs: true,
|
customInputs: true,
|
||||||
team: { select: { slug: true } },
|
team: { select: { slug: true } },
|
||||||
@@ -56,13 +48,74 @@ async function getHandler(req: NextApiRequest) {
|
|||||||
owner: { select: { username: true, id: true } },
|
owner: { select: { username: true, id: true } },
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
// this really should return [], but backwards compatibility..
|
||||||
if (data.length === 0) new HttpError({ statusCode: 404, message: "No event types were found" });
|
if (data.length === 0) new HttpError({ statusCode: 404, message: "No event types were found" });
|
||||||
return {
|
return {
|
||||||
event_types: data.map((eventType) => {
|
event_types: (await defaultScheduleId<(typeof data)[number]>({ eventTypes: data, prisma, userIds })).map(
|
||||||
const link = getCalLink(eventType);
|
(eventType) => {
|
||||||
return schemaEventTypeReadPublic.parse({ ...eventType, link });
|
const link = getCalLink(eventType);
|
||||||
}),
|
return schemaEventTypeReadPublic.parse({ ...eventType, link });
|
||||||
|
}
|
||||||
|
),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
// TODO: Extract & reuse.
|
||||||
|
function extractUserIdsFromQuery({ isAdmin, query }: NextApiRequest) {
|
||||||
|
/** Guard: Only admins can query other users */
|
||||||
|
if (!isAdmin) {
|
||||||
|
throw new HttpError({ statusCode: 401, message: "ADMIN required" });
|
||||||
|
}
|
||||||
|
const { userId: userIdOrUserIds } = schemaQuerySingleOrMultipleUserIds.parse(query);
|
||||||
|
return Array.isArray(userIdOrUserIds) ? userIdOrUserIds : [userIdOrUserIds];
|
||||||
|
}
|
||||||
|
|
||||||
|
type DefaultScheduleIdEventTypeBase = {
|
||||||
|
scheduleId: number | null;
|
||||||
|
userId: number | null;
|
||||||
|
};
|
||||||
|
// If an eventType is given w/o a scheduleId
|
||||||
|
// Then we associate the default user schedule id to the eventType
|
||||||
|
async function defaultScheduleId<T extends DefaultScheduleIdEventTypeBase>({
|
||||||
|
prisma,
|
||||||
|
eventTypes,
|
||||||
|
userIds,
|
||||||
|
}: {
|
||||||
|
prisma: PrismaClient;
|
||||||
|
eventTypes: (T & Partial<Omit<T, keyof DefaultScheduleIdEventTypeBase>>)[];
|
||||||
|
userIds: number[];
|
||||||
|
}) {
|
||||||
|
// there is no event types without a scheduleId, skip the user query
|
||||||
|
if (eventTypes.every((eventType) => eventType.scheduleId)) return eventTypes;
|
||||||
|
|
||||||
|
const users = await prisma.user.findMany({
|
||||||
|
where: {
|
||||||
|
id: {
|
||||||
|
in: userIds,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
select: {
|
||||||
|
id: true,
|
||||||
|
defaultScheduleId: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!users.length) {
|
||||||
|
return eventTypes;
|
||||||
|
}
|
||||||
|
|
||||||
|
const defaultScheduleIds = users.reduce((result, user) => {
|
||||||
|
result[user.id] = user.defaultScheduleId;
|
||||||
|
return result;
|
||||||
|
}, {} as { [x: number]: number | null });
|
||||||
|
|
||||||
|
return eventTypes.map((eventType) => {
|
||||||
|
// realistically never happens, userId should't be null on personal event types.
|
||||||
|
if (!eventType.userId) return eventType;
|
||||||
|
return {
|
||||||
|
...eventType,
|
||||||
|
scheduleId: eventType.scheduleId || defaultScheduleIds[eventType.userId],
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
export default defaultResponder(getHandler);
|
export default defaultResponder(getHandler);
|
||||||
@@ -60,6 +60,9 @@ import ensureOnlyMembersAsHosts from "./_utils/ensureOnlyMembersAsHosts";
|
|||||||
* hidden:
|
* hidden:
|
||||||
* type: boolean
|
* type: boolean
|
||||||
* description: If the event type should be hidden from your public booking page
|
* description: If the event type should be hidden from your public booking page
|
||||||
|
* scheduleId:
|
||||||
|
* type: number
|
||||||
|
* description: The ID of the schedule for this event type
|
||||||
* position:
|
* position:
|
||||||
* type: integer
|
* type: integer
|
||||||
* description: The position of the event type on the public booking page
|
* description: The position of the event type on the public booking page
|
||||||
@@ -178,6 +181,7 @@ import ensureOnlyMembersAsHosts from "./_utils/ensureOnlyMembersAsHosts";
|
|||||||
* position: 0
|
* position: 0
|
||||||
* eventName: null
|
* eventName: null
|
||||||
* timeZone: null
|
* timeZone: null
|
||||||
|
* scheduleId: 5
|
||||||
* periodType: UNLIMITED
|
* periodType: UNLIMITED
|
||||||
* periodStartDate: 2023-02-15T08:46:16.000Z
|
* periodStartDate: 2023-02-15T08:46:16.000Z
|
||||||
* periodEndDate: 2023-0-15T08:46:16.000Z
|
* periodEndDate: 2023-0-15T08:46:16.000Z
|
||||||
|
|||||||
@@ -59,7 +59,7 @@ async function getHandler(req: NextApiRequest) {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const data = await prisma.eventType.findMany(args);
|
const data = await prisma.eventType.findMany(args);
|
||||||
return { event_types: data.map((attendee) => schemaEventTypeReadPublic.parse(attendee)) };
|
return { event_types: data.map((eventType) => schemaEventTypeReadPublic.parse(eventType)) };
|
||||||
}
|
}
|
||||||
|
|
||||||
export default defaultResponder(getHandler);
|
export default defaultResponder(getHandler);
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
# Set the version of docker compose to use
|
# Set the version of docker compose to use
|
||||||
version: '3.9'
|
version: "3.9"
|
||||||
|
|
||||||
# The containers that compose the project
|
# The containers that compose the project
|
||||||
services:
|
services:
|
||||||
@@ -8,8 +8,8 @@ services:
|
|||||||
restart: always
|
restart: always
|
||||||
container_name: integration-tests-prisma
|
container_name: integration-tests-prisma
|
||||||
ports:
|
ports:
|
||||||
- '5433:5432'
|
- "5433:5432"
|
||||||
environment:
|
environment:
|
||||||
POSTGRES_USER: prisma
|
POSTGRES_USER: prisma
|
||||||
POSTGRES_PASSWORD: prisma
|
POSTGRES_PASSWORD: prisma
|
||||||
POSTGRES_DB: tests
|
POSTGRES_DB: tests
|
||||||
@@ -15,6 +15,7 @@ module.exports = {
|
|||||||
"storybook-addon-rtl-direction",
|
"storybook-addon-rtl-direction",
|
||||||
"storybook-react-i18next",
|
"storybook-react-i18next",
|
||||||
"storybook-addon-next",
|
"storybook-addon-next",
|
||||||
|
"storybook-addon-next-router",
|
||||||
/*{
|
/*{
|
||||||
name: "storybook-addon-next",
|
name: "storybook-addon-next",
|
||||||
options: {
|
options: {
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
<script>
|
<script>
|
||||||
window.global = window;
|
window.global = window;
|
||||||
window.isEmbed = ()=> {
|
window.isEmbed = () => {
|
||||||
return location.search.includes("embed=")
|
return location.search.includes("embed=");
|
||||||
}
|
};
|
||||||
</script>
|
</script>
|
||||||
<style>
|
<style>
|
||||||
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@500&display=swap');
|
@import url("https://fonts.googleapis.com/css2?family=Inter:wght@500&display=swap");
|
||||||
</style>
|
</style>
|
||||||
@@ -1,4 +1,5 @@
|
|||||||
import { addDecorator } from "@storybook/react";
|
import { addDecorator } from "@storybook/react";
|
||||||
|
import { AppRouterContext } from "next/dist/shared/lib/app-router-context";
|
||||||
import { I18nextProvider } from "react-i18next";
|
import { I18nextProvider } from "react-i18next";
|
||||||
|
|
||||||
import "../styles/globals.css";
|
import "../styles/globals.css";
|
||||||
@@ -13,6 +14,13 @@ export const parameters = {
|
|||||||
date: /Date$/,
|
date: /Date$/,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
nextRouter: {
|
||||||
|
pathname: "/",
|
||||||
|
asPath: "/",
|
||||||
|
query: {},
|
||||||
|
push() {},
|
||||||
|
Provider: AppRouterContext.Provider,
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
addDecorator((storyFn) => (
|
addDecorator((storyFn) => (
|
||||||
|
|||||||
@@ -1,11 +1,21 @@
|
|||||||
import { Meta } from '@storybook/addon-docs';
|
import { Meta } from "@storybook/addon-docs";
|
||||||
|
|
||||||
<Meta title="Introduction" />
|
<Meta title="Introduction" />
|
||||||
|
|
||||||
<div className="text-center flex flex-col items-center">
|
<div className="flex flex-col items-center text-center">
|
||||||
<h1 style={{marginBottom: '24px', marginTop: '36px'}}>Welcome to Cal.com UI</h1>
|
<h1 style={{ marginBottom: "24px", marginTop: "36px" }}>Welcome to Cal.com UI</h1>
|
||||||
<p>This is the beginning of our storybook improvements to match Figma as close as possible. Like our Figma library, we will be adding more components as we go along.</p>
|
<p>
|
||||||
<p>Our <a href="https://www.figma.com/file/9MOufQNLtdkpnDucmNX10R/%E2%9D%96-Cal-DS" target="_blank">Figma</a> library is avalible for anyone to view and use. If you have any questions or concerns, please reach out to the design team.</p>
|
This is the beginning of our storybook improvements to match Figma as close as possible. Like our Figma
|
||||||
|
library, we will be adding more components as we go along.
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
Our{" "}
|
||||||
|
<a href="https://www.figma.com/file/9MOufQNLtdkpnDucmNX10R/%E2%9D%96-Cal-DS" target="_blank">
|
||||||
|
Figma
|
||||||
|
</a>{" "}
|
||||||
|
library is avalible for anyone to view and use. If you have any questions or concerns, please reach out to
|
||||||
|
the design team.
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<img src="/sb-cover.jpg" />
|
<img src="/sb-cover.jpg" />
|
||||||
@@ -23,6 +23,7 @@
|
|||||||
"next": "^13.4.6",
|
"next": "^13.4.6",
|
||||||
"react": "^18.2.0",
|
"react": "^18.2.0",
|
||||||
"react-dom": "^18.2.0",
|
"react-dom": "^18.2.0",
|
||||||
|
"storybook-addon-next-router": "^4.0.2",
|
||||||
"storybook-addon-rtl-direction": "^0.0.19"
|
"storybook-addon-rtl-direction": "^0.0.19"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
@import url("../../../packages/features/calendars/weeklyview/styles/styles.css");
|
@import url("../../../packages/features/calendars/weeklyview/styles/styles.css");
|
||||||
|
|
||||||
.sbdocs {
|
.sbdocs {
|
||||||
font-family: 'Inter var' !important;
|
font-family: "Inter var" !important;
|
||||||
padding: 0!important;
|
padding: 0 !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
#docs-root {
|
#docs-root {
|
||||||
@@ -21,8 +21,8 @@
|
|||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
border: none;
|
border: none;
|
||||||
border-top: 2px solid rgba(0, 0, 0, 0.12);
|
border-top: 2px solid rgba(0, 0, 0, 0.12);
|
||||||
padding-top: 12px!important;
|
padding-top: 12px !important;
|
||||||
padding-bottom: 12px!important;
|
padding-bottom: 12px !important;
|
||||||
margin: 82px 0 0 0;
|
margin: 82px 0 0 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -37,7 +37,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
/** Docs table **/
|
/** Docs table **/
|
||||||
.custom-args-wrapper{
|
.custom-args-wrapper {
|
||||||
max-height: 400px;
|
max-height: 400px;
|
||||||
overflow-y: scroll;
|
overflow-y: scroll;
|
||||||
overflow-x: hidden;
|
overflow-x: hidden;
|
||||||
@@ -45,7 +45,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.docblock-argstable-body {
|
.docblock-argstable-body {
|
||||||
box-shadow: none!important;
|
box-shadow: none !important;
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -64,7 +64,7 @@
|
|||||||
|
|
||||||
.docblock-argstable-body div p,
|
.docblock-argstable-body div p,
|
||||||
.docblock-argstable-body div span {
|
.docblock-argstable-body div span {
|
||||||
color: #8F8F8F !important;
|
color: #8f8f8f !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Custom components **/
|
/** Custom components **/
|
||||||
@@ -83,8 +83,8 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.story-title h1 span {
|
.story-title h1 span {
|
||||||
color: #9CA3AF;
|
color: #9ca3af;
|
||||||
font-family: 'Inter var';
|
font-family: "Inter var";
|
||||||
font-weight: normal;
|
font-weight: normal;
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
margin-left: 8px;
|
margin-left: 8px;
|
||||||
@@ -96,7 +96,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.examples {
|
.examples {
|
||||||
background-color: #F9FAFB;
|
background-color: #f9fafb;
|
||||||
padding: 20px;
|
padding: 20px;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
@@ -111,14 +111,14 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.examples-title {
|
.examples-title {
|
||||||
color: #8F8F8F;
|
color: #8f8f8f;
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
margin-bottom: 30px;
|
margin-bottom: 30px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.examples-item-title {
|
.examples-item-title {
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
color: #8F8F8F;
|
color: #8f8f8f;
|
||||||
margin-bottom: 12px;
|
margin-bottom: 12px;
|
||||||
display: block;
|
display: block;
|
||||||
}
|
}
|
||||||
@@ -130,7 +130,7 @@
|
|||||||
.examples-footnote p,
|
.examples-footnote p,
|
||||||
.examples-footnote ul,
|
.examples-footnote ul,
|
||||||
.examples-footnote li {
|
.examples-footnote li {
|
||||||
color: #8F8F8F;
|
color: #8f8f8f;
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -139,11 +139,11 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.examples-footnote li {
|
.examples-footnote li {
|
||||||
margin: 0!important;
|
margin: 0 !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.story-note {
|
.story-note {
|
||||||
background-color: #F9FAFB;
|
background-color: #f9fafb;
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
padding: 20px;
|
padding: 20px;
|
||||||
margin-bottom: 12px;
|
margin-bottom: 12px;
|
||||||
@@ -161,12 +161,12 @@
|
|||||||
box-shadow: none !important;
|
box-shadow: none !important;
|
||||||
margin: 0 !important;
|
margin: 0 !important;
|
||||||
border: none !important;
|
border: none !important;
|
||||||
border-radius: none!important;
|
border-radius: none !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.docs-story > div:first-child {
|
.docs-story > div:first-child {
|
||||||
padding: 0!important;
|
padding: 0 !important;
|
||||||
margin: 0!important;
|
margin: 0 !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.docs-story .innerZoomElementWrapper > div {
|
.docs-story .innerZoomElementWrapper > div {
|
||||||
@@ -174,7 +174,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.sb-main-padded {
|
.sb-main-padded {
|
||||||
padding: 0!important;
|
padding: 0 !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
@media screen and (max-width: 1200px) {
|
@media screen and (max-width: 1200px) {
|
||||||
@@ -198,74 +198,72 @@
|
|||||||
:root {
|
:root {
|
||||||
/* background */
|
/* background */
|
||||||
|
|
||||||
--cal-bg-emphasis: #E5E7EB;
|
--cal-bg-emphasis: #e5e7eb;
|
||||||
--cal-bg: white;
|
--cal-bg: white;
|
||||||
--cal-bg-subtle: #F3F4F6;
|
--cal-bg-subtle: #f3f4f6;
|
||||||
--cal-bg-muted: #F9FAFB;
|
--cal-bg-muted: #f9fafb;
|
||||||
--cal-bg-inverted: #111827;
|
--cal-bg-inverted: #111827;
|
||||||
|
|
||||||
/* background -> components*/
|
/* background -> components*/
|
||||||
--cal-bg-info: #DEE9FC;
|
--cal-bg-info: #dee9fc;
|
||||||
--cal-bg-success: #E2FBE8;
|
--cal-bg-success: #e2fbe8;
|
||||||
--cal-bg-attention: #FCEED8;
|
--cal-bg-attention: #fceed8;
|
||||||
--cal-bg-error: #F9E3E2;
|
--cal-bg-error: #f9e3e2;
|
||||||
--cal-bg-dark-error: #752522;
|
--cal-bg-dark-error: #752522;
|
||||||
|
|
||||||
|
|
||||||
/* Borders */
|
/* Borders */
|
||||||
--cal-border-emphasis:#9CA3AF;
|
--cal-border-emphasis: #9ca3af;
|
||||||
--cal-border: #D1D5DB;
|
--cal-border: #d1d5db;
|
||||||
--cal-border-subtle:#E5E7EB;
|
--cal-border-subtle: #e5e7eb;
|
||||||
--cal-border-muted:#F3F4F6;
|
--cal-border-muted: #f3f4f6;
|
||||||
--cal-border-error: #aa2e26;
|
--cal-border-error: #aa2e26;
|
||||||
|
|
||||||
/* Content/Text */
|
/* Content/Text */
|
||||||
--cal-text-emphasis: #111827;
|
--cal-text-emphasis: #111827;
|
||||||
--cal-text:#374151;
|
--cal-text: #374151;
|
||||||
--cal-text-subtle:#6B7280;
|
--cal-text-subtle: #6b7280;
|
||||||
--cal-text-muted:#9CA3AF;
|
--cal-text-muted: #9ca3af;
|
||||||
--cal-text-inverted:white;
|
--cal-text-inverted: white;
|
||||||
|
|
||||||
/* Content/Text -> components */
|
/* Content/Text -> components */
|
||||||
--cal-text-info:#253985;
|
--cal-text-info: #253985;
|
||||||
--cal-text-success:#285231;
|
--cal-text-success: #285231;
|
||||||
--cal-text-attention:#73321B;
|
--cal-text-attention: #73321b;
|
||||||
--cal-text-error:#752522;
|
--cal-text-error: #752522;
|
||||||
|
|
||||||
/* Brand shinanigans
|
/* Brand shinanigans
|
||||||
-> These will be computed for the users theme at runtime.
|
-> These will be computed for the users theme at runtime.
|
||||||
*/
|
*/
|
||||||
--cal-brand:#111827;
|
--cal-brand: #111827;
|
||||||
--cal-brand-emphasis:#101010;
|
--cal-brand-emphasis: #101010;
|
||||||
--cal-brand-text: white;
|
--cal-brand-text: white;
|
||||||
}
|
}
|
||||||
.dark {
|
.dark {
|
||||||
/* background */
|
/* background */
|
||||||
|
|
||||||
--cal-bg-emphasis: #2B2B2B;
|
--cal-bg-emphasis: #2b2b2b;
|
||||||
--cal-bg: #101010;
|
--cal-bg: #101010;
|
||||||
--cal-bg-subtle: #2B2B2B;
|
--cal-bg-subtle: #2b2b2b;
|
||||||
--cal-bg-muted: #1C1C1C;
|
--cal-bg-muted: #1c1c1c;
|
||||||
--cal-bg-inverted: #F3F4F6;
|
--cal-bg-inverted: #f3f4f6;
|
||||||
|
|
||||||
/* background -> components*/
|
/* background -> components*/
|
||||||
--cal-bg-info: #DEE9FC;
|
--cal-bg-info: #dee9fc;
|
||||||
--cal-bg-success: #E2FBE8;
|
--cal-bg-success: #e2fbe8;
|
||||||
--cal-bg-attention: #FCEED8;
|
--cal-bg-attention: #fceed8;
|
||||||
--cal-bg-error: #F9E3E2;
|
--cal-bg-error: #f9e3e2;
|
||||||
--cal-bg-dark-error: #752522;
|
--cal-bg-dark-error: #752522;
|
||||||
|
|
||||||
|
|
||||||
/* Borders */
|
/* Borders */
|
||||||
--cal-border-emphasis: #575757;
|
--cal-border-emphasis: #575757;
|
||||||
--cal-border: #444444;
|
--cal-border: #444444;
|
||||||
--cal-border-subtle: #2B2B2B;
|
--cal-border-subtle: #2b2b2b;
|
||||||
--cal-border-muted: #1C1C1C;
|
--cal-border-muted: #1c1c1c;
|
||||||
--cal-border-error: #aa2e26;
|
--cal-border-error: #aa2e26;
|
||||||
|
|
||||||
/* Content/Text */
|
/* Content/Text */
|
||||||
--cal-text-emphasis: #F3F4F6;
|
--cal-text-emphasis: #f3f4f6;
|
||||||
--cal-text: #D6D6D6;
|
--cal-text: #d6d6d6;
|
||||||
--cal-text-subtle: #757575;
|
--cal-text-subtle: #757575;
|
||||||
--cal-text-muted: #575757;
|
--cal-text-muted: #575757;
|
||||||
--cal-text-inverted: #101010;
|
--cal-text-inverted: #101010;
|
||||||
@@ -273,7 +271,7 @@
|
|||||||
/* Content/Text -> components */
|
/* Content/Text -> components */
|
||||||
--cal-text-info: #253985;
|
--cal-text-info: #253985;
|
||||||
--cal-text-success: #285231;
|
--cal-text-success: #285231;
|
||||||
--cal-text-attention: #73321B;
|
--cal-text-attention: #73321b;
|
||||||
--cal-text-error: #752522;
|
--cal-text-error: #752522;
|
||||||
|
|
||||||
/* Brand shenanigans
|
/* Brand shenanigans
|
||||||
|
|||||||
@@ -2,8 +2,8 @@ html,
|
|||||||
body {
|
body {
|
||||||
padding: 0;
|
padding: 0;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
font-family: -apple-system, BlinkMacSystemFont, Segoe UI, Roboto, Oxygen,
|
font-family: -apple-system, BlinkMacSystemFont, Segoe UI, Roboto, Oxygen, Ubuntu, Cantarell, Fira Sans,
|
||||||
Ubuntu, Cantarell, Fira Sans, Droid Sans, Helvetica Neue, sans-serif;
|
Droid Sans, Helvetica Neue, sans-serif;
|
||||||
}
|
}
|
||||||
|
|
||||||
a {
|
a {
|
||||||
@@ -21,7 +21,7 @@ a {
|
|||||||
}
|
}
|
||||||
.swagger-ui .opblock .opblock-summary {
|
.swagger-ui .opblock .opblock-summary {
|
||||||
display: grid;
|
display: grid;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
}
|
}
|
||||||
.opblock-summary-path {
|
.opblock-summary-path {
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
@@ -43,22 +43,22 @@ a {
|
|||||||
font-size: 22px;
|
font-size: 22px;
|
||||||
}
|
}
|
||||||
.swagger-ui .scheme-container {
|
.swagger-ui .scheme-container {
|
||||||
padding: 14px 0;
|
padding: 14px 0;
|
||||||
}
|
}
|
||||||
.swagger-ui .info {
|
.swagger-ui .info {
|
||||||
margin: 10px 0;
|
margin: 10px 0;
|
||||||
}
|
}
|
||||||
.swagger-ui .auth-wrapper {
|
.swagger-ui .auth-wrapper {
|
||||||
margin: 10px 0;
|
margin: 10px 0;
|
||||||
}
|
}
|
||||||
.swagger-ui .authorization__btn {
|
.swagger-ui .authorization__btn {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
.swagger-ui .opblock {
|
.swagger-ui .opblock {
|
||||||
margin: 0 0 5px;
|
margin: 0 0 5px;
|
||||||
}
|
}
|
||||||
button.opblock-summary-control > svg {
|
button.opblock-summary-control > svg {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
.swagger-ui .filter .operation-filter-input {
|
.swagger-ui .filter .operation-filter-input {
|
||||||
border: 2px solid #d8dde7;
|
border: 2px solid #d8dde7;
|
||||||
@@ -73,15 +73,16 @@ a {
|
|||||||
.swagger-ui .info .title small {
|
.swagger-ui .info .title small {
|
||||||
top: 5px;
|
top: 5px;
|
||||||
}
|
}
|
||||||
.swagger-ui a.nostyle, .swagger-ui a.nostyle:visited {
|
.swagger-ui a.nostyle,
|
||||||
|
.swagger-ui a.nostyle:visited {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
div.request-snippets > div.curl-command > div:nth-child(1) {
|
div.request-snippets > div.curl-command > div:nth-child(1) {
|
||||||
overscroll-behavior: contain;
|
overscroll-behavior: contain;
|
||||||
overflow-x: scroll;
|
overflow-x: scroll;
|
||||||
}
|
}
|
||||||
.swagger-ui .opblock-body pre.microlight {
|
.swagger-ui .opblock-body pre.microlight {
|
||||||
font-size: 9px;
|
font-size: 9px;
|
||||||
}
|
}
|
||||||
.swagger-ui table tbody tr td {
|
.swagger-ui table tbody tr td {
|
||||||
padding: 0px 0 0;
|
padding: 0px 0 0;
|
||||||
@@ -91,7 +92,7 @@ a {
|
|||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
}
|
}
|
||||||
div.no-margin > div > div.responses-wrapper > div.responses-inner > div > div > table > tbody > tr {
|
div.no-margin > div > div.responses-wrapper > div.responses-inner > div > div > table > tbody > tr {
|
||||||
display: flex;
|
display: flex;
|
||||||
width: 100vw;
|
width: 100vw;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
font-size: 60%;
|
font-size: 60%;
|
||||||
@@ -99,4 +100,4 @@ a {
|
|||||||
div.no-margin > div > div.responses-wrapper > div.responses-inner > div > div > table > thead > tr {
|
div.no-margin > div > div.responses-wrapper > div.responses-inner > div > div > table > thead > tr {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -12,6 +12,7 @@ import { z } from "zod";
|
|||||||
import type { EventLocationType } from "@calcom/app-store/locations";
|
import type { EventLocationType } from "@calcom/app-store/locations";
|
||||||
import { getEventLocationType, MeetLocationType, LocationType } from "@calcom/app-store/locations";
|
import { getEventLocationType, MeetLocationType, LocationType } from "@calcom/app-store/locations";
|
||||||
import useLockedFieldsManager from "@calcom/features/ee/managed-event-types/hooks/useLockedFieldsManager";
|
import useLockedFieldsManager from "@calcom/features/ee/managed-event-types/hooks/useLockedFieldsManager";
|
||||||
|
import { useOrgBranding } from "@calcom/features/ee/organizations/context/provider";
|
||||||
import cx from "@calcom/lib/classNames";
|
import cx from "@calcom/lib/classNames";
|
||||||
import { CAL_URL } from "@calcom/lib/constants";
|
import { CAL_URL } from "@calcom/lib/constants";
|
||||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||||
@@ -116,6 +117,7 @@ export const EventSetupTab = (
|
|||||||
const [editingLocationType, setEditingLocationType] = useState<string>("");
|
const [editingLocationType, setEditingLocationType] = useState<string>("");
|
||||||
const [selectedLocation, setSelectedLocation] = useState<LocationOption | undefined>(undefined);
|
const [selectedLocation, setSelectedLocation] = useState<LocationOption | undefined>(undefined);
|
||||||
const [multipleDuration, setMultipleDuration] = useState(eventType.metadata?.multipleDuration);
|
const [multipleDuration, setMultipleDuration] = useState(eventType.metadata?.multipleDuration);
|
||||||
|
const orgBranding = useOrgBranding();
|
||||||
|
|
||||||
const locationOptions = props.locationOptions.map((locationOption) => {
|
const locationOptions = props.locationOptions.map((locationOption) => {
|
||||||
const options = locationOption.options.filter((option) => {
|
const options = locationOption.options.filter((option) => {
|
||||||
@@ -129,10 +131,12 @@ export const EventSetupTab = (
|
|||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
const multipleDurationOptions = [5, 10, 15, 20, 25, 30, 45, 50, 60, 75, 80, 90, 120, 180].map((mins) => ({
|
const multipleDurationOptions = [5, 10, 15, 20, 25, 30, 45, 50, 60, 75, 80, 90, 120, 150, 180].map(
|
||||||
value: mins,
|
(mins) => ({
|
||||||
label: t("multiple_duration_mins", { count: mins }),
|
value: mins,
|
||||||
}));
|
label: t("multiple_duration_mins", { count: mins }),
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
const [selectedMultipleDuration, setSelectedMultipleDuration] = useState<
|
const [selectedMultipleDuration, setSelectedMultipleDuration] = useState<
|
||||||
MultiValue<{
|
MultiValue<{
|
||||||
@@ -383,6 +387,9 @@ export const EventSetupTab = (
|
|||||||
|
|
||||||
const lengthLockedProps = shouldLockDisableProps("length");
|
const lengthLockedProps = shouldLockDisableProps("length");
|
||||||
const descriptionLockedProps = shouldLockDisableProps("description");
|
const descriptionLockedProps = shouldLockDisableProps("description");
|
||||||
|
const urlPrefix = orgBranding
|
||||||
|
? orgBranding?.fullDomain.replace(/^(https?:|)\/\//, "")
|
||||||
|
: `${CAL_URL?.replace(/^(https?:|)\/\//, "")}`;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
@@ -411,10 +418,10 @@ export const EventSetupTab = (
|
|||||||
defaultValue={eventType.slug}
|
defaultValue={eventType.slug}
|
||||||
addOnLeading={
|
addOnLeading={
|
||||||
<>
|
<>
|
||||||
{CAL_URL?.replace(/^(https?:|)\/\//, "")}/
|
{urlPrefix}/
|
||||||
{!isManagedEventType
|
{!isManagedEventType
|
||||||
? team
|
? team
|
||||||
? "team/" + team.slug
|
? (orgBranding ? "" : "team/") + team.slug
|
||||||
: eventType.users[0].username
|
: eventType.users[0].username
|
||||||
: t("username_placeholder")}
|
: t("username_placeholder")}
|
||||||
/
|
/
|
||||||
|
|||||||
@@ -65,7 +65,7 @@ export const UsernameAvailabilityField = ({
|
|||||||
setInputUsernameValue={onChange}
|
setInputUsernameValue={onChange}
|
||||||
onSuccessMutation={onSuccessMutation}
|
onSuccessMutation={onSuccessMutation}
|
||||||
onErrorMutation={onErrorMutation}
|
onErrorMutation={onErrorMutation}
|
||||||
addOnLeading={usernamePrefix}
|
addOnLeading={`${usernamePrefix}/`}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}}
|
}}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@calcom/web",
|
"name": "@calcom/web",
|
||||||
"version": "3.2.0",
|
"version": "3.2.1",
|
||||||
"private": true,
|
"private": true,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"analyze": "ANALYZE=true next build",
|
"analyze": "ANALYZE=true next build",
|
||||||
@@ -166,6 +166,7 @@
|
|||||||
"msw": "^0.42.3",
|
"msw": "^0.42.3",
|
||||||
"postcss": "^8.4.18",
|
"postcss": "^8.4.18",
|
||||||
"tailwindcss": "^3.3.1",
|
"tailwindcss": "^3.3.1",
|
||||||
|
"tailwindcss-animate": "^1.0.6",
|
||||||
"ts-node": "^10.9.1",
|
"ts-node": "^10.9.1",
|
||||||
"typescript": "^4.9.4"
|
"typescript": "^4.9.4"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -36,6 +36,7 @@ import { ssrInit } from "@server/lib/ssr";
|
|||||||
|
|
||||||
export function UserPage(props: InferGetServerSidePropsType<typeof getServerSideProps>) {
|
export function UserPage(props: InferGetServerSidePropsType<typeof getServerSideProps>) {
|
||||||
const { users, profile, eventTypes, markdownStrippedBio, entity } = props;
|
const { users, profile, eventTypes, markdownStrippedBio, entity } = props;
|
||||||
|
|
||||||
const [user] = users; //To be used when we only have a single user, not dynamic group
|
const [user] = users; //To be used when we only have a single user, not dynamic group
|
||||||
useTheme(profile.theme);
|
useTheme(profile.theme);
|
||||||
const { t } = useLocale();
|
const { t } = useLocale();
|
||||||
@@ -82,6 +83,10 @@ export function UserPage(props: InferGetServerSidePropsType<typeof getServerSide
|
|||||||
profile: { name: `${profile.name}`, image: null },
|
profile: { name: `${profile.name}`, image: null },
|
||||||
users: [{ username: `${user.username}`, name: `${user.name}` }],
|
users: [{ username: `${user.username}`, name: `${user.name}` }],
|
||||||
}}
|
}}
|
||||||
|
nextSeoProps={{
|
||||||
|
noindex: !profile.allowSEOIndexing,
|
||||||
|
nofollow: !profile.allowSEOIndexing,
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<div className={classNames(shouldAlignCentrally ? "mx-auto" : "", isEmbed ? "max-w-3xl" : "")}>
|
<div className={classNames(shouldAlignCentrally ? "mx-auto" : "", isEmbed ? "max-w-3xl" : "")}>
|
||||||
@@ -214,6 +219,7 @@ export type UserPageProps = {
|
|||||||
theme: string | null;
|
theme: string | null;
|
||||||
brandColor: string;
|
brandColor: string;
|
||||||
darkBrandColor: string;
|
darkBrandColor: string;
|
||||||
|
allowSEOIndexing: boolean;
|
||||||
};
|
};
|
||||||
users: Pick<User, "away" | "name" | "username" | "bio" | "verified">[];
|
users: Pick<User, "away" | "name" | "username" | "bio" | "verified">[];
|
||||||
themeBasis: string | null;
|
themeBasis: string | null;
|
||||||
@@ -276,6 +282,7 @@ export const getServerSideProps: GetServerSideProps<UserPageProps> = async (cont
|
|||||||
away: true,
|
away: true,
|
||||||
verified: true,
|
verified: true,
|
||||||
allowDynamicBooking: true,
|
allowDynamicBooking: true,
|
||||||
|
allowSEOIndexing: true,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -315,6 +322,7 @@ export const getServerSideProps: GetServerSideProps<UserPageProps> = async (cont
|
|||||||
theme: user.theme,
|
theme: user.theme,
|
||||||
brandColor: user.brandColor,
|
brandColor: user.brandColor,
|
||||||
darkBrandColor: user.darkBrandColor,
|
darkBrandColor: user.darkBrandColor,
|
||||||
|
allowSEOIndexing: user.allowSEOIndexing ?? true,
|
||||||
};
|
};
|
||||||
|
|
||||||
const eventTypesWithHidden = await getEventTypesWithHiddenFromDB(user.id);
|
const eventTypesWithHidden = await getEventTypesWithHiddenFromDB(user.id);
|
||||||
|
|||||||
@@ -30,6 +30,7 @@ export default function Type({
|
|||||||
booking,
|
booking,
|
||||||
away,
|
away,
|
||||||
isBrandingHidden,
|
isBrandingHidden,
|
||||||
|
isSEOIndexable,
|
||||||
rescheduleUid,
|
rescheduleUid,
|
||||||
entity,
|
entity,
|
||||||
duration,
|
duration,
|
||||||
@@ -41,6 +42,7 @@ export default function Type({
|
|||||||
eventSlug={slug}
|
eventSlug={slug}
|
||||||
rescheduleUid={rescheduleUid ?? undefined}
|
rescheduleUid={rescheduleUid ?? undefined}
|
||||||
hideBranding={isBrandingHidden}
|
hideBranding={isBrandingHidden}
|
||||||
|
isSEOIndexable={isSEOIndexable ?? true}
|
||||||
entity={entity}
|
entity={entity}
|
||||||
/>
|
/>
|
||||||
<Booker
|
<Booker
|
||||||
@@ -128,6 +130,7 @@ async function getDynamicGroupPageProps(context: GetServerSidePropsContext) {
|
|||||||
away: false,
|
away: false,
|
||||||
trpcState: ssr.dehydrate(),
|
trpcState: ssr.dehydrate(),
|
||||||
isBrandingHidden: false,
|
isBrandingHidden: false,
|
||||||
|
isSEOIndexable: true,
|
||||||
themeBasis: null,
|
themeBasis: null,
|
||||||
bookingUid: bookingUid ? `${bookingUid}` : null,
|
bookingUid: bookingUid ? `${bookingUid}` : null,
|
||||||
rescheduleUid: rescheduleUid ? `${rescheduleUid}` : null,
|
rescheduleUid: rescheduleUid ? `${rescheduleUid}` : null,
|
||||||
@@ -154,6 +157,7 @@ async function getUserPageProps(context: GetServerSidePropsContext) {
|
|||||||
select: {
|
select: {
|
||||||
away: true,
|
away: true,
|
||||||
hideBranding: true,
|
hideBranding: true,
|
||||||
|
allowSEOIndexing: true,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -199,6 +203,7 @@ async function getUserPageProps(context: GetServerSidePropsContext) {
|
|||||||
entity: eventData.entity,
|
entity: eventData.entity,
|
||||||
trpcState: ssr.dehydrate(),
|
trpcState: ssr.dehydrate(),
|
||||||
isBrandingHidden: user?.hideBranding,
|
isBrandingHidden: user?.hideBranding,
|
||||||
|
isSEOIndexable: user?.allowSEOIndexing,
|
||||||
themeBasis: username,
|
themeBasis: username,
|
||||||
bookingUid: bookingUid ? `${bookingUid}` : null,
|
bookingUid: bookingUid ? `${bookingUid}` : null,
|
||||||
rescheduleUid: rescheduleUid ? `${rescheduleUid}` : null,
|
rescheduleUid: rescheduleUid ? `${rescheduleUid}` : null,
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
import type { NextApiRequest, NextApiResponse } from "next";
|
import type { NextApiRequest, NextApiResponse } from "next";
|
||||||
import { authenticator } from "otplib";
|
|
||||||
|
|
||||||
import { ErrorCode } from "@calcom/features/auth/lib/ErrorCode";
|
import { ErrorCode } from "@calcom/features/auth/lib/ErrorCode";
|
||||||
import { getServerSession } from "@calcom/features/auth/lib/getServerSession";
|
import { getServerSession } from "@calcom/features/auth/lib/getServerSession";
|
||||||
import { verifyPassword } from "@calcom/features/auth/lib/verifyPassword";
|
import { verifyPassword } from "@calcom/features/auth/lib/verifyPassword";
|
||||||
import { symmetricDecrypt } from "@calcom/lib/crypto";
|
import { symmetricDecrypt } from "@calcom/lib/crypto";
|
||||||
|
import { totpAuthenticatorCheck } from "@calcom/lib/totp";
|
||||||
import prisma from "@calcom/prisma";
|
import prisma from "@calcom/prisma";
|
||||||
import { IdentityProvider } from "@calcom/prisma/client";
|
import { IdentityProvider } from "@calcom/prisma/client";
|
||||||
|
|
||||||
@@ -69,7 +69,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
|
|||||||
}
|
}
|
||||||
|
|
||||||
// If user has 2fa enabled, check if body.code is correct
|
// If user has 2fa enabled, check if body.code is correct
|
||||||
const isValidToken = authenticator.check(req.body.code, secret);
|
const isValidToken = totpAuthenticatorCheck(req.body.code, secret);
|
||||||
if (!isValidToken) {
|
if (!isValidToken) {
|
||||||
return res.status(400).json({ error: ErrorCode.IncorrectTwoFactorCode });
|
return res.status(400).json({ error: ErrorCode.IncorrectTwoFactorCode });
|
||||||
|
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
import type { NextApiRequest, NextApiResponse } from "next";
|
import type { NextApiRequest, NextApiResponse } from "next";
|
||||||
import { authenticator } from "otplib";
|
|
||||||
|
|
||||||
import { ErrorCode } from "@calcom/features/auth/lib/ErrorCode";
|
import { ErrorCode } from "@calcom/features/auth/lib/ErrorCode";
|
||||||
import { getServerSession } from "@calcom/features/auth/lib/getServerSession";
|
import { getServerSession } from "@calcom/features/auth/lib/getServerSession";
|
||||||
import { symmetricDecrypt } from "@calcom/lib/crypto";
|
import { symmetricDecrypt } from "@calcom/lib/crypto";
|
||||||
|
import { totpAuthenticatorCheck } from "@calcom/lib/totp";
|
||||||
import prisma from "@calcom/prisma";
|
import prisma from "@calcom/prisma";
|
||||||
|
|
||||||
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
|
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
|
||||||
@@ -48,7 +48,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
|
|||||||
return res.status(500).json({ error: ErrorCode.InternalServerError });
|
return res.status(500).json({ error: ErrorCode.InternalServerError });
|
||||||
}
|
}
|
||||||
|
|
||||||
const isValidToken = authenticator.check(req.body.code, secret);
|
const isValidToken = totpAuthenticatorCheck(req.body.code, secret);
|
||||||
if (!isValidToken) {
|
if (!isValidToken) {
|
||||||
return res.status(400).json({ error: ErrorCode.IncorrectTwoFactorCode });
|
return res.status(400).json({ error: ErrorCode.IncorrectTwoFactorCode });
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -40,9 +40,11 @@ export default function Apps({ apps }: InferGetStaticPropsType<typeof getStaticP
|
|||||||
}>
|
}>
|
||||||
<div className="mb-16">
|
<div className="mb-16">
|
||||||
<div className="grid-col-1 grid grid-cols-1 gap-3 md:grid-cols-3">
|
<div className="grid-col-1 grid grid-cols-1 gap-3 md:grid-cols-3">
|
||||||
{apps.map((app) => {
|
{apps
|
||||||
return <AppCard key={app.slug} app={app} />;
|
.sort((a, b) => (b.installCount || 0) - (a.installCount || 0))
|
||||||
})}
|
.map((app) => {
|
||||||
|
return <AppCard key={app.slug} app={app} />;
|
||||||
|
})}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</Shell>
|
</Shell>
|
||||||
|
|||||||
@@ -23,12 +23,9 @@ import {
|
|||||||
useIsEmbed,
|
useIsEmbed,
|
||||||
} from "@calcom/embed-core/embed-iframe";
|
} from "@calcom/embed-core/embed-iframe";
|
||||||
import { getServerSession } from "@calcom/features/auth/lib/getServerSession";
|
import { getServerSession } from "@calcom/features/auth/lib/getServerSession";
|
||||||
import { SystemField } from "@calcom/features/bookings/lib/SystemField";
|
import { SMS_REMINDER_NUMBER_FIELD, SystemField } from "@calcom/features/bookings/lib/SystemField";
|
||||||
import { getBookingWithResponses } from "@calcom/features/bookings/lib/get-booking";
|
import { getBookingWithResponses } from "@calcom/features/bookings/lib/get-booking";
|
||||||
import {
|
import { getBookingFieldsWithSystemFields } from "@calcom/features/bookings/lib/getBookingFields";
|
||||||
getBookingFieldsWithSystemFields,
|
|
||||||
SMS_REMINDER_NUMBER_FIELD,
|
|
||||||
} from "@calcom/features/bookings/lib/getBookingFields";
|
|
||||||
import { parseRecurringEvent } from "@calcom/lib";
|
import { parseRecurringEvent } from "@calcom/lib";
|
||||||
import { APP_NAME } from "@calcom/lib/constants";
|
import { APP_NAME } from "@calcom/lib/constants";
|
||||||
import {
|
import {
|
||||||
@@ -96,7 +93,7 @@ const querySchema = z.object({
|
|||||||
});
|
});
|
||||||
|
|
||||||
export default function Success(props: SuccessProps) {
|
export default function Success(props: SuccessProps) {
|
||||||
const { t } = useLocale();
|
const { t, i18n } = useLocale();
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const routerQuery = useRouterQuery();
|
const routerQuery = useRouterQuery();
|
||||||
const pathname = usePathname();
|
const pathname = usePathname();
|
||||||
@@ -113,7 +110,8 @@ export default function Success(props: SuccessProps) {
|
|||||||
const attendeeTimeZone = props?.bookingInfo?.attendees.find(
|
const attendeeTimeZone = props?.bookingInfo?.attendees.find(
|
||||||
(attendee) => attendee.email === email
|
(attendee) => attendee.email === email
|
||||||
)?.timeZone;
|
)?.timeZone;
|
||||||
const tz = isSuccessBookingPage && attendeeTimeZone ? attendeeTimeZone : props.tz ? props.tz : timeZone();
|
|
||||||
|
const tz = props.tz ? props.tz : isSuccessBookingPage && attendeeTimeZone ? attendeeTimeZone : timeZone();
|
||||||
|
|
||||||
const location = props.bookingInfo.location as ReturnType<typeof getEventLocationValue>;
|
const location = props.bookingInfo.location as ReturnType<typeof getEventLocationValue>;
|
||||||
|
|
||||||
@@ -480,6 +478,21 @@ export default function Success(props: SuccessProps) {
|
|||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
{props.paymentStatus && (
|
||||||
|
<>
|
||||||
|
<div className="mt-3 font-medium">
|
||||||
|
{props.paymentStatus.paymentOption === "HOLD"
|
||||||
|
? t("complete_your_booking")
|
||||||
|
: t("payment")}
|
||||||
|
</div>
|
||||||
|
<div className="col-span-2 mb-2 mt-3">
|
||||||
|
{new Intl.NumberFormat(i18n.language, {
|
||||||
|
style: "currency",
|
||||||
|
currency: props.paymentStatus.currency,
|
||||||
|
}).format(props.paymentStatus.amount / 100.0)}
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
{bookingInfo?.description && (
|
{bookingInfo?.description && (
|
||||||
<>
|
<>
|
||||||
<div className="mt-9 font-medium">{t("additional_notes")}</div>
|
<div className="mt-9 font-medium">{t("additional_notes")}</div>
|
||||||
@@ -1117,6 +1130,9 @@ export async function getServerSideProps(context: GetServerSidePropsContext) {
|
|||||||
select: {
|
select: {
|
||||||
success: true,
|
success: true,
|
||||||
refunded: true,
|
refunded: true,
|
||||||
|
currency: true,
|
||||||
|
amount: true,
|
||||||
|
paymentOption: true,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -737,7 +737,9 @@ const EventTypeListHeading = ({
|
|||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
{profile?.slug && (
|
{profile?.slug && (
|
||||||
<Link href={`${CAL_URL}/${profile.slug}`} className="text-subtle block text-xs">
|
<Link
|
||||||
|
href={`${orgBranding ? orgBranding.fullDomain : CAL_URL}/${profile.slug}`}
|
||||||
|
className="text-subtle block text-xs">
|
||||||
{`${bookerUrl.replace("https://", "").replace("http://", "")}/${profile.slug}`}
|
{`${bookerUrl.replace("https://", "").replace("http://", "")}/${profile.slug}`}
|
||||||
</Link>
|
</Link>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -116,6 +116,7 @@ const GeneralView = ({ localeProp, user }: GeneralViewProps) => {
|
|||||||
label: nameOfDay(localeProp, user.weekStart === "Sunday" ? 0 : 1),
|
label: nameOfDay(localeProp, user.weekStart === "Sunday" ? 0 : 1),
|
||||||
},
|
},
|
||||||
allowDynamicBooking: user.allowDynamicBooking ?? true,
|
allowDynamicBooking: user.allowDynamicBooking ?? true,
|
||||||
|
allowSEOIndexing: user.allowSEOIndexing ?? true,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
const {
|
const {
|
||||||
@@ -124,6 +125,7 @@ const GeneralView = ({ localeProp, user }: GeneralViewProps) => {
|
|||||||
getValues,
|
getValues,
|
||||||
} = formMethods;
|
} = formMethods;
|
||||||
const isDisabled = isSubmitting || !isDirty;
|
const isDisabled = isSubmitting || !isDirty;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Form
|
<Form
|
||||||
form={formMethods}
|
form={formMethods}
|
||||||
@@ -225,6 +227,24 @@ const GeneralView = ({ localeProp, user }: GeneralViewProps) => {
|
|||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div className="mt-8">
|
||||||
|
<Controller
|
||||||
|
name="allowSEOIndexing"
|
||||||
|
control={formMethods.control}
|
||||||
|
render={() => (
|
||||||
|
<SettingsToggle
|
||||||
|
title={t("seo_indexing")}
|
||||||
|
description={t("allow_seo_indexing")}
|
||||||
|
checked={formMethods.getValues("allowSEOIndexing")}
|
||||||
|
onCheckedChange={(checked) => {
|
||||||
|
formMethods.setValue("allowSEOIndexing", checked, { shouldDirty: true });
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
<Button
|
<Button
|
||||||
loading={mutation.isLoading}
|
loading={mutation.isLoading}
|
||||||
disabled={isDisabled}
|
disabled={isDisabled}
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { expect } from "@playwright/test";
|
import { expect } from "@playwright/test";
|
||||||
|
|
||||||
import { test } from "./lib/fixtures";
|
import { test } from "./lib/fixtures";
|
||||||
|
import { installAppleCalendar } from "./lib/testUtils";
|
||||||
|
|
||||||
test.describe.configure({ mode: "parallel" });
|
test.describe.configure({ mode: "parallel" });
|
||||||
|
|
||||||
@@ -10,10 +11,9 @@ test.describe("App Store - Authed", () => {
|
|||||||
test("Browse apple-calendar and try to install", async ({ page, users }) => {
|
test("Browse apple-calendar and try to install", async ({ page, users }) => {
|
||||||
const pro = await users.create();
|
const pro = await users.create();
|
||||||
await pro.apiLogin();
|
await pro.apiLogin();
|
||||||
await page.goto("/apps/categories/calendar");
|
|
||||||
await page.click('[data-testid="app-store-app-card-apple-calendar"]');
|
await installAppleCalendar(page);
|
||||||
await page.waitForURL("/apps/apple-calendar");
|
|
||||||
await page.click('[data-testid="install-app-button"]');
|
|
||||||
await expect(page.locator(`text=Connect to Apple Server`)).toBeVisible();
|
await expect(page.locator(`text=Connect to Apple Server`)).toBeVisible();
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -31,9 +31,8 @@ test.describe("App Store - Authed", () => {
|
|||||||
|
|
||||||
test.describe("App Store - Unauthed", () => {
|
test.describe("App Store - Unauthed", () => {
|
||||||
test("Browse apple-calendar and try to install", async ({ page }) => {
|
test("Browse apple-calendar and try to install", async ({ page }) => {
|
||||||
await page.goto("/apps/categories/calendar");
|
await installAppleCalendar(page);
|
||||||
await page.click('[data-testid="app-store-app-card-apple-calendar"]');
|
|
||||||
await page.click('[data-testid="install-app-button"]');
|
|
||||||
await expect(page.locator(`[data-testid="login-form"]`)).toBeVisible();
|
await expect(page.locator(`[data-testid="login-form"]`)).toBeVisible();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
/* eslint-disable @typescript-eslint/no-non-null-assertion */
|
||||||
|
import { expect } from "@playwright/test";
|
||||||
|
|
||||||
|
import { test } from "./lib/fixtures";
|
||||||
|
import { installAppleCalendar } from "./lib/testUtils";
|
||||||
|
|
||||||
|
test.describe.configure({ mode: "parallel" });
|
||||||
|
|
||||||
|
test.afterEach(({ users }) => users.deleteAll());
|
||||||
|
|
||||||
|
const APPLE_CALENDAR_EMAIL = process.env.E2E_TEST_APPLE_CALENDAR_EMAIL!;
|
||||||
|
const APPLE_CALENDAR_PASSWORD = process.env.E2E_TEST_APPLE_CALENDAR_PASSWORD!;
|
||||||
|
|
||||||
|
const SHOULD_SKIP_TESTS = !APPLE_CALENDAR_EMAIL || !APPLE_CALENDAR_PASSWORD;
|
||||||
|
|
||||||
|
test.describe("Apple Calendar", () => {
|
||||||
|
// eslint-disable-next-line playwright/no-skipped-test
|
||||||
|
test.skip(SHOULD_SKIP_TESTS, "Skipping due to missing the testing credentials");
|
||||||
|
|
||||||
|
test("Should be able to install and login on Apple Calendar", async ({ page, users }) => {
|
||||||
|
const user = await users.create();
|
||||||
|
await user.apiLogin();
|
||||||
|
|
||||||
|
await installAppleCalendar(page);
|
||||||
|
|
||||||
|
await expect(page.locator('[data-testid="apple-calendar-form"]')).toBeVisible();
|
||||||
|
|
||||||
|
await page.fill('[data-testid="apple-calendar-email"]', APPLE_CALENDAR_EMAIL);
|
||||||
|
await page.fill('[data-testid="apple-calendar-password"]', APPLE_CALENDAR_PASSWORD);
|
||||||
|
await page.click('[data-testid="apple-calendar-login-button"]');
|
||||||
|
|
||||||
|
await expect(page.getByText("Apple Calendar")).toBeVisible();
|
||||||
|
await expect(page.getByText(APPLE_CALENDAR_EMAIL)).toBeVisible();
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -183,3 +183,10 @@ export async function gotoRoutingLink({
|
|||||||
// HACK: There seems to be some issue with the inputs to the form getting reset if we don't wait.
|
// HACK: There seems to be some issue with the inputs to the form getting reset if we don't wait.
|
||||||
await new Promise((resolve) => setTimeout(resolve, 1000));
|
await new Promise((resolve) => setTimeout(resolve, 1000));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function installAppleCalendar(page: Page) {
|
||||||
|
await page.goto("/apps/categories/calendar");
|
||||||
|
await page.click('[data-testid="app-store-app-card-apple-calendar"]');
|
||||||
|
await page.waitForURL("/apps/apple-calendar");
|
||||||
|
await page.click('[data-testid="install-app-button"]');
|
||||||
|
}
|
||||||
@@ -3,6 +3,7 @@ import { expect } from "@playwright/test";
|
|||||||
import { authenticator } from "otplib";
|
import { authenticator } from "otplib";
|
||||||
|
|
||||||
import { symmetricDecrypt } from "@calcom/lib/crypto";
|
import { symmetricDecrypt } from "@calcom/lib/crypto";
|
||||||
|
import { totpAuthenticatorCheck } from "@calcom/lib/totp";
|
||||||
|
|
||||||
import { test } from "./lib/fixtures";
|
import { test } from "./lib/fixtures";
|
||||||
|
|
||||||
@@ -141,7 +142,7 @@ test.describe("2FA Tests", async () => {
|
|||||||
|
|
||||||
async function fillOtp({ page, secret, noRetry }: { page: Page; secret: string; noRetry?: boolean }) {
|
async function fillOtp({ page, secret, noRetry }: { page: Page; secret: string; noRetry?: boolean }) {
|
||||||
let token = authenticator.generate(secret);
|
let token = authenticator.generate(secret);
|
||||||
if (!noRetry && !authenticator.check(token, secret)) {
|
if (!noRetry && !totpAuthenticatorCheck(token, secret)) {
|
||||||
console.log("Token expired, Renerating.");
|
console.log("Token expired, Renerating.");
|
||||||
// Maybe token was just about to expire, try again just once more
|
// Maybe token was just about to expire, try again just once more
|
||||||
token = authenticator.generate(secret);
|
token = authenticator.generate(secret);
|
||||||
|
|||||||
@@ -137,7 +137,7 @@
|
|||||||
"upgrade_banner_action": "Hier aufrüsten",
|
"upgrade_banner_action": "Hier aufrüsten",
|
||||||
"team_upgraded_successfully": "Das Update für Ihr Team war erfolgreich!",
|
"team_upgraded_successfully": "Das Update für Ihr Team war erfolgreich!",
|
||||||
"org_upgrade_banner_description": "Vielen Dank, dass Sie unseren neuen Organisationsplan getestet haben. Wir haben festgestellt, dass Ihre Organisation „{{teamName}}“ ein Upgrade benötigt.",
|
"org_upgrade_banner_description": "Vielen Dank, dass Sie unseren neuen Organisationsplan getestet haben. Wir haben festgestellt, dass Ihre Organisation „{{teamName}}“ ein Upgrade benötigt.",
|
||||||
"org_upgraded_successfully": "Das Update für Ihre Organisation war erfolgreich!",
|
"org_upgraded_successfully": "Das Update für Ihre Organization war erfolgreich!",
|
||||||
"use_link_to_reset_password": "Benutzen Sie den Link, um Ihr Passwort zurückzusetzen",
|
"use_link_to_reset_password": "Benutzen Sie den Link, um Ihr Passwort zurückzusetzen",
|
||||||
"hey_there": "Hallo,",
|
"hey_there": "Hallo,",
|
||||||
"forgot_your_password_calcom": "Passwort vergessen? - {{appName}}",
|
"forgot_your_password_calcom": "Passwort vergessen? - {{appName}}",
|
||||||
@@ -322,7 +322,7 @@
|
|||||||
"bookerlayout_user_settings_title": "Buchungslayout",
|
"bookerlayout_user_settings_title": "Buchungslayout",
|
||||||
"bookerlayout_user_settings_description": "Sie können mehrere auswählen und Bucher können die Ansicht wechseln. Dies kann pro Termin überschrieben werden.",
|
"bookerlayout_user_settings_description": "Sie können mehrere auswählen und Bucher können die Ansicht wechseln. Dies kann pro Termin überschrieben werden.",
|
||||||
"bookerlayout_month_view": "Monat",
|
"bookerlayout_month_view": "Monat",
|
||||||
"bookerlayout_week_view": "Woche",
|
"bookerlayout_week_view": "Wöchentlich",
|
||||||
"bookerlayout_column_view": "Spalte",
|
"bookerlayout_column_view": "Spalte",
|
||||||
"bookerlayout_error_min_one_enabled": "Es muss mindestens ein Layout aktiviert sein.",
|
"bookerlayout_error_min_one_enabled": "Es muss mindestens ein Layout aktiviert sein.",
|
||||||
"bookerlayout_error_default_not_enabled": "Das Layout, das Sie als Standardansicht ausgewählt haben, ist nicht Teil der aktivierten Layouts.",
|
"bookerlayout_error_default_not_enabled": "Das Layout, das Sie als Standardansicht ausgewählt haben, ist nicht Teil der aktivierten Layouts.",
|
||||||
@@ -404,6 +404,8 @@
|
|||||||
"allow_dynamic_booking_tooltip": "Gruppenbuchungslinks, die dynamisch durch Hinzufügen mehrerer Benutzernamen mit einem '+' erstellt werden können. Beispiel: '{{appName}}/bailey+peer'",
|
"allow_dynamic_booking_tooltip": "Gruppenbuchungslinks, die dynamisch durch Hinzufügen mehrerer Benutzernamen mit einem '+' erstellt werden können. Beispiel: '{{appName}}/bailey+peer'",
|
||||||
"allow_dynamic_booking": "Ermöglichen Sie es den Teilnehmenden, Sie über dynamische Gruppenbuchungen zu buchen",
|
"allow_dynamic_booking": "Ermöglichen Sie es den Teilnehmenden, Sie über dynamische Gruppenbuchungen zu buchen",
|
||||||
"dynamic_booking": "Dynamische Gruppenlinks",
|
"dynamic_booking": "Dynamische Gruppenlinks",
|
||||||
|
"allow_seo_indexing": "Suchmaschinen erlauben, auf Ihre öffentlichen Inhalte zuzugreifen",
|
||||||
|
"seo_indexing": "Suchmaschinen-Indexierung erlauben",
|
||||||
"email": "E-Mail",
|
"email": "E-Mail",
|
||||||
"email_placeholder": "max@example.com",
|
"email_placeholder": "max@example.com",
|
||||||
"full_name": "Vollständiger Name",
|
"full_name": "Vollständiger Name",
|
||||||
@@ -565,7 +567,7 @@
|
|||||||
"team_description": "Ein paar Sätze über Ihr Team auf der öffentlichen Teamseite.",
|
"team_description": "Ein paar Sätze über Ihr Team auf der öffentlichen Teamseite.",
|
||||||
"org_description": "Ein paar Sätze zu Ihrer Organisation. Dies wird auf der URL-Seite Ihrer Organisation erscheinen.",
|
"org_description": "Ein paar Sätze zu Ihrer Organisation. Dies wird auf der URL-Seite Ihrer Organisation erscheinen.",
|
||||||
"members": "Mitglieder",
|
"members": "Mitglieder",
|
||||||
"organization_members": "Organisationsmitglieder",
|
"organization_members": "Mitglieder der Organization",
|
||||||
"member": "Mitglied",
|
"member": "Mitglied",
|
||||||
"number_member_one": "{{count}} Mitglied",
|
"number_member_one": "{{count}} Mitglied",
|
||||||
"number_member_other": "{{count}} Mitglieder",
|
"number_member_other": "{{count}} Mitglieder",
|
||||||
@@ -600,7 +602,7 @@
|
|||||||
"confirm_all": "Alle bestätigen",
|
"confirm_all": "Alle bestätigen",
|
||||||
"disband_team": "Team auflösen",
|
"disband_team": "Team auflösen",
|
||||||
"disband_team_confirmation_message": "Bist du sicher, dass du dieses Team auflösen möchtest? Jeder der diesen Team-Link erhalten hat, kann Sie nicht mehr buchen.",
|
"disband_team_confirmation_message": "Bist du sicher, dass du dieses Team auflösen möchtest? Jeder der diesen Team-Link erhalten hat, kann Sie nicht mehr buchen.",
|
||||||
"disband_org": "Organisation auflösen",
|
"disband_org": "Organization auflösen",
|
||||||
"disband_org_confirmation_message": "Sind Sie sicher, dass Sie diese Organisation auflösen möchten? Alle Teams und Mitglieder werden gelöscht.",
|
"disband_org_confirmation_message": "Sind Sie sicher, dass Sie diese Organisation auflösen möchten? Alle Teams und Mitglieder werden gelöscht.",
|
||||||
"remove_member_confirmation_message": "Sind Sie sicher, dass Sie dieses Mitglied aus dem Team entfernen möchten?",
|
"remove_member_confirmation_message": "Sind Sie sicher, dass Sie dieses Mitglied aus dem Team entfernen möchten?",
|
||||||
"confirm_disband_team": "Ja, Team auflösen",
|
"confirm_disband_team": "Ja, Team auflösen",
|
||||||
@@ -713,7 +715,7 @@
|
|||||||
"create_team_to_get_started": "Erstellen Sie ein Team um loszulegen",
|
"create_team_to_get_started": "Erstellen Sie ein Team um loszulegen",
|
||||||
"teams": "Teams",
|
"teams": "Teams",
|
||||||
"team": "Team",
|
"team": "Team",
|
||||||
"organization": "Organisation",
|
"organization": "Organization",
|
||||||
"team_billing": "Abrechnung der Teams",
|
"team_billing": "Abrechnung der Teams",
|
||||||
"team_billing_description": "Verwalte die Abrechungen deines Teams",
|
"team_billing_description": "Verwalte die Abrechungen deines Teams",
|
||||||
"upgrade_to_flexible_pro_title": "Wir haben die Abrechnung für Teams geändert",
|
"upgrade_to_flexible_pro_title": "Wir haben die Abrechnung für Teams geändert",
|
||||||
@@ -1351,9 +1353,9 @@
|
|||||||
"event_one": "Ereignis",
|
"event_one": "Ereignis",
|
||||||
"event_other": "Ereignisse",
|
"event_other": "Ereignisse",
|
||||||
"profile_team_description": "Einstellungen für Ihr Teamprofil verwalten",
|
"profile_team_description": "Einstellungen für Ihr Teamprofil verwalten",
|
||||||
"profile_org_description": "Einstellungen für Ihr Organisationsprofil verwalten",
|
"profile_org_description": "Einstellungen für Ihr Organization-Profil verwalten",
|
||||||
"members_team_description": "Benutzer in der Gruppe",
|
"members_team_description": "Benutzer in der Gruppe",
|
||||||
"organization_description": "Administratoren und Mitglieder in Ihrer Organisation verwalten",
|
"organization_description": "Administratoren und Mitglieder in Ihrer Organization verwalten",
|
||||||
"team_url": "Team-URL",
|
"team_url": "Team-URL",
|
||||||
"team_members": "Teammitglieder",
|
"team_members": "Teammitglieder",
|
||||||
"more": "Mehr",
|
"more": "Mehr",
|
||||||
@@ -1499,7 +1501,7 @@
|
|||||||
"yearly": "Jährlich",
|
"yearly": "Jährlich",
|
||||||
"checkout": "Kasse",
|
"checkout": "Kasse",
|
||||||
"your_team_disbanded_successfully": "Ihr Team wurde erfolgreich aufgelöst",
|
"your_team_disbanded_successfully": "Ihr Team wurde erfolgreich aufgelöst",
|
||||||
"your_org_disbanded_successfully": "Ihre Organisation wurde erfolgreich aufgelöst",
|
"your_org_disbanded_successfully": "Ihre Organization wurde erfolgreich aufgelöst",
|
||||||
"error_creating_team": "Fehler beim Erstellen des Teams",
|
"error_creating_team": "Fehler beim Erstellen des Teams",
|
||||||
"you": "Sie",
|
"you": "Sie",
|
||||||
"resend_email": "E-Mail erneut senden",
|
"resend_email": "E-Mail erneut senden",
|
||||||
@@ -1896,34 +1898,35 @@
|
|||||||
"first_event_type_webhook_description": "Erstellen Sie Ihren ersten Webhook für diesen Termintypen",
|
"first_event_type_webhook_description": "Erstellen Sie Ihren ersten Webhook für diesen Termintypen",
|
||||||
"install_app_on": "App installieren auf",
|
"install_app_on": "App installieren auf",
|
||||||
"create_for": "Erstellen für",
|
"create_for": "Erstellen für",
|
||||||
"setup_organization": "Eine Organisation einrichten",
|
"currency": "Währung",
|
||||||
"organization_banner_description": "Schaffen Sie eine Umgebung, in der Teams gemeinsame Apps, Workflows und Termintypen mit Round Robin und kollektiver Terminplanung erstellen können.",
|
"setup_organization": "Eine Organization einrichten",
|
||||||
"organization_banner_title": "Verwalten Sie Organisationen mit mehreren Teams",
|
"organization_banner_description": "Schaffen Sie Umgebungen, in der Teams gemeinsame Apps, Workflows und Termintypen mit Round Robin und kollektiver Terminplanung erstellen können.",
|
||||||
"set_up_your_organization": "Ihre Organisation einrichten",
|
"organization_banner_title": "Verwalten Sie Organizations mit mehreren Teams",
|
||||||
"organizations_description": "Organisationen sind geteilte Umgebungen, in denen Teams gemeinsame Termintypen, Apps, Workflows und mehr erstellen können.",
|
"set_up_your_organization": "Ihre Organization einrichten",
|
||||||
"must_enter_organization_name": "Es muss ein Organisationsname eingegeben werden",
|
"organizations_description": "Organizations sind geteilte Umgebungen, in denen Teams gemeinsame Termintypen, Apps, Workflows und mehr erstellen können.",
|
||||||
"must_enter_organization_admin_email": "Sie müssen Ihre Organisations-Mailadresse eingeben",
|
"must_enter_organization_name": "Es muss ein Name für die Organization eingegeben werden",
|
||||||
"admin_email": "Ihre Organisations-E-Mail-Adresse",
|
"must_enter_organization_admin_email": "Sie müssen Ihre Organization-Mailadresse eingeben\n",
|
||||||
|
"admin_email": "Ihre Organization-E-Mail-Adresse",
|
||||||
"admin_username": "Benutzername des Administrators",
|
"admin_username": "Benutzername des Administrators",
|
||||||
"organization_name": "Name der Organisation",
|
"organization_name": "Name der Organization",
|
||||||
"organization_url": "URL der Organisation",
|
"organization_url": "URL der Organization ",
|
||||||
"organization_verify_header": "Bestätigen Sie Ihre Organisations-Mailadresse",
|
"organization_verify_header": "Bestätigen Sie Ihre Organization-Mailadresse",
|
||||||
"organization_verify_email_body": "Bitte verwenden Sie den folgenden Code, um Ihre E-Mail-Adresse zu bestätigen, um die Einrichtung Ihrer Organisation fortzusetzen.",
|
"organization_verify_email_body": "Bitte verwenden Sie den folgenden Code, um Ihre E-Mail-Adresse zu bestätigen, um die Einrichtung Ihrer Organization fortzusetzen.",
|
||||||
"additional_url_parameters": "Zusätzliche URL-Parameter",
|
"additional_url_parameters": "Zusätzliche URL-Parameter",
|
||||||
"about_your_organization": "Über Ihre Organisation",
|
"about_your_organization": "Über Ihre Organization",
|
||||||
"about_your_organization_description": "Organisationen sind geteilte Umgebungen, in denen Sie mehrere Teams mit gemeinsamen Mitgliedern, Termintypen, Apps, Workflows und mehr erstellen können.",
|
"about_your_organization_description": "Organizations sind geteilte Umgebungen, in denen Sie mehrere Teams mit gemeinsamen Mitgliedern, Termintypen, Apps, Workflows und mehr erstellen können.\n",
|
||||||
"create_your_teams": "Erstellen Sie Ihre Teams",
|
"create_your_teams": "Erstellen Sie Ihre Teams",
|
||||||
"create_your_teams_description": "Fangen Sie an, gemeinsam zu planen, indem Sie Ihre Teammitglieder zu Ihrer Organisation hinzufügen",
|
"create_your_teams_description": "Fangen Sie an, gemeinsam zu planen, indem Sie Ihre Teammitglieder zu Ihrer Organization hinzufügen",
|
||||||
"invite_organization_admins": "Laden Sie die Administratoren Ihrer Organisation ein",
|
"invite_organization_admins": "Laden Sie die Administratoren Ihrer Organization ein",
|
||||||
"invite_organization_admins_description": "Diese Administratoren haben Zugriff auf alle Teams in Ihrer Organisation. Sie können Team-Administratoren und -mitglieder später hinzufügen.",
|
"invite_organization_admins_description": "Diese Administratoren haben Zugriff auf alle Teams in Ihrer Organization. Sie können Team-Administratoren und -mitglieder später hinzufügen.",
|
||||||
"set_a_password": "Legen Sie ein Passwort fest",
|
"set_a_password": "Legen Sie ein Passwort fest",
|
||||||
"set_a_password_description": "Dies wird ein neues Benutzerkonto mit Ihrer Organisations-Mailadresse und diesem Passwort erstellen.",
|
"set_a_password_description": "Dies wird ein neues Benutzerkonto mit Ihrer Organization-Mailadresse und diesem Passwort erstellen.",
|
||||||
"organization_logo": "Logo der Organisation",
|
"organization_logo": "Logo der Organization",
|
||||||
"organization_about_description": "Ein paar Sätze zu Ihrer Organisation. Dies wird auf der öffentlichen Profilseite Ihrer Organisation erscheinen.",
|
"organization_about_description": "Ein paar Sätze zu Ihrer Organization. Dies wird auf der öffentlichen Profilseite Ihrer Organization erscheinen.",
|
||||||
"ill_do_this_later": "Ich werde das später tun",
|
"ill_do_this_later": "Ich werde das später tun",
|
||||||
"verify_your_email": "Bestätigen Sie Ihre E-Mail",
|
"verify_your_email": "Bestätigen Sie Ihre E-Mail",
|
||||||
"enter_digit_code": "Geben Sie den 6-stelligen Code ein, den wir an {{email}} gesendet haben",
|
"enter_digit_code": "Geben Sie den 6-stelligen Code ein, den wir an {{email}} gesendet haben",
|
||||||
"verify_email_organization": "Bestätigen Sie Ihre E-Mail, um eine Organisation zu erstellen",
|
"verify_email_organization": "Bestätigen Sie Ihre E-Mail, um eine Organization zu erstellen",
|
||||||
"code_provided_invalid": "Der eingegebene Code ist ungültig, versuchen Sie es erneut",
|
"code_provided_invalid": "Der eingegebene Code ist ungültig, versuchen Sie es erneut",
|
||||||
"email_already_used": "Die E-Mail-Adresse wird bereits verwendet",
|
"email_already_used": "Die E-Mail-Adresse wird bereits verwendet",
|
||||||
"organization_admin_invited_heading": "Sie wurden eingeladen, ein Mitglied von {{orgName}} zu werden",
|
"organization_admin_invited_heading": "Sie wurden eingeladen, ein Mitglied von {{orgName}} zu werden",
|
||||||
@@ -1931,8 +1934,8 @@
|
|||||||
"duplicated_slugs_warning": "Die folgenden Teams konnten aufgrund von doppelten Slugs nicht erstellt werden: {{slugs}}",
|
"duplicated_slugs_warning": "Die folgenden Teams konnten aufgrund von doppelten Slugs nicht erstellt werden: {{slugs}}",
|
||||||
"team_names_empty": "Teamnamen dürfen nicht leer sein",
|
"team_names_empty": "Teamnamen dürfen nicht leer sein",
|
||||||
"team_names_repeated": "Teamnamen dürfen nicht wiederholt vorkommen",
|
"team_names_repeated": "Teamnamen dürfen nicht wiederholt vorkommen",
|
||||||
"user_belongs_organization": "Der Benutzer gehört einer Organisation an",
|
"user_belongs_organization": "Der Benutzer gehört einer Organization an",
|
||||||
"org_no_teams_yet": "Diese Organisation hat noch keine Teams",
|
"org_no_teams_yet": "Diese Organization hat noch keine Teams",
|
||||||
"org_no_teams_yet_description": "Falls Sie ein Administrator sind, sollten Sie unbedingt Teams erstellen, welche hier angezeigt werden können.",
|
"org_no_teams_yet_description": "Falls Sie ein Administrator sind, sollten Sie unbedingt Teams erstellen, welche hier angezeigt werden können.",
|
||||||
"set_up": "Einrichten",
|
"set_up": "Einrichten",
|
||||||
"set_up_your_profile": "Richten Sie Ihr Profil ein",
|
"set_up_your_profile": "Richten Sie Ihr Profil ein",
|
||||||
@@ -1942,16 +1945,16 @@
|
|||||||
"crm": "CRM",
|
"crm": "CRM",
|
||||||
"messaging": "Nachrichten",
|
"messaging": "Nachrichten",
|
||||||
"sender_id_info": "Name oder Nummer, welcher bzw. welche als Absender einer SMS angezeigt wird (einige Länder erlauben keine alphanumerischen Absender-IDs)",
|
"sender_id_info": "Name oder Nummer, welcher bzw. welche als Absender einer SMS angezeigt wird (einige Länder erlauben keine alphanumerischen Absender-IDs)",
|
||||||
"org_admins_can_create_new_teams": "Nur der Administrator Ihrer Organisation kann neue Teams erstellen",
|
"org_admins_can_create_new_teams": "Nur der Administrator Ihrer Organization kann neue Teams erstellen",
|
||||||
"google_new_spam_policy": "Die neuen Spam-Richtlinien von Google könnten verhindern, dass Sie E-Mail- und Kalenderbenachrichtigungen zu dieser Buchung erhalten.",
|
"google_new_spam_policy": "Die neuen Spam-Richtlinien von Google könnten verhindern, dass Sie E-Mail- und Kalenderbenachrichtigungen zu dieser Buchung erhalten.",
|
||||||
"resolve": "Beheben",
|
"resolve": "Beheben",
|
||||||
"no_organization_slug": "Beim Erstellen von Teams für diese Organisation ist ein Fehler aufgetreten. URL Slug fehlt.",
|
"no_organization_slug": "Beim Erstellen von Teams für diese Organization ist ein Fehler aufgetreten. URL Slug fehlt.",
|
||||||
"org_name": "Name der Organisation",
|
"org_name": "Name der Organization",
|
||||||
"org_url": "URL der Organisation",
|
"org_url": "URL der Organization",
|
||||||
"copy_link_org": "Link zur Organisation kopieren",
|
"copy_link_org": "Link zur Organization kopieren",
|
||||||
"404_the_org": "Die Organisation",
|
"404_the_org": "Die Organization",
|
||||||
"404_the_team": "Das Team",
|
"404_the_team": "Das Team",
|
||||||
"404_claim_entity_org": "Beanspruchen Sie Ihre Subdomain für Ihre Organisation",
|
"404_claim_entity_org": "Beanspruchen Sie Ihre Subdomain für Ihre Organization",
|
||||||
"404_claim_entity_team": "Dieses Team beanspruchen und gemeinsam mit der Verwaltung von Verfügbarkeitsplänen beginnen",
|
"404_claim_entity_team": "Dieses Team beanspruchen und gemeinsam mit der Verwaltung von Verfügbarkeitsplänen beginnen",
|
||||||
"insights_all_org_filter": "Alle Apps",
|
"insights_all_org_filter": "Alle Apps",
|
||||||
"insights_team_filter": "Team: {{teamName}}",
|
"insights_team_filter": "Team: {{teamName}}",
|
||||||
@@ -1971,6 +1974,7 @@
|
|||||||
"org_team_names_example_5": "z.B. Data Analytics Team",
|
"org_team_names_example_5": "z.B. Data Analytics Team",
|
||||||
"org_max_team_warnings": "Weitere Teams können Sie zu einem späteren Zeitpunkt hinzufügen.",
|
"org_max_team_warnings": "Weitere Teams können Sie zu einem späteren Zeitpunkt hinzufügen.",
|
||||||
"what_is_this_meeting_about": "Worum geht es in diesem Termin?",
|
"what_is_this_meeting_about": "Worum geht es in diesem Termin?",
|
||||||
|
"kyc_verification_information": "Aus Sicherheitsgründen müssen Sie Ihren {{teamOrAccount}} verifizieren, bevor Sie Textnachrichten an die Teilnehmer senden können. Bitte kontaktieren Sie uns unter <a>{{supportEmail}}</a> und geben Sie folgende Informationen an:",
|
||||||
"org_admin_other_teams": "Weitere Teams",
|
"org_admin_other_teams": "Weitere Teams",
|
||||||
"no_other_teams_found": "Keine weiteren Teams gefunden",
|
"no_other_teams_found": "Keine weiteren Teams gefunden",
|
||||||
"no_other_teams_found_description": "Es gibt keine weiteren Teams in dieser Organisation.",
|
"no_other_teams_found_description": "Es gibt keine weiteren Teams in dieser Organisation.",
|
||||||
|
|||||||
@@ -407,6 +407,8 @@
|
|||||||
"allow_dynamic_booking_tooltip": "Group booking links that can be created dynamically by adding multiple usernames with a '+'. example: '{{appName}}/bailey+peer'",
|
"allow_dynamic_booking_tooltip": "Group booking links that can be created dynamically by adding multiple usernames with a '+'. example: '{{appName}}/bailey+peer'",
|
||||||
"allow_dynamic_booking": "Allow attendees to book you through dynamic group bookings",
|
"allow_dynamic_booking": "Allow attendees to book you through dynamic group bookings",
|
||||||
"dynamic_booking": "Dynamic group links",
|
"dynamic_booking": "Dynamic group links",
|
||||||
|
"allow_seo_indexing": "Allow search engines to access your public content",
|
||||||
|
"seo_indexing": "Allow SEO Indexing",
|
||||||
"email": "Email",
|
"email": "Email",
|
||||||
"email_placeholder": "jdoe@example.com",
|
"email_placeholder": "jdoe@example.com",
|
||||||
"full_name": "Full name",
|
"full_name": "Full name",
|
||||||
@@ -1263,6 +1265,7 @@
|
|||||||
"error_updating_settings": "Error updating settings",
|
"error_updating_settings": "Error updating settings",
|
||||||
"personal_cal_url": "My personal {{appName}} URL",
|
"personal_cal_url": "My personal {{appName}} URL",
|
||||||
"bio_hint": "A few sentences about yourself. this will appear on your personal url page.",
|
"bio_hint": "A few sentences about yourself. this will appear on your personal url page.",
|
||||||
|
"user_has_no_bio": "This user has not added a bio yet.",
|
||||||
"delete_account_modal_title": "Delete Account",
|
"delete_account_modal_title": "Delete Account",
|
||||||
"confirm_delete_account_modal": "Are you sure you want to delete your {{appName}} account?",
|
"confirm_delete_account_modal": "Are you sure you want to delete your {{appName}} account?",
|
||||||
"delete_my_account": "Delete my account",
|
"delete_my_account": "Delete my account",
|
||||||
@@ -1980,6 +1983,11 @@
|
|||||||
"org_team_names_example_5": "e.g. Data Analytics Team",
|
"org_team_names_example_5": "e.g. Data Analytics Team",
|
||||||
"org_max_team_warnings": "You will be able to add more teams later on.",
|
"org_max_team_warnings": "You will be able to add more teams later on.",
|
||||||
"what_is_this_meeting_about": "What is this meeting about?",
|
"what_is_this_meeting_about": "What is this meeting about?",
|
||||||
|
"add_to_team":"Add to team",
|
||||||
|
"remove_users_from_org": "Remove users from organization",
|
||||||
|
"remove_users_from_org_confirm":"Are you sure you want to remove {{userCount}} users from this organization?",
|
||||||
|
"user_has_no_schedules":"This user has not setup any schedules yet",
|
||||||
|
"user_isnt_in_any_teams":"This user is not in any teams",
|
||||||
"requires_booker_email_verification": "Requires booker email verification",
|
"requires_booker_email_verification": "Requires booker email verification",
|
||||||
"description_requires_booker_email_verification": "To ensure booker's email verification before scheduling events",
|
"description_requires_booker_email_verification": "To ensure booker's email verification before scheduling events",
|
||||||
"requires_confirmation_mandatory": "Text messages can only be sent to attendees when event type requires confirmation.",
|
"requires_confirmation_mandatory": "Text messages can only be sent to attendees when event type requires confirmation.",
|
||||||
|
|||||||
@@ -118,7 +118,7 @@
|
|||||||
"team_info": "Informacje o zespole",
|
"team_info": "Informacje o zespole",
|
||||||
"request_another_invitation_email": "Jeśli wolisz nie używać {{toEmail}} jako adresu e-mail {{appName}} lub masz już konto {{appName}}, poproś o inne zaproszenie na ten e-mail.",
|
"request_another_invitation_email": "Jeśli wolisz nie używać {{toEmail}} jako adresu e-mail {{appName}} lub masz już konto {{appName}}, poproś o inne zaproszenie na ten e-mail.",
|
||||||
"you_have_been_invited": "Zostałeś zaproszony do dołączenia do zespołu {{teamName}}",
|
"you_have_been_invited": "Zostałeś zaproszony do dołączenia do zespołu {{teamName}}",
|
||||||
"user_invited_you": "Użytkownik {{user}} zaprosił Cię do dołączenia do zespołu {{team}} ({{entity}}) w aplikacji {{appName}}.",
|
"user_invited_you": "Użytkownik {{user}} zaprosił Cię do dołączenia do: {{entity}} {{team}} w aplikacji {{appName}}.",
|
||||||
"hidden_team_member_title": "Jesteś ukryty w tym zespole",
|
"hidden_team_member_title": "Jesteś ukryty w tym zespole",
|
||||||
"hidden_team_member_message": "Twoje miejsce nie jest opłacone, przejdź na wersję Pro lub poinformuj właściciela zespołu o tym, że może zapłacić za Twoje miejsce.",
|
"hidden_team_member_message": "Twoje miejsce nie jest opłacone, przejdź na wersję Pro lub poinformuj właściciela zespołu o tym, że może zapłacić za Twoje miejsce.",
|
||||||
"hidden_team_owner_message": "Potrzebujesz konta pro aby korzystać z zespołów, jesteś ukryty do czasu ulepszenia.",
|
"hidden_team_owner_message": "Potrzebujesz konta pro aby korzystać z zespołów, jesteś ukryty do czasu ulepszenia.",
|
||||||
@@ -1661,7 +1661,7 @@
|
|||||||
"email_user_cta": "Wyświetl zaproszenie",
|
"email_user_cta": "Wyświetl zaproszenie",
|
||||||
"email_no_user_invite_heading": "Zaproszono Cię do dołączenia do aplikacji {{appName}} ({{entity}})",
|
"email_no_user_invite_heading": "Zaproszono Cię do dołączenia do aplikacji {{appName}} ({{entity}})",
|
||||||
"email_no_user_invite_subheading": "Użytkownik {{invitedBy}} zaprasza Cię do dołączenia do jego zespołu w aplikacji {{appName}}. Aplikacja {{appName}} to terminarz do planowania wydarzeń, który umożliwi Tobie i Twojemu zespołowi planowanie spotkań bez czasochłonnej wymiany wiadomości e-mail.",
|
"email_no_user_invite_subheading": "Użytkownik {{invitedBy}} zaprasza Cię do dołączenia do jego zespołu w aplikacji {{appName}}. Aplikacja {{appName}} to terminarz do planowania wydarzeń, który umożliwi Tobie i Twojemu zespołowi planowanie spotkań bez czasochłonnej wymiany wiadomości e-mail.",
|
||||||
"email_user_invite_subheading": "Użytkownik {{invitedBy}} zaprasza Cię do dołączenia do jego zespołu „{{teamName}}” ({{entity}}) w aplikacji {{appName}}. Aplikacja {{appName}} to terminarz do planowania wydarzeń, który umożliwi {{entity}} planowanie spotkań bez czasochłonnej wymiany wiadomości e-mail.",
|
"email_user_invite_subheading": "Użytkownik {{invitedBy}} zaprasza Cię do dołączenia do: {{entity}} „{{teamName}}” w aplikacji {{appName}}. Aplikacja {{appName}} to terminarz do planowania wydarzeń, który umożliwi {{entity}} planowanie spotkań bez czasochłonnej wymiany wiadomości e-mail.",
|
||||||
"email_no_user_invite_steps_intro": "Przeprowadzimy Cię przez kilka krótkich etapów i wkrótce w ramach {{entity}} zaczniesz z przyjemnością korzystać z bezstresowego narzędzia planowania.",
|
"email_no_user_invite_steps_intro": "Przeprowadzimy Cię przez kilka krótkich etapów i wkrótce w ramach {{entity}} zaczniesz z przyjemnością korzystać z bezstresowego narzędzia planowania.",
|
||||||
"email_no_user_step_one": "Wybierz nazwę użytkownika",
|
"email_no_user_step_one": "Wybierz nazwę użytkownika",
|
||||||
"email_no_user_step_two": "Połącz swoje konto kalendarza",
|
"email_no_user_step_two": "Połącz swoje konto kalendarza",
|
||||||
|
|||||||
@@ -11,10 +11,10 @@
|
|||||||
"calcom_explained_new_user": "Conclua a configuração da sua conta {{appName}}! Está apenas a alguns passos de resolver todos os seus problemas com agendamentos.",
|
"calcom_explained_new_user": "Conclua a configuração da sua conta {{appName}}! Está apenas a alguns passos de resolver todos os seus problemas com agendamentos.",
|
||||||
"have_any_questions": "Tem perguntas? Estamos disponíveis para ajudar.",
|
"have_any_questions": "Tem perguntas? Estamos disponíveis para ajudar.",
|
||||||
"reset_password_subject": "{{appName}}: Instruções de redefinição da senha",
|
"reset_password_subject": "{{appName}}: Instruções de redefinição da senha",
|
||||||
"verify_email_subject": "{{appName}}: Verificar sua conta",
|
"verify_email_subject": "{{appName}}: Verifique a sua conta",
|
||||||
"check_your_email": "Confirme o seu e-mail",
|
"check_your_email": "Confirme o seu e-mail",
|
||||||
"verify_email_page_body": "Nós enviamos um e-mail para {{email}}. É importante verificar seu endereço de e-mail para garantir que você receberá as comunicações de {{appName}}.",
|
"verify_email_page_body": "Enviámos um e-mail para {{email}}. É importante verificar o seu endereço de e-mail para garantir que receberá as comunicações de {{appName}}.",
|
||||||
"verify_email_banner_body": "Confirme o seu endereço de e-mail para garantir a melhor entrega possível de correio eletrónico e de agenda",
|
"verify_email_banner_body": "Confirme o seu endereço de e-mail para garantir a melhor entrega possível de e-mail e de agenda",
|
||||||
"verify_email_email_header": "Confirme o seu endereço de e-mail",
|
"verify_email_email_header": "Confirme o seu endereço de e-mail",
|
||||||
"verify_email_email_button": "Confirmar o seu e-mail",
|
"verify_email_email_button": "Confirmar o seu e-mail",
|
||||||
"verify_email_email_body": "Confirme o seu endereço de e-mail clicando no botão abaixo.",
|
"verify_email_email_body": "Confirme o seu endereço de e-mail clicando no botão abaixo.",
|
||||||
@@ -87,9 +87,9 @@
|
|||||||
"your_meeting_has_been_booked": "A sua reunião foi reservada",
|
"your_meeting_has_been_booked": "A sua reunião foi reservada",
|
||||||
"event_type_has_been_rescheduled_on_time_date": "Seu {{title}} foi remarcado para {{date}}.",
|
"event_type_has_been_rescheduled_on_time_date": "Seu {{title}} foi remarcado para {{date}}.",
|
||||||
"event_has_been_rescheduled": "O seu evento foi reagendado.",
|
"event_has_been_rescheduled": "O seu evento foi reagendado.",
|
||||||
"request_reschedule_subtitle": "{{organizer}} cancelou a reserva e pediu para que escolhesse outro horário.",
|
"request_reschedule_subtitle": "{{organizer}} cancelou a reserva e pediu que escolhesse outro horário.",
|
||||||
"request_reschedule_title_organizer": "Solicitou um reagendamento a {{attendee}}",
|
"request_reschedule_title_organizer": "Solicitou um reagendamento a {{attendee}}",
|
||||||
"request_reschedule_subtitle_organizer": "Você cancelou a reserva e {{attendee}} deve escolher um novo horário para a reserva consigo.",
|
"request_reschedule_subtitle_organizer": "Cancelou a reserva e {{attendee}} deve escolher um novo horário para a reserva consigo.",
|
||||||
"rescheduled_event_type_subject": "Pedido de reagendamento enviado: {{eventType}} com {{name}} em {{date}}",
|
"rescheduled_event_type_subject": "Pedido de reagendamento enviado: {{eventType}} com {{name}} em {{date}}",
|
||||||
"requested_to_reschedule_subject_attendee": "Acção necessária para reagendamento: Por favor reserve uma nova hora para {{eventType}} com {{name}}",
|
"requested_to_reschedule_subject_attendee": "Acção necessária para reagendamento: Por favor reserve uma nova hora para {{eventType}} com {{name}}",
|
||||||
"hi_user_name": "Olá {{name}}",
|
"hi_user_name": "Olá {{name}}",
|
||||||
@@ -129,7 +129,7 @@
|
|||||||
"team_upgrade_banner_description": "Obrigado por experimentar o nosso novo plano de equipa. Verificámos que a sua equipa \"{{teamName}}\" necessita de ser atualizada.",
|
"team_upgrade_banner_description": "Obrigado por experimentar o nosso novo plano de equipa. Verificámos que a sua equipa \"{{teamName}}\" necessita de ser atualizada.",
|
||||||
"upgrade_banner_action": "Atualize aqui",
|
"upgrade_banner_action": "Atualize aqui",
|
||||||
"team_upgraded_successfully": "A sua equipa foi actualizada com sucesso!",
|
"team_upgraded_successfully": "A sua equipa foi actualizada com sucesso!",
|
||||||
"org_upgrade_banner_description": "Obrigado por experimentar o nosso plano Organization. Verificámos que a sua Organização \"{{teamName}}\" necessita de ser atualizada.",
|
"org_upgrade_banner_description": "Obrigado por experimentar o nosso plano Organization. Verificámos que a sua Organização \"{{teamName}}\" precisa de ser atualizada.",
|
||||||
"org_upgraded_successfully": "A sua Organização foi atualizada com sucesso!",
|
"org_upgraded_successfully": "A sua Organização foi atualizada com sucesso!",
|
||||||
"use_link_to_reset_password": "Use a ligação abaixo para redefinir a sua senha",
|
"use_link_to_reset_password": "Use a ligação abaixo para redefinir a sua senha",
|
||||||
"hey_there": "Olá,",
|
"hey_there": "Olá,",
|
||||||
@@ -313,9 +313,9 @@
|
|||||||
"bookerlayout_week_view": "Semana",
|
"bookerlayout_week_view": "Semana",
|
||||||
"bookerlayout_column_view": "Coluna",
|
"bookerlayout_column_view": "Coluna",
|
||||||
"bookerlayout_error_min_one_enabled": "Tem de estar ativa pelo menos uma disposição.",
|
"bookerlayout_error_min_one_enabled": "Tem de estar ativa pelo menos uma disposição.",
|
||||||
"bookerlayout_error_default_not_enabled": "A disposição selecionada como a vista predefinida não faz parte das disposições ativas.",
|
"bookerlayout_error_default_not_enabled": "A disposição selecionada como vista predefinida não faz parte das disposições ativas.",
|
||||||
"bookerlayout_error_unknown_layout": "A disposição que selecionou não é uma disposição válida.",
|
"bookerlayout_error_unknown_layout": "A disposição que selecionou não é uma disposição válida.",
|
||||||
"bookerlayout_override_global_settings": "Pode gerir isto para todos os seus tipos de eventos em <2>definições/ aparência</2> ou <6>sobrepor apenas para este evento</6>.",
|
"bookerlayout_override_global_settings": "Pode gerir isto para todos os seus tipos de eventos em <2>definições / aparência</2> ou <6>sobrepor apenas para este evento</6>.",
|
||||||
"unexpected_error_try_again": "Ocorreu um erro inesperado. Tente de novo.",
|
"unexpected_error_try_again": "Ocorreu um erro inesperado. Tente de novo.",
|
||||||
"sunday_time_error": "Hora inválida no domingo",
|
"sunday_time_error": "Hora inválida no domingo",
|
||||||
"monday_time_error": "Hora inválida na segunda-feira",
|
"monday_time_error": "Hora inválida na segunda-feira",
|
||||||
@@ -337,11 +337,11 @@
|
|||||||
"cancelling_event_recurring": "O evento é uma instância de um evento recorrente.",
|
"cancelling_event_recurring": "O evento é uma instância de um evento recorrente.",
|
||||||
"cancelling_all_recurring": "Estas são todas as restantes instâncias do evento recorrente.",
|
"cancelling_all_recurring": "Estas são todas as restantes instâncias do evento recorrente.",
|
||||||
"error_with_status_code_occured": "Ocorreu um erro com o código de estado {{status}}",
|
"error_with_status_code_occured": "Ocorreu um erro com o código de estado {{status}}",
|
||||||
"error_event_type_url_duplicate": "Um tipo de evento com essa URL já existe.",
|
"error_event_type_url_duplicate": "Já existe um tipo de evento com este URL.",
|
||||||
"error_event_type_unauthorized_create": "Você não tem autorização para criar esse evento",
|
"error_event_type_unauthorized_create": "Não tem autorização para criar este evento",
|
||||||
"error_event_type_unauthorized_update": "Você não tem autorização para editar esse evento",
|
"error_event_type_unauthorized_update": "Não tem autorização para editar este evento",
|
||||||
"error_workflow_unauthorized_create": "Você não tem autorização para criar esse evento",
|
"error_workflow_unauthorized_create": "Não tem autorização para criar este evento",
|
||||||
"error_schedule_unauthorized_create": "Você não tem autorização para criar essa agenda",
|
"error_schedule_unauthorized_create": "Não tem autorização para criar esta agenda",
|
||||||
"booking_already_cancelled": "Esta reserva já foi cancelada",
|
"booking_already_cancelled": "Esta reserva já foi cancelada",
|
||||||
"booking_already_accepted_rejected": "Esta reserva já foi aceite ou rejeitada",
|
"booking_already_accepted_rejected": "Esta reserva já foi aceite ou rejeitada",
|
||||||
"go_back_home": "Voltar à Página Inicial",
|
"go_back_home": "Voltar à Página Inicial",
|
||||||
@@ -357,7 +357,7 @@
|
|||||||
"upcoming_bookings": "Assim que alguém reservar uma hora, ela aparecerá aqui.",
|
"upcoming_bookings": "Assim que alguém reservar uma hora, ela aparecerá aqui.",
|
||||||
"recurring_bookings": "Assim que alguém reservar uma reunião recorrente, será mostrada aqui.",
|
"recurring_bookings": "Assim que alguém reservar uma reunião recorrente, será mostrada aqui.",
|
||||||
"past_bookings": "As suas reservas anteriores aparecerão aqui.",
|
"past_bookings": "As suas reservas anteriores aparecerão aqui.",
|
||||||
"cancelled_bookings": "Suas reservas canceladas aparecerão aqui.",
|
"cancelled_bookings": "As suas reservas canceladas vão aparecer aqui.",
|
||||||
"unconfirmed_bookings": "As suas marcações não confirmadas irão aparecer aqui.",
|
"unconfirmed_bookings": "As suas marcações não confirmadas irão aparecer aqui.",
|
||||||
"unconfirmed_bookings_tooltip": "Reservas não confirmadas",
|
"unconfirmed_bookings_tooltip": "Reservas não confirmadas",
|
||||||
"on": "em",
|
"on": "em",
|
||||||
@@ -398,12 +398,12 @@
|
|||||||
"browse_api_documentation": "Consulte a nossa documentação da API",
|
"browse_api_documentation": "Consulte a nossa documentação da API",
|
||||||
"leverage_our_api": "Tire partido da nossa API para controlo e personalização total.",
|
"leverage_our_api": "Tire partido da nossa API para controlo e personalização total.",
|
||||||
"create_webhook": "Criar Webhook",
|
"create_webhook": "Criar Webhook",
|
||||||
"booking_cancelled": "Reserva Cancelada",
|
"booking_cancelled": "Reserva cancelada",
|
||||||
"booking_rescheduled": "Reserva Reagendada",
|
"booking_rescheduled": "Reserva Reagendada",
|
||||||
"recording_ready": "Ligação de transferência da gravação pronta",
|
"recording_ready": "Ligação de transferência da gravação pronta",
|
||||||
"booking_created": "Reserva Criada",
|
"booking_created": "Reserva Criada",
|
||||||
"booking_rejected": "Reserva Rejeitada",
|
"booking_rejected": "Reserva rejeitada",
|
||||||
"booking_requested": "Reserva Rejeitada",
|
"booking_requested": "Reserva solicitada",
|
||||||
"meeting_ended": "A reunião terminou",
|
"meeting_ended": "A reunião terminou",
|
||||||
"form_submitted": "Formulário enviado",
|
"form_submitted": "Formulário enviado",
|
||||||
"event_triggers": "Causadores de eventos",
|
"event_triggers": "Causadores de eventos",
|
||||||
@@ -587,7 +587,7 @@
|
|||||||
"disband_team": "Dissolver Equipa",
|
"disband_team": "Dissolver Equipa",
|
||||||
"disband_team_confirmation_message": "De certeza que quer desmantelar esta equipa? Qualquer pessoa com quem tenha partilhado esta ligação de equipa deixará de poder fazer reservas através da respectiva ligação.",
|
"disband_team_confirmation_message": "De certeza que quer desmantelar esta equipa? Qualquer pessoa com quem tenha partilhado esta ligação de equipa deixará de poder fazer reservas através da respectiva ligação.",
|
||||||
"disband_org": "Dissolver Organização",
|
"disband_org": "Dissolver Organização",
|
||||||
"disband_org_confirmation_message": "Tem a certeza que deseja dissolver esta Organização? Todas as equipas e membros serão eliminados.",
|
"disband_org_confirmation_message": "Tem a certeza que quer dissolver esta organização? Todas as equipas e membros serão eliminados.",
|
||||||
"remove_member_confirmation_message": "Tem a certeza de que deseja remover este membro da equipa?",
|
"remove_member_confirmation_message": "Tem a certeza de que deseja remover este membro da equipa?",
|
||||||
"confirm_disband_team": "Sim, dissolver equipa",
|
"confirm_disband_team": "Sim, dissolver equipa",
|
||||||
"confirm_remove_member": "Sim, remover membro",
|
"confirm_remove_member": "Sim, remover membro",
|
||||||
@@ -874,16 +874,16 @@
|
|||||||
"no_category_apps_description_other": "Adicione qualquer outro tipo de aplicações para fazer todos os tipos de coisas",
|
"no_category_apps_description_other": "Adicione qualquer outro tipo de aplicações para fazer todos os tipos de coisas",
|
||||||
"no_category_apps_description_web3": "Adicione uma aplicação web3 às suas páginas de reservas",
|
"no_category_apps_description_web3": "Adicione uma aplicação web3 às suas páginas de reservas",
|
||||||
"no_category_apps_description_messaging": "Adicione uma aplicação de mensagens para configurar notificações e lembretes personalizados",
|
"no_category_apps_description_messaging": "Adicione uma aplicação de mensagens para configurar notificações e lembretes personalizados",
|
||||||
"no_category_apps_description_crm": "Adicione um aplicativo CRM para manter o controle de com quem você encontrou",
|
"no_category_apps_description_crm": "Adicione uma aplicação de CRM para manter um registo das pessoas com quem se reuniu",
|
||||||
"installed_app_calendar_description": "Defina o(s) calendário(s) para verificar se existem conflitos e assim evitar marcações sobrepostas.",
|
"installed_app_calendar_description": "Defina o(s) calendário(s) para verificar se existem conflitos e assim evitar marcações sobrepostas.",
|
||||||
"installed_app_payment_description": "Configure os serviços de processamento de pagamentos que serão utilizados nas cobranças aos seus clientes.",
|
"installed_app_payment_description": "Configure os serviços de processamento de pagamentos que serão utilizados nas cobranças aos seus clientes.",
|
||||||
"installed_app_analytics_description": "Configurar as aplicações de análise a utilizar nas suas páginas de reservas",
|
"installed_app_analytics_description": "Configurar as aplicações de análise a utilizar nas suas páginas de reservas",
|
||||||
"installed_app_other_description": "Todas as aplicações instaladas de outras categorias.",
|
"installed_app_other_description": "Todas as aplicações instaladas de outras categorias.",
|
||||||
"installed_app_conferencing_description": "Configure que aplicativos de conferência utilizar",
|
"installed_app_conferencing_description": "Configurar as aplicações de conferência a utilizar",
|
||||||
"installed_app_automation_description": "Configurar as aplicações de automatização a utilizar",
|
"installed_app_automation_description": "Configurar as aplicações de automatização a utilizar",
|
||||||
"installed_app_web3_description": "Configure as aplicações web3 a utilizar nas suas páginas de reservas",
|
"installed_app_web3_description": "Configure as aplicações web3 a utilizar nas suas páginas de reservas",
|
||||||
"installed_app_messaging_description": "Configure que aplicativos de mensagens utilizar para configurar notificações e lembretes personalizados",
|
"installed_app_messaging_description": "Configurar as aplicações de mensagens a utilizar para definir notificações e lembretes personalizados",
|
||||||
"installed_app_crm_description": "Configure que aplicativos de CRM utilizar para manter o controle de com quem você encontrou",
|
"installed_app_crm_description": "Configurar as aplicações de CRM a utilizar para manter um registo das pessoas com quem se reuniu",
|
||||||
"analytics": "Estatísticas",
|
"analytics": "Estatísticas",
|
||||||
"empty_installed_apps_headline": "Nenhuma aplicação instalada",
|
"empty_installed_apps_headline": "Nenhuma aplicação instalada",
|
||||||
"empty_installed_apps_description": "As aplicações permitem-lhe melhorar o seu fluxo de trabalho e os seus agendamentos, significativamente.",
|
"empty_installed_apps_description": "As aplicações permitem-lhe melhorar o seu fluxo de trabalho e os seus agendamentos, significativamente.",
|
||||||
@@ -909,7 +909,7 @@
|
|||||||
"create_events_on": "Criar eventos em:",
|
"create_events_on": "Criar eventos em:",
|
||||||
"enterprise_license": "Esta é uma funcionalidade empresarial",
|
"enterprise_license": "Esta é uma funcionalidade empresarial",
|
||||||
"enterprise_license_description": "Para ativar esta funcionalidade, obtenha uma chave de instalação na consola {{consoleUrl}} e adicione-a ao seu .env como CALCOM_LICENSE_KEY. Se a sua equipa já tem uma licença, entre em contacto com {{supportMail}} para obter ajuda.",
|
"enterprise_license_description": "Para ativar esta funcionalidade, obtenha uma chave de instalação na consola {{consoleUrl}} e adicione-a ao seu .env como CALCOM_LICENSE_KEY. Se a sua equipa já tem uma licença, entre em contacto com {{supportMail}} para obter ajuda.",
|
||||||
"enterprise_license_development": "Pode testar este recurso no modo de desenvolvimento. Para utilização em produção, um administrador deverá aceder a <2>/auth/setup</2> para inserir a chave da licença.",
|
"enterprise_license_development": "Pode testar esta funcionalidade no modo de desenvolvimento. Para utilização em produção, um administrador deverá aceder a <2>/auth/setup</2> para inserir a chave da licença.",
|
||||||
"missing_license": "Licença em falta",
|
"missing_license": "Licença em falta",
|
||||||
"signup_requires": "Necessita de licença comercial",
|
"signup_requires": "Necessita de licença comercial",
|
||||||
"signup_requires_description": "De momento a {{companyName}} não oferece uma versão gratuita de código aberto da página de registo. Para aceder completamente aos componentes de registo tem de adquirir uma licença comercial. Para uso pessoal, recomendamos a Prisma Data Platform ou qualquer outra interface Postgres para criar contas.",
|
"signup_requires_description": "De momento a {{companyName}} não oferece uma versão gratuita de código aberto da página de registo. Para aceder completamente aos componentes de registo tem de adquirir uma licença comercial. Para uso pessoal, recomendamos a Prisma Data Platform ou qualquer outra interface Postgres para criar contas.",
|
||||||
@@ -1072,7 +1072,7 @@
|
|||||||
"event_location": "Localização do evento",
|
"event_location": "Localização do evento",
|
||||||
"reschedule_optional": "Razão do reagendamento (opcional)",
|
"reschedule_optional": "Razão do reagendamento (opcional)",
|
||||||
"reschedule_placeholder": "Informe os restantes para que saibam porque precisa de reagendar",
|
"reschedule_placeholder": "Informe os restantes para que saibam porque precisa de reagendar",
|
||||||
"event_cancelled": "Esse evento foi cancelado",
|
"event_cancelled": "Este evento foi cancelado",
|
||||||
"emailed_information_about_cancelled_event": "Enviámos um email com o aviso para si e para os restantes participantes.",
|
"emailed_information_about_cancelled_event": "Enviámos um email com o aviso para si e para os restantes participantes.",
|
||||||
"this_input_will_shown_booking_this_event": "Este campo será mostrado na reserva deste evento",
|
"this_input_will_shown_booking_this_event": "Este campo será mostrado na reserva deste evento",
|
||||||
"meeting_url_in_conformation_email": "O URL da reunião está no email de confirmação",
|
"meeting_url_in_conformation_email": "O URL da reunião está no email de confirmação",
|
||||||
@@ -1292,7 +1292,7 @@
|
|||||||
"conferencing_description": "Adicione as suas aplicações de videoconferência para reuniões",
|
"conferencing_description": "Adicione as suas aplicações de videoconferência para reuniões",
|
||||||
"add_conferencing_app": "Adicionar aplicação de conferência",
|
"add_conferencing_app": "Adicionar aplicação de conferência",
|
||||||
"password_description": "Faça a gestão das definições para as palavras-passe da sua conta",
|
"password_description": "Faça a gestão das definições para as palavras-passe da sua conta",
|
||||||
"set_up_two_factor_authentication": "Configure sua autenticação em duas etapas",
|
"set_up_two_factor_authentication": "Configurar a autenticação de dois fatores",
|
||||||
"we_just_need_basic_info": "Precisamos apenas de algumas informações básicas para configurar o seu perfil.",
|
"we_just_need_basic_info": "Precisamos apenas de algumas informações básicas para configurar o seu perfil.",
|
||||||
"skip": "Ignorar",
|
"skip": "Ignorar",
|
||||||
"do_this_later": "Fazer isto mais tarde",
|
"do_this_later": "Fazer isto mais tarde",
|
||||||
@@ -1306,8 +1306,8 @@
|
|||||||
"connect_analytics_apps": "Associar aplicações de estatística",
|
"connect_analytics_apps": "Associar aplicações de estatística",
|
||||||
"connect_other_apps": "Associe outras aplicações",
|
"connect_other_apps": "Associe outras aplicações",
|
||||||
"connect_web3_apps": "Associe aplicações web3",
|
"connect_web3_apps": "Associe aplicações web3",
|
||||||
"connect_messaging_apps": "Conecte aplicações de mensagens",
|
"connect_messaging_apps": "Ligar aplicações de mensagens",
|
||||||
"connect_crm_apps": "Conecte aplicações de CRM",
|
"connect_crm_apps": "Ligar aplicações CRM",
|
||||||
"current_step_of_total": "Passo {{currentStep}} de {{maxSteps}}",
|
"current_step_of_total": "Passo {{currentStep}} de {{maxSteps}}",
|
||||||
"add_variable": "Adicionar variável",
|
"add_variable": "Adicionar variável",
|
||||||
"custom_phone_number": "Número do telefone personalizado",
|
"custom_phone_number": "Número do telefone personalizado",
|
||||||
@@ -1320,14 +1320,14 @@
|
|||||||
"location_info": "O local do evento",
|
"location_info": "O local do evento",
|
||||||
"additional_notes_info": "As notas adicionais da reserva",
|
"additional_notes_info": "As notas adicionais da reserva",
|
||||||
"attendee_name_info": "O nome do responsável pela reserva",
|
"attendee_name_info": "O nome do responsável pela reserva",
|
||||||
"organizer_name_info": "Nome do(a) organizador(a)",
|
"organizer_name_info": "Nome do organizador",
|
||||||
"to": "Para",
|
"to": "Para",
|
||||||
"workflow_turned_on_successfully": "Fluxo de trabalho {{workflowName}} {{offOn}} com sucesso",
|
"workflow_turned_on_successfully": "Fluxo de trabalho {{workflowName}} {{offOn}} com sucesso",
|
||||||
"download_responses": "Descarregar respostas",
|
"download_responses": "Descarregar respostas",
|
||||||
"download_responses_description": "Descarregue todas as respostas ao seu formulário em formato CSV.",
|
"download_responses_description": "Descarregue todas as respostas ao seu formulário em formato CSV.",
|
||||||
"download": "Descarregar",
|
"download": "Descarregar",
|
||||||
"download_recording": "Transferir gravação",
|
"download_recording": "Transferir gravação",
|
||||||
"recording_from_your_recent_call": "A gravação da sua chamada recente em {{appName}} está pronta para download",
|
"recording_from_your_recent_call": "A gravação da sua chamada recente em {{appName}} está pronta para descarregar",
|
||||||
"create_your_first_form": "Crie o seu primeiro formulário",
|
"create_your_first_form": "Crie o seu primeiro formulário",
|
||||||
"create_your_first_form_description": "Com os Formulários de encaminhamento, pode fazer questões qualitativas e encaminhar para a pessoa ou o tipo de evento correto.",
|
"create_your_first_form_description": "Com os Formulários de encaminhamento, pode fazer questões qualitativas e encaminhar para a pessoa ou o tipo de evento correto.",
|
||||||
"create_your_first_webhook": "Crie o seu primeiro Webhook",
|
"create_your_first_webhook": "Crie o seu primeiro Webhook",
|
||||||
@@ -1336,9 +1336,9 @@
|
|||||||
"event_one": "evento",
|
"event_one": "evento",
|
||||||
"event_other": "eventos",
|
"event_other": "eventos",
|
||||||
"profile_team_description": "Faça a gestão das definições do seu perfil de equipa",
|
"profile_team_description": "Faça a gestão das definições do seu perfil de equipa",
|
||||||
"profile_org_description": "Gerencie as configurações do perfil da sua organização",
|
"profile_org_description": "Gerir definições para o perfil da sua organização",
|
||||||
"members_team_description": "Utilizadores que estão no grupo",
|
"members_team_description": "Utilizadores que estão no grupo",
|
||||||
"organization_description": "Gerencie os membros e administradores da sua organização",
|
"organization_description": "Gerir os administradores e membros da sua organização",
|
||||||
"team_url": "Endereço da equipa",
|
"team_url": "Endereço da equipa",
|
||||||
"team_members": "Membros da equipa",
|
"team_members": "Membros da equipa",
|
||||||
"more": "Mais",
|
"more": "Mais",
|
||||||
@@ -1519,8 +1519,8 @@
|
|||||||
"find_the_best_person": "Encontre a melhor pessoa disponível e faça um ciclo pela sua equipa.",
|
"find_the_best_person": "Encontre a melhor pessoa disponível e faça um ciclo pela sua equipa.",
|
||||||
"fixed_round_robin": "Distribuição equilibrada fixa",
|
"fixed_round_robin": "Distribuição equilibrada fixa",
|
||||||
"add_one_fixed_attendee": "Adicione um participante fixo e distribua de forma equilibrada a partir de um número de participantes.",
|
"add_one_fixed_attendee": "Adicione um participante fixo e distribua de forma equilibrada a partir de um número de participantes.",
|
||||||
"calcom_is_better_with_team": "{{appName}} é melhor com times",
|
"calcom_is_better_with_team": "{{appName}} é melhor com equipas",
|
||||||
"the_calcom_team": "A equipe {{companyName}}",
|
"the_calcom_team": "A equipa {{companyName}}",
|
||||||
"add_your_team_members": "Adicione os membros da sua equipa aos seus tipos de eventos. Utilize o agendamento coletivo para incluir todos os membros ou encontre a pessoa mais adequada com o agendamento baseado numa distribuição equilibrada.",
|
"add_your_team_members": "Adicione os membros da sua equipa aos seus tipos de eventos. Utilize o agendamento coletivo para incluir todos os membros ou encontre a pessoa mais adequada com o agendamento baseado numa distribuição equilibrada.",
|
||||||
"booking_limit_reached": "O limite de reservas para este tipo de evento foi atingido",
|
"booking_limit_reached": "O limite de reservas para este tipo de evento foi atingido",
|
||||||
"duration_limit_reached": "O limite de duração para este tipo de evento foi atingido",
|
"duration_limit_reached": "O limite de duração para este tipo de evento foi atingido",
|
||||||
@@ -1539,7 +1539,7 @@
|
|||||||
"navigate_installed_apps": "Ir para as aplicações instaladas",
|
"navigate_installed_apps": "Ir para as aplicações instaladas",
|
||||||
"disabled_calendar": "Se tiver outro calendário instalado, as novas reservas serão adicionadas ao mesmo. Se não for o caso, deve associar um novo calendário para garantir que não perde novas reservas.",
|
"disabled_calendar": "Se tiver outro calendário instalado, as novas reservas serão adicionadas ao mesmo. Se não for o caso, deve associar um novo calendário para garantir que não perde novas reservas.",
|
||||||
"enable_apps": "Ativar aplicações",
|
"enable_apps": "Ativar aplicações",
|
||||||
"enable_apps_description": "Ative aplicativos que usuários podem integrar com {{appName}}",
|
"enable_apps_description": "Ativar aplicações que os utilizadores podem integrar com {{appName}}",
|
||||||
"purchase_license": "Adquira uma Licença",
|
"purchase_license": "Adquira uma Licença",
|
||||||
"already_have_key": "Eu já tenho uma chave:",
|
"already_have_key": "Eu já tenho uma chave:",
|
||||||
"already_have_key_suggestion": "Por favor, copie a sua variável de ambiente CALCOM_LICENSE_KEY existente aqui.",
|
"already_have_key_suggestion": "Por favor, copie a sua variável de ambiente CALCOM_LICENSE_KEY existente aqui.",
|
||||||
@@ -1553,9 +1553,9 @@
|
|||||||
"no_available_apps": "Não existem aplicações disponíveis",
|
"no_available_apps": "Não existem aplicações disponíveis",
|
||||||
"no_available_apps_description": "Garanta que existem aplicações na sua instalação em 'packages/app-store'",
|
"no_available_apps_description": "Garanta que existem aplicações na sua instalação em 'packages/app-store'",
|
||||||
"no_apps": "Não existem aplicações ativas nesta instância do Cal",
|
"no_apps": "Não existem aplicações ativas nesta instância do Cal",
|
||||||
"no_apps_configured": "Nenhuma aplicação foi configurada ainda",
|
"no_apps_configured": "Ainda não foi configurada nenhuma aplicação",
|
||||||
"enable_in_settings": "Você pode habilitar os aplicatovs nas configurações",
|
"enable_in_settings": "Pode ativar as aplicações nas definições",
|
||||||
"please_contact_admin": "Por favor, contate seu administrador",
|
"please_contact_admin": "Contacte o seu administrador",
|
||||||
"apps_settings": "Definições de aplicações",
|
"apps_settings": "Definições de aplicações",
|
||||||
"fill_this_field": "Por favor, preencha este campo",
|
"fill_this_field": "Por favor, preencha este campo",
|
||||||
"options": "Opções",
|
"options": "Opções",
|
||||||
@@ -1674,7 +1674,7 @@
|
|||||||
"no_workflows": "Sem fluxos de trabalho",
|
"no_workflows": "Sem fluxos de trabalho",
|
||||||
"change_filter": "Altere o filtro para ver os seus fluxos de trabalho pessoais e da equipa.",
|
"change_filter": "Altere o filtro para ver os seus fluxos de trabalho pessoais e da equipa.",
|
||||||
"change_filter_common": "Alterar o filtro para ver os resultados.",
|
"change_filter_common": "Alterar o filtro para ver os resultados.",
|
||||||
"no_results_for_filter": "Sem resultados para o filtro",
|
"no_results_for_filter": "Nenhum resultado para o filtro",
|
||||||
"recommended_next_steps": "Próximos passos recomendados",
|
"recommended_next_steps": "Próximos passos recomendados",
|
||||||
"create_a_managed_event": "Criar um tipo de evento gerido",
|
"create_a_managed_event": "Criar um tipo de evento gerido",
|
||||||
"meetings_are_better_with_the_right": "Os eventos são melhores com os elementos certos da equipa. Convide-os agora.",
|
"meetings_are_better_with_the_right": "Os eventos são melhores com os elementos certos da equipa. Convide-os agora.",
|
||||||
@@ -1802,7 +1802,7 @@
|
|||||||
"least_booked_members": "Membros com menos reservas",
|
"least_booked_members": "Membros com menos reservas",
|
||||||
"events_created": "Eventos criados",
|
"events_created": "Eventos criados",
|
||||||
"events_completed": "Eventos concluídos",
|
"events_completed": "Eventos concluídos",
|
||||||
"events_cancelled": "Eventos Cancelados",
|
"events_cancelled": "Eventos cancelados",
|
||||||
"events_rescheduled": "Eventos reagendados",
|
"events_rescheduled": "Eventos reagendados",
|
||||||
"from_last_period": "do último período",
|
"from_last_period": "do último período",
|
||||||
"from_to_date_period": "De: {{startDate}} A: {{endDate}}",
|
"from_to_date_period": "De: {{startDate}} A: {{endDate}}",
|
||||||
@@ -1836,9 +1836,9 @@
|
|||||||
"complete_your_booking": "Concluir a sua reserva",
|
"complete_your_booking": "Concluir a sua reserva",
|
||||||
"complete_your_booking_subject": "Concluir a sua reserva: {{title}} a {{date}}",
|
"complete_your_booking_subject": "Concluir a sua reserva: {{title}} a {{date}}",
|
||||||
"confirm_your_details": "Confirme os seus dados",
|
"confirm_your_details": "Confirme os seus dados",
|
||||||
"copy_invite_link": "Copiar link do convite",
|
"copy_invite_link": "Copiar a ligação do convite",
|
||||||
"edit_invite_link": "Editar configurações do link",
|
"edit_invite_link": "Editar definições da ligação",
|
||||||
"invite_link_copied": "Link do convite copiado",
|
"invite_link_copied": "Ligação de convite copiada",
|
||||||
"invite_link_deleted": "Ligação do convite copiada",
|
"invite_link_deleted": "Ligação do convite copiada",
|
||||||
"invite_link_updated": "Definições da ligação de convite guardadas",
|
"invite_link_updated": "Definições da ligação de convite guardadas",
|
||||||
"link_expires_after": "Ligações definidas para expirar depois...",
|
"link_expires_after": "Ligações definidas para expirar depois...",
|
||||||
@@ -1901,26 +1901,26 @@
|
|||||||
"set_a_password": "Definir uma palavra-passe",
|
"set_a_password": "Definir uma palavra-passe",
|
||||||
"set_a_password_description": "Isto irá criar uma conta de utilizador com o e-mail da sua organização e esta palavra-passe.",
|
"set_a_password_description": "Isto irá criar uma conta de utilizador com o e-mail da sua organização e esta palavra-passe.",
|
||||||
"organization_logo": "Logo da Organização",
|
"organization_logo": "Logo da Organização",
|
||||||
"organization_about_description": "Breve descrição sobre sua organização. Isso vai aparecer na página de perfil pública da sua organização.",
|
"organization_about_description": "Algumas frases sobre a sua organização. Isto aparecerá na página de perfil público da sua organização.",
|
||||||
"ill_do_this_later": "Farei isso depois",
|
"ill_do_this_later": "Faço mais tarde",
|
||||||
"verify_your_email": "Verifique seu e-mail",
|
"verify_your_email": "Verifique o seu e-mail",
|
||||||
"enter_digit_code": "Entre o código de 6 dígitos enviado para {{email}}",
|
"enter_digit_code": "Introduza o código de 6 dígitos que enviámos para {{email}}",
|
||||||
"verify_email_organization": "Verifique seu e-mail para criar uma organização",
|
"verify_email_organization": "Verifique o seu e-mail para criar uma organização",
|
||||||
"code_provided_invalid": "O código fornecido inválido, tente novamente",
|
"code_provided_invalid": "O código fornecido não é válido, tente novamente",
|
||||||
"email_already_used": "E-mail já está sendo usado",
|
"email_already_used": "O e-mail já está a ser utilizado",
|
||||||
"organization_admin_invited_heading": "Recebeu um convite para se juntar a {{orgName}}",
|
"organization_admin_invited_heading": "Recebeu um convite para se juntar a {{orgName}}",
|
||||||
"organization_admin_invited_body": "Junte-se à sua equipa em {{orgName}} e comece a concentrar-se em reunir e não em fazer reuniões!",
|
"organization_admin_invited_body": "Junte-se à sua equipa em {{orgName}} e comece a concentrar-se nas reuniões, não em fazer reuniões!",
|
||||||
"duplicated_slugs_warning": "Os seguintes times não puderam ser criados devido a slugs duplicados: {{slugs}}",
|
"duplicated_slugs_warning": "As seguintes equipas não puderam ser criadas devido à duplicação de slugs: {{slugs}}",
|
||||||
"team_names_empty": "Nomes de equipes não podem estar vazios",
|
"team_names_empty": "Os nomes das equipas não podem ficar vazios",
|
||||||
"team_names_repeated": "Nomes de equipes não podem se repetir",
|
"team_names_repeated": "Os nomes das equipas não podem ser repetidos",
|
||||||
"user_belongs_organization": "Usuário pertence a uma organização",
|
"user_belongs_organization": "O utilizador pertence a uma organização",
|
||||||
"org_no_teams_yet": "Essaorganização ainda não tem times",
|
"org_no_teams_yet": "Esta organização ainda não tem equipas",
|
||||||
"org_no_teams_yet_description": "Se é um administrador, deve criar equipas para serem apresentadas aqui.",
|
"org_no_teams_yet_description": "Se é um administrador, deve criar equipas para serem apresentadas aqui.",
|
||||||
"set_up": "Configurar",
|
"set_up": "Configurar",
|
||||||
"set_up_your_profile": "Configurar o seu perfil",
|
"set_up_your_profile": "Configurar o seu perfil",
|
||||||
"set_up_your_profile_description": "Deixe as pessoas saber quem você é dentro de {{orgName}} e quando interagem com a sua ligação pública.",
|
"set_up_your_profile_description": "Permita que as pessoas saibam quem você é em {{orgName}} e quando se envolvem com o seu link público.",
|
||||||
"my_profile": "Meu perfil",
|
"my_profile": "O meu perfil",
|
||||||
"my_settings": "Minhas configurações",
|
"my_settings": "As minhas definições",
|
||||||
"crm": "CRM",
|
"crm": "CRM",
|
||||||
"messaging": "Mensagens",
|
"messaging": "Mensagens",
|
||||||
"sender_id_info": "Nome ou número mostrado como remetente de um SMS (alguns países não permitem identificadores alfanuméricos de remetentes)",
|
"sender_id_info": "Nome ou número mostrado como remetente de um SMS (alguns países não permitem identificadores alfanuméricos de remetentes)",
|
||||||
@@ -1930,15 +1930,15 @@
|
|||||||
"no_organization_slug": "Ocorreu um erro ao criar equipas para esta organização. Falta o slug do URL.",
|
"no_organization_slug": "Ocorreu um erro ao criar equipas para esta organização. Falta o slug do URL.",
|
||||||
"org_name": "Nome da organização",
|
"org_name": "Nome da organização",
|
||||||
"org_url": "URL da Organização",
|
"org_url": "URL da Organização",
|
||||||
"copy_link_org": "Copie o link para organização",
|
"copy_link_org": "Copiar a ligação para a organização",
|
||||||
"404_the_org": "A organização",
|
"404_the_org": "A organização",
|
||||||
"404_the_team": "O time",
|
"404_the_team": "A equipa",
|
||||||
"404_claim_entity_org": "Reivindicar o seu subdomínio para a sua organização",
|
"404_claim_entity_org": "Reivindicar o seu subdomínio para a sua organização",
|
||||||
"404_claim_entity_team": "Reivindicar esta equipa e começar a gerir os horários em grupo",
|
"404_claim_entity_team": "Reivindicar esta equipa e começar a gerir os horários em grupo",
|
||||||
"insights_all_org_filter": "Todos as aplicações",
|
"insights_all_org_filter": "Todos as aplicações",
|
||||||
"insights_team_filter": "Time: {{teamName}}",
|
"insights_team_filter": "Equipa: {{teamName}}",
|
||||||
"insights_user_filter": "Usuário {{userName}}",
|
"insights_user_filter": "Utilizador: {{userName}}",
|
||||||
"insights_subtitle": "Veja as informações de reservas sobre os seus eventos",
|
"insights_subtitle": "Ver informações sobre reservas nos seus eventos",
|
||||||
"custom_plan": "Plano Personalizado",
|
"custom_plan": "Plano personalizado",
|
||||||
"ADD_NEW_STRINGS_ABOVE_THIS_LINE_TO_PREVENT_MERGE_CONFLICTS": "↑↑↑↑↑↑↑↑↑↑↑↑↑ Adicione as suas novas strings acima aqui ↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑"
|
"ADD_NEW_STRINGS_ABOVE_THIS_LINE_TO_PREVENT_MERGE_CONFLICTS": "↑↑↑↑↑↑↑↑↑↑↑↑↑ Add your new strings above here ↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑"
|
||||||
}
|
}
|
||||||
@@ -118,7 +118,7 @@
|
|||||||
"team_info": "Thông Tin Nhóm",
|
"team_info": "Thông Tin Nhóm",
|
||||||
"request_another_invitation_email": "Nếu bạn không muốn sử dụng {{toEmail}} là email {{appName}} của bạn hay đã có tài khoản {{appName}}, hãy yêu cầu lời mời sang email đó.",
|
"request_another_invitation_email": "Nếu bạn không muốn sử dụng {{toEmail}} là email {{appName}} của bạn hay đã có tài khoản {{appName}}, hãy yêu cầu lời mời sang email đó.",
|
||||||
"you_have_been_invited": "Bạn đã được mời để gia nhập nhóm {{teamName}}",
|
"you_have_been_invited": "Bạn đã được mời để gia nhập nhóm {{teamName}}",
|
||||||
"user_invited_you": "{{user}} đã mời bạn để gia nhập {{team}} {{entity}} trên {{appName}}",
|
"user_invited_you": "{{user}} đã mời bạn để gia nhập {{entity}} {{team}} trên {{appName}}",
|
||||||
"hidden_team_member_title": "Bạn đã bị ẩn trong nhóm này",
|
"hidden_team_member_title": "Bạn đã bị ẩn trong nhóm này",
|
||||||
"hidden_team_member_message": "Ghế ngồi của bạn không được thanh toán bởi nhóm, hãy nâng cấp lên PRO hoặc thông báo cho người quản lý nhóm để họ có thể thanh toán cho ghế ngồi của bạn.",
|
"hidden_team_member_message": "Ghế ngồi của bạn không được thanh toán bởi nhóm, hãy nâng cấp lên PRO hoặc thông báo cho người quản lý nhóm để họ có thể thanh toán cho ghế ngồi của bạn.",
|
||||||
"hidden_team_owner_message": "Bạn cần tài khoản Pro để sử dụng nhóm, bạn sẽ bị ẩn cho đến khi bạn thanh toán.",
|
"hidden_team_owner_message": "Bạn cần tài khoản Pro để sử dụng nhóm, bạn sẽ bị ẩn cho đến khi bạn thanh toán.",
|
||||||
@@ -129,8 +129,8 @@
|
|||||||
"team_upgrade_banner_description": "Cám ơn bạn đã dùng thử gói kế hoạch mới dành cho nhóm. Chúng tôi để ý thấy nhóm \"{{teamName}}\" của bạn cần được nâng cấp.",
|
"team_upgrade_banner_description": "Cám ơn bạn đã dùng thử gói kế hoạch mới dành cho nhóm. Chúng tôi để ý thấy nhóm \"{{teamName}}\" của bạn cần được nâng cấp.",
|
||||||
"upgrade_banner_action": "Nâng cấp tại đây",
|
"upgrade_banner_action": "Nâng cấp tại đây",
|
||||||
"team_upgraded_successfully": "Nhóm bạn đã được nâng cấp thành công!",
|
"team_upgraded_successfully": "Nhóm bạn đã được nâng cấp thành công!",
|
||||||
"org_upgrade_banner_description": "Cám ơn bạn đã dùng thử gói Tổ chức. Chúng tôi để ý thấy Tổ chức \"{{teamName}}\" của bạn cần được nâng cấp.",
|
"org_upgrade_banner_description": "Cám ơn bạn đã dùng thử gói Organization. Chúng tôi để ý thấy Organization \"{{teamName}}\" của bạn cần được nâng cấp.",
|
||||||
"org_upgraded_successfully": "Tổ chức của bạn đã được nâng cấp thành công!",
|
"org_upgraded_successfully": "Organization của bạn đã được nâng cấp thành công!",
|
||||||
"use_link_to_reset_password": "Sử dụng đường link dưới đây để thay đổi mật khẩu của bạn",
|
"use_link_to_reset_password": "Sử dụng đường link dưới đây để thay đổi mật khẩu của bạn",
|
||||||
"hey_there": "Xin chào,",
|
"hey_there": "Xin chào,",
|
||||||
"forgot_your_password_calcom": "Quên mật khẩu của bạn? - {{appName}}",
|
"forgot_your_password_calcom": "Quên mật khẩu của bạn? - {{appName}}",
|
||||||
@@ -551,7 +551,7 @@
|
|||||||
"team_description": "Một vài câu về nhóm của bạn. Thông tin này sẽ xuất hiện trên trang URL của nhóm của bạn.",
|
"team_description": "Một vài câu về nhóm của bạn. Thông tin này sẽ xuất hiện trên trang URL của nhóm của bạn.",
|
||||||
"org_description": "Một vài câu về tổ chức của bạn. Thông tin này sẽ xuất hiện trên trang URL của tổ chức của bạn.",
|
"org_description": "Một vài câu về tổ chức của bạn. Thông tin này sẽ xuất hiện trên trang URL của tổ chức của bạn.",
|
||||||
"members": "Các thành viên",
|
"members": "Các thành viên",
|
||||||
"organization_members": "Thành viên trong tổ chức",
|
"organization_members": "Thành viên trong Organization",
|
||||||
"member": "Thành viên",
|
"member": "Thành viên",
|
||||||
"number_member_one": "{{count}} thành viên",
|
"number_member_one": "{{count}} thành viên",
|
||||||
"number_member_other": "{{count}} thành viên",
|
"number_member_other": "{{count}} thành viên",
|
||||||
@@ -1327,7 +1327,7 @@
|
|||||||
"download_responses_description": "Tải xuống tất cả phản hồi vào biểu mẫu của bạn ở định dạng CSV.",
|
"download_responses_description": "Tải xuống tất cả phản hồi vào biểu mẫu của bạn ở định dạng CSV.",
|
||||||
"download": "Tải về",
|
"download": "Tải về",
|
||||||
"download_recording": "Tải xuống bản ghi âm",
|
"download_recording": "Tải xuống bản ghi âm",
|
||||||
"recording_from_your_recent_call": "Một bản ghi âm từ cuộc gọi gần đây trên {{appName}} đã sẵn sàng để tải xuống",
|
"recording_from_your_recent_call": "Một bản ghi từ cuộc gọi gần đây trên {{appName}} đã sẵn sàng để tải xuống",
|
||||||
"create_your_first_form": "Tạo biểu mẫu đầu tiên",
|
"create_your_first_form": "Tạo biểu mẫu đầu tiên",
|
||||||
"create_your_first_form_description": "Với Biểu mẫu định hướng, bạn có thể hỏi một số câu sơ tuyển và định hướng đến đúng người hay đúng loại sự kiện.",
|
"create_your_first_form_description": "Với Biểu mẫu định hướng, bạn có thể hỏi một số câu sơ tuyển và định hướng đến đúng người hay đúng loại sự kiện.",
|
||||||
"create_your_first_webhook": "Tạo Webhook đầu tiên",
|
"create_your_first_webhook": "Tạo Webhook đầu tiên",
|
||||||
@@ -1876,10 +1876,10 @@
|
|||||||
"connect_google_workspace": "Kết nối 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 Không gian làm việc để dùng tính năng này",
|
"google_workspace_admin_tooltip": "Bạn phải là một Quản trị viên Không gian làm việc để dùng 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",
|
"first_event_type_webhook_description": "Tạo webhook đầu tiên của bạn cho loại sự kiện này",
|
||||||
"install_app_on": "Cài đặt ứng dụng trên",
|
"install_app_on": "Cài đặt ứng dụng cho",
|
||||||
"create_for": "Tạo cho",
|
"create_for": "Tạo cho",
|
||||||
"setup_organization": "Thiết lập một Tổ chức",
|
"setup_organization": "Thiết lập một Tổ chức",
|
||||||
"organization_banner_description": "Tạo một môi trường tại đó các nhóm của bạn có thể tạo những ứng dụng, tiến độ công việc và loại sự kiện mà có thể chia sẻ được, với chức năng đặt lịch dạng round-robin và tập thể.",
|
"organization_banner_description": "Tạo môi trường tại đó các nhóm của bạn có thể tạo những ứng dụng, tiến độ công việc và loại sự kiện mà có thể chia sẻ được, với chức năng đặt lịch dạng round-robin và tập thể.",
|
||||||
"organization_banner_title": "Quản lý các tổ chức có nhiều nhóm",
|
"organization_banner_title": "Quản lý các tổ chức có nhiều nhóm",
|
||||||
"set_up_your_organization": "Thiết lập tổ chức của bạn",
|
"set_up_your_organization": "Thiết lập tổ chức của bạn",
|
||||||
"organizations_description": "Tổ chức là môi trường chia sẻ tại đó các nhóm có thể tạo loại sự kiện, ứng dụng, tiến độ công việc, v. v. vốn được chia sẻ dùng chung.",
|
"organizations_description": "Tổ chức là môi trường chia sẻ tại đó các nhóm có thể tạo loại sự kiện, ứng dụng, tiến độ công việc, v. v. vốn được chia sẻ dùng chung.",
|
||||||
@@ -1918,7 +1918,7 @@
|
|||||||
"org_no_teams_yet_description": "Nếu bạn là một quản trị viên, hãy bảo đảm tạo nhóm các nhóm cần được hiển thị tại đây.",
|
"org_no_teams_yet_description": "Nếu bạn là một quản trị viên, hãy bảo đảm tạo nhóm các nhóm cần được hiển thị tại đây.",
|
||||||
"set_up": "Thiết lập",
|
"set_up": "Thiết lập",
|
||||||
"set_up_your_profile": "Thiết lập hồ sơ của bạn",
|
"set_up_your_profile": "Thiết lập hồ sơ của bạn",
|
||||||
"set_up_your_profile_description": "Hãy để mọi người biết bạn là ai trong {{orgName}}, và họ biết khi nhấp vào liên kết công cộng của bạn.",
|
"set_up_your_profile_description": "Hãy để mọi người biết bạn là ai trong {{orgName}}, và cho họ biết khi nhấp vào liên kết công cộng của bạn.",
|
||||||
"my_profile": "Hồ sơ của tôi",
|
"my_profile": "Hồ sơ của tôi",
|
||||||
"my_settings": "Cài đặt của tôi",
|
"my_settings": "Cài đặt của tôi",
|
||||||
"crm": "CRM",
|
"crm": "CRM",
|
||||||
@@ -1938,7 +1938,7 @@
|
|||||||
"insights_all_org_filter": "Tất cả",
|
"insights_all_org_filter": "Tất cả",
|
||||||
"insights_team_filter": "Nhóm: {{teamName}}",
|
"insights_team_filter": "Nhóm: {{teamName}}",
|
||||||
"insights_user_filter": "Người dùng: {{userName}}",
|
"insights_user_filter": "Người dùng: {{userName}}",
|
||||||
"insights_subtitle": "Xem số liệu của lịch hẹn ở mọi sự kiện của bạn",
|
"insights_subtitle": "Xem insights của lịch hẹn ở mọi sự kiện của bạn",
|
||||||
"custom_plan": "Gói Tuỳ chỉnh",
|
"custom_plan": "Gói Tuỳ chỉnh",
|
||||||
"ADD_NEW_STRINGS_ABOVE_THIS_LINE_TO_PREVENT_MERGE_CONFLICTS": "↑↑↑↑↑↑↑↑↑↑↑↑↑ Add your new strings above here ↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑"
|
"ADD_NEW_STRINGS_ABOVE_THIS_LINE_TO_PREVENT_MERGE_CONFLICTS": "↑↑↑↑↑↑↑↑↑↑↑↑↑ Add your new strings above here ↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑"
|
||||||
}
|
}
|
||||||
@@ -75,7 +75,7 @@
|
|||||||
/* Content/Text */
|
/* Content/Text */
|
||||||
--cal-text-emphasis: #f3f4f6;
|
--cal-text-emphasis: #f3f4f6;
|
||||||
--cal-text: #d6d6d6;
|
--cal-text: #d6d6d6;
|
||||||
--cal-text-subtle: #A5a5a5;
|
--cal-text-subtle: #a5a5a5;
|
||||||
--cal-text-muted: #575757;
|
--cal-text-muted: #575757;
|
||||||
--cal-text-inverted: #101010;
|
--cal-text-inverted: #101010;
|
||||||
|
|
||||||
@@ -100,7 +100,7 @@
|
|||||||
|
|
||||||
::selection {
|
::selection {
|
||||||
color: var(--cal-brand-text);
|
color: var(--cal-brand-text);
|
||||||
background: var(--cal-brand);
|
background: var(--cal-brand);
|
||||||
}
|
}
|
||||||
|
|
||||||
body {
|
body {
|
||||||
@@ -108,8 +108,6 @@ body {
|
|||||||
-webkit-font-smoothing: antialiased;
|
-webkit-font-smoothing: antialiased;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
/*
|
/*
|
||||||
Desktop App specific CSS
|
Desktop App specific CSS
|
||||||
https://docs.todesktop.com/
|
https://docs.todesktop.com/
|
||||||
@@ -170,17 +168,17 @@ html.todesktop aside {
|
|||||||
margin: 0px -6px;
|
margin: 0px -6px;
|
||||||
}
|
}
|
||||||
|
|
||||||
html.todesktop-platform-darwin .desktop-transparent{
|
html.todesktop-platform-darwin .desktop-transparent {
|
||||||
background: transparent !important;
|
background: transparent !important;
|
||||||
border: none !important;
|
border: none !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
html.todesktop-platform-darwin.dark main.bg-default{
|
html.todesktop-platform-darwin.dark main.bg-default {
|
||||||
background: rgba(0,0,0,0.6) !important
|
background: rgba(0, 0, 0, 0.6) !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
html.todesktop-platform-darwin.light main.bg-default{
|
html.todesktop-platform-darwin.light main.bg-default {
|
||||||
background: rgba(255,255,255,0.8) !important
|
background: rgba(255, 255, 255, 0.8) !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
@@ -247,13 +245,10 @@ select:focus {
|
|||||||
border-color: var(--brand-color);
|
border-color: var(--brand-color);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@layer components {
|
@layer components {
|
||||||
.scroll-bar {
|
.scroll-bar {
|
||||||
@apply scrollbar-thin scrollbar-thumb-rounded-md dark:scrollbar-thumb-darkgray-300 scrollbar-thumb-gray-300 scrollbar-track-transparent;
|
@apply scrollbar-thin scrollbar-thumb-rounded-md dark:scrollbar-thumb-darkgray-300 scrollbar-thumb-gray-300 scrollbar-track-transparent;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/* TODO: avoid global specific css */
|
/* TODO: avoid global specific css */
|
||||||
@@ -261,7 +256,6 @@ select:focus {
|
|||||||
transform: translateX(16px);
|
transform: translateX(16px);
|
||||||
} */
|
} */
|
||||||
|
|
||||||
|
|
||||||
@layer components {
|
@layer components {
|
||||||
/* slider */
|
/* slider */
|
||||||
.slider {
|
.slider {
|
||||||
@@ -512,5 +506,5 @@ select:focus {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.react-tel-input .flag-dropdown {
|
.react-tel-input .flag-dropdown {
|
||||||
@apply !border-r-default !border-y-0 !border-l-0 left-0.5;
|
@apply !border-r-default left-0.5 !border-y-0 !border-l-0;
|
||||||
}
|
}
|
||||||
@@ -3,4 +3,5 @@ const base = require("@calcom/config/tailwind-preset");
|
|||||||
module.exports = {
|
module.exports = {
|
||||||
...base,
|
...base,
|
||||||
content: [...base.content, "../../node_modules/@tremor/**/*.{js,ts,jsx,tsx}"],
|
content: [...base.content, "../../node_modules/@tremor/**/*.{js,ts,jsx,tsx}"],
|
||||||
|
plugins: [...base.plugins, require("tailwindcss-animate")],
|
||||||
};
|
};
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
# Set the version of docker compose to use
|
# Set the version of docker compose to use
|
||||||
version: '3.9'
|
version: "3.9"
|
||||||
|
|
||||||
# The containers that compose the project
|
# The containers that compose the project
|
||||||
services:
|
services:
|
||||||
@@ -8,8 +8,8 @@ services:
|
|||||||
restart: always
|
restart: always
|
||||||
container_name: integration-tests-prisma
|
container_name: integration-tests-prisma
|
||||||
ports:
|
ports:
|
||||||
- '5433:5432'
|
- "5433:5432"
|
||||||
environment:
|
environment:
|
||||||
POSTGRES_USER: prisma
|
POSTGRES_USER: prisma
|
||||||
POSTGRES_PASSWORD: prisma
|
POSTGRES_PASSWORD: prisma
|
||||||
POSTGRES_DB: tests
|
POSTGRES_DB: tests
|
||||||
+96
-97
@@ -1,10 +1,10 @@
|
|||||||
#
|
#
|
||||||
# Your Crowdin credentials
|
# Your Crowdin credentials
|
||||||
#
|
#
|
||||||
"project_id_env" : "CROWDIN_PROJECT_ID"
|
"project_id_env": "CROWDIN_PROJECT_ID"
|
||||||
"api_token_env" : "CROWDIN_PERSONAL_TOKEN"
|
"api_token_env": "CROWDIN_PERSONAL_TOKEN"
|
||||||
"base_path" : "."
|
"base_path": "."
|
||||||
"base_url" : "https://cal.crowdin.com"
|
"base_url": "https://cal.crowdin.com"
|
||||||
|
|
||||||
#
|
#
|
||||||
# Choose file structure in Crowdin
|
# Choose file structure in Crowdin
|
||||||
@@ -16,112 +16,111 @@
|
|||||||
# Files configuration
|
# Files configuration
|
||||||
#
|
#
|
||||||
files: [
|
files: [
|
||||||
{
|
{
|
||||||
#
|
#
|
||||||
# Source files filter
|
# Source files filter
|
||||||
# e.g. "/resources/en/*.json"
|
# e.g. "/resources/en/*.json"
|
||||||
#
|
#
|
||||||
"source" : "/apps/web/public/static/locales/en/common.json",
|
"source": "/apps/web/public/static/locales/en/common.json",
|
||||||
|
|
||||||
#
|
#
|
||||||
# Where translations will be placed
|
# Where translations will be placed
|
||||||
# e.g. "/resources/%two_letters_code%/%original_file_name%"
|
# e.g. "/resources/%two_letters_code%/%original_file_name%"
|
||||||
#
|
#
|
||||||
"translation" : "/apps/web/public/static/locales/%two_letters_code%/%original_file_name%",
|
"translation": "/apps/web/public/static/locales/%two_letters_code%/%original_file_name%",
|
||||||
|
#
|
||||||
|
# Files or directories for ignore
|
||||||
|
# e.g. ["/**/?.txt", "/**/[0-9].txt", "/**/*\?*.txt"]
|
||||||
|
#
|
||||||
|
#"ignore" : [],
|
||||||
|
|
||||||
#
|
#
|
||||||
# Files or directories for ignore
|
# The dest allows you to specify a file name in Crowdin
|
||||||
# e.g. ["/**/?.txt", "/**/[0-9].txt", "/**/*\?*.txt"]
|
# e.g. "/messages.json"
|
||||||
#
|
#
|
||||||
#"ignore" : [],
|
#"dest" : "",
|
||||||
|
|
||||||
#
|
#
|
||||||
# The dest allows you to specify a file name in Crowdin
|
# File type
|
||||||
# e.g. "/messages.json"
|
# e.g. "json"
|
||||||
#
|
#
|
||||||
#"dest" : "",
|
#"type" : "",
|
||||||
|
|
||||||
#
|
#
|
||||||
# File type
|
# The parameter "update_option" is optional. If it is not set, after the files update the translations for changed strings will be removed. Use to fix typos and for minor changes in the source strings
|
||||||
# e.g. "json"
|
# e.g. "update_as_unapproved" or "update_without_changes"
|
||||||
#
|
#
|
||||||
#"type" : "",
|
#"update_option" : "",
|
||||||
|
|
||||||
#
|
#
|
||||||
# The parameter "update_option" is optional. If it is not set, after the files update the translations for changed strings will be removed. Use to fix typos and for minor changes in the source strings
|
# Start block (for XML only)
|
||||||
# e.g. "update_as_unapproved" or "update_without_changes"
|
#
|
||||||
#
|
|
||||||
#"update_option" : "",
|
|
||||||
|
|
||||||
#
|
#
|
||||||
# Start block (for XML only)
|
# Defines whether to translate tags attributes.
|
||||||
#
|
# e.g. 0 or 1 (Default is 1)
|
||||||
|
#
|
||||||
|
# "translate_attributes" : 1,
|
||||||
|
|
||||||
#
|
#
|
||||||
# Defines whether to translate tags attributes.
|
# Defines whether to translate texts placed inside the tags.
|
||||||
# e.g. 0 or 1 (Default is 1)
|
# e.g. 0 or 1 (Default is 1)
|
||||||
#
|
#
|
||||||
# "translate_attributes" : 1,
|
# "translate_content" : 1,
|
||||||
|
|
||||||
#
|
#
|
||||||
# Defines whether to translate texts placed inside the tags.
|
# This is an array of strings, where each item is the XPaths to DOM element that should be imported
|
||||||
# e.g. 0 or 1 (Default is 1)
|
# e.g. ["/content/text", "/content/text[@value]"]
|
||||||
#
|
#
|
||||||
# "translate_content" : 1,
|
# "translatable_elements" : [],
|
||||||
|
|
||||||
#
|
#
|
||||||
# This is an array of strings, where each item is the XPaths to DOM element that should be imported
|
# Defines whether to split long texts into smaller text segments
|
||||||
# e.g. ["/content/text", "/content/text[@value]"]
|
# e.g. 0 or 1 (Default is 1)
|
||||||
#
|
#
|
||||||
# "translatable_elements" : [],
|
# "content_segmentation" : 1,
|
||||||
|
|
||||||
#
|
#
|
||||||
# Defines whether to split long texts into smaller text segments
|
# End block (for XML only)
|
||||||
# e.g. 0 or 1 (Default is 1)
|
#
|
||||||
#
|
|
||||||
# "content_segmentation" : 1,
|
|
||||||
|
|
||||||
#
|
#
|
||||||
# End block (for XML only)
|
# Start .properties block
|
||||||
#
|
#
|
||||||
|
|
||||||
#
|
#
|
||||||
# Start .properties block
|
# Defines whether single quote should be escaped by another single quote or backslash in exported translations
|
||||||
#
|
# e.g. 0 or 1 or 2 or 3 (Default is 3)
|
||||||
|
# 0 - do not escape single quote;
|
||||||
|
# 1 - escape single quote by another single quote;
|
||||||
|
# 2 - escape single quote by backslash;
|
||||||
|
# 3 - escape single quote by another single quote only in strings containing variables ( {0} ).
|
||||||
|
#
|
||||||
|
# "escape_quotes" : 3,
|
||||||
|
|
||||||
#
|
#
|
||||||
# Defines whether single quote should be escaped by another single quote or backslash in exported translations
|
# Defines whether any special characters (=, :, ! and #) should be escaped by backslash in exported translations.
|
||||||
# e.g. 0 or 1 or 2 or 3 (Default is 3)
|
# e.g. 0 or 1 (Default is 0)
|
||||||
# 0 - do not escape single quote;
|
# 0 - do not escape special characters
|
||||||
# 1 - escape single quote by another single quote;
|
# 1 - escape special characters by a backslash
|
||||||
# 2 - escape single quote by backslash;
|
#
|
||||||
# 3 - escape single quote by another single quote only in strings containing variables ( {0} ).
|
# "escape_special_characters": 0
|
||||||
#
|
#
|
||||||
# "escape_quotes" : 3,
|
|
||||||
|
|
||||||
#
|
#
|
||||||
# Defines whether any special characters (=, :, ! and #) should be escaped by backslash in exported translations.
|
# End .properties block
|
||||||
# e.g. 0 or 1 (Default is 0)
|
#
|
||||||
# 0 - do not escape special characters
|
|
||||||
# 1 - escape special characters by a backslash
|
|
||||||
#
|
|
||||||
# "escape_special_characters": 0
|
|
||||||
#
|
|
||||||
|
|
||||||
#
|
#
|
||||||
# End .properties block
|
# Does the first line contain header?
|
||||||
#
|
# e.g. true or false
|
||||||
|
#
|
||||||
|
#"first_line_contains_header" : true,
|
||||||
|
|
||||||
#
|
#
|
||||||
# Does the first line contain header?
|
# for spreadsheets
|
||||||
# e.g. true or false
|
# e.g. "identifier,source_phrase,context,uk,ru,fr"
|
||||||
#
|
#
|
||||||
#"first_line_contains_header" : true,
|
# "scheme" : "",
|
||||||
|
},
|
||||||
#
|
]
|
||||||
# for spreadsheets
|
|
||||||
# e.g. "identifier,source_phrase,context,uk,ru,fr"
|
|
||||||
#
|
|
||||||
# "scheme" : "",
|
|
||||||
}
|
|
||||||
]
|
|
||||||
+16
-18
@@ -1,27 +1,25 @@
|
|||||||
|
|
||||||
// see https://inlang.com/
|
// see https://inlang.com/
|
||||||
export async function defineConfig(env) {
|
export async function defineConfig(env) {
|
||||||
|
const { default: i18nextPlugin } = await env.$import(
|
||||||
const { default: i18nextPlugin } = await env.$import(
|
"https://cdn.jsdelivr.net/npm/@inlang/plugin-i18next@2/dist/index.js"
|
||||||
"https://cdn.jsdelivr.net/npm/@inlang/plugin-i18next@2/dist/index.js",
|
);
|
||||||
)
|
|
||||||
|
|
||||||
const { default: standardLintRules } = await env.$import(
|
const { default: standardLintRules } = await env.$import(
|
||||||
"https://cdn.jsdelivr.net/npm/@inlang/plugin-standard-lint-rules@3/dist/index.js",
|
"https://cdn.jsdelivr.net/npm/@inlang/plugin-standard-lint-rules@3/dist/index.js"
|
||||||
)
|
);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
referenceLanguage: "en",
|
referenceLanguage: "en",
|
||||||
plugins: [
|
plugins: [
|
||||||
i18nextPlugin({
|
i18nextPlugin({
|
||||||
pathPattern: "./apps/web/public/static/locales/{language}/common.json",
|
pathPattern: "./apps/web/public/static/locales/{language}/common.json",
|
||||||
}),
|
}),
|
||||||
standardLintRules({
|
standardLintRules({
|
||||||
// deactivating identical pattern because of nested
|
// deactivating identical pattern because of nested
|
||||||
// resources like "de-DE" and "de-AT" where "de-AT"
|
// resources like "de-DE" and "de-AT" where "de-AT"
|
||||||
// contrains overwrites but the majority are identical patterns.
|
// contrains overwrites but the majority are identical patterns.
|
||||||
identicalPattern: "off"
|
identicalPattern: "off",
|
||||||
}),
|
}),
|
||||||
],
|
],
|
||||||
}
|
};
|
||||||
}
|
}
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
import path from "path";
|
|
||||||
import os from "os";
|
import os from "os";
|
||||||
|
import path from "path";
|
||||||
|
|
||||||
export const APP_STORE_PATH = path.join(__dirname, "..", "..", "app-store");
|
export const APP_STORE_PATH = path.join(__dirname, "..", "..", "app-store");
|
||||||
export const TEMPLATES_PATH = path.join(APP_STORE_PATH, "templates");
|
export const TEMPLATES_PATH = path.join(APP_STORE_PATH, "templates");
|
||||||
export const IS_WINDOWS_PLATFORM = os.platform() === "win32";
|
export const IS_WINDOWS_PLATFORM = os.platform() === "win32";
|
||||||
@@ -18,12 +18,14 @@ export const getEventTypeAppData = <T extends EventTypeAppsList>(
|
|||||||
return allowDataGet
|
return allowDataGet
|
||||||
? {
|
? {
|
||||||
...appMetadata,
|
...appMetadata,
|
||||||
|
// We should favor eventType's price and currency over appMetadata's price and currency
|
||||||
|
price: eventType.price || appMetadata.price,
|
||||||
|
currency: eventType.currency || appMetadata.currency,
|
||||||
// trackingId is legacy way to store value for TRACKING_ID. So, we need to support both.
|
// trackingId is legacy way to store value for TRACKING_ID. So, we need to support both.
|
||||||
TRACKING_ID: appMetadata.TRACKING_ID || appMetadata.trackingId,
|
TRACKING_ID: appMetadata.TRACKING_ID || appMetadata.trackingId,
|
||||||
}
|
}
|
||||||
: null;
|
: null;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Backward compatibility for existing event types.
|
// Backward compatibility for existing event types.
|
||||||
// TODO: After the new AppStore EventType App flow is stable, write a migration to migrate metadata to new format which will let us remove this compatibility code
|
// TODO: After the new AppStore EventType App flow is stable, write a migration to migrate metadata to new format which will let us remove this compatibility code
|
||||||
// Migration isn't being done right now, to allow a revert if needed
|
// Migration isn't being done right now, to allow a revert if needed
|
||||||
|
|||||||
@@ -64,13 +64,17 @@ export default function AppleCalendarSetup() {
|
|||||||
router.push(json.url);
|
router.push(json.url);
|
||||||
}
|
}
|
||||||
}}>
|
}}>
|
||||||
<fieldset className="space-y-2" disabled={form.formState.isSubmitting}>
|
<fieldset
|
||||||
|
className="space-y-2"
|
||||||
|
disabled={form.formState.isSubmitting}
|
||||||
|
data-testid="apple-calendar-form">
|
||||||
<TextField
|
<TextField
|
||||||
required
|
required
|
||||||
type="text"
|
type="text"
|
||||||
{...form.register("username")}
|
{...form.register("username")}
|
||||||
label="Apple ID"
|
label="Apple ID"
|
||||||
placeholder="appleid@domain.com"
|
placeholder="appleid@domain.com"
|
||||||
|
data-testid="apple-calendar-email"
|
||||||
/>
|
/>
|
||||||
<TextField
|
<TextField
|
||||||
required
|
required
|
||||||
@@ -79,6 +83,7 @@ export default function AppleCalendarSetup() {
|
|||||||
label={t("password")}
|
label={t("password")}
|
||||||
placeholder="•••••••••••••"
|
placeholder="•••••••••••••"
|
||||||
autoComplete="password"
|
autoComplete="password"
|
||||||
|
data-testid="apple-calendar-password"
|
||||||
/>
|
/>
|
||||||
</fieldset>
|
</fieldset>
|
||||||
|
|
||||||
@@ -87,7 +92,10 @@ export default function AppleCalendarSetup() {
|
|||||||
<Button type="button" color="secondary" onClick={() => router.back()}>
|
<Button type="button" color="secondary" onClick={() => router.back()}>
|
||||||
{t("cancel")}
|
{t("cancel")}
|
||||||
</Button>
|
</Button>
|
||||||
<Button type="submit" loading={form.formState.isSubmitting}>
|
<Button
|
||||||
|
type="submit"
|
||||||
|
loading={form.formState.isSubmitting}
|
||||||
|
data-testid="apple-calendar-login-button">
|
||||||
{t("save")}
|
{t("save")}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import { useState } from "react";
|
|||||||
|
|
||||||
import { useAppContextWithSchema } from "@calcom/app-store/EventTypeAppContext";
|
import { useAppContextWithSchema } from "@calcom/app-store/EventTypeAppContext";
|
||||||
import AppCard from "@calcom/app-store/_components/AppCard";
|
import AppCard from "@calcom/app-store/_components/AppCard";
|
||||||
import { currencyOptions } from "@calcom/app-store/paypal/pages/setup";
|
import { currencyOptions } from "@calcom/app-store/paypal/lib/currencyOptions";
|
||||||
import type { EventTypeAppCardComponent } from "@calcom/app-store/types";
|
import type { EventTypeAppCardComponent } from "@calcom/app-store/types";
|
||||||
import { WEBAPP_URL } from "@calcom/lib/constants";
|
import { WEBAPP_URL } from "@calcom/lib/constants";
|
||||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||||
|
|||||||
@@ -12,12 +12,25 @@ class Paypal {
|
|||||||
accessToken: string | null = null;
|
accessToken: string | null = null;
|
||||||
expiresAt: number | null = null;
|
expiresAt: number | null = null;
|
||||||
|
|
||||||
constructor({ clientId, secretKey }: { clientId: string; secretKey: string }) {
|
constructor(opts: { clientId: string; secretKey: string }) {
|
||||||
this.url = IS_PRODUCTION ? "https://api-m.paypal.com" : "https://api-m.sandbox.paypal.com";
|
this.url = IS_PRODUCTION ? "https://api-m.paypal.com" : "https://api-m.sandbox.paypal.com";
|
||||||
this.clientId = clientId;
|
this.clientId = opts.clientId;
|
||||||
this.secretKey = secretKey;
|
this.secretKey = opts.secretKey;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fetcher = async (endpoint: string, init?: RequestInit | undefined) => {
|
||||||
|
this.getAccessToken();
|
||||||
|
return fetch(`${this.url}${endpoint}`, {
|
||||||
|
method: "get",
|
||||||
|
...init,
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
Authorization: `Bearer ${this.accessToken}`,
|
||||||
|
...init?.headers,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
async getAccessToken(): Promise<void> {
|
async getAccessToken(): Promise<void> {
|
||||||
if (this.accessToken && this.expiresAt && this.expiresAt > Date.now()) {
|
if (this.accessToken && this.expiresAt && this.expiresAt > Date.now()) {
|
||||||
return;
|
return;
|
||||||
@@ -64,15 +77,6 @@ class Paypal {
|
|||||||
cancelUrl: string;
|
cancelUrl: string;
|
||||||
intent?: "CAPTURE" | "AUTHORIZE";
|
intent?: "CAPTURE" | "AUTHORIZE";
|
||||||
}): Promise<CreateOrderResponse> {
|
}): Promise<CreateOrderResponse> {
|
||||||
// Always get a new access token
|
|
||||||
await this.getAccessToken();
|
|
||||||
|
|
||||||
const headers = {
|
|
||||||
"Content-Type": "application/json",
|
|
||||||
Authorization: `Bearer ${this.accessToken}`,
|
|
||||||
"PayPal-Request-Id": uuidv4(),
|
|
||||||
};
|
|
||||||
|
|
||||||
const createOrderRequestBody: CreateOrderRequestBody = {
|
const createOrderRequestBody: CreateOrderRequestBody = {
|
||||||
intent,
|
intent,
|
||||||
purchase_units: [
|
purchase_units: [
|
||||||
@@ -96,9 +100,11 @@ class Paypal {
|
|||||||
};
|
};
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const response = await fetch(`${this.url}/v2/checkout/orders`, {
|
const response = await this.fetcher("/v2/checkout/orders", {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
headers,
|
headers: {
|
||||||
|
"PayPal-Request-Id": uuidv4(),
|
||||||
|
},
|
||||||
body: JSON.stringify(createOrderRequestBody),
|
body: JSON.stringify(createOrderRequestBody),
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -116,16 +122,8 @@ class Paypal {
|
|||||||
|
|
||||||
async captureOrder(orderId: string): Promise<boolean> {
|
async captureOrder(orderId: string): Promise<boolean> {
|
||||||
try {
|
try {
|
||||||
await this.getAccessToken();
|
const captureResult = await this.fetcher(`/v2/checkout/orders/${orderId}/capture`, {
|
||||||
|
|
||||||
const headers = {
|
|
||||||
"Content-Type": "application/json",
|
|
||||||
Authorization: `Bearer ${this.accessToken}`,
|
|
||||||
};
|
|
||||||
|
|
||||||
const captureResult = await fetch(`${this.url}/v2/checkout/orders/${orderId}/capture`, {
|
|
||||||
method: "POST",
|
method: "POST",
|
||||||
headers,
|
|
||||||
});
|
});
|
||||||
if (captureResult.ok) {
|
if (captureResult.ok) {
|
||||||
const result = await captureResult.json();
|
const result = await captureResult.json();
|
||||||
@@ -181,14 +179,6 @@ class Paypal {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async createWebhook(): Promise<boolean | string> {
|
async createWebhook(): Promise<boolean | string> {
|
||||||
// Always get a new access token
|
|
||||||
await this.getAccessToken();
|
|
||||||
|
|
||||||
const headers = {
|
|
||||||
"Content-Type": "application/json",
|
|
||||||
Authorization: `Bearer ${this.accessToken}`,
|
|
||||||
};
|
|
||||||
|
|
||||||
const body = {
|
const body = {
|
||||||
url: `${WEBAPP_URL}/api/integrations/paypal/webhook`,
|
url: `${WEBAPP_URL}/api/integrations/paypal/webhook`,
|
||||||
event_types: [
|
event_types: [
|
||||||
@@ -202,9 +192,8 @@ class Paypal {
|
|||||||
};
|
};
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const response = await fetch(`${this.url}/v1/notifications/webhooks`, {
|
const response = await this.fetcher(`/v1/notifications/webhooks`, {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
headers,
|
|
||||||
body: JSON.stringify(body),
|
body: JSON.stringify(body),
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -220,19 +209,8 @@ class Paypal {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async listWebhooks(): Promise<string[]> {
|
async listWebhooks(): Promise<string[]> {
|
||||||
// Always get a new access token
|
|
||||||
await this.getAccessToken();
|
|
||||||
|
|
||||||
const headers = {
|
|
||||||
"Content-Type": "application/json",
|
|
||||||
Authorization: `Bearer ${this.accessToken}`,
|
|
||||||
};
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const response = await fetch(`${this.url}/v1/notifications/webhooks`, {
|
const response = await this.fetcher(`/v1/notifications/webhooks`);
|
||||||
method: "GET",
|
|
||||||
headers,
|
|
||||||
});
|
|
||||||
|
|
||||||
if (response.ok) {
|
if (response.ok) {
|
||||||
const { webhooks } = await response.json();
|
const { webhooks } = await response.json();
|
||||||
@@ -251,18 +229,9 @@ class Paypal {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async deleteWebhook(webhookId: string): Promise<boolean> {
|
async deleteWebhook(webhookId: string): Promise<boolean> {
|
||||||
// Always get a new access token
|
|
||||||
await this.getAccessToken();
|
|
||||||
|
|
||||||
const headers = {
|
|
||||||
"Content-Type": "application/json",
|
|
||||||
Authorization: `Bearer ${this.accessToken}`,
|
|
||||||
};
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const response = await fetch(`${this.url}/v1/notifications/webhooks/${webhookId}`, {
|
const response = await this.fetcher(`/v1/notifications/webhooks/${webhookId}`, {
|
||||||
method: "DELETE",
|
method: "DELETE",
|
||||||
headers,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
if (response.ok) {
|
if (response.ok) {
|
||||||
@@ -286,7 +255,6 @@ class Paypal {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async verifyWebhook(options: WebhookEventVerifyRequest): Promise<void> {
|
async verifyWebhook(options: WebhookEventVerifyRequest): Promise<void> {
|
||||||
const url = `${this.url}/v1/notifications/verify-webhook-signature`;
|
|
||||||
const parseRequest = webhookEventVerifyRequestSchema.safeParse(options);
|
const parseRequest = webhookEventVerifyRequestSchema.safeParse(options);
|
||||||
|
|
||||||
// Webhook event should be parsable
|
// Webhook event should be parsable
|
||||||
@@ -307,12 +275,8 @@ class Paypal {
|
|||||||
const bodyToString = stringy.slice(0, -1) + `,"webhook_event":${options.body.webhook_event}` + "}";
|
const bodyToString = stringy.slice(0, -1) + `,"webhook_event":${options.body.webhook_event}` + "}";
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const response = await fetch(url, {
|
const response = await this.fetcher(`/v1/notifications/verify-webhook-signature`, {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
headers: {
|
|
||||||
"Content-Type": "application/json",
|
|
||||||
Authorization: `Bearer ${this.accessToken}`,
|
|
||||||
},
|
|
||||||
body: bodyToString,
|
body: bodyToString,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,27 @@
|
|||||||
|
export const currencyOptions = [
|
||||||
|
{ label: "United States dollar", value: "USD" },
|
||||||
|
{ label: "Australian dollar", value: "AUD" },
|
||||||
|
{ label: "Brazilian real 2", value: "BRL" },
|
||||||
|
{ label: "Canadian dollar", value: "CAD" },
|
||||||
|
{ label: "Chinese Renmenbi 3", value: "CNY" },
|
||||||
|
{ label: "Czech koruna", value: "CZK" },
|
||||||
|
{ label: "Danish krone", value: "DKK" },
|
||||||
|
{ label: "Euro", value: "EUR" },
|
||||||
|
{ label: "Hong Kong dollar", value: "HKD" },
|
||||||
|
{ label: "Hungarian forint 1", value: "HUF" },
|
||||||
|
{ label: "Israeli new shekel", value: "ILS" },
|
||||||
|
{ label: "Japanese yen 1", value: "JPY" },
|
||||||
|
{ label: "Malaysian ringgit 3", value: "MYR" },
|
||||||
|
{ label: "Mexican peso", value: "MXN" },
|
||||||
|
{ label: "New Taiwan dollar 1", value: "TWD" },
|
||||||
|
{ label: "New Zealand dollar", value: "NZD" },
|
||||||
|
{ label: "Norwegian krone", value: "NOK" },
|
||||||
|
{ label: "Philippine peso", value: "PHP" },
|
||||||
|
{ label: "Polish złoty", value: "PLN" },
|
||||||
|
{ label: "Pound sterling", value: "GBP" },
|
||||||
|
{ label: "Russian ruble", value: "RUB" },
|
||||||
|
{ label: "Singapore dollar", value: "SGD" },
|
||||||
|
{ label: "Swedish krona", value: "SEK" },
|
||||||
|
{ label: "Swiss franc", value: "CHF" },
|
||||||
|
{ label: "Thai baht", value: "THB" },
|
||||||
|
];
|
||||||
@@ -0,0 +1,47 @@
|
|||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
|
import type { UpdateAppCredentialsOptions } from "@calcom/trpc/server/routers/viewer/apps/updateAppCredentials.handler";
|
||||||
|
|
||||||
|
import Paypal from "./Paypal";
|
||||||
|
|
||||||
|
const schema = z.object({
|
||||||
|
credentialId: z.coerce.number(),
|
||||||
|
key: z.object({
|
||||||
|
client_id: z.string(),
|
||||||
|
secret_key: z.string(),
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
const handlePaypalValidations = async ({ input }: UpdateAppCredentialsOptions) => {
|
||||||
|
const validated = schema.safeParse(input);
|
||||||
|
if (!validated.success) throw new Error("Invalid input");
|
||||||
|
const { key } = validated.data;
|
||||||
|
|
||||||
|
// Test credentials before saving
|
||||||
|
const paypalClient = new Paypal({ clientId: key.client_id, secretKey: key.secret_key });
|
||||||
|
const test = await paypalClient.test();
|
||||||
|
if (!test) throw new Error("Provided credentials failed to authenticate");
|
||||||
|
|
||||||
|
// Delete all existing webhooks
|
||||||
|
const webhooksToDelete = await paypalClient.listWebhooks();
|
||||||
|
if (webhooksToDelete) {
|
||||||
|
const promises = webhooksToDelete.map((webhook) => paypalClient.deleteWebhook(webhook));
|
||||||
|
await Promise.all(promises);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create webhook for this installation
|
||||||
|
const webhookId = await paypalClient.createWebhook();
|
||||||
|
if (!webhookId) {
|
||||||
|
// @TODO: make a button that tries to create the webhook again
|
||||||
|
console.error("Failed to create webhook", webhookId);
|
||||||
|
throw new Error("Failed to create webhook");
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
client_id: key.client_id,
|
||||||
|
secret_key: key.secret_key,
|
||||||
|
webhook_id: webhookId,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export default handlePaypalValidations;
|
||||||
@@ -1,57 +1,20 @@
|
|||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { useRouter } from "next/router";
|
import { useRouter } from "next/navigation";
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import { Toaster } from "react-hot-toast";
|
import { Toaster } from "react-hot-toast";
|
||||||
|
|
||||||
import Paypal from "@calcom/app-store/paypal/lib/Paypal";
|
|
||||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||||
import { trpc } from "@calcom/trpc";
|
import { trpc } from "@calcom/trpc";
|
||||||
import { Button, showToast, TextField } from "@calcom/ui";
|
import { Button, showToast, TextField } from "@calcom/ui";
|
||||||
|
|
||||||
export interface PayPalSetupProps {
|
export default function PayPalSetup() {
|
||||||
public_key: string;
|
|
||||||
access_token: string;
|
|
||||||
currency: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const currencyOptions = [
|
|
||||||
{ label: "United States dollar", value: "USD" },
|
|
||||||
{ label: "Australian dollar", value: "AUD" },
|
|
||||||
{ label: "Brazilian real 2", value: "BRL" },
|
|
||||||
{ label: "Canadian dollar", value: "CAD" },
|
|
||||||
{ label: "Chinese Renmenbi 3", value: "CNY" },
|
|
||||||
{ label: "Czech koruna", value: "CZK" },
|
|
||||||
{ label: "Danish krone", value: "DKK" },
|
|
||||||
{ label: "Euro", value: "EUR" },
|
|
||||||
{ label: "Hong Kong dollar", value: "HKD" },
|
|
||||||
{ label: "Hungarian forint 1", value: "HUF" },
|
|
||||||
{ label: "Israeli new shekel", value: "ILS" },
|
|
||||||
{ label: "Japanese yen 1", value: "JPY" },
|
|
||||||
{ label: "Malaysian ringgit 3", value: "MYR" },
|
|
||||||
{ label: "Mexican peso", value: "MXN" },
|
|
||||||
{ label: "New Taiwan dollar 1", value: "TWD" },
|
|
||||||
{ label: "New Zealand dollar", value: "NZD" },
|
|
||||||
{ label: "Norwegian krone", value: "NOK" },
|
|
||||||
{ label: "Philippine peso", value: "PHP" },
|
|
||||||
{ label: "Polish złoty", value: "PLN" },
|
|
||||||
{ label: "Pound sterling", value: "GBP" },
|
|
||||||
{ label: "Russian ruble", value: "RUB" },
|
|
||||||
{ label: "Singapore dollar", value: "SGD" },
|
|
||||||
{ label: "Swedish krona", value: "SEK" },
|
|
||||||
{ label: "Swiss franc", value: "CHF" },
|
|
||||||
{ label: "Thai baht", value: "THB" },
|
|
||||||
];
|
|
||||||
|
|
||||||
export default function PayPalSetup(props: PayPalSetupProps) {
|
|
||||||
const [newClientId, setNewClientId] = useState("");
|
const [newClientId, setNewClientId] = useState("");
|
||||||
const [newSecretKey, setNewSecretKey] = useState("");
|
const [newSecretKey, setNewSecretKey] = useState("");
|
||||||
|
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const { t } = useLocale();
|
const { t } = useLocale();
|
||||||
|
const integrations = trpc.viewer.integrations.useQuery({ variant: "payment", appId: "paypal" });
|
||||||
const integrations = trpc.viewer.integrations.useQuery({ variant: "payment" });
|
const [paypalPaymentAppCredentials] = integrations.data?.items || [];
|
||||||
const paypalPaymentAppCredentials = integrations.data?.items.find((item) => item.type === "paypal_payment");
|
const [credentialId] = paypalPaymentAppCredentials?.userCredentialIds || [-1];
|
||||||
const [credentialId] = paypalPaymentAppCredentials?.userCredentialIds || [false];
|
|
||||||
const showContent = !!integrations.data && integrations.isSuccess && !!credentialId;
|
const showContent = !!integrations.data && integrations.isSuccess && !!credentialId;
|
||||||
const saveKeysMutation = trpc.viewer.appsRouter.updateAppCredentials.useMutation({
|
const saveKeysMutation = trpc.viewer.appsRouter.updateAppCredentials.useMutation({
|
||||||
onSuccess: () => {
|
onSuccess: () => {
|
||||||
@@ -63,48 +26,6 @@ export default function PayPalSetup(props: PayPalSetupProps) {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const saveKeys = async (key: { clientId: string; secretKey: string }) => {
|
|
||||||
if (!key.clientId || !key.secretKey || key.clientId === key.secretKey) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
if (typeof credentialId !== "number") {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Test credentials before saving
|
|
||||||
const paypalClient = new Paypal({ clientId: key.clientId, secretKey: key.secretKey });
|
|
||||||
const test = await paypalClient.test();
|
|
||||||
if (!test) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Delete all existing webhooks
|
|
||||||
const webhooksToDelete = await paypalClient.listWebhooks();
|
|
||||||
if (webhooksToDelete) {
|
|
||||||
for (const webhook of webhooksToDelete) {
|
|
||||||
await paypalClient.deleteWebhook(webhook);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create webhook for this installation
|
|
||||||
const webhookId = await paypalClient.createWebhook();
|
|
||||||
if (!webhookId) {
|
|
||||||
// @TODO: make a button that tries to create the webhook again
|
|
||||||
console.log("Failed to create webhook", webhookId);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
saveKeysMutation.mutate({
|
|
||||||
credentialId,
|
|
||||||
key: {
|
|
||||||
client_id: key.clientId,
|
|
||||||
secret_key: key.secretKey,
|
|
||||||
webhook_id: webhookId,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
return true;
|
|
||||||
};
|
|
||||||
|
|
||||||
if (integrations.isLoading) {
|
if (integrations.isLoading) {
|
||||||
return <div className="absolute z-50 flex h-screen w-full items-center bg-gray-200" />;
|
return <div className="absolute z-50 flex h-screen w-full items-center bg-gray-200" />;
|
||||||
}
|
}
|
||||||
@@ -144,12 +65,15 @@ export default function PayPalSetup(props: PayPalSetupProps) {
|
|||||||
<div className="mt-5 flex flex-row justify-end">
|
<div className="mt-5 flex flex-row justify-end">
|
||||||
<Button
|
<Button
|
||||||
color="secondary"
|
color="secondary"
|
||||||
onClick={() =>
|
onClick={() => {
|
||||||
saveKeys({
|
saveKeysMutation.mutate({
|
||||||
clientId: newClientId,
|
credentialId,
|
||||||
secretKey: newSecretKey,
|
key: {
|
||||||
})
|
client_id: newClientId,
|
||||||
}>
|
secret_key: newSecretKey,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}}>
|
||||||
{t("save")}
|
{t("save")}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -56,7 +56,6 @@
|
|||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
.cal-query-builder .rule--drag-handler,
|
.cal-query-builder .rule--drag-handler,
|
||||||
.cal-query-builder .rule--header {
|
.cal-query-builder .rule--header {
|
||||||
display: flex;
|
display: flex;
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
import type { TFunction } from "next-i18next";
|
import type { TFunction } from "next-i18next";
|
||||||
|
|
||||||
import type { CredentialDataWithTeamName } from "@calcom/app-store/utils";
|
import type { CredentialDataWithTeamName } from "@calcom/app-store/utils";
|
||||||
import getEnabledApps from "@calcom/lib/apps/getEnabledApps";
|
import { defaultVideoAppCategories } from "@calcom/app-store/utils";
|
||||||
|
import getEnabledAppsFromCredentials from "@calcom/lib/apps/getEnabledAppsFromCredentials";
|
||||||
import { prisma } from "@calcom/prisma";
|
import { prisma } from "@calcom/prisma";
|
||||||
import { AppCategories } from "@calcom/prisma/enums";
|
import { AppCategories } from "@calcom/prisma/enums";
|
||||||
|
|
||||||
@@ -56,7 +57,7 @@ export async function getLocationGroupedOptions(
|
|||||||
...idToSearchObject,
|
...idToSearchObject,
|
||||||
app: {
|
app: {
|
||||||
categories: {
|
categories: {
|
||||||
hasSome: [AppCategories.conferencing, AppCategories.video],
|
hasSome: defaultVideoAppCategories,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -76,29 +77,27 @@ export async function getLocationGroupedOptions(
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const integrations = await getEnabledApps(credentials, true);
|
const integrations = await getEnabledAppsFromCredentials(credentials, { filterOnCredentials: true });
|
||||||
|
|
||||||
integrations.forEach((app) => {
|
integrations.forEach((app) => {
|
||||||
|
// All apps that are labeled as a locationOption are video apps.
|
||||||
if (app.locationOption) {
|
if (app.locationOption) {
|
||||||
// All apps that are labeled as a locationOption are video apps. Extract the secondary category if available
|
// All apps that are labeled as a locationOption are video apps. Extract the secondary category if available
|
||||||
let category =
|
let groupByCategory =
|
||||||
app.categories.length >= 2
|
app.categories.length >= 2
|
||||||
? app.categories.find(
|
? app.categories.find((groupByCategory) => !defaultVideoAppCategories.includes(groupByCategory))
|
||||||
(category) =>
|
: app.categories[0] || app.category;
|
||||||
!([AppCategories.video, AppCategories.conferencing] as string[]).includes(category)
|
if (!groupByCategory) groupByCategory = AppCategories.conferencing;
|
||||||
)
|
|
||||||
: app.category;
|
|
||||||
if (!category) category = AppCategories.conferencing;
|
|
||||||
|
|
||||||
for (const credential of app.credentials) {
|
for (const credential of app.credentials) {
|
||||||
const label = `${app.locationOption.label} ${
|
const label = `${app.locationOption.label} ${
|
||||||
credential.team?.name ? `(${credential.team.name})` : ""
|
credential.team?.name ? `(${credential.team.name})` : ""
|
||||||
}`;
|
}`;
|
||||||
const option = { ...app.locationOption, label, icon: app.logo, slug: app.slug, credential };
|
const option = { ...app.locationOption, label, icon: app.logo, slug: app.slug, credential };
|
||||||
if (apps[category]) {
|
if (apps[groupByCategory]) {
|
||||||
apps[category] = [...apps[category], option];
|
apps[groupByCategory] = [...apps[groupByCategory], option];
|
||||||
} else {
|
} else {
|
||||||
apps[category] = [option];
|
apps[groupByCategory] = [option];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,9 +6,7 @@
|
|||||||
"logo": "icon.svg",
|
"logo": "icon.svg",
|
||||||
"url": "https://skiff.com/blog/skiff-cal-com-launch",
|
"url": "https://skiff.com/blog/skiff-cal-com-launch",
|
||||||
"variant": "calendar",
|
"variant": "calendar",
|
||||||
"categories": [
|
"categories": ["calendar"],
|
||||||
"calendar"
|
|
||||||
],
|
|
||||||
"publisher": "Skiff World, Inc.",
|
"publisher": "Skiff World, Inc.",
|
||||||
"email": "hello@skiff.org",
|
"email": "hello@skiff.org",
|
||||||
"description": "End-to-end encrypted, private calendar, email, notes, and drive.",
|
"description": "End-to-end encrypted, private calendar, email, notes, and drive.",
|
||||||
@@ -16,4 +14,4 @@
|
|||||||
"__createdUsingCli": true,
|
"__createdUsingCli": true,
|
||||||
"__template": "link-as-an-app",
|
"__template": "link-as-an-app",
|
||||||
"dirName": "skiff"
|
"dirName": "skiff"
|
||||||
}
|
}
|
||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import type { AppCategories } from "@prisma/client";
|
||||||
import { Prisma } from "@prisma/client";
|
import { Prisma } from "@prisma/client";
|
||||||
|
|
||||||
// If you import this file on any app it should produce circular dependency
|
// If you import this file on any app it should produce circular dependency
|
||||||
@@ -143,8 +144,17 @@ export function doesAppSupportTeamInstall(
|
|||||||
concurrentMeetings: boolean | undefined = undefined
|
concurrentMeetings: boolean | undefined = undefined
|
||||||
) {
|
) {
|
||||||
return !appCategories.some(
|
return !appCategories.some(
|
||||||
(category) => category === "calendar" || (category === "conferencing" && !concurrentMeetings)
|
(category) =>
|
||||||
|
category === "calendar" ||
|
||||||
|
(defaultVideoAppCategories.includes(category as AppCategories) && !concurrentMeetings)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const defaultVideoAppCategories: AppCategories[] = [
|
||||||
|
"conferencing",
|
||||||
|
"messaging",
|
||||||
|
// Legacy name for conferencing
|
||||||
|
"video",
|
||||||
|
];
|
||||||
|
|
||||||
export default getApps;
|
export default getApps;
|
||||||
@@ -1,11 +1,13 @@
|
|||||||
import { Canvas, Meta, Story, ArgsTable } from '@storybook/addon-docs';
|
import { Canvas, Meta, Story, ArgsTable } from "@storybook/addon-docs";
|
||||||
import { Title } from '@calcom/storybook/components'
|
|
||||||
|
import { Title } from "@calcom/storybook/components";
|
||||||
import { Icon } from "@calcom/ui";
|
import { Icon } from "@calcom/ui";
|
||||||
import { Booker } from './Booker';
|
|
||||||
|
import { Booker } from "./Booker";
|
||||||
|
|
||||||
<Meta title="Atoms/Booker" component={Booker} />
|
<Meta title="Atoms/Booker" component={Booker} />
|
||||||
|
|
||||||
<Title title="Booker"/>
|
<Title title="Booker" />
|
||||||
|
|
||||||
<Canvas>
|
<Canvas>
|
||||||
<Story name="Booker">
|
<Story name="Booker">
|
||||||
|
|||||||
@@ -7,4 +7,4 @@
|
|||||||
@tailwind components;
|
@tailwind components;
|
||||||
@tailwind utilities;
|
@tailwind utilities;
|
||||||
|
|
||||||
@import "../ui/styles/shared-globals.css";
|
@import "../ui/styles/shared-globals.css";
|
||||||
@@ -1,46 +1,44 @@
|
|||||||
|
|
||||||
:root {
|
:root {
|
||||||
/* background */
|
/* background */
|
||||||
|
|
||||||
--cal-bg-emphasis: #E5E7EB;
|
--cal-bg-emphasis: #e5e7eb;
|
||||||
--cal-bg: white;
|
--cal-bg: white;
|
||||||
--cal-bg-subtle: #F3F4F6;
|
--cal-bg-subtle: #f3f4f6;
|
||||||
--cal-bg-muted: #F9FAFB;
|
--cal-bg-muted: #f9fafb;
|
||||||
--cal-bg-inverted: #111827;
|
--cal-bg-inverted: #111827;
|
||||||
|
|
||||||
/* background -> components*/
|
/* background -> components*/
|
||||||
--cal-bg-info: #DEE9FC;
|
--cal-bg-info: #dee9fc;
|
||||||
--cal-bg-success: #E2FBE8;
|
--cal-bg-success: #e2fbe8;
|
||||||
--cal-bg-attention: #FCEED8;
|
--cal-bg-attention: #fceed8;
|
||||||
--cal-bg-error: #F9E3E2;
|
--cal-bg-error: #f9e3e2;
|
||||||
--cal-bg-dark-error: #752522;
|
--cal-bg-dark-error: #752522;
|
||||||
|
|
||||||
|
|
||||||
/* Borders */
|
/* Borders */
|
||||||
--cal-border-emphasis:#9CA3AF;
|
--cal-border-emphasis: #9ca3af;
|
||||||
--cal-border: #D1D5DB;
|
--cal-border: #d1d5db;
|
||||||
--cal-border-subtle:#E5E7EB;
|
--cal-border-subtle: #e5e7eb;
|
||||||
--cal-border-muted:#F3F4F6;
|
--cal-border-muted: #f3f4f6;
|
||||||
--cal-border-error: #aa2e26;
|
--cal-border-error: #aa2e26;
|
||||||
|
|
||||||
/* Content/Text */
|
/* Content/Text */
|
||||||
--cal-text-emphasis: #111827;
|
--cal-text-emphasis: #111827;
|
||||||
--cal-text:#374151;
|
--cal-text: #374151;
|
||||||
--cal-text-subtle:#6B7280;
|
--cal-text-subtle: #6b7280;
|
||||||
--cal-text-muted:#9CA3AF;
|
--cal-text-muted: #9ca3af;
|
||||||
--cal-text-inverted:white;
|
--cal-text-inverted: white;
|
||||||
|
|
||||||
/* Content/Text -> components */
|
/* Content/Text -> components */
|
||||||
--cal-text-info:#253985;
|
--cal-text-info: #253985;
|
||||||
--cal-text-success:#285231;
|
--cal-text-success: #285231;
|
||||||
--cal-text-attention:#73321B;
|
--cal-text-attention: #73321b;
|
||||||
--cal-text-error:#752522;
|
--cal-text-error: #752522;
|
||||||
|
|
||||||
/* Brand shenanigans
|
/* Brand shenanigans
|
||||||
-> These will be computed for the users theme at runtime.
|
-> These will be computed for the users theme at runtime.
|
||||||
*/
|
*/
|
||||||
--cal-brand:#111827;
|
--cal-brand: #111827;
|
||||||
--cal-brand-emphasis:#101010;
|
--cal-brand-emphasis: #101010;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Darkmode Colors -- TODO: WIP*/
|
/* Darkmode Colors -- TODO: WIP*/
|
||||||
@@ -54,37 +52,35 @@
|
|||||||
--cal-bg-inverted: #f3f4f6;
|
--cal-bg-inverted: #f3f4f6;
|
||||||
|
|
||||||
/* background -> components*/
|
/* background -> components*/
|
||||||
--cal-bg-info: #DEE9FC;
|
--cal-bg-info: #dee9fc;
|
||||||
--cal-bg-success: #E2FBE8;
|
--cal-bg-success: #e2fbe8;
|
||||||
--cal-bg-attention: #FCEED8;
|
--cal-bg-attention: #fceed8;
|
||||||
--cal-bg-error: #F9E3E2;
|
--cal-bg-error: #f9e3e2;
|
||||||
--cal-bg-dark-error: #752522;
|
--cal-bg-dark-error: #752522;
|
||||||
|
|
||||||
|
|
||||||
/* Borders */
|
/* Borders */
|
||||||
--cal-border-emphasis:#575757;
|
--cal-border-emphasis: #575757;
|
||||||
--cal-border: #444444;
|
--cal-border: #444444;
|
||||||
--cal-border-subtle:#2b2b2b;
|
--cal-border-subtle: #2b2b2b;
|
||||||
--cal-border-muted:#1c1c1c;
|
--cal-border-muted: #1c1c1c;
|
||||||
--cal-border-error: #aa2e26;
|
--cal-border-error: #aa2e26;
|
||||||
|
|
||||||
/* Content/Text */
|
/* Content/Text */
|
||||||
--cal-text-emphasis: #f3f4f6;
|
--cal-text-emphasis: #f3f4f6;
|
||||||
--cal-text:#d6d6d6;
|
--cal-text: #d6d6d6;
|
||||||
--cal-text-subtle:#767676;
|
--cal-text-subtle: #767676;
|
||||||
--cal-text-muted:#575757;
|
--cal-text-muted: #575757;
|
||||||
--cal-text-inverted:white;
|
--cal-text-inverted: white;
|
||||||
|
|
||||||
/* Content/Text -> components */
|
/* Content/Text -> components */
|
||||||
--cal-text-info:#253985;
|
--cal-text-info: #253985;
|
||||||
--cal-text-success:#285231;
|
--cal-text-success: #285231;
|
||||||
--cal-text-attention:#73321B;
|
--cal-text-attention: #73321b;
|
||||||
--cal-text-error:#752522;
|
--cal-text-error: #752522;
|
||||||
|
|
||||||
/* Brand shenanigans
|
/* Brand shenanigans
|
||||||
-> These will be computed for the users theme at runtime.
|
-> These will be computed for the users theme at runtime.
|
||||||
*/
|
*/
|
||||||
--cal-brand:#111827;
|
--cal-brand: #111827;
|
||||||
--cal-brand-emphasis:#101010;
|
--cal-brand-emphasis: #101010;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "@calcom/debugging",
|
"name": "@calcom/debugging",
|
||||||
"description": "Debugging utilities",
|
"description": "Debugging utilities",
|
||||||
"private": true,
|
"private": true,
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"main": "./index.ts"
|
"main": "./index.ts"
|
||||||
}
|
}
|
||||||
@@ -1,15 +1,14 @@
|
|||||||
{
|
{
|
||||||
"extends": "@calcom/tsconfig/react-library.json",
|
"extends": "@calcom/tsconfig/react-library.json",
|
||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
"resolveJsonModule": true
|
"resolveJsonModule": true
|
||||||
},
|
},
|
||||||
"include": [
|
"include": [
|
||||||
"../../apps/web/next-env.d.ts",
|
"../../apps/web/next-env.d.ts",
|
||||||
"../types/*.d.ts",
|
"../types/*.d.ts",
|
||||||
"../types/next-auth.d.ts",
|
"../types/next-auth.d.ts",
|
||||||
"**/*.ts",
|
"**/*.ts",
|
||||||
"**/*.tsx"
|
"**/*.tsx"
|
||||||
],
|
],
|
||||||
"exclude": ["dist", "build", "node_modules"]
|
"exclude": ["dist", "build", "node_modules"]
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1,8 +1,8 @@
|
|||||||
# Starts mailhog SMTP server on port 1025, web interface on port 8025
|
# Starts mailhog SMTP server on port 1025, web interface on port 8025
|
||||||
version: "3.6"
|
version: "3.6"
|
||||||
services:
|
services:
|
||||||
mailhog:
|
mailhog:
|
||||||
image: 'mailhog/mailhog:latest'
|
image: "mailhog/mailhog:latest"
|
||||||
ports:
|
ports:
|
||||||
- "1025:1025"
|
- "1025:1025"
|
||||||
- "8025:8025"
|
- "8025:8025"
|
||||||
@@ -153,3 +153,10 @@ export async function rescheduleEvent(username: string, frame: Frame, page: Page
|
|||||||
await expect(frame.locator("[data-testid=success-page]")).toBeVisible();
|
await expect(frame.locator("[data-testid=success-page]")).toBeVisible();
|
||||||
return booking;
|
return booking;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function installAppleCalendar(page: Page) {
|
||||||
|
await page.goto("/apps/categories/calendar");
|
||||||
|
await page.click('[data-testid="app-store-app-card-apple-calendar"]');
|
||||||
|
await page.waitForURL("/apps/apple-calendar");
|
||||||
|
await page.click('[data-testid="install-app-button"]');
|
||||||
|
}
|
||||||
@@ -13,7 +13,7 @@
|
|||||||
margin: 10px;
|
margin: 10px;
|
||||||
}
|
}
|
||||||
.dark {
|
.dark {
|
||||||
background-color:rgb(16 16 16);
|
background-color: rgb(16 16 16);
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
<script>
|
<script>
|
||||||
|
|||||||
@@ -58,8 +58,8 @@
|
|||||||
border-style: solid;
|
border-style: solid;
|
||||||
-webkit-animation: loader 2s infinite ease;
|
-webkit-animation: loader 2s infinite ease;
|
||||||
animation: loader 2s infinite ease;
|
animation: loader 2s infinite ease;
|
||||||
}
|
}
|
||||||
|
|
||||||
.loader.modal-loader {
|
.loader.modal-loader {
|
||||||
margin: 60px auto;
|
margin: 60px auto;
|
||||||
}
|
}
|
||||||
@@ -2,7 +2,7 @@
|
|||||||
@tailwind components;
|
@tailwind components;
|
||||||
@tailwind utilities;
|
@tailwind utilities;
|
||||||
@font-face {
|
@font-face {
|
||||||
font-family: 'Cal Sans';
|
font-family: "Cal Sans";
|
||||||
src: url("https://cal.com/cal.ttf");
|
src: url("https://cal.com/cal.ttf");
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -12,11 +12,14 @@ h3,
|
|||||||
h4,
|
h4,
|
||||||
h5,
|
h5,
|
||||||
h6 {
|
h6 {
|
||||||
font-family: 'Cal Sans';
|
font-family: "Cal Sans";
|
||||||
font-weight: normal;
|
font-weight: normal;
|
||||||
letter-spacing: normal;
|
letter-spacing: normal;
|
||||||
}
|
}
|
||||||
|
|
||||||
html, body, :host {
|
html,
|
||||||
font-family: ui-sans-serif,system-ui,-apple-system,BlinkMacSystemFont,Segoe UI,Roboto,Helvetica Neue,Arial,Noto Sans,sans-serif,Apple Color Emoji,Segoe UI Emoji,Segoe UI Symbol,Noto Color Emoji
|
body,
|
||||||
}
|
:host {
|
||||||
|
font-family: ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, Segoe UI, Roboto, Helvetica Neue,
|
||||||
|
Arial, Noto Sans, sans-serif, Apple Color Emoji, Segoe UI Emoji, Segoe UI Symbol, Noto Color Emoji;
|
||||||
|
}
|
||||||
@@ -148,7 +148,10 @@ const providers: Provider[] = [
|
|||||||
throw new Error(ErrorCode.InternalServerError);
|
throw new Error(ErrorCode.InternalServerError);
|
||||||
}
|
}
|
||||||
|
|
||||||
const isValidToken = (await import("otplib")).authenticator.check(credentials.totpCode, secret);
|
const isValidToken = (await import("@calcom/lib/totp")).totpAuthenticatorCheck(
|
||||||
|
credentials.totpCode,
|
||||||
|
secret
|
||||||
|
);
|
||||||
if (!isValidToken) {
|
if (!isValidToken) {
|
||||||
throw new Error(ErrorCode.IncorrectTwoFactorCode);
|
throw new Error(ErrorCode.IncorrectTwoFactorCode);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -20,7 +20,6 @@ import {
|
|||||||
mapRecurringBookingToMutationInput,
|
mapRecurringBookingToMutationInput,
|
||||||
useTimePreferences,
|
useTimePreferences,
|
||||||
} from "@calcom/features/bookings/lib";
|
} from "@calcom/features/bookings/lib";
|
||||||
import { getBookingFieldsWithSystemFields } from "@calcom/features/bookings/lib/getBookingFields";
|
|
||||||
import getBookingResponsesSchema, {
|
import getBookingResponsesSchema, {
|
||||||
getBookingResponsesPartialSchema,
|
getBookingResponsesPartialSchema,
|
||||||
} from "@calcom/features/bookings/lib/getBookingResponsesSchema";
|
} from "@calcom/features/bookings/lib/getBookingResponsesSchema";
|
||||||
@@ -179,14 +178,11 @@ export const BookEventForm = ({ onCancel }: BookEventFormProps) => {
|
|||||||
return defaults;
|
return defaults;
|
||||||
}, [eventType?.bookingFields, formValues, isRescheduling, bookingData, rescheduleUid]);
|
}, [eventType?.bookingFields, formValues, isRescheduling, bookingData, rescheduleUid]);
|
||||||
|
|
||||||
const disableBookingTitle = !!event.data?.isDynamic;
|
|
||||||
const bookingFormSchema = z
|
const bookingFormSchema = z
|
||||||
.object({
|
.object({
|
||||||
responses: event?.data
|
responses: event?.data
|
||||||
? getBookingResponsesSchema({
|
? getBookingResponsesSchema({
|
||||||
eventType: {
|
eventType: event?.data,
|
||||||
bookingFields: getBookingFieldsWithSystemFields({ ...event.data, disableBookingTitle }),
|
|
||||||
},
|
|
||||||
view: rescheduleUid ? "reschedule" : "booking",
|
view: rescheduleUid ? "reschedule" : "booking",
|
||||||
})
|
})
|
||||||
: // Fallback until event is loaded.
|
: // Fallback until event is loaded.
|
||||||
|
|||||||
@@ -9,35 +9,42 @@ import { useBookerStore } from "../../store";
|
|||||||
import { useEvent } from "../../utils/event";
|
import { useEvent } from "../../utils/event";
|
||||||
import { BookEventForm } from "./BookEventForm";
|
import { BookEventForm } from "./BookEventForm";
|
||||||
|
|
||||||
export function BookFormAsModal({ visible, onCancel }: { visible: boolean; onCancel: () => void }) {
|
const BookEventFormWrapper = ({ onCancel }: { onCancel: () => void }) => {
|
||||||
const { t } = useLocale();
|
const { t } = useLocale();
|
||||||
const selectedTimeslot = useBookerStore((state) => state.selectedTimeslot);
|
const selectedTimeslot = useBookerStore((state) => state.selectedTimeslot);
|
||||||
const selectedDuration = useBookerStore((state) => state.selectedDuration);
|
const selectedDuration = useBookerStore((state) => state.selectedDuration);
|
||||||
const { data } = useEvent();
|
const { data } = useEvent();
|
||||||
const parsedSelectedTimeslot = dayjs(selectedTimeslot);
|
const parsedSelectedTimeslot = dayjs(selectedTimeslot);
|
||||||
const { timeFormat, timezone } = useTimePreferences();
|
const { timeFormat, timezone } = useTimePreferences();
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<h1 className="font-cal text-emphasis text-xl leading-5">{t("confirm_your_details")} </h1>
|
||||||
|
<div className="my-4 flex space-x-2 rounded-md leading-none">
|
||||||
|
<Badge variant="grayWithoutHover" startIcon={Calendar} size="lg">
|
||||||
|
<span>
|
||||||
|
{parsedSelectedTimeslot.format("LL")} {parsedSelectedTimeslot.tz(timezone).format(timeFormat)}
|
||||||
|
</span>
|
||||||
|
</Badge>
|
||||||
|
{(selectedDuration || data?.length) && (
|
||||||
|
<Badge variant="grayWithoutHover" startIcon={Clock} size="lg">
|
||||||
|
<span>{selectedDuration || data?.length}</span>
|
||||||
|
</Badge>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<BookEventForm onCancel={onCancel} />
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const BookFormAsModal = ({ visible, onCancel }: { visible: boolean; onCancel: () => void }) => {
|
||||||
return (
|
return (
|
||||||
<Dialog open={visible} onOpenChange={onCancel}>
|
<Dialog open={visible} onOpenChange={onCancel}>
|
||||||
<DialogContent
|
<DialogContent
|
||||||
type={undefined}
|
type={undefined}
|
||||||
enableOverflow
|
enableOverflow
|
||||||
className="[&_.modalsticky]:border-t-subtle [&_.modalsticky]:bg-default max-h-[80vh] pb-0 [&_.modalsticky]:sticky [&_.modalsticky]:bottom-0 [&_.modalsticky]:left-0 [&_.modalsticky]:right-0 [&_.modalsticky]:-mx-8 [&_.modalsticky]:border-t [&_.modalsticky]:px-8 [&_.modalsticky]:py-4">
|
className="[&_.modalsticky]:border-t-subtle [&_.modalsticky]:bg-default max-h-[80vh] pb-0 [&_.modalsticky]:sticky [&_.modalsticky]:bottom-0 [&_.modalsticky]:left-0 [&_.modalsticky]:right-0 [&_.modalsticky]:-mx-8 [&_.modalsticky]:border-t [&_.modalsticky]:px-8 [&_.modalsticky]:py-4">
|
||||||
<h1 className="font-cal text-emphasis text-xl leading-5">{t("confirm_your_details")} </h1>
|
<BookEventFormWrapper onCancel={onCancel} />
|
||||||
<div className="my-4 flex space-x-2 rounded-md leading-none">
|
|
||||||
<Badge variant="grayWithoutHover" startIcon={Calendar} size="lg">
|
|
||||||
<span>
|
|
||||||
{parsedSelectedTimeslot.format("LL")} {parsedSelectedTimeslot.tz(timezone).format(timeFormat)}
|
|
||||||
</span>
|
|
||||||
</Badge>
|
|
||||||
{(selectedDuration || data?.length) && (
|
|
||||||
<Badge variant="grayWithoutHover" startIcon={Clock} size="lg">
|
|
||||||
<span>{selectedDuration || data?.length}</span>
|
|
||||||
</Badge>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
<BookEventForm onCancel={onCancel} />
|
|
||||||
</DialogContent>
|
</DialogContent>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
);
|
);
|
||||||
}
|
};
|
||||||
@@ -7,7 +7,6 @@ import { EventDetails, EventMembers, EventMetaSkeleton, EventTitle } from "@calc
|
|||||||
import { EventMetaBlock } from "@calcom/features/bookings/components/event-meta/Details";
|
import { EventMetaBlock } from "@calcom/features/bookings/components/event-meta/Details";
|
||||||
import { useTimePreferences } from "@calcom/features/bookings/lib";
|
import { useTimePreferences } from "@calcom/features/bookings/lib";
|
||||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||||
import { markdownToSafeHTML } from "@calcom/lib/markdownToSafeHTML";
|
|
||||||
import { Calendar, Globe, User } from "@calcom/ui/components/icon";
|
import { Calendar, Globe, User } from "@calcom/ui/components/icon";
|
||||||
|
|
||||||
import { fadeInUp } from "../config";
|
import { fadeInUp } from "../config";
|
||||||
@@ -68,7 +67,7 @@ export const EventMeta = () => {
|
|||||||
<EventTitle className="my-2">{event?.title}</EventTitle>
|
<EventTitle className="my-2">{event?.title}</EventTitle>
|
||||||
{event.description && (
|
{event.description && (
|
||||||
<EventMetaBlock contentClassName="mb-8 break-words max-w-full max-h-[180px] scroll-bar pr-4">
|
<EventMetaBlock contentClassName="mb-8 break-words max-w-full max-h-[180px] scroll-bar pr-4">
|
||||||
<div dangerouslySetInnerHTML={{ __html: markdownToSafeHTML(event.description) }} />
|
<div dangerouslySetInnerHTML={{ __html: event.description }} />
|
||||||
</EventMetaBlock>
|
</EventMetaBlock>
|
||||||
)}
|
)}
|
||||||
<div className="space-y-4 font-medium rtl:-mr-2">
|
<div className="space-y-4 font-medium rtl:-mr-2">
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ interface BookerSeoProps {
|
|||||||
eventSlug: string;
|
eventSlug: string;
|
||||||
rescheduleUid: string | undefined;
|
rescheduleUid: string | undefined;
|
||||||
hideBranding?: boolean;
|
hideBranding?: boolean;
|
||||||
|
isSEOIndexable?: boolean;
|
||||||
isTeamEvent?: boolean;
|
isTeamEvent?: boolean;
|
||||||
entity: {
|
entity: {
|
||||||
orgSlug?: string | null;
|
orgSlug?: string | null;
|
||||||
@@ -16,7 +17,7 @@ interface BookerSeoProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const BookerSeo = (props: BookerSeoProps) => {
|
export const BookerSeo = (props: BookerSeoProps) => {
|
||||||
const { eventSlug, username, rescheduleUid, hideBranding, isTeamEvent, entity } = props;
|
const { eventSlug, username, rescheduleUid, hideBranding, isTeamEvent, entity, isSEOIndexable } = props;
|
||||||
const { t } = useLocale();
|
const { t } = useLocale();
|
||||||
const { data: event } = trpc.viewer.public.event.useQuery(
|
const { data: event } = trpc.viewer.public.event.useQuery(
|
||||||
{ username, eventSlug, isTeamEvent, org: entity.orgSlug ?? null },
|
{ username, eventSlug, isTeamEvent, org: entity.orgSlug ?? null },
|
||||||
@@ -41,8 +42,8 @@ export const BookerSeo = (props: BookerSeoProps) => {
|
|||||||
],
|
],
|
||||||
}}
|
}}
|
||||||
nextSeoProps={{
|
nextSeoProps={{
|
||||||
nofollow: event?.hidden,
|
nofollow: event?.hidden || !isSEOIndexable,
|
||||||
noindex: event?.hidden,
|
noindex: event?.hidden || !isSEOIndexable,
|
||||||
}}
|
}}
|
||||||
isBrandingHidden={hideBranding}
|
isBrandingHidden={hideBranding}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -1,38 +1,48 @@
|
|||||||
import { Canvas, Meta, Story, ArgsTable } from '@storybook/addon-docs';
|
import { Canvas, Meta, Story, ArgsTable } from "@storybook/addon-docs";
|
||||||
import { Examples, Example, Note, Title, VariantsTable, VariantColumn, RowTitles, CustomArgsTable} from '@calcom/storybook/components'
|
|
||||||
|
import {
|
||||||
|
Examples,
|
||||||
|
Example,
|
||||||
|
Note,
|
||||||
|
Title,
|
||||||
|
VariantsTable,
|
||||||
|
VariantColumn,
|
||||||
|
RowTitles,
|
||||||
|
CustomArgsTable,
|
||||||
|
} from "@calcom/storybook/components";
|
||||||
import { Icon } from "@calcom/ui";
|
import { Icon } from "@calcom/ui";
|
||||||
|
|
||||||
import { EventDetails } from './Details';
|
import { EventDetails } from "./Details";
|
||||||
import { EventTitle } from './Title';
|
import { EventMembers } from "./Members";
|
||||||
import { EventMembers } from './Members';
|
import { EventTitle } from "./Title";
|
||||||
import { mockEvent } from './event.mock.ts';
|
import { mockEvent } from "./event.mock.ts";
|
||||||
|
|
||||||
<Meta title="Features/Events/Meta" component={EventDetails} />
|
<Meta title="Features/Events/Meta" component={EventDetails} />
|
||||||
|
|
||||||
<Title title="Event Meta" suffix="Brief" subtitle="Version 2.0 — Last Update: 12 Dec 2022"/>
|
<Title title="Event Meta" suffix="Brief" subtitle="Version 2.0 — Last Update: 12 Dec 2022" />
|
||||||
|
|
||||||
<Examples title="Combined event meta block">
|
<Examples title="Combined event meta block">
|
||||||
<div style={{maxWidth: 300}}>
|
<div style={{ maxWidth: 300 }}>
|
||||||
<Example title="Event Title">
|
<Example title="Event Title">
|
||||||
<EventTitle event={mockEvent}/>
|
<EventTitle event={mockEvent} />
|
||||||
</Example>
|
</Example>
|
||||||
<Example title="Event Details">
|
<Example title="Event Details">
|
||||||
<EventDetails event={mockEvent}/>
|
<EventDetails event={mockEvent} />
|
||||||
</Example>
|
</Example>
|
||||||
</div>
|
</div>
|
||||||
</Examples>
|
</Examples>
|
||||||
|
|
||||||
<Canvas>
|
<Canvas>
|
||||||
<Story name="All variants">
|
<Story name="All variants">
|
||||||
<VariantsTable titles={['Event Meta Components']} columnMinWidth={150}>
|
<VariantsTable titles={["Event Meta Components"]} columnMinWidth={150}>
|
||||||
<VariantRow variant="">
|
<VariantRow variant="">
|
||||||
<div style={{maxWidth: 300}}>
|
<div style={{ maxWidth: 300 }}>
|
||||||
<EventMembers users={
|
<EventMembers
|
||||||
[
|
users={[
|
||||||
{name: "Pro example", username: "pro"},
|
{ name: "Pro example", username: "pro" },
|
||||||
{name: "Team example", username: "team"}
|
{ name: "Team example", username: "team" },
|
||||||
]
|
]}
|
||||||
} />
|
/>
|
||||||
<EventTitle>Quick catch-up</EventTitle>
|
<EventTitle>Quick catch-up</EventTitle>
|
||||||
<EventDetails event={mockEvent} />
|
<EventDetails event={mockEvent} />
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -10,3 +10,5 @@ export const SystemField = z.enum([
|
|||||||
"rescheduleReason",
|
"rescheduleReason",
|
||||||
"smsReminderNumber",
|
"smsReminderNumber",
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
export const SMS_REMINDER_NUMBER_FIELD = "smsReminderNumber";
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
import type { EventTypeCustomInput, EventType, Prisma, Workflow } from "@prisma/client";
|
import type { EventTypeCustomInput, EventType, Prisma, Workflow } from "@prisma/client";
|
||||||
import type { z } from "zod";
|
import type { z } from "zod";
|
||||||
|
|
||||||
|
import { SMS_REMINDER_NUMBER_FIELD } from "@calcom/features/bookings/lib/SystemField";
|
||||||
import { fieldsThatSupportLabelAsSafeHtml } from "@calcom/features/form-builder/fieldsThatSupportLabelAsSafeHtml";
|
import { fieldsThatSupportLabelAsSafeHtml } from "@calcom/features/form-builder/fieldsThatSupportLabelAsSafeHtml";
|
||||||
import { markdownToSafeHTML } from "@calcom/lib/markdownToSafeHTML";
|
import { markdownToSafeHTML } from "@calcom/lib/markdownToSafeHTML";
|
||||||
import slugify from "@calcom/lib/slugify";
|
import slugify from "@calcom/lib/slugify";
|
||||||
@@ -15,11 +16,10 @@ import {
|
|||||||
type Fields = z.infer<typeof eventTypeBookingFields>;
|
type Fields = z.infer<typeof eventTypeBookingFields>;
|
||||||
|
|
||||||
if (typeof window !== "undefined") {
|
if (typeof window !== "undefined") {
|
||||||
console.warn("This file should not be imported on the client side");
|
// This file imports some costly dependencies, so we want to make sure it's not imported on the client side.
|
||||||
|
throw new Error("`getBookingFields` must not be imported on the client side.");
|
||||||
}
|
}
|
||||||
|
|
||||||
export const SMS_REMINDER_NUMBER_FIELD = "smsReminderNumber";
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* PHONE -> Phone
|
* PHONE -> Phone
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import type { EventType, Prisma } from "@prisma/client";
|
||||||
import type z from "zod";
|
import type z from "zod";
|
||||||
|
|
||||||
import { SystemField } from "@calcom/features/bookings/lib/SystemField";
|
import { SystemField } from "@calcom/features/bookings/lib/SystemField";
|
||||||
@@ -6,8 +7,6 @@ import { getBookingWithResponses } from "@calcom/lib/getBooking";
|
|||||||
import { eventTypeBookingFields } from "@calcom/prisma/zod-utils";
|
import { eventTypeBookingFields } from "@calcom/prisma/zod-utils";
|
||||||
import type { CalendarEvent } from "@calcom/types/Calendar";
|
import type { CalendarEvent } from "@calcom/types/Calendar";
|
||||||
|
|
||||||
import type { EventType, Prisma } from ".prisma/client";
|
|
||||||
|
|
||||||
export const getCalEventResponses = ({
|
export const getCalEventResponses = ({
|
||||||
bookingFields,
|
bookingFields,
|
||||||
booking,
|
booking,
|
||||||
|
|||||||
@@ -55,30 +55,31 @@ export const Template = (args) => <Calendar {...args} />;
|
|||||||
</Canvas>
|
</Canvas>
|
||||||
|
|
||||||
<Canvas>
|
<Canvas>
|
||||||
<Story name="Onclick Handlers"
|
<Story
|
||||||
args={{
|
name="Onclick Handlers"
|
||||||
startHour: 8,
|
args={{
|
||||||
endHour: 17,
|
startHour: 8,
|
||||||
hoverEventDuration: 30,
|
endHour: 17,
|
||||||
}}
|
hoverEventDuration: 30,
|
||||||
argTypes={{
|
}}
|
||||||
startHour: {
|
argTypes={{
|
||||||
control: { type: "number", min: 0, max: 23, step: 1 },
|
startHour: {
|
||||||
},
|
control: { type: "number", min: 0, max: 23, step: 1 },
|
||||||
endHour: {
|
},
|
||||||
control: { type: "number", min: 0, max: 23, step: 1 },
|
endHour: {
|
||||||
},
|
control: { type: "number", min: 0, max: 23, step: 1 },
|
||||||
hoverEventDuration: {
|
},
|
||||||
control: { type: "number", min: 0, max: 60, step: 1 },
|
hoverEventDuration: {
|
||||||
},
|
control: { type: "number", min: 0, max: 60, step: 1 },
|
||||||
}}
|
},
|
||||||
>
|
}}>
|
||||||
{({...args}) => (
|
{({ ...args }) => (
|
||||||
<Calendar {...args}
|
<Calendar
|
||||||
events={events}
|
{...args}
|
||||||
onEventClick={(e) => alert(e.title)}
|
events={events}
|
||||||
onEmptyCellClick={(date) => alert(date.toString())}
|
onEventClick={(e) => alert(e.title)}
|
||||||
sortEvents
|
onEmptyCellClick={(date) => alert(date.toString())}
|
||||||
|
sortEvents
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</Story>
|
</Story>
|
||||||
|
|||||||
@@ -1,18 +1,20 @@
|
|||||||
.scheduler-wrapper {
|
.scheduler-wrapper {
|
||||||
--gridDefaultSize: 1.75rem;
|
--gridDefaultSize: 1.75rem;
|
||||||
--hoursInDay: 24;
|
--hoursInDay: 24;
|
||||||
--minuteInHour:60;
|
--minuteInHour: 60;
|
||||||
--gridMobileSize: 100vw;
|
--gridMobileSize: 100vw;
|
||||||
--calendar-offset-top: 28px;
|
--calendar-offset-top: 28px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.scheduler-grid-row-template{
|
.scheduler-grid-row-template {
|
||||||
grid-template-rows: var(--gridMobileSize) repeat(attr(data-gridstoperperday number), var(--gridMobileSize)) auto;
|
grid-template-rows: var(--gridMobileSize) repeat(attr(data-gridstoperperday number), var(--gridMobileSize)) auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (min-width: 640px) {
|
@media (min-width: 640px) {
|
||||||
.scheduler-grid-row-template {
|
.scheduler-grid-row-template {
|
||||||
grid-template-rows: var(--gridDefaultSize) repeat(attr(data-gridstoperperday number), var(--gridDefaultSize)) auto;
|
grid-template-rows: var(--gridDefaultSize) repeat(
|
||||||
|
attr(data-gridstoperperday number),
|
||||||
|
var(--gridDefaultSize)
|
||||||
|
) auto;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
import { ALLOWED_HOSTNAMES, RESERVED_SUBDOMAINS, WEBAPP_URL } from "@calcom/lib/constants";
|
import type { Prisma } from "@prisma/client";
|
||||||
|
|
||||||
import type { Prisma } from ".prisma/client";
|
import { ALLOWED_HOSTNAMES, RESERVED_SUBDOMAINS, WEBAPP_URL } from "@calcom/lib/constants";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* return the org slug
|
* return the org slug
|
||||||
|
|||||||
@@ -57,7 +57,7 @@ export default function OtherTeamListItem(props: Props) {
|
|||||||
<span className="text-muted block text-xs">
|
<span className="text-muted block text-xs">
|
||||||
{team.slug
|
{team.slug
|
||||||
? orgBranding
|
? orgBranding
|
||||||
? `${orgBranding.fullDomain}${team.slug}`
|
? `${orgBranding.fullDomain}/${team.slug}`
|
||||||
: `${process.env.NEXT_PUBLIC_WEBSITE_URL}/team/${team.slug}`
|
: `${process.env.NEXT_PUBLIC_WEBSITE_URL}/team/${team.slug}`
|
||||||
: "Unpublished team"}
|
: "Unpublished team"}
|
||||||
</span>
|
</span>
|
||||||
@@ -81,8 +81,8 @@ export default function OtherTeamListItem(props: Props) {
|
|||||||
`${
|
`${
|
||||||
orgBranding
|
orgBranding
|
||||||
? `${orgBranding.fullDomain}`
|
? `${orgBranding.fullDomain}`
|
||||||
: process.env.NEXT_PUBLIC_WEBSITE_URL + "/team/"
|
: process.env.NEXT_PUBLIC_WEBSITE_URL + "/team"
|
||||||
}${team.slug}`
|
}/${team.slug}`
|
||||||
);
|
);
|
||||||
showToast(t("link_copied"), "success");
|
showToast(t("link_copied"), "success");
|
||||||
}}
|
}}
|
||||||
@@ -119,8 +119,8 @@ export default function OtherTeamListItem(props: Props) {
|
|||||||
href={`${
|
href={`${
|
||||||
orgBranding
|
orgBranding
|
||||||
? `${orgBranding.fullDomain}`
|
? `${orgBranding.fullDomain}`
|
||||||
: `${process.env.NEXT_PUBLIC_WEBSITE_URL}/team/other/`
|
: `${process.env.NEXT_PUBLIC_WEBSITE_URL}/team/other`
|
||||||
}${team.slug}`}
|
}/${team.slug}`}
|
||||||
StartIcon={ExternalLink}>
|
StartIcon={ExternalLink}>
|
||||||
{t("preview_team") as string}
|
{t("preview_team") as string}
|
||||||
</DropdownItem>
|
</DropdownItem>
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import type { Payment } from "@prisma/client";
|
import type { Payment } from "@prisma/client";
|
||||||
|
import type { EventType } from "@prisma/client";
|
||||||
import { Elements, PaymentElement, useElements, useStripe } from "@stripe/react-stripe-js";
|
import { Elements, PaymentElement, useElements, useStripe } from "@stripe/react-stripe-js";
|
||||||
import type stripejs from "@stripe/stripe-js";
|
import type stripejs from "@stripe/stripe-js";
|
||||||
import type { StripeElementLocale } from "@stripe/stripe-js";
|
import type { StripeElementLocale } from "@stripe/stripe-js";
|
||||||
@@ -12,8 +13,6 @@ import { CAL_URL } from "@calcom/lib/constants";
|
|||||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||||
import { Button, CheckboxField } from "@calcom/ui";
|
import { Button, CheckboxField } from "@calcom/ui";
|
||||||
|
|
||||||
import type { EventType } from ".prisma/client";
|
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
payment: Omit<Payment, "id" | "fee" | "success" | "refunded" | "externalId" | "data"> & {
|
payment: Omit<Payment, "id" | "fee" | "success" | "refunded" | "externalId" | "data"> & {
|
||||||
data: Record<string, unknown>;
|
data: Record<string, unknown>;
|
||||||
|
|||||||
@@ -111,8 +111,9 @@ export const CreateANewTeamForm = () => {
|
|||||||
placeholder="acme"
|
placeholder="acme"
|
||||||
label={t("team_url")}
|
label={t("team_url")}
|
||||||
addOnLeading={`${
|
addOnLeading={`${
|
||||||
orgBranding?.fullDomain.replace("https://", "").replace("http://", "") ??
|
orgBranding
|
||||||
`${extractDomainFromWebsiteUrl}/team/`
|
? orgBranding.fullDomain.replace("https://", "").replace("http://", "") + "/"
|
||||||
|
: `${extractDomainFromWebsiteUrl}/team/`
|
||||||
}`}
|
}`}
|
||||||
defaultValue={value}
|
defaultValue={value}
|
||||||
onChange={(e) => {
|
onChange={(e) => {
|
||||||
|
|||||||
@@ -103,7 +103,7 @@ export default function TeamListItem(props: Props) {
|
|||||||
<span className="text-muted block text-xs">
|
<span className="text-muted block text-xs">
|
||||||
{team.slug
|
{team.slug
|
||||||
? orgBranding
|
? orgBranding
|
||||||
? `${orgBranding.fullDomain}${team.slug}`
|
? `${orgBranding.fullDomain}/${team.slug}`
|
||||||
: `${process.env.NEXT_PUBLIC_WEBSITE_URL}/team/${team.slug}`
|
: `${process.env.NEXT_PUBLIC_WEBSITE_URL}/team/${team.slug}`
|
||||||
: "Unpublished team"}
|
: "Unpublished team"}
|
||||||
</span>
|
</span>
|
||||||
@@ -238,8 +238,8 @@ export default function TeamListItem(props: Props) {
|
|||||||
`${
|
`${
|
||||||
orgBranding
|
orgBranding
|
||||||
? `${orgBranding.fullDomain}`
|
? `${orgBranding.fullDomain}`
|
||||||
: process.env.NEXT_PUBLIC_WEBSITE_URL + "/team/"
|
: process.env.NEXT_PUBLIC_WEBSITE_URL + "/team"
|
||||||
}${team.slug}`
|
}/${team.slug}`
|
||||||
);
|
);
|
||||||
showToast(t("link_copied"), "success");
|
showToast(t("link_copied"), "success");
|
||||||
}}
|
}}
|
||||||
@@ -278,8 +278,8 @@ export default function TeamListItem(props: Props) {
|
|||||||
href={`${
|
href={`${
|
||||||
orgBranding
|
orgBranding
|
||||||
? `${orgBranding.fullDomain}`
|
? `${orgBranding.fullDomain}`
|
||||||
: `${process.env.NEXT_PUBLIC_WEBSITE_URL}/team/`
|
: `${process.env.NEXT_PUBLIC_WEBSITE_URL}/team`
|
||||||
}${team.slug}`}
|
}/${team.slug}`}
|
||||||
StartIcon={ExternalLink}>
|
StartIcon={ExternalLink}>
|
||||||
{t("preview_team") as string}
|
{t("preview_team") as string}
|
||||||
</DropdownItem>
|
</DropdownItem>
|
||||||
|
|||||||
@@ -227,7 +227,7 @@ const ProfileView = () => {
|
|||||||
value={value}
|
value={value}
|
||||||
addOnLeading={
|
addOnLeading={
|
||||||
team.parent && orgBranding
|
team.parent && orgBranding
|
||||||
? getOrgFullDomain(orgBranding?.slug, { protocol: false })
|
? getOrgFullDomain(orgBranding?.slug, { protocol: false }) + "/"
|
||||||
: `${WEBAPP_URL}/team/`
|
: `${WEBAPP_URL}/team/`
|
||||||
}
|
}
|
||||||
onChange={(e) => {
|
onChange={(e) => {
|
||||||
|
|||||||
@@ -46,7 +46,7 @@ export const EventTypeDescription = ({
|
|||||||
{eventType.description && (
|
{eventType.description && (
|
||||||
<div
|
<div
|
||||||
className={classNames(
|
className={classNames(
|
||||||
"text-subtle max-w-[280px] break-words py-1 text-sm sm:max-w-[650px] [&_a]:text-blue-500 [&_a]:underline [&_a]:hover:text-blue-600",
|
"text-subtle line-clamp-3 max-w-[280px] break-words py-1 text-sm sm:max-w-[650px] [&_a]:text-blue-500 [&_a]:underline [&_a]:hover:text-blue-600",
|
||||||
shortenDescription ? "line-clamp-4" : ""
|
shortenDescription ? "line-clamp-4" : ""
|
||||||
)}
|
)}
|
||||||
dangerouslySetInnerHTML={{
|
dangerouslySetInnerHTML={{
|
||||||
|
|||||||
@@ -10,38 +10,40 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.tremor-DateRangePicker-calendarButton,
|
||||||
.tremor-DateRangePicker-calendarButton, .tremor-DateRangePicker-dropdownButton {
|
.tremor-DateRangePicker-dropdownButton {
|
||||||
@apply border-subtle bg-default text-sm leading-4 placeholder:text-sm placeholder:font-normal focus-within:ring-0 focus-within:ring-emphasis hover:border-subtle dark:focus-within:ring-emphasis hover:bg-subtle
|
@apply border-subtle bg-default focus-within:ring-emphasis hover:border-subtle dark:focus-within:ring-emphasis hover:bg-subtle text-sm leading-4 placeholder:text-sm placeholder:font-normal focus-within:ring-0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.tremor-DateRangePicker-calendarButtonText,.tremor-DateRangePicker-dropdownButtonText {
|
.tremor-DateRangePicker-calendarButtonText,
|
||||||
@apply text-default
|
.tremor-DateRangePicker-dropdownButtonText {
|
||||||
|
@apply text-default;
|
||||||
}
|
}
|
||||||
|
|
||||||
.tremor-DateRangePicker-calendarModal,.tremor-DateRangePicker-dropdownModal{
|
.tremor-DateRangePicker-calendarModal,
|
||||||
@apply dark:invert
|
.tremor-DateRangePicker-dropdownModal {
|
||||||
|
@apply dark:invert;
|
||||||
}
|
}
|
||||||
|
|
||||||
.tremor-Card-root{
|
.tremor-Card-root {
|
||||||
@apply p-5
|
@apply p-5;
|
||||||
}
|
}
|
||||||
|
|
||||||
.tremor-TableCell-root{
|
.tremor-TableCell-root {
|
||||||
@apply pl-0
|
@apply pl-0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.recharts-responsive-container{
|
.recharts-responsive-container {
|
||||||
@apply -mx-4
|
@apply -mx-4;
|
||||||
}
|
}
|
||||||
.tremor-Card-root > p {
|
.tremor-Card-root > p {
|
||||||
@apply text-base font-semibold mb-2
|
@apply mb-2 text-base font-semibold;
|
||||||
}
|
}
|
||||||
|
|
||||||
.tremor-Legend-legendItem{
|
.tremor-Legend-legendItem {
|
||||||
@apply ml-2
|
@apply ml-2;
|
||||||
}
|
}
|
||||||
|
|
||||||
.tremor-TableBody-root{
|
.tremor-TableBody-root {
|
||||||
@apply divide-subtle
|
@apply divide-subtle;
|
||||||
}
|
}
|
||||||
@@ -243,7 +243,7 @@ const RemoveTimeButton = ({
|
|||||||
const TimeRangeField = ({ className, value, onChange }: { className?: string } & ControllerRenderProps) => {
|
const TimeRangeField = ({ className, value, onChange }: { className?: string } & ControllerRenderProps) => {
|
||||||
// this is a controlled component anyway given it uses LazySelect, so keep it RHF agnostic.
|
// this is a controlled component anyway given it uses LazySelect, so keep it RHF agnostic.
|
||||||
return (
|
return (
|
||||||
<div className={classNames("flex flex-row gap-1",className)}>
|
<div className={classNames("flex flex-row gap-1", className)}>
|
||||||
<LazySelect
|
<LazySelect
|
||||||
className="inline-block w-[100px]"
|
className="inline-block w-[100px]"
|
||||||
value={value.start}
|
value={value.start}
|
||||||
|
|||||||
@@ -984,7 +984,7 @@ export function ShellMain(props: LayoutProps) {
|
|||||||
: "pwa:bottom-24 fixed bottom-20 z-40 ltr:right-4 rtl:left-4 md:z-auto md:ltr:right-0 md:rtl:left-0",
|
: "pwa:bottom-24 fixed bottom-20 z-40 ltr:right-4 rtl:left-4 md:z-auto md:ltr:right-0 md:rtl:left-0",
|
||||||
"flex-shrink-0 md:relative md:bottom-auto md:right-auto"
|
"flex-shrink-0 md:relative md:bottom-auto md:right-auto"
|
||||||
)}>
|
)}>
|
||||||
{props.CTA}
|
{isLocaleReady && props.CTA}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{props.actions && props.actions}
|
{props.actions && props.actions}
|
||||||
|
|||||||
@@ -0,0 +1,49 @@
|
|||||||
|
import { BanIcon } from "lucide-react";
|
||||||
|
|
||||||
|
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||||
|
import { trpc } from "@calcom/trpc/react";
|
||||||
|
import { Dialog, DialogTrigger, ConfirmationDialogContent, Button, showToast } from "@calcom/ui";
|
||||||
|
|
||||||
|
import type { User } from "../UserListTable";
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
users: User[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export function DeleteBulkUsers({ users }: Props) {
|
||||||
|
const { t } = useLocale();
|
||||||
|
const selectedRows = users; // Get selected rows from table
|
||||||
|
const utils = trpc.useContext();
|
||||||
|
const deleteMutation = trpc.viewer.organizations.bulkDeleteUsers.useMutation({
|
||||||
|
onSuccess: () => {
|
||||||
|
utils.viewer.organizations.listMembers.invalidate();
|
||||||
|
showToast("Deleted Users", "success");
|
||||||
|
},
|
||||||
|
onError: (error) => {
|
||||||
|
showToast(error.message, "error");
|
||||||
|
},
|
||||||
|
});
|
||||||
|
return (
|
||||||
|
<Dialog>
|
||||||
|
<DialogTrigger asChild>
|
||||||
|
<Button StartIcon={BanIcon}>{t("Delete")}</Button>
|
||||||
|
</DialogTrigger>
|
||||||
|
<ConfirmationDialogContent
|
||||||
|
variety="danger"
|
||||||
|
title={t("remove_users_from_org")}
|
||||||
|
confirmBtnText={t("remove")}
|
||||||
|
isLoading={deleteMutation.isLoading}
|
||||||
|
onConfirm={() => {
|
||||||
|
deleteMutation.mutateAsync({
|
||||||
|
userIds: selectedRows.map((user) => user.id),
|
||||||
|
});
|
||||||
|
}}>
|
||||||
|
<p className="mt-5">
|
||||||
|
{t("remove_users_from_org_confirm", {
|
||||||
|
userCount: selectedRows.length,
|
||||||
|
})}
|
||||||
|
</p>
|
||||||
|
</ConfirmationDialogContent>
|
||||||
|
</Dialog>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,126 @@
|
|||||||
|
import type { Table } from "@tanstack/react-table";
|
||||||
|
import { Users, Check } from "lucide-react";
|
||||||
|
import { useState } from "react";
|
||||||
|
|
||||||
|
import classNames from "@calcom/lib/classNames";
|
||||||
|
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||||
|
import { trpc } from "@calcom/trpc";
|
||||||
|
import {
|
||||||
|
Command,
|
||||||
|
CommandInput,
|
||||||
|
CommandList,
|
||||||
|
CommandItem,
|
||||||
|
CommandEmpty,
|
||||||
|
CommandGroup,
|
||||||
|
Button,
|
||||||
|
Popover,
|
||||||
|
PopoverContent,
|
||||||
|
PopoverTrigger,
|
||||||
|
showToast,
|
||||||
|
} from "@calcom/ui";
|
||||||
|
|
||||||
|
import type { User } from "../UserListTable";
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
table: Table<User>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function TeamListBulkAction({ table }: Props) {
|
||||||
|
const { data: teams } = trpc.viewer.organizations.getTeams.useQuery();
|
||||||
|
const [selectedValues, setSelectedValues] = useState<Set<number>>(new Set());
|
||||||
|
const utils = trpc.useContext();
|
||||||
|
const mutation = trpc.viewer.organizations.bulkAddToTeams.useMutation({
|
||||||
|
onError: (error) => {
|
||||||
|
showToast(error.message, "error");
|
||||||
|
},
|
||||||
|
onSuccess: (res) => {
|
||||||
|
showToast(
|
||||||
|
`${res.invitedTotalUsers} Users invited to ${Array.from(selectedValues).length} teams`,
|
||||||
|
"success"
|
||||||
|
);
|
||||||
|
// Optimistically update the data from query trpc cache listMembers
|
||||||
|
// We may need to set this data instread of invalidating. Will see how performance handles it
|
||||||
|
utils.viewer.organizations.listMembers.invalidate();
|
||||||
|
|
||||||
|
// Clear the selected values
|
||||||
|
setSelectedValues(new Set());
|
||||||
|
table.toggleAllRowsSelected(false);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const { t } = useLocale();
|
||||||
|
|
||||||
|
// Add a value to the set
|
||||||
|
const addValue = (value: number) => {
|
||||||
|
const updatedSet = new Set(selectedValues);
|
||||||
|
updatedSet.add(value);
|
||||||
|
setSelectedValues(updatedSet);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Remove a value from the set
|
||||||
|
const removeValue = (value: number) => {
|
||||||
|
const updatedSet = new Set(selectedValues);
|
||||||
|
updatedSet.delete(value);
|
||||||
|
setSelectedValues(updatedSet);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Popover>
|
||||||
|
<PopoverTrigger asChild>
|
||||||
|
<Button StartIcon={Users}>{t("add_to_team")}</Button>
|
||||||
|
</PopoverTrigger>
|
||||||
|
{/* We dont really use shadows much - but its needed here */}
|
||||||
|
<PopoverContent className="w-[200px] p-0 shadow-md" align="start" sideOffset={12}>
|
||||||
|
<Command>
|
||||||
|
<CommandInput placeholder={t("search")} />
|
||||||
|
<CommandList>
|
||||||
|
<CommandEmpty>No results found.</CommandEmpty>
|
||||||
|
<CommandGroup>
|
||||||
|
{teams &&
|
||||||
|
teams.map((option) => {
|
||||||
|
const isSelected = selectedValues.has(option.id);
|
||||||
|
return (
|
||||||
|
<CommandItem
|
||||||
|
key={option.id}
|
||||||
|
onSelect={() => {
|
||||||
|
if (!isSelected) {
|
||||||
|
addValue(option.id);
|
||||||
|
} else {
|
||||||
|
removeValue(option.id);
|
||||||
|
}
|
||||||
|
}}>
|
||||||
|
<span>{option.name}</span>
|
||||||
|
<div
|
||||||
|
className={classNames(
|
||||||
|
"border-subtle ml-auto flex h-4 w-4 items-center justify-center rounded-sm border",
|
||||||
|
isSelected ? "text-emphasis" : "opacity-50 [&_svg]:invisible"
|
||||||
|
)}>
|
||||||
|
<Check className={classNames("h-4 w-4")} />
|
||||||
|
</div>
|
||||||
|
</CommandItem>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</CommandGroup>
|
||||||
|
</CommandList>
|
||||||
|
</Command>
|
||||||
|
<div className="my-1.5 flex w-full">
|
||||||
|
<Button
|
||||||
|
loading={mutation.isLoading}
|
||||||
|
className="ml-auto mr-1.5 rounded-md"
|
||||||
|
size="sm"
|
||||||
|
onClick={async () => {
|
||||||
|
const selectedRows = table.getSelectedRowModel().flatRows.map((row) => row.original);
|
||||||
|
mutation.mutateAsync({
|
||||||
|
userIds: selectedRows.map((row) => row.id),
|
||||||
|
teamIds: Array.from(selectedValues),
|
||||||
|
});
|
||||||
|
}}>
|
||||||
|
{t("apply")}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</PopoverContent>
|
||||||
|
</Popover>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,70 @@
|
|||||||
|
import { classNames } from "@calcom/lib";
|
||||||
|
import { useCopy } from "@calcom/lib/hooks/useCopy";
|
||||||
|
import type { BadgeProps } from "@calcom/ui";
|
||||||
|
import { Badge, Button, Label } from "@calcom/ui";
|
||||||
|
import { ClipboardCheck, Clipboard } from "@calcom/ui/components/icon";
|
||||||
|
|
||||||
|
type DisplayInfoType<T extends boolean> = {
|
||||||
|
label: string;
|
||||||
|
value: T extends true ? string[] : string;
|
||||||
|
asBadge?: boolean;
|
||||||
|
isArray?: T;
|
||||||
|
displayCopy?: boolean;
|
||||||
|
badgeColor?: BadgeProps["variant"];
|
||||||
|
} & (T extends false
|
||||||
|
? { displayCopy?: boolean; displayCount?: never }
|
||||||
|
: { displayCopy?: never; displayCount?: number }); // Only show displayCopy if its not an array is false
|
||||||
|
|
||||||
|
export function DisplayInfo<T extends boolean>({
|
||||||
|
label,
|
||||||
|
value,
|
||||||
|
asBadge,
|
||||||
|
isArray,
|
||||||
|
displayCopy,
|
||||||
|
displayCount,
|
||||||
|
badgeColor,
|
||||||
|
}: DisplayInfoType<T>) {
|
||||||
|
const { copyToClipboard, isCopied } = useCopy();
|
||||||
|
const values = (isArray ? value : [value]) as string[];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex flex-col">
|
||||||
|
<Label className="text-subtle mb-1 text-xs font-semibold uppercase leading-none">
|
||||||
|
{label} {displayCount && `(${displayCount})`}
|
||||||
|
</Label>
|
||||||
|
<div className={classNames(asBadge ? "mt-0.5 flex space-x-2" : "flex flex-col")}>
|
||||||
|
<>
|
||||||
|
{values.map((v) => {
|
||||||
|
const content = (
|
||||||
|
<span
|
||||||
|
className={classNames(
|
||||||
|
"text-emphasis inline-flex items-center gap-1 font-normal leading-5",
|
||||||
|
asBadge ? "text-xs" : "text-sm"
|
||||||
|
)}>
|
||||||
|
{v}
|
||||||
|
{displayCopy && (
|
||||||
|
<Button
|
||||||
|
size="sm"
|
||||||
|
variant="icon"
|
||||||
|
onClick={() => copyToClipboard(v)}
|
||||||
|
color="minimal"
|
||||||
|
className="text-subtle rounded-md"
|
||||||
|
StartIcon={isCopied ? ClipboardCheck : Clipboard}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
|
||||||
|
return asBadge ? (
|
||||||
|
<Badge variant={badgeColor} size="sm">
|
||||||
|
{content}
|
||||||
|
</Badge>
|
||||||
|
) : (
|
||||||
|
content
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,142 @@
|
|||||||
|
import { zodResolver } from "@hookform/resolvers/zod";
|
||||||
|
import type { Dispatch } from "react";
|
||||||
|
import { useForm } from "react-hook-form";
|
||||||
|
import { z } from "zod";
|
||||||
|
import shallow from "zustand/shallow";
|
||||||
|
|
||||||
|
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||||
|
import { trpc, type RouterOutputs } from "@calcom/trpc/react";
|
||||||
|
import {
|
||||||
|
Form,
|
||||||
|
TextField,
|
||||||
|
ToggleGroup,
|
||||||
|
TextAreaField,
|
||||||
|
TimezoneSelect,
|
||||||
|
Label,
|
||||||
|
showToast,
|
||||||
|
Avatar,
|
||||||
|
} from "@calcom/ui";
|
||||||
|
|
||||||
|
import type { Action } from "../UserListTable";
|
||||||
|
import { useEditMode } from "./store";
|
||||||
|
|
||||||
|
const editSchema = z.object({
|
||||||
|
name: z.string(),
|
||||||
|
email: z.string().email(),
|
||||||
|
bio: z.string(),
|
||||||
|
role: z.enum(["ADMIN", "MEMBER"]),
|
||||||
|
timeZone: z.string(),
|
||||||
|
// schedules: z.array(z.string()),
|
||||||
|
// teams: z.array(z.string()),
|
||||||
|
});
|
||||||
|
|
||||||
|
type EditSchema = z.infer<typeof editSchema>;
|
||||||
|
|
||||||
|
export function EditForm({
|
||||||
|
selectedUser,
|
||||||
|
avatarUrl,
|
||||||
|
domainUrl,
|
||||||
|
dispatch,
|
||||||
|
}: {
|
||||||
|
selectedUser: RouterOutputs["viewer"]["organizations"]["getUser"];
|
||||||
|
avatarUrl: string;
|
||||||
|
domainUrl: string;
|
||||||
|
dispatch: Dispatch<Action>;
|
||||||
|
}) {
|
||||||
|
const [setMutationLoading] = useEditMode((state) => [state.setMutationloading], shallow);
|
||||||
|
const { t } = useLocale();
|
||||||
|
const utils = trpc.useContext();
|
||||||
|
const form = useForm({
|
||||||
|
resolver: zodResolver(editSchema),
|
||||||
|
defaultValues: {
|
||||||
|
name: selectedUser?.name ?? "",
|
||||||
|
email: selectedUser?.email ?? "",
|
||||||
|
bio: selectedUser?.bio ?? "",
|
||||||
|
role: selectedUser?.role ?? "",
|
||||||
|
timeZone: selectedUser?.timeZone ?? "",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const mutation = trpc.viewer.organizations.updateUser.useMutation({
|
||||||
|
onSuccess: () => {
|
||||||
|
dispatch({ type: "CLOSE_MODAL" });
|
||||||
|
utils.viewer.organizations.listMembers.invalidate();
|
||||||
|
showToast(t("profile_updated_successfully"), "success");
|
||||||
|
},
|
||||||
|
onError: (error) => {
|
||||||
|
showToast(error.message, "error");
|
||||||
|
},
|
||||||
|
onSettled: () => {
|
||||||
|
/**
|
||||||
|
* /We need to do this as the submit button lives out side
|
||||||
|
* the form for some complicated reason so we can't relay on mutationState
|
||||||
|
*/
|
||||||
|
setMutationLoading(false);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const watchTimezone = form.watch("timeZone");
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Form
|
||||||
|
form={form}
|
||||||
|
id="edit-user-form"
|
||||||
|
handleSubmit={(values) => {
|
||||||
|
setMutationLoading(true);
|
||||||
|
mutation.mutate({
|
||||||
|
userId: selectedUser?.id ?? "",
|
||||||
|
role: values.role as "ADMIN" | "MEMBER", // Cast needed as we dont provide an option for owner
|
||||||
|
name: values.name,
|
||||||
|
email: values.email,
|
||||||
|
bio: values.bio,
|
||||||
|
timeZone: values.timeZone,
|
||||||
|
});
|
||||||
|
}}>
|
||||||
|
<div className="mt-4 flex items-center gap-2">
|
||||||
|
<Avatar
|
||||||
|
size="lg"
|
||||||
|
alt={`${selectedUser?.name} avatar`}
|
||||||
|
imageSrc={avatarUrl}
|
||||||
|
gravatarFallbackMd5="fallback"
|
||||||
|
/>
|
||||||
|
<div className="space-between flex flex-col leading-none">
|
||||||
|
<span className="text-emphasis text-lg font-semibold">{selectedUser?.name ?? "Nameless User"}</span>
|
||||||
|
<p className="subtle text-sm font-normal">
|
||||||
|
{domainUrl}/{selectedUser?.username}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="mt-6 flex flex-col space-y-3">
|
||||||
|
<TextField label={t("name")} {...form.register("name")} />
|
||||||
|
<TextField label={t("email")} {...form.register("email")} />
|
||||||
|
|
||||||
|
<TextAreaField label={t("bio")} {...form.register("bio")} className="min-h-52" />
|
||||||
|
<div>
|
||||||
|
<Label>{t("role")}</Label>
|
||||||
|
<ToggleGroup
|
||||||
|
isFullWidth
|
||||||
|
defaultValue={selectedUser?.role ?? "MEMBER"}
|
||||||
|
value={form.watch("role")}
|
||||||
|
options={[
|
||||||
|
{
|
||||||
|
value: "MEMBER",
|
||||||
|
label: t("member"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: "ADMIN",
|
||||||
|
label: t("admin"),
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
onValueChange={(value: EditSchema["role"]) => {
|
||||||
|
form.setValue("role", value);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<Label>{t("timezone")}</Label>
|
||||||
|
<TimezoneSelect value={watchTimezone ?? "America/Los_Angeles"} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Form>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,111 @@
|
|||||||
|
import type { Dispatch } from "react";
|
||||||
|
import { shallow } from "zustand/shallow";
|
||||||
|
|
||||||
|
import { useOrgBranding } from "@calcom/ee/organizations/context/provider";
|
||||||
|
import { WEBAPP_URL } from "@calcom/lib/constants";
|
||||||
|
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||||
|
import { trpc } from "@calcom/trpc/react";
|
||||||
|
import { Sheet, SheetContent, SheetFooter, Avatar, Skeleton, Loader } from "@calcom/ui";
|
||||||
|
|
||||||
|
import type { State, Action } from "../UserListTable";
|
||||||
|
import { DisplayInfo } from "./DisplayInfo";
|
||||||
|
import { EditForm } from "./EditUserForm";
|
||||||
|
import { SheetFooterControls } from "./SheetFooterControls";
|
||||||
|
import { useEditMode } from "./store";
|
||||||
|
|
||||||
|
export function EditUserSheet({ state, dispatch }: { state: State; dispatch: Dispatch<Action> }) {
|
||||||
|
const { t } = useLocale();
|
||||||
|
const { user: selectedUser } = state.editSheet;
|
||||||
|
const orgBranding = useOrgBranding();
|
||||||
|
const [editMode, setEditMode] = useEditMode((state) => [state.editMode, state.setEditMode], shallow);
|
||||||
|
const { data: loadedUser, isLoading } = trpc.viewer.organizations.getUser.useQuery({
|
||||||
|
userId: selectedUser?.id,
|
||||||
|
});
|
||||||
|
|
||||||
|
const avatarURL = `${orgBranding?.fullDomain ?? WEBAPP_URL}/${loadedUser?.username}/avatar.png`;
|
||||||
|
|
||||||
|
const schedulesNames = loadedUser?.schedules && loadedUser?.schedules.map((s) => s.name);
|
||||||
|
const teamNames =
|
||||||
|
loadedUser?.teams && loadedUser?.teams.map((t) => `${t.name} ${!t.accepted ? "(pending)" : ""}`);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Sheet
|
||||||
|
open={true}
|
||||||
|
onOpenChange={() => {
|
||||||
|
setEditMode(false);
|
||||||
|
dispatch({ type: "CLOSE_MODAL" });
|
||||||
|
}}>
|
||||||
|
<SheetContent position="right" size="default">
|
||||||
|
{!isLoading && loadedUser ? (
|
||||||
|
<div className="flex h-full flex-col">
|
||||||
|
{!editMode ? (
|
||||||
|
<div className="flex-grow">
|
||||||
|
<div className="mt-4 flex items-center gap-2">
|
||||||
|
<Avatar
|
||||||
|
asChild
|
||||||
|
className="h-[36px] w-[36px]"
|
||||||
|
alt={`${loadedUser?.name} avatar`}
|
||||||
|
imageSrc={avatarURL}
|
||||||
|
gravatarFallbackMd5="fallback"
|
||||||
|
/>
|
||||||
|
<div className="space-between flex flex-col leading-none">
|
||||||
|
<Skeleton loading={isLoading} as="p" waitForTranslation={false}>
|
||||||
|
<span className="text-emphasis text-lg font-semibold">
|
||||||
|
{loadedUser?.name ?? "Nameless User"}
|
||||||
|
</span>
|
||||||
|
</Skeleton>
|
||||||
|
<Skeleton loading={isLoading} as="p" waitForTranslation={false}>
|
||||||
|
<p className="subtle text-sm font-normal">
|
||||||
|
{orgBranding?.fullDomain ?? WEBAPP_URL}/{loadedUser?.username}
|
||||||
|
</p>
|
||||||
|
</Skeleton>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="mt-6 flex flex-col space-y-5">
|
||||||
|
<DisplayInfo label={t("email")} value={loadedUser?.email ?? ""} displayCopy />
|
||||||
|
<DisplayInfo
|
||||||
|
label={t("bio")}
|
||||||
|
badgeColor="gray"
|
||||||
|
value={loadedUser?.bio ? loadedUser?.bio : t("user_has_no_bio")}
|
||||||
|
/>
|
||||||
|
<DisplayInfo label={t("role")} value={loadedUser?.role ?? ""} asBadge badgeColor="blue" />
|
||||||
|
<DisplayInfo label={t("timezone")} value={loadedUser?.timeZone ?? ""} />
|
||||||
|
<DisplayInfo
|
||||||
|
label={t("availability_schedules")}
|
||||||
|
value={
|
||||||
|
schedulesNames && schedulesNames?.length === 0
|
||||||
|
? [t("user_has_no_schedules")]
|
||||||
|
: schedulesNames ?? "" // TS wtf
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<DisplayInfo
|
||||||
|
label={t("teams")}
|
||||||
|
displayCount={teamNames?.length ?? 0}
|
||||||
|
value={
|
||||||
|
teamNames && teamNames?.length === 0 ? [t("user_isnt_in_any_teams")] : teamNames ?? "" // TS wtf
|
||||||
|
}
|
||||||
|
asBadge={teamNames && teamNames?.length > 0}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="flex-grow">
|
||||||
|
<EditForm
|
||||||
|
selectedUser={loadedUser}
|
||||||
|
avatarUrl={avatarURL}
|
||||||
|
domainUrl={orgBranding?.fullDomain ?? WEBAPP_URL}
|
||||||
|
dispatch={dispatch}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<SheetFooter className="mt-auto">
|
||||||
|
<SheetFooterControls />
|
||||||
|
</SheetFooter>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<Loader />
|
||||||
|
)}
|
||||||
|
</SheetContent>
|
||||||
|
</Sheet>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,60 @@
|
|||||||
|
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||||
|
import { SheetClose, Button } from "@calcom/ui";
|
||||||
|
import { Pencil } from "@calcom/ui/components/icon";
|
||||||
|
|
||||||
|
import { useEditMode } from "./store";
|
||||||
|
|
||||||
|
function EditModeFooter() {
|
||||||
|
const { t } = useLocale();
|
||||||
|
const setEditMode = useEditMode((state) => state.setEditMode);
|
||||||
|
const isLoading = useEditMode((state) => state.mutationLoading);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Button
|
||||||
|
color="secondary"
|
||||||
|
type="button"
|
||||||
|
className="justify-center md:w-1/5"
|
||||||
|
onClick={() => {
|
||||||
|
setEditMode(false);
|
||||||
|
}}>
|
||||||
|
{t("cancel")}
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
<Button type="submit" className="w-full justify-center" form="edit-user-form" loading={isLoading}>
|
||||||
|
{t("update")}
|
||||||
|
</Button>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function MoreInfoFooter() {
|
||||||
|
const { t } = useLocale();
|
||||||
|
const setEditMode = useEditMode((state) => state.setEditMode);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<SheetClose asChild>
|
||||||
|
<Button color="secondary" type="button" className="justify-center md:w-1/5">
|
||||||
|
{t("close")}
|
||||||
|
</Button>
|
||||||
|
</SheetClose>
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
onClick={() => {
|
||||||
|
setEditMode(true);
|
||||||
|
}}
|
||||||
|
className="w-full justify-center gap-2"
|
||||||
|
variant="icon"
|
||||||
|
key="EDIT_BUTTON"
|
||||||
|
StartIcon={Pencil}>
|
||||||
|
{t("edit")}
|
||||||
|
</Button>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function SheetFooterControls() {
|
||||||
|
const editMode = useEditMode((state) => state.editMode);
|
||||||
|
return <>{editMode ? <EditModeFooter /> : <MoreInfoFooter />}</>;
|
||||||
|
}
|
||||||
Loaded 100 of 193 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user