Compare commits

...
61 Commits
Author SHA1 Message Date
Peer Richelsen 791927e45e Update package.json
release.yml / release (push) Canceled after 0s
2023-01-20 23:06:43 +01:00
Alex van Andel fa984e1568 Changed a neutral- classes to gray (#6603)
* Changed a neutral- classes to gray

* Changed all border-1 to border
2023-01-20 23:04:58 +01:00
Peer Richelsen 9b197da5be Update README.md
release.yml / release (push) Canceled after 0s
2023-01-20 21:58:11 +01:00
Peer Richelsen b7c316b01b Update README.md 2023-01-20 21:57:52 +01:00
Udit Takkar 5ab56310bf fix: add delete in dropdown (#6599)
Signed-off-by: Udit Takkar <udit.07814802719@cse.mait.ac.in>

Signed-off-by: Udit Takkar <udit.07814802719@cse.mait.ac.in>
2023-01-20 14:57:14 -05:00
Udit Takkar 52e318ca18 fix: show button on empty string (#6601)
Signed-off-by: Udit Takkar <udit.07814802719@cse.mait.ac.in>

Signed-off-by: Udit Takkar <udit.07814802719@cse.mait.ac.in>
2023-01-20 12:27:58 -07:00
Omar López 8d078564dd Bye bye submodules (#6585)
* WIP

* Uses ssh instead

* Update .gitignore

* Update .gitignore

* Update Makefile

* Update git-setup.sh

* Update git-setup.sh

* Replaced Makefile with bash script

* Update package.json
2023-01-20 12:17:51 -07:00
7b5482e83d [CAL-808] /availability/single - UI issue on buttons beside time inputs (#6561)
* [CAL-808] /availability/single - UI issue on buttons beside time inputs

* Update apps/web/public/static/locales/en/common.json

* Update packages/features/schedules/components/Schedule.tsx

* create new translation for tooltip

Co-authored-by: gitstart-calcom <gitstart@users.noreply.github.com>
Co-authored-by: Peer Richelsen <peeroke@gmail.com>
Co-authored-by: CarinaWolli <wollencarina@gmail.com>
2023-01-20 17:56:28 +00:00
sean-brydon 23086476b3 Tailwind Merge (#6596)
* Tailwind Merge

* Fix merge classNames
2023-01-20 15:19:12 +01:00
Nafees Nazik 00cab5c41f fix: react-select overflow issue in some modals. (#6587)
* feat: add a disable overflow prop

* feat: use the disable overflow prop
2023-01-20 13:29:37 +00:00
sean-brydon fbc27e1d52 Fix booking filters not working for admin (#6576) 2023-01-19 19:23:58 +01:00
Nafees Nazik e1c9a8cbfc fix: i18n placeholder in awaiting approval email (#6573) 2023-01-19 16:56:44 +01:00
Nafees Nazik 8ee109e975 fix: app list item ui issues in /settings/my-account/conferencing (#6565)
* fix: border color

* fix: padding and title line-height

* feat: add app-list card

* feat: use the AppListCard component

* nit: remove unused imports
2023-01-19 15:02:01 +00:00
sean-brydonandAlex van Andel cfaecc3e02 Icon small button variant (#6525)
* Icon small button variant

* Rename

* Update CVA + Update button WIP

* add ticket ref and fix padding

* Update button and all uses of Icon button

* SB deployment test

* Fixing type error

* Fix lint

* Add icon button to canvas

Co-authored-by: Alex van Andel <me@alexvanandel.com>
2023-01-19 14:55:32 +00:00
Syed Ali Shahbaz 18246e0dc1 path.join causing issues on windows so we keep path like the other exports (#6568) 2023-01-19 13:21:51 +00:00
sean-brydon 6dcb16724c Fixing tabs again (#6566) 2023-01-19 12:05:00 +00:00
d4c5a906b5 App Store Templates (#5289)
* Start by moving what we have to _templates

* WIP

* WIP

* Add create/edit/delete template commands

* Type fixes cli

Co-authored-by: Peer Richelsen <peeroke@gmail.com>
Co-authored-by: zomars <zomars@me.com>
2023-01-18 15:30:25 -07:00
Alex van Andel 27e8d73b82 Revert "experiment: disco border (#6531)"
This reverts commit b5f274d663.
2023-01-18 19:29:36 +00:00
Alex van Andel 25eae4a546 find indexes instead of just one (#6547) 2023-01-18 12:26:07 -07:00
Udit Takkar f663774fee feat: add most popular section in apps (#6539)
* feat: add most popular section in apps

Signed-off-by: Udit Takkar <udit.07814802719@cse.mait.ac.in>

* chore: remove margin

Signed-off-by: Udit Takkar <udit.07814802719@cse.mait.ac.in>

* fix: merge conflict

Signed-off-by: Udit Takkar <udit.07814802719@cse.mait.ac.in>

Signed-off-by: Udit Takkar <udit.07814802719@cse.mait.ac.in>
2023-01-18 11:48:10 -07:00
Nafees NazikandLeo Giovanetti e2258fba27 Feat: webhook form UX improvement (#6448)
* feat: add type and required attribute

* feat: disable ping button when subscriberUrl is
not present

Co-authored-by: Leo Giovanetti <hello@leog.me>
2023-01-18 16:02:20 +00:00
Carina WollendorferandCarinaWolli d0d8878f34 Adds ability to add custom sender names for workflow emails (#6463)
Co-authored-by: CarinaWolli <wollencarina@gmail.com>
2023-01-18 15:32:39 +01:00
Udit Takkar 59ff6c35ee fix: limits tab in event types (#6545)
Signed-off-by: Udit Takkar <udit.07814802719@cse.mait.ac.in>

Signed-off-by: Udit Takkar <udit.07814802719@cse.mait.ac.in>
2023-01-18 12:58:13 +01:00
Alex van Andel 3c07e305ce Create local next.config.js, add storybook-static to .gitignore (#6544) 2023-01-18 11:54:03 +00:00
Alex van Andel b5f274d663 experiment: disco border (#6531)
* Had a play, looking good (now needs isLoading state or however we want to do that)

* Also add tailwind config..

* Don't show conic bg on lightmode

* Slow down animation, show on light mode

* Apply disco border both on light/dark mode & only on success page

* 5s->8s animation speed, remove border crisping up the corners
2023-01-18 10:40:06 +00:00
Peer Richelsen 6c3ae984fc add tracking URL to LicenseRequired (#6526) 2023-01-17 21:49:40 +00:00
Mohammed Cherfaoui a2e9bf0504 fix toast dimensions issues (#6522) 2023-01-18 02:45:04 +05:30
GitStart-Cal.comandgitstart-calcom 8aa089efe6 [CAL-760] Emails: Design Tokens are not update to 2.0 (#6530)
Co-authored-by: gitstart-calcom <gitstart@users.noreply.github.com>
2023-01-17 22:14:16 +01:00
sean-brydonandzomars de64d3d9d4 Move exports up a folder (#6519)
* Move exports up a folder

* Fix imports

* Fix imports again

Co-authored-by: zomars <zomars@me.com>
2023-01-17 12:10:47 -07:00
Udit Takkar 71833c0737 fix: type error and border (#6520)
* fix: type error and border

Signed-off-by: Udit Takkar <udit.07814802719@cse.mait.ac.in>

* fix: sepeator

Signed-off-by: Udit Takkar <udit.07814802719@cse.mait.ac.in>

Signed-off-by: Udit Takkar <udit.07814802719@cse.mait.ac.in>
2023-01-17 17:12:22 +00:00
Udit Takkar c78f4deaf7 fix: use suffix in text field (#6532)
Signed-off-by: Udit Takkar <udit.07814802719@cse.mait.ac.in>

Signed-off-by: Udit Takkar <udit.07814802719@cse.mait.ac.in>
2023-01-17 18:08:00 +01:00
Hariom Balhara 24fc7d611f Add routing-forms README (#6506) 2023-01-17 17:58:10 +01:00
sean-brydonandHariom Balhara 8afbc3b457 Fixing horizontal tabs (#6454)
* Fix spacing and height

* Testing if its timing out or something else

* So, timep=out is the problem. Increase the timeout to 1s for now

Co-authored-by: Hariom Balhara <hariombalhara@gmail.com>
2023-01-17 16:36:06 +00:00
sean-brydon da18aae21c Adding rtl support for SB (#6523) 2023-01-17 13:38:14 +01:00
Nafees Nazik 87a0d533ef fix: german translation (#6512) 2023-01-17 13:35:35 +01:00
Nafees Nazik 7cdd453953 fix: button shifting while focusing (#6521)
* fix: remove border on focus

* refactor: remove dark mode styles
2023-01-17 12:21:05 +00:00
Omar López 8143816c18 v2.5.0
release.yml / release (push) Canceled after 0s
2023-01-16 15:34:47 -07:00
Nafees Nazik 8243ac3c00 Fix: modal content hidden (#6508) 2023-01-16 18:08:34 +01:00
Alex van Andel 207e120d66 Bring back PublicShell (#6505) 2023-01-16 14:59:54 +01:00
Ronit PandaandPeer Richelsen b25d04da1c fix: tablet sidebar profile being too wide and not centered (#6490)
* feat: updated app list items styling issues inside settings/conferencing

* feat: fixed tablet sidebar width and not centered issue

* fix: removed un-related changes from this pr

* deleted .env

* fix: added the accidentaly deleted .env file

* fix: removed unwanted .env file from the pr

Co-authored-by: Peer Richelsen <peeroke@gmail.com>
2023-01-16 12:57:38 +00:00
Alex van Andel 7064acf756 Don't update the field when changing the select (#6486)
* Don't update the field when changing the select

* Fixing missing label

* Applied the correct y-8 vertical margin

* Changed the description vertical offset from title a bit more

* Add Trash icon on Availability list dropdown
2023-01-16 12:18:18 +00:00
Alex van Andel 716407ad90 Added fallback to hostsFixed too which is used in COLLECTIVE (#6500) 2023-01-16 10:46:06 +00:00
GitStart-Cal.comandgitstart-calcom af0b08bb2b [CAL-677] /event-type : Event Setup sidebar item doesn't reflect multiple durations (#6390)
Co-authored-by: gitstart-calcom <gitstart@users.noreply.github.com>
2023-01-16 11:45:54 +01:00
Hariom Balhara 3e27735ccb Disable redirect for cancellation page (#6498) 2023-01-16 09:20:50 +00:00
Nafees Nazik 76c087f29c fix: wrong i18n placeholder in many locales (#6496)
* fix: czech translation

* fix: spanish translation

* fix: italian translation

* fix: japanese translation

* fix: korean translation

* fix: dutch translation

* fix: norwegian translation

* fix: polish translation

* fix: portuguese translation

* fix: portuguese translation

* fix: romanian translation

* fix: russian translation

* fix: serbian translation

* fix: swedish translation

* fix: turkish translation

* fix: ukrainian translation

* fix: vietnamese translation

* fix: chinese simplified translation

* fix: chinese traditional translation
2023-01-16 08:48:07 +00:00
Peer RichelsenandAlex van Andel e05bcf98c2 readding sidebar tips for everyone, added 3 more features to teams empty screen (#6475)
* readding sidebar tips for everyone

* added 3 more features to teams plan

* responsive fixes

* responsive fixes

* small responsive fix

* CTA alignment fixed

* Minor alignment fixes to Shell for event-types/[type]

Co-authored-by: Alex van Andel <me@alexvanandel.com>
2023-01-15 18:54:52 +01:00
Udit TakkarandPeer Richelsen 636e88d467 fix: styling of dropdown component (#6440)
* fix: styling of dropdown component

Signed-off-by: Udit Takkar <udit.07814802719@cse.mait.ac.in>

* Apply suggestions from code review

* fix: use dropdown item in all pages

Signed-off-by: Udit Takkar <udit.07814802719@cse.mait.ac.in>

* chore: undo

Signed-off-by: Udit Takkar <udit.07814802719@cse.mait.ac.in>

Signed-off-by: Udit Takkar <udit.07814802719@cse.mait.ac.in>
Co-authored-by: Peer Richelsen <peeroke@gmail.com>
2023-01-15 11:40:40 +00:00
Nafees Nazik 34a61214d3 fix: app store spacing issues (#6481)
* feat: add equal spacing with gap

* fix: title style

* fix: title style

* fix: remove margin

* fix: remove margin

* fix: sub header spacing and slider button

* fix: category card text

* fix: app store spacing issues

* fix: tab in tablet

* fix: shell
2023-01-14 20:11:28 +01:00
Nafees Nazik b1af30e026 fix: german translation for reschedule booking email subject (#6483)
* fix: german translation for reschedule subject

* fix: add a full stop
2023-01-14 19:10:30 +01:00
Nafees Nazik 870b59d617 fix: darkmode in booking/[id] page (#6458)
* fix: text-area style

* fix: border styles
2023-01-14 16:49:15 +01:00
Peer Richelsen e25094ccab i18n udpate 2023-01-14 02:12:07 +01:00
Carina WollendorferandCarinaWolli 1376f0991f Reusable Upgrade component for teams only features (#6473)
Co-authored-by: CarinaWolli <wollencarina@gmail.com>
2023-01-14 01:48:19 +01:00
443329b99f disables signups via env variable (#6212)
* disables signups via env variable

* Apply suggestions from code review

Co-authored-by: Peer Richelsen <peeroke@gmail.com>
Co-authored-by: Omar López <zomars@me.com>
2023-01-13 17:42:13 -07:00
Joe Au-Yeung a1fc4ec26d Disable email input if new user invited to team (#6471) 2023-01-13 17:21:57 -07:00
Udit TakkarandPeer Richelsen d462298e79 fix: remove website url in download recording (#6466)
Co-authored-by: Peer Richelsen <peeroke@gmail.com>
2023-01-14 00:03:13 +00:00
Nafees NazikandPeer Richelsen 965cff38c2 fix: HOSTED_CAL_FEATURES env doesn't get passed to the frontend (#6392)
* fix: rename HOSTED_CAL_FEATURES into NEXT_PUBLIC_HOSTED_CAL_FEATURES

* chore: add the new env to example

* fix: docs link

Co-authored-by: Peer Richelsen <peeroke@gmail.com>
2023-01-13 23:51:37 +00:00
Joe Au-Yeungandkodiakhq[bot] f96e909dce Team invites - transform email or username to lower case (#6470)
* Fix string

* Use current hashPassword

* Transform email to lowercase

Co-authored-by: kodiakhq[bot] <49736102+kodiakhq[bot]@users.noreply.github.com>
2023-01-13 18:58:41 +00:00
Leo Giovanetti a4d6f7da62 Fixing GCal emailing notif and sequential deleting (#6460) 2023-01-13 11:29:42 -07:00
Nafees Nazikandkodiakhq[bot] ce9413e0dd fix: missing empty state in /apps/installed/web3 (#6377)
* refactor: turn enum into const assertion

* feat: add web3 to icon list

* fix: type

* fix: type cast query params and add category list

* fix: conditional rendering

* feat: add i18n keys

* fix: type errors

Co-authored-by: kodiakhq[bot] <49736102+kodiakhq[bot]@users.noreply.github.com>
2023-01-13 14:12:43 +00:00
Peer Richelsen 8fc0dfb763 booking page: only scroll to bottom on mobile (#6453)
* move useMediaQuery into packages

* revert logo.tsx
2023-01-13 17:40:02 +05:30
690edcf0a7 New Crowdin translations by Github Action (#6444)
Co-authored-by: Crowdin Bot <support+bot@crowdin.com>
Co-authored-by: kodiakhq[bot] <49736102+kodiakhq[bot]@users.noreply.github.com>
2023-01-12 23:21:06 +00:00
370 changed files with 4434 additions and 3721 deletions
+9 -1
View File
@@ -31,11 +31,14 @@ NEXT_PUBLIC_CONSOLE_URL='http://localhost:3004'
NEXT_PUBLIC_EMBED_LIB_URL='http://localhost:3000/embed/embed.js'
# To enable SAML login, set both these variables
# @see https://github.com/calcom/cal.com/tree/main/packages/ee#setting-up-saml-login
# @see https://github.com/calcom/cal.com/tree/main/packages/features/ee#setting-up-saml-login
# SAML_DATABASE_URL="postgresql://postgres:@localhost:5450/cal-saml"
SAML_DATABASE_URL=
# SAML_ADMINS='pro@example.com'
SAML_ADMINS=
# NEXT_PUBLIC_HOSTED_CAL_FEATURES=1
NEXT_PUBLIC_HOSTED_CAL_FEATURES=
# If you use Heroku to deploy Postgres (or use self-signed certs for Postgres) then uncomment the follow line.
# @see https://devcenter.heroku.com/articles/connecting-heroku-postgres#connecting-in-node-js
# PGSSLMODE='no-verify'
@@ -79,6 +82,7 @@ SEND_FEEDBACK_EMAIL=
# Used for email reminders in workflows and internal sync services
SENDGRID_API_KEY=
SENDGRID_EMAIL=
NEXT_PUBLIC_SENDGRID_SENDER_NAME=
# Twilio
# Used to send SMS reminders in workflows
@@ -86,6 +90,7 @@ TWILIO_SID=
TWILIO_TOKEN=
TWILIO_MESSAGING_SID=
TWILIO_PHONE_NUMBER=
# For NEXT_PUBLIC_SENDER_ID only letters, numbers and spaces are allowed (max. 11 characters)
NEXT_PUBLIC_SENDER_ID=
TWILIO_VERIFY_SID=
@@ -149,3 +154,6 @@ SENTRY_IGNORE_API_RESOLUTION_ERROR=
NEXT_PUBLIC_APP_NAME="Cal.com"
NEXT_PUBLIC_SUPPORT_MAIL_ADDRESS="help@cal.com"
NEXT_PUBLIC_COMPANY_NAME="Cal.com, Inc."
# Set this to true in to disable new signups
# NEXT_PUBLIC_DISABLE_SIGNUP=true
NEXT_PUBLIC_DISABLE_SIGNUP=
+6
View File
@@ -80,3 +80,9 @@ apps/storybook/build-storybook.log
# Snaplet
.snaplet/snapshots
.snaplet/structure.d.ts
# Submodules
.gitmodules
apps/api
apps/website
apps/console
-12
View File
@@ -1,12 +0,0 @@
[submodule "apps/console"]
path = apps/console
url = https://github.com/calcom/console.git
branch = main
[submodule "apps/api"]
path = apps/api
url = https://github.com/calcom/api.git
branch = main
[submodule "apps/website"]
path = apps/website
url = https://github.com/calcom/website.git
branch = main
+7 -11
View File
@@ -305,6 +305,12 @@ Currently Vercel Pro Plan is required to be able to Deploy this application with
See the [roadmap project](https://cal.com/roadmap) for a list of proposed features (and known issues). You can change the view to see planned tagged releases.
<!-- RORADMAP -->
## Repo Activity
<img width="100%" src="https://repobeats.axiom.co/api/embed/6bfca2f20f39738048b6e70ca205efde46352c3d.svg" />
<!-- CONTRIBUTING -->
## Contributing
@@ -405,17 +411,6 @@ following
9. Click the "Save" button at the bottom footer.
10. You're good to go. Now you can see any booking in Cal.com created as a meeting in HubSpot for your contacts.
### Obtaining Vital API Keys
1. Open [Vital](https://tryvital.io/) and click Get API Keys.
1. Create a team with the team name you desire
1. Head to the configuration section on the sidebar of the dashboard
1. Click on API keys and you'll find your sandbox `api_key`.
1. Copy your `api_key` to `VITAL_API_KEY` in the .env.appStore file.
1. Open [Vital Webhooks](https://app.tryvital.io/team/{team_id}/webhooks) and add `<CALCOM BASE URL>/api/integrations/vital/webhook` as webhook for connected applications.
1. Select all events for the webhook you interested, e.g. `sleep_created`
1. Copy the webhook secret (`sec...`) to `VITAL_WEBHOOK_SECRET` in the .env.appStore file.
## Workflows
### Setting up SendGrid for Email reminders
@@ -425,6 +420,7 @@ following
3. Copy API key to your .env file into the SENDGRID_API_KEY field
4. Go to Settings -> Sender Authentication and verify a single sender
5. Copy the verified E-Mail to your .env file into the SENDGRID_EMAIL field
6. Add your custom sender name to the .env file into the NEXT_PUBLIC_SENDGRID_SENDER_NAME field (fallback is Cal.com)
### Setting up Twilio for SMS reminders
Submodule apps/api deleted from 7aebdb8c96
Submodule apps/console deleted from 9aac72159e
+2 -1
View File
@@ -8,6 +8,7 @@ pnpm-debug.log*
lerna-debug.log*
node_modules
storybook-static
dist
dist-ssr
*.local
@@ -21,4 +22,4 @@ dist-ssr
*.ntvs*
*.njsproj
*.sln
*.sw?
*.sw?
+4 -2
View File
@@ -11,13 +11,15 @@ module.exports = {
"@storybook/addon-links",
"@storybook/addon-essentials",
"@storybook/addon-interactions",
"storybook-addon-rtl-direction",
"storybook-react-i18next",
{
"storybook-addon-next",
/*{
name: "storybook-addon-next",
options: {
nextConfigPath: path.resolve(__dirname, "../../web/next.config.js"),
},
},
},*/
],
framework: "@storybook/react",
core: {
+45
View File
@@ -0,0 +1,45 @@
const withBundleAnalyzer = require("@next/bundle-analyzer");
const withTM = require("next-transpile-modules")([
"@calcom/app-store",
"@calcom/dayjs",
"@calcom/emails",
"@calcom/trpc",
"@calcom/embed-core",
"@calcom/embed-react",
"@calcom/features",
"@calcom/lib",
"@calcom/prisma",
"@calcom/ui",
]);
const glob = require("glob");
const plugins = [];
plugins.push(withTM, withBundleAnalyzer({ enabled: process.env.ANALYZE === "true" }));
/** @type {import("next").NextConfig} */
const nextConfig = {
reactStrictMode: true,
images: {
domains: ["www.datocms-assets.com"],
formats: ["image/avif", "image/webp"],
},
typescript: {
ignoreBuildErrors: true,
},
experimental: { images: { allowFutureImage: true } },
eslint: {
ignoreDuringBuilds: true,
},
webpack: (config, { isServer }) => {
if (!isServer) {
// don't resolve 'fs' module on the client to prevent this error on build --> Error: Can't resolve 'fs'
config.resolve.fallback = {
fs: false,
};
}
return config;
},
};
module.exports = () => plugins.reduce((acc, next) => next(acc), nextConfig);
+2 -1
View File
@@ -22,7 +22,8 @@
"@radix-ui/react-tooltip": "^1.0.0",
"next": "^13.1.1",
"react": "^18.2.0",
"react-dom": "^18.2.0"
"react-dom": "^18.2.0",
"storybook-addon-rtl-direction": "^0.0.19"
},
"devDependencies": {
"@babel/core": "^7.19.6",
+35
View File
@@ -0,0 +1,35 @@
import { ReactNode } from "react";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { Badge, ListItemText } from "@calcom/ui";
interface AppListCardProps {
logo?: string;
title: string;
description: string;
actions?: ReactNode;
isDefault?: boolean;
}
export default function AppListCard(props: AppListCardProps) {
const { t } = useLocale();
const { logo, title, description, actions, isDefault } = props;
return (
<div className="p-4">
<div className="flex items-center gap-x-3">
{logo ? <img className="h-10 w-10" src={logo} alt={`${title} logo`} /> : null}
<div className="flex grow flex-col gap-y-1 truncate">
<div className="flex items-center gap-x-2">
<h3 className="truncate text-sm font-semibold text-gray-900">{title}</h3>
{isDefault ? <Badge variant="green">{t("default")}</Badge> : null}
</div>
<ListItemText component="p">{description}</ListItemText>
</div>
{actions}
</div>
</div>
);
}
+3 -3
View File
@@ -597,7 +597,7 @@ ${getEmbedTypeSpecificString({ embedFramework: "react", embedType, calLink, prev
<iframe
ref={ref as typeof ref & MutableRefObject<HTMLIFrameElement>}
data-testid="embed-preview"
className="border-1 h-[100vh] border"
className="h-[100vh] border"
width="100%"
height="100%"
src={`${WEBAPP_URL}/embed/preview.html?embedType=${embedType}&calLink=${calLink}`}
@@ -639,7 +639,7 @@ const ChooseEmbedTypesDialogContent = () => {
<div className="flex items-start">
{embeds.map((embed, index) => (
<button
className="w-1/3 border border-transparent p-3 text-left hover:rounded-md hover:border-gray-200 hover:bg-neutral-100 ltr:mr-2 rtl:ml-2"
className="w-1/3 border border-transparent p-3 text-left hover:rounded-md hover:border-gray-200 hover:bg-gray-100 ltr:mr-2 rtl:ml-2"
key={index}
data-testid={embed.type}
onClick={() => {
@@ -1163,7 +1163,7 @@ export const EmbedButton = <T extends React.ElementType>({
...props
}: EmbedButtonProps<T> & React.ComponentPropsWithoutRef<T>) => {
const router = useRouter();
className = classNames(className, "hidden lg:inline-flex");
className = classNames("hidden lg:inline-flex", className);
const openEmbedModal = () => {
goto(router, {
dialog: "embed",
+1 -1
View File
@@ -142,7 +142,7 @@ export default function ImageUploader({
</div>
)}
{result && <CropContainer imageSrc={result as string} onCropComplete={setCroppedAreaPixels} />}
<label className="mt-8 rounded-sm border border-gray-300 bg-white px-3 py-1 text-xs font-medium leading-4 text-gray-700 hover:bg-gray-50 hover:text-gray-900 focus:outline-none focus:ring-2 focus:ring-neutral-900 focus:ring-offset-1 dark:border-gray-800 dark:bg-transparent dark:text-white dark:hover:bg-gray-900">
<label className="mt-8 rounded-sm border border-gray-300 bg-white px-3 py-1 text-xs font-medium leading-4 text-gray-700 hover:bg-gray-50 hover:text-gray-900 focus:outline-none focus:ring-2 focus:ring-gray-900 focus:ring-offset-1 dark:border-gray-800 dark:bg-transparent dark:text-white dark:hover:bg-gray-900">
<input
onInput={onInputFile}
type="file"
+1 -1
View File
@@ -67,7 +67,7 @@ const NavTabs: FC<NavTabProps> = ({ tabs, linkProps, ...props }) => {
onClick={onClick}
className={classNames(
isCurrent
? "border-neutral-900 text-gray-900"
? "border-gray-900 text-gray-900"
: "border-transparent text-gray-500 hover:border-gray-300 hover:text-gray-700",
"group inline-flex items-center border-b-2 py-4 px-1 text-sm font-medium",
className
+8 -1
View File
@@ -12,7 +12,7 @@ import { APP_NAME, COMPANY_NAME, SUPPORT_MAIL_ADDRESS } from "@calcom/lib/consta
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { trpc } from "@calcom/trpc/react";
import { App as AppType } from "@calcom/types/App";
import { Button, Icon, showToast, SkeletonButton, SkeletonText, HeadSeo } from "@calcom/ui";
import { Button, Icon, showToast, SkeletonButton, SkeletonText, HeadSeo, Badge } from "@calcom/ui";
const Component = ({
name,
@@ -34,6 +34,7 @@ const Component = ({
privacy,
isProOnly,
images,
isTemplate,
}: Parameters<typeof App>[0]) => {
const { t } = useLocale();
const hasImages = images && images.length > 0;
@@ -106,6 +107,11 @@ const Component = ({
</Link>{" "}
• {t("published_by", { author })}
</h2>
{isTemplate && (
<Badge variant="red" className="mt-4">
Template - Available in Dev Environment only for testing
</Badge>
)}
</header>
</div>
{!appCredentials.isLoading ? (
@@ -310,6 +316,7 @@ export default function App(props: {
licenseRequired: AppType["licenseRequired"];
isProOnly: AppType["isProOnly"];
images?: string[];
isTemplate?: boolean;
}) {
const { t } = useLocale();
@@ -3,7 +3,7 @@ import { useRouter } from "next/router";
import { ReactNode, useEffect, useState } from "react";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { Icon, ListItem, ListItemText, ListItemTitle, showToast } from "@calcom/ui";
import { Badge, Icon, ListItem, ListItemText, ListItemTitle, showToast } from "@calcom/ui";
import classNames from "@lib/classNames";
@@ -19,6 +19,7 @@ function IntegrationListItem(props: {
destination?: boolean;
separate?: boolean;
invalidCredential?: boolean;
isTemplate?: boolean;
}): JSX.Element {
const { t } = useLocale();
const router = useRouter();
@@ -50,8 +51,13 @@ function IntegrationListItem(props: {
<div className={classNames("flex w-full flex-1 items-center space-x-2 p-4 rtl:space-x-reverse")}>
{props.logo && <img className="h-11 w-11" src={props.logo} alt={title} />}
<div className="flex-grow truncate pl-2">
<ListItemTitle component="h3">
<ListItemTitle component="h3" className="flex ">
<Link href={"/apps/" + props.slug}>{props.name || title}</Link>
{props.isTemplate && (
<Badge variant="red" className="ml-4">
Template
</Badge>
)}
</ListItemTitle>
<ListItemText component="p">{props.description}</ListItemText>
{/* Alert error that key stopped working. */}
@@ -4,18 +4,7 @@ import React, { ComponentProps } from "react";
import Shell from "@calcom/features/shell/Shell";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { EmptyScreen, Icon, HorizontalTabs, HorizontalTabItemProps } from "@calcom/ui";
const tabs: HorizontalTabItemProps[] = [
{
name: "app_store",
href: "/apps",
},
{
name: "installed_apps",
href: "/apps/installed",
},
];
import { EmptyScreen, Icon } from "@calcom/ui";
type AppsLayoutProps = {
children: React.ReactNode;
@@ -31,11 +20,8 @@ export default function AppsLayout({ children, actions, emptyStore, ...rest }: A
if (session.status === "loading") return <></>;
return (
<Shell {...rest} actions={actions?.("hidden sm:block")}>
<Shell {...rest} actions={actions?.("block")}>
<div className="flex flex-col xl:flex-row">
<div className="block lg:hidden">
<HorizontalTabs tabs={tabs} actions={actions?.("ml-6 mr-4 mt-3 block sm:hidden")} />
</div>
<main className="w-full">
{emptyStore ? (
<EmptyScreen
@@ -44,13 +44,15 @@ export default function InstalledAppsLayout({
children,
...rest
}: { children: React.ReactNode } & ComponentProps<typeof Shell>) {
const variant: typeof InstalledAppVariants[number] = "payment";
const query = trpc.viewer.integrations.useQuery({
variant: InstalledAppVariants.payment,
variant,
onlyInstalled: true,
});
let actualTabs = tabs;
if (query.data?.items.length === 0) {
actualTabs = tabs.filter((tab) => tab.name !== InstalledAppVariants.payment);
actualTabs = tabs.filter((tab) => tab.name !== variant);
}
return (
@@ -6,7 +6,7 @@ import classNames from "@lib/classNames";
function SkeletonLoader() {
return (
<ul className="animate-pulse divide-y divide-neutral-200 rounded-md border border-gray-200 bg-white sm:mx-0 sm:overflow-hidden">
<ul className="animate-pulse divide-y divide-gray-200 rounded-md border border-gray-200 bg-white sm:mx-0 sm:overflow-hidden">
<SkeletonItem />
<SkeletonItem />
<SkeletonItem />
@@ -220,7 +220,8 @@ function BookingListItem(booking: BookingItemProps) {
},
});
};
const showRecordingsButtons = booking.location === "integrations:daily" && isPast && isConfirmed;
const showRecordingsButtons =
(booking.location === "integrations:daily" || booking?.location?.trim() === "") && isPast && isConfirmed;
return (
<>
<RescheduleDialog
@@ -273,7 +274,7 @@ function BookingListItem(booking: BookingItemProps) {
</DialogContent>
</Dialog>
<tr className="group flex flex-col hover:bg-neutral-50 sm:flex-row">
<tr className="group flex flex-col hover:bg-gray-50 sm:flex-row">
<td
className="hidden align-top ltr:pl-6 rtl:pr-6 sm:table-cell sm:min-w-[12rem]"
onClick={onClickTableData}>
@@ -55,7 +55,7 @@ export default function CancelBooking(props: Props) {
placeholder={t("cancellation_reason_placeholder")}
value={cancellationReason}
onChange={(e) => setCancellationReason(e.target.value)}
className="mt-2 mb-4 w-full dark:border-gray-900 dark:bg-gray-700 dark:text-white "
className="dark:bg-darkgray-100 dark:border-darkgray-400 mt-2 mb-4 w-full dark:text-white "
rows={3}
/>
<div className="flex flex-col-reverse rtl:space-x-reverse ">
@@ -4,7 +4,7 @@ import { SkeletonText } from "@calcom/ui";
function SkeletonLoader() {
return (
<ul className="animate-pulse divide-y divide-neutral-200 rounded-md border border-gray-200 bg-white sm:overflow-hidden">
<ul className="animate-pulse divide-y divide-gray-200 rounded-md border border-gray-200 bg-white sm:overflow-hidden">
<SkeletonItem />
<SkeletonItem />
<SkeletonItem />
@@ -492,7 +492,7 @@ const BookingPage = ({
<div
className={classNames(
"main overflow-hidden",
isBackgroundTransparent ? "" : "dark:border-1 dark:bg-darkgray-100 bg-white",
isBackgroundTransparent ? "" : "dark:bg-darkgray-100 bg-white dark:border",
"dark:border-darkgray-300 rounded-md sm:border"
)}>
<div className="sm:flex">
@@ -954,19 +954,14 @@ const BookingPage = ({
<Button
type="button"
color="minimal"
size="icon"
variant="icon"
tooltip={t("additional_guests")}
StartIcon={Icon.FiUserPlus}
onClick={() => setGuestToggle(!guestToggle)}
className="mr-auto"
/>
)}
<Button
color="minimal"
type="button"
onClick={() => router.back()}
// We override this for this component only for now - as we don't support darkmode everywhere in the app
className="dark:hover:bg-darkgray-200 dark:border-none dark:text-white">
<Button color="minimal" type="button" onClick={() => router.back()}>
{t("cancel")}
</Button>
<Button
@@ -212,7 +212,7 @@ export const EditLocationDialog = (props: ISetLocationDialog) => {
return (
<Dialog open={isOpenDialog}>
<DialogContent>
<DialogContent disableOverflow>
<div className="flex flex-row space-x-3">
<div className="bg-secondary-100 mx-auto flex h-12 w-12 flex-shrink-0 items-center justify-center rounded-full sm:mx-0 sm:h-10 sm:w-10">
<Icon.FiMapPin className="text-primary-600 h-6 w-6" />
@@ -183,7 +183,7 @@ function RadioInputHandler({
{...register(`options.${index}.label` as const, { required: true })}
addOnSuffix={
<Button
size="icon"
variant="icon"
color="minimal"
StartIcon={Icon.FiX}
onClick={() => {
@@ -129,7 +129,7 @@ export const EventAdvancedTab = ({ eventType, team }: Pick<EventTypeSetupProps,
<Button
type="button"
StartIcon={Icon.FiEdit}
size="icon"
variant="icon"
color="minimal"
className="hover:stroke-3 min-w-fit px-0 hover:bg-transparent hover:text-black"
onClick={() => setShowEventNameTip((old) => !old)}
+7 -33
View File
@@ -1,43 +1,17 @@
import { EventTypeSetupProps, FormValues } from "pages/event-types/[type]";
import { useFormContext } from "react-hook-form";
import EventTypeAppContext, { GetAppData, SetAppData } from "@calcom/app-store/EventTypeAppContext";
import { EventTypeAddonMap } from "@calcom/app-store/apps.browser.generated";
import { GetAppData, SetAppData } from "@calcom/app-store/EventTypeAppContext";
import { EventTypeAppCard } from "@calcom/app-store/_components/EventTypeAppCardInterface";
import { EventTypeAppCardComponentProps } from "@calcom/app-store/types";
import { EventTypeAppsList } from "@calcom/app-store/utils";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { RouterOutputs, trpc } from "@calcom/trpc/react";
import { Button, EmptyScreen, ErrorBoundary, Icon } from "@calcom/ui";
import { trpc } from "@calcom/trpc/react";
import { Button, EmptyScreen, Icon } from "@calcom/ui";
type EventType = Pick<EventTypeSetupProps, "eventType">["eventType"] &
export type EventType = Pick<EventTypeSetupProps, "eventType">["eventType"] &
EventTypeAppCardComponentProps["eventType"];
function AppCardWrapper({
app,
eventType,
getAppData,
setAppData,
}: {
app: RouterOutputs["viewer"]["apps"][number];
eventType: EventType;
getAppData: GetAppData;
setAppData: SetAppData;
}) {
const dirName = app.slug === "stripe" ? "stripepayment" : app.slug;
const Component = EventTypeAddonMap[dirName as keyof typeof EventTypeAddonMap];
if (!Component) {
throw new Error('No component found for "' + dirName + '"');
}
return (
<ErrorBoundary message={`There is some problem with ${app.name} App`}>
<EventTypeAppContext.Provider value={[getAppData, setAppData]}>
<Component key={app.slug} app={app} eventType={eventType} />
</EventTypeAppContext.Provider>
</ErrorBoundary>
);
}
export const EventAppsTab = ({ eventType }: { eventType: EventType }) => {
const { t } = useLocale();
const { data: eventTypeApps, isLoading } = trpc.viewer.apps.useQuery({
@@ -97,7 +71,7 @@ export const EventAppsTab = ({ eventType }: { eventType: EventType }) => {
/>
) : null}
{installedApps?.map((app) => (
<AppCardWrapper
<EventTypeAppCard
getAppData={getAppDataGetter(app.slug as EventTypeAppsList)}
setAppData={getAppDataSetter(app.slug as EventTypeAppsList)}
key={app.slug}
@@ -113,7 +87,7 @@ export const EventAppsTab = ({ eventType }: { eventType: EventType }) => {
) : null}
<div className="before:border-0">
{notInstalledApps?.map((app) => (
<AppCardWrapper
<EventTypeAppCard
getAppData={getAppDataGetter(app.slug as EventTypeAppsList)}
setAppData={getAppDataSetter(app.slug as EventTypeAppsList)}
key={app.slug}
+192 -208
View File
@@ -1,8 +1,8 @@
import { useAutoAnimate } from "@formkit/auto-animate/react";
import * as RadioGroup from "@radix-ui/react-radio-group";
import { EventTypeSetupProps, FormValues } from "pages/event-types/[type]";
import { useMemo, useRef, useState } from "react";
import { Controller, useFormContext, useWatch } from "react-hook-form";
import React, { useEffect, useState } from "react";
import { Controller, useFormContext, UseFormRegisterReturn, useWatch } from "react-hook-form";
import { classNames } from "@calcom/lib";
import convertToNewDurationType, { DurationType } from "@calcom/lib/convertToNewDurationType";
@@ -12,13 +12,95 @@ import { PeriodType } from "@calcom/prisma/client";
import type { BookingLimit } from "@calcom/types/Calendar";
import { Button, DateRangePicker, Icon, Input, InputField, Label, Select, SettingsToggle } from "@calcom/ui";
const MinimumBookingNoticeInput = React.forwardRef<
HTMLInputElement,
Omit<UseFormRegisterReturn<"minimumBookingNotice">, "ref">
>(function MinimumBookingNoticeInput({ ...passThroughProps }, ref) {
const { t } = useLocale();
const { setValue, getValues } = useFormContext<FormValues>();
const durationTypeOptions: {
value: DurationType;
label: string;
}[] = [
{
label: t("minutes"),
value: "minutes",
},
{
label: t("hours"),
value: "hours",
},
{
label: t("days"),
value: "days",
},
];
const [minimumBookingNoticeDisplayValues, setMinimumBookingNoticeDisplayValues] = useState<{
type: DurationType;
value: number;
}>({
type: findDurationType(getValues(passThroughProps.name)),
value: convertToNewDurationType(
"minutes",
findDurationType(getValues(passThroughProps.name)),
getValues(passThroughProps.name)
),
});
// keep hidden field in sync with minimumBookingNoticeDisplayValues
useEffect(() => {
setValue(
passThroughProps.name,
convertToNewDurationType(
minimumBookingNoticeDisplayValues.type,
"minutes",
minimumBookingNoticeDisplayValues.value
)
);
}, [minimumBookingNoticeDisplayValues, setValue, passThroughProps.name]);
return (
<div className="flex items-end justify-end">
<div className="w-1/2 md:w-full">
<InputField
required
defaultValue={minimumBookingNoticeDisplayValues.value}
onChange={(e) =>
setMinimumBookingNoticeDisplayValues({
...minimumBookingNoticeDisplayValues,
value: parseInt(e.target.value || "0", 10),
})
}
label={t("minimum_booking_notice")}
type="number"
placeholder="0"
className="mb-0 h-[38px] rounded-[4px] ltr:mr-2 rtl:ml-2"
/>
<input type="hidden" ref={ref} {...passThroughProps} />
</div>
<Select
isSearchable={false}
className="mb-0 ml-2 h-[38px] w-full capitalize md:min-w-[150px] md:max-w-[200px]"
defaultValue={durationTypeOptions.find(
(option) => option.value === minimumBookingNoticeDisplayValues.type
)}
onChange={(input) => {
if (input) {
setMinimumBookingNoticeDisplayValues({
...minimumBookingNoticeDisplayValues,
type: input.value,
});
}
}}
options={durationTypeOptions}
/>
</div>
);
});
export const EventLimitsTab = ({ eventType }: Pick<EventTypeSetupProps, "eventType">) => {
const { t } = useLocale();
const formMethods = useFormContext<FormValues>();
const minimumBookingNoticeType = useRef<DurationType>(findDurationType(eventType.minimumBookingNotice));
const prevBookingNoticeType = useRef<DurationType>(minimumBookingNoticeType.current);
const minimumBookingNoticeInDurationTypeFormValue = formMethods.watch("minimumBookingNoticeInDurationType");
const PERIOD_TYPES = [
{
@@ -49,212 +131,115 @@ export const EventLimitsTab = ({ eventType }: Pick<EventTypeSetupProps, "eventTy
defaultValue: periodType?.type,
});
const onMinimumNoticeDurationTypeChange = useMemo(
() => (durationType?: DurationType) => {
if (typeof durationType === "undefined") return;
// Store current selected type in ref to use in previous run for comparrison.
minimumBookingNoticeType.current = durationType;
// Uses the previous selected type and converts the value to the new type.
// Eg day was selected before, user selects hours now, so we multiple * 24 and round up.
const minimumBookingNoticeInDurationType = convertToNewDurationType(
prevBookingNoticeType.current,
durationType,
Number(minimumBookingNoticeInDurationTypeFormValue)
);
// Uses the round up value in the new type to calculate the value in minutes.
// We should NOT use minimumBookingNoticeInDurationTypeFormValue here, since that
// would break in the case that the user enters 90 minutes, changes the type to
// hours (that will show 2 hours), but the value in minutes would then remain 90,
// which is something else than the user things they save.
const minimumBookingNoticeInMinutes = convertToNewDurationType(
durationType,
"minutes",
Number(minimumBookingNoticeInDurationType)
);
// Updates both form values as well as stores the current type as previous type in ref.
formMethods.setValue("minimumBookingNotice", minimumBookingNoticeInMinutes);
formMethods.setValue("minimumBookingNoticeInDurationType", minimumBookingNoticeInDurationType);
prevBookingNoticeType.current = durationType;
},
[formMethods, minimumBookingNoticeInDurationTypeFormValue]
);
/**
* When the user inputs a new value for minimumBookingNoticeInDurationType,
* we calculate the value in minutes, and update this hidden field as well.
*/
const onMinimumNoticeChange = useMemo(
() => (duration: string) => {
const minimumBookingNoticeInMinutes = convertToNewDurationType(
minimumBookingNoticeType.current,
"minutes",
Number(duration)
);
formMethods.setValue("minimumBookingNotice", minimumBookingNoticeInMinutes);
},
[formMethods]
);
return (
<div>
<div className="flex flex-col space-y-4 lg:flex-row lg:space-y-0 lg:space-x-4">
<div className="w-full">
<Label htmlFor="beforeBufferTime">{t("before_event")} </Label>
<Controller
name="beforeBufferTime"
control={formMethods.control}
defaultValue={eventType.beforeEventBuffer || 0}
render={({ field: { onChange, value } }) => {
const beforeBufferOptions = [
{
label: t("event_buffer_default"),
value: 0,
},
...[5, 10, 15, 20, 30, 45, 60, 90, 120].map((minutes) => ({
label: minutes + " " + t("minutes"),
value: minutes,
})),
];
return (
<Select
isSearchable={false}
onChange={(val) => {
if (val) onChange(val.value);
}}
defaultValue={
beforeBufferOptions.find((option) => option.value === value) || beforeBufferOptions[0]
}
options={beforeBufferOptions}
/>
);
}}
/>
</div>
<div className="w-full">
<Label htmlFor="afterBufferTime">{t("after_event")} </Label>
<Controller
name="afterBufferTime"
control={formMethods.control}
defaultValue={eventType.afterEventBuffer || 0}
render={({ field: { onChange, value } }) => {
const afterBufferOptions = [
{
label: t("event_buffer_default"),
value: 0,
},
...[5, 10, 15, 20, 30, 45, 60, 90, 120].map((minutes) => ({
label: minutes + " " + t("minutes"),
value: minutes,
})),
];
return (
<Select
isSearchable={false}
onChange={(val) => {
if (val) onChange(val.value);
}}
defaultValue={
afterBufferOptions.find((option) => option.value === value) || afterBufferOptions[0]
}
options={afterBufferOptions}
/>
);
}}
/>
</div>
</div>
<div className="flex flex-col space-y-4 pt-4 lg:flex-row lg:space-y-0 lg:space-x-4">
<div className="flex w-full items-end justify-end">
<Controller
name="minimumBookingNotice"
control={formMethods.control}
render={() => {
const durationTypeOptions = [
{
label: t("minutes"),
value: "minutes",
},
{
label: t("hours"),
value: "hours",
},
{
label: t("days"),
value: "days",
},
];
return (
<>
<div className="w-1/2 md:w-full">
<InputField
required
label={t("minimum_booking_notice")}
type="number"
placeholder="120"
className="mb-0 h-[38px] rounded-[4px] ltr:mr-2 rtl:ml-2"
{...formMethods.register("minimumBookingNoticeInDurationType", {
onChange: (event: React.ChangeEvent<HTMLInputElement>) =>
onMinimumNoticeChange(event.target.value),
})}
/>
<input type="hidden" {...formMethods.register("minimumBookingNotice")} />
</div>
<div className="space-y-8">
<div className="space-y-4 lg:space-y-8">
<div className="flex flex-col space-y-4 lg:flex-row lg:space-y-0 lg:space-x-4">
<div className="w-full">
<Label htmlFor="beforeBufferTime">{t("before_event")} </Label>
<Controller
name="beforeBufferTime"
control={formMethods.control}
defaultValue={eventType.beforeEventBuffer || 0}
render={({ field: { onChange, value } }) => {
const beforeBufferOptions = [
{
label: t("event_buffer_default"),
value: 0,
},
...[5, 10, 15, 20, 30, 45, 60, 90, 120].map((minutes) => ({
label: minutes + " " + t("minutes"),
value: minutes,
})),
];
return (
<Select
isSearchable={false}
className="mb-0 ml-2 h-[38px] w-full capitalize md:min-w-[150px] md:max-w-[200px]"
defaultValue={durationTypeOptions.find(
(option) => option.value === minimumBookingNoticeType.current
)}
onChange={(input) => onMinimumNoticeDurationTypeChange(input?.value as DurationType)}
options={durationTypeOptions}
onChange={(val) => {
if (val) onChange(val.value);
}}
defaultValue={
beforeBufferOptions.find((option) => option.value === value) || beforeBufferOptions[0]
}
options={beforeBufferOptions}
/>
</>
);
}}
/>
);
}}
/>
</div>
<div className="w-full">
<Label htmlFor="afterBufferTime">{t("after_event")} </Label>
<Controller
name="afterBufferTime"
control={formMethods.control}
defaultValue={eventType.afterEventBuffer || 0}
render={({ field: { onChange, value } }) => {
const afterBufferOptions = [
{
label: t("event_buffer_default"),
value: 0,
},
...[5, 10, 15, 20, 30, 45, 60, 90, 120].map((minutes) => ({
label: minutes + " " + t("minutes"),
value: minutes,
})),
];
return (
<Select
isSearchable={false}
onChange={(val) => {
if (val) onChange(val.value);
}}
defaultValue={
afterBufferOptions.find((option) => option.value === value) || afterBufferOptions[0]
}
options={afterBufferOptions}
/>
);
}}
/>
</div>
</div>
<div className="w-full">
<Label htmlFor="slotInterval">{t("slot_interval")} </Label>
<Controller
name="slotInterval"
control={formMethods.control}
render={() => {
const slotIntervalOptions = [
{
label: t("slot_interval_default"),
value: -1,
},
...[5, 10, 15, 20, 30, 45, 60, 75, 90, 105, 120].map((minutes) => ({
label: minutes + " " + t("minutes"),
value: minutes,
})),
];
return (
<Select
isSearchable={false}
onChange={(val) => {
formMethods.setValue("slotInterval", val && (val.value || 0) > 0 ? val.value : null);
}}
defaultValue={
slotIntervalOptions.find((option) => option.value === eventType.slotInterval) ||
slotIntervalOptions[0]
}
options={slotIntervalOptions}
/>
);
}}
/>
<div className="flex flex-col space-y-4 lg:flex-row lg:space-y-0 lg:space-x-4">
<div className="w-full">
<Label htmlFor="minimumBookingNotice">{t("minimum_booking_notice")} </Label>
<MinimumBookingNoticeInput {...formMethods.register("minimumBookingNotice")} />
</div>
<div className="w-full">
<Label htmlFor="slotInterval">{t("slot_interval")} </Label>
<Controller
name="slotInterval"
control={formMethods.control}
render={() => {
const slotIntervalOptions = [
{
label: t("slot_interval_default"),
value: -1,
},
...[5, 10, 15, 20, 30, 45, 60, 75, 90, 105, 120].map((minutes) => ({
label: minutes + " " + t("minutes"),
value: minutes,
})),
];
return (
<Select
isSearchable={false}
onChange={(val) => {
formMethods.setValue("slotInterval", val && (val.value || 0) > 0 ? val.value : null);
}}
defaultValue={
slotIntervalOptions.find((option) => option.value === eventType.slotInterval) ||
slotIntervalOptions[0]
}
options={slotIntervalOptions}
/>
);
}}
/>
</div>
</div>
</div>
<hr className="my-8" />
<hr />
<Controller
name="bookingLimits"
control={formMethods.control}
@@ -276,8 +261,7 @@ export const EventLimitsTab = ({ eventType }: Pick<EventTypeSetupProps, "eventTy
</SettingsToggle>
)}
/>
<hr className="my-8" />
<hr />
<Controller
name="periodType"
control={formMethods.control}
@@ -446,7 +430,7 @@ const BookingLimits = () => {
}}
/>
<Button
size="icon"
variant="icon"
StartIcon={Icon.FiTrash}
color="destructive"
onClick={() => {
@@ -162,7 +162,7 @@ export const EventSetupTab = (
return null;
}
return (
<li key={location.type} className="mb-2 rounded-md border border-neutral-300 py-1.5 px-2">
<li key={location.type} className="mb-2 rounded-md border border-gray-300 py-1.5 px-2">
<div className="flex max-w-full justify-between">
<div key={index} className="flex flex-grow items-center">
<img
@@ -16,10 +16,11 @@ import {
ButtonGroup,
ConfirmationDialogContent,
Dialog,
Divider,
DropdownMenuSeparator,
Dropdown,
DropdownMenuContent,
DropdownMenuItem,
DropdownItem,
DropdownMenuTrigger,
HorizontalTabs,
Icon,
@@ -55,12 +56,15 @@ function getNavigation(props: {
installedAppsNumber: number;
}) {
const { eventType, t, enabledAppsNumber, installedAppsNumber, enabledWorkflowsNumber } = props;
const duration =
eventType.metadata?.multipleDuration?.map((duration) => ` ${duration}`) || eventType.length;
return [
{
name: "event_setup_tab_title",
href: `/event-types/${eventType.id}?tabName=setup`,
icon: Icon.FiLink,
info: `${eventType.length} Mins`, // TODO: Get this from props
info: `${duration} ${t("minute_timeUnit")}`, // TODO: Get this from props
},
{
name: "availability",
@@ -204,7 +208,7 @@ function EventTypeSingleLayout({
<Button
color="secondary"
target="_blank"
size="icon"
variant="icon"
href={permalink}
rel="noreferrer"
StartIcon={Icon.FiExternalLink}
@@ -213,7 +217,7 @@ function EventTypeSingleLayout({
<Button
color="secondary"
size="icon"
variant="icon"
StartIcon={Icon.FiLink}
tooltip={t("copy_link")}
onClick={() => {
@@ -225,12 +229,12 @@ function EventTypeSingleLayout({
embedUrl={encodeURIComponent(embedLink)}
StartIcon={Icon.FiCode}
color="secondary"
size="icon"
variant="icon"
tooltip={t("embed")}
/>
<Button
color="secondary"
size="icon"
variant="icon"
StartIcon={Icon.FiTrash}
tooltip={t("delete")}
disabled={!hasPermsToDelete}
@@ -246,32 +250,36 @@ function EventTypeSingleLayout({
</DropdownMenuTrigger>
<DropdownMenuContent>
<DropdownMenuItem className="focus:ring-gray-100">
<Button
color="minimal"
StartIcon={Icon.FiExternalLink}
<DropdownItem
target="_blank"
type="button"
StartIcon={Icon.FiExternalLink}
href={permalink}
rel="noreferrer"
className="min-w-full">
rel="noreferrer">
{t("preview")}
</Button>
</DropdownItem>
</DropdownMenuItem>
<DropdownMenuItem className="focus:ring-gray-100">
<Button color="minimal" StartIcon={Icon.FiLink}>
<DropdownItem
type="button"
StartIcon={Icon.FiLink}
onClick={() => {
navigator.clipboard.writeText(permalink);
showToast("Link copied!", "success");
}}>
{t("copy_link")}
</Button>
</DropdownItem>
</DropdownMenuItem>
<DropdownMenuItem className="focus:ring-gray-100">
<Button
color="minimal"
<DropdownItem
type="button"
StartIcon={Icon.FiTrash}
disabled={!hasPermsToDelete}
onClick={() => setDeleteDialogOpen(true)}
className="min-w-full">
onClick={() => setDeleteDialogOpen(true)}>
{t("delete")}
</Button>
</DropdownItem>
</DropdownMenuItem>
<Divider />
<DropdownMenuSeparator className="block sm:hidden" />
<div className="flex items-center rounded-md py-1.5 px-4 sm:hidden sm:hover:bg-gray-100">
<Skeleton
as={Label}
@@ -316,7 +324,7 @@ function EventTypeSingleLayout({
<div className="w-full ltr:mr-2 rtl:ml-2">
<div
className={classNames(
"mt-4 rounded-md border-neutral-200 bg-white sm:mx-0 xl:mt-0",
"mt-4 rounded-md border-gray-200 bg-white sm:mx-0 xl:mt-0",
disableBorder ? "border-0 xl:-mt-4 " : "p-2 md:border md:p-6"
)}>
{children}
@@ -10,7 +10,7 @@ function SkeletonLoader() {
<SkeletonText className="h-4 w-24" />
</div>
</div>
<ul className="divide-y divide-neutral-200 rounded-md border border-gray-200 bg-white sm:mx-0 sm:overflow-hidden">
<ul className="divide-y divide-gray-200 rounded-md border border-gray-200 bg-white sm:mx-0 sm:overflow-hidden">
<SkeletonItem />
<SkeletonItem />
<SkeletonItem />
@@ -113,7 +113,7 @@ const UserProfile = (props: IUserProfileProps) => {
name="avatar"
id="avatar"
placeholder="URL"
className="mt-1 block w-full rounded-sm border border-gray-300 px-3 py-2 text-sm focus:border-neutral-800 focus:outline-none focus:ring-neutral-800"
className="mt-1 block w-full rounded-sm border border-gray-300 px-3 py-2 text-sm focus:border-gray-800 focus:outline-none focus:ring-gray-800"
defaultValue={imageSrc}
/>
<div className="flex items-center px-4">
@@ -146,7 +146,7 @@ const UserProfile = (props: IUserProfileProps) => {
ref={bioRef}
name="bio"
id="bio"
className="mt-1 block h-[60px] w-full rounded-sm border border-gray-300 px-3 py-2 focus:border-neutral-500 focus:outline-none focus:ring-neutral-500 sm:text-sm"
className="mt-1 block h-[60px] w-full rounded-sm border border-gray-300 px-3 py-2 focus:border-gray-500 focus:outline-none focus:ring-gray-500 sm:text-sm"
defaultValue={user?.bio || undefined}
onChange={(event) => {
setValue("bio", event.target.value);
+1 -1
View File
@@ -32,7 +32,7 @@ export default function AuthContainer(props: React.PropsWithChildren<Props>) {
</div>
)}
<div className="mb-auto mt-8 sm:mx-auto sm:w-full sm:max-w-md">
<div className="border-1 mx-2 rounded-md border-gray-200 bg-white px-4 py-10 sm:px-10">
<div className="mx-2 rounded-md border border-gray-200 bg-white px-4 py-10 sm:px-10">
{props.children}
</div>
<div className="mt-8 text-center text-sm text-gray-600">
@@ -190,7 +190,7 @@ const PremiumTextfield = (props: ICustomUsernameProps) => {
<div className="flex rounded-md">
<span
className={classNames(
isInputUsernamePremium ? "border-1 border-orange-400 " : "",
isInputUsernamePremium ? "border border-orange-400 " : "",
"hidden h-9 items-center rounded-l-md border border-r-0 border-gray-300 border-r-gray-300 bg-gray-50 px-3 text-sm text-gray-500 md:inline-flex"
)}>
{process.env.NEXT_PUBLIC_WEBSITE_URL.replace("https://", "").replace("http://", "")}/
@@ -207,8 +207,8 @@ const PremiumTextfield = (props: ICustomUsernameProps) => {
className={classNames(
"border-l-1 mb-0 mt-0 rounded-md rounded-l-none font-sans text-sm leading-4 focus:!ring-0",
isInputUsernamePremium
? "border-1 focus:border-1 border-orange-400 focus:border-orange-400"
: "border-1 focus:border-2",
? "border border-orange-400 focus:border focus:border-orange-400"
: "border focus:border",
markAsError
? "focus:shadow-0 focus:ring-shadow-0 border-red-500 focus:border-red-500 focus:outline-none"
: "border-l-gray-300",
@@ -41,7 +41,7 @@ export const CheckedSelect = ({
{...props}
/>
{value.map((option) => (
<div key={option.value} className="border-1 border p-2 font-medium">
<div key={option.value} className="border p-2 font-medium">
<Avatar
className="inline h-6 w-6 rounded-full ltr:mr-2 rtl:ml-2"
imageSrc={option.avatar}
+1
View File
@@ -1,3 +1,4 @@
// lets refactor and move this into packages/lib/hooks/
import { useState, useEffect } from "react";
const useMediaQuery = (query: string) => {
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@calcom/web",
"version": "2.4.7",
"version": "2.5.2",
"private": true,
"scripts": {
"analyze": "ANALYZE=true next build",
+3 -4
View File
@@ -56,7 +56,7 @@ export default function User(props: inferSSRProps<typeof getServerSideProps> & E
{eventTypes.map((type, index) => (
<li
key={index}
className="dark:bg-darkgray-100 group relative border-b border-neutral-200 bg-white first:rounded-t-md last:rounded-b-md last:border-b-0 hover:bg-gray-50 dark:border-neutral-700 dark:hover:border-neutral-600">
className="dark:bg-darkgray-100 group relative border-b border-gray-200 bg-white first:rounded-t-md last:rounded-b-md last:border-b-0 hover:bg-gray-50 dark:border-gray-700 dark:hover:border-gray-600">
<Icon.FiArrowRight className="absolute right-3 top-3 h-4 w-4 text-black opacity-0 transition-opacity group-hover:opacity-100 dark:text-white" />
<Link
href={getUsernameSlugLink({ users: props.users, slug: type.slug })}
@@ -145,8 +145,7 @@ export default function User(props: inferSSRProps<typeof getServerSideProps> & E
<div
className={classNames(
"rounded-md ",
!isEventListEmpty &&
"border border-neutral-200 dark:border-neutral-700 dark:hover:border-neutral-600"
!isEventListEmpty && "border border-gray-200 dark:border-gray-700 dark:hover:border-gray-600"
)}
data-testid="event-types">
{user.away ? (
@@ -165,7 +164,7 @@ export default function User(props: inferSSRProps<typeof getServerSideProps> & E
<div
key={type.id}
style={{ display: "flex", ...eventTypeListItemEmbedStyles }}
className="dark:bg-darkgray-100 group relative border-b border-neutral-200 bg-white first:rounded-t-md last:rounded-b-md last:border-b-0 hover:bg-gray-50 dark:border-neutral-700 dark:hover:border-neutral-600">
className="dark:bg-darkgray-100 group relative border-b border-gray-200 bg-white first:rounded-t-md last:rounded-b-md last:border-b-0 hover:bg-gray-50 dark:border-gray-700 dark:hover:border-gray-600">
<Icon.FiArrowRight className="absolute right-4 top-4 h-4 w-4 text-black opacity-0 transition-opacity group-hover:opacity-100 dark:text-white" />
{/* Don't prefetch till the time we drop the amount of javascript in [user][type] page which is impacting score for [user] page */}
<Link
+1 -1
View File
@@ -24,7 +24,7 @@ export default function Type(props: AvailabilityPageProps) {
const { t } = useLocale();
return props.away ? (
<div className="h-screen dark:bg-neutral-900">
<div className="h-screen dark:bg-gray-900">
<main className="mx-auto max-w-3xl px-4 py-24">
<div className="space-y-6" data-testid="event-types">
<div className="overflow-hidden rounded-sm border dark:border-gray-900">
+2 -2
View File
@@ -27,7 +27,7 @@ export type BookPageProps = inferSSRProps<typeof getServerSideProps>;
export default function Book(props: BookPageProps) {
const { t } = useLocale();
return props.away ? (
<div className="h-screen dark:bg-neutral-900">
<div className="h-screen dark:bg-gray-900">
<main className="mx-auto max-w-3xl px-4 py-24">
<div className="space-y-6" data-testid="event-types">
<div className="overflow-hidden rounded-sm border dark:border-gray-900">
@@ -42,7 +42,7 @@ export default function Book(props: BookPageProps) {
</main>
</div>
) : props.isDynamicGroupBooking && !props.profile.allowDynamicBooking ? (
<div className="h-screen dark:bg-neutral-900">
<div className="h-screen dark:bg-gray-900">
<main className="mx-auto max-w-3xl px-4 py-24">
<div className="space-y-6" data-testid="event-types">
<div className="overflow-hidden rounded-sm border dark:border-gray-900">
+1 -1
View File
@@ -81,7 +81,7 @@ class MyDocument extends Document<Props> {
</Head>
<body
className="dark:bg-darkgray-50 desktop-transparent bg-gray-100"
className="dark:bg-darkgray-50 desktop-transparent bg-gray-100 antialiased"
style={
this.props.isEmbed
? {
+6 -1
View File
@@ -1,10 +1,10 @@
import { IdentityProvider } from "@prisma/client";
import { NextApiRequest, NextApiResponse } from "next";
import { hashPassword } from "@calcom/lib/auth";
import { closeComUpsertTeamUser } from "@calcom/lib/sync/SyncServiceManager";
import prisma from "@calcom/prisma";
import { hashPassword } from "@lib/auth";
import slugify from "@lib/slugify";
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
@@ -12,6 +12,11 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
return;
}
if (process.env.NEXT_PUBLIC_DISABLE_SIGNUP === "true") {
res.status(403).json({ message: "Signup is disabled" });
return;
}
const data = req.body;
const { email, password } = data;
const username = slugify(data.username);
+14 -3
View File
@@ -35,6 +35,7 @@ function SingleAppPage({ data, source }: inferSSRProps<typeof getStaticProps>) {
licenseRequired={data.licenseRequired}
isProOnly={data.isProOnly}
images={source.data?.items as string[] | undefined}
isTemplate={data.isTemplate}
// tos="https://zoom.us/terms"
// privacy="https://zoom.us/privacy"
body={
@@ -70,21 +71,31 @@ export const getStaticProps = async (ctx: GetStaticPropsContext) => {
if (!singleApp) return { notFound: true };
const appDirname = app.dirName;
const isTemplate = singleApp.isTemplate;
const appDirname = path.join(isTemplate ? "templates" : "", app.dirName);
const README_PATH = path.join(process.cwd(), "..", "..", `packages/app-store/${appDirname}/DESCRIPTION.md`);
const postFilePath = path.join(README_PATH);
let source = "";
try {
/* If the app doesn't have a README we fallback to the package description */
source = fs.readFileSync(postFilePath).toString();
source = source.replace(/{DESCRIPTION}/g, singleApp.description);
} catch (error) {
/* If the app doesn't have a README we fallback to the package description */
console.log(`No DESCRIPTION.md provided for: ${appDirname}`);
source = singleApp.description;
}
const { content, data } = matter(source);
if (data.items) {
data.items = data.items.map((item: string) => {
if (!item.includes("/api/app-store")) {
// Make relative paths absolute
return `/api/app-store/${appDirname}/${item}`;
}
return item;
});
}
return {
props: {
source: { content, data },
+42 -13
View File
@@ -7,12 +7,31 @@ import { getSession } from "@calcom/lib/auth";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import type { AppCategories } from "@calcom/prisma/client";
import { inferSSRProps } from "@calcom/types/inferSSRProps";
import { AllApps, AppStoreCategories, Icon, TextField, TrendingAppsSlider } from "@calcom/ui";
import {
AllApps,
AppStoreCategories,
HorizontalTabItemProps,
HorizontalTabs,
Icon,
TextField,
PopularAppsSlider,
} from "@calcom/ui";
import AppsLayout from "@components/apps/layouts/AppsLayout";
import { ssgInit } from "@server/lib/ssg";
const tabs: HorizontalTabItemProps[] = [
{
name: "app_store",
href: "/apps",
},
{
name: "installed_apps",
href: "/apps/installed",
},
];
function AppsSearch({
onChange,
className,
@@ -43,20 +62,30 @@ export default function Apps({ categories, appStore }: inferSSRProps<typeof getS
heading={t("app_store")}
subtitle={t("app_store_description")}
actions={(className) => (
<AppsSearch className={className} onChange={(e) => setSearchText(e.target.value)} />
<div className="flex w-full flex-col md:flex-row md:justify-between lg:w-auto">
<div className="lg:hidden">
<HorizontalTabs tabs={tabs} />
</div>
<div>
<AppsSearch className={className} onChange={(e) => setSearchText(e.target.value)} />
</div>
</div>
)}
headerClassName="sm:hidden lg:block hidden"
emptyStore={!appStore.length}>
{!searchText && (
<>
<AppStoreCategories categories={categories} />
<TrendingAppsSlider items={appStore} />
</>
)}
<AllApps
apps={appStore}
searchText={searchText}
categories={categories.map((category) => category.name)}
/>
<div className="flex flex-col gap-y-8">
{!searchText && (
<>
<AppStoreCategories categories={categories} />
<PopularAppsSlider items={appStore} />
</>
)}
<AllApps
apps={appStore}
searchText={searchText}
categories={categories.map((category) => category.name)}
/>
</div>
</AppsLayout>
);
}
+21 -20
View File
@@ -92,8 +92,8 @@ function ConnectOrDisconnectIntegrationButton(props: {
}
interface IntegrationsContainerProps {
variant?: keyof typeof InstalledAppVariants;
exclude?: (keyof typeof InstalledAppVariants)[];
variant?: typeof InstalledAppVariants[number];
exclude?: typeof InstalledAppVariants[number][];
}
interface IntegrationsListProps {
@@ -115,6 +115,7 @@ const IntegrationsList = ({ data }: IntegrationsListProps) => {
logo={item.logo}
description={item.description}
separate={true}
isTemplate={item.isTemplate}
invalidCredential={item.invalidCredentialIds.length > 0}
actions={
<div className="flex w-16 justify-end">
@@ -143,8 +144,10 @@ const IntegrationsContainer = ({ variant, exclude }: IntegrationsContainerProps)
automation: Icon.FiShare2,
analytics: Icon.FiBarChart,
payment: Icon.FiCreditCard,
web3: Icon.FiBarChart,
other: Icon.FiGrid,
};
return (
<QueryCell
query={query}
@@ -198,31 +201,29 @@ const IntegrationsContainer = ({ variant, exclude }: IntegrationsContainerProps)
};
const querySchema = z.object({
category: z.nativeEnum(InstalledAppVariants),
category: z.enum(InstalledAppVariants),
});
type querySchemaType = z.infer<typeof querySchema>;
export default function InstalledApps() {
const { t } = useLocale();
const router = useRouter();
const category = router.query.category;
const category = router.query.category as querySchemaType["category"];
const categoryList: querySchemaType["category"][] = [
"payment",
"conferencing",
"automation",
"analytics",
"web3",
];
return (
<InstalledAppsLayout heading={t("installed_apps")} subtitle={t("manage_your_connected_apps")}>
{(category === InstalledAppVariants.payment || category === InstalledAppVariants.conferencing) && (
<IntegrationsContainer variant={category} />
)}
{(category === InstalledAppVariants.automation || category === InstalledAppVariants.analytics) && (
<IntegrationsContainer variant={category} />
)}
{category === InstalledAppVariants.calendar && <CalendarListContainer />}
{category === InstalledAppVariants.other && (
<IntegrationsContainer
exclude={[
InstalledAppVariants.conferencing,
InstalledAppVariants.calendar,
InstalledAppVariants.analytics,
InstalledAppVariants.automation,
]}
/>
{categoryList.includes(category) && <IntegrationsContainer variant={category} />}
{category === "calendar" && <CalendarListContainer />}
{category === "other" && (
<IntegrationsContainer variant={category} exclude={[...categoryList, "calendar"]} />
)}
</InstalledAppsLayout>
);
+7 -1
View File
@@ -115,7 +115,13 @@ export default function Login({
description={t("login")}
showLogo
heading={twoFactorRequired ? t("2fa_code") : t("welcome_back")}
footerText={twoFactorRequired ? TwoFactorFooter : LoginFooter}>
footerText={
twoFactorRequired
? TwoFactorFooter
: process.env.NEXT_PUBLIC_DISABLE_SIGNUP !== "true"
? LoginFooter
: null
}>
<FormProvider {...methods}>
<form onSubmit={methods.handleSubmit(onSubmit)} data-testid="login-form">
<div>
+1 -4
View File
@@ -186,11 +186,8 @@ export default function Availability({ schedule }: { schedule: number }) {
});
}}
className="flex flex-col pb-16 sm:mx-0 xl:flex-row xl:space-x-6">
<div className="flex-1 divide-y divide-neutral-200 rounded-md border">
<div className="flex-1 divide-y divide-gray-200 rounded-md border">
<div className=" py-5 sm:p-6">
<h3 className="mb-2 px-5 text-base font-medium leading-6 text-gray-900 sm:pl-0">
{t("change_start_end")}
</h3>
{typeof me.data?.weekStart === "string" && (
<Schedule
control={control}
+1 -1
View File
@@ -84,7 +84,7 @@ export function AvailabilityList({ schedules }: RouterOutputs["viewer"]["availab
</div>
) : (
<div className="mb-16 overflow-hidden rounded-md border border-gray-200 bg-white">
<ul className="divide-y divide-neutral-200" data-testid="schedules" ref={animationParentRef}>
<ul className="divide-y divide-gray-200" data-testid="schedules" ref={animationParentRef}>
{schedules.map((schedule) => (
<ScheduleListItem
displayOptions={{
+2 -2
View File
@@ -78,7 +78,7 @@ const AvailabilityView = ({ user }: { user: User }) => {
.map((slot: IBusySlot) => (
<div
key={dayjs(slot.start).format("HH:mm")}
className="overflow-hidden rounded-md bg-neutral-100"
className="overflow-hidden rounded-md bg-gray-100"
data-testid="troubleshooter-busy-time">
<div className="px-4 py-5 text-black sm:p-6">
{t("calendar_shows_busy_between")}{" "}
@@ -97,7 +97,7 @@ const AvailabilityView = ({ user }: { user: User }) => {
</div>
));
return (
<div className="overflow-hidden rounded-md bg-neutral-100">
<div className="overflow-hidden rounded-md bg-gray-100">
<div className="px-4 py-5 text-black sm:p-6">{t("calendar_no_busy_slots")}</div>
</div>
);
+11 -9
View File
@@ -379,7 +379,7 @@ export default function Success(props: SuccessProps) {
<div
className={classNames(
"main inline-block transform overflow-hidden rounded-lg border sm:my-8 sm:max-w-xl",
isBackgroundTransparent ? "" : "dark:bg-darkgray-100 bg-white dark:border-neutral-700",
isBackgroundTransparent ? "" : "dark:bg-darkgray-100 dark:border-darkgray-200 bg-white",
"px-8 pt-5 pb-4 text-left align-bottom transition-all sm:w-full sm:py-8 sm:align-middle"
)}
role="dialog"
@@ -424,7 +424,8 @@ export default function Success(props: SuccessProps) {
<div className="mt-3">
<p className="text-gray-600 dark:text-gray-300">{getTitle()}</p>
</div>
<div className="border-bookinglightest text-bookingdark dark:border-darkgray-300 mt-8 grid grid-cols-3 border-t pt-8 text-left dark:text-gray-300">
<div className="border-bookinglightest text-bookingdark dark:border-darkgray-200 mt-8 grid grid-cols-3 border-t pt-8 text-left dark:text-gray-300">
{(isCancelled || reschedule) && cancellationReason && (
<>
<div className="font-medium">
@@ -594,7 +595,7 @@ export default function Success(props: SuccessProps) {
</>
) : (
<>
<hr className="border-bookinglightest dark:border-darkgray-300" />
<hr className="border-bookinglightest dark:border-darkgray-200" />
<CancelBooking
booking={{ uid: bookingInfo?.uid, title: bookingInfo?.title, id: bookingInfo?.id }}
profile={{ name: props.profile.name, slug: props.profile.slug }}
@@ -636,7 +637,7 @@ export default function Success(props: SuccessProps) {
encodeURIComponent(new RRule(props.eventType.recurringEvent).toString())
: "")
}
className="h-10 w-10 rounded-sm border border-neutral-200 px-3 py-2 ltr:mr-2 rtl:ml-2 dark:border-neutral-700 dark:text-white">
className="h-10 w-10 rounded-sm border border-gray-200 px-3 py-2 ltr:mr-2 rtl:ml-2 dark:border-gray-700 dark:text-white">
<svg
className="-mt-1.5 inline-block h-4 w-4"
fill="currentColor"
@@ -659,7 +660,7 @@ export default function Success(props: SuccessProps) {
eventName
) + (location ? "&location=" + location : "")
}
className="mx-2 h-10 w-10 rounded-sm border border-neutral-200 px-3 py-2 dark:border-neutral-700 dark:text-white"
className="mx-2 h-10 w-10 rounded-sm border border-gray-200 px-3 py-2 dark:border-gray-700 dark:text-white"
target="_blank">
<svg
className="mr-1 -mt-1.5 inline-block h-4 w-4"
@@ -683,7 +684,7 @@ export default function Success(props: SuccessProps) {
eventName
) + (location ? "&location=" + location : "")
}
className="mx-2 h-10 w-10 rounded-sm border border-neutral-200 px-3 py-2 dark:border-neutral-700 dark:text-white"
className="mx-2 h-10 w-10 rounded-sm border border-gray-200 px-3 py-2 dark:border-gray-700 dark:text-white"
target="_blank">
<svg
className="mr-1 -mt-1.5 inline-block h-4 w-4"
@@ -696,7 +697,7 @@ export default function Success(props: SuccessProps) {
</Link>
<Link
href={"data:text/calendar," + eventLink()}
className="mx-2 h-10 w-10 rounded-sm border border-neutral-200 px-3 py-2 dark:border-neutral-700 dark:text-white"
className="mx-2 h-10 w-10 rounded-sm border border-gray-200 px-3 py-2 dark:border-gray-700 dark:text-white"
download={props.eventType.title + ".ics"}>
<svg
version="1.1"
@@ -928,6 +929,7 @@ const schema = z.object({
uid: z.string(),
email: z.string().optional(),
eventTypeSlug: z.string().optional(),
cancel: stringToBoolean,
});
const handleSeatsEventTypeOnBooking = (
@@ -961,7 +963,7 @@ export async function getServerSideProps(context: GetServerSidePropsContext) {
const ssr = await ssrInit(context);
const parsedQuery = schema.safeParse(context.query);
if (!parsedQuery.success) return { notFound: true };
const { uid, email, eventTypeSlug } = parsedQuery.data;
const { uid, email, eventTypeSlug, cancel } = parsedQuery.data;
const bookingInfo = await prisma.booking.findFirst({
where: {
@@ -1047,7 +1049,7 @@ export async function getServerSideProps(context: GetServerSidePropsContext) {
customInputs: customInputSchema.array().parse(eventTypeRaw.customInputs),
};
if (eventType.metadata?.disableSuccessPage && eventType.successRedirectUrl) {
if (eventType.metadata?.disableSuccessPage && eventType.successRedirectUrl && !cancel) {
return {
redirect: {
destination: eventType.successRedirectUrl,
+8 -7
View File
@@ -196,16 +196,17 @@ const EventTypePage = (props: EventTypeSetupProps) => {
periodCountCalendarDays: eventType.periodCountCalendarDays ? "1" : "0",
schedulingType: eventType.schedulingType,
minimumBookingNotice: eventType.minimumBookingNotice,
minimumBookingNoticeInDurationType: convertToNewDurationType(
"minutes",
findDurationType(eventType.minimumBookingNotice),
eventType.minimumBookingNotice
),
metadata,
hosts: !!eventType.hosts?.length
? eventType.hosts.filter((host) => !host.isFixed)
: eventType.users.map((user) => ({ userId: user.id })),
hostsFixed: eventType.hosts.filter((host) => host.isFixed),
: eventType.users
.filter(() => eventType.schedulingType === SchedulingType.ROUND_ROBIN)
.map((user) => ({ userId: user.id })),
hostsFixed: !!eventType.hosts?.length
? eventType.hosts.filter((host) => host.isFixed)
: eventType.users
.filter(() => eventType.schedulingType === SchedulingType.COLLECTIVE)
.map((user) => ({ userId: user.id })),
},
resolver: zodResolver(
z
+13 -7
View File
@@ -251,7 +251,7 @@ export const EventTypeList = ({ group, groupIndex, readOnly, types }: EventTypeL
const lastItem = types[types.length - 1];
return (
<div className="mb-16 flex overflow-hidden rounded-md border border-gray-200 bg-white">
<ul ref={parent} className="!static w-full divide-y divide-neutral-200" data-testid="event-types">
<ul ref={parent} className="!static w-full divide-y divide-gray-200" data-testid="event-types">
{types.map((type, index) => {
const embedLink = `${group.profile.slug}/${type.slug}`;
const calLink = `${CAL_URL}/${embedLink}`;
@@ -312,7 +312,7 @@ export const EventTypeList = ({ group, groupIndex, readOnly, types }: EventTypeL
<Button
color="secondary"
target="_blank"
size="icon"
variant="icon"
href={calLink}
StartIcon={Icon.FiExternalLink}
/>
@@ -321,7 +321,7 @@ export const EventTypeList = ({ group, groupIndex, readOnly, types }: EventTypeL
<Tooltip content={t("copy_link")}>
<Button
color="secondary"
size="icon"
variant="icon"
StartIcon={Icon.FiLink}
onClick={() => {
showToast(t("link_copied"), "success");
@@ -336,7 +336,7 @@ export const EventTypeList = ({ group, groupIndex, readOnly, types }: EventTypeL
className="radix-state-open:rounded-r-md">
<Button
type="button"
size="icon"
variant="icon"
color="secondary"
StartIcon={Icon.FiMoreHorizontal}
/>
@@ -370,11 +370,12 @@ export const EventTypeList = ({ group, groupIndex, readOnly, types }: EventTypeL
{t("embed")}
</EmbedButton>
</DropdownMenuItem>
<DropdownMenuSeparator className="h-px bg-gray-200" />
<DropdownMenuSeparator />
{/* readonly is only set when we are on a team - if we are on a user event type null will be the value. */}
{(group.metadata?.readOnly === false || group.metadata.readOnly === null) && (
<DropdownMenuItem>
<DropdownItem
color="destructive"
onClick={() => {
setDeleteDialogOpen(true);
setDeleteDialogTypeId(type.id);
@@ -395,7 +396,12 @@ export const EventTypeList = ({ group, groupIndex, readOnly, types }: EventTypeL
<div className="min-w-9 mx-5 flex sm:hidden">
<Dropdown>
<DropdownMenuTrigger asChild data-testid={"event-type-options-" + type.id}>
<Button type="button" size="icon" color="secondary" StartIcon={Icon.FiMoreHorizontal} />
<Button
type="button"
variant="icon"
color="secondary"
StartIcon={Icon.FiMoreHorizontal}
/>
</DropdownMenuTrigger>
<DropdownMenuPortal>
<DropdownMenuContent>
@@ -466,7 +472,7 @@ export const EventTypeList = ({ group, groupIndex, readOnly, types }: EventTypeL
{t("duplicate")}
</Button>
</DropdownMenuItem>
<DropdownMenuSeparator className="h-px bg-gray-200" />
<DropdownMenuSeparator />
<DropdownMenuItem className="outline-none">
<Button
onClick={() => {
+1 -1
View File
@@ -29,7 +29,7 @@ const CtaRow = ({ title, description, className, children }: CtaRowProps) => {
</div>
<div className="flex-shrink-0 pt-3 sm:ml-auto sm:pt-0 sm:pl-3">{children}</div>
</section>
<hr className="border-neutral-200" />
<hr className="border-gray-200" />
</>
);
};
@@ -7,7 +7,6 @@ import { APP_NAME } from "@calcom/lib/constants";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { trpc } from "@calcom/trpc/react";
import {
Badge,
Button,
ColorPicker,
Form,
@@ -17,6 +16,7 @@ import {
SkeletonContainer,
SkeletonText,
Switch,
UpgradeTeamsBadge,
} from "@calcom/ui";
import { ssrInit } from "@server/lib/ssr";
@@ -49,6 +49,7 @@ const AppearanceView = () => {
const session = useSession();
const utils = trpc.useContext();
const { data: user, isLoading } = trpc.viewer.me.useQuery();
const { data: dataHasTeamPlan, isLoading: isLoadingHasTeamPlan } = trpc.viewer.teams.hasTeamPlan.useQuery();
const formMethods = useForm({
defaultValues: {
@@ -73,7 +74,8 @@ const AppearanceView = () => {
},
});
if (isLoading) return <SkeletonLoader title={t("appearance")} description={t("appearance_description")} />;
if (isLoading || isLoadingHasTeamPlan)
return <SkeletonLoader title={t("appearance")} description={t("appearance_description")} />;
if (!user) return null;
@@ -121,7 +123,7 @@ const AppearanceView = () => {
/>
</div>
<hr className="border-1 my-8 border-neutral-200" />
<hr className="my-8 border border-gray-200" />
<div className="mb-6 flex items-center text-sm">
<div>
<p className="font-semibold">{t("custom_brand_colors")}</p>
@@ -167,7 +169,7 @@ const AppearanceView = () => {
onClick={() => window.open(`${WEBAPP_URL}/${user.username}/${user.eventTypes[0].title}`, "_blank")}>
Preview
</Button> */}
<hr className="border-1 my-8 border-neutral-200" />
<hr className="my-8 border border-gray-200" />
<Controller
name="hideBranding"
control={formMethods.control}
@@ -180,18 +182,18 @@ const AppearanceView = () => {
<p className="font-semibold ltr:mr-2 rtl:ml-2">
{t("disable_cal_branding", { appName: APP_NAME })}
</p>
<Badge variant="gray">{t("pro")}</Badge>
{!dataHasTeamPlan?.hasTeamPlan && <UpgradeTeamsBadge />}
</div>
<p className="mt-0.5 text-gray-600">{t("removes_cal_branding", { appName: APP_NAME })}</p>
</div>
<div className="flex-none">
<Switch
id="hideBranding"
disabled={!session.data?.user.belongsToActiveTeam}
disabled={!dataHasTeamPlan?.hasTeamPlan}
onCheckedChange={(checked) =>
formMethods.setValue("hideBranding", checked, { shouldDirty: true })
}
checked={!session.data?.user.belongsToActiveTeam ? false : value}
checked={!dataHasTeamPlan?.hasTeamPlan ? false : value}
/>
</div>
</div>
@@ -13,18 +13,18 @@ import {
Dropdown,
DropdownMenuContent,
DropdownMenuItem,
DropdownItem,
DropdownMenuTrigger,
Icon,
List,
ListItem,
ListItemText,
ListItemTitle,
Meta,
showToast,
SkeletonContainer,
SkeletonText,
} from "@calcom/ui";
import AppListCard from "@components/AppListCard";
import { ssrInit } from "@server/lib/ssr";
const SkeletonLoader = ({ title, description }: { title: string; description: string }) => {
@@ -75,41 +75,37 @@ const ConferencingLayout = () => {
apps.items
.map((app) => ({ ...app, title: app.title || app.name }))
.map((app) => (
<ListItem className="flex-col border-0" key={app.title}>
<div className="flex w-full flex-1 items-center space-x-2 p-4 rtl:space-x-reverse">
{
// eslint-disable-next-line @next/next/no-img-element
app.logo && <img className="h-10 w-10" src={app.logo} alt={app.title} />
}
<div className="flex-grow truncate pl-2">
<ListItemTitle component="h3" className="mb-1 space-x-2 rtl:space-x-reverse">
<h3 className="truncate text-sm font-medium text-gray-900">{app.title}</h3>
</ListItemTitle>
<ListItemText component="p">{app.description}</ListItemText>
</div>
<AppListCard
description={app.description}
title={app.title}
logo={app.logo}
key={app.title}
isDefault={app.isGlobal}
actions={
<div>
<Dropdown>
<DropdownMenuTrigger asChild>
<Button StartIcon={Icon.FiMoreHorizontal} size="icon" color="secondary" />
<Button StartIcon={Icon.FiMoreHorizontal} variant="icon" color="secondary" />
</DropdownMenuTrigger>
<DropdownMenuContent>
<DropdownMenuItem>
<Button
<DropdownItem
type="button"
color="destructive"
StartIcon={Icon.FiTrash}
disabled={app.isGlobal}
StartIcon={Icon.FiTrash}
onClick={() => {
setDeleteCredentialId(app.credentialIds[0]);
setDeleteAppModal(true);
}}>
{t("remove_app")}
</Button>
</DropdownItem>
</DropdownMenuItem>
</DropdownMenuContent>
</Dropdown>
</div>
</div>
</ListItem>
}
/>
))}
</List>
@@ -223,7 +223,7 @@ const ProfileView = () => {
}
/>
<hr className="my-6 border-neutral-200" />
<hr className="my-6 border-gray-200" />
<Label>{t("danger_zone")}</Label>
{/* Delete account Dialog */}
+11 -1
View File
@@ -92,7 +92,11 @@ export default function Signup({ prepopulateFormValues }: inferSSRProps<typeof g
{...register("username")}
required
/>
<EmailField {...register("email")} />
<EmailField
{...register("email")}
disabled={prepopulateFormValues?.email}
className="disabled:bg-gray-200 disabled:hover:cursor-not-allowed"
/>
<PasswordField
labelProps={{
className: "block text-sm font-medium text-gray-700",
@@ -145,6 +149,12 @@ export const getServerSideProps = async (ctx: GetServerSidePropsContext) => {
prepopulateFormValues: undefined,
};
if (process.env.NEXT_PUBLIC_DISABLE_SIGNUP === "true") {
return {
notFound: true,
};
}
// no token given, treat as a normal signup without verification token
if (!token) {
return {
+2 -2
View File
@@ -36,12 +36,12 @@ function TeamPage({ team }: TeamPageProps) {
}, [telemetry, router.asPath]);
const EventTypes = () => (
<ul className="rounded-md border border-neutral-200 dark:border-neutral-700">
<ul className="rounded-md border border-gray-200 dark:border-gray-700">
{team.eventTypes.map((type, index) => (
<li
key={index}
className={classNames(
"dark:bg-darkgray-100 group relative border-b border-neutral-200 bg-white first:rounded-t-md last:rounded-b-md last:border-b-0 hover:bg-gray-50 dark:border-neutral-700 dark:hover:border-neutral-600",
"dark:bg-darkgray-100 group relative border-b border-gray-200 bg-white first:rounded-t-md last:rounded-b-md last:border-b-0 hover:bg-gray-50 dark:border-gray-700 dark:hover:border-gray-600",
!isEmbed && "bg-white"
)}>
<Link
+1 -1
View File
@@ -16,7 +16,7 @@ function Teams() {
subtitle={t("create_manage_teams_collaborative")}
CTA={
<Button
size="fab"
variant="fab"
StartIcon={Icon.FiPlus}
type="button"
href={`${WEBAPP_URL}/settings/teams/new?returnTo=${WEBAPP_URL}/teams`}>
@@ -68,7 +68,7 @@
"event_still_awaiting_approval": "Událost stále čeká na schválení",
"booking_submitted_subject": "Rezervace odeslána: {{eventType}} s {{name}} během {{date}}",
"your_meeting_has_been_booked": "Vaše schůzka byla rezervována",
"event_type_has_been_rescheduled_on_time_date": "Váš {{eventType}} s {{name}} byl přesunut na {{date}}.",
"event_type_has_been_rescheduled_on_time_date": "Váš {{title}} byl přeplánován na {{date}}.",
"event_has_been_rescheduled": "Změna - Vaše událost byla přesunuta na jindy",
"request_reschedule_title_attendee": "Žádost o přeplánování rezervace",
"request_reschedule_subtitle": "Organizátor {{organizer}} zrušil rezervaci a požádal vás, abyste si vybrali jiný čas.",
@@ -9,7 +9,7 @@
"have_any_questions": "Haben Sie Fragen? Wir sind hier um Ihnen zu helfen.",
"reset_password_subject": "{{appName}}: Anleitung zum Zurücksetzen des Passworts",
"event_declined_subject": "Abgelehnt: {{eventType}} mit {{name}} am {{date}}",
"event_cancelled_subject": "Abgesagt: {{eventType}} mit {{name}} am {{date}}",
"event_cancelled_subject": "Storniert: {{title}} um {{date}}",
"event_request_declined": "Ihre Event-Anfrage wurde abgelehnt",
"event_request_declined_recurring": "Ihre wiederkehrende Terminanfrage wurde abgelehnt",
"event_request_cancelled": "Ihr geplanter Termin wurde abgesagt",
@@ -67,7 +67,7 @@
"event_still_awaiting_approval": "Ein Termin wartet noch auf Ihre Bestätigung",
"booking_submitted_subject": "Buchung eingereicht: {{eventType}} mit {{name}} am {{date}}",
"your_meeting_has_been_booked": "Dein Termin wurde gebucht",
"event_type_has_been_rescheduled_on_time_date": "Ihr {{eventType}} mit {{name}} wurde auf {{time}} ({{timeZone}}) am {{date}} verschoben.",
"event_type_has_been_rescheduled_on_time_date": "Ihr {{title}} wurde auf den {{date}} verschoben.",
"event_has_been_rescheduled": "Ihr Termin wurde verschoben.",
"request_reschedule_title_attendee": "Anfrage zur Neuplanung Ihrer Buchung",
"request_reschedule_subtitle": "{{organizer}} hat die Buchung storniert und Sie gebeten, eine andere Zeit auszuwählen.",
+167 -1
View File
@@ -38,20 +38,37 @@
"confirm_or_reject_request": "Επιβεβαιώστε ή απορρίψτε το αίτημα",
"check_bookings_page_to_confirm_or_reject": "Ελέγξτε τη σελίδα κρατήσεων για να επιβεβαιώσετε ή να απορρίψετε την κράτηση.",
"event_awaiting_approval": "Ένα γεγονός περιμένει την έγκρισή σας",
"ics_event_title": "{{eventType}} με {{name}}",
"join_by_entrypoint": "Συμμετάσχετε από {{entryPoint}}",
"notes": "Σημειώσεις",
"manage_my_bookings": "Διαχείριση κρατήσεων",
"event_type": "Τύπος Συμβάντος",
"meeting_password": "Συνθηματικό Συνάντησης",
"meeting_url": "URL Συνάντησης",
"meeting_request_rejected": "Το αίτημα συνάντησής σας απορρίφθηκε",
"hi": "Γεια",
"join_team": "Γίνε μέλος της ομάδας",
"manage_this_team": "Διαχείριση αυτής της ομάδας",
"team_info": "Πληροφορίες Ομάδας",
"request_another_invitation_email": "Αν προτιμάτε να μην χρησιμοποιείτε το {{toEmail}} ως {{appName}} email σας ή να έχετε ήδη λογαριασμό {{appName}} , παρακαλώ ζητήστε μια άλλη πρόσκληση σε αυτό το email.",
"you_have_been_invited": "Έχετε προσκληθεί να γίνετε μέλος της ομάδας {{teamName}}",
"user_invited_you": "Ο χρήστης {{user}} σας προσκάλεσε να συμμετάσχετε στην ομάδα {{team}} στο {{appName}}",
"hidden_team_member_title": "Είστε κρυμμένοι σε αυτήν την ομάδα",
"link_expires": "Υ.Γ. Λήγει σε {{expiresIn}} ώρες.",
"team_upgrade_banner_action": "Αναβάθμισε εδώ",
"team_upgraded_successfully": "Η ομάδα σας αναβαθμίστηκε επιτυχώς!",
"use_link_to_reset_password": "Χρησιμοποιήστε τον παρακάτω σύνδεσμο για να επαναφέρετε τον κωδικό πρόσβασης",
"hey_there": "Γεια σας,",
"forgot_your_password_calcom": "Ξεχάσατε τον κωδικό πρόσβασης? - {{appName}}",
"confirm_delete_webhook": "Ναι, διαγραφή webhook",
"edit_webhook": "Επεξεργασία Webhook",
"delete_webhook": "Διαγραφή Webhook",
"webhook_status": "Κατάσταση Webhook",
"webhook_enabled": "Ενεργοποιημένο Webhook",
"webhook_disabled": "Απενεργοποιημένο Webhook",
"webhook_response": "Απάντηση Webhook",
"webhook_test": "Έλεγχος Webhook",
"manage_your_webhook": "Διαχείριση του webhook σας",
"webhook_created_successfully": "Το Webhook δημιουργήθηκε επιτυχώς!",
"webhook_updated_successfully": "Το Webhook ενημερώθηκε επιτυχώς!",
"webhook_removed_successfully": "Το Webhook αφαιρέθηκε επιτυχώς!",
@@ -62,6 +79,14 @@
"past": "Παρελθοντικά",
"choose_a_file": "Επιλογή αρχείου...",
"upload_image": "Μεταφόρτωση εικόνας",
"view_notifications": "Εμφάνιση ειδοποιήσεων",
"view_public_page": "Προβολή δημόσιας σελίδας",
"copy_public_page_link": "Αντιγραφή συνδέσμου δημόσιας σελίδας",
"sign_out": "Αποσύνδεση",
"2fa_confirm_current_password": "Επιβεβαιώστε τον κωδικό πρόσβασης σας για να ξεκινήσετε.",
"required": "Υποχρεωτικό",
"optional": "Προαιρετικό",
"guests": "Επισκέπτες",
"username": "Όνομα χρήστη",
"is_still_available": "είναι ακόμα διαθέσιμο.",
"documentation": "Τεκμηρίωση",
@@ -88,17 +113,37 @@
"dont_have_an_account": "Δεν έχετε λογαριασμό;",
"sign_in_account": "Συνδεθείτε στο λογαριασμό σας",
"sign_in": "Είσοδος",
"error_during_login": "Παρουσιάστηκε σφάλμα κατά τη σύνδεσή σας. Επιστρέψτε στην οθόνη σύνδεσης και προσπαθήστε ξανά.",
"request_password_reset": "Αποστολή email επαναφοράς",
"forgot_password": "Ξεχάσατε τον κωδικό πρόσβασης?",
"all_apps": "Όλα",
"check_email_reset_password": "Ελέγξτε το email σας. Σας στείλαμε ένα σύνδεσμο για να επαναφέρετε τον κωδικό πρόσβασής σας.",
"few_sentences_about_yourself": "Μερικά λόγια για τον εσάς. Αυτό θα εμφανιστεί στην προσωπική σελίδα σας.",
"set_availability": "Όρισε τη διαθεσιμότητά σου",
"details": "Λεπτομέρειες",
"welcome": "Καλωσορίσατε",
"welcome_back": "Καλωσορίσατε πίσω",
"welcome_to_calcom": "Καλωσορίσατε στο {{appName}}",
"connect_caldav": "Σύνδεση στο CalDav (Beta)",
"credentials_stored_and_encrypted": "Τα διαπιστευτήριά σας θα αποθηκευτούν και θα κρυπτογραφηθούν.",
"connect": "Σύνδεση",
"try_for_free": "Δοκιμάστε δωρεάν",
"add_to_calendar": "Προσθήκη στο ημερολόγιο",
"add_another_calendar": "Προσθήκη άλλου ημερολογίου",
"email_sign_in_subject": "Ο σύνδεσμος εισόδου για {{appName}}",
"meeting_is_scheduled": "Η συνάντηση έχει προγραμματιστεί",
"submitted": "Η κράτησή σας έχει υποβληθεί",
"booking_submitted": "Η κράτησή σας έχει υποβληθεί",
"booking_confirmed": "Η κράτησή σας έχει επιβεβαιωθεί",
"booking_confirmed_recurring": "Η επαναλαμβανόμενη συνάντησή σας έχει επιβεβαιωθεί",
"warning_recurring_event_payment": "Οι πληρωμές δεν υποστηρίζονται ακόμη στις επαναλαμβανόμενες εκδηλώσεις",
"enter_new_password": "Εισάγετε τον νέο κωδικό πρόσβασης που θέλετε για το λογαριασμό σας.",
"reset_password": "Επαναφορά Κωδικού Πρόσβασης",
"change_your_password": "Αλλαγή κωδικού πρόσβασης",
"show_password": "Εμφάνιση κωδικού πρόσβασης",
"hide_password": "Απόκρυψη κωδικού πρόσβασης",
"try_again": "Δοκιμάστε ξανά",
"request_is_expired": "Το αίτημα έχει λήξει.",
"sunday_time_error": "Μη έγκυρη ώρα την Κυριακή",
"monday_time_error": "Μη έγκυρη ώρα τη Δευτέρα",
"tuesday_time_error": "Μη έγκυρη ώρα την Τρίτη",
@@ -106,6 +151,127 @@
"thursday_time_error": "Μη έγκυρη ώρα την Πέμπτη",
"friday_time_error": "Μη έγκυρη ώρα την Παρασκευή",
"saturday_time_error": "Μη έγκυρη ώρα το Σάββατο",
"error_with_status_code_occured": "Παρουσιάστηκε ένα σφάλμα με κωδικό κατάστασης {{status}}.",
"booking_already_cancelled": "Αυτή η κράτηση είχε ήδη ακυρωθεί",
"booking_already_accepted_rejected": "Αυτή η κράτηση έγινε ήδη αποδεκτή ή απορρίφθηκε",
"go_back_home": "Επιστροφή στην αρχική σελίδα",
"or_go_back_home": "Ή επιστροφή στην αρχική σελίδα",
"no_meeting_found": "Δε βρέθηκε συνάντηση",
"bookings": "Κρατήσεις"
"no_status_bookings_yet": "Δεν υπάρχουν {{status}} κρατήσεις",
"no_status_bookings_yet_description": "Δεν έχετε {{status}} κρατήσεις. {{description}}",
"bookings": "Κρατήσεις",
"unconfirmed_bookings_tooltip": "Μη επιβεβαιωμένες κρατήσεις",
"start_time": "Ώρα έναρξης",
"end_time": "Ώρα λήξης",
"your_day_starts_at": "Η ημέρα σας ξεκινάει στις",
"your_day_ends_at": "Η ημέρα σας τελειώνει στις",
"change_available_times": "Αλλαγή διαθέσιμων ωρών",
"change_your_available_times": "Αλλάξτε τις διαθέσιμες ώρες σας",
"change_start_end": "Αλλάξτε την ώρα έναρξης και λήξης της ημέρας σας",
"current_start_date": "Αυτή τη στιγμή, η ημέρα σας έχει οριστεί να ξεκινήσει στις",
"email": "Email",
"email_placeholder": "jdoe@example.com",
"loading": "Φόρτωση...",
"deleting": "Διαγραφή...",
"developer": "Προγραμματιστές",
"schedule_a_meeting": "Προγραμματισμός συνάντησης",
"view_and_manage_billing_details": "Προβολή και διαχείριση των στοιχείων χρέωσης",
"view_and_edit_billing_details": "Προβολή και επεξεργασία των στοιχείων χρέωσης σας, καθώς και ακύρωση της συνδρομής σας.",
"need_anything_else": "Χρειάζεστε οτιδήποτε άλλο;",
"further_billing_help": "Αν χρειάζεστε περαιτέρω βοήθεια με την τιμολόγηση, η ομάδα υποστήριξής μας είναι εδώ για να βοηθήσει.",
"contact": "Επικοινωνία",
"contact_our_support_team": "Επικοινωνήστε με την ομάδα υποστήριξης",
"no_event_types_have_been_setup": "Αυτός ο χρήστης δεν έχει ορίσει ακόμα τύπους γεγονότων.",
"edit_logo": "Επεξεργασία λογοτύπου",
"upload_a_logo": "Ανεβάστε ένα λογότυπο",
"remove_logo": "Αφαίρεση λογότυπου",
"enable": "Ενεργοποίηση",
"2fa": "Ταυτοποίηση Δύο Παραμέτρων",
"security": "Ασφάλεια",
"manage_account_security": "Διαχειριστείτε την ασφάλεια του λογαριασμού σας.",
"password": "Κωδικός",
"password_updated_successfully": "Ο κωδικός πρόσβασης ενημερώθηκε επιτυχώς",
"password_has_been_changed": "Ο κωδικός πρόσβασής σας έχει αλλάξει με επιτυχία.",
"error_changing_password": "Σφάλμα στην αλλαγή κωδικού πρόσβασης",
"something_went_wrong": "Κάτι πήγε στραβά.",
"something_doesnt_look_right": "Κάτι δεν φαίνεται σωστά;",
"please_try_again": "Παρακαλώ δοκιμάστε ξανά.",
"super_secure_new_password": "Ο σούπερ ασφαλής νέος κωδικός πρόσβασης σας",
"new_password": "Νέος Κωδικός",
"your_old_password": "Ο παλιός σας κωδικός",
"current_password": "Τρέχων Κωδικός",
"change_password": "Αλλαγή Κωδικού",
"change_secret": "Αλλαγή Μυστικού",
"new_password_matches_old_password": "Ο νέος κωδικός πρόσβασης ταιριάζει με τον παλιό σας κωδικό. Παρακαλώ επιλέξτε διαφορετικό κωδικό πρόσβασης.",
"your_name": "Το όνομά σας",
"email_address": "Διεύθυνση ηλεκτρονικού ταχυδρομείου",
"enter_valid_email": "Παρακαλώ εισάγετε ένα έγκυρο email",
"location": "Τοποθεσία",
"address": "Διεύθυνση",
"enter_address": "Εισάγετε διεύθυνση",
"yes": "ναι",
"no": "όχι",
"additional_notes": "Πρόσθετες σημειώσεις",
"your_number": "Ο αριθμός τηλεφώνου σας",
"phone_number": "Αριθμός Τηλεφώνου",
"attendee_phone_number": "Αριθμός Τηλεφώνου Συμμετέχοντα",
"host_phone_number": "Ο αριθμός τηλεφώνου σας",
"enter_phone_number": "Εισάγετε αριθμό τηλεφώνου",
"or": "Ή",
"go_back": "Επιστροφή",
"email_or_username": "Email ή Όνομα χρήστη",
"role": "Ρόλος",
"edit_role": "Επεξεργασία Ρόλου",
"edit_team": "Επεξεργασία ομάδας",
"reject": "Απορρίψτε",
"reject_all": "Απόρριψη όλων",
"accept": "Αποδοχή",
"leave": "Αποχώρηση",
"team_name": "Όνομα ομάδας",
"your_team_name": "Το όνομα της ομάδας σας",
"team_updated_successfully": "Επιτυχής ενημέρωση ομάδας",
"your_team_updated_successfully": "Η ομάδα σας ενημερώθηκε με επιτυχία.",
"about": "Σχετικά",
"confirm_remove_member": "Ναι, αφαίρεση μέλους",
"remove_member": "Αφαίρεση μέλους",
"manage_your_team": "Διαχείριση ομάδας",
"no_teams": "Δεν έχετε καμία ομάδα ακόμη.",
"submit": "Υποβολή",
"delete": "Διαγραφή",
"update": "Ενημέρωση",
"save": "Αποθήκευση",
"pending": "Εκκρεμεί",
"share": "Κοινοποίηση",
"leave_team": "Αποχώρηση από ομάδα",
"confirm_leave_team": "Ναι, αποχώρηση από την ομάδα",
"preview": "Προεπισκόπηση",
"link_copied": "Ο σύνδεσμος αντιγράφηκε!",
"private_link_copied": "Ο ιδιωτικός σύνδεσμος αντιγράφηκε!",
"title": "Τίτλος",
"description": "Περιγραφή",
"preview_team": "Προεπισκόπηση ομάδας",
"duration": "Διάρκεια",
"minutes": "Λεπτά",
"one_time_link": "Σύνδεσμος μιας χρήσης",
"first_day_of_week": "Πρώτη ημέρα της εβδομάδας",
"weekly_one": "εβδομάδα",
"weekly_other": "εβδομάδες",
"monthly_one": "μήνας",
"monthly_other": "μήνες",
"yearly_one": "έτος",
"yearly_other": "έτη",
"max": "Μέγιστο",
"create_team": "Δημιουργία ομάδας",
"name": "Όνομα",
"create_new_team": "Δημιουργία νέας ομάδας",
"new_team": "Νέα Ομάδα",
"teams": "Ομάδες",
"team": "Ομάδα",
"enabled": "Ενεργοποιημένο",
"disabled": "Απενεργοποιημένο",
"show_advanced_settings": "Εμφάνιση προχωρημένων ρυθμίσεων",
"event_name_tooltip": "Το όνομα που θα εμφανίζεται στα ημερολόγια",
"label": "Ετικέτα",
"edit": "Επεξεργασία",
"disable_guests": "Απενεργοποίηση επισκεπτών"
}
+10 -3
View File
@@ -64,7 +64,7 @@
"someone_requested_an_event": "Someone has requested to schedule an event on your calendar.",
"someone_requested_password_reset": "Someone has requested a link to change your password.",
"password_reset_instructions": "If you didn't request this, you can safely ignore this email and your password will not be changed.",
"event_awaiting_approval_subject": "Awaiting Approval: {{eventType}} at {{date}}",
"event_awaiting_approval_subject": "Awaiting Approval: {{title}} at {{date}}",
"event_still_awaiting_approval": "An event is still waiting for your approval",
"booking_submitted_subject": "Booking Submitted: {{title}} at {{date}}",
"your_meeting_has_been_booked": "Your meeting has been booked",
@@ -660,6 +660,7 @@
"edit_availability": "Edit availability",
"configure_availability": "Configure times when you are available for bookings.",
"copy_times_to": "Copy times to",
"copy_times_to_tooltip": "Copy times to …",
"change_weekly_schedule": "Change your weekly schedule",
"logo": "Logo",
"error": "Error",
@@ -789,6 +790,7 @@
"number_apps_one": "{{count}} App",
"number_apps_other": "{{count}} Apps",
"trending_apps": "Trending Apps",
"most_popular":"Most Popular",
"explore_apps": "{{category}} apps",
"installed_apps": "Installed Apps",
"free_to_use_apps": "Free",
@@ -799,12 +801,14 @@
"no_category_apps_description_analytics": "Add an analytics app for your booking pages",
"no_category_apps_description_automation": "Add an automation app to use",
"no_category_apps_description_other": "Add any other type of app to do all sorts of things",
"no_category_apps_description_web3": "Add a web3 app for your booking pages",
"installed_app_calendar_description": "Set the calendars to check for conflicts to prevent double bookings.",
"installed_app_conferencing_description": "Add your favourite video conferencing apps for your meetings",
"installed_app_payment_description": "Configure which payment processing services to use when charging your clients.",
"installed_app_analytics_description": "Configure which analytics apps to use for your booking pages",
"installed_app_other_description": "All your installed apps from other categories.",
"installed_app_automation_description": "Configure which automation apps to use",
"installed_app_web3_description": "Configure which web3 apps to use for your booking pages",
"analytics": "Analytics",
"empty_installed_apps_headline": "No apps installed",
"empty_installed_apps_description": "Apps enable you to enhance your workflow and improve your scheduling life significantly.",
@@ -1217,6 +1221,7 @@
"connect_automation_apps": "Connect automation apps",
"connect_analytics_apps": "Connect analytics apps",
"connect_other_apps": "Connect other apps",
"connect_web3_apps": "Connect web3 apps",
"current_step_of_total": "Step {{currentStep}} of {{maxSteps}}",
"add_variable": "Add variable",
"custom_phone_number": "Custom phone number",
@@ -1498,7 +1503,7 @@
"round_robin_hosts": "Round-Robin Hosts",
"minimum_round_robin_hosts_count": "Number of hosts required to attend",
"hosts": "Hosts",
"upgrade_to_enable_feature": "You need to upgrade to enable this feature. Click to upgrade.",
"upgrade_to_enable_feature": "You need to create a team to enable this feature. Click to create a team.",
"new_attendee": "New Attendee",
"awaiting_approval": "Awaiting Approval",
"requires_google_calendar": "This app requires a Google Calendar connection",
@@ -1506,5 +1511,7 @@
"using_meet_requires_calendar": "Using Google Meet requires a connected Google Calendar",
"continue_to_install_google_calendar": "Continue to install Google Calendar",
"install_google_meet": "Install Google Meet",
"install_google_calendar": "Install Google Calendar"
"install_google_calendar": "Install Google Calendar",
"sender_name": "Sender name",
"no_recordings_found": "No recordings found"
}
@@ -68,7 +68,7 @@
"event_still_awaiting_approval": "Un evento aún está esperando su aprobación",
"booking_submitted_subject": "Reserva enviada: {{eventType}} con {{name}} el {{date}}",
"your_meeting_has_been_booked": "Su reunión ha sido reservada",
"event_type_has_been_rescheduled_on_time_date": "Tu {{eventType}} con {{name}} se reprogramó para el {{date}}.",
"event_type_has_been_rescheduled_on_time_date": "Tu {{title}} ha sido reprogramado para {{date}}.",
"event_has_been_rescheduled": "Tu evento ha sido reprogramado.",
"request_reschedule_title_attendee": "Solicitud para reprogramar su reserva",
"request_reschedule_subtitle": "{{organizer}} ha cancelado la reserva y le ha solicitado que elija otra hora.",
@@ -0,0 +1,79 @@
{
"day_one": "egun {{count}}",
"day_other": "{{count}} egun",
"second_one": "segundo {{count}}",
"second_other": "{{count}} segundo",
"accept_invitation": "Onartu gonbidapena",
"have_any_questions": "Galderarik? Laguntzeko gaude.",
"reset_password_subject": "{{appName}}: Pasahitza berrezartzeko argibideak",
"event_declined_subject": "Baztertua: {{title}} {{date}}(e)an",
"event_cancelled_subject": "Bertan behera: {{title}} {{date}}(e)an",
"event_request_declined": "Zure gertaera-eskaera baztertua izan da",
"event_request_declined_recurring": "Zure gertaera errepikari-eskaera baztertua izan da",
"event_request_cancelled": "Zure programatutako gertaera baztertua izan da",
"organizer": "Antolatzailea",
"need_to_reschedule_or_cancel": "Programazioa aldatu edo bertan behera utzi behar duzu?",
"cancellation_reason": "Bertan behera uztearen arrazoia (aukerakoa)",
"cancellation_reason_placeholder": "Zergatik utzi duzu bertan behera?",
"rejection_reason": "Errefusatzeko arrazoia",
"rejection_reason_title": "Errefusatu erreserba-eskaera?",
"rejection_reason_description": "Ziur zaude erreserba errefusatu nahi duzula? Erreserba-eskaera egin duen pertsonari jakinaraziko zaio. Arrazoi bat adieraz dezakezu behean.",
"rejection_confirmation": "Errefusatu erreserba",
"manage_this_event": "Kudeatu gertaera hau",
"your_event_has_been_scheduled": "Zure gertaera programatu da",
"error_message": "Errore-mezua honakoa ian da: '{{errorMessage}}'",
"refund_failed_subject": "Itzulketak huts egin du: {{name}} - {{date}} - {{eventType}}",
"refund_failed": "Huts egin du itzulketak {{eventType}} gertaerarako, {{userName}}(r)ekin {{date}}(e)an.",
"a_refund_failed": "Itzulketa batek huts egin du",
"awaiting_payment_subject": "Ordainketaren zain: {{title}} {{date}}(e)an",
"help": "Laguntza",
"price": "Prezioa",
"paid": "Ordainduta",
"refunded": "Itzulita",
"payment": "Ordainketa",
"pay_now": "Ordaindu orain",
"no_more_results": "Emaitza gehiagorik ez",
"no_results": "Emaitzarik ez",
"load_more_results": "Kargatu emaitza gehiago",
"confirmed_event_type_subject": "Baieztatua: {{eventType}} {{name}}(r)ekin {{date}}(e)an",
"confirm_or_reject_request": "Baieztatu edo errefusatu eskaera",
"check_bookings_page_to_confirm_or_reject": "Begiratu zure erreserba-orrialdea erreserba baieztatu edo errefusatzeko.",
"event_awaiting_approval": "Gertaera bat zure onarpenaren zain dago",
"booking_submitted_subject": "Erreserba bidalita: {{title}} {{date}}(e)an",
"event_type_has_been_rescheduled_on_time_date": "Zure {{title}} getaeraren programazioa aldatu egin da {{date}}(e)ra.",
"hi_user_name": "Kaixo {{name}}",
"ics_event_title": "{{eventType}} {{name}}(r)ekin",
"notes": "Oharrak",
"manage_my_bookings": "Kudeatu nire erreserbak",
"rejected_event_type_with_organizer": "Errefusatua: {{eventType}} {{organizer}}(r)ekin {{date}}(e)an",
"hi": "Kaixo",
"change_bookings_availability": "Aldatu noiz zauden prest erreserbak jasotzeko",
"create_booking_link_with_calcom": "Sor ezazu zeure erreserba-esteka {{appName}}(e)kin",
"user_needs_to_confirm_or_reject_booking": "{{user}}(e)k erreserba baieztatu edo errefusatu behar du oraindik.",
"booking_submitted": "Zure erreserba bidali da",
"booking_confirmed": "Zure erreserba baieztatu da",
"back_to_bookings": "Itzuli erreserbatara",
"really_cancel_booking": "Benetan bertan behera utzi nahi duzu zure erreserba?",
"cannot_cancel_booking": "Ezin duzu erreserba hau bertan behera utzi",
"booking_already_cancelled": "Erreserba hau bertan behera utzi da dagoeneko",
"booking_already_accepted_rejected": "Erreserba hau onartu edo errefusatu da dagoeneko",
"bookings": "Erreserbak",
"past_bookings": "Zure iraganeko erreserbak agertuko dira hemen.",
"cancelled_bookings": "Zure bertan behera utzitako erreserbak agertuko dira hemen.",
"unconfirmed_bookings": "Zure baieztatu gabeko erreserbak agertuko dira hemen.",
"unconfirmed_bookings_tooltip": "Baieztatu gabeko erreserbak",
"booking_cancelled": "Erreserba bertan behera",
"booking_rescheduled": "Erreserbaren programazioa aldatuta",
"booking_created": "Erreserba sortuta",
"edit_booking": "Aldatu erreserba",
"reschedule_booking": "Aldatu erreserbaren programazioa",
"location_changed_event_type_subject": "Kokapena aldatu da: {{eventType}} {{name}}(r)ekin {{date}}(e)an",
"booking_details": "Erreserbaren xehetasunak",
"reminder_email": "Gogorarazpena: {{eventType}} {{name}}(r)ekin {{date}}(e)an",
"new_seat_subject": "{{name}} parte-hartzaile berria {{eventType}}(e)n {{date}}(e)an",
"booking_appearance": "Erreserba-orriaren itxura",
"limit_booking_frequency": "Mugatu erreserbatzeko maiztasuna",
"booking_tentative": "Erreserba hau behin-behinekoa da",
"limit_future_bookings": "Mugatu etorkizuneko erreserbak",
"all_bookings_filter_label": "Erreserba guztiak"
}
File diff suppressed because it is too large Load Diff
@@ -68,7 +68,7 @@
"event_still_awaiting_approval": "Un evento è ancora in attesa della tua approvazione",
"booking_submitted_subject": "Prenotazione inviata: {{eventType}} con {{name}} il {{date}}",
"your_meeting_has_been_booked": "Il tuo meeting è stata prenotato",
"event_type_has_been_rescheduled_on_time_date": "Il tuo {{eventType}} con {{name}} è stato riprogrammato il {{date}}.",
"event_type_has_been_rescheduled_on_time_date": "Il tuo {{title}} è stato riprogrammato per il {{date}}.",
"event_has_been_rescheduled": "Aggiornato: il tuo evento è stato riprogrammato",
"request_reschedule_title_attendee": "Richiesta di riprogrammare la prenotazione",
"request_reschedule_subtitle": "{{organizer}} ha annullato la prenotazione e ti ha chiesto di scegliere un altro orario.",
@@ -68,7 +68,7 @@
"event_still_awaiting_approval": "イベントはまだあなたの承認待ちです",
"booking_submitted_subject": "予約を送信しました: {{date}} の {{name}} との {{eventType}}",
"your_meeting_has_been_booked": "ミーティングが予約されました",
"event_type_has_been_rescheduled_on_time_date": "{{name}} との {{eventType}} は {{date}} にスケジュールが変更されました。",
"event_type_has_been_rescheduled_on_time_date": "あなたの {{title}} は {{date}} に再スケジュールされました。",
"event_has_been_rescheduled": "更新 - イベントのスケジュールが変更されました",
"request_reschedule_title_attendee": "予約のスケジュール変更をリクエスト",
"request_reschedule_subtitle": "{{organizer}} が予約をキャンセルしました。別の時間を選択することをあなたにリクエストしています。",
@@ -68,7 +68,7 @@
"event_still_awaiting_approval": "이벤트가 아직 승인을 기다리고 있습니다.",
"booking_submitted_subject": "예약 제출됨: {{date}}에 {{name}}에 의한 {{eventType}}",
"your_meeting_has_been_booked": "회의가 예약되었습니다.",
"event_type_has_been_rescheduled_on_time_date": "{{name}}의 {{eventType}} 일정이 {{date}}(으)로 변경되었습니다.",
"event_type_has_been_rescheduled_on_time_date": "귀하의 {{title}} 일정이 {{date}}로 변경되었습니다.",
"event_has_been_rescheduled": "업데이트 - 이벤트 일정이 변경되었습니다.",
"request_reschedule_title_attendee": "예약 일정 변경 요청",
"request_reschedule_subtitle": "{{organizer}}에서 예약을 취소했고 다른 시간을 선택할 것을 요청했습니다.",
@@ -68,7 +68,7 @@
"event_still_awaiting_approval": "Een gebeurtenis wacht nog op uw goedkeuring",
"booking_submitted_subject": "Boeking verzonden: {{eventType}} met {{name}} om {{date}}",
"your_meeting_has_been_booked": "Uw afspraak is geboekt",
"event_type_has_been_rescheduled_on_time_date": "Uw {{eventType}} met {{name}} is verplaatst naar {{date}}.",
"event_type_has_been_rescheduled_on_time_date": "Uw {{title}} is verplaatst naar {{date}}.",
"event_has_been_rescheduled": "Bijgewerkt - Uw gebeurtenis is verplaatst",
"request_reschedule_title_attendee": "Vezoek om uw boeking te verplaatsen",
"request_reschedule_subtitle": "{{organizer}} heeft de boeking geannuleerd en u verzocht een andere tijd te kiezen.",
@@ -68,7 +68,7 @@
"event_still_awaiting_approval": "En hendelse venter fortsatt på din godkjenning",
"booking_submitted_subject": "Booking sendt inn: {{eventType}} med {{name}} kl. {{date}}",
"your_meeting_has_been_booked": "Møtet ditt er booket",
"event_type_has_been_rescheduled_on_time_date": "Din {{eventType}} med {{name}} har blitt flyttet til {{date}}.",
"event_type_has_been_rescheduled_on_time_date": "Din {{title}} har blitt flyttet til {{date}}.",
"event_has_been_rescheduled": "Oppdatert – Hendelsen din har blitt endret",
"request_reschedule_title_attendee": "Be om å få endre tidspunkt for bookingen din",
"request_reschedule_subtitle": "{{organizer}} har kansellert bookingen og bedt deg om å velge et annet tidspunkt.",
@@ -68,7 +68,7 @@
"event_still_awaiting_approval": "Wydarzenie nadal oczekuje na Twoje zatwierdzenie",
"booking_submitted_subject": "Rezerwacja potwierdzona: {{eventType}} z {{name}} w {{date}}",
"your_meeting_has_been_booked": "Twoje spotkanie zostało zarezerwowane",
"event_type_has_been_rescheduled_on_time_date": "Wydarzenie {{eventType}} z {{name}} przełożono na {{date}}.",
"event_type_has_been_rescheduled_on_time_date": "Twój {{title}} został przełożony na {{date}}.",
"event_has_been_rescheduled": "Zaktualizowano - Twoje wydarzenie zostało przełożone",
"request_reschedule_title_attendee": "Poproś o przełożenie rezerwacji",
"request_reschedule_subtitle": "{{organizer}} anulował(a) rezerwację i prosi Cię o wybranie innego terminu.",
@@ -68,7 +68,7 @@
"event_still_awaiting_approval": "Um evento está aguardando a sua aprovação",
"booking_submitted_subject": "Reserva enviada: {{eventType}} com {{name}} em {{date}}",
"your_meeting_has_been_booked": "A sua reunião foi agendada",
"event_type_has_been_rescheduled_on_time_date": "O evento {{eventType}} com {{name}} foi reagendado para as {{time}} ({{timeZone}}) de {{date}}.",
"event_type_has_been_rescheduled_on_time_date": "Seu {{title}} foi remarcado para {{date}}.",
"event_has_been_rescheduled": "Atualização - O seu evento foi reagendado",
"request_reschedule_title_attendee": "Solicitar reagendamento da reserva",
"request_reschedule_subtitle": "{{organizer}} cancelou a reserva e solicitou que você escolhesse outro horário.",
@@ -68,7 +68,7 @@
"event_still_awaiting_approval": "Um evento ainda aguarda a sua aprovação",
"booking_submitted_subject": "Reserva submetida: {{eventType}} com {{name}} em {{date}}",
"your_meeting_has_been_booked": "A sua reunião foi reservada",
"event_type_has_been_rescheduled_on_time_date": "O evento {{eventType}} com {{name}} foi reagendado para as {{time}} ({{timeZone}}) de {{date}}.",
"event_type_has_been_rescheduled_on_time_date": "Seu {{title}} foi remarcado para {{date}}.",
"event_has_been_rescheduled": "O seu evento foi reagendado.",
"request_reschedule_title_attendee": "Solicitar o reagendamento da sua reserva",
"request_reschedule_subtitle": "{{organizer}} cancelou a reserva e pediu para que escolhesse outro horário.",
@@ -68,7 +68,7 @@
"event_still_awaiting_approval": "Un eveniment încă așteaptă aprobarea dvs.",
"booking_submitted_subject": "Rezervare transmisă: {{eventType}} cu {{name}} în {{date}}",
"your_meeting_has_been_booked": "Întâlnirea dvs. a fost programată",
"event_type_has_been_rescheduled_on_time_date": "{{eventType}} cu {{name}} a fost reprogramat/ă pe {{date}}.",
"event_type_has_been_rescheduled_on_time_date": "{{title}} dvs. a fost reprogramat la {{date}}.",
"event_has_been_rescheduled": "Evenimentul dvs. a fost reprogramat.",
"request_reschedule_title_attendee": "Solicitați reprogramarea rezervării",
"request_reschedule_subtitle": "{{organizer}} a anulat rezervarea și v-a cerut să alegeți o altă dată.",
@@ -68,7 +68,7 @@
"event_still_awaiting_approval": "Событие все еще ждет вашего подтверждения",
"booking_submitted_subject": "Бронирование отправлено: {{eventType}} с {{name}}, {{date}}",
"your_meeting_has_been_booked": "Ваша встреча была забронирована",
"event_type_has_been_rescheduled_on_time_date": "Перенесено: {{eventType}} с {{name}} на {{date}}.",
"event_type_has_been_rescheduled_on_time_date": "Ваше {{title}} было перенесено на {{date}}.",
"event_has_been_rescheduled": "Ваше событие было перенесено.",
"request_reschedule_title_attendee": "Запросить перенос бронирования",
"request_reschedule_subtitle": "{{organizer}} отменил(-а) бронирование и попросил(-а) вас выбрать другое время.",
@@ -68,7 +68,7 @@
"event_still_awaiting_approval": "Događaj još čeka na vaše odobrenje",
"booking_submitted_subject": "Rezervacija podneta: {{eventType}} sa {{name}} datuma {{date}}",
"your_meeting_has_been_booked": "Sastanak je zakazan",
"event_type_has_been_rescheduled_on_time_date": "Vaš {{eventType}} sa {{name}} je odložen za {{date}}.",
"event_type_has_been_rescheduled_on_time_date": "Ваш {{титле}} је померен за {{дате}}.",
"event_has_been_rescheduled": "Ažurirano - Vaš dogadjaj je odložen",
"request_reschedule_title_attendee": "Zahtev za promenu vremena rezervacije",
"request_reschedule_subtitle": "{{organizer}} je otkazao/la rezervaciju i tražio/la je da odaberete drugo vreme.",
@@ -68,7 +68,7 @@
"event_still_awaiting_approval": "En bokning väntar fortfarande på ditt godkännande",
"booking_submitted_subject": "Bokning skickad: {{eventType}} med {{name}} {{date}}",
"your_meeting_has_been_booked": "Ditt möte har bokats",
"event_type_has_been_rescheduled_on_time_date": "Din {{eventType}} med {{name}} har bokats om till {{date}}.",
"event_type_has_been_rescheduled_on_time_date": "Din {{title}} har ändrats till {{date}}.",
"event_has_been_rescheduled": "Uppdatering - Din bokning har blivit omplanerad",
"request_reschedule_title_attendee": "Begäran om att boka om din bokning",
"request_reschedule_subtitle": "{{organizer}} har avbokat bokningen och bett dig att välja en annan tid.",
@@ -68,7 +68,7 @@
"event_still_awaiting_approval": "Bir etkinlik hâlâ onayınızı bekliyor",
"booking_submitted_subject": "Rezervasyon Gönderildi: {{date}} tarihinde {{name}} ile {{eventType}}",
"your_meeting_has_been_booked": "Toplantı rezervasyonunuz yapıldı",
"event_type_has_been_rescheduled_on_time_date": "{{name}} ile yapacağınız {{eventType}} etkinliğiniz {{date}} olarak yeniden planlandı.",
"event_type_has_been_rescheduled_on_time_date": "{{title}}, {{date}} olarak yeniden planlandı.",
"event_has_been_rescheduled": "Güncellendi - Etkinliğiniz yeniden planlandı",
"request_reschedule_title_attendee": "Rezervasyonunuz için yeniden planlama isteğinde bulunun",
"request_reschedule_subtitle": "{{organizer}} rezervasyonu iptal etti ve sizden başka bir saat seçmenizi istiyor.",
@@ -68,7 +68,7 @@
"event_still_awaiting_approval": "Захід досі чекає на ваше схвалення",
"booking_submitted_subject": "Бронювання надіслано: {{eventType}} «{{name}}», {{date}}",
"your_meeting_has_been_booked": "Вашу нараду заброньовано",
"event_type_has_been_rescheduled_on_time_date": "{{eventType}} «{{name}}» перенесено. Новий час: {{time}} ({{timeZone}}), {{date}}.",
"event_type_has_been_rescheduled_on_time_date": "Ваш {{title}} перенесено на {{date}}.",
"event_has_been_rescheduled": "Оновлено – ваш захід перенесено",
"request_reschedule_title_attendee": "Надіслати запит на перенесення бронювання",
"request_reschedule_subtitle": "{{organizer}} скасував(-ла) бронювання та попросив(-ла) вас вибрати інший час.",
@@ -68,7 +68,7 @@
"event_still_awaiting_approval": "Một sự kiện vẫn đang chờ bạn phê duyệt",
"booking_submitted_subject": "Đặt chỗ đã được gửi: {{eventType}} với {{name}} lúc {{date}}",
"your_meeting_has_been_booked": "Cuộc họp của bạn đã được đặt",
"event_type_has_been_rescheduled_on_time_date": "{{eventType}} của bạn với {{name}} đã được chuyển sang ngày {{date}}.",
"event_type_has_been_rescheduled_on_time_date": "{{title}} của bạn đã được lên lịch lại vào {{date}}.",
"event_has_been_rescheduled": "Đã cập nhật - Sự kiện của bạn đã được đổi",
"request_reschedule_title_attendee": "Yêu cầu xếp lại lịch đặt chỗ của bạn",
"request_reschedule_subtitle": "{{organizer}} đã huỷ đặt chỗ đó và yêu cầu bạn chọn thời gian khác.",
@@ -68,7 +68,7 @@
"event_still_awaiting_approval": "有活动仍在等待您的批准",
"booking_submitted_subject": "预约已提交: 名称为 {{name}} 的 {{eventType}},日期为 {{date}}",
"your_meeting_has_been_booked": "您的会议已预定",
"event_type_has_been_rescheduled_on_time_date": "您与 {{name}} 的 {{eventType}} 已被重新安排到 {{date}} {{time}} ({{timeZone}})。",
"event_type_has_been_rescheduled_on_time_date": "您的 {{title}} 已改期至 {{date}}。",
"event_has_been_rescheduled": "已更新 - 您的活动已被重新安排",
"request_reschedule_title_attendee": "请求重新安排您的预约",
"request_reschedule_subtitle": "{{organizer}} 取消了预约并要求您选择其他时间。",
@@ -68,7 +68,7 @@
"event_still_awaiting_approval": "活動還在等待核准",
"booking_submitted_subject": "已提出預約:{{date}} 與 {{name}} 的 {{eventType}}",
"your_meeting_has_been_booked": "會議已經預定",
"event_type_has_been_rescheduled_on_time_date": "您和 {{name}} 的 {{eventType}} 的預定時間已改為 {{date}}。",
"event_type_has_been_rescheduled_on_time_date": "您的 {{title}} 已改期至 {{date}}。",
"event_has_been_rescheduled": "已更新 - 已經重新預定活動時間",
"request_reschedule_title_attendee": "要求重新安排您的預約",
"request_reschedule_subtitle": "{{organizer}} 已取消預約,並要求您選擇其他時間。",
+8 -8
View File
@@ -183,19 +183,19 @@ button[role="switch"][data-state="checked"] {
}
.slider > .slider-track {
@apply relative h-1 flex-grow rounded-md bg-neutral-400;
@apply relative h-1 flex-grow rounded-md bg-gray-400;
}
.slider .slider-range {
@apply absolute h-full rounded-full bg-neutral-700;
@apply absolute h-full rounded-full bg-gray-700;
}
.slider .slider-thumb {
@apply block h-3 w-3 cursor-pointer rounded-full bg-neutral-700 transition-all;
@apply block h-3 w-3 cursor-pointer rounded-full bg-gray-700 transition-all;
}
.slider .slider-thumb:hover {
@apply bg-neutral-600;
@apply bg-gray-600;
}
.slider .slider-thumb:focus {
@@ -237,12 +237,12 @@ button[role="switch"][data-state="checked"] {
}
.react-multi-email > [type="text"] {
@apply dark:placeholder:text-darkgray-600 focus:border-brand dark:border-darkgray-300 dark:text-darkgray-900 block w-full rounded-md border-gray-300 text-sm focus:ring-black disabled:bg-gray-200 disabled:hover:cursor-not-allowed dark:bg-transparent dark:selection:bg-green-500 disabled:dark:text-gray-500 dark:text-white;
@apply dark:placeholder:text-darkgray-600 focus:border-brand dark:border-darkgray-300 dark:text-white block w-full rounded-md border-gray-300 text-sm focus:ring-black disabled:bg-gray-200 disabled:hover:cursor-not-allowed dark:bg-transparent dark:selection:bg-green-500 disabled:dark:text-gray-500;
}
.react-multi-email [data-tag] {
box-shadow: none !important;
@apply dark:bg-brand my-1 inline-flex items-center rounded-md border border-transparent bg-neutral-200 px-2 py-1 text-sm font-medium text-gray-900 hover:bg-neutral-100 focus:outline-none focus:ring-2 focus:ring-neutral-500 focus:ring-offset-2 ltr:mr-2 rtl:ml-2 dark:text-white;
@apply dark:bg-brand my-1 inline-flex items-center rounded-md border border-transparent bg-gray-200 px-2 py-1 text-sm font-medium text-gray-900 hover:bg-gray-100 focus:outline-none focus:ring-2 focus:ring-gray-500 focus:ring-offset-2 ltr:mr-2 rtl:ml-2 dark:text-white;
}
.react-multi-email > span[data-placeholder] {
@@ -270,7 +270,7 @@ button[role="switch"][data-state="checked"] {
}
.react-multi-email [data-tag] {
@apply my-1 inline-flex items-center rounded-md border border-transparent bg-neutral-200 px-2 py-1 text-sm font-medium text-gray-900 hover:bg-neutral-100 focus:outline-none focus:ring-2 focus:ring-neutral-500 focus:ring-offset-2 ltr:mr-2 rtl:ml-2 dark:bg-neutral-900 dark:text-white dark:hover:bg-neutral-700;
@apply my-1 inline-flex items-center rounded-md border border-transparent bg-gray-200 px-2 py-1 text-sm font-medium text-gray-900 hover:bg-gray-100 focus:outline-none focus:ring-2 focus:ring-gray-500 focus:ring-offset-2 ltr:mr-2 rtl:ml-2 dark:bg-gray-900 dark:text-white dark:hover:bg-gray-700;
}
.react-multi-email [data-tag] [data-tag-item] {
@@ -383,7 +383,7 @@ nav#nav--settings > a svg {
}
nav#nav--settings > a.active {
@apply border-neutral-500 bg-neutral-50 text-gray-700 hover:bg-neutral-50 hover:text-gray-700;
@apply border-gray-500 bg-gray-50 text-gray-700 hover:bg-gray-50 hover:text-gray-700;
}
nav#nav--settings > a.active svg {
Submodule apps/website deleted from 8f5d66f2cc
Executable
+8
View File
@@ -0,0 +1,8 @@
#!/bin/sh
# If there's a `.gitmodule` file skip this script
[ -f .gitmodules ] && {
echo ".gitmodules already initializied"
exit 0
}
./git-setup.sh api website console
Executable
+35
View File
@@ -0,0 +1,35 @@
#!/bin/sh
# If no project name is given
if [ $# -eq 0 ]; then
# Display usage and stop
echo "Usage: git-setup.sh <api,console,website>"
exit 1
fi
# Loop through the requested modules
for module in "$@"; do
echo "Setting up '$module' module..."
# Set the project git URL
project=$(echo "git@github.com:calcom/$module.git")
# Check if we have access to the module
if [ "$(git ls-remote "$project" 2>/dev/null)" ]; then
echo "You have access to '${module}'"
# Create the .gitmodules file if it doesn't exist
([ -e ".gitmodules" ] || touch ".gitmodules") && [ ! -w ".gitmodules" ] && echo cannot write to .gitmodules && exit 1
# Prevents duplicate entries
git config -f .gitmodules --unset-all "submodule.apps/$module.branch"
# Add the submodule
git submodule add --force "git@github.com:calcom/$module.git" "apps/$module"
# Set the default branch to main
git config -f .gitmodules --add "submodule.apps/$module.branch" main
# Adding the subdmoule ignores the `.gitignore` so a reset is needed
git reset
else
# If the module is the API, display a link to request access
if [ "$module" == "api" ]; then
echo "You don't have access to: '${module}' module. You can request access in: https://console.cal.com"
else
# If the module is not the API, display normal message
echo "You don't have access to: '${module}' module."
fi
fi
done
+7
View File
@@ -15,6 +15,12 @@
"app-store:build": "yarn app-store-cli build",
"app-store:watch": "yarn app-store-cli watch",
"app-store": "yarn app-store-cli cli",
"create-app": "yarn app-store create",
"edit-app": "yarn app-store edit",
"delete-app": "yarn app-store delete",
"create-app-template": "yarn app-store create-template",
"edit-app-template": "yarn app-store edit-template",
"delete-app-template": "yarn app-store delete-template",
"build": "turbo run build --filter=@calcom/web...",
"clean": "find . -name node_modules -o -name .next -o -name .turbo -o -name dist -type d -prune | xargs rm -rf",
"db-deploy": "turbo run db-deploy",
@@ -44,6 +50,7 @@
"lint": "turbo run lint",
"postinstall": "turbo run post-install",
"pre-commit": "lint-staged",
"preinstall": "./git-init.sh",
"predev": "echo 'Checking env files'",
"prepare": "husky install",
"prisma": "yarn workspace @calcom/prisma prisma",
+9
View File
@@ -0,0 +1,9 @@
## How to build an App using the CLI
Refer to https://developer.cal.com/guides/how-to-build-an-app
## TODO
- Merge app-store:watch and app-store commands; introduce app-store --watch
- An app created through CLI should be able to completely skip API validation for testing purposes. Credentials should be created with no API specified specific to the app. It would allow us to test any app end to end not worrying about the corresponding API endpoint.
- Someone can add wrong directory name(which doesn't satisfy slug requirements) manually. How to handle it.
- Allow editing and updating app from the cal app itself - including assets uploading when developing locally.
- Use AppDeclarativeHandler across all apps. Whatever isn't supported in it, support that.
+3 -3
View File
@@ -7,10 +7,10 @@
"node": ">=10"
},
"scripts": {
"build": "ts-node --transpile-only src/app-store.ts",
"build": "ts-node --transpile-only src/build.ts",
"cli": "ts-node --transpile-only src/cli.tsx",
"watch": "ts-node --transpile-only src/app-store.ts --watch",
"generate": "ts-node --transpile-only src/app-store.ts",
"watch": "ts-node --transpile-only src/build.ts --watch",
"generate": "ts-node --transpile-only src/build.ts",
"post-install": "yarn build"
},
"files": [
-37
View File
@@ -1,37 +0,0 @@
## Steps to create an app
- Create a folder in packages/app-store/{APP_NAME} = {APP}
- Fill it with a sample app
- Modify {APP}/\_metadata.ts with the data provided
## Approach
- appType is derived from App Name(a slugify operation that makes a string that can be used as a director name, a variable name for imports and a URL path).
- appType is then used to create the app directory. It becomes `config.type` of config.json. config.type is the value used to create an entry in App table and retrieve any apps or credentials. It also becomes App.dirName
- dirnames that don't start with \_ are considered apps in packages/app-store and based on those apps .generated.ts\* files are created. This allows pre-cli apps to keep on working.
- app directory is populated with app-store/\_baseApp with newly updated config.json and package.json
- `packages/prisma/seed-app-store.config.json` is updated with new app.
NOTE: After app-store-cli is live, Credential.appId and Credential.type would be same for new apps. For old apps they would remain different. Credential.type would be used to identify credentials in integrations call and Credential.appId/App.slug would be used to identify apps.
If we rename all existing apps to their slug names, we can remove type and then use just appId to refer to things everywhere. This can be done later on.
## TODO
- Improvements
- Edit command Improvements
- Prefill fields in edit command -> It allows only that content to change which user wants to change.
- Don't override icon.svg
- Merge app-store:watch and app-store commands; introduce app-store --watch
- Allow inputs in non interactive way as well - That would allow easily copy pasting commands.
- An app created through CLI should be able to completely skip API validation for testing purposes. Credentials should be created with no API specified specific to the app. It would allow us to test any app end to end not worrying about the corresponding API endpoint.
- Require assets path relative to app dir.
## Roadmap
- Avoid delete and edit on apps created outside of cli
- Someone can add wrong directory name(which doesn't satisfy slug requirements) manually. How to handle it.
- Allow editing and updating app from the cal app itself - including assets uploading when developing locally.
- Improvements in shared code across app
- Use baseApp/api/add.ts for all apps with configuration of credentials creation and redirection URL.
- Delete creation side effects if App creation fails - Might make debugging difficult
- This is so that web app doesn't break because of additional app folders or faulty db-seed
+49
View File
@@ -0,0 +1,49 @@
import React, { FC } from "react";
import { SupportedCommands } from "src/types";
import Create from "./commandViews/Create";
import CreateTemplate from "./commandViews/Create";
import Delete from "./commandViews/Delete";
import DeleteTemplate from "./commandViews/DeleteTemplate";
import Edit from "./commandViews/Edit";
import EditTemplate from "./commandViews/EditTemplate";
export const App: FC<{
template: string;
command: SupportedCommands;
slug?: string;
}> = ({ command, template, slug }) => {
if (command === "create") {
return <Create template={template} />;
}
if (command === "edit") {
return <Edit slug={slug} />;
}
if (command === "edit-template") {
return <EditTemplate slug={slug} />;
}
if (command === "delete") {
if (!slug) {
throw new Error('Slug is required for "delete" command');
}
return <Delete slug={slug} />;
}
if (command === "create-template") {
return <CreateTemplate template={template} />;
}
if (command === "delete-template") {
if (!slug) {
throw new Error('Slug is required for "delete-template" command');
}
return <DeleteTemplate slug={slug} />;
}
return null;
};
export default App;
-463
View File
@@ -1,463 +0,0 @@
import fs from "fs";
import { Box, Text } from "ink";
import SelectInput from "ink-select-input";
import TextInput from "ink-text-input";
import path from "path";
import React, { FC, useEffect, useState } from "react";
import execSync from "./execSync";
const slugify = (str: string) => {
// It is to be a valid dir name, a valid JS variable name and a valid URL path
return str.replace(/[^a-zA-Z0-9]/g, "_").toLowerCase();
};
function getSlugFromAppName(appName: string | null): string | null {
if (!appName) {
return appName;
}
return slugify(appName);
}
function getAppDirPath(slug: any) {
return path.join(appStoreDir, `${slug}`);
}
const appStoreDir = path.resolve(__dirname, "..", "..", "app-store");
const workspaceDir = path.resolve(__dirname, "..", "..", "..");
function absolutePath(appRelativePath) {
return path.join(appStoreDir, appRelativePath);
}
const updatePackageJson = ({ slug, appDescription, appDirPath }) => {
const packageJsonConfig = JSON.parse(fs.readFileSync(`${appDirPath}/package.json`).toString());
packageJsonConfig.name = `@calcom/${slug}`;
packageJsonConfig.description = appDescription;
// packageJsonConfig.description = `@calcom/${appName}`;
fs.writeFileSync(`${appDirPath}/package.json`, JSON.stringify(packageJsonConfig, null, 2));
};
const BaseAppFork = {
create: function* ({
category,
subCategory,
editMode = false,
appDescription,
appName,
slug,
publisherName,
publisherEmail,
extendsFeature,
}) {
const appDirPath = getAppDirPath(slug);
let message = !editMode ? "Forking base app" : "Updating app";
yield message;
if (!editMode) {
execSync(`mkdir -p ${appDirPath}`);
execSync(`cp -r ${absolutePath("_baseApp/*")} ${appDirPath}`);
}
updatePackageJson({ slug, appDirPath, appDescription });
const categoryToVariantMap = {
video: "conferencing",
};
const dataFromCategory =
category === "video"
? {
appData: {
location: {
type: `integrations:${slug}_video`,
label: `${appName}`,
},
},
}
: {};
const dataFromSubCategory =
category === "video" && subCategory === "static"
? {
appData: {
...dataFromCategory.appData,
location: {
...dataFromCategory.appData.location,
linkType: "static",
organizerInputPlaceholder: "https://anything.anything",
urlRegExp: "",
},
},
}
: {};
let config = {
"/*": "Don't modify slug - If required, do it using cli edit command",
name: appName,
// Plan to remove it. DB already has it and name of dir is also the same.
slug: slug,
type: `${slug}_${category}`,
imageSrc: `/api/app-store/${slug}/icon.svg`,
logo: `/api/app-store/${slug}/icon.svg`,
url: `https://cal.com/apps/${slug}`,
variant: categoryToVariantMap[category] || category,
categories: [category],
publisher: publisherName,
email: publisherEmail,
description: appDescription,
extendsFeature: extendsFeature,
// TODO: Use this to avoid edit and delete on the apps created outside of cli
__createdUsingCli: true,
...dataFromCategory,
...dataFromSubCategory,
};
const currentConfig = JSON.parse(fs.readFileSync(`${appDirPath}/config.json`).toString());
config = {
...currentConfig,
...config,
};
fs.writeFileSync(`${appDirPath}/config.json`, JSON.stringify(config, null, 2));
fs.writeFileSync(
`${appDirPath}/DESCRIPTION.md`,
fs
.readFileSync(`${appDirPath}/DESCRIPTION.md`)
.toString()
.replace(/_DESCRIPTION_/g, appDescription)
.replace(/_APP_DIR_/g, slug)
);
message = !editMode ? "Forked base app" : "Updated app";
yield message;
},
delete: function ({ slug }) {
const appDirPath = getAppDirPath(slug);
execSync(`rm -rf ${appDirPath}`);
},
};
const Seed = {
seedConfigPath: absolutePath("../prisma/seed-app-store.config.json"),
update: function ({ slug, category, noDbUpdate }) {
let configContent = "[]";
try {
if (fs.statSync(this.seedConfigPath)) {
configContent = fs.readFileSync(this.seedConfigPath).toString();
}
} catch (e) {}
const seedConfig = JSON.parse(configContent);
if (!seedConfig.find((app) => app.slug === slug)) {
seedConfig.push({
dirName: slug,
categories: [category],
slug: slug,
type: `${slug}_${category}`,
});
}
// Add the message as a property to first item so that it stays always at the top
seedConfig[0]["/*"] =
"This file is auto-generated and updated by `yarn app-store create/edit`. Don't edit it manually";
// Add the message as a property to first item so that it stays always at the top
seedConfig[0]["/*"] =
"This file is auto-generated and updated by `yarn app-store create/edit`. Don't edit it manually";
fs.writeFileSync(this.seedConfigPath, JSON.stringify(seedConfig, null, 2));
if (!noDbUpdate) {
execSync(`cd ${workspaceDir} && yarn db-seed`);
}
},
revert: async function ({ slug, noDbUpdate }) {
let seedConfig = JSON.parse(fs.readFileSync(this.seedConfigPath).toString());
seedConfig = seedConfig.filter((app) => app.slug !== slug);
fs.writeFileSync(this.seedConfigPath, JSON.stringify(seedConfig, null, 2));
if (!noDbUpdate) {
execSync(`yarn workspace @calcom/prisma delete-app ${slug}`);
}
},
};
const generateAppFiles = () => {
execSync(`cd ${__dirname} && yarn ts-node --transpile-only src/app-store.ts`);
};
const CreateApp = ({ noDbUpdate, slug = null, editMode = false }) => {
// AppName
// Type of App - Other, Calendar, Video, Payment, Messaging, Web3
const [appInputData, setAppInputData] = useState({});
const [inputIndex, setInputIndex] = useState(0);
const fields = [
{ label: "App Title", name: "appName", type: "text", explainer: "Keep it very short" },
{
label: "App Description",
name: "appDescription",
type: "text",
explainer:
"A detailed description of your app. You can later modify DESCRIPTION.md to add slider and other components",
},
{
label: "Category of App",
name: "appCategory",
type: "select",
options: [
{ label: "Calendar", value: "calendar" },
{
label:
"Static Link - Video(Apps like Ping.gg/Riverside/Whereby which require you to provide a link to join your room)",
value: "video_static",
},
{ label: "Other - Video", value: "video_other" },
{ label: "Payment", value: "payment" },
{ label: "Messaging", value: "messaging" },
{ label: "Web3", value: "web3" },
{ label: "Automation", value: "automation" },
{ label: "Analytics", value: "analytics" },
{ label: "Other", value: "other" },
],
explainer: "This is how apps are categorized in App Store.",
},
{
label: "What kind of app would you consider it?",
name: "extendsFeature",
options: [
{ label: "User", value: "User" },
{
label: "Event Type(Available for configuration in Apps tab for all Event Types)",
value: "EventType",
},
],
},
{ label: "Publisher Name", name: "publisherName", type: "text", explainer: "Let users know who you are" },
{
label: "Publisher Email",
name: "publisherEmail",
type: "text",
explainer: "Let users know how they can contact you.",
},
];
const field = fields[inputIndex];
const fieldLabel = field?.label || "";
const fieldName = field?.name || "";
const fieldValue = appInputData[fieldName] || "";
const appName = appInputData["appName"];
const rawCategory = appInputData["appCategory"] || "";
const appDescription = appInputData["appDescription"];
const publisherName = appInputData["publisherName"];
const publisherEmail = appInputData["publisherEmail"];
let extendsFeature = appInputData["extendsFeature"] || [];
if (rawCategory === "analytics") {
// Analytics only means EventType Analytics as of now
extendsFeature = "EventType";
}
const [status, setStatus] = useState<"inProgress" | "done">("inProgress");
const allFieldsFilled = inputIndex === fields.length;
const [progressUpdate, setProgressUpdate] = useState("");
const category = rawCategory.split("_")[0];
const subCategory = rawCategory.split("_")[1];
if (!editMode) {
slug = getSlugFromAppName(appName);
}
useEffect(() => {
// When all fields have been filled
if (allFieldsFilled) {
const it = BaseAppFork.create({
category,
subCategory,
appDescription,
appName,
slug,
publisherName,
publisherEmail,
extendsFeature,
});
for (const item of it) {
setProgressUpdate(item);
}
Seed.update({ slug, category, noDbUpdate });
generateAppFiles();
// FIXME: Even after CLI showing this message, it is stuck doing work before exiting
// So we ask the user to wait for some time
setStatus("done");
}
});
if (!slug && editMode) {
return <Text>--slug is required</Text>;
}
if (allFieldsFilled) {
return (
<Box flexDirection="column">
<Text>
{editMode
? `Editing app with slug ${slug}`
: `Creating app with name '${appName}' of type '${category}'`}
</Text>
<Text>{progressUpdate}</Text>
{status === "done" ? (
<Box flexDirection="column" paddingTop={2} paddingBottom={2}>
<Text bold italic>
Just wait for a few seconds for process to exit and then you are good to go. Your App code
exists at ${getAppDirPath(slug)}
Tip : Go and change the logo of your app by replacing {getAppDirPath(slug) + "/static/icon.svg"}
</Text>
<Text bold italic>
App Summary:
</Text>
<Box flexDirection="column">
<Box flexDirection="row">
<Text color="green">Slug: </Text>
<Text>{slug}</Text>
</Box>
<Box flexDirection="row">
<Text color="green">App URL: </Text>
<Text>{`http://localhost:3000/apps/${slug}`}</Text>
</Box>
<Box flexDirection="row">
<Text color="green">Name: </Text>
<Text>{appName}</Text>
</Box>
<Box flexDirection="row">
<Text color="green">Description: </Text>
<Text>{appDescription}</Text>
</Box>
<Box flexDirection="row">
<Text color="green">Category: </Text>
<Text>{category}</Text>
</Box>
<Box flexDirection="row">
<Text color="green">Publisher Name: </Text>
<Text>{publisherName}</Text>
</Box>
<Box flexDirection="row">
<Text color="green">Publisher Email: </Text>
<Text>{publisherEmail}</Text>
</Box>
</Box>
</Box>
) : (
<Text>Please wait...</Text>
)}
<Text italic color="gray">
Note: You should not rename app directory manually. Use cli only to do that as it needs to be
updated in DB as well
</Text>
</Box>
);
}
// Hack: using field.name == "appTitle" to identify that app Name has been submitted and not being edited.
if (!editMode && field.name === "appTitle" && slug && fs.existsSync(getAppDirPath(slug))) {
return (
<>
<Text>App with slug {slug} already exists. If you want to edit it, use edit command</Text>
</>
);
}
return (
<Box flexDirection="column">
<Box flexDirection="column">
<Box>
<Text color="green">{`${fieldLabel}:`}</Text>
{field.type == "text" ? (
<TextInput
value={fieldValue}
onSubmit={(value) => {
if (!value) {
return;
}
setInputIndex((index) => {
return index + 1;
});
}}
onChange={(value) => {
setAppInputData((appInputData) => {
return {
...appInputData,
[fieldName]: value,
};
});
}}
/>
) : (
<SelectInput<string>
items={field.options}
onSelect={(item) => {
setAppInputData((appInputData) => {
return {
...appInputData,
[fieldName]: item.value,
};
});
setInputIndex((index) => {
return index + 1;
});
}}
/>
)}
</Box>
<Box>
<Text color="gray" italic>
{field.explainer}
</Text>
</Box>
</Box>
</Box>
);
};
const DeleteApp = ({ noDbUpdate, slug }) => {
const [confirmedAppSlug, setConfirmedAppSlug] = useState("");
const [allowDeletion, setAllowDeletion] = useState(false);
const [state, setState] = useState({ done: null, description: null });
useEffect(() => {
if (allowDeletion) {
BaseAppFork.delete({ slug });
Seed.revert({ slug });
generateAppFiles();
setState({ description: `App with slug ${slug} has been deleted`, done: true });
}
}, [allowDeletion, slug]);
return (
<>
<Text>
Confirm the slug of the app that you want to delete. Note, that it would cleanup the app directory,
App table and Credential table
</Text>
{!state.done && (
<TextInput
value={confirmedAppSlug}
onSubmit={(value) => {
if (value === slug) {
setState({ description: `Deletion started`, done: true });
setAllowDeletion(true);
} else {
setState({ description: `Slug doesn't match - Should have been ${slug}`, done: true });
}
}}
onChange={(val) => {
setConfirmedAppSlug(val);
}}
/>
)}
<Text>{state.description}</Text>
</>
);
};
const App: FC<{ noDbUpdate?: boolean; command: "create" | "delete"; slug?: string }> = ({
command,
noDbUpdate,
slug,
}) => {
if (command === "create") {
return <CreateApp noDbUpdate={noDbUpdate} />;
}
if (command === "delete") {
return <DeleteApp slug={slug} noDbUpdate={noDbUpdate} />;
}
if (command === "edit") {
return <CreateApp slug={slug} editMode={true} noDbUpdate={noDbUpdate} />;
}
};
module.exports = App;
export default App;
-278
View File
@@ -1,278 +0,0 @@
import chokidar from "chokidar";
import fs from "fs";
import { debounce } from "lodash";
import path from "path";
import prettier from "prettier";
import { AppMeta } from "@calcom/types/App";
import prettierConfig from "../../config/prettier-preset";
import execSync from "./execSync";
function isFileThere(path) {
try {
fs.statSync(path);
return true;
} catch (e) {
return false;
}
}
let isInWatchMode = false;
if (process.argv[2] === "--watch") {
isInWatchMode = true;
}
const formatOutput = (source: string) => prettier.format(source, prettierConfig);
const getVariableName = function (appName: string) {
return appName.replace("-", "_");
};
const getAppId = function (app: { name: string }) {
// Handle stripe separately as it's an old app with different dirName than slug/appId
return app.name === "stripepayment" ? "stripe" : app.name;
};
const APP_STORE_PATH = path.join(__dirname, "..", "..", "app-store");
type App = Partial<AppMeta> & {
name: string;
path: string;
};
function getAppName(candidatePath) {
function isValidAppName(candidatePath) {
if (
!candidatePath.startsWith("_") &&
candidatePath !== "ee" &&
!candidatePath.includes("/") &&
!candidatePath.includes("\\")
) {
return candidatePath;
}
}
if (isValidAppName(candidatePath)) {
// Already a dirname of an app
return candidatePath;
}
// Get dirname of app from full path
const dirName = path.relative(APP_STORE_PATH, candidatePath);
return isValidAppName(dirName) ? dirName : null;
}
function generateFiles() {
const browserOutput = [`import dynamic from "next/dynamic"`];
const metadataOutput = [];
const schemasOutput = [];
const appKeysSchemasOutput = [];
const serverOutput = [];
const appDirs: { name: string; path: string }[] = [];
fs.readdirSync(`${APP_STORE_PATH}`).forEach(function (dir) {
if (dir === "ee") {
fs.readdirSync(path.join(APP_STORE_PATH, dir)).forEach(function (eeDir) {
if (fs.statSync(path.join(APP_STORE_PATH, dir, eeDir)).isDirectory()) {
if (!getAppName(path.resolve(eeDir))) {
appDirs.push({
name: eeDir,
path: path.join(dir, eeDir),
});
}
}
});
} else {
if (fs.statSync(path.join(APP_STORE_PATH, dir)).isDirectory()) {
if (!getAppName(dir)) {
return;
}
appDirs.push({
name: dir,
path: dir,
});
}
}
});
function forEachAppDir(callback: (arg: App) => void) {
for (let i = 0; i < appDirs.length; i++) {
const configPath = path.join(APP_STORE_PATH, appDirs[i].path, "config.json");
let app;
if (fs.existsSync(configPath)) {
app = JSON.parse(fs.readFileSync(configPath).toString());
} else {
app = {};
}
callback({
...app,
name: appDirs[i].name,
path: appDirs[i].path,
});
}
}
forEachAppDir((app) => {
const templateDestinationDir = path.join(APP_STORE_PATH, app.path, "extensions");
const templateDestinationFilePath = path.join(templateDestinationDir, "EventTypeAppCard.tsx");
const zodDestinationFilePath = path.join(APP_STORE_PATH, app.path, "zod.ts");
if (app.extendsFeature === "EventType" && !isFileThere(templateDestinationFilePath)) {
execSync(`mkdir -p ${templateDestinationDir}`);
execSync(`cp ../app-store/_templates/extensions/EventTypeAppCard.tsx ${templateDestinationFilePath}`);
execSync(`cp ../app-store/_templates/zod.ts ${zodDestinationFilePath}`);
}
});
function getObjectExporter(
objectName,
{
fileToBeImported,
importBuilder,
entryBuilder,
}: {
fileToBeImported: string;
importBuilder?: (arg: App) => string;
entryBuilder: (arg: App) => string;
}
) {
const output = [];
forEachAppDir((app) => {
if (
fs.existsSync(path.join(APP_STORE_PATH, app.path, fileToBeImported)) &&
typeof importBuilder === "function"
) {
output.push(importBuilder(app));
}
});
output.push(`export const ${objectName} = {`);
forEachAppDir((app) => {
if (fs.existsSync(path.join(APP_STORE_PATH, app.path, fileToBeImported))) {
output.push(entryBuilder(app));
}
});
output.push(`};`);
return output;
}
serverOutput.push(
...getObjectExporter("apiHandlers", {
fileToBeImported: "api/index.ts",
// Import path must have / even for windows and not \
entryBuilder: (app) => ` "${app.name}": import("./${app.path.replace(/\\/g, "/")}/api"),`,
})
);
metadataOutput.push(
...getObjectExporter("appStoreMetadata", {
fileToBeImported: "_metadata.ts",
// Import path must have / even for windows and not \
importBuilder: (app) =>
`import { metadata as ${getVariableName(app.name)}_meta } from "./${app.path.replace(
/\\/g,
"/"
)}/_metadata";`,
entryBuilder: (app) => ` "${app.name}":${getVariableName(app.name)}_meta,`,
})
);
schemasOutput.push(
...getObjectExporter("appDataSchemas", {
fileToBeImported: "zod.ts",
// Import path must have / even for windows and not \
importBuilder: (app) =>
`import { appDataSchema as ${getVariableName(app.name)}_schema } from "./${app.path.replace(
/\\/g,
"/"
)}/zod";`,
// Key must be appId as this is used by eventType metadata and lookup is by appId
entryBuilder: (app) => ` "${getAppId(app)}":${getVariableName(app.name)}_schema ,`,
})
);
appKeysSchemasOutput.push(
...getObjectExporter("appKeysSchemas", {
fileToBeImported: "zod.ts",
// Import path must have / even for windows and not \
importBuilder: (app) =>
`import { appKeysSchema as ${getVariableName(app.name)}_keys_schema } from "./${app.path.replace(
/\\/g,
"/"
)}/zod";`,
// Key must be appId as this is used by eventType metadata and lookup is by appId
entryBuilder: (app) => ` "${getAppId(app)}":${getVariableName(app.name)}_keys_schema ,`,
})
);
browserOutput.push(
...getObjectExporter("InstallAppButtonMap", {
fileToBeImported: "components/InstallAppButton.tsx",
entryBuilder: (app) =>
` ${app.name}: dynamic(() =>import("./${app.path}/components/InstallAppButton")),`,
})
);
// TODO: Make a component map creator that accepts ComponentName and does the rest.
// TODO: dailyvideo has a slug of daily-video, so that mapping needs to be taken care of. But it is an old app, so it doesn't need AppSettings
browserOutput.push(
...getObjectExporter("AppSettingsComponentsMap", {
fileToBeImported: "components/AppSettings.tsx",
entryBuilder: (app) => ` ${app.name}: dynamic(() =>import("./${app.path}/components/AppSettings")),`,
})
);
browserOutput.push(
...getObjectExporter("EventTypeAddonMap", {
fileToBeImported: path.join("extensions", "EventTypeAppCard.tsx"),
entryBuilder: (app) =>
` ${app.name}: dynamic(() =>import("./${app.path}/extensions/EventTypeAppCard")),`,
})
);
const banner = `/**
This file is autogenerated using the command \`yarn app-store:build --watch\`.
Don't modify this file manually.
**/
`;
const filesToGenerate: [string, string[]][] = [
["apps.metadata.generated.ts", metadataOutput],
["apps.server.generated.ts", serverOutput],
["apps.browser.generated.tsx", browserOutput],
["apps.schemas.generated.ts", schemasOutput],
["apps.keys-schemas.generated.ts", appKeysSchemasOutput],
];
filesToGenerate.forEach(([fileName, output]) => {
fs.writeFileSync(`${APP_STORE_PATH}/${fileName}`, formatOutput(`${banner}${output.join("\n")}`));
});
console.log(`Generated ${filesToGenerate.map(([fileName]) => fileName).join(", ")}`);
}
const debouncedGenerateFiles = debounce(generateFiles);
if (isInWatchMode) {
chokidar
.watch(APP_STORE_PATH)
.on("addDir", (dirPath) => {
const appName = getAppName(dirPath);
if (appName) {
console.log(`Added ${appName}`);
debouncedGenerateFiles();
}
})
.on("change", (filePath) => {
if (filePath.endsWith("config.json")) {
console.log("Config file changed");
debouncedGenerateFiles();
}
})
.on("unlinkDir", (dirPath) => {
const appName = getAppName(dirPath);
if (appName) {
console.log(`Removed ${appName}`);
debouncedGenerateFiles();
}
});
} else {
generateFiles();
}
+348
View File
@@ -0,0 +1,348 @@
import chokidar from "chokidar";
import fs from "fs";
import { debounce } from "lodash";
import path from "path";
import prettier from "prettier";
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
//@ts-ignore
import prettierConfig from "@calcom/config/prettier-preset";
import { AppMeta } from "@calcom/types/App";
import { APP_STORE_PATH } from "./constants";
import { getAppName } from "./utils/getAppName";
let isInWatchMode = false;
if (process.argv[2] === "--watch") {
isInWatchMode = true;
}
const formatOutput = (source: string) =>
prettier.format(source, {
parser: "babel",
...prettierConfig,
});
const getVariableName = function (appName: string) {
return appName.replace(/[-.]/g, "_");
};
const getAppId = function (app: { name: string }) {
// Handle stripe separately as it's an old app with different dirName than slug/appId
return app.name === "stripepayment" ? "stripe" : app.name;
};
type App = Partial<AppMeta> & {
name: string;
path: string;
};
function generateFiles() {
const browserOutput = [`import dynamic from "next/dynamic"`];
const metadataOutput = [];
const schemasOutput = [];
const appKeysSchemasOutput = [];
const serverOutput = [];
const appDirs: { name: string; path: string }[] = [];
fs.readdirSync(`${APP_STORE_PATH}`).forEach(function (dir) {
if (dir === "ee" || dir === "templates") {
fs.readdirSync(path.join(APP_STORE_PATH, dir)).forEach(function (subDir) {
if (fs.statSync(path.join(APP_STORE_PATH, dir, subDir)).isDirectory()) {
if (getAppName(subDir)) {
appDirs.push({
name: subDir,
path: path.join(dir, subDir),
});
}
}
});
} else {
if (fs.statSync(path.join(APP_STORE_PATH, dir)).isDirectory()) {
if (!getAppName(dir)) {
return;
}
appDirs.push({
name: dir,
path: dir,
});
}
}
});
function forEachAppDir(callback: (arg: App) => void) {
for (let i = 0; i < appDirs.length; i++) {
const configPath = path.join(APP_STORE_PATH, appDirs[i].path, "config.json");
let app;
if (fs.existsSync(configPath)) {
app = JSON.parse(fs.readFileSync(configPath).toString());
} else {
app = {};
}
callback({
...app,
name: appDirs[i].name,
path: appDirs[i].path,
});
}
}
/**
* Windows has paths with backslashes, so we need to replace them with forward slashes
* .ts and .tsx files are imported without extensions
* If a file has index.ts or index.tsx, it can be imported after removing the index.ts* part
*/
function getModulePath(path: string, moduleName: string) {
return (
`./${path.replace(/\\/g, "/")}/` +
moduleName.replace(/\/index\.ts|\/index\.tsx/, "").replace(/\.tsx$|\.ts$/, "")
);
}
type ImportConfig =
| {
fileToBeImported: string;
importName?: string;
}
| [
{
fileToBeImported: string;
importName?: string;
},
{
fileToBeImported: string;
importName: string;
}
];
/**
* If importConfig is an array, only 2 items are allowed. First one is the main one and second one is the fallback
*/
function getExportedObject(
objectName: string,
{
lazyImport = false,
importConfig,
entryObjectKeyGetter = (app) => app.name,
}: {
lazyImport?: boolean;
importConfig: ImportConfig;
entryObjectKeyGetter?: (arg: App, importName?: string) => string;
}
) {
const output: string[] = [];
const getLocalImportName = (
app: { name: string },
chosenConfig: ReturnType<typeof getChosenImportConfig>
) => `${getVariableName(app.name)}_${getVariableName(chosenConfig.fileToBeImported)}`;
const fileToBeImportedExists = (
app: { path: string },
chosenConfig: ReturnType<typeof getChosenImportConfig>
) => fs.existsSync(path.join(APP_STORE_PATH, app.path, chosenConfig.fileToBeImported));
addImportStatements();
createExportObject();
return output;
function addImportStatements() {
forEachAppDir((app) => {
const chosenConfig = getChosenImportConfig(importConfig, app);
if (fileToBeImportedExists(app, chosenConfig) && chosenConfig.importName) {
const importName = chosenConfig.importName;
if (!lazyImport) {
if (importName !== "default") {
// Import with local alias that will be used by createExportObject
output.push(
`import { ${importName} as ${getLocalImportName(app, chosenConfig)} } from "${getModulePath(
app.path,
chosenConfig.fileToBeImported
)}"`
);
} else {
// Default Import
output.push(
`import ${getLocalImportName(app, chosenConfig)} from "${getModulePath(
app.path,
chosenConfig.fileToBeImported
)}"`
);
}
}
}
});
}
function createExportObject() {
output.push(`export const ${objectName} = {`);
forEachAppDir((app) => {
const chosenConfig = getChosenImportConfig(importConfig, app);
if (fileToBeImportedExists(app, chosenConfig)) {
if (!lazyImport) {
const key = entryObjectKeyGetter(app);
output.push(`"${key}": ${getLocalImportName(app, chosenConfig)},`);
} else {
const key = entryObjectKeyGetter(app);
if (chosenConfig.fileToBeImported.endsWith(".tsx")) {
output.push(
`"${key}": dynamic(() => import("${getModulePath(
app.path,
chosenConfig.fileToBeImported
)}")),`
);
} else {
output.push(`"${key}": import("${getModulePath(app.path, chosenConfig.fileToBeImported)}"),`);
}
}
}
});
output.push(`};`);
}
function getChosenImportConfig(importConfig: ImportConfig, app: { path: string }) {
let chosenConfig;
if (!(importConfig instanceof Array)) {
chosenConfig = importConfig;
} else {
if (fs.existsSync(path.join(APP_STORE_PATH, app.path, importConfig[0].fileToBeImported))) {
chosenConfig = importConfig[0];
} else {
chosenConfig = importConfig[1];
}
}
return chosenConfig;
}
}
serverOutput.push(
...getExportedObject("apiHandlers", {
importConfig: {
fileToBeImported: "api/index.ts",
},
lazyImport: true,
})
);
metadataOutput.push(
...getExportedObject("appStoreMetadata", {
// Try looking for config.json and if it's not found use _metadata.ts to generate appStoreMetadata
importConfig: [
{
fileToBeImported: "config.json",
importName: "default",
},
{
fileToBeImported: "_metadata.ts",
importName: "metadata",
},
],
})
);
schemasOutput.push(
...getExportedObject("appDataSchemas", {
// Import path must have / even for windows and not \
importConfig: {
fileToBeImported: "zod.ts",
importName: "appDataSchema",
},
// HACK: Key must be appId as this is used by eventType metadata and lookup is by appId
// This can be removed once we rename the ids of apps like stripe to that of their app folder name
entryObjectKeyGetter: (app) => getAppId(app),
})
);
appKeysSchemasOutput.push(
...getExportedObject("appKeysSchemas", {
importConfig: {
fileToBeImported: "zod.ts",
importName: "appKeysSchema",
},
// HACK: Key must be appId as this is used by eventType metadata and lookup is by appId
// This can be removed once we rename the ids of apps like stripe to that of their app folder name
entryObjectKeyGetter: (app) => getAppId(app),
})
);
browserOutput.push(
...getExportedObject("InstallAppButtonMap", {
importConfig: {
fileToBeImported: "components/InstallAppButton.tsx",
},
lazyImport: true,
})
);
// TODO: Make a component map creator that accepts ComponentName and does the rest.
// TODO: dailyvideo has a slug of daily-video, so that mapping needs to be taken care of. But it is an old app, so it doesn't need AppSettings
browserOutput.push(
...getExportedObject("AppSettingsComponentsMap", {
importConfig: {
fileToBeImported: "components/AppSettingsInterface.tsx",
},
lazyImport: true,
})
);
browserOutput.push(
...getExportedObject("EventTypeAddonMap", {
importConfig: {
fileToBeImported: "components/EventTypeAppCardInterface.tsx",
},
lazyImport: true,
})
);
const banner = `/**
This file is autogenerated using the command \`yarn app-store:build --watch\`.
Don't modify this file manually.
**/
`;
const filesToGenerate: [string, string[]][] = [
["apps.metadata.generated.ts", metadataOutput],
["apps.server.generated.ts", serverOutput],
["apps.browser.generated.tsx", browserOutput],
["apps.schemas.generated.ts", schemasOutput],
["apps.keys-schemas.generated.ts", appKeysSchemasOutput],
];
filesToGenerate.forEach(([fileName, output]) => {
fs.writeFileSync(`${APP_STORE_PATH}/${fileName}`, formatOutput(`${banner}${output.join("\n")}`));
});
console.log(`Generated ${filesToGenerate.map(([fileName]) => fileName).join(", ")}`);
}
const debouncedGenerateFiles = debounce(generateFiles);
if (isInWatchMode) {
chokidar
.watch(APP_STORE_PATH)
.on("addDir", (dirPath) => {
const appName = getAppName(dirPath);
if (appName) {
console.log(`Added ${appName}`);
debouncedGenerateFiles();
}
})
.on("change", (filePath) => {
if (filePath.endsWith("config.json")) {
console.log("Config file changed");
debouncedGenerateFiles();
}
})
.on("unlinkDir", (dirPath) => {
const appName = getAppName(dirPath);
if (appName) {
console.log(`Removed ${appName}`);
debouncedGenerateFiles();
}
});
} else {
generateFiles();
}
+38 -14
View File
@@ -3,23 +3,35 @@ import { render } from "ink";
import meow from "meow";
import React from "react";
import App from "./CliApp";
import App from "./App";
import { SupportedCommands } from "./types";
const cli = meow(
`
Usage
$ app-store create/delete/edit - Edit and Delete commands must be used on apps created using cli
$ 'app-store create' or 'app-store create-template' - Creates a new app or template
Options
[--template -t] Template to use.
Options
[--slug] Slug. This is the name of app dir for apps created with cli.
$ 'app-store edit' or 'app-store edit-template' - Edit the App or Template identified by slug
Options
[--slug -s] Slug. This is the name of app dir for apps created with cli.
$ 'app-store delete' or 'app-store delete-template' - Deletes the app or template identified by slug
Options
[--slug -s] Slug. This is the name of app dir for apps created with cli.
`,
{
flags: {
noDbUpdate: {
type: "boolean",
},
slug: {
type: "string",
alias: "s",
},
template: {
type: "string",
alias: "t",
},
},
allowUnknownFlags: false,
@@ -30,20 +42,32 @@ if (cli.input.length !== 1) {
cli.showHelp();
}
const command = cli.input[0] as "create" | "delete" | "edit";
const supportedCommands = ["create", "delete", "edit"];
const command = cli.input[0] as SupportedCommands;
const supportedCommands = [
"create",
"delete",
"edit",
"create-template",
"delete-template",
"edit-template",
] as const;
if (!supportedCommands.includes(command)) {
cli.showHelp();
}
let slug;
let slug = null;
if (command === "delete" || command === "edit") {
if (
command === "delete" ||
command === "edit" ||
command === "delete-template" ||
command === "edit-template"
) {
slug = cli.flags.slug;
if (!slug) {
console.log("--slug is required");
cli.showHelp();
cli.showHelp(0);
}
}
render(<App slug={slug} command={command} noDbUpdate={cli.flags.noDbUpdate} />);
render(<App slug={slug} template={cli.flags.template || ""} command={command} />);
@@ -0,0 +1,7 @@
import React from "react";
import { AppForm } from "../components/AppCreateUpdateForm";
export default function Create(props: Omit<React.ComponentProps<typeof AppForm>, "action">) {
return <AppForm action="create-template" {...props} />;
}
@@ -0,0 +1,7 @@
import React from "react";
import { AppForm } from "../components/AppCreateUpdateForm";
export default function CreateTemplate(props: Omit<React.ComponentProps<typeof AppForm>, "action">) {
return <AppForm action="create-template" {...props} />;
}
@@ -0,0 +1,7 @@
import React from "react";
import DeleteForm from "../components/DeleteForm";
export default function Delete({ slug }: { slug: string }) {
return <DeleteForm slug={slug} action="delete" />;
}
@@ -0,0 +1,7 @@
import React from "react";
import DeleteForm from "../components/DeleteForm";
export default function Delete({ slug }: { slug: string }) {
return <DeleteForm slug={slug} action="delete-template" />;
}

Some files were not shown because too many files have changed in this diff Show More