Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b83ee2d57d | ||
|
|
5f39bbe730 | ||
|
|
cf3d8cb4bb | ||
|
|
fbe0018d01 | ||
|
|
6586ed4d87 | ||
|
|
81e57afcf4 | ||
|
|
5cc530593b | ||
|
|
4111f78431 | ||
|
|
def80ea4cf | ||
|
|
2fc4676f6d | ||
|
|
318bafaab8 | ||
|
|
fc10c29037 | ||
|
|
ccb7b1f9ac | ||
|
|
c6c6544f50 | ||
|
|
8a6b93a747 | ||
|
|
ff6c6ccec0 | ||
|
|
c8db177f7d | ||
|
|
8871e1eb21 | ||
|
|
92b2d57871 | ||
|
|
77623c72c5 | ||
|
|
3239976e18 | ||
|
|
611939ffe8 | ||
|
|
1b8ecbe449 | ||
|
|
cd4882429d | ||
|
|
4fd2d80637 | ||
|
|
a343d934fe | ||
|
|
933cc739bd | ||
|
|
c7c75ff4d3 | ||
|
|
41f1920a7a | ||
|
|
e4836ea8f3 | ||
|
|
bfa30aa504 | ||
|
|
b36043b637 | ||
|
|
75026289fd | ||
|
|
80649c9f1a | ||
|
|
7dd75afb63 | ||
|
|
f66e4aeeed | ||
|
|
8225e61710 | ||
|
|
175fc4fcf3 | ||
|
|
f20a5df322 | ||
|
|
62544931b2 | ||
|
|
f6ad1d6ed5 | ||
|
|
d22b1072b5 | ||
|
|
4b356dd937 | ||
|
|
1f51199a7b | ||
|
|
b80a631998 | ||
|
|
bef4422080 | ||
|
|
cfd1a72c64 | ||
|
|
23b3a6661c | ||
|
|
b8b6c48d7d | ||
|
|
81655f9988 | ||
|
|
ff74822c48 | ||
|
|
8ec178debd | ||
|
|
f881061d8d | ||
|
|
2525559d6b | ||
|
|
38fb7a3f77 | ||
|
|
08956e54c5 | ||
|
|
ea65a2a27e | ||
|
|
0d45cfb7ea | ||
|
|
a4fc25e260 | ||
|
|
796c932311 | ||
|
|
4137f63ec7 | ||
|
|
2147cc82a5 | ||
|
|
40e646e9a4 | ||
|
|
cb2225259c | ||
|
|
372cd94d9f | ||
|
|
c7c282f30f | ||
|
|
959488910f | ||
|
|
0ca0cc3265 | ||
|
|
69bc12187e | ||
|
|
5e772530b2 | ||
|
|
3088b1174a | ||
|
|
fadf12b2bd | ||
|
|
c81f30b092 | ||
|
|
476119d627 | ||
|
|
040f03a9b5 | ||
|
|
b552bf6956 | ||
|
|
82d3c4370d | ||
|
|
492b4d3e97 | ||
|
|
0ae319d206 | ||
|
|
749311be2a | ||
|
|
3cce4e5009 | ||
|
|
341f82ccb9 | ||
|
|
e028a339f5 | ||
|
|
c55e432bc3 | ||
|
|
f47c3cd538 | ||
|
|
4b70a20941 | ||
|
|
c873b83965 | ||
|
|
ff859737ec | ||
|
|
607ef71f9f | ||
|
|
24dd4002fa | ||
|
|
078c9ddca5 | ||
|
|
b44e5a7aae | ||
|
|
4374da08e2 | ||
|
|
98b531e6b5 | ||
|
|
74414db760 | ||
|
|
bdf3e34ea1 | ||
|
|
738e0f3e50 | ||
|
|
16ae19abdb | ||
|
|
a2e18ae9ba | ||
|
|
e2cfd0d1d1 | ||
|
|
067b77faf1 | ||
|
|
740c01ac29 | ||
|
|
97cfd1ffa7 | ||
|
|
a7b2cfc90f | ||
|
|
d067b0782a | ||
|
|
203a36e2b5 | ||
|
|
f5a38c3f89 | ||
|
|
f179c23cc5 | ||
|
|
9f83885e8c | ||
|
|
8ffb5529e6 | ||
|
|
2e686d03fa | ||
|
|
cc3be0a6ac | ||
|
|
ec3a3268c9 | ||
|
|
77eed5611e | ||
|
|
a827d1defb | ||
|
|
b08a62d30b | ||
|
|
d6fb0df64f | ||
|
|
03548d8381 | ||
|
|
36d8554fd5 | ||
|
|
6fc9e9315d | ||
|
|
6eb8119a82 | ||
|
|
f4c613e6d1 | ||
|
|
082507aea5 | ||
|
|
90c1270e3d | ||
|
|
94917dbb1e | ||
|
|
fbbcc401cb | ||
|
|
af7d7ec678 | ||
|
|
3b7d85852a | ||
|
|
5362cd66b4 | ||
|
|
2907405d4e | ||
|
|
3c46c39114 | ||
|
|
c6ab889b98 | ||
|
|
a2e2b0ed34 | ||
|
|
bc38b3bc55 | ||
|
|
7611d5c24e | ||
|
|
5d2fd7aafb | ||
|
|
b9ade55ac1 | ||
|
|
d7051c4a3f | ||
|
|
ab50067194 | ||
|
|
cdba1920fc | ||
|
|
98a3508b8f | ||
|
|
446f384405 | ||
|
|
be79b8ecd6 | ||
|
|
371e7b024a | ||
|
|
76807a7e70 | ||
|
|
d3f77bf292 | ||
|
|
2f175eacf5 | ||
|
|
b45aea3f26 | ||
|
|
c401a58bb1 | ||
|
|
05b42c001f | ||
|
|
20f7a5841e | ||
|
|
dbf6bb991f | ||
|
|
75cd5a05b8 | ||
|
|
ca6afed27c | ||
|
|
45250f6f3e | ||
|
|
bd5af9e821 | ||
|
|
4267c9063b | ||
|
|
2b49d96e4a | ||
|
|
127c96b46c | ||
|
|
3595ba9824 | ||
|
|
56acfa38f1 | ||
|
|
e78fb22451 | ||
|
|
99f736b317 | ||
|
|
ee9f80ae3b | ||
|
|
a3c283da5e | ||
|
|
0ef678b2cf | ||
|
|
4f466fd95d | ||
|
|
5672a721d9 | ||
|
|
5ad45900ea | ||
|
|
ada88a2f7e | ||
|
|
9ba90e18de | ||
|
|
a88eb0fdc5 | ||
|
|
4b4c1200cd | ||
|
|
1cea7f9760 | ||
|
|
28dcf69d18 | ||
|
|
83d5176ddb | ||
|
|
3815a4c4bf | ||
|
|
53748eb380 | ||
|
|
28af11f90e | ||
|
|
ea676907b2 | ||
|
|
221c8b75bc | ||
|
|
5b9b7ab22d | ||
|
|
668ea088ef | ||
|
|
8a6c420268 | ||
|
|
d2f8a5c076 | ||
|
|
9c0f7bbfc7 | ||
|
|
8297824051 | ||
|
|
7ff5b66f46 | ||
|
|
55570ed4bf | ||
|
|
51695ee79d | ||
|
|
543466f1b9 | ||
|
|
4abf2d67d9 | ||
|
|
1eeb91a793 | ||
|
|
a4725920ff | ||
|
|
4cc7766f88 | ||
|
|
0c78715e9a | ||
|
|
671d9b4903 | ||
|
|
524cefcbad | ||
|
|
72f72d86b2 | ||
|
|
c5c29322d3 | ||
|
|
863b04e9c1 | ||
|
|
c2833f1b44 | ||
|
|
25df264823 | ||
|
|
e870312ebe | ||
|
|
5371bff6ff | ||
|
|
c791344c8b | ||
|
|
5e149d9f5f | ||
|
|
3e53aa13eb | ||
|
|
317f961bc8 | ||
|
|
b0c57a5c38 | ||
|
|
39c16bebd5 | ||
|
|
b3a5f4a6c0 | ||
|
|
379785196d | ||
|
|
04f8de238f | ||
|
|
0af2be9d8f | ||
|
|
98d31157cb | ||
|
|
f99d518a06 | ||
|
|
62770ecc49 | ||
|
|
be05ec66ce | ||
|
|
8defc351be | ||
|
|
ab112d92ea | ||
|
|
cf1b124dc1 | ||
|
|
38158ee043 | ||
|
|
8d214cd8ac | ||
|
|
01ed11ae33 | ||
|
|
5c7756ce6b | ||
|
|
a8c5227536 | ||
|
|
f3730daf05 | ||
|
|
47d5d800b0 | ||
|
|
f6d00a41fb | ||
|
|
692f2778e4 | ||
|
|
81d8628a87 | ||
|
|
07fbc4cbd3 | ||
|
|
8cb9a96c3a | ||
|
|
73b1c1dfb7 | ||
|
|
224a8864a6 | ||
|
|
6d02ac6729 | ||
|
|
f838e8bd53 | ||
|
|
5dac6896d2 | ||
|
|
496cb265d1 | ||
|
|
c037a62a9d | ||
|
|
88d17165ec | ||
|
|
0e7e1c2cc0 | ||
|
|
eb97feb173 | ||
|
|
7d702dd546 | ||
|
|
82b6b83885 | ||
|
|
39b6def444 | ||
|
|
7f5cdbab9b | ||
|
|
1770b832f4 | ||
|
|
d30c0a626d | ||
|
|
2eed750ef6 | ||
|
|
dd8968d0e7 | ||
|
|
827bebbc31 | ||
|
|
d97ac0e0f8 | ||
|
|
600d2725aa | ||
|
|
2a09dc6a46 | ||
|
|
1f746b67f4 | ||
|
|
bb95f2ba39 | ||
|
|
3d93b4127c | ||
|
|
094fae38fc | ||
|
|
3af614f778 | ||
|
|
77b41a4623 | ||
|
|
c23738b885 | ||
|
|
afe1f5b72f | ||
|
|
5ce341093b | ||
|
|
fc6d2839fb | ||
|
|
2fadba85f4 | ||
|
|
473b36a6be | ||
|
|
f74b9eb70a | ||
|
|
a845841ad0 | ||
|
|
dba80a1b64 | ||
|
|
41558d297c | ||
|
|
cd3946d482 | ||
|
|
dcbf695217 | ||
|
|
49cbdda4cf | ||
|
|
22f458cb74 | ||
|
|
561d9f154b | ||
|
|
0c9b9bc296 | ||
|
|
552bfcc7db | ||
|
|
986f9dca8d | ||
|
|
3af5a7404e | ||
|
|
10d3c6ecaa | ||
|
|
a0c37ea4b7 | ||
|
|
798999ad1a | ||
|
|
d40e306baa | ||
|
|
e558e61f6b | ||
|
|
210eafcd6e | ||
|
|
c05e7d75a5 | ||
|
|
d0ebf3319a | ||
|
|
9eb4187644 | ||
|
|
958cebc82c | ||
|
|
0c36dcac4c | ||
|
|
fe996b4e41 | ||
|
|
c1c95f711b | ||
|
|
7cb7006c54 | ||
|
|
e1403bfbeb | ||
|
|
c38dd2d072 | ||
|
|
f9d9e74d6b | ||
|
|
2ab597addc | ||
|
|
33e394d5ed | ||
|
|
4577de3b17 | ||
|
|
658b529840 | ||
|
|
4aed769da4 | ||
|
|
6c753290aa | ||
|
|
59c91607db | ||
|
|
b0530d59af | ||
|
|
b1ff829745 | ||
|
|
d202b536b5 | ||
|
|
2ac5ef10ba | ||
|
|
8a2ab68e9a | ||
|
|
877220caa0 | ||
|
|
0686c08de3 | ||
|
|
d3f1f8b906 | ||
|
|
33a69d3ef6 | ||
|
|
d91b451d7b | ||
|
|
9e17149e5e | ||
|
|
de61b032b7 | ||
|
|
bdbf9bd04d | ||
|
|
ccd589ef38 | ||
|
|
b4c3753566 | ||
|
|
2fdadb98ac | ||
|
|
a33847a878 | ||
|
|
88cf535c8e | ||
|
|
ea0b59d7c0 | ||
|
|
39b93cdb5b | ||
|
|
e27fc05d5f | ||
|
|
20c010fef1 | ||
|
|
1a7911e9e2 | ||
|
|
cb09b5bc5f | ||
|
|
a99031bdd2 | ||
|
|
ada8a98362 | ||
|
|
e176e81bae | ||
|
|
b64dd66c2c | ||
|
|
7fd162601b | ||
|
|
c1f346c935 | ||
|
|
36d47bd2d8 | ||
|
|
26cdb6521f | ||
|
|
014f1986de | ||
|
|
3ef328421f | ||
|
|
c4fe69d826 | ||
|
|
6fd64b2209 | ||
|
|
585e36793a | ||
|
|
09b9370e13 | ||
|
|
a76a4996b4 | ||
|
|
b28d60b6dc | ||
|
|
a716eda14c | ||
|
|
99f1524183 | ||
|
|
7ce6454915 | ||
|
|
93cb21f55a | ||
|
|
0aca769d06 | ||
|
|
c0bda6257e | ||
|
|
ae22246f05 | ||
|
|
e2931d9fa2 | ||
|
|
e6a8a31b53 | ||
|
|
ee86c751f7 | ||
|
|
073b212c05 | ||
|
|
cd8e2124c4 | ||
|
|
9dd36bd20e | ||
|
|
b1404356ff | ||
|
|
f467bb7957 | ||
|
|
597b14cf4c | ||
|
|
2bdd6df755 | ||
|
|
ad958f10bd | ||
|
|
e5eb7c3906 | ||
|
|
667d42d5da | ||
|
|
a23a9ad15a | ||
|
|
4482ffc66b | ||
|
|
493efabd2e | ||
|
|
a0355e276d | ||
|
|
b5c993e153 | ||
|
|
da3ee3f1fe | ||
|
|
d9fc595444 | ||
|
|
06d13e0f6e | ||
|
|
7dc3266133 | ||
|
|
2c96444058 | ||
|
|
39c48e7f79 | ||
|
|
b6e1989f17 | ||
|
|
45b5b2a376 | ||
|
|
05726ec94c | ||
|
|
6340ff43d8 | ||
|
|
e64b26c7e1 | ||
|
|
c829a7de82 | ||
|
|
0205bfb066 | ||
|
|
5fd7d63388 | ||
|
|
e1ea2cb532 | ||
|
|
88d151aa35 | ||
|
|
4e7c93a322 | ||
|
|
281c7628c1 | ||
|
|
1b24bc14cd | ||
|
|
be3d2fca05 | ||
|
|
327e11e7c7 | ||
|
|
bd999345f4 | ||
|
|
081776b4f9 | ||
|
|
8c328ce5d7 | ||
|
|
0fa2a6aea3 | ||
|
|
440251f9c3 | ||
|
|
7f6847f7c7 | ||
|
|
9f1fa83a97 | ||
|
|
f00b112792 | ||
|
|
5339a489f5 | ||
|
|
f7df204e0b | ||
|
|
4975aba5e0 | ||
|
|
29d61ad67b | ||
|
|
6e555e64fa | ||
|
|
c748bbc7e6 | ||
|
|
0681359bf3 | ||
|
|
4c9d4d7972 | ||
|
|
67f8de42b3 | ||
|
|
87584132b5 | ||
|
|
5b943742e0 | ||
|
|
fe155b0b97 | ||
|
|
6639588c2d | ||
|
|
7f9e8ca7a6 | ||
|
|
e478a46358 | ||
|
|
7c9012738a | ||
|
|
5c763389f7 | ||
|
|
7fde30edf9 | ||
|
|
0e22e92cab | ||
|
|
17f48614ff | ||
|
|
2f33b57eb7 | ||
|
|
6e5c3a88ee | ||
|
|
9024914f23 | ||
|
|
966bc08eb4 | ||
|
|
c545331cfe | ||
|
|
ce763f30f6 | ||
|
|
d453ae9f2b | ||
|
|
9f2b41e597 | ||
|
|
86e903ce4a | ||
|
|
9d3963436b | ||
|
|
d8218a6243 | ||
|
|
cdc3833e09 | ||
|
|
52b7e401a1 | ||
|
|
5170fc2424 | ||
|
|
7349fb9f6d | ||
|
|
a8368aac79 | ||
|
|
91f381bce9 | ||
|
|
fb9c1b18e0 | ||
|
|
1d18a09cc8 | ||
|
|
887d63ceef | ||
|
|
555fcc21f2 | ||
|
|
f45fb03838 | ||
|
|
46611fdf77 | ||
|
|
c0f7c40bec | ||
|
|
f6f257c705 | ||
|
|
d61f50724b | ||
|
|
960aa0e00b | ||
|
|
817e20f11e | ||
|
|
af0123a606 | ||
|
|
709bbf6626 | ||
|
|
53097ee3bc | ||
|
|
31f2002654 | ||
|
|
ee155c3643 | ||
|
|
8ab20c3c9f | ||
|
|
4f6d035d29 | ||
|
|
2e5b42ef47 | ||
|
|
b055672ea0 | ||
|
|
e4fbca388c | ||
|
|
2b8bba09c9 | ||
|
|
98cf29f655 | ||
|
|
ae8706316f | ||
|
|
ec8e08d442 | ||
|
|
c33f424333 | ||
|
|
6f2312d1e2 | ||
|
|
c2b6070b7e | ||
|
|
a5bebe2b48 | ||
|
|
e3a1f20cc8 | ||
|
|
ab639591da | ||
|
|
21a9b64eb0 | ||
|
|
636ee7b8a1 | ||
|
|
543e526568 | ||
|
|
e465c1d71d | ||
|
|
cdab9037ce | ||
|
|
21e454d0e2 | ||
|
|
7384f29a1c | ||
|
|
9016a854ca | ||
|
|
446c29dd9c | ||
|
|
2cf1f07d8f | ||
|
|
4f03f8e66d | ||
|
|
e1096620e6 | ||
|
|
d416386d7b | ||
|
|
5b7c55b029 | ||
|
|
9b05311d7f | ||
|
|
313d25d7d8 | ||
|
|
5ec9c41f7c | ||
|
|
d56386d016 | ||
|
|
0968ede733 | ||
|
|
8e3ed28ba9 | ||
|
|
82ea4e9fb7 | ||
|
|
8a440c24c0 | ||
|
|
88a8da3ef6 | ||
|
|
49290fba72 | ||
|
|
a363b1e903 | ||
|
|
7b5e2c8bab | ||
|
|
4b0d22655d | ||
|
|
4f92f60d85 | ||
|
|
6986b5af40 | ||
|
|
e043a5efd8 | ||
|
|
bff28e5d6f | ||
|
|
f10ff18d9f | ||
|
|
cefe4754c7 | ||
|
|
d7aa3c7163 | ||
|
|
e899a92071 | ||
|
|
ebdbe407cd | ||
|
|
e7f917bab9 | ||
|
|
ba9688a04a | ||
|
|
f0e15d77dc | ||
|
|
30b72b687e | ||
|
|
bbf50dbc53 | ||
|
|
bb547a0111 | ||
|
|
1758288948 | ||
|
|
11789c725a | ||
|
|
73bfe0aebe | ||
|
|
24140f0f29 | ||
|
|
e504f5cd9e | ||
|
|
72629ffee0 | ||
|
|
5b6f007d8f | ||
|
|
2e0951d4dc | ||
|
|
fd84b5754d | ||
|
|
73afb03308 | ||
|
|
99dba73d0b | ||
|
|
db4449d849 | ||
|
|
53e52a0510 | ||
|
|
cf4bd8a77d | ||
|
|
9d61dc2f7c | ||
|
|
caf5918b72 | ||
|
|
84f64f5d06 | ||
|
|
13a53c020b | ||
|
|
a72f90fe02 | ||
|
|
283d5b4a65 | ||
|
|
81b2e37efd | ||
|
|
469967b320 | ||
|
|
d708625da0 | ||
|
|
acf2487eaa | ||
|
|
b81ca0e2ab | ||
|
|
2357f76cc8 | ||
|
|
304e8aaa68 | ||
|
|
9fe912c392 | ||
|
|
ab7ecff580 | ||
|
|
19e830acad | ||
|
|
458300bc83 | ||
|
|
352a467104 | ||
|
|
c052c66abc | ||
|
|
8f8ed4d80d | ||
|
|
04fb7e07b4 | ||
|
|
20d2129f81 | ||
|
|
ae5425f908 | ||
|
|
762810684e | ||
|
|
4720dc410f | ||
|
|
715aea5733 | ||
|
|
6cbb9bf967 | ||
|
|
d8176b2eb4 | ||
|
|
6c51e2a970 | ||
|
|
3a08eb3dca | ||
|
|
cee905bc5a | ||
|
|
72fd6334c6 | ||
|
|
63f51abd84 | ||
|
|
6e856b3bd1 | ||
|
|
5fceee27fe | ||
|
|
4f590ebbf7 | ||
|
|
736cd63509 | ||
|
|
3aaa1cde0d | ||
|
|
02d32321aa | ||
|
|
697f6db2c4 | ||
|
|
83364a9e97 | ||
|
|
1620ae660f | ||
|
|
605f9c61f3 | ||
|
|
564862369f | ||
|
|
be6cf4705c | ||
|
|
a2457ab619 | ||
|
|
867966dd75 | ||
|
|
08ad4c07ea | ||
|
|
78da57055e | ||
|
|
d5c3d9d967 | ||
|
|
aa0e9e6a02 | ||
|
|
cad97f1779 | ||
|
|
31ee689672 | ||
|
|
3edb6f5ffd | ||
|
|
a8825badec | ||
|
|
04c634ec4b | ||
|
|
8dd9b8d74b | ||
|
|
0365157638 | ||
|
|
a571d40958 | ||
|
|
69491d5700 | ||
|
|
20022e2cdd | ||
|
|
492858b45b | ||
|
|
b49ad8294b | ||
|
|
aa30ff7d1a | ||
|
|
cfa720a523 | ||
|
|
9ab196c30e |
No files matched your search
@@ -0,0 +1,8 @@
|
|||||||
|
# Changesets
|
||||||
|
|
||||||
|
Hello and welcome! This folder has been automatically generated by `@changesets/cli`, a build tool that works
|
||||||
|
with multi-package repos, or single-package repos to help you version and publish your code. You can
|
||||||
|
find the full documentation for it [in our repository](https://github.com/changesets/changesets)
|
||||||
|
|
||||||
|
We have a quick list of common questions to get you started engaging with this project in
|
||||||
|
[our documentation](https://github.com/changesets/changesets/blob/main/docs/common-questions.md)
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
{
|
||||||
|
"$schema": "https://unpkg.com/@changesets/config@2.3.0/schema.json",
|
||||||
|
"changelog": "@changesets/cli/changelog",
|
||||||
|
"commit": false,
|
||||||
|
"fixed": [],
|
||||||
|
"linked": [],
|
||||||
|
"access": "public",
|
||||||
|
"baseBranch": "main",
|
||||||
|
"updateInternalDependencies": "patch",
|
||||||
|
"ignore": []
|
||||||
|
}
|
||||||
@@ -1,42 +0,0 @@
|
|||||||
// For format details, see https://aka.ms/devcontainer.json. For config options, see the
|
|
||||||
// README at: https://github.com/devcontainers/templates/tree/main/src/docker-outside-of-docker
|
|
||||||
{
|
|
||||||
"name": "Docker outside of Docker",
|
|
||||||
// Or use a Dockerfile or Docker Compose file. More info: https://containers.dev/guide/dockerfile
|
|
||||||
"image": "mcr.microsoft.com/devcontainers/base:bullseye",
|
|
||||||
|
|
||||||
"features": {
|
|
||||||
"ghcr.io/devcontainers/features/docker-from-docker:1": {
|
|
||||||
"version": "latest",
|
|
||||||
"enableNonRootDocker": "true",
|
|
||||||
"moby": "true"
|
|
||||||
},
|
|
||||||
"ghcr.io/devcontainers/features/node:1": {},
|
|
||||||
"ghcr.io/devcontainers-contrib/features/npm-package:1": {},
|
|
||||||
"ghcr.io/devcontainers-contrib/features/jest:2": {},
|
|
||||||
"ghcr.io/devcontainers-contrib/features/prisma:2": {},
|
|
||||||
"ghcr.io/guiyomh/features/vim:0": {}
|
|
||||||
},
|
|
||||||
|
|
||||||
// Use this environment variable if you need to bind mount your local source code into a new container.
|
|
||||||
"remoteEnv": {
|
|
||||||
"LOCAL_WORKSPACE_FOLDER": "${localWorkspaceFolder}"
|
|
||||||
},
|
|
||||||
|
|
||||||
"hostRequirements": {
|
|
||||||
"cpus": 4,
|
|
||||||
"memory": "8gb"
|
|
||||||
},
|
|
||||||
|
|
||||||
// Use 'postCreateCommand' to run commands after the container is created.
|
|
||||||
"postCreateCommand": "./deploy/install.sh"
|
|
||||||
|
|
||||||
// Use 'forwardPorts' to make a list of ports inside the container available locally.
|
|
||||||
// "forwardPorts": [],
|
|
||||||
|
|
||||||
// Configure tool-specific properties.
|
|
||||||
// "customizations": {},
|
|
||||||
|
|
||||||
// Uncomment to connect as root instead. More info: https://aka.ms/dev-containers-non-root.
|
|
||||||
// "remoteUser": "root"
|
|
||||||
}
|
|
||||||
@@ -16,6 +16,7 @@
|
|||||||
# - WEB3
|
# - WEB3
|
||||||
# - SALESFORCE
|
# - SALESFORCE
|
||||||
# - ZOHOCRM
|
# - ZOHOCRM
|
||||||
|
# - ZOHO_BIGIN
|
||||||
|
|
||||||
# - APP STORE **********************************************************************************************
|
# - APP STORE **********************************************************************************************
|
||||||
# ⚠️ ⚠️ ⚠️ THESE WILL BE MIGRATED TO THE DATABASE TO PREVENT AWS's 4KB ENV QUOTA ⚠️ ⚠️ ⚠️
|
# ⚠️ ⚠️ ⚠️ THESE WILL BE MIGRATED TO THE DATABASE TO PREVENT AWS's 4KB ENV QUOTA ⚠️ ⚠️ ⚠️
|
||||||
|
|||||||
+13
-7
@@ -79,6 +79,9 @@ CALENDSO_ENCRYPTION_KEY=
|
|||||||
# Intercom Config
|
# Intercom Config
|
||||||
NEXT_PUBLIC_INTERCOM_APP_ID=
|
NEXT_PUBLIC_INTERCOM_APP_ID=
|
||||||
|
|
||||||
|
# Secret to enable Intercom Identity Verification
|
||||||
|
INTERCOM_SECRET=
|
||||||
|
|
||||||
# Zendesk Config
|
# Zendesk Config
|
||||||
NEXT_PUBLIC_ZENDESK_KEY=
|
NEXT_PUBLIC_ZENDESK_KEY=
|
||||||
|
|
||||||
@@ -135,12 +138,16 @@ API_KEY_PREFIX=cal_
|
|||||||
EMAIL_FROM='notifications@yourselfhostedcal.com'
|
EMAIL_FROM='notifications@yourselfhostedcal.com'
|
||||||
|
|
||||||
# Configure SMTP settings (@see https://nodemailer.com/smtp/).
|
# Configure SMTP settings (@see https://nodemailer.com/smtp/).
|
||||||
|
# Configuration to receive emails locally (mailhog)
|
||||||
|
EMAIL_SERVER_HOST='localhost'
|
||||||
|
EMAIL_SERVER_PORT=1025
|
||||||
|
|
||||||
# Note: The below configuration for Office 365 has been verified to work.
|
# Note: The below configuration for Office 365 has been verified to work.
|
||||||
EMAIL_SERVER_HOST='smtp.office365.com'
|
# EMAIL_SERVER_HOST='smtp.office365.com'
|
||||||
EMAIL_SERVER_PORT=587
|
# EMAIL_SERVER_PORT=587
|
||||||
EMAIL_SERVER_USER='<office365_emailAddress>'
|
# EMAIL_SERVER_USER='<office365_emailAddress>'
|
||||||
# Keep in mind that if you have 2FA enabled, you will need to provision an App Password.
|
# Keep in mind that if you have 2FA enabled, you will need to provision an App Password.
|
||||||
EMAIL_SERVER_PASSWORD='<office365_password>'
|
# EMAIL_SERVER_PASSWORD='<office365_password>'
|
||||||
|
|
||||||
# The following configuration for Gmail has been verified to work.
|
# The following configuration for Gmail has been verified to work.
|
||||||
# EMAIL_SERVER_HOST='smtp.gmail.com'
|
# EMAIL_SERVER_HOST='smtp.gmail.com'
|
||||||
@@ -160,9 +167,6 @@ CLOSECOM_API_KEY=
|
|||||||
# Sendgrid internal sync service
|
# Sendgrid internal sync service
|
||||||
SENDGRID_SYNC_API_KEY=
|
SENDGRID_SYNC_API_KEY=
|
||||||
|
|
||||||
# Sentry
|
|
||||||
NEXT_PUBLIC_SENTRY_DSN=
|
|
||||||
SENTRY_IGNORE_API_RESOLUTION_ERROR=
|
|
||||||
|
|
||||||
# Change your Brand
|
# Change your Brand
|
||||||
NEXT_PUBLIC_APP_NAME="Cal.com"
|
NEXT_PUBLIC_APP_NAME="Cal.com"
|
||||||
@@ -177,3 +181,5 @@ CSP_POLICY=
|
|||||||
|
|
||||||
# Vercel Edge Config
|
# Vercel Edge Config
|
||||||
EDGE_CONFIG=
|
EDGE_CONFIG=
|
||||||
|
|
||||||
|
NEXT_PUBLIC_MINUTES_TO_BOOK=5 # Minutes
|
||||||
+2
-1
@@ -5,7 +5,8 @@
|
|||||||
# the repo. Unless a later match takes precedence,
|
# the repo. Unless a later match takes precedence,
|
||||||
# @global-owner1 and @global-owner2 will be requested for
|
# @global-owner1 and @global-owner2 will be requested for
|
||||||
# review when someone opens a pull request.
|
# review when someone opens a pull request.
|
||||||
* @calcom/reviewers
|
# ⬇️ Removed so we can manually trigger checks
|
||||||
|
# * @calcom/reviewers
|
||||||
|
|
||||||
.github/ @calcom/core
|
.github/ @calcom/core
|
||||||
.husky/ @calcom/core
|
.husky/ @calcom/core
|
||||||
|
|||||||
@@ -12,13 +12,13 @@ Fixes # (issue)
|
|||||||
|
|
||||||
## Type of change
|
## Type of change
|
||||||
|
|
||||||
<!-- Please delete options that are not relevant. -->
|
<!-- Please delete bullets that are not relevant. -->
|
||||||
|
|
||||||
- [ ] Bug fix (non-breaking change which fixes an issue)
|
- Bug fix (non-breaking change which fixes an issue)
|
||||||
- [ ] Chore (refactoring code, technical debt, workflow improvements)
|
- Chore (refactoring code, technical debt, workflow improvements)
|
||||||
- [ ] New feature (non-breaking change which adds functionality)
|
- New feature (non-breaking change which adds functionality)
|
||||||
- [ ] Breaking change (fix or feature that would cause existing functionality to not work as expected)
|
- Breaking change (fix or feature that would cause existing functionality to not work as expected)
|
||||||
- [ ] This change requires a documentation update
|
- This change requires a documentation update
|
||||||
|
|
||||||
## How should this be tested?
|
## How should this be tested?
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,18 @@
|
|||||||
|
{
|
||||||
|
"problemMatcher": [
|
||||||
|
{
|
||||||
|
"owner": "tsc-absolute",
|
||||||
|
"pattern": [
|
||||||
|
{
|
||||||
|
"regexp": "(?:^|\\s)([^\\s].*)[\\(:](\\d+)[,:](\\d+)(?:\\):\\s+|\\s+-\\s+)(error|warning|info)\\s+(TS\\d+)\\s*:\\s*(.*)$",
|
||||||
|
"file": 1,
|
||||||
|
"line": 2,
|
||||||
|
"column": 3,
|
||||||
|
"severity": 4,
|
||||||
|
"code": 5,
|
||||||
|
"message": 6
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
name: "Apply issue labels to PR"
|
||||||
|
|
||||||
|
on:
|
||||||
|
pull_request:
|
||||||
|
types: [opened]
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
label_on_pr:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
pull-requests: write
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Checkout repository
|
||||||
|
uses: actions/checkout@v2
|
||||||
|
|
||||||
|
- name: Setup Node.js
|
||||||
|
uses: actions/setup-node@v2
|
||||||
|
with:
|
||||||
|
node-version: 16
|
||||||
|
|
||||||
|
- name: Apply labels from linked issue to PR
|
||||||
|
env:
|
||||||
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
PR_DATA: ${{ toJson(github.event.pull_request) }}
|
||||||
|
run: node ./.github/workflows/scripts/apply-issue-labels-to-pr.ts
|
||||||
@@ -10,8 +10,10 @@ jobs:
|
|||||||
- uses: actions/checkout@v3
|
- uses: actions/checkout@v3
|
||||||
- uses: ./.github/actions/dangerous-git-checkout
|
- uses: ./.github/actions/dangerous-git-checkout
|
||||||
- uses: ./.github/actions/yarn-install
|
- uses: ./.github/actions/yarn-install
|
||||||
|
|
||||||
- name: Show info
|
- name: Show info
|
||||||
run: node -e "console.log(require('v8').getHeapStatistics())"
|
run: node -e "console.log(require('v8').getHeapStatistics())"
|
||||||
|
- name: Configure TSC problem matcher
|
||||||
- run: yarn type-check
|
run: |
|
||||||
|
echo "::remove-matcher owner=tsc::"
|
||||||
|
echo "::add-matcher::.github/matchers/tsc-absolute.json"
|
||||||
|
- run: yarn type-check:ci
|
||||||
@@ -1,11 +1,12 @@
|
|||||||
name: Cron - downgradeUsers
|
name: Cron - downgradeUsers
|
||||||
|
|
||||||
on:
|
on:
|
||||||
|
workflow_dispatch:
|
||||||
# "Scheduled workflows run on the latest commit on the default or base branch."
|
# "Scheduled workflows run on the latest commit on the default or base branch."
|
||||||
# — https://docs.github.com/en/actions/learn-github-actions/events-that-trigger-workflows#schedule
|
# — https://docs.github.com/en/actions/learn-github-actions/events-that-trigger-workflows#schedule
|
||||||
schedule:
|
schedule:
|
||||||
# Runs “At minute 0, 15, 30, and 45.” (see https://crontab.guru)
|
# Runs “Every month at 1st (see https://crontab.guru)
|
||||||
- cron: "0,15,30,45 * * * *"
|
- cron: "0 0 1 * *"
|
||||||
jobs:
|
jobs:
|
||||||
cron-downgradeUsers:
|
cron-downgradeUsers:
|
||||||
env:
|
env:
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ name: Cron - mark stale for inactive issues
|
|||||||
|
|
||||||
permissions:
|
permissions:
|
||||||
issues: write
|
issues: write
|
||||||
|
pull-requests: write
|
||||||
|
|
||||||
on:
|
on:
|
||||||
# "Scheduled workflows run on the latest commit on the default or base branch."
|
# "Scheduled workflows run on the latest commit on the default or base branch."
|
||||||
@@ -15,6 +16,9 @@ jobs:
|
|||||||
steps:
|
steps:
|
||||||
- uses: actions/stale@v7
|
- uses: actions/stale@v7
|
||||||
with:
|
with:
|
||||||
days-before-stale: 60
|
days-before-issue-stale: 60
|
||||||
include-only-assigned: true
|
days-before-issue-close: -1
|
||||||
days-before-close: -1
|
days-before-pr-stale: 14
|
||||||
|
days-before-pr-close: 7
|
||||||
|
stale-pr-message: "This PR is being marked as stale due to inactivity."
|
||||||
|
close-pr-message: "This PR is being closed due to inactivity. Please reopen if work is intended to be continued."
|
||||||
@@ -15,6 +15,8 @@ jobs:
|
|||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v3
|
uses: actions/checkout@v3
|
||||||
|
with:
|
||||||
|
token: ${{ secrets.GH_ACCESS_TOKEN }}
|
||||||
|
|
||||||
- name: crowdin action
|
- name: crowdin action
|
||||||
uses: crowdin/github-action@1.5.1
|
uses: crowdin/github-action@1.5.1
|
||||||
@@ -26,6 +28,6 @@ jobs:
|
|||||||
localization_branch_name: main
|
localization_branch_name: main
|
||||||
create_pull_request: false
|
create_pull_request: false
|
||||||
env:
|
env:
|
||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
GITHUB_TOKEN: ${{ secrets.GH_ACCESS_TOKEN }}
|
||||||
CROWDIN_PROJECT_ID: ${{ secrets.CROWDIN_PROJECT_ID }}
|
CROWDIN_PROJECT_ID: ${{ secrets.CROWDIN_PROJECT_ID }}
|
||||||
CROWDIN_PERSONAL_TOKEN: ${{ secrets.CROWDIN_PERSONAL_TOKEN }}
|
CROWDIN_PERSONAL_TOKEN: ${{ secrets.CROWDIN_PERSONAL_TOKEN }}
|
||||||
@@ -24,7 +24,7 @@ jobs:
|
|||||||
- name: Analyze bundle
|
- name: Analyze bundle
|
||||||
run: |
|
run: |
|
||||||
cd apps/web
|
cd apps/web
|
||||||
npx -p nextjs-bundle-analysis report
|
npx -p nextjs-bundle-analysis@0.5.0 report
|
||||||
|
|
||||||
- name: Upload bundle
|
- name: Upload bundle
|
||||||
uses: actions/upload-artifact@v2
|
uses: actions/upload-artifact@v2
|
||||||
@@ -76,7 +76,7 @@ jobs:
|
|||||||
id: fc
|
id: fc
|
||||||
with:
|
with:
|
||||||
issue-number: ${{ github.event.number }}
|
issue-number: ${{ github.event.number }}
|
||||||
body-includes: "<!-- __NEXTJS_BUNDLE -->"
|
body-includes: "<!-- __NEXTJS_BUNDLE_@calcom/web -->"
|
||||||
|
|
||||||
- name: Create Comment
|
- name: Create Comment
|
||||||
uses: peter-evans/create-or-update-comment@v1.4.4
|
uses: peter-evans/create-or-update-comment@v1.4.4
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ on:
|
|||||||
paths-ignore:
|
paths-ignore:
|
||||||
- "**.md"
|
- "**.md"
|
||||||
- ".github/CODEOWNERS"
|
- ".github/CODEOWNERS"
|
||||||
|
merge_group:
|
||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
|
|
||||||
concurrency:
|
concurrency:
|
||||||
|
|||||||
@@ -0,0 +1,139 @@
|
|||||||
|
const https = require("https");
|
||||||
|
|
||||||
|
async function applyLabelFromLinkedIssueToPR(pr, token) {
|
||||||
|
// Get the labels from issues linked to the PR
|
||||||
|
const query = `
|
||||||
|
query GetLinkedIssues($owner: String!, $repo: String!, $prNumber: Int!) {
|
||||||
|
repository(owner: $owner, name: $repo) {
|
||||||
|
pullRequest(number: $prNumber) {
|
||||||
|
closingIssuesReferences(first: 10) {
|
||||||
|
nodes {
|
||||||
|
number
|
||||||
|
labels(first: 10) {
|
||||||
|
nodes {
|
||||||
|
name
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
const graphqlData = JSON.stringify({
|
||||||
|
query,
|
||||||
|
variables: {
|
||||||
|
owner: pr.base.repo.owner.login,
|
||||||
|
repo: pr.base.repo.name,
|
||||||
|
prNumber: pr.number,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const requestOptions = {
|
||||||
|
hostname: "api.github.com",
|
||||||
|
path: "/graphql",
|
||||||
|
method: "POST",
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
"Content-Length": graphqlData.length,
|
||||||
|
Authorization: "Bearer " + token,
|
||||||
|
"User-Agent": "Node.js",
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
// Use the native Node.js request library to make the request
|
||||||
|
let linkedIssues;
|
||||||
|
linkedIssues = await new Promise((resolve, reject) => {
|
||||||
|
const request = https.request(requestOptions, (response) => {
|
||||||
|
let responseBody = "";
|
||||||
|
response.on("data", (chunk) => {
|
||||||
|
responseBody += chunk;
|
||||||
|
});
|
||||||
|
response.on("end", () => {
|
||||||
|
resolve(JSON.parse(responseBody)?.data?.repository?.pullRequest?.closingIssuesReferences?.nodes);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
request.on("error", (error) => {
|
||||||
|
reject(error);
|
||||||
|
});
|
||||||
|
|
||||||
|
request.write(graphqlData);
|
||||||
|
request.end();
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!linkedIssues || linkedIssues.length === 0) {
|
||||||
|
console.log("No issue linked.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Iterate over linked issues and apply labels to PR
|
||||||
|
for (const issue of linkedIssues) {
|
||||||
|
const labels = issue?.labels?.nodes?.map((label) => label.name);
|
||||||
|
|
||||||
|
if (!labels || labels.length === 0) {
|
||||||
|
console.log(`No labels found on the linked issue #${issue.number}.`);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const labelsData = JSON.stringify({
|
||||||
|
labels: labels,
|
||||||
|
});
|
||||||
|
|
||||||
|
const requestOptions = {
|
||||||
|
hostname: "api.github.com",
|
||||||
|
path: `/repos/${pr.base.repo.owner.login}/${pr.base.repo.name}/issues/${pr.number}/labels`,
|
||||||
|
method: "POST",
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
"Content-Length": labelsData.length,
|
||||||
|
Authorization: "Bearer " + token,
|
||||||
|
"User-Agent": "Node.js",
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
let labelResult;
|
||||||
|
labelResult = await new Promise((resolve, reject) => {
|
||||||
|
const request = https.request(requestOptions, (response) => {
|
||||||
|
let responseBody = "";
|
||||||
|
response.on("data", (chunk) => {
|
||||||
|
responseBody += chunk;
|
||||||
|
});
|
||||||
|
response.on("data", () => {});
|
||||||
|
response.on("end", () => {
|
||||||
|
resolve(JSON.parse(responseBody));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
request.on("error", (error) => {
|
||||||
|
reject(error);
|
||||||
|
});
|
||||||
|
|
||||||
|
request.write(labelsData);
|
||||||
|
request.end();
|
||||||
|
});
|
||||||
|
|
||||||
|
if (labelResult.message) {
|
||||||
|
console.log(`Error labelling PR: ${labelResult.message}`);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(
|
||||||
|
`Applied labels: ${labels.join(", ")} to PR #${pr.number} from linked issue #${issue.number}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Pass the PR data and GitHub token to the script
|
||||||
|
(async () => {
|
||||||
|
if (!process.env.PR_DATA) {
|
||||||
|
console.log("No PR data found.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const prData = JSON.parse(process.env.PR_DATA);
|
||||||
|
const token = process.env.GITHUB_TOKEN;
|
||||||
|
|
||||||
|
await applyLabelFromLinkedIssueToPR(prData, token);
|
||||||
|
})();
|
||||||
@@ -1,28 +0,0 @@
|
|||||||
name: TODO to Issue
|
|
||||||
on:
|
|
||||||
push:
|
|
||||||
branches:
|
|
||||||
- main
|
|
||||||
workflow_dispatch:
|
|
||||||
inputs:
|
|
||||||
MANUAL_COMMIT_REF:
|
|
||||||
description: "The SHA of the commit to get the diff for"
|
|
||||||
required: true
|
|
||||||
MANUAL_BASE_REF:
|
|
||||||
description: "By default, the commit entered above is compared to the one directly before it; to go back further, enter an earlier SHA here"
|
|
||||||
required: false
|
|
||||||
concurrency:
|
|
||||||
group: ${{ github.workflow }}-${{ github.ref }}
|
|
||||||
cancel-in-progress: true
|
|
||||||
jobs:
|
|
||||||
todos:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v3
|
|
||||||
- name: "Add TODOs to issues"
|
|
||||||
uses: "alstr/todo-to-issue-action@v4"
|
|
||||||
env:
|
|
||||||
MANUAL_COMMIT_REF: ${{ inputs.MANUAL_COMMIT_REF }}
|
|
||||||
MANUAL_BASE_REF: ${{ inputs.MANUAL_BASE_REF }}
|
|
||||||
with:
|
|
||||||
IDENTIFIERS: '[{"name": "TODO", "labels": ["🙋🏻♂️help wanted"]}, {"name": "FIXME", "labels": ["🐛 bug"]}]'
|
|
||||||
+2
-1
@@ -65,8 +65,9 @@ tsconfig.tsbuildinfo
|
|||||||
# turbo
|
# turbo
|
||||||
.turbo
|
.turbo
|
||||||
|
|
||||||
# Prisma-Zod
|
# Prisma generated files
|
||||||
packages/prisma/zod/*.ts
|
packages/prisma/zod/*.ts
|
||||||
|
packages/prisma/enums
|
||||||
|
|
||||||
# Builds
|
# Builds
|
||||||
dist
|
dist
|
||||||
|
|||||||
+43
@@ -0,0 +1,43 @@
|
|||||||
|
tasks:
|
||||||
|
- init: |
|
||||||
|
yarn &&
|
||||||
|
cp .env.example .env &&
|
||||||
|
next_auth_secret=$(openssl rand -base64 32) &&
|
||||||
|
calendso_encryption_key=$(openssl rand -base64 24) &&
|
||||||
|
sed -i -e "s|^NEXTAUTH_SECRET=.*|NEXTAUTH_SECRET=$next_auth_secret|" \
|
||||||
|
-e "s|^CALENDSO_ENCRYPTION_KEY=.*|CALENDSO_ENCRYPTION_KEY=$calendso_encryption_key|" .env
|
||||||
|
command: yarn dx
|
||||||
|
|
||||||
|
ports:
|
||||||
|
- port: 3000
|
||||||
|
visibility: public
|
||||||
|
onOpen: open-preview
|
||||||
|
- port: 5420
|
||||||
|
visibility: private
|
||||||
|
onOpen: ignore
|
||||||
|
- port: 1025
|
||||||
|
visibility: private
|
||||||
|
onOpen: ignore
|
||||||
|
- port: 8025
|
||||||
|
visibility: private
|
||||||
|
onOpen: ignore
|
||||||
|
|
||||||
|
github:
|
||||||
|
prebuilds:
|
||||||
|
master: true
|
||||||
|
pullRequests: true
|
||||||
|
pullRequestsFromForks: true
|
||||||
|
addCheck: true
|
||||||
|
addComment: true
|
||||||
|
addBadge: true
|
||||||
|
|
||||||
|
vscode:
|
||||||
|
extensions:
|
||||||
|
- DavidAnson.vscode-markdownlint
|
||||||
|
- yzhang.markdown-all-in-one
|
||||||
|
- esbenp.prettier-vscode
|
||||||
|
- dbaeumer.vscode-eslint
|
||||||
|
- bradlc.vscode-tailwindcss
|
||||||
|
- ban.spellright
|
||||||
|
- stripe.vscode-stripe
|
||||||
|
- Prisma.prisma
|
||||||
@@ -14,4 +14,5 @@ public
|
|||||||
.DS_Store
|
.DS_Store
|
||||||
.eslintignore
|
.eslintignore
|
||||||
packages/prisma/zod
|
packages/prisma/zod
|
||||||
|
packages/prisma/enums
|
||||||
apps/web/public/embed
|
apps/web/public/embed
|
||||||
Vendored
+1
-1
@@ -6,6 +6,6 @@
|
|||||||
},
|
},
|
||||||
"typescript.preferences.importModuleSpecifier": "non-relative",
|
"typescript.preferences.importModuleSpecifier": "non-relative",
|
||||||
"spellright.language": ["en"],
|
"spellright.language": ["en"],
|
||||||
"spellright.documentTypes": ["markdown", "typescript"],
|
"spellright.documentTypes": ["markdown", "typescript", "typescriptreact"],
|
||||||
"tailwindCSS.experimental.classRegex": [["cva\\(([^)]*)\\)", "[\"'`]([^\"'`]*).*?[\"'`]"]]
|
"tailwindCSS.experimental.classRegex": [["cva\\(([^)]*)\\)", "[\"'`]([^\"'`]*).*?[\"'`]"]]
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
/* eslint-disable */
|
||||||
|
//prettier-ignore
|
||||||
|
module.exports = {
|
||||||
|
name: "@yarnpkg/plugin-workspace-tools",
|
||||||
|
factory: function (require) {
|
||||||
|
var plugin=(()=>{var yr=Object.create;var we=Object.defineProperty;var _r=Object.getOwnPropertyDescriptor;var Er=Object.getOwnPropertyNames;var br=Object.getPrototypeOf,xr=Object.prototype.hasOwnProperty;var W=(e=>typeof require<"u"?require:typeof Proxy<"u"?new Proxy(e,{get:(r,t)=>(typeof require<"u"?require:r)[t]}):e)(function(e){if(typeof require<"u")return require.apply(this,arguments);throw new Error('Dynamic require of "'+e+'" is not supported')});var q=(e,r)=>()=>(r||e((r={exports:{}}).exports,r),r.exports),Cr=(e,r)=>{for(var t in r)we(e,t,{get:r[t],enumerable:!0})},Je=(e,r,t,n)=>{if(r&&typeof r=="object"||typeof r=="function")for(let s of Er(r))!xr.call(e,s)&&s!==t&&we(e,s,{get:()=>r[s],enumerable:!(n=_r(r,s))||n.enumerable});return e};var Be=(e,r,t)=>(t=e!=null?yr(br(e)):{},Je(r||!e||!e.__esModule?we(t,"default",{value:e,enumerable:!0}):t,e)),wr=e=>Je(we({},"__esModule",{value:!0}),e);var ve=q(ee=>{"use strict";ee.isInteger=e=>typeof e=="number"?Number.isInteger(e):typeof e=="string"&&e.trim()!==""?Number.isInteger(Number(e)):!1;ee.find=(e,r)=>e.nodes.find(t=>t.type===r);ee.exceedsLimit=(e,r,t=1,n)=>n===!1||!ee.isInteger(e)||!ee.isInteger(r)?!1:(Number(r)-Number(e))/Number(t)>=n;ee.escapeNode=(e,r=0,t)=>{let n=e.nodes[r];!n||(t&&n.type===t||n.type==="open"||n.type==="close")&&n.escaped!==!0&&(n.value="\\"+n.value,n.escaped=!0)};ee.encloseBrace=e=>e.type!=="brace"?!1:e.commas>>0+e.ranges>>0===0?(e.invalid=!0,!0):!1;ee.isInvalidBrace=e=>e.type!=="brace"?!1:e.invalid===!0||e.dollar?!0:e.commas>>0+e.ranges>>0===0||e.open!==!0||e.close!==!0?(e.invalid=!0,!0):!1;ee.isOpenOrClose=e=>e.type==="open"||e.type==="close"?!0:e.open===!0||e.close===!0;ee.reduce=e=>e.reduce((r,t)=>(t.type==="text"&&r.push(t.value),t.type==="range"&&(t.type="text"),r),[]);ee.flatten=(...e)=>{let r=[],t=n=>{for(let s=0;s<n.length;s++){let i=n[s];Array.isArray(i)?t(i,r):i!==void 0&&r.push(i)}return r};return t(e),r}});var He=q((zn,rt)=>{"use strict";var tt=ve();rt.exports=(e,r={})=>{let t=(n,s={})=>{let i=r.escapeInvalid&&tt.isInvalidBrace(s),a=n.invalid===!0&&r.escapeInvalid===!0,c="";if(n.value)return(i||a)&&tt.isOpenOrClose(n)?"\\"+n.value:n.value;if(n.value)return n.value;if(n.nodes)for(let p of n.nodes)c+=t(p);return c};return t(e)}});var st=q((Vn,nt)=>{"use strict";nt.exports=function(e){return typeof e=="number"?e-e===0:typeof e=="string"&&e.trim()!==""?Number.isFinite?Number.isFinite(+e):isFinite(+e):!1}});var ht=q((Jn,pt)=>{"use strict";var at=st(),le=(e,r,t)=>{if(at(e)===!1)throw new TypeError("toRegexRange: expected the first argument to be a number");if(r===void 0||e===r)return String(e);if(at(r)===!1)throw new TypeError("toRegexRange: expected the second argument to be a number.");let n={relaxZeros:!0,...t};typeof n.strictZeros=="boolean"&&(n.relaxZeros=n.strictZeros===!1);let s=String(n.relaxZeros),i=String(n.shorthand),a=String(n.capture),c=String(n.wrap),p=e+":"+r+"="+s+i+a+c;if(le.cache.hasOwnProperty(p))return le.cache[p].result;let m=Math.min(e,r),h=Math.max(e,r);if(Math.abs(m-h)===1){let y=e+"|"+r;return n.capture?`(${y})`:n.wrap===!1?y:`(?:${y})`}let R=ft(e)||ft(r),f={min:e,max:r,a:m,b:h},$=[],_=[];if(R&&(f.isPadded=R,f.maxLen=String(f.max).length),m<0){let y=h<0?Math.abs(h):1;_=it(y,Math.abs(m),f,n),m=f.a=0}return h>=0&&($=it(m,h,f,n)),f.negatives=_,f.positives=$,f.result=Sr(_,$,n),n.capture===!0?f.result=`(${f.result})`:n.wrap!==!1&&$.length+_.length>1&&(f.result=`(?:${f.result})`),le.cache[p]=f,f.result};function Sr(e,r,t){let n=Pe(e,r,"-",!1,t)||[],s=Pe(r,e,"",!1,t)||[],i=Pe(e,r,"-?",!0,t)||[];return n.concat(i).concat(s).join("|")}function vr(e,r){let t=1,n=1,s=ut(e,t),i=new Set([r]);for(;e<=s&&s<=r;)i.add(s),t+=1,s=ut(e,t);for(s=ct(r+1,n)-1;e<s&&s<=r;)i.add(s),n+=1,s=ct(r+1,n)-1;return i=[...i],i.sort(Tr),i}function Hr(e,r,t){if(e===r)return{pattern:e,count:[],digits:0};let n=$r(e,r),s=n.length,i="",a=0;for(let c=0;c<s;c++){let[p,m]=n[c];p===m?i+=p:p!=="0"||m!=="9"?i+=kr(p,m,t):a++}return a&&(i+=t.shorthand===!0?"\\d":"[0-9]"),{pattern:i,count:[a],digits:s}}function it(e,r,t,n){let s=vr(e,r),i=[],a=e,c;for(let p=0;p<s.length;p++){let m=s[p],h=Hr(String(a),String(m),n),R="";if(!t.isPadded&&c&&c.pattern===h.pattern){c.count.length>1&&c.count.pop(),c.count.push(h.count[0]),c.string=c.pattern+lt(c.count),a=m+1;continue}t.isPadded&&(R=Lr(m,t,n)),h.string=R+h.pattern+lt(h.count),i.push(h),a=m+1,c=h}return i}function Pe(e,r,t,n,s){let i=[];for(let a of e){let{string:c}=a;!n&&!ot(r,"string",c)&&i.push(t+c),n&&ot(r,"string",c)&&i.push(t+c)}return i}function $r(e,r){let t=[];for(let n=0;n<e.length;n++)t.push([e[n],r[n]]);return t}function Tr(e,r){return e>r?1:r>e?-1:0}function ot(e,r,t){return e.some(n=>n[r]===t)}function ut(e,r){return Number(String(e).slice(0,-r)+"9".repeat(r))}function ct(e,r){return e-e%Math.pow(10,r)}function lt(e){let[r=0,t=""]=e;return t||r>1?`{${r+(t?","+t:"")}}`:""}function kr(e,r,t){return`[${e}${r-e===1?"":"-"}${r}]`}function ft(e){return/^-?(0+)\d/.test(e)}function Lr(e,r,t){if(!r.isPaddLine truncated
|
||||||
|
`,CHAR_NO_BREAK_SPACE:"\xA0",CHAR_PERCENT:"%",CHAR_PLUS:"+",CHAR_QUESTION_MARK:"?",CHAR_RIGHT_ANGLE_BRACKET:">",CHAR_RIGHT_CURLY_BRACE:"}",CHAR_RIGHT_SQUARE_BRACKET:"]",CHAR_SEMICOLON:";",CHAR_SINGLE_QUOTE:"'",CHAR_SPACE:" ",CHAR_TAB:" ",CHAR_UNDERSCORE:"_",CHAR_VERTICAL_LINE:"|",CHAR_ZERO_WIDTH_NOBREAK_SPACE:"\uFEFF"}});var Nt=q((ss,Ot)=>{"use strict";var jr=He(),{MAX_LENGTH:Tt,CHAR_BACKSLASH:Ge,CHAR_BACKTICK:Fr,CHAR_COMMA:Qr,CHAR_DOT:Xr,CHAR_LEFT_PARENTHESES:Zr,CHAR_RIGHT_PARENTHESES:Yr,CHAR_LEFT_CURLY_BRACE:zr,CHAR_RIGHT_CURLY_BRACE:Vr,CHAR_LEFT_SQUARE_BRACKET:kt,CHAR_RIGHT_SQUARE_BRACKET:Lt,CHAR_DOUBLE_QUOTE:Jr,CHAR_SINGLE_QUOTE:en,CHAR_NO_BREAK_SPACE:tn,CHAR_ZERO_WIDTH_NOBREAK_SPACE:rn}=$t(),nn=(e,r={})=>{if(typeof e!="string")throw new TypeError("Expected a string");let t=r||{},n=typeof t.maxLength=="number"?Math.min(Tt,t.maxLength):Tt;if(e.length>n)throw new SyntaxError(`Input length (${e.length}), exceeds max characters (${n})`);let s={type:"root",input:e,nodes:[]},i=[s],a=s,c=s,p=0,m=e.length,h=0,R=0,f,$={},_=()=>e[h++],y=E=>{if(E.type==="text"&&c.type==="dot"&&(c.type="text"),c&&c.type==="text"&&E.type==="text"){c.value+=E.value;return}return a.nodes.push(E),E.parent=a,E.prev=c,c=E,E};for(y({type:"bos"});h<m;)if(a=i[i.length-1],f=_(),!(f===rn||f===tn)){if(f===Ge){y({type:"text",value:(r.keepEscaping?f:"")+_()});continue}if(f===Lt){y({type:"text",value:"\\"+f});continue}if(f===kt){p++;let E=!0,S;for(;h<m&&(S=_());){if(f+=S,S===kt){p++;continue}if(S===Ge){f+=_();continue}if(S===Lt&&(p--,p===0))break}y({type:"text",value:f});continue}if(f===Zr){a=y({type:"paren",nodes:[]}),i.push(a),y({type:"text",value:f});continue}if(f===Yr){if(a.type!=="paren"){y({type:"text",value:f});continue}a=i.pop(),y({type:"text",value:f}),a=i[i.length-1];continue}if(f===Jr||f===en||f===Fr){let E=f,S;for(r.keepQuotes!==!0&&(f="");h<m&&(S=_());){if(S===Ge){f+=S+_();continue}if(S===E){r.keepQuotes===!0&&(f+=S);break}f+=S}y({type:"text",value:f});continue}if(f===zr){R++;let E=c.value&&c.value.slice(-1)==="$"||a.dollar===!0;a=y({type:"brace",open:!0,close:!1,dollar:E,depth:R,commas:0,ranges:0,nodes:[]}),i.push(a),y({type:"open",value:f});continue}if(f===Vr){if(a.type!=="brace"){y({type:"text",value:f});continue}let E="close";a=i.pop(),a.close=!0,y({type:E,value:f}),R--,a=i[i.length-1];continue}if(f===Qr&&R>0){if(a.ranges>0){a.ranges=0;let E=a.nodes.shift();a.nodes=[E,{type:"text",value:jr(a)}]}y({type:"comma",value:f}),a.commas++;continue}if(f===Xr&&R>0&&a.commas===0){let E=a.nodes;if(R===0||E.length===0){y({type:"text",value:f});continue}if(c.type==="dot"){if(a.range=[],c.value+=f,c.type="range",a.nodes.length!==3&&a.nodes.length!==5){a.invalid=!0,a.ranges=0,c.type="text";continue}a.ranges++,a.args=[];continue}if(c.type==="range"){E.pop();let S=E[E.length-1];S.value+=c.value+f,c=S,a.ranges--;continue}y({type:"dot",value:f});continue}y({type:"text",value:f})}do if(a=i.pop(),a.type!=="root"){a.nodes.forEach(T=>{T.nodes||(T.type==="open"&&(T.isOpen=!0),T.type==="close"&&(T.isClose=!0),T.nodes||(T.type="text"),T.invalid=!0)});let E=i[i.length-1],S=E.nodes.indexOf(a);E.nodes.splice(S,1,...a.nodes)}while(i.length>0);return y({type:"eos"}),s};Ot.exports=nn});var Pt=q((as,Bt)=>{"use strict";var It=He(),sn=Ct(),an=vt(),on=Nt(),Z=(e,r={})=>{let t=[];if(Array.isArray(e))for(let n of e){let s=Z.create(n,r);Array.isArray(s)?t.push(...s):t.push(s)}else t=[].concat(Z.create(e,r));return r&&r.expand===!0&&r.nodupes===!0&&(t=[...new Set(t)]),t};Z.parse=(e,r={})=>on(e,r);Z.stringify=(e,r={})=>It(typeof e=="string"?Z.parse(e,r):e,r);Z.compile=(e,r={})=>(typeof e=="string"&&(e=Z.parse(e,r)),sn(e,r));Z.expand=(e,r={})=>{typeof e=="string"&&(e=Z.parse(e,r));let t=an(e,r);return r.noempty===!0&&(t=t.filter(Boolean)),r.nodupes===!0&&(t=[...new Set(t)]),t};Z.create=(e,r={})=>e===""||e.length<3?[e]:r.expand!==!0?Z.compile(e,r):Z.expand(e,r);Bt.exports=Z});var me=q((is,qt)=>{"use strict";var un=W("path"),se="\\\\/",Mt=`[^${se}]`,ie="\\.",cn="\\+",ln="\\?",Te="\\/",fn="(?=.)",Dt="[^/]",qe=`(?:${Te}|$)`,Ut=`(?:^|${Te})`,Ke=`${ie}{1,2}${qe}`,pn=`(?!${ie})`,hn=`(?!${Ut}${Ke})`,dn=`(?!${ie}{0,1}${qe})`,gn=`(?!${Ke})`,An=`[^.${Te}]`,mn=`${Dt}*?`,Gt={DOT_LITERAL:ie,PLUS_LITERAL:cn,QMARK_LITERAL:ln,SLASH_LITERAL:Te,ONE_CHAR:fn,QMARK:Dt,END_ANCHOR:qe,DOTS_SLASH:Ke,NO_DOT:pn,NO_DOTS:hn,NO_DOT_SLASH:dn,NO_DOTS_SLASH:gn,QMARK_NO_DOT:An,STAR:mn,START_ANCHOR:Ut},Rn={...Gt,SLASH_LITERAL:`[${se}]`,QMARK:Mt,STAR:`${Mt}*?`,DOTS_SLASH:`${ie}{1,2}(?:[${se}]|$)`,NO_DOT:`(?!${ie})`,NO_DOTS:`(?!(?:^|[${se}])${ie}{1,2}(?:[${se}]|$))`,NO_DOT_SLASH:`(?!${ie}{0,1}(?:[${se}]|$))`,NO_DOTS_SLASH:`(?!${ie}{1,2}(?:[${se}]|$))`,QMARK_NO_DOT:`[^.${se}]`,START_ANCHOR:`(?:^|[${se}])`,END_ANCHOR:`(?:[${se}]|$)`},yn={alnum:"a-zA-Z0-9",alpha:"a-zA-Z",ascii:"\\x00-\\x7F",blank:" \\t",cntrl:"\\x00-\\x1F\\x7F",digit:"0-9",graph:"\\x21-\\x7E",lower:"a-z",print:"\\x20-\\x7E ",punct:"\\-!\"#$%&'()\\*+,./:;<=>?@[\\]^_`{|}~",space:" \\t\\r\\n\\v\\f",upper:"A-Z",word:"A-Za-z0-9_",xdigit:"A-Fa-f0-9"};Line truncated
|
||||||
|
/*!
|
||||||
|
* fill-range <https://github.com/jonschlinkert/fill-range>
|
||||||
|
*
|
||||||
|
* Copyright (c) 2014-present, Jon Schlinkert.
|
||||||
|
* Licensed under the MIT License.
|
||||||
|
*/
|
||||||
|
/*!
|
||||||
|
* is-number <https://github.com/jonschlinkert/is-number>
|
||||||
|
*
|
||||||
|
* Copyright (c) 2014-present, Jon Schlinkert.
|
||||||
|
* Released under the MIT License.
|
||||||
|
*/
|
||||||
|
/*!
|
||||||
|
* to-regex-range <https://github.com/micromatch/to-regex-range>
|
||||||
|
*
|
||||||
|
* Copyright (c) 2015-present, Jon Schlinkert.
|
||||||
|
* Released under the MIT License.
|
||||||
|
*/
|
||||||
|
return plugin;
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -3,5 +3,7 @@ nodeLinker: node-modules
|
|||||||
plugins:
|
plugins:
|
||||||
- path: .yarn/plugins/@yarnpkg/plugin-interactive-tools.cjs
|
- path: .yarn/plugins/@yarnpkg/plugin-interactive-tools.cjs
|
||||||
spec: "@yarnpkg/plugin-interactive-tools"
|
spec: "@yarnpkg/plugin-interactive-tools"
|
||||||
|
- path: .yarn/plugins/@yarnpkg/plugin-workspace-tools.cjs
|
||||||
|
spec: "@yarnpkg/plugin-workspace-tools"
|
||||||
|
|
||||||
yarnPath: .yarn/releases/yarn-3.4.1.cjs
|
yarnPath: .yarn/releases/yarn-3.4.1.cjs
|
||||||
+6
-1
@@ -86,7 +86,12 @@ To develop locally:
|
|||||||
yarn
|
yarn
|
||||||
```
|
```
|
||||||
|
|
||||||
5. Start developing and watch for code changes:
|
5. Set up your `.env` file:
|
||||||
|
|
||||||
|
- Use `openssl rand -base64 32` to generate a key and add it under `NEXTAUTH_SECRET` in the `.env` file.
|
||||||
|
- Use `openssl rand -base64 24` to generate a key and add it under `CALENDSO_ENCRYPTION_KEY` in the `.env` file.
|
||||||
|
|
||||||
|
6. Start developing and watch for code changes:
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
yarn dev
|
yarn dev
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ Copyright (c) 2020-present Cal.com, Inc.
|
|||||||
|
|
||||||
Portions of this software are licensed as follows:
|
Portions of this software are licensed as follows:
|
||||||
|
|
||||||
* All content that resides under https://github.com/calcom/cal.com/tree/main/packages/features/ee directory of this repository (Enterprise Edition) is licensed under the license defined in "ee/LICENSE".
|
* All content that resides under https://github.com/calcom/cal.com/tree/main/packages/features/ee directory of this repository (Commercial License) is licensed under the license defined in "ee/LICENSE".
|
||||||
* All third party components incorporated into the Cal.com Software are licensed under the original license provided by the owner of the applicable component.
|
* All third party components incorporated into the Cal.com Software are licensed under the original license provided by the owner of the applicable component.
|
||||||
* Content outside of the above mentioned directories or restrictions above is available under the "AGPLv3" license as defined below.
|
* Content outside of the above mentioned directories or restrictions above is available under the "AGPLv3" license as defined below.
|
||||||
|
|
||||||
|
|||||||
@@ -24,7 +24,7 @@
|
|||||||
|
|
||||||
<p align="center">
|
<p align="center">
|
||||||
<a href="https://cal.com/slack"><img src="https://img.shields.io/badge/Slack-calendso.slack.com-%234A154B" alt="Join Cal.com Slack"></a>
|
<a href="https://cal.com/slack"><img src="https://img.shields.io/badge/Slack-calendso.slack.com-%234A154B" alt="Join Cal.com Slack"></a>
|
||||||
<a href="https://www.producthunt.com/posts/calendso"><img src="https://img.shields.io/badge/Product%20Hunt-%231%20Product%20of%20the%20Month-%23DA552E" alt="Product Hunt"></a>
|
<a href="https://producthunt.com/posts/calendso"><img src="https://img.shields.io/badge/Product%20Hunt-%231%20Product%20of%20the%20Month-%23DA552E" alt="Product Hunt"></a>
|
||||||
<a href="https://status.cal.com"><img height="20px" src="https://betteruptime.com/status-badges/v1/monitor/a9kf.svg" alt="Uptime"></a>
|
<a href="https://status.cal.com"><img height="20px" src="https://betteruptime.com/status-badges/v1/monitor/a9kf.svg" alt="Uptime"></a>
|
||||||
<a href="https://github.com/calcom/cal.com/stargazers"><img src="https://img.shields.io/github/stars/calcom/cal.com" alt="Github Stars"></a>
|
<a href="https://github.com/calcom/cal.com/stargazers"><img src="https://img.shields.io/github/stars/calcom/cal.com" alt="Github Stars"></a>
|
||||||
<a href="https://news.ycombinator.com/item?id=34507672"><img src="https://img.shields.io/badge/Hacker%20News-%231-%23FF6600" alt="Hacker News"></a>
|
<a href="https://news.ycombinator.com/item?id=34507672"><img src="https://img.shields.io/badge/Hacker%20News-%231-%23FF6600" alt="Hacker News"></a>
|
||||||
@@ -38,13 +38,13 @@
|
|||||||
<a href="https://github.com/calcom/cal.com/issues?q=is:issue+is:open+label:%22%F0%9F%99%8B%F0%9F%8F%BB%E2%80%8D%E2%99%82%EF%B8%8Fhelp+wanted%22"><img src="https://img.shields.io/badge/Help%20Wanted-Contribute-blue"></a>
|
<a href="https://github.com/calcom/cal.com/issues?q=is:issue+is:open+label:%22%F0%9F%99%8B%F0%9F%8F%BB%E2%80%8D%E2%99%82%EF%B8%8Fhelp+wanted%22"><img src="https://img.shields.io/badge/Help%20Wanted-Contribute-blue"></a>
|
||||||
<a href="https://cal.com/figma"><img src="https://img.shields.io/badge/Figma-Design%20System-blueviolet"></a>
|
<a href="https://cal.com/figma"><img src="https://img.shields.io/badge/Figma-Design%20System-blueviolet"></a>
|
||||||
<a href="https://calendso.slack.com/archives/C02BY67GMMW"><img src="https://img.shields.io/badge/translations-contribute-brightgreen" /></a>
|
<a href="https://calendso.slack.com/archives/C02BY67GMMW"><img src="https://img.shields.io/badge/translations-contribute-brightgreen" /></a>
|
||||||
<a href="https://www.contributor-covenant.org/version/1/4/code-of-conduct/ "><img src="https://img.shields.io/badge/Contributor%20Covenant-1.4-purple" /></a>
|
<a href="https://contributor-covenant.org/version/1/4/code-of-conduct/ "><img src="https://img.shields.io/badge/Contributor%20Covenant-1.4-purple" /></a>
|
||||||
<a href="https://console.algora.io/org/cal/bounties?status=open"><img src="https://img.shields.io/endpoint?url=https%3A%2F%2Fconsole.algora.io%2Fapi%2Fshields%2Fcal%2Fbounties%3Fstatus%3Dopen" /></a>
|
<a href="https://console.algora.io/org/cal/bounties?status=open"><img src="https://img.shields.io/endpoint?url=https%3A%2F%2Fconsole.algora.io%2Fapi%2Fshields%2Fcal%2Fbounties%3Fstatus%3Dopen" /></a>
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<!-- ABOUT THE PROJECT -->
|
<!-- ABOUT THE PROJECT -->
|
||||||
|
|
||||||
## About The Project
|
## About the Project
|
||||||
|
|
||||||
<img width="100%" alt="booking-screen" src="https://user-images.githubusercontent.com/8019099/176390354-f1bc7069-0341-437a-9bb8-eb41092b4016.gif">
|
<img width="100%" alt="booking-screen" src="https://user-images.githubusercontent.com/8019099/176390354-f1bc7069-0341-437a-9bb8-eb41092b4016.gif">
|
||||||
|
|
||||||
@@ -53,13 +53,13 @@
|
|||||||
The open source Calendly alternative. You are in charge
|
The open source Calendly alternative. You are in charge
|
||||||
of your own data, workflow and appearance.
|
of your own data, workflow and appearance.
|
||||||
|
|
||||||
Calendly and other scheduling tools are awesome. It made our lives massively easier. We're using it for business meetings, seminars, yoga classes and even calls with our families. However, most tools are very limited in terms of control and customisations.
|
Calendly and other scheduling tools are awesome. It made our lives massively easier. We're using it for business meetings, seminars, yoga classes and even calls with our families. However, most tools are very limited in terms of control and customizations.
|
||||||
|
|
||||||
That's where Cal.com comes in. Self-hosted or hosted by us. White-label by design. API-driven and ready to be deployed on your own domain. Full control of your events and data.
|
That's where Cal.com comes in. Self-hosted or hosted by us. White-label by design. API-driven and ready to be deployed on your own domain. Full control of your events and data.
|
||||||
|
|
||||||
## Recognition
|
## Recognition
|
||||||
|
|
||||||
#### Hacker News
|
#### [Hacker News](https://news.ycombinator.com/from?site=cal.com)
|
||||||
|
|
||||||
<a href="https://news.ycombinator.com/item?id=34507672">
|
<a href="https://news.ycombinator.com/item?id=34507672">
|
||||||
<img
|
<img
|
||||||
@@ -77,22 +77,22 @@ That's where Cal.com comes in. Self-hosted or hosted by us. White-label by desig
|
|||||||
/>
|
/>
|
||||||
</a>
|
</a>
|
||||||
|
|
||||||
#### [Product Hunt](https://www.producthunt.com/posts/calendso?utm_source=badge-top-post-badge&utm_medium=badge&utm_souce=badge-calendso)
|
#### [Product Hunt](https://producthunt.com/products/cal-com?utm_source=badge-top-post-badge&utm_medium=badge)
|
||||||
|
|
||||||
<a href="https://www.producthunt.com/posts/calendso?utm_source=badge-top-post-badge&utm_medium=badge&utm_souce=badge-calendso" target="_blank"><img src="https://api.producthunt.com/widgets/embed-image/v1/top-post-badge.svg?post_id=291910&theme=light&period=monthly" alt="Cal.com - The open source Calendly alternative | Product Hunt" style="width: 250px; height: 54px;" width="250" height="54" /></a> <a href="https://www.producthunt.com/posts/calendso?utm_source=badge-featured&utm_medium=badge&utm_souce=badge-calendso" target="_blank"><img src="https://api.producthunt.com/widgets/embed-image/v1/featured.svg?post_id=291910&theme=light" alt="Cal.com - The open source Calendly alternative | Product Hunt" style="width: 250px; height: 54px;" width="250" height="54" /></a> <a href="https://www.producthunt.com/stories/how-this-open-source-calendly-alternative-rocketed-to-product-of-the-day" target="_blank"><img src="https://cal.com/maker-grant.svg" alt="Cal.com - The open source Calendly alternative | Product Hunt" style="width: 250px; height: 54px;" width="250" height="54" /></a>
|
<a href="https://producthunt.com/posts/calendso?utm_source=badge-top-post-badge&utm_medium=badge&utm_souce=badge-calendso" target="_blank"><img src="https://api.producthunt.com/widgets/embed-image/v1/top-post-badge.svg?post_id=291910&theme=light&period=monthly" alt="Cal.com - The open source Calendly alternative | Product Hunt" style="width: 250px; height: 54px;" width="250" height="54" /></a> <a href="https://producthunt.com/posts/calendso?utm_source=badge-featured&utm_medium=badge&utm_souce=badge-calendso" target="_blank"><img src="https://api.producthunt.com/widgets/embed-image/v1/featured.svg?post_id=291910&theme=light" alt="Cal.com - The open source Calendly alternative | Product Hunt" style="width: 250px; height: 54px;" width="250" height="54" /></a> <a href="https://producthunt.com/stories/how-this-open-source-calendly-alternative-rocketed-to-product-of-the-day" target="_blank"><img src="https://cal.com/maker-grant.svg" alt="Cal.com - The open source Calendly alternative | Product Hunt" style="width: 250px; height: 54px;" width="250" height="54" /></a>
|
||||||
|
|
||||||
### Built With
|
### Built With
|
||||||
|
|
||||||
- [Next.js](https://nextjs.org/?ref=cal.com)
|
- [Next.js](https://nextjs.org/?ref=cal.com)
|
||||||
- [tRPC](https://trpc.io/?ref=cal.com)
|
- [tRPC](https://trpc.io/?ref=cal.com)
|
||||||
- [React](https://reactjs.org/?ref=cal.com)
|
- [React.js](https://reactjs.org/?ref=cal.com)
|
||||||
- [Tailwind](https://tailwindcss.com/?ref=cal.com)
|
- [Tailwind CSS](https://tailwindcss.com/?ref=cal.com)
|
||||||
- [Prisma](https://prisma.io/?ref=cal.com)
|
- [Prisma.io](https://prisma.io/?ref=cal.com)
|
||||||
- [Daily.co](https://go.cal.com/daily)
|
- [Daily.co](https://go.cal.com/daily)
|
||||||
|
|
||||||
## Stay Up-to-Date
|
## Stay Up-to-Date
|
||||||
|
|
||||||
Cal officially launched as v.1.0 on 15th of September, however a lot of new features are coming. Watch **releases** of this repository to be notified for future updates:
|
Cal.com officially launched as v.1.0 on 15th of September, however a lot of new features are coming. Watch **releases** of this repository to be notified for future updates:
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
@@ -104,7 +104,7 @@ To get a local copy up and running, please follow these simple steps.
|
|||||||
|
|
||||||
### Prerequisites
|
### Prerequisites
|
||||||
|
|
||||||
Here is what you need to be able to run Cal.
|
Here is what you need to be able to run Cal.com.
|
||||||
|
|
||||||
- Node.js (Version: >=15.x <17)
|
- Node.js (Version: >=15.x <17)
|
||||||
- PostgreSQL
|
- PostgreSQL
|
||||||
@@ -121,6 +121,9 @@ Here is what you need to be able to run Cal.
|
|||||||
```sh
|
```sh
|
||||||
git clone https://github.com/calcom/cal.com.git
|
git clone https://github.com/calcom/cal.com.git
|
||||||
```
|
```
|
||||||
|
> If you are on windows, run the following command on `gitbash` with admin privileges: <br>
|
||||||
|
```git clone -c core.symlinks=true https://github.com/calcom/cal.com.git``` <br>
|
||||||
|
See [docs](https://cal.com/docs/how-to-guides/how-to-troubleshoot-symbolic-link-issues-on-windows#enable-symbolic-links) for more details.
|
||||||
|
|
||||||
1. Go to the project folder
|
1. Go to the project folder
|
||||||
|
|
||||||
@@ -134,10 +137,10 @@ Here is what you need to be able to run Cal.
|
|||||||
yarn
|
yarn
|
||||||
```
|
```
|
||||||
|
|
||||||
1. Set up your .env file
|
1. Set up your `.env` file
|
||||||
- Duplicate `.env.example` to `.env`
|
- Duplicate `.env.example` to `.env`
|
||||||
- Use `openssl rand -base64 32` to generate a key and add it under `NEXTAUTH_SECRET` in the .env file.
|
- Use `openssl rand -base64 32` to generate a key and add it under `NEXTAUTH_SECRET` in the `.env` file.
|
||||||
- Use `openssl rand -base64 24` to generate a key and add it under `CALENDSO_ENCRYPTION_KEY` in the .env file.
|
- Use `openssl rand -base64 24` to generate a key and add it under `CALENDSO_ENCRYPTION_KEY` in the `.env` file.
|
||||||
|
|
||||||
#### Quick start with `yarn dx`
|
#### Quick start with `yarn dx`
|
||||||
|
|
||||||
@@ -150,53 +153,63 @@ yarn dx
|
|||||||
|
|
||||||
#### Development tip
|
#### Development tip
|
||||||
|
|
||||||
> Add `NEXT_PUBLIC_DEBUG=1` anywhere in your `.env` to get logging information for all the queries and mutations driven by **trpc**.
|
> Add `NEXT_PUBLIC_DEBUG=1` anywhere in your `.env` to get logging information for all the queries and mutations driven by **tRPC**.
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
echo 'NEXT_PUBLIC_DEBUG=1' >> .env
|
echo 'NEXT_PUBLIC_DEBUG=1' >> .env
|
||||||
```
|
```
|
||||||
|
#### Gitpod Setup
|
||||||
|
|
||||||
|
1. Click the button below to open this project in Gitpod.
|
||||||
|
|
||||||
|
2. This will open a fully configured workspace in your browser with all the necessary dependencies already installed.
|
||||||
|
|
||||||
|
[](https://gitpod.io/#https://github.com/calcom/cal.com)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
#### Manual setup
|
#### Manual setup
|
||||||
|
|
||||||
1. Configure environment variables in the `.env` file. Replace `<user>`, `<pass>`, `<db-host>`, `<db-port>` with their applicable values
|
1. Configure environment variables in the `.env` file. Replace `<user>`, `<pass>`, `<db-host>`, and `<db-port>` with their applicable values
|
||||||
|
|
||||||
```
|
```
|
||||||
DATABASE_URL='postgresql://<user>:<pass>@<db-host>:<db-port>'
|
DATABASE_URL='postgresql://<user>:<pass>@<db-host>:<db-port>'
|
||||||
```
|
```
|
||||||
|
|
||||||
<details>
|
<details>
|
||||||
<summary>If you don't know how to configure the DATABASE_URL, then follow the steps here to create a quick DB using Heroku</summary>
|
<summary>If you don't know how to configure the DATABASE_URL, then follow the steps here to create a quick local DB</summary>
|
||||||
|
|
||||||
1. Create a free account with [Heroku](https://www.heroku.com/).
|
1. [Download](https://www.postgresql.org/download/) and install postgres in your local (if you don't have it already).
|
||||||
|
|
||||||
2. Create a new app.
|
2. Create your own local db by executing `createDB <DB name>`
|
||||||
<img width="306" alt="Create an App" src="https://user-images.githubusercontent.com/16905768/115322780-b3d58c00-a17e-11eb-8a52-b758fb0ea942.png">
|
|
||||||
|
|
||||||
3. In your new app, go to `Overview` and next to `Installed add-ons`, click `Configure Add-ons`. We need this to set up our database.
|
3. Now open your psql shell with the DB you created: `psql -h localhost -U postgres -d <DB name>`
|
||||||

|
|
||||||
|
|
||||||
4. Once you clicked on `Configure Add-ons`, click on `Find more add-ons` and search for `postgres`. One of the options will be `Heroku Postgres` - click on that option.
|
4. Inside the psql shell execute `\conninfo`. And you will get the following info.
|
||||||

|

|
||||||
|
|
||||||
5. Once the pop-up appears, click `Submit Order Form` - plan name should be `Hobby Dev - Free`.
|
5. Now extract all the info and add it to your DATABASE_URL. The url would look something like this
|
||||||
<img width="512" alt="Submit Order Form" src="https://user-images.githubusercontent.com/16905768/115323265-b4baed80-a17f-11eb-99f0-d67f019aa6df.png">
|
`postgresql://postgres:postgres@localhost:5432/Your-DB-Name`.
|
||||||
|
|
||||||
6. Once you completed the above steps, click on your newly created `Heroku Postgres` and go to its `Settings`.
|
|
||||||

|
|
||||||
|
|
||||||
7. In `Settings`, copy your URI to your Cal.com .env file and replace the `postgresql://<user>:<pass>@<db-host>:<db-port>` with it.
|
|
||||||

|
|
||||||

|
|
||||||
|
|
||||||
8. To view your DB, once you add new data in Prisma, you can use [Heroku Data Explorer](https://heroku-data-explorer.herokuapp.com/).
|
|
||||||
</details>
|
</details>
|
||||||
|
|
||||||
1. Set a 32 character random string in your .env file for the `CALENDSO_ENCRYPTION_KEY` (You can use a command like `openssl rand -base64 24` to generate one).
|
If you don't want to create a local DB. Then you can also consider using services like railway.app or render.
|
||||||
|
- [Setup postgres DB with railway.app](https://arctype.com/postgres/setup/railway-postgres)
|
||||||
|
- [Setup postgres DB with render](https://render.com/docs/databases)
|
||||||
|
|
||||||
|
1. Copy and paste your `DATABASE_URL` from `.env` to `.env.appStore`.
|
||||||
|
|
||||||
|
1. Set a 32 character random string in your `.env` file for the `CALENDSO_ENCRYPTION_KEY` (You can use a command like `openssl rand -base64 24` to generate one).
|
||||||
1. Set up the database using the Prisma schema (found in `packages/prisma/schema.prisma`)
|
1. Set up the database using the Prisma schema (found in `packages/prisma/schema.prisma`)
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
yarn workspace @calcom/prisma db-deploy
|
yarn workspace @calcom/prisma db-deploy
|
||||||
```
|
```
|
||||||
|
1. Run [mailhog](https://github.com/mailhog/MailHog) to view emails sent during development
|
||||||
|
|
||||||
|
```sh
|
||||||
|
docker pull mailhog/mailhog
|
||||||
|
docker run -d -p 8025:8025 -p 1025:1025 mailhog/mailhog
|
||||||
|
```
|
||||||
|
|
||||||
1. Run (in development mode)
|
1. Run (in development mode)
|
||||||
|
|
||||||
@@ -206,7 +219,7 @@ echo 'NEXT_PUBLIC_DEBUG=1' >> .env
|
|||||||
|
|
||||||
#### Setting up your first user
|
#### Setting up your first user
|
||||||
|
|
||||||
1. Open [Prisma Studio](https://www.prisma.io/studio) to look at or modify the database content:
|
1. Open [Prisma Studio](https://prisma.io/studio) to look at or modify the database content:
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
yarn db-studio
|
yarn db-studio
|
||||||
@@ -251,7 +264,7 @@ yarn playwright show-report test-results/reports/playwright-html-report
|
|||||||
yarn workspace @calcom/prisma db-migrate
|
yarn workspace @calcom/prisma db-migrate
|
||||||
```
|
```
|
||||||
|
|
||||||
(this can clear your development database in some cases)
|
(This can clear your development database in some cases)
|
||||||
|
|
||||||
In a production environment, run:
|
In a production environment, run:
|
||||||
|
|
||||||
@@ -285,7 +298,7 @@ yarn playwright show-report test-results/reports/playwright-html-report
|
|||||||
|
|
||||||
### Docker
|
### Docker
|
||||||
|
|
||||||
The Docker configuration for Cal is an effort powered by people within the community.
|
The Docker configuration for Cal.com is an effort powered by people within the community.
|
||||||
|
|
||||||
If you want to contribute to the Docker repository, [reply here](https://github.com/calcom/docker/discussions/32).
|
If you want to contribute to the Docker repository, [reply here](https://github.com/calcom/docker/discussions/32).
|
||||||
|
|
||||||
@@ -298,14 +311,14 @@ Cal.com, Inc. does not provide official support for Docker, but we will accept f
|
|||||||
### Heroku
|
### Heroku
|
||||||
|
|
||||||
<a href="https://heroku.com/deploy?template=https://github.com/calcom/cal.com">
|
<a href="https://heroku.com/deploy?template=https://github.com/calcom/cal.com">
|
||||||
<img width="185px" height="auto" src="https://www.herokucdn.com/deploy/button.svg" alt="Deploy">
|
<img width="185px" height="auto" src="https://herokucdn.com/deploy/button.svg" alt="Deploy">
|
||||||
</a>
|
</a>
|
||||||
|
|
||||||
### Railway
|
### Railway
|
||||||
|
|
||||||
[](https://railway.app/new/template/cal)
|
[](https://railway.app/new/template/cal)
|
||||||
|
|
||||||
You can deploy Cal on [Railway](https://railway.app/) using the button above. The team at Railway also have a [detailed blog post](https://blog.railway.app/p/calendso) on deploying Cal on their platform.
|
You can deploy Cal.com on [Railway](https://railway.app) using the button above. The team at Railway also have a [detailed blog post](https://blog.railway.app/p/calendso) on deploying Cal.com on their platform.
|
||||||
|
|
||||||
### Vercel
|
### Vercel
|
||||||
|
|
||||||
@@ -352,7 +365,6 @@ We have a list of [help wanted](https://github.com/calcom/cal.com/issues?q=is:is
|
|||||||
</picture>
|
</picture>
|
||||||
</a>
|
</a>
|
||||||
|
|
||||||
|
|
||||||
<!-- CONTRIBUTORS -->
|
<!-- CONTRIBUTORS -->
|
||||||
|
|
||||||
### Contributors
|
### Contributors
|
||||||
@@ -387,7 +399,7 @@ Don't code but still want to contribute? Join our [slack](https://cal.com/slack)
|
|||||||
8. Select Web Application as the Application Type.
|
8. Select Web Application as the Application Type.
|
||||||
9. Under Authorized redirect URI's, select Add URI and then add the URI `<Cal.com URL>/api/integrations/googlecalendar/callback` and `<Cal.com URL>/api/auth/callback/google` replacing Cal.com URL with the URI at which your application runs.
|
9. Under Authorized redirect URI's, select Add URI and then add the URI `<Cal.com URL>/api/integrations/googlecalendar/callback` and `<Cal.com URL>/api/auth/callback/google` replacing Cal.com URL with the URI at which your application runs.
|
||||||
10. The key will be created and you will be redirected back to the Credentials page. Select the newly generated client ID under OAuth 2.0 Client IDs.
|
10. The key will be created and you will be redirected back to the Credentials page. Select the newly generated client ID under OAuth 2.0 Client IDs.
|
||||||
11. Select Download JSON. Copy the contents of this file and paste the entire JSON string in the .env file as the value for GOOGLE_API_CREDENTIALS key.
|
11. Select Download JSON. Copy the contents of this file and paste the entire JSON string in the `.env` file as the value for `GOOGLE_API_CREDENTIALS` key.
|
||||||
|
|
||||||
#### _Adding google calendar to Cal.com App Store_
|
#### _Adding google calendar to Cal.com App Store_
|
||||||
|
|
||||||
@@ -424,7 +436,7 @@ following
|
|||||||
5. Choose "User-managed app" as the app type.
|
5. Choose "User-managed app" as the app type.
|
||||||
6. De-select the option to publish the app on the Zoom App Marketplace.
|
6. De-select the option to publish the app on the Zoom App Marketplace.
|
||||||
7. Click "Create".
|
7. Click "Create".
|
||||||
8. Now copy the Client ID and Client Secret to your .env file into the `ZOOM_CLIENT_ID` and `ZOOM_CLIENT_SECRET` fields.
|
8. Now copy the Client ID and Client Secret to your `.env` file into the `ZOOM_CLIENT_ID` and `ZOOM_CLIENT_SECRET` fields.
|
||||||
9. Set the Redirect URL for OAuth `<Cal.com URL>/api/integrations/zoomvideo/callback` replacing Cal.com URL with the URI at which your application runs.
|
9. Set the Redirect URL for OAuth `<Cal.com URL>/api/integrations/zoomvideo/callback` replacing Cal.com URL with the URI at which your application runs.
|
||||||
10. Also add the redirect URL given above as a allow list URL and enable "Subdomain check". Make sure, it says "saved" below the form.
|
10. Also add the redirect URL given above as a allow list URL and enable "Subdomain check". Make sure, it says "saved" below the form.
|
||||||
11. You don't need to provide basic information about your app. Instead click at "Scopes" and then at "+ Add Scopes". On the left, click the category "Meeting" and check the scope `meeting:write`.
|
11. You don't need to provide basic information about your app. Instead click at "Scopes" and then at "+ Add Scopes". On the left, click the category "Meeting" and check the scope `meeting:write`.
|
||||||
@@ -436,8 +448,8 @@ following
|
|||||||
1. Visit our [Daily.co Partnership Form](https://go.cal.com/daily) and enter your information
|
1. Visit our [Daily.co Partnership Form](https://go.cal.com/daily) and enter your information
|
||||||
2. From within your dashboard, go to the [developers](https://dashboard.daily.co/developers) tab.
|
2. From within your dashboard, go to the [developers](https://dashboard.daily.co/developers) tab.
|
||||||
3. Copy your API key.
|
3. Copy your API key.
|
||||||
4. Now paste the API key to your .env file into the `DAILY_API_KEY` field in your .env file.
|
4. Now paste the API key to your `.env` file into the `DAILY_API_KEY` field in your `.env` file.
|
||||||
5. If you have the [Daily Scale Plan](https://www.daily.co/pricing) set the `DAILY_SCALE_PLAN` variable to `true` in order to use features like video recording.
|
5. If you have the [Daily Scale Plan](https://daily.co/pricing) set the `DAILY_SCALE_PLAN` variable to `true` in order to use features like video recording.
|
||||||
|
|
||||||
### Obtaining HubSpot Client ID and Secret
|
### Obtaining HubSpot Client ID and Secret
|
||||||
|
|
||||||
@@ -446,12 +458,16 @@ following
|
|||||||
3. Click "Create app" button top right.
|
3. Click "Create app" button top right.
|
||||||
4. Fill in any information you want in the "App info" tab
|
4. Fill in any information you want in the "App info" tab
|
||||||
5. Go to tab "Auth"
|
5. Go to tab "Auth"
|
||||||
6. Now copy the Client ID and Client Secret to your .env file into the `HUBSPOT_CLIENT_ID` and `HUBSPOT_CLIENT_SECRET` fields.
|
6. Now copy the Client ID and Client Secret to your `.env` file into the `HUBSPOT_CLIENT_ID` and `HUBSPOT_CLIENT_SECRET` fields.
|
||||||
7. Set the Redirect URL for OAuth `<Cal.com URL>/api/integrations/hubspot/callback` replacing Cal.com URL with the URI at which your application runs.
|
7. Set the Redirect URL for OAuth `<Cal.com URL>/api/integrations/hubspot/callback` replacing Cal.com URL with the URI at which your application runs.
|
||||||
8. In the "Scopes" section at the bottom of the page, make sure you select "Read" and "Write" for scope called `crm.objects.contacts`
|
8. In the "Scopes" section at the bottom of the page, make sure you select "Read" and "Write" for scope called `crm.objects.contacts`
|
||||||
9. Click the "Save" button at the bottom footer.
|
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.
|
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 Webex Client ID and Secret
|
||||||
|
|
||||||
|
[See Webex Readme](./packages/app-store/webex/)
|
||||||
|
|
||||||
### Obtaining ZohoCRM Client ID and Secret
|
### Obtaining ZohoCRM Client ID and Secret
|
||||||
|
|
||||||
1. Open [Zoho API Console](https://api-console.zoho.com/) and sign into your account, or create a new one.
|
1. Open [Zoho API Console](https://api-console.zoho.com/) and sign into your account, or create a new one.
|
||||||
@@ -459,31 +475,33 @@ following
|
|||||||
3. Click "ADD CLIENT" button top right and select "Server-based Applications".
|
3. Click "ADD CLIENT" button top right and select "Server-based Applications".
|
||||||
4. Fill in any information you want in the "Client Details" tab
|
4. Fill in any information you want in the "Client Details" tab
|
||||||
5. Go to tab "Client Secret" tab.
|
5. Go to tab "Client Secret" tab.
|
||||||
6. Now copy the Client ID and Client Secret to your .env file into the `ZOHOCRM_CLIENT_ID` and `ZOHOCRM_CLIENT_SECRET` fields.
|
6. Now copy the Client ID and Client Secret to your `.env` file into the `ZOHOCRM_CLIENT_ID` and `ZOHOCRM_CLIENT_SECRET` fields.
|
||||||
7. Set the Redirect URL for OAuth `<Cal.com URL>/api/integrations/zohocrm/callback` replacing Cal.com URL with the URI at which your application runs.
|
7. Set the Redirect URL for OAuth `<Cal.com URL>/api/integrations/zohocrm/callback` replacing Cal.com URL with the URI at which your application runs.
|
||||||
8. In the "Settings" section check the "Multi-DC" option if you wish to use the same OAuth credentials for all data centers.
|
8. In the "Settings" section check the "Multi-DC" option if you wish to use the same OAuth credentials for all data centers.
|
||||||
9. Click the "Save"/ "UPDATE" button at the bottom footer.
|
9. Click the "Save"/ "UPDATE" button at the bottom footer.
|
||||||
10. You're good to go. Now you can easily add your ZohoCRM integration in the Cal.com settings.
|
10. You're good to go. Now you can easily add your ZohoCRM integration in the Cal.com settings.
|
||||||
|
|
||||||
|
### Obtaining Zoho Bigin Client ID and Secret
|
||||||
|
[Follow these steps](./packages/app-store/zoho-bigin/)
|
||||||
## Workflows
|
## Workflows
|
||||||
|
|
||||||
### Setting up SendGrid for Email reminders
|
### Setting up SendGrid for Email reminders
|
||||||
|
|
||||||
1. Create a SendGrid account (https://signup.sendgrid.com/)
|
1. Create a SendGrid account (https://signup.sendgrid.com/)
|
||||||
2. Go to Settings -> API keys and create an API key
|
2. Go to Settings -> API keys and create an API key
|
||||||
3. Copy API key to your .env file into the SENDGRID_API_KEY field
|
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
|
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
|
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)
|
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
|
### Setting up Twilio for SMS reminders
|
||||||
|
|
||||||
1. Create a Twilio account (https://www.twilio.com/try-twilio)
|
1. Create a Twilio account (https://twilio.com/try-twilio)
|
||||||
2. Click ‘Get a Twilio phone number’
|
2. Click ‘Get a Twilio phone number’
|
||||||
3. Copy Account SID to your .env file into the TWILIO_SID field
|
3. Copy Account SID to your `.env` file into the `TWILIO_SID` field
|
||||||
4. Copy Auth Token to your .env file into the TWILIO_TOKEN field
|
4. Copy Auth Token to your `.env` file into the `TWILIO_TOKEN` field
|
||||||
5. Copy your Twilio phone number to your .env file into the TWILIO_PHONE_NUMBER field
|
5. Copy your Twilio phone number to your `.env` file into the `TWILIO_PHONE_NUMBER` field
|
||||||
6. Add your own sender id to the .env file into the NEXT_PUBLIC_SENDER_ID field (fallback is Cal)
|
6. Add your own sender id to the `.env` file into the `NEXT_PUBLIC_SENDER_ID` field (fallback is Cal.com)
|
||||||
7. Create a messaging service (Develop -> Messaging -> Services)
|
7. Create a messaging service (Develop -> Messaging -> Services)
|
||||||
8. Choose any name for the messaging service
|
8. Choose any name for the messaging service
|
||||||
9. Click 'Add Senders'
|
9. Click 'Add Senders'
|
||||||
@@ -491,15 +509,15 @@ following
|
|||||||
11. Add the listed phone number
|
11. Add the listed phone number
|
||||||
12. Leave all other fields as they are
|
12. Leave all other fields as they are
|
||||||
13. Complete setup and click ‘View my new Messaging Service’
|
13. Complete setup and click ‘View my new Messaging Service’
|
||||||
14. Copy Messaging Service SID to your .env file into the TWILIO_MESSAGING_SID field
|
14. Copy Messaging Service SID to your `.env` file into the `TWILIO_MESSAGING_SID` field
|
||||||
15. Create a verify service
|
15. Create a verify service
|
||||||
16. Copy Verify Service SID to your .env file into the TWILIO_VERIFY_SID field
|
16. Copy Verify Service SID to your `.env` file into the `TWILIO_VERIFY_SID` field
|
||||||
|
|
||||||
<!-- LICENSE -->
|
<!-- LICENSE -->
|
||||||
|
|
||||||
## License
|
## License
|
||||||
|
|
||||||
Distributed under the AGPLv3 License. See `LICENSE` for more information.
|
Distributed under the [AGPLv3 License](https://github.com/calcom/cal.com/blob/main/LICENSE). See `LICENSE` for more information.
|
||||||
|
|
||||||
<!-- ACKNOWLEDGEMENTS -->
|
<!-- ACKNOWLEDGEMENTS -->
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "@calcom/docs",
|
"name": "@calcom/docs",
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
|
"private": true,
|
||||||
"description": "",
|
"description": "",
|
||||||
"main": "index.js",
|
"main": "index.js",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
@@ -11,7 +12,8 @@
|
|||||||
"lint:fix": "eslint . --ext .ts,.js,.tsx,.jsx --fix",
|
"lint:fix": "eslint . --ext .ts,.js,.tsx,.jsx --fix",
|
||||||
"lint:report": "eslint . --format json --output-file ../../lint-results/docs.json",
|
"lint:report": "eslint . --format json --output-file ../../lint-results/docs.json",
|
||||||
"start": "PORT=4000 next start",
|
"start": "PORT=4000 next start",
|
||||||
"type-check": "tsc --pretty --noEmit"
|
"type-check": "tsc --pretty --noEmit",
|
||||||
|
"type-check:ci": "tsc-absolute --pretty --noEmit"
|
||||||
},
|
},
|
||||||
"author": "Cal.com, Inc.",
|
"author": "Cal.com, Inc.",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ module.exports = {
|
|||||||
stories: [
|
stories: [
|
||||||
"../intro.stories.mdx",
|
"../intro.stories.mdx",
|
||||||
"../../../packages/ui/components/**/*.stories.mdx",
|
"../../../packages/ui/components/**/*.stories.mdx",
|
||||||
|
"../../../packages/atoms/**/*.stories.mdx",
|
||||||
"../../../packages/features/**/*.stories.mdx",
|
"../../../packages/features/**/*.stories.mdx",
|
||||||
"../../../packages/ui/components/**/*.stories.@(js|jsx|ts|tsx)",
|
"../../../packages/ui/components/**/*.stories.@(js|jsx|ts|tsx)",
|
||||||
],
|
],
|
||||||
@@ -70,4 +71,5 @@ module.exports = {
|
|||||||
|
|
||||||
return config;
|
return config;
|
||||||
},
|
},
|
||||||
|
typescript: { reactDocgen: "react-docgen" },
|
||||||
};
|
};
|
||||||
@@ -1,4 +1,5 @@
|
|||||||
import React, { ReactElement, ReactNode } from "react";
|
import type { ReactElement, ReactNode } from "react";
|
||||||
|
import React from "react";
|
||||||
|
|
||||||
import { classNames } from "@calcom/lib";
|
import { classNames } from "@calcom/lib";
|
||||||
|
|
||||||
@@ -42,7 +43,7 @@ export function VariantsTable({
|
|||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
{!isDark && (
|
{!isDark && (
|
||||||
<div data-mode="dark">
|
<div data-mode="dark" className="dark">
|
||||||
<VariantsTable titles={titles} isDark columnMinWidth={columnMinWidth}>
|
<VariantsTable titles={titles} isDark columnMinWidth={columnMinWidth}>
|
||||||
{children}
|
{children}
|
||||||
</VariantsTable>
|
</VariantsTable>
|
||||||
|
|||||||
@@ -193,3 +193,88 @@
|
|||||||
.docs-story {
|
.docs-story {
|
||||||
padding: 24px !important;
|
padding: 24px !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@layer {
|
||||||
|
:root {
|
||||||
|
/* background */
|
||||||
|
|
||||||
|
--cal-bg-emphasis: #E5E7EB;
|
||||||
|
--cal-bg: white;
|
||||||
|
--cal-bg-subtle: #F3F4F6;
|
||||||
|
--cal-bg-muted: #F9FAFB;
|
||||||
|
--cal-bg-inverted: #111827;
|
||||||
|
|
||||||
|
/* background -> components*/
|
||||||
|
--cal-bg-info: #DEE9FC;
|
||||||
|
--cal-bg-success: #E2FBE8;
|
||||||
|
--cal-bg-attention: #FCEED8;
|
||||||
|
--cal-bg-error: #F9E3E2;
|
||||||
|
|
||||||
|
|
||||||
|
/* Borders */
|
||||||
|
--cal-border-emphasis:#9CA3AF;
|
||||||
|
--cal-border: #D1D5DB;
|
||||||
|
--cal-border-subtle:#E5E7EB;
|
||||||
|
--cal-border-muted:#F3F4F6;
|
||||||
|
|
||||||
|
/* Content/Text */
|
||||||
|
--cal-text-emphasis: #111827;
|
||||||
|
--cal-text:#374151;
|
||||||
|
--cal-text-subtle:#6B7280;
|
||||||
|
--cal-text-muted:#9CA3AF;
|
||||||
|
--cal-text-inverted:white;
|
||||||
|
|
||||||
|
/* Content/Text -> components */
|
||||||
|
--cal-text-info:#253985;
|
||||||
|
--cal-text-success:#285231;
|
||||||
|
--cal-text-attention:#73321B;
|
||||||
|
--cal-text-error:#752522;
|
||||||
|
|
||||||
|
/* Brand shinanigans
|
||||||
|
-> These will be computed for the users theme at runtime.
|
||||||
|
*/
|
||||||
|
--cal-brand:#111827;
|
||||||
|
--cal-brand-emphasis:#101010;
|
||||||
|
}
|
||||||
|
.dark {
|
||||||
|
/* background */
|
||||||
|
|
||||||
|
--cal-bg-emphasis: #2B2B2B;
|
||||||
|
--cal-bg: #101010;
|
||||||
|
--cal-bg-subtle: #2B2B2B;
|
||||||
|
--cal-bg-muted: #1C1C1C;
|
||||||
|
--cal-bg-inverted: #F3F4F6;
|
||||||
|
|
||||||
|
/* background -> components*/
|
||||||
|
--cal-bg-info: #DEE9FC;
|
||||||
|
--cal-bg-success: #E2FBE8;
|
||||||
|
--cal-bg-attention: #FCEED8;
|
||||||
|
--cal-bg-error: #F9E3E2;
|
||||||
|
|
||||||
|
|
||||||
|
/* Borders */
|
||||||
|
--cal-border-emphasis: #575757;
|
||||||
|
--cal-border: #444444;
|
||||||
|
--cal-border-subtle: #2B2B2B;
|
||||||
|
--cal-border-muted: #1C1C1C;
|
||||||
|
|
||||||
|
/* Content/Text */
|
||||||
|
--cal-text-emphasis: #F3F4F6;
|
||||||
|
--cal-text: #D6D6D6;
|
||||||
|
--cal-text-subtle: #757575;
|
||||||
|
--cal-text-muted: #575757;
|
||||||
|
--cal-text-inverted: #101010;
|
||||||
|
|
||||||
|
/* Content/Text -> components */
|
||||||
|
--cal-text-info: #253985;
|
||||||
|
--cal-text-success: #285231;
|
||||||
|
--cal-text-attention: #73321B;
|
||||||
|
--cal-text-error: #752522;
|
||||||
|
|
||||||
|
/* Brand shenanigans
|
||||||
|
-> These will be computed for the users theme at runtime.
|
||||||
|
*/
|
||||||
|
--cal-brand: #111827;
|
||||||
|
--cal-brand-emphasis: #101010;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -68,5 +68,3 @@ public/embed
|
|||||||
|
|
||||||
# Copied app-store images
|
# Copied app-store images
|
||||||
public/app-store
|
public/app-store
|
||||||
# Sentry
|
|
||||||
.sentryclirc
|
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
# @calcom/web
|
||||||
|
|
||||||
|
## 2.7.16
|
||||||
|
|
||||||
|
### Patch Changes
|
||||||
|
|
||||||
|
- Updated dependencies
|
||||||
|
- @calcom/embed-snippet@1.0.7
|
||||||
|
- @calcom/embed-react@1.0.12
|
||||||
|
- @calcom/embed-core@1.1.5
|
||||||
@@ -103,59 +103,6 @@ paths:
|
|||||||
summary: Reset a user's password
|
summary: Reset a user's password
|
||||||
tags:
|
tags:
|
||||||
- Authentication
|
- Authentication
|
||||||
"/api/availability/{user}?dateFrom={dateFrom}&dateTo={dateTo}":
|
|
||||||
get:
|
|
||||||
description: "Gets the busy times for a particular user, by username."
|
|
||||||
summary: Gets the busy times for a user
|
|
||||||
tags:
|
|
||||||
- Availability
|
|
||||||
responses:
|
|
||||||
"200":
|
|
||||||
description: OK
|
|
||||||
content:
|
|
||||||
application/json:
|
|
||||||
schema:
|
|
||||||
type: array
|
|
||||||
description: ""
|
|
||||||
minItems: 1
|
|
||||||
uniqueItems: true
|
|
||||||
x-examples:
|
|
||||||
example-1:
|
|
||||||
- start: "Fri, 03 Sep 2021 17:00:00 GMT"
|
|
||||||
end: "Fri, 03 Sep 2021 17:40:00 GMT"
|
|
||||||
items:
|
|
||||||
type: object
|
|
||||||
properties:
|
|
||||||
start:
|
|
||||||
type: string
|
|
||||||
minLength: 1
|
|
||||||
end:
|
|
||||||
type: string
|
|
||||||
minLength: 1
|
|
||||||
required:
|
|
||||||
- start
|
|
||||||
- end
|
|
||||||
"500":
|
|
||||||
description: Internal Server Error
|
|
||||||
parameters:
|
|
||||||
- schema:
|
|
||||||
type: string
|
|
||||||
name: user
|
|
||||||
in: path
|
|
||||||
required: true
|
|
||||||
description: The username of who you want to check availability for
|
|
||||||
- schema:
|
|
||||||
type: string
|
|
||||||
name: dateFrom
|
|
||||||
in: path
|
|
||||||
required: true
|
|
||||||
description: The timestamp of which time you want to get busy times from
|
|
||||||
- schema:
|
|
||||||
type: string
|
|
||||||
name: dateTo
|
|
||||||
in: path
|
|
||||||
required: true
|
|
||||||
description: The timestamp of which time you want to get busy times until
|
|
||||||
/api/availability/calendar:
|
/api/availability/calendar:
|
||||||
get:
|
get:
|
||||||
description: Gets the user's selected calendars.
|
description: Gets the user's selected calendars.
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
|
|
||||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||||
import { FiX } from "@calcom/ui/components/icon";
|
import { X } from "@calcom/ui/components/icon";
|
||||||
|
|
||||||
export default function AddToHomescreen() {
|
export default function AddToHomescreen() {
|
||||||
const { t } = useLocale();
|
const { t } = useLocale();
|
||||||
@@ -29,7 +29,7 @@ export default function AddToHomescreen() {
|
|||||||
<path d="M35 40H15c-1.7 0-3-1.3-3-3V19c0-1.7 1.3-3 3-3h7v2h-7c-.6 0-1 .4-1 1v18c0 .6.4 1 1 1h20c.6 0 1-.4 1-1V19c0-.6-.4-1-1-1h-7v-2h7c1.7 0 3 1.3 3 3v18c0 1.7-1.3 3-3 3z" />
|
<path d="M35 40H15c-1.7 0-3-1.3-3-3V19c0-1.7 1.3-3 3-3h7v2h-7c-.6 0-1 .4-1 1v18c0 .6.4 1 1 1h20c.6 0 1-.4 1-1V19c0-.6-.4-1-1-1h-7v-2h7c1.7 0 3 1.3 3 3v18c0 1.7-1.3 3-3 3z" />
|
||||||
</svg>
|
</svg>
|
||||||
</span>
|
</span>
|
||||||
<p className="text-xs font-medium text-white ltr:ml-3 rtl:mr-3">
|
<p className="text-inverted ms-3 text-xs font-medium">
|
||||||
<span className="inline">{t("add_to_homescreen")}</span>
|
<span className="inline">{t("add_to_homescreen")}</span>
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
@@ -40,7 +40,7 @@ export default function AddToHomescreen() {
|
|||||||
type="button"
|
type="button"
|
||||||
className="-mr-1 flex rounded-md p-2 hover:bg-gray-800 focus:outline-none focus:ring-2 focus:ring-white">
|
className="-mr-1 flex rounded-md p-2 hover:bg-gray-800 focus:outline-none focus:ring-2 focus:ring-white">
|
||||||
<span className="sr-only">{t("dismiss")}</span>
|
<span className="sr-only">{t("dismiss")}</span>
|
||||||
<FiX className="h-6 w-6 text-white" aria-hidden="true" />
|
<X className="text-inverted h-6 w-6" aria-hidden="true" />
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,86 +0,0 @@
|
|||||||
import React from "react";
|
|
||||||
import Select from "react-select";
|
|
||||||
import type { OptionProps } from "react-select";
|
|
||||||
|
|
||||||
import { InstallAppButton } from "@calcom/app-store/components";
|
|
||||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
|
||||||
import { trpc } from "@calcom/trpc/react";
|
|
||||||
import type { App } from "@calcom/types/App";
|
|
||||||
import { Button } from "@calcom/ui";
|
|
||||||
|
|
||||||
import { QueryCell } from "@lib/QueryCell";
|
|
||||||
|
|
||||||
interface AdditionalCalendarSelectorProps {
|
|
||||||
isLoading?: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
const ImageOption = (optionProps: OptionProps<{ [key: string]: string; type: App["type"] }>) => {
|
|
||||||
const { data } = optionProps;
|
|
||||||
return (
|
|
||||||
<InstallAppButton
|
|
||||||
type={data.type}
|
|
||||||
render={(installProps) => {
|
|
||||||
return (
|
|
||||||
<Button {...installProps} className="w-full" color="minimal">
|
|
||||||
{/* eslint-disable @next/next/no-img-element */}
|
|
||||||
{data.image && (
|
|
||||||
<img className="float-left mr-3 inline h-5 w-5" src={data.image} alt={data.label} />
|
|
||||||
)}
|
|
||||||
<p>{data.label}</p>
|
|
||||||
</Button>
|
|
||||||
);
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
const AdditionalCalendarSelector = ({ isLoading }: AdditionalCalendarSelectorProps): JSX.Element | null => {
|
|
||||||
const { t } = useLocale();
|
|
||||||
const query = trpc.viewer.integrations.useQuery({ variant: "calendar", onlyInstalled: true });
|
|
||||||
|
|
||||||
return (
|
|
||||||
<QueryCell
|
|
||||||
query={query}
|
|
||||||
success={({ data }) => {
|
|
||||||
const options = data.items.map((item) => ({
|
|
||||||
label: item.name,
|
|
||||||
slug: item.slug,
|
|
||||||
image: item.logo,
|
|
||||||
type: item.type,
|
|
||||||
}));
|
|
||||||
return (
|
|
||||||
<Select
|
|
||||||
name="additionalCalendar"
|
|
||||||
placeholder={t("connect_additional_calendar")}
|
|
||||||
options={options}
|
|
||||||
styles={{
|
|
||||||
placeholder: (defaultStyles) => {
|
|
||||||
return {
|
|
||||||
...defaultStyles,
|
|
||||||
color: "#3E3E3E",
|
|
||||||
marginLeft: "3px",
|
|
||||||
};
|
|
||||||
},
|
|
||||||
control: (defaultStyles) => {
|
|
||||||
return {
|
|
||||||
...defaultStyles,
|
|
||||||
borderRadius: "2px",
|
|
||||||
"@media only screen and (min-width: 640px)": {
|
|
||||||
...(defaultStyles["@media only screen and (min-width: 640px)"] as object),
|
|
||||||
maxWidth: "320px",
|
|
||||||
},
|
|
||||||
};
|
|
||||||
},
|
|
||||||
}}
|
|
||||||
isSearchable={false}
|
|
||||||
className="mt-1 mb-2 block w-full min-w-0 flex-1 rounded-none rounded-r-sm border-gray-300 text-sm font-medium text-gray-700"
|
|
||||||
isLoading={isLoading}
|
|
||||||
components={{ Option: ImageOption }}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default AdditionalCalendarSelector;
|
|
||||||
@@ -6,7 +6,7 @@ import { z } from "zod";
|
|||||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||||
import { useTypedQuery } from "@calcom/lib/hooks/useTypedQuery";
|
import { useTypedQuery } from "@calcom/lib/hooks/useTypedQuery";
|
||||||
import { Badge, ListItemText } from "@calcom/ui";
|
import { Badge, ListItemText } from "@calcom/ui";
|
||||||
import { FiAlertCircle } from "@calcom/ui/components/icon";
|
import { AlertCircle } from "@calcom/ui/components/icon";
|
||||||
|
|
||||||
type ShouldHighlight = { slug: string; shouldHighlight: true } | { shouldHighlight?: never; slug?: never };
|
type ShouldHighlight = { slug: string; shouldHighlight: true } | { shouldHighlight?: never; slug?: never };
|
||||||
|
|
||||||
@@ -63,12 +63,12 @@ export default function AppListCard(props: AppListCardProps) {
|
|||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={`${highlight ? "bg-yellow-100" : ""}`}>
|
<div className={`${highlight ? "dark:bg-muted bg-yellow-100" : ""}`}>
|
||||||
<div className="flex gap-x-3 px-5 py-4">
|
<div className="flex gap-x-3 px-5 py-4">
|
||||||
{logo ? <img className="h-10 w-10" src={logo} alt={`${title} logo`} /> : null}
|
{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 grow flex-col gap-y-1 truncate">
|
||||||
<div className="flex items-center gap-x-2">
|
<div className="flex items-center gap-x-2">
|
||||||
<h3 className="truncate text-sm font-semibold text-gray-900">{title}</h3>
|
<h3 className="text-emphasis truncate text-sm font-semibold">{title}</h3>
|
||||||
<div className="flex items-center gap-x-2">
|
<div className="flex items-center gap-x-2">
|
||||||
{isDefault && <Badge variant="green">{t("default")}</Badge>}
|
{isDefault && <Badge variant="green">{t("default")}</Badge>}
|
||||||
{isTemplate && <Badge variant="red">Template</Badge>}
|
{isTemplate && <Badge variant="red">Template</Badge>}
|
||||||
@@ -77,7 +77,7 @@ export default function AppListCard(props: AppListCardProps) {
|
|||||||
<ListItemText component="p">{description}</ListItemText>
|
<ListItemText component="p">{description}</ListItemText>
|
||||||
{invalidCredential && (
|
{invalidCredential && (
|
||||||
<div className="flex gap-x-2 pt-2">
|
<div className="flex gap-x-2 pt-2">
|
||||||
<FiAlertCircle className="h-8 w-8 text-red-500 sm:h-4 sm:w-4" />
|
<AlertCircle className="h-8 w-8 text-red-500 sm:h-4 sm:w-4" />
|
||||||
<ListItemText component="p" className="whitespace-pre-wrap text-red-500">
|
<ListItemText component="p" className="whitespace-pre-wrap text-red-500">
|
||||||
{t("invalid_credential")}
|
{t("invalid_credential")}
|
||||||
</ListItemText>
|
</ListItemText>
|
||||||
|
|||||||
@@ -1,137 +0,0 @@
|
|||||||
import classNames from "classnames";
|
|
||||||
import React, { useEffect, useState } from "react";
|
|
||||||
import Select from "react-select";
|
|
||||||
|
|
||||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
|
||||||
import type { DestinationCalendar } from "@calcom/prisma/client";
|
|
||||||
import { trpc } from "@calcom/trpc/react";
|
|
||||||
|
|
||||||
interface Props {
|
|
||||||
onChange: (value: { externalId: string; integration: string }) => void;
|
|
||||||
isLoading?: boolean;
|
|
||||||
hidePlaceholder?: boolean;
|
|
||||||
/** The external Id of the connected calendar */
|
|
||||||
value: string | undefined;
|
|
||||||
destinationCalendar?: DestinationCalendar | null;
|
|
||||||
}
|
|
||||||
|
|
||||||
const DestinationCalendarSelector = ({
|
|
||||||
onChange,
|
|
||||||
isLoading,
|
|
||||||
value,
|
|
||||||
hidePlaceholder,
|
|
||||||
destinationCalendar,
|
|
||||||
}: Props): JSX.Element | null => {
|
|
||||||
const { t } = useLocale();
|
|
||||||
const query = trpc.viewer.connectedCalendars.useQuery();
|
|
||||||
const [selectedOption, setSelectedOption] = useState<{ value: string; label: string } | null>(null);
|
|
||||||
|
|
||||||
// Extra styles to show prefixed text in react-select
|
|
||||||
const content = (hidePlaceholder = false) => {
|
|
||||||
if (!hidePlaceholder) {
|
|
||||||
return {
|
|
||||||
alignItems: "center",
|
|
||||||
display: "flex",
|
|
||||||
":before": {
|
|
||||||
content: `'${t("select_destination_calendar")}:'`,
|
|
||||||
display: "block",
|
|
||||||
marginRight: 8,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}
|
|
||||||
return {};
|
|
||||||
};
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const selected = query.data?.connectedCalendars
|
|
||||||
.map((connected) => connected.calendars ?? [])
|
|
||||||
.flat()
|
|
||||||
.find((cal) => cal.externalId === value);
|
|
||||||
|
|
||||||
if (selected) {
|
|
||||||
setSelectedOption({
|
|
||||||
value: `${selected.integration}:${selected.externalId}`,
|
|
||||||
label: selected.name || "",
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}, [query.data?.connectedCalendars, value]);
|
|
||||||
|
|
||||||
if (!query.data?.connectedCalendars.length) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
const options =
|
|
||||||
query.data.connectedCalendars.map((selectedCalendar) => ({
|
|
||||||
key: selectedCalendar.credentialId,
|
|
||||||
label: `${selectedCalendar.integration.title} (${selectedCalendar.primary?.name})`,
|
|
||||||
options: (selectedCalendar.calendars ?? [])
|
|
||||||
.filter((cal) => cal.readOnly === false)
|
|
||||||
.map((cal) => ({
|
|
||||||
label: cal.name || "",
|
|
||||||
value: `${cal.integration}:${cal.externalId}`,
|
|
||||||
})),
|
|
||||||
})) ?? [];
|
|
||||||
const defaultCalendarSelectedString = destinationCalendar?.externalId
|
|
||||||
? `(${
|
|
||||||
destinationCalendar.externalId.length > 15
|
|
||||||
? destinationCalendar.externalId.substring(0, 15) + "..."
|
|
||||||
: destinationCalendar.externalId
|
|
||||||
})`
|
|
||||||
: "";
|
|
||||||
return (
|
|
||||||
<div className="relative" title={`${t("select_destination_calendar")}: ${selectedOption?.label || ""}`}>
|
|
||||||
<Select
|
|
||||||
name="primarySelectedCalendar"
|
|
||||||
placeholder={
|
|
||||||
!hidePlaceholder ? (
|
|
||||||
`${t("select_destination_calendar")}`
|
|
||||||
) : (
|
|
||||||
<span className="whitespace-nowrap">
|
|
||||||
{t("default_calendar_selected")} {defaultCalendarSelectedString}
|
|
||||||
</span>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
options={options}
|
|
||||||
styles={{
|
|
||||||
placeholder: (styles) => ({ ...styles, ...content(hidePlaceholder) }),
|
|
||||||
singleValue: (styles) => ({ ...styles, ...content(hidePlaceholder) }),
|
|
||||||
control: (defaultStyles) => {
|
|
||||||
return {
|
|
||||||
...defaultStyles,
|
|
||||||
borderRadius: "6px",
|
|
||||||
"@media only screen and (min-width: 640px)": {
|
|
||||||
...(defaultStyles["@media only screen and (min-width: 640px)"] as object),
|
|
||||||
width: "100%",
|
|
||||||
},
|
|
||||||
};
|
|
||||||
},
|
|
||||||
}}
|
|
||||||
isSearchable={false}
|
|
||||||
className={classNames(
|
|
||||||
"mt-1 mb-2 block w-full min-w-0 flex-1 rounded-none rounded-r-sm border-gray-300 text-sm",
|
|
||||||
!hidePlaceholder && "font-medium"
|
|
||||||
)}
|
|
||||||
onChange={(option) => {
|
|
||||||
setSelectedOption(option);
|
|
||||||
if (!option) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Split only the first `:`, since Apple uses the full URL as externalId */
|
|
||||||
const [integration, externalId] = option.value.split(/:(.+)/);
|
|
||||||
|
|
||||||
onChange({
|
|
||||||
integration,
|
|
||||||
externalId,
|
|
||||||
});
|
|
||||||
}}
|
|
||||||
isLoading={isLoading}
|
|
||||||
value={selectedOption}
|
|
||||||
components={{
|
|
||||||
IndicatorSeparator: () => null,
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default DestinationCalendarSelector;
|
|
||||||
+324
-336
@@ -1,5 +1,6 @@
|
|||||||
import { Collapsible, CollapsibleContent, CollapsibleTrigger } from "@radix-ui/react-collapsible";
|
import { Collapsible, CollapsibleContent } from "@radix-ui/react-collapsible";
|
||||||
import classNames from "classnames";
|
import classNames from "classnames";
|
||||||
|
import type { TFunction } from "next-i18next";
|
||||||
import type { NextRouter } from "next/router";
|
import type { NextRouter } from "next/router";
|
||||||
import { useRouter } from "next/router";
|
import { useRouter } from "next/router";
|
||||||
import type { MutableRefObject, RefObject } from "react";
|
import type { MutableRefObject, RefObject } from "react";
|
||||||
@@ -21,10 +22,9 @@ import {
|
|||||||
TextArea,
|
TextArea,
|
||||||
TextField,
|
TextField,
|
||||||
ColorPicker,
|
ColorPicker,
|
||||||
|
Select,
|
||||||
} from "@calcom/ui";
|
} from "@calcom/ui";
|
||||||
import { FiCode, FiTrello, FiSun, FiArrowLeft, FiChevronRight } from "@calcom/ui/components/icon";
|
import { Code, Trello, Sun, ArrowLeft } from "@calcom/ui/components/icon";
|
||||||
|
|
||||||
import Select from "@components/ui/form/Select";
|
|
||||||
|
|
||||||
type EmbedType = "inline" | "floating-popup" | "element-click";
|
type EmbedType = "inline" | "floating-popup" | "element-click";
|
||||||
type EmbedFramework = "react" | "HTML";
|
type EmbedFramework = "react" | "HTML";
|
||||||
@@ -167,7 +167,7 @@ const Codes: Record<string, Record<string, (...args: any[]) => string>> = {
|
|||||||
const height = getDimension(previewState.inline.height);
|
const height = getDimension(previewState.inline.height);
|
||||||
return code`
|
return code`
|
||||||
import Cal, { getCalApi } from "@calcom/embed-react";
|
import Cal, { getCalApi } from "@calcom/embed-react";
|
||||||
|
import { useEffect } from "react";
|
||||||
function MyComponent() {
|
function MyComponent() {
|
||||||
useEffect(()=>{
|
useEffect(()=>{
|
||||||
(async function () {
|
(async function () {
|
||||||
@@ -187,12 +187,12 @@ function MyComponent() {
|
|||||||
}) => {
|
}) => {
|
||||||
return code`
|
return code`
|
||||||
import Cal, { getCalApi } from "@calcom/embed-react";
|
import Cal, { getCalApi } from "@calcom/embed-react";
|
||||||
|
import { useEffect } from "react";
|
||||||
function MyComponent() {
|
function MyComponent() {
|
||||||
useEffect(()=>{
|
useEffect(()=>{
|
||||||
(async function () {
|
(async function () {
|
||||||
const cal = await getCalApi();
|
const cal = await getCalApi();
|
||||||
Cal("floatingButton", ${floatingButtonArg});
|
cal("floatingButton", ${floatingButtonArg});
|
||||||
${uiInstructionCode}
|
${uiInstructionCode}
|
||||||
})();
|
})();
|
||||||
}, [])
|
}, [])
|
||||||
@@ -201,7 +201,7 @@ function MyComponent() {
|
|||||||
"element-click": ({ calLink, uiInstructionCode }: { calLink: string; uiInstructionCode: string }) => {
|
"element-click": ({ calLink, uiInstructionCode }: { calLink: string; uiInstructionCode: string }) => {
|
||||||
return code`
|
return code`
|
||||||
import Cal, { getCalApi } from "@calcom/embed-react";
|
import Cal, { getCalApi } from "@calcom/embed-react";
|
||||||
|
import { useEffect } from "react";
|
||||||
function MyComponent() {
|
function MyComponent() {
|
||||||
useEffect(()=>{
|
useEffect(()=>{
|
||||||
(async function () {
|
(async function () {
|
||||||
@@ -305,195 +305,194 @@ const getEmbedTypeSpecificString = ({
|
|||||||
return "";
|
return "";
|
||||||
};
|
};
|
||||||
|
|
||||||
const embeds: {
|
const embeds = (t: TFunction) =>
|
||||||
illustration: React.ReactElement;
|
(() => {
|
||||||
title: string;
|
return [
|
||||||
subtitle: string;
|
{
|
||||||
type: EmbedType;
|
title: t("inline_embed"),
|
||||||
}[] = [
|
subtitle: t("load_inline_content"),
|
||||||
{
|
type: "inline",
|
||||||
title: "Inline Embed",
|
illustration: (
|
||||||
subtitle: "Loads your Cal scheduling page directly inline with your other website content",
|
<svg
|
||||||
type: "inline",
|
width="100%"
|
||||||
illustration: (
|
height="100%"
|
||||||
<svg
|
className="rounded-md"
|
||||||
width="100%"
|
viewBox="0 0 308 265"
|
||||||
height="100%"
|
fill="none"
|
||||||
className="rounded-md"
|
xmlns="http://www.w3.org/2000/svg">
|
||||||
viewBox="0 0 308 265"
|
<path
|
||||||
fill="none"
|
d="M0 1.99999C0 0.895423 0.895431 0 2 0H306C307.105 0 308 0.895431 308 2V263C308 264.105 307.105 265 306 265H2C0.895431 265 0 264.105 0 263V1.99999Z"
|
||||||
xmlns="http://www.w3.org/2000/svg">
|
fill="white"
|
||||||
<path
|
/>
|
||||||
d="M0 1.99999C0 0.895423 0.895431 0 2 0H306C307.105 0 308 0.895431 308 2V263C308 264.105 307.105 265 306 265H2C0.895431 265 0 264.105 0 263V1.99999Z"
|
<rect x="24" width="260" height="38.5" rx="6" fill="#F3F4F6" />
|
||||||
fill="white"
|
<rect x="24.5" y="51" width="139" height="163" rx="1.5" fill="#F8F8F8" />
|
||||||
/>
|
<rect opacity="0.8" x="48" y="74.5" width="80" height="8" rx="6" fill="#F3F4F6" />
|
||||||
<rect x="24" width="260" height="38.5" rx="6" fill="#F3F4F6" />
|
<rect x="48" y="86.5" width="48" height="4" rx="6" fill="#F3F4F6" />
|
||||||
<rect x="24.5" y="51" width="139" height="163" rx="1.5" fill="#F8F8F8" />
|
<rect x="49" y="99.5" width="6" height="6" rx="1" fill="#C6C6C6" />
|
||||||
<rect opacity="0.8" x="48" y="74.5" width="80" height="8" rx="6" fill="#F3F4F6" />
|
<rect x="61" y="99.5" width="6" height="6" rx="1" fill="#3E3E3E" />
|
||||||
<rect x="48" y="86.5" width="48" height="4" rx="6" fill="#F3F4F6" />
|
<rect x="73" y="99.5" width="6" height="6" rx="1" fill="#C6C6C6" />
|
||||||
<rect x="49" y="99.5" width="6" height="6" rx="1" fill="#C6C6C6" />
|
<rect x="85" y="99.5" width="6" height="6" rx="1" fill="#C6C6C6" />
|
||||||
<rect x="61" y="99.5" width="6" height="6" rx="1" fill="#3E3E3E" />
|
<rect x="97" y="99.5" width="6" height="6" rx="1" fill="#C6C6C6" />
|
||||||
<rect x="73" y="99.5" width="6" height="6" rx="1" fill="#C6C6C6" />
|
<rect x="109" y="99.5" width="6" height="6" rx="1" fill="#C6C6C6" />
|
||||||
<rect x="85" y="99.5" width="6" height="6" rx="1" fill="#C6C6C6" />
|
<rect x="121" y="99.5" width="6" height="6" rx="1" fill="#C6C6C6" />
|
||||||
<rect x="97" y="99.5" width="6" height="6" rx="1" fill="#C6C6C6" />
|
<rect x="133" y="99.5" width="6" height="6" rx="1" fill="#C6C6C6" />
|
||||||
<rect x="109" y="99.5" width="6" height="6" rx="1" fill="#C6C6C6" />
|
<rect x="85" y="113.5" width="6" height="6" rx="1" fill="#C6C6C6" />
|
||||||
<rect x="121" y="99.5" width="6" height="6" rx="1" fill="#C6C6C6" />
|
<rect x="97" y="113.5" width="6" height="6" rx="1" fill="#C6C6C6" />
|
||||||
<rect x="133" y="99.5" width="6" height="6" rx="1" fill="#C6C6C6" />
|
<rect x="109" y="113.5" width="6" height="6" rx="1" fill="#C6C6C6" />
|
||||||
<rect x="85" y="113.5" width="6" height="6" rx="1" fill="#C6C6C6" />
|
<rect x="121" y="113.5" width="6" height="6" rx="1" fill="#C6C6C6" />
|
||||||
<rect x="97" y="113.5" width="6" height="6" rx="1" fill="#C6C6C6" />
|
<rect x="133" y="113.5" width="6" height="6" rx="1" fill="#C6C6C6" />
|
||||||
<rect x="109" y="113.5" width="6" height="6" rx="1" fill="#C6C6C6" />
|
<rect x="49" y="125.5" width="6" height="6" rx="1" fill="#C6C6C6" />
|
||||||
<rect x="121" y="113.5" width="6" height="6" rx="1" fill="#C6C6C6" />
|
<rect x="61" y="125.5" width="6" height="6" rx="1" fill="#3E3E3E" />
|
||||||
<rect x="133" y="113.5" width="6" height="6" rx="1" fill="#C6C6C6" />
|
<path
|
||||||
<rect x="49" y="125.5" width="6" height="6" rx="1" fill="#C6C6C6" />
|
d="M61 124.5H67V122.5H61V124.5ZM68 125.5V131.5H70V125.5H68ZM67 132.5H61V134.5H67V132.5ZM60 131.5V125.5H58V131.5H60ZM61 132.5C60.4477 132.5 60 132.052 60 131.5H58C58 133.157 59.3431 134.5 61 134.5V132.5ZM68 131.5C68 132.052 67.5523 132.5 67 132.5V134.5C68.6569 134.5 70 133.157 70 131.5H68ZM67 124.5C67.5523 124.5 68 124.948 68 125.5H70C70 123.843 68.6569 122.5 67 122.5V124.5ZM61 122.5C59.3431 122.5 58 123.843 58 125.5H60C60 124.948 60.4477 124.5 61 124.5V122.5Z"
|
||||||
<rect x="61" y="125.5" width="6" height="6" rx="1" fill="#3E3E3E" />
|
fill="#3E3E3E"
|
||||||
<path
|
/>
|
||||||
d="M61 124.5H67V122.5H61V124.5ZM68 125.5V131.5H70V125.5H68ZM67 132.5H61V134.5H67V132.5ZM60 131.5V125.5H58V131.5H60ZM61 132.5C60.4477 132.5 60 132.052 60 131.5H58C58 133.157 59.3431 134.5 61 134.5V132.5ZM68 131.5C68 132.052 67.5523 132.5 67 132.5V134.5C68.6569 134.5 70 133.157 70 131.5H68ZM67 124.5C67.5523 124.5 68 124.948 68 125.5H70C70 123.843 68.6569 122.5 67 122.5V124.5ZM61 122.5C59.3431 122.5 58 123.843 58 125.5H60C60 124.948 60.4477 124.5 61 124.5V122.5Z"
|
<rect x="73" y="125.5" width="6" height="6" rx="1" fill="#C6C6C6" />
|
||||||
fill="#3E3E3E"
|
<rect x="85" y="125.5" width="6" height="6" rx="1" fill="#C6C6C6" />
|
||||||
/>
|
<rect x="97" y="125.5" width="6" height="6" rx="1" fill="#C6C6C6" />
|
||||||
<rect x="73" y="125.5" width="6" height="6" rx="1" fill="#C6C6C6" />
|
<rect x="109" y="125.5" width="6" height="6" rx="1" fill="#C6C6C6" />
|
||||||
<rect x="85" y="125.5" width="6" height="6" rx="1" fill="#C6C6C6" />
|
<rect x="121" y="125.5" width="6" height="6" rx="1" fill="#C6C6C6" />
|
||||||
<rect x="97" y="125.5" width="6" height="6" rx="1" fill="#C6C6C6" />
|
<rect x="133" y="125.5" width="6" height="6" rx="1" fill="#C6C6C6" />
|
||||||
<rect x="109" y="125.5" width="6" height="6" rx="1" fill="#C6C6C6" />
|
<rect x="49" y="137.5" width="6" height="6" rx="1" fill="#C6C6C6" />
|
||||||
<rect x="121" y="125.5" width="6" height="6" rx="1" fill="#C6C6C6" />
|
<rect x="61" y="137.5" width="6" height="6" rx="1" fill="#C6C6C6" />
|
||||||
<rect x="133" y="125.5" width="6" height="6" rx="1" fill="#C6C6C6" />
|
<rect x="73" y="137.5" width="6" height="6" rx="1" fill="#C6C6C6" />
|
||||||
<rect x="49" y="137.5" width="6" height="6" rx="1" fill="#C6C6C6" />
|
<rect x="85" y="137.5" width="6" height="6" rx="1" fill="#C6C6C6" />
|
||||||
<rect x="61" y="137.5" width="6" height="6" rx="1" fill="#C6C6C6" />
|
<rect x="97" y="137.5" width="6" height="6" rx="1" fill="#3E3E3E" />
|
||||||
<rect x="73" y="137.5" width="6" height="6" rx="1" fill="#C6C6C6" />
|
<rect x="109" y="137.5" width="6" height="6" rx="1" fill="#3E3E3E" />
|
||||||
<rect x="85" y="137.5" width="6" height="6" rx="1" fill="#C6C6C6" />
|
<rect x="121" y="137.5" width="6" height="6" rx="1" fill="#C6C6C6" />
|
||||||
<rect x="97" y="137.5" width="6" height="6" rx="1" fill="#3E3E3E" />
|
<rect x="133" y="137.5" width="6" height="6" rx="1" fill="#C6C6C6" />
|
||||||
<rect x="109" y="137.5" width="6" height="6" rx="1" fill="#3E3E3E" />
|
<rect x="49" y="149.5" width="6" height="6" rx="1" fill="#C6C6C6" />
|
||||||
<rect x="121" y="137.5" width="6" height="6" rx="1" fill="#C6C6C6" />
|
<rect x="61" y="149.5" width="6" height="6" rx="1" fill="#C6C6C6" />
|
||||||
<rect x="133" y="137.5" width="6" height="6" rx="1" fill="#C6C6C6" />
|
<rect x="73" y="149.5" width="6" height="6" rx="1" fill="#C6C6C6" />
|
||||||
<rect x="49" y="149.5" width="6" height="6" rx="1" fill="#C6C6C6" />
|
<rect x="85" y="149.5" width="6" height="6" rx="1" fill="#C6C6C6" />
|
||||||
<rect x="61" y="149.5" width="6" height="6" rx="1" fill="#C6C6C6" />
|
<rect x="97" y="149.5" width="6" height="6" rx="1" fill="#3E3E3E" />
|
||||||
<rect x="73" y="149.5" width="6" height="6" rx="1" fill="#C6C6C6" />
|
<rect x="109" y="149.5" width="6" height="6" rx="1" fill="#3E3E3E" />
|
||||||
<rect x="85" y="149.5" width="6" height="6" rx="1" fill="#C6C6C6" />
|
<rect x="121" y="149.5" width="6" height="6" rx="1" fill="#C6C6C6" />
|
||||||
<rect x="97" y="149.5" width="6" height="6" rx="1" fill="#3E3E3E" />
|
<rect x="133" y="149.5" width="6" height="6" rx="1" fill="#C6C6C6" />
|
||||||
<rect x="109" y="149.5" width="6" height="6" rx="1" fill="#3E3E3E" />
|
<rect x="49" y="161.5" width="6" height="6" rx="1" fill="#C6C6C6" />
|
||||||
<rect x="121" y="149.5" width="6" height="6" rx="1" fill="#C6C6C6" />
|
<rect x="61" y="161.5" width="6" height="6" rx="1" fill="#C6C6C6" />
|
||||||
<rect x="133" y="149.5" width="6" height="6" rx="1" fill="#C6C6C6" />
|
<rect x="73" y="161.5" width="6" height="6" rx="1" fill="#C6C6C6" />
|
||||||
<rect x="49" y="161.5" width="6" height="6" rx="1" fill="#C6C6C6" />
|
<rect x="85" y="161.5" width="6" height="6" rx="1" fill="#C6C6C6" />
|
||||||
<rect x="61" y="161.5" width="6" height="6" rx="1" fill="#C6C6C6" />
|
<rect x="97" y="161.5" width="6" height="6" rx="1" fill="#3E3E3E" />
|
||||||
<rect x="73" y="161.5" width="6" height="6" rx="1" fill="#C6C6C6" />
|
<rect x="109" y="161.5" width="6" height="6" rx="1" fill="#C6C6C6" />
|
||||||
<rect x="85" y="161.5" width="6" height="6" rx="1" fill="#C6C6C6" />
|
<rect x="24.5" y="51" width="139" height="163" rx="6" stroke="#292929" />
|
||||||
<rect x="97" y="161.5" width="6" height="6" rx="1" fill="#3E3E3E" />
|
<rect x="176" y="50.5" width="108" height="164" rx="6" fill="#F3F4F6" />
|
||||||
<rect x="109" y="161.5" width="6" height="6" rx="1" fill="#C6C6C6" />
|
<rect x="24" y="226.5" width="260" height="38.5" rx="6" fill="#F3F4F6" />
|
||||||
<rect x="24.5" y="51" width="139" height="163" rx="6" stroke="#292929" />
|
</svg>
|
||||||
<rect x="176" y="50.5" width="108" height="164" rx="6" fill="#F3F4F6" />
|
),
|
||||||
<rect x="24" y="226.5" width="260" height="38.5" rx="6" fill="#F3F4F6" />
|
},
|
||||||
</svg>
|
{
|
||||||
),
|
title: t("floating_pop_up_button"),
|
||||||
},
|
subtitle: t("floating_button_trigger_modal"),
|
||||||
{
|
type: "floating-popup",
|
||||||
title: "Floating pop-up button",
|
illustration: (
|
||||||
subtitle: "Adds a floating button on your site that launches Cal in a dialog.",
|
<svg
|
||||||
type: "floating-popup",
|
width="100%"
|
||||||
illustration: (
|
height="100%"
|
||||||
<svg
|
className="rounded-md"
|
||||||
width="100%"
|
viewBox="0 0 308 265"
|
||||||
height="100%"
|
fill="none"
|
||||||
className="rounded-md"
|
xmlns="http://www.w3.org/2000/svg">
|
||||||
viewBox="0 0 308 265"
|
<path
|
||||||
fill="none"
|
d="M0 1.99999C0 0.895423 0.895431 0 2 0H306C307.105 0 308 0.895431 308 2V263C308 264.105 307.105 265 306 265H2C0.895431 265 0 264.105 0 263V1.99999Z"
|
||||||
xmlns="http://www.w3.org/2000/svg">
|
fill="white"
|
||||||
<path
|
/>
|
||||||
d="M0 1.99999C0 0.895423 0.895431 0 2 0H306C307.105 0 308 0.895431 308 2V263C308 264.105 307.105 265 306 265H2C0.895431 265 0 264.105 0 263V1.99999Z"
|
<rect x="24" width="260" height="38.5" rx="6" fill="#F3F4F6" />
|
||||||
fill="white"
|
<rect x="24" y="50.5" width="120" height="76" rx="6" fill="#F3F4F6" />
|
||||||
/>
|
<rect x="24" y="138.5" width="120" height="76" rx="6" fill="#F3F4F6" />
|
||||||
<rect x="24" width="260" height="38.5" rx="6" fill="#F3F4F6" />
|
<rect x="156" y="50.5" width="128" height="164" rx="6" fill="#F3F4F6" />
|
||||||
<rect x="24" y="50.5" width="120" height="76" rx="6" fill="#F3F4F6" />
|
<rect x="24" y="226.5" width="260" height="38.5" rx="6" fill="#F3F4F6" />
|
||||||
<rect x="24" y="138.5" width="120" height="76" rx="6" fill="#F3F4F6" />
|
<rect x="226" y="223.5" width="66" height="26" rx="6" fill="#292929" />
|
||||||
<rect x="156" y="50.5" width="128" height="164" rx="6" fill="#F3F4F6" />
|
<rect x="242" y="235.5" width="34" height="2" rx="1" fill="white" />
|
||||||
<rect x="24" y="226.5" width="260" height="38.5" rx="6" fill="#F3F4F6" />
|
</svg>
|
||||||
<rect x="226" y="223.5" width="66" height="26" rx="6" fill="#292929" />
|
),
|
||||||
<rect x="242" y="235.5" width="34" height="2" rx="1" fill="white" />
|
},
|
||||||
</svg>
|
{
|
||||||
),
|
title: t("pop_up_element_click"),
|
||||||
},
|
subtitle: t("open_dialog_with_element_click"),
|
||||||
{
|
type: "element-click",
|
||||||
title: "Pop up via element click",
|
illustration: (
|
||||||
subtitle: "Open your Cal dialog when someone clicks an element.",
|
<svg
|
||||||
type: "element-click",
|
width="100%"
|
||||||
illustration: (
|
height="100%"
|
||||||
<svg
|
className="rounded-md"
|
||||||
width="100%"
|
viewBox="0 0 308 265"
|
||||||
height="100%"
|
fill="none"
|
||||||
className="rounded-md"
|
xmlns="http://www.w3.org/2000/svg">
|
||||||
viewBox="0 0 308 265"
|
<path
|
||||||
fill="none"
|
d="M0 1.99999C0 0.895423 0.895431 0 2 0H306C307.105 0 308 0.895431 308 2V263C308 264.105 307.105 265 306 265H2C0.895431 265 0 264.105 0 263V1.99999Z"
|
||||||
xmlns="http://www.w3.org/2000/svg">
|
fill="white"
|
||||||
<path
|
/>
|
||||||
d="M0 1.99999C0 0.895423 0.895431 0 2 0H306C307.105 0 308 0.895431 308 2V263C308 264.105 307.105 265 306 265H2C0.895431 265 0 264.105 0 263V1.99999Z"
|
<rect x="24" y="0.50293" width="260" height="24" rx="6" fill="#F3F4F6" />
|
||||||
fill="white"
|
<rect x="24" y="35" width="259" height="192" rx="5.5" fill="#F9FAFB" />
|
||||||
/>
|
<g filter="url(#filter0_i_3223_14162)">
|
||||||
<rect x="24" y="0.50293" width="260" height="24" rx="6" fill="#F3F4F6" />
|
<rect opacity="0.8" x="40" y="99" width="24" height="24" rx="2" fill="#E5E7EB" />
|
||||||
<rect x="24" y="35" width="259" height="192" rx="5.5" fill="#F9FAFB" />
|
<rect x="40" y="127" width="48" height="8" rx="1" fill="#E5E7EB" />
|
||||||
<g filter="url(#filter0_i_3223_14162)">
|
<rect x="40" y="139" width="82" height="8" rx="1" fill="#E5E7EB" />
|
||||||
<rect opacity="0.8" x="40" y="99" width="24" height="24" rx="2" fill="#E5E7EB" />
|
<rect x="40" y="151" width="34" height="4" rx="1" fill="#E5E7EB" />
|
||||||
<rect x="40" y="127" width="48" height="8" rx="1" fill="#E5E7EB" />
|
<rect x="40" y="159" width="34" height="4" rx="1" fill="#E5E7EB" />
|
||||||
<rect x="40" y="139" width="82" height="8" rx="1" fill="#E5E7EB" />
|
</g>
|
||||||
<rect x="40" y="151" width="34" height="4" rx="1" fill="#E5E7EB" />
|
<rect x="152" y="48" width="2" height="169" rx="2" fill="#E5E7EB" />
|
||||||
<rect x="40" y="159" width="34" height="4" rx="1" fill="#E5E7EB" />
|
|
||||||
</g>
|
<rect opacity="0.8" x="176" y="84" width="80" height="8" rx="2" fill="#E5E7EB" />
|
||||||
<rect x="152" y="48" width="2" height="169" rx="2" fill="#E5E7EB" />
|
<rect x="176" y="96" width="48" height="4" rx="1" fill="#E5E7EB" />
|
||||||
|
<rect x="177" y="109" width="6" height="6" rx="1" fill="#E5E7EB" />
|
||||||
|
<rect x="189" y="109" width="6" height="6" rx="1" fill="#0D121D" />
|
||||||
|
<rect x="201" y="109" width="6" height="6" rx="1" fill="#E5E7EB" />
|
||||||
|
<rect x="213" y="109" width="6" height="6" rx="1" fill="#E5E7EB" />
|
||||||
|
<rect x="225" y="109" width="6" height="6" rx="1" fill="#E5E7EB" />
|
||||||
|
<rect x="237" y="109" width="6" height="6" rx="1" fill="#E5E7EB" />
|
||||||
|
<rect x="249" y="109" width="6" height="6" rx="1" fill="#E5E7EB" />
|
||||||
|
<rect x="261" y="109" width="6" height="6" rx="1" fill="#E5E7EB" />
|
||||||
|
<rect x="213" y="123" width="6" height="6" rx="1" fill="#E5E7EB" />
|
||||||
|
<rect x="225" y="123" width="6" height="6" rx="1" fill="#E5E7EB" />
|
||||||
|
<rect x="237" y="123" width="6" height="6" rx="1" fill="#E5E7EB" />
|
||||||
|
<rect x="249" y="123" width="6" height="6" rx="1" fill="#E5E7EB" />
|
||||||
|
<rect x="261" y="123" width="6" height="6" rx="1" fill="#E5E7EB" />
|
||||||
|
<rect x="177" y="135" width="6" height="6" rx="1" fill="#E5E7EB" />
|
||||||
|
<rect x="189" y="135" width="6" height="6" rx="1" fill="#0D121D" />
|
||||||
|
<rect x="187.3" y="133.4" width="9" height="9" rx="1.5" stroke="#0D121D" />
|
||||||
|
<rect x="201" y="135" width="6" height="6" rx="1" fill="#E5E7EB" />
|
||||||
|
<rect x="213" y="135" width="6" height="6" rx="1" fill="#E5E7EB" />
|
||||||
|
<rect x="225" y="135" width="6" height="6" rx="1" fill="#E5E7EB" />
|
||||||
|
<rect x="237" y="135" width="6" height="6" rx="1" fill="#E5E7EB" />
|
||||||
|
<rect x="249" y="135" width="6" height="6" rx="1" fill="#E5E7EB" />
|
||||||
|
<rect x="261" y="135" width="6" height="6" rx="1" fill="#E5E7EB" />
|
||||||
|
<rect x="177" y="147" width="6" height="6" rx="1" fill="#E5E7EB" />
|
||||||
|
<rect x="189" y="147" width="6" height="6" rx="1" fill="#E5E7EB" />
|
||||||
|
<rect x="201" y="147" width="6" height="6" rx="1" fill="#E5E7EB" />
|
||||||
|
<rect x="213" y="147" width="6" height="6" rx="1" fill="#E5E7EB" />
|
||||||
|
<rect x="225" y="147" width="6" height="6" rx="1" fill="#0D121D" />
|
||||||
|
<rect x="237" y="147" width="6" height="6" rx="1" fill="#0D121D" />
|
||||||
|
<rect x="249" y="147" width="6" height="6" rx="1" fill="#E5E7EB" />
|
||||||
|
<rect x="261" y="147" width="6" height="6" rx="1" fill="#E5E7EB" />
|
||||||
|
<rect x="177" y="159" width="6" height="6" rx="1" fill="#E5E7EB" />
|
||||||
|
<rect x="189" y="159" width="6" height="6" rx="1" fill="#E5E7EB" />
|
||||||
|
<rect x="201" y="159" width="6" height="6" rx="1" fill="#E5E7EB" />
|
||||||
|
<rect x="213" y="159" width="6" height="6" rx="1" fill="#E5E7EB" />
|
||||||
|
<rect x="225" y="159" width="6" height="6" rx="1" fill="#0D121D" />
|
||||||
|
<rect x="237" y="159" width="6" height="6" rx="1" fill="#0D121D" />
|
||||||
|
<rect x="249" y="159" width="6" height="6" rx="1" fill="#E5E7EB" />
|
||||||
|
<rect x="261" y="159" width="6" height="6" rx="1" fill="#E5E7EB" />
|
||||||
|
<rect x="177" y="171" width="6" height="6" rx="1" fill="#E5E7EB" />
|
||||||
|
<rect x="189" y="171" width="6" height="6" rx="1" fill="#E5E7EB" />
|
||||||
|
<rect x="201" y="171" width="6" height="6" rx="1" fill="#E5E7EB" />
|
||||||
|
<rect x="213" y="171" width="6" height="6" rx="1" fill="#E5E7EB" />
|
||||||
|
<rect x="225" y="171" width="6" height="6" rx="1" fill="#0D121D" />
|
||||||
|
<rect x="237" y="171" width="6" height="6" rx="1" fill="#E5E7EB" />
|
||||||
|
<rect x="24" y="35" width="259" height="192" rx="5.5" stroke="#101010" />
|
||||||
|
<rect x="24" y="241.503" width="260" height="24" rx="6" fill="#F3F4F6" />
|
||||||
|
</svg>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
];
|
||||||
|
})();
|
||||||
|
|
||||||
<rect opacity="0.8" x="176" y="84" width="80" height="8" rx="2" fill="#E5E7EB" />
|
|
||||||
<rect x="176" y="96" width="48" height="4" rx="1" fill="#E5E7EB" />
|
|
||||||
<rect x="177" y="109" width="6" height="6" rx="1" fill="#E5E7EB" />
|
|
||||||
<rect x="189" y="109" width="6" height="6" rx="1" fill="#0D121D" />
|
|
||||||
<rect x="201" y="109" width="6" height="6" rx="1" fill="#E5E7EB" />
|
|
||||||
<rect x="213" y="109" width="6" height="6" rx="1" fill="#E5E7EB" />
|
|
||||||
<rect x="225" y="109" width="6" height="6" rx="1" fill="#E5E7EB" />
|
|
||||||
<rect x="237" y="109" width="6" height="6" rx="1" fill="#E5E7EB" />
|
|
||||||
<rect x="249" y="109" width="6" height="6" rx="1" fill="#E5E7EB" />
|
|
||||||
<rect x="261" y="109" width="6" height="6" rx="1" fill="#E5E7EB" />
|
|
||||||
<rect x="213" y="123" width="6" height="6" rx="1" fill="#E5E7EB" />
|
|
||||||
<rect x="225" y="123" width="6" height="6" rx="1" fill="#E5E7EB" />
|
|
||||||
<rect x="237" y="123" width="6" height="6" rx="1" fill="#E5E7EB" />
|
|
||||||
<rect x="249" y="123" width="6" height="6" rx="1" fill="#E5E7EB" />
|
|
||||||
<rect x="261" y="123" width="6" height="6" rx="1" fill="#E5E7EB" />
|
|
||||||
<rect x="177" y="135" width="6" height="6" rx="1" fill="#E5E7EB" />
|
|
||||||
<rect x="189" y="135" width="6" height="6" rx="1" fill="#0D121D" />
|
|
||||||
<rect x="187.3" y="133.4" width="9" height="9" rx="1.5" stroke="#0D121D" />
|
|
||||||
<rect x="201" y="135" width="6" height="6" rx="1" fill="#E5E7EB" />
|
|
||||||
<rect x="213" y="135" width="6" height="6" rx="1" fill="#E5E7EB" />
|
|
||||||
<rect x="225" y="135" width="6" height="6" rx="1" fill="#E5E7EB" />
|
|
||||||
<rect x="237" y="135" width="6" height="6" rx="1" fill="#E5E7EB" />
|
|
||||||
<rect x="249" y="135" width="6" height="6" rx="1" fill="#E5E7EB" />
|
|
||||||
<rect x="261" y="135" width="6" height="6" rx="1" fill="#E5E7EB" />
|
|
||||||
<rect x="177" y="147" width="6" height="6" rx="1" fill="#E5E7EB" />
|
|
||||||
<rect x="189" y="147" width="6" height="6" rx="1" fill="#E5E7EB" />
|
|
||||||
<rect x="201" y="147" width="6" height="6" rx="1" fill="#E5E7EB" />
|
|
||||||
<rect x="213" y="147" width="6" height="6" rx="1" fill="#E5E7EB" />
|
|
||||||
<rect x="225" y="147" width="6" height="6" rx="1" fill="#0D121D" />
|
|
||||||
<rect x="237" y="147" width="6" height="6" rx="1" fill="#0D121D" />
|
|
||||||
<rect x="249" y="147" width="6" height="6" rx="1" fill="#E5E7EB" />
|
|
||||||
<rect x="261" y="147" width="6" height="6" rx="1" fill="#E5E7EB" />
|
|
||||||
<rect x="177" y="159" width="6" height="6" rx="1" fill="#E5E7EB" />
|
|
||||||
<rect x="189" y="159" width="6" height="6" rx="1" fill="#E5E7EB" />
|
|
||||||
<rect x="201" y="159" width="6" height="6" rx="1" fill="#E5E7EB" />
|
|
||||||
<rect x="213" y="159" width="6" height="6" rx="1" fill="#E5E7EB" />
|
|
||||||
<rect x="225" y="159" width="6" height="6" rx="1" fill="#0D121D" />
|
|
||||||
<rect x="237" y="159" width="6" height="6" rx="1" fill="#0D121D" />
|
|
||||||
<rect x="249" y="159" width="6" height="6" rx="1" fill="#E5E7EB" />
|
|
||||||
<rect x="261" y="159" width="6" height="6" rx="1" fill="#E5E7EB" />
|
|
||||||
<rect x="177" y="171" width="6" height="6" rx="1" fill="#E5E7EB" />
|
|
||||||
<rect x="189" y="171" width="6" height="6" rx="1" fill="#E5E7EB" />
|
|
||||||
<rect x="201" y="171" width="6" height="6" rx="1" fill="#E5E7EB" />
|
|
||||||
<rect x="213" y="171" width="6" height="6" rx="1" fill="#E5E7EB" />
|
|
||||||
<rect x="225" y="171" width="6" height="6" rx="1" fill="#0D121D" />
|
|
||||||
<rect x="237" y="171" width="6" height="6" rx="1" fill="#E5E7EB" />
|
|
||||||
<rect x="24" y="35" width="259" height="192" rx="5.5" stroke="#101010" />
|
|
||||||
<rect x="24" y="241.503" width="260" height="24" rx="6" fill="#F3F4F6" />
|
|
||||||
</svg>
|
|
||||||
),
|
|
||||||
},
|
|
||||||
];
|
|
||||||
const tabs = [
|
const tabs = [
|
||||||
{
|
{
|
||||||
name: "HTML",
|
name: "HTML",
|
||||||
href: "embedTabName=embed-code",
|
href: "embedTabName=embed-code",
|
||||||
icon: FiCode,
|
icon: Code,
|
||||||
type: "code",
|
type: "code",
|
||||||
Component: forwardRef<
|
Component: forwardRef<
|
||||||
HTMLTextAreaElement | HTMLIFrameElement | null,
|
HTMLTextAreaElement | HTMLIFrameElement | null,
|
||||||
@@ -509,7 +508,7 @@ const tabs = [
|
|||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div>
|
<div>
|
||||||
<small className="flex py-4 text-gray-500">
|
<small className="text-subtle flex py-4">
|
||||||
{t("place_where_cal_widget_appear", { appName: APP_NAME })}
|
{t("place_where_cal_widget_appear", { appName: APP_NAME })}
|
||||||
</small>
|
</small>
|
||||||
</div>
|
</div>
|
||||||
@@ -517,7 +516,7 @@ const tabs = [
|
|||||||
data-testid="embed-code"
|
data-testid="embed-code"
|
||||||
ref={ref as typeof ref & MutableRefObject<HTMLTextAreaElement>}
|
ref={ref as typeof ref & MutableRefObject<HTMLTextAreaElement>}
|
||||||
name="embed-code"
|
name="embed-code"
|
||||||
className="h-[calc(100%-50px)] font-mono"
|
className="text-default bg-default selection:bg-subtle h-[calc(100%-50px)] font-mono"
|
||||||
style={{ resize: "none", overflow: "auto" }}
|
style={{ resize: "none", overflow: "auto" }}
|
||||||
readOnly
|
readOnly
|
||||||
value={
|
value={
|
||||||
@@ -534,11 +533,7 @@ ${getEmbedTypeSpecificString({ embedFramework: "HTML", embedType, calLink, previ
|
|||||||
<!-- Cal ${embedType} embed code ends -->`
|
<!-- Cal ${embedType} embed code ends -->`
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
<p className="hidden text-sm text-gray-500">
|
<p className="text-subtle hidden text-sm">{t("need_help_embedding")}</p>
|
||||||
{t(
|
|
||||||
"Need help? See our guides for embedding Cal on Wix, Squarespace, or WordPress, check our common questions, or explore advanced embed options."
|
|
||||||
)}
|
|
||||||
</p>
|
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}),
|
}),
|
||||||
@@ -546,7 +541,7 @@ ${getEmbedTypeSpecificString({ embedFramework: "HTML", embedType, calLink, previ
|
|||||||
{
|
{
|
||||||
name: "React",
|
name: "React",
|
||||||
href: "embedTabName=embed-react",
|
href: "embedTabName=embed-react",
|
||||||
icon: FiCode,
|
icon: Code,
|
||||||
type: "code",
|
type: "code",
|
||||||
Component: forwardRef<
|
Component: forwardRef<
|
||||||
HTMLTextAreaElement | HTMLIFrameElement | null,
|
HTMLTextAreaElement | HTMLIFrameElement | null,
|
||||||
@@ -561,12 +556,12 @@ ${getEmbedTypeSpecificString({ embedFramework: "HTML", embedType, calLink, previ
|
|||||||
}
|
}
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<small className="flex py-4 text-gray-500">{t("create_update_react_component")}</small>
|
<small className="text-subtle flex py-4">{t("create_update_react_component")}</small>
|
||||||
<TextArea
|
<TextArea
|
||||||
data-testid="embed-react"
|
data-testid="embed-react"
|
||||||
ref={ref as typeof ref & MutableRefObject<HTMLTextAreaElement>}
|
ref={ref as typeof ref & MutableRefObject<HTMLTextAreaElement>}
|
||||||
name="embed-react"
|
name="embed-react"
|
||||||
className="h-[calc(100%-50px)] font-mono"
|
className="text-default bg-default selection:bg-subtle h-[calc(100%-50px)] font-mono"
|
||||||
readOnly
|
readOnly
|
||||||
style={{ resize: "none", overflow: "auto" }}
|
style={{ resize: "none", overflow: "auto" }}
|
||||||
value={`/* First make sure that you have installed the package */
|
value={`/* First make sure that you have installed the package */
|
||||||
@@ -586,7 +581,7 @@ ${getEmbedTypeSpecificString({ embedFramework: "react", embedType, calLink, prev
|
|||||||
{
|
{
|
||||||
name: "Preview",
|
name: "Preview",
|
||||||
href: "embedTabName=embed-preview",
|
href: "embedTabName=embed-preview",
|
||||||
icon: FiTrello,
|
icon: Trello,
|
||||||
type: "iframe",
|
type: "iframe",
|
||||||
Component: forwardRef<
|
Component: forwardRef<
|
||||||
HTMLIFrameElement | HTMLTextAreaElement | null,
|
HTMLIFrameElement | HTMLTextAreaElement | null,
|
||||||
@@ -622,7 +617,7 @@ Cal("init", {origin:"${WEBAPP_URL}"});
|
|||||||
const ThemeSelectControl = ({ children, ...props }: ControlProps<{ value: Theme; label: string }, false>) => {
|
const ThemeSelectControl = ({ children, ...props }: ControlProps<{ value: Theme; label: string }, false>) => {
|
||||||
return (
|
return (
|
||||||
<components.Control {...props}>
|
<components.Control {...props}>
|
||||||
<FiSun className="ml-2 h-4 w-4 text-gray-500" />
|
<Sun className="text-subtle mr-2 h-4 w-4" />
|
||||||
{children}
|
{children}
|
||||||
</components.Control>
|
</components.Control>
|
||||||
);
|
);
|
||||||
@@ -634,17 +629,17 @@ const ChooseEmbedTypesDialogContent = () => {
|
|||||||
return (
|
return (
|
||||||
<DialogContent className="rounded-lg p-10" type="creation" size="lg">
|
<DialogContent className="rounded-lg p-10" type="creation" size="lg">
|
||||||
<div className="mb-2">
|
<div className="mb-2">
|
||||||
<h3 className="mb-2 text-2xl font-bold leading-none text-gray-900" id="modal-title">
|
<h3 className="font-cal text-emphasis mb-2 text-2xl font-bold leading-none" id="modal-title">
|
||||||
{t("how_you_want_add_cal_site", { appName: APP_NAME })}
|
{t("how_you_want_add_cal_site", { appName: APP_NAME })}
|
||||||
</h3>
|
</h3>
|
||||||
<div>
|
<div>
|
||||||
<p className="text-sm text-gray-500">{t("choose_ways_put_cal_site", { appName: APP_NAME })}</p>
|
<p className="text-subtle text-sm">{t("choose_ways_put_cal_site", { appName: APP_NAME })}</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-start">
|
<div className="items-start space-y-2 md:flex md:space-y-0">
|
||||||
{embeds.map((embed, index) => (
|
{embeds(t).map((embed, index) => (
|
||||||
<button
|
<button
|
||||||
className="hover:border-primary-900 w-1/3 rounded-md border border-gray-200 bg-gray-50 p-6 text-left hover:rounded-md hover:bg-gray-100 ltr:mr-4 ltr:last:mr-0 rtl:ml-4 rtl:last:ml-0"
|
className="hover:bg-subtle bg-muted w-full rounded-md border border-transparent p-6 text-left hover:rounded-md ltr:mr-4 ltr:last:mr-0 rtl:ml-4 rtl:last:ml-0 lg:w-1/3"
|
||||||
key={index}
|
key={index}
|
||||||
data-testid={embed.type}
|
data-testid={embed.type}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
@@ -652,11 +647,11 @@ const ChooseEmbedTypesDialogContent = () => {
|
|||||||
embedType: embed.type,
|
embedType: embed.type,
|
||||||
});
|
});
|
||||||
}}>
|
}}>
|
||||||
<div className="order-none box-border flex-none rounded-md border border-solid bg-white">
|
<div className="bg-default order-none box-border flex-none rounded-md border border-solid dark:bg-transparent dark:invert">
|
||||||
{embed.illustration}
|
{embed.illustration}
|
||||||
</div>
|
</div>
|
||||||
<div className="mt-4 font-semibold leading-none text-gray-900">{embed.title}</div>
|
<div className="text-emphasis mt-4 font-semibold">{embed.title}</div>
|
||||||
<p className="mt-2 text-sm text-gray-500">{embed.subtitle}</p>
|
<p className="text-subtle mt-2 text-sm">{embed.subtitle}</p>
|
||||||
</button>
|
</button>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
@@ -692,7 +687,7 @@ const EmbedTypeCodeAndPreviewDialogContent = ({
|
|||||||
});
|
});
|
||||||
|
|
||||||
const refOfEmbedCodesRefs = useRef(embedCodeRefs);
|
const refOfEmbedCodesRefs = useRef(embedCodeRefs);
|
||||||
const embed = embeds.find((embed) => embed.type === embedType);
|
const embed = embeds(t).find((embed) => embed.type === embedType);
|
||||||
|
|
||||||
const [isEmbedCustomizationOpen, setIsEmbedCustomizationOpen] = useState(true);
|
const [isEmbedCustomizationOpen, setIsEmbedCustomizationOpen] = useState(true);
|
||||||
const [isBookingCustomizationOpen, setIsBookingCustomizationOpen] = useState(true);
|
const [isBookingCustomizationOpen, setIsBookingCustomizationOpen] = useState(true);
|
||||||
@@ -798,7 +793,7 @@ const EmbedTypeCodeAndPreviewDialogContent = ({
|
|||||||
}
|
}
|
||||||
|
|
||||||
const ThemeOptions = [
|
const ThemeOptions = [
|
||||||
{ value: Theme.auto, label: "Auto Theme" },
|
{ value: Theme.auto, label: "Auto" },
|
||||||
{ value: Theme.dark, label: "Dark Theme" },
|
{ value: Theme.dark, label: "Dark Theme" },
|
||||||
{ value: Theme.light, label: "Light Theme" },
|
{ value: Theme.light, label: "Light Theme" },
|
||||||
];
|
];
|
||||||
@@ -806,76 +801,71 @@ const EmbedTypeCodeAndPreviewDialogContent = ({
|
|||||||
const FloatingPopupPositionOptions = [
|
const FloatingPopupPositionOptions = [
|
||||||
{
|
{
|
||||||
value: "bottom-right",
|
value: "bottom-right",
|
||||||
label: "Bottom Right",
|
label: "Bottom right",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
value: "bottom-left",
|
value: "bottom-left",
|
||||||
label: "Bottom Left",
|
label: "Bottom left",
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<DialogContent ref={dialogContentRef} size="xl" className="p-0.5" type="creation">
|
<DialogContent
|
||||||
|
ref={dialogContentRef}
|
||||||
|
className="rounded-lg p-0.5 sm:max-w-[80rem]"
|
||||||
|
enableOverflow
|
||||||
|
type="creation">
|
||||||
<div className="flex">
|
<div className="flex">
|
||||||
<div className="flex w-1/3 flex-col bg-gray-50 p-8">
|
<div className="bg-muted flex w-1/3 flex-col p-8">
|
||||||
<h3 className="mb-2 flex text-xl font-bold leading-6 text-gray-900" id="modal-title">
|
<h3
|
||||||
|
className="text-emphasis mb-2.5 flex items-center text-xl font-semibold leading-5"
|
||||||
|
id="modal-title">
|
||||||
<button
|
<button
|
||||||
|
className="h-6 w-6"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
removeQueryParams(router, ["embedType", "embedTabName"]);
|
removeQueryParams(router, ["embedType", "embedTabName"]);
|
||||||
}}>
|
}}>
|
||||||
<FiArrowLeft className="mr-4 w-4" />
|
<ArrowLeft className="mr-4 w-4" />
|
||||||
</button>
|
</button>
|
||||||
{embed.title}
|
{embed.title}
|
||||||
</h3>
|
</h3>
|
||||||
<hr className={classNames("mt-4", embedType === "element-click" ? "hidden" : "")} />
|
<h4 className="text-subtle mb-6 text-sm font-normal">{embed.subtitle}</h4>
|
||||||
<div className="flex flex-col overflow-y-auto">
|
<div className="flex flex-col">
|
||||||
<div className={classNames("mt-4 font-medium", embedType === "element-click" ? "hidden" : "")}>
|
<div className={classNames("font-medium", embedType === "element-click" ? "hidden" : "")}>
|
||||||
<Collapsible
|
<Collapsible
|
||||||
open={isEmbedCustomizationOpen}
|
open={isEmbedCustomizationOpen}
|
||||||
onOpenChange={() => setIsEmbedCustomizationOpen((val) => !val)}>
|
onOpenChange={() => setIsEmbedCustomizationOpen((val) => !val)}>
|
||||||
<CollapsibleTrigger
|
|
||||||
type="button"
|
|
||||||
className="flex w-full items-center text-base font-medium text-gray-900">
|
|
||||||
<div>
|
|
||||||
{embedType === "inline"
|
|
||||||
? "Inline Embed Customization"
|
|
||||||
: embedType === "floating-popup"
|
|
||||||
? "Floating Popup Customization"
|
|
||||||
: "Element Click Customization"}
|
|
||||||
</div>
|
|
||||||
<FiChevronRight
|
|
||||||
className={`${
|
|
||||||
isEmbedCustomizationOpen ? "rotate-90 transform" : ""
|
|
||||||
} ml-auto h-5 w-5 text-gray-500`}
|
|
||||||
/>
|
|
||||||
</CollapsibleTrigger>
|
|
||||||
<CollapsibleContent className="text-sm">
|
<CollapsibleContent className="text-sm">
|
||||||
<div className={classNames("mt-6", embedType === "inline" ? "block" : "hidden")}>
|
<div className={classNames(embedType === "inline" ? "block" : "hidden")}>
|
||||||
{/*TODO: Add Auto/Fixed toggle from Figma */}
|
{/*TODO: Add Auto/Fixed toggle from Figma */}
|
||||||
<div className="text-sm">Embed Window Sizing</div>
|
<div className="text-default mb-[9px] text-sm">Window sizing</div>
|
||||||
<div className="justify-left flex items-center">
|
<div className="justify-left mb-6 flex items-center !font-normal ">
|
||||||
<TextField
|
<div className="mr-[9px]">
|
||||||
labelProps={{ className: "hidden" }}
|
<TextField
|
||||||
required
|
labelProps={{ className: "hidden" }}
|
||||||
value={previewState.inline.width}
|
className="focus:ring-offset-0"
|
||||||
onChange={(e) => {
|
required
|
||||||
setPreviewState((previewState) => {
|
value={previewState.inline.width}
|
||||||
const width = e.target.value || "100%";
|
onChange={(e) => {
|
||||||
|
setPreviewState((previewState) => {
|
||||||
|
const width = e.target.value || "100%";
|
||||||
|
|
||||||
|
return {
|
||||||
|
...previewState,
|
||||||
|
inline: {
|
||||||
|
...previewState.inline,
|
||||||
|
width,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
addOnLeading={<>W</>}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
return {
|
|
||||||
...previewState,
|
|
||||||
inline: {
|
|
||||||
...previewState.inline,
|
|
||||||
width,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
});
|
|
||||||
}}
|
|
||||||
addOnLeading={<>W</>}
|
|
||||||
/>
|
|
||||||
<span className="p-2">×</span>
|
|
||||||
<TextField
|
<TextField
|
||||||
labelProps={{ className: "hidden" }}
|
labelProps={{ className: "hidden" }}
|
||||||
|
className="focus:ring-offset-0"
|
||||||
value={previewState.inline.height}
|
value={previewState.inline.height}
|
||||||
required
|
required
|
||||||
onChange={(e) => {
|
onChange={(e) => {
|
||||||
@@ -897,10 +887,10 @@ const EmbedTypeCodeAndPreviewDialogContent = ({
|
|||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
className={classNames(
|
className={classNames(
|
||||||
"mt-4 items-center justify-between",
|
"items-center justify-between",
|
||||||
embedType === "floating-popup" ? "" : "hidden"
|
embedType === "floating-popup" ? "text-emphasis" : "hidden"
|
||||||
)}>
|
)}>
|
||||||
<div className="mb-2 text-sm">Button Text</div>
|
<div className="mb-2 text-sm">Button text</div>
|
||||||
{/* Default Values should come from preview iframe */}
|
{/* Default Values should come from preview iframe */}
|
||||||
<TextField
|
<TextField
|
||||||
labelProps={{ className: "hidden" }}
|
labelProps={{ className: "hidden" }}
|
||||||
@@ -915,14 +905,16 @@ const EmbedTypeCodeAndPreviewDialogContent = ({
|
|||||||
};
|
};
|
||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
defaultValue="Book my Cal"
|
defaultValue={t("book_my_cal")}
|
||||||
required
|
required
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
className={classNames(
|
className={classNames(
|
||||||
"mt-4 flex items-center justify-start",
|
"mt-4 flex items-center justify-start",
|
||||||
embedType === "floating-popup" ? "space-x-2 rtl:space-x-reverse" : "hidden"
|
embedType === "floating-popup"
|
||||||
|
? "text-emphasis space-x-2 rtl:space-x-reverse"
|
||||||
|
: "hidden"
|
||||||
)}>
|
)}>
|
||||||
<Switch
|
<Switch
|
||||||
defaultChecked={true}
|
defaultChecked={true}
|
||||||
@@ -938,14 +930,14 @@ const EmbedTypeCodeAndPreviewDialogContent = ({
|
|||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<div className="text-sm">Display Calendar Icon Button</div>
|
<div className="text-default my-2 text-sm">Display calendar icon</div>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
className={classNames(
|
className={classNames(
|
||||||
"mt-4 items-center justify-between",
|
"mt-4 items-center justify-between",
|
||||||
embedType === "floating-popup" ? "" : "hidden"
|
embedType === "floating-popup" ? "text-emphasis" : "hidden"
|
||||||
)}>
|
)}>
|
||||||
<div className="mb-2">Position of Button</div>
|
<div className="mb-2">Position of button</div>
|
||||||
<Select
|
<Select
|
||||||
onChange={(position) => {
|
onChange={(position) => {
|
||||||
setPreviewState((previewState) => {
|
setPreviewState((previewState) => {
|
||||||
@@ -962,87 +954,69 @@ const EmbedTypeCodeAndPreviewDialogContent = ({
|
|||||||
options={FloatingPopupPositionOptions}
|
options={FloatingPopupPositionOptions}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className={classNames("mt-4", embedType === "floating-popup" ? "" : "hidden")}>
|
<div className="mt-3 flex flex-col xl:flex-row xl:justify-between">
|
||||||
<div>Button Color</div>
|
<div className={classNames("mt-4", embedType === "floating-popup" ? "" : "hidden")}>
|
||||||
<div className="w-full">
|
<div className="whitespace-nowrap">Button color</div>
|
||||||
<ColorPicker
|
<div className="mt-2 w-40 xl:mt-0 xl:w-full">
|
||||||
popoverAlign="start"
|
<ColorPicker
|
||||||
container={dialogContentRef?.current ?? undefined}
|
className="w-[130px]"
|
||||||
defaultValue="#000000"
|
popoverAlign="start"
|
||||||
onChange={(color) => {
|
container={dialogContentRef?.current ?? undefined}
|
||||||
setPreviewState((previewState) => {
|
defaultValue="#000000"
|
||||||
return {
|
onChange={(color) => {
|
||||||
...previewState,
|
setPreviewState((previewState) => {
|
||||||
floatingPopup: {
|
return {
|
||||||
...previewState.floatingPopup,
|
...previewState,
|
||||||
buttonColor: color,
|
floatingPopup: {
|
||||||
},
|
...previewState.floatingPopup,
|
||||||
};
|
buttonColor: color,
|
||||||
});
|
},
|
||||||
}}
|
};
|
||||||
/>
|
});
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
<div className={classNames("mt-4", embedType === "floating-popup" ? "" : "hidden")}>
|
||||||
<div className={classNames("mt-4", embedType === "floating-popup" ? "" : "hidden")}>
|
<div className="whitespace-nowrap">Text color</div>
|
||||||
<div>Text Color</div>
|
<div className="mt-2 mb-6 w-40 xl:mt-0 xl:w-full">
|
||||||
<div className="w-full">
|
<ColorPicker
|
||||||
<ColorPicker
|
className="w-[130px]"
|
||||||
popoverAlign="start"
|
popoverAlign="start"
|
||||||
container={dialogContentRef?.current ?? undefined}
|
container={dialogContentRef?.current ?? undefined}
|
||||||
defaultValue="#000000"
|
defaultValue="#000000"
|
||||||
onChange={(color) => {
|
onChange={(color) => {
|
||||||
setPreviewState((previewState) => {
|
setPreviewState((previewState) => {
|
||||||
return {
|
return {
|
||||||
...previewState,
|
...previewState,
|
||||||
floatingPopup: {
|
floatingPopup: {
|
||||||
...previewState.floatingPopup,
|
...previewState.floatingPopup,
|
||||||
buttonTextColor: color,
|
buttonTextColor: color,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</CollapsibleContent>
|
</CollapsibleContent>
|
||||||
</Collapsible>
|
</Collapsible>
|
||||||
</div>
|
</div>
|
||||||
<hr className="mt-4" />
|
<div className="font-medium">
|
||||||
<div className="mt-4 font-medium">
|
|
||||||
<Collapsible
|
<Collapsible
|
||||||
open={isBookingCustomizationOpen}
|
open={isBookingCustomizationOpen}
|
||||||
onOpenChange={() => setIsBookingCustomizationOpen((val) => !val)}>
|
onOpenChange={() => setIsBookingCustomizationOpen((val) => !val)}>
|
||||||
<CollapsibleTrigger className="flex w-full" type="button">
|
|
||||||
<div className="text-base font-medium text-gray-900">Cal Booking Customization</div>
|
|
||||||
<FiChevronRight
|
|
||||||
className={`${
|
|
||||||
isBookingCustomizationOpen ? "rotate-90 transform" : ""
|
|
||||||
} ml-auto h-5 w-5 text-gray-500`}
|
|
||||||
/>
|
|
||||||
</CollapsibleTrigger>
|
|
||||||
<CollapsibleContent>
|
<CollapsibleContent>
|
||||||
<div className="mt-6 text-sm">
|
<div className="text-sm">
|
||||||
<div className="mb-4 flex items-center justify-start space-x-2 rtl:space-x-reverse">
|
<Label className="mb-6">
|
||||||
<Switch
|
|
||||||
checked={previewState.hideEventTypeDetails}
|
|
||||||
onCheckedChange={(checked) => {
|
|
||||||
setPreviewState((previewState) => {
|
|
||||||
return {
|
|
||||||
...previewState,
|
|
||||||
hideEventTypeDetails: checked,
|
|
||||||
};
|
|
||||||
});
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<div className="text-sm">{t("hide_eventtype_details")}</div>
|
|
||||||
</div>
|
|
||||||
<Label className="">
|
|
||||||
<div className="mb-2">Theme</div>
|
<div className="mb-2">Theme</div>
|
||||||
<Select
|
<Select
|
||||||
className="w-full"
|
className="w-full"
|
||||||
defaultValue={ThemeOptions[0]}
|
defaultValue={ThemeOptions[0]}
|
||||||
components={{
|
components={{
|
||||||
Control: ThemeSelectControl,
|
Control: ThemeSelectControl,
|
||||||
|
IndicatorSeparator: () => null,
|
||||||
}}
|
}}
|
||||||
onChange={(option) => {
|
onChange={(option) => {
|
||||||
if (!option) {
|
if (!option) {
|
||||||
@@ -1058,6 +1032,20 @@ const EmbedTypeCodeAndPreviewDialogContent = ({
|
|||||||
options={ThemeOptions}
|
options={ThemeOptions}
|
||||||
/>
|
/>
|
||||||
</Label>
|
</Label>
|
||||||
|
<div className="mb-6 flex items-center justify-start space-x-2 rtl:space-x-reverse">
|
||||||
|
<Switch
|
||||||
|
checked={previewState.hideEventTypeDetails}
|
||||||
|
onCheckedChange={(checked) => {
|
||||||
|
setPreviewState((previewState) => {
|
||||||
|
return {
|
||||||
|
...previewState,
|
||||||
|
hideEventTypeDetails: checked,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<div className="text-default text-sm">{t("hide_eventtype_details")}</div>
|
||||||
|
</div>
|
||||||
{[
|
{[
|
||||||
{ name: "brandColor", title: "Brand Color" },
|
{ name: "brandColor", title: "Brand Color" },
|
||||||
// { name: "lightColor", title: "Light Color" },
|
// { name: "lightColor", title: "Light Color" },
|
||||||
@@ -1066,8 +1054,8 @@ const EmbedTypeCodeAndPreviewDialogContent = ({
|
|||||||
// { name: "highlightColor", title: "Highlight Color" },
|
// { name: "highlightColor", title: "Highlight Color" },
|
||||||
// { name: "medianColor", title: "Median Color" },
|
// { name: "medianColor", title: "Median Color" },
|
||||||
].map((palette) => (
|
].map((palette) => (
|
||||||
<Label key={palette.name} className="pb-4">
|
<Label key={palette.name} className="mb-6">
|
||||||
<div className="mb-2 pt-2">{palette.title}</div>
|
<div className="mb-2">{palette.title}</div>
|
||||||
<div className="w-full">
|
<div className="w-full">
|
||||||
<ColorPicker
|
<ColorPicker
|
||||||
popoverAlign="start"
|
popoverAlign="start"
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
|
import { isEmpty } from "lodash";
|
||||||
import { useTranslation } from "next-i18next";
|
import { useTranslation } from "next-i18next";
|
||||||
import { useEffect } from "react";
|
import { useEffect } from "react";
|
||||||
|
|
||||||
import { getDirFromLang } from "@calcom/lib/i18n";
|
|
||||||
import { trpc } from "@calcom/trpc/react";
|
import { trpc } from "@calcom/trpc/react";
|
||||||
|
|
||||||
export function useViewerI18n() {
|
export function useViewerI18n() {
|
||||||
@@ -22,16 +22,18 @@ export function useViewerI18n() {
|
|||||||
*/
|
*/
|
||||||
const I18nLanguageHandler = (): null => {
|
const I18nLanguageHandler = (): null => {
|
||||||
const { i18n } = useTranslation("common");
|
const { i18n } = useTranslation("common");
|
||||||
const locale = useViewerI18n().data?.locale || "en";
|
const locale = useViewerI18n().data?.locale || i18n.language;
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (locale && i18n.language && i18n.language !== locale) {
|
// bail early when i18n = {}
|
||||||
if (typeof i18n.changeLanguage === "function") i18n.changeLanguage(locale);
|
if (isEmpty(i18n)) return;
|
||||||
|
// if locale is ready and the i18n.language does != locale - changeLanguage
|
||||||
|
if (locale && i18n.language !== locale) {
|
||||||
|
i18n.changeLanguage(locale);
|
||||||
}
|
}
|
||||||
|
// set dir="rtl|ltr"
|
||||||
const dir = getDirFromLang(locale);
|
document.dir = i18n.dir();
|
||||||
document.documentElement.setAttribute("lang", locale);
|
document.documentElement.setAttribute("lang", locale);
|
||||||
document.documentElement.setAttribute("dir", dir);
|
|
||||||
}, [locale, i18n]);
|
}, [locale, i18n]);
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
|
|||||||
@@ -1,167 +0,0 @@
|
|||||||
import type { FormEvent } from "react";
|
|
||||||
import { useCallback, useEffect, useState } from "react";
|
|
||||||
import Cropper from "react-easy-crop";
|
|
||||||
|
|
||||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
|
||||||
import { Button, Dialog, DialogClose, DialogContent, DialogTrigger } from "@calcom/ui";
|
|
||||||
|
|
||||||
import type { Area } from "@lib/cropImage";
|
|
||||||
import { getCroppedImg } from "@lib/cropImage";
|
|
||||||
import { useFileReader } from "@lib/hooks/useFileReader";
|
|
||||||
|
|
||||||
import Slider from "@components/Slider";
|
|
||||||
|
|
||||||
type ImageUploaderProps = {
|
|
||||||
id: string;
|
|
||||||
buttonMsg: string;
|
|
||||||
handleAvatarChange: (imageSrc: string) => void;
|
|
||||||
imageSrc?: string;
|
|
||||||
target: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
interface FileEvent<T = Element> extends FormEvent<T> {
|
|
||||||
target: EventTarget & T;
|
|
||||||
}
|
|
||||||
|
|
||||||
// This is separate to prevent loading the component until file upload
|
|
||||||
function CropContainer({
|
|
||||||
onCropComplete,
|
|
||||||
imageSrc,
|
|
||||||
}: {
|
|
||||||
imageSrc: string;
|
|
||||||
onCropComplete: (croppedAreaPixels: Area) => void;
|
|
||||||
}) {
|
|
||||||
const { t } = useLocale();
|
|
||||||
const [crop, setCrop] = useState({ x: 0, y: 0 });
|
|
||||||
const [zoom, setZoom] = useState(1);
|
|
||||||
|
|
||||||
const handleZoomSliderChange = (value: number) => {
|
|
||||||
value < 1 ? setZoom(1) : setZoom(value);
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="crop-container h-40 max-h-40 w-40 rounded-full">
|
|
||||||
<div className="relative h-40 w-40 rounded-full">
|
|
||||||
<Cropper
|
|
||||||
image={imageSrc}
|
|
||||||
crop={crop}
|
|
||||||
zoom={zoom}
|
|
||||||
aspect={1}
|
|
||||||
onCropChange={setCrop}
|
|
||||||
onCropComplete={(croppedArea, croppedAreaPixels) => onCropComplete(croppedAreaPixels)}
|
|
||||||
onZoomChange={setZoom}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<Slider
|
|
||||||
value={zoom}
|
|
||||||
min={1}
|
|
||||||
max={3}
|
|
||||||
step={0.1}
|
|
||||||
label={t("slide_zoom_drag_instructions")}
|
|
||||||
changeHandler={handleZoomSliderChange}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/** @deprecated Use `packages/ui/v2/core/ImageUploader.tsx` */
|
|
||||||
export default function ImageUploader({
|
|
||||||
target,
|
|
||||||
id,
|
|
||||||
buttonMsg,
|
|
||||||
handleAvatarChange,
|
|
||||||
...props
|
|
||||||
}: ImageUploaderProps) {
|
|
||||||
const { t } = useLocale();
|
|
||||||
const [imageSrc, setImageSrc] = useState<string | null>(null);
|
|
||||||
const [croppedAreaPixels, setCroppedAreaPixels] = useState<Area | null>(null);
|
|
||||||
|
|
||||||
const [{ result }, setFile] = useFileReader({
|
|
||||||
method: "readAsDataURL",
|
|
||||||
});
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (props.imageSrc) setImageSrc(props.imageSrc);
|
|
||||||
}, [props.imageSrc]);
|
|
||||||
|
|
||||||
const onInputFile = (e: FileEvent<HTMLInputElement>) => {
|
|
||||||
if (!e.target.files?.length) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
setFile(e.target.files[0]);
|
|
||||||
};
|
|
||||||
|
|
||||||
const showCroppedImage = useCallback(
|
|
||||||
async (croppedAreaPixels: Area | null) => {
|
|
||||||
try {
|
|
||||||
if (!croppedAreaPixels) return;
|
|
||||||
const croppedImage = await getCroppedImg(
|
|
||||||
result as string /* result is always string when using readAsDataUrl */,
|
|
||||||
croppedAreaPixels
|
|
||||||
);
|
|
||||||
setImageSrc(croppedImage);
|
|
||||||
handleAvatarChange(croppedImage);
|
|
||||||
} catch (e) {
|
|
||||||
console.error(e);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
[result, handleAvatarChange]
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Dialog
|
|
||||||
onOpenChange={
|
|
||||||
(opened) => !opened && setFile(null) // unset file on close
|
|
||||||
}>
|
|
||||||
<DialogTrigger asChild>
|
|
||||||
<div className="flex items-center">
|
|
||||||
<Button color="secondary" type="button" className="py-1 text-xs">
|
|
||||||
{buttonMsg}
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</DialogTrigger>
|
|
||||||
<DialogContent>
|
|
||||||
<div className="mb-4 sm:flex sm:items-start">
|
|
||||||
<div className="mt-3 text-center sm:mt-0 sm:text-left">
|
|
||||||
<h3 className="font-cal text-lg leading-6 text-gray-900" id="modal-title">
|
|
||||||
{t("upload_target", { target })}
|
|
||||||
</h3>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="mb-4">
|
|
||||||
<div className="cropper mt-6 flex flex-col items-center justify-center p-8">
|
|
||||||
{!result && (
|
|
||||||
<div className="flex h-20 max-h-20 w-20 items-center justify-start rounded-full bg-gray-50">
|
|
||||||
{!imageSrc && (
|
|
||||||
<p className="w-full text-center text-sm text-white sm:text-xs">
|
|
||||||
{t("no_target", { target })}
|
|
||||||
</p>
|
|
||||||
)}
|
|
||||||
{imageSrc && (
|
|
||||||
// eslint-disable-next-line @next/next/no-img-element
|
|
||||||
<img className="h-20 w-20 rounded-full" src={imageSrc} alt={target} />
|
|
||||||
)}
|
|
||||||
</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-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"
|
|
||||||
name={id}
|
|
||||||
placeholder={t("upload_image")}
|
|
||||||
className="pointer-events-none absolute mt-4 opacity-0"
|
|
||||||
accept="image/*"
|
|
||||||
/>
|
|
||||||
{t("choose_a_file")}
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="mt-5 gap-x-2 sm:mt-4 sm:flex sm:flex-row-reverse">
|
|
||||||
<DialogClose onClick={() => showCroppedImage(croppedAreaPixels)}>{t("save")}</DialogClose>
|
|
||||||
<DialogClose color="secondary">{t("cancel")}</DialogClose>
|
|
||||||
</div>
|
|
||||||
</DialogContent>
|
|
||||||
</Dialog>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,4 +1,3 @@
|
|||||||
import { AdminRequired } from "components/ui/AdminRequired";
|
|
||||||
import { noop } from "lodash";
|
import { noop } from "lodash";
|
||||||
import type { LinkProps } from "next/link";
|
import type { LinkProps } from "next/link";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
@@ -6,6 +5,7 @@ import { useRouter } from "next/router";
|
|||||||
import type { FC, MouseEventHandler } from "react";
|
import type { FC, MouseEventHandler } from "react";
|
||||||
import { Fragment } from "react";
|
import { Fragment } from "react";
|
||||||
|
|
||||||
|
import { PermissionContainer } from "@calcom/features/auth/PermissionContainer";
|
||||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||||
|
|
||||||
import classNames from "@lib/classNames";
|
import classNames from "@lib/classNames";
|
||||||
@@ -60,7 +60,7 @@ const NavTabs: FC<NavTabProps> = ({ tabs, linkProps, ...props }) => {
|
|||||||
}
|
}
|
||||||
: noop;
|
: noop;
|
||||||
|
|
||||||
const Component = tab.adminRequired ? AdminRequired : Fragment;
|
const Component = tab.adminRequired ? PermissionContainer : Fragment;
|
||||||
const className = tab.className || "";
|
const className = tab.className || "";
|
||||||
return (
|
return (
|
||||||
<Component key={tab.name}>
|
<Component key={tab.name}>
|
||||||
@@ -69,8 +69,8 @@ const NavTabs: FC<NavTabProps> = ({ tabs, linkProps, ...props }) => {
|
|||||||
onClick={onClick}
|
onClick={onClick}
|
||||||
className={classNames(
|
className={classNames(
|
||||||
isCurrent
|
isCurrent
|
||||||
? "border-gray-900 text-gray-900"
|
? "text-emphasis border-gray-900"
|
||||||
: "border-transparent text-gray-500 hover:border-gray-300 hover:text-gray-700",
|
: "hover:border-default hover:text-default text-subtle border-transparent",
|
||||||
"group inline-flex items-center border-b-2 py-4 px-1 text-sm font-medium",
|
"group inline-flex items-center border-b-2 py-4 px-1 text-sm font-medium",
|
||||||
className
|
className
|
||||||
)}
|
)}
|
||||||
@@ -78,7 +78,7 @@ const NavTabs: FC<NavTabProps> = ({ tabs, linkProps, ...props }) => {
|
|||||||
{tab.icon && (
|
{tab.icon && (
|
||||||
<tab.icon
|
<tab.icon
|
||||||
className={classNames(
|
className={classNames(
|
||||||
isCurrent ? "text-gray-900" : "text-gray-400 group-hover:text-gray-500",
|
isCurrent ? "text-emphasis" : "group-hover:text-subtle text-muted",
|
||||||
"-ml-0.5 hidden h-4 w-4 ltr:mr-2 rtl:ml-2 sm:inline-block"
|
"-ml-0.5 hidden h-4 w-4 ltr:mr-2 rtl:ml-2 sm:inline-block"
|
||||||
)}
|
)}
|
||||||
aria-hidden="true"
|
aria-hidden="true"
|
||||||
@@ -91,7 +91,7 @@ const NavTabs: FC<NavTabProps> = ({ tabs, linkProps, ...props }) => {
|
|||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
</nav>
|
</nav>
|
||||||
<hr />
|
<hr className="border-subtle" />
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -0,0 +1,82 @@
|
|||||||
|
import { DefaultSeo } from "next-seo";
|
||||||
|
import { Inter } from "next/font/google";
|
||||||
|
import localFont from "next/font/local";
|
||||||
|
import Script from "next/script";
|
||||||
|
|
||||||
|
import "@calcom/embed-core/src/embed-iframe";
|
||||||
|
import LicenseRequired from "@calcom/features/ee/common/components/LicenseRequired";
|
||||||
|
|
||||||
|
import type { AppProps } from "@lib/app-providers";
|
||||||
|
import AppProviders from "@lib/app-providers";
|
||||||
|
import { seoConfig } from "@lib/config/next-seo.config";
|
||||||
|
|
||||||
|
import I18nLanguageHandler from "@components/I18nLanguageHandler";
|
||||||
|
|
||||||
|
export interface CalPageWrapper {
|
||||||
|
(props?: any): JSX.Element;
|
||||||
|
PageWrapper?: AppProps["Component"]["PageWrapper"];
|
||||||
|
}
|
||||||
|
|
||||||
|
const interFont = Inter({ subsets: ["latin"], variable: "--font-inter", preload: true, display: "swap" });
|
||||||
|
const calFont = localFont({
|
||||||
|
src: "../fonts/CalSans-SemiBold.woff2",
|
||||||
|
variable: "--font-cal",
|
||||||
|
preload: true,
|
||||||
|
display: "swap",
|
||||||
|
});
|
||||||
|
|
||||||
|
function PageWrapper(props: AppProps) {
|
||||||
|
const { Component, pageProps, err, router } = props;
|
||||||
|
let pageStatus = "200";
|
||||||
|
|
||||||
|
if (router.pathname === "/404") {
|
||||||
|
pageStatus = "404";
|
||||||
|
} else if (router.pathname === "/500") {
|
||||||
|
pageStatus = "500";
|
||||||
|
}
|
||||||
|
|
||||||
|
// On client side don't let nonce creep into DOM
|
||||||
|
// It also avoids hydration warning that says that Client has the nonce value but server has "" because browser removes nonce attributes before DOM is built
|
||||||
|
// See https://github.com/kentcdodds/nonce-hydration-issues
|
||||||
|
// Set "" only if server had it set otherwise keep it undefined because server has to match with client to avoid hydration error
|
||||||
|
const nonce = typeof window !== "undefined" ? (pageProps.nonce ? "" : undefined) : pageProps.nonce;
|
||||||
|
const providerProps = {
|
||||||
|
...props,
|
||||||
|
pageProps: {
|
||||||
|
...props.pageProps,
|
||||||
|
nonce,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
// Use the layout defined at the page level, if available
|
||||||
|
const getLayout = Component.getLayout ?? ((page) => page);
|
||||||
|
return (
|
||||||
|
<AppProviders {...providerProps}>
|
||||||
|
<DefaultSeo {...seoConfig.defaultNextSeo} />
|
||||||
|
<I18nLanguageHandler />
|
||||||
|
<Script
|
||||||
|
nonce={nonce}
|
||||||
|
id="page-status"
|
||||||
|
dangerouslySetInnerHTML={{ __html: `window.CalComPageStatus = '${pageStatus}'` }}
|
||||||
|
/>
|
||||||
|
<style jsx global>{`
|
||||||
|
:root {
|
||||||
|
--font-inter: ${interFont.style.fontFamily};
|
||||||
|
--font-cal: ${calFont.style.fontFamily};
|
||||||
|
}
|
||||||
|
`}</style>
|
||||||
|
|
||||||
|
{getLayout(
|
||||||
|
Component.requiresLicense ? (
|
||||||
|
<LicenseRequired>
|
||||||
|
<Component {...pageProps} err={err} />
|
||||||
|
</LicenseRequired>
|
||||||
|
) : (
|
||||||
|
<Component {...pageProps} err={err} />
|
||||||
|
),
|
||||||
|
router
|
||||||
|
)}
|
||||||
|
</AppProviders>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default PageWrapper;
|
||||||
@@ -3,7 +3,7 @@ import React from "react";
|
|||||||
|
|
||||||
import Shell from "@calcom/features/shell/Shell";
|
import Shell from "@calcom/features/shell/Shell";
|
||||||
import { ErrorBoundary } from "@calcom/ui";
|
import { ErrorBoundary } from "@calcom/ui";
|
||||||
import { FiCreditCard, FiKey, FiLock, FiTerminal, FiUser, FiUsers } from "@calcom/ui/components/icon";
|
import { CreditCard, Key, Lock, Terminal, User, Users } from "@calcom/ui/components/icon";
|
||||||
|
|
||||||
import NavTabs from "./NavTabs";
|
import NavTabs from "./NavTabs";
|
||||||
|
|
||||||
@@ -11,32 +11,32 @@ const tabs = [
|
|||||||
{
|
{
|
||||||
name: "profile",
|
name: "profile",
|
||||||
href: "/settings/my-account/profile",
|
href: "/settings/my-account/profile",
|
||||||
icon: FiUser,
|
icon: User,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "teams",
|
name: "teams",
|
||||||
href: "/settings/teams",
|
href: "/settings/teams",
|
||||||
icon: FiUsers,
|
icon: Users,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "security",
|
name: "security",
|
||||||
href: "/settings/security",
|
href: "/settings/security",
|
||||||
icon: FiKey,
|
icon: Key,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "developer",
|
name: "developer",
|
||||||
href: "/settings/developer",
|
href: "/settings/developer",
|
||||||
icon: FiTerminal,
|
icon: Terminal,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "billing",
|
name: "billing",
|
||||||
href: "/settings/billing",
|
href: "/settings/billing",
|
||||||
icon: FiCreditCard,
|
icon: CreditCard,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "admin",
|
name: "admin",
|
||||||
href: "/settings/admin",
|
href: "/settings/admin",
|
||||||
icon: FiLock,
|
icon: Lock,
|
||||||
adminRequired: true,
|
adminRequired: true,
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
@@ -46,7 +46,7 @@ export default function SettingsShell({
|
|||||||
...rest
|
...rest
|
||||||
}: { children: React.ReactNode } & ComponentProps<typeof Shell>) {
|
}: { children: React.ReactNode } & ComponentProps<typeof Shell>) {
|
||||||
return (
|
return (
|
||||||
<Shell {...rest}>
|
<Shell {...rest} hideHeadingOnMobile>
|
||||||
<div className="sm:mx-auto">
|
<div className="sm:mx-auto">
|
||||||
<NavTabs tabs={tabs} />
|
<NavTabs tabs={tabs} />
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,27 +0,0 @@
|
|||||||
import * as SliderPrimitive from "@radix-ui/react-slider";
|
|
||||||
import React from "react";
|
|
||||||
|
|
||||||
const Slider = ({
|
|
||||||
value,
|
|
||||||
label,
|
|
||||||
changeHandler,
|
|
||||||
...props
|
|
||||||
}: Omit<SliderPrimitive.SliderProps, "value"> & {
|
|
||||||
value: number;
|
|
||||||
label: string;
|
|
||||||
changeHandler: (value: number) => void;
|
|
||||||
}) => (
|
|
||||||
<SliderPrimitive.Root
|
|
||||||
className="slider mt-2"
|
|
||||||
value={[value]}
|
|
||||||
aria-label={label}
|
|
||||||
onValueChange={(value: number[]) => changeHandler(value[0] ?? value)}
|
|
||||||
{...props}>
|
|
||||||
<SliderPrimitive.Track className="slider-track">
|
|
||||||
<SliderPrimitive.Range className="slider-range" />
|
|
||||||
</SliderPrimitive.Track>
|
|
||||||
<SliderPrimitive.Thumb className="slider-thumb" />
|
|
||||||
</SliderPrimitive.Root>
|
|
||||||
);
|
|
||||||
|
|
||||||
export default Slider;
|
|
||||||
@@ -1,11 +1,15 @@
|
|||||||
import type { OptionProps } from "react-select";
|
|
||||||
|
|
||||||
import { InstallAppButton } from "@calcom/app-store/components";
|
import { InstallAppButton } from "@calcom/app-store/components";
|
||||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||||
import { trpc } from "@calcom/trpc/react";
|
import { trpc } from "@calcom/trpc/react";
|
||||||
import type { App } from "@calcom/types/App";
|
import {
|
||||||
import { Button, Select } from "@calcom/ui";
|
Button,
|
||||||
import { FiPlus } from "@calcom/ui/components/icon";
|
Dropdown,
|
||||||
|
DropdownMenuContent,
|
||||||
|
DropdownMenuTrigger,
|
||||||
|
DropdownMenuItem,
|
||||||
|
DropdownItem,
|
||||||
|
} from "@calcom/ui";
|
||||||
|
import { Plus } from "@calcom/ui/components/icon";
|
||||||
|
|
||||||
import { QueryCell } from "@lib/QueryCell";
|
import { QueryCell } from "@lib/QueryCell";
|
||||||
|
|
||||||
@@ -13,31 +17,6 @@ interface AdditionalCalendarSelectorProps {
|
|||||||
isLoading?: boolean;
|
isLoading?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
const ImageOption = (optionProps: OptionProps<{ [key: string]: string; type: App["type"] }>) => {
|
|
||||||
const { t } = useLocale();
|
|
||||||
const { data } = optionProps;
|
|
||||||
return data.slug !== "add-new" ? (
|
|
||||||
<InstallAppButton
|
|
||||||
type={data.type}
|
|
||||||
render={(installProps) => {
|
|
||||||
return (
|
|
||||||
<Button {...installProps} className="flex w-full align-top" color="minimal">
|
|
||||||
{data.image && (
|
|
||||||
<img className="float-left mr-3 inline h-5 w-5" src={data.image} alt={data.label} />
|
|
||||||
)}
|
|
||||||
<p className="text-left">{`${t("add")} ${data.label}`}</p>
|
|
||||||
</Button>
|
|
||||||
);
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
) : (
|
|
||||||
<Button className="w-full" color="minimal" href="/apps/categories/calendar">
|
|
||||||
<FiPlus className="text-color mr-3 ml-1 h-4 w-4" />
|
|
||||||
<p>{t("install_new_calendar_app")}</p>
|
|
||||||
</Button>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
const AdditionalCalendarSelector = ({ isLoading }: AdditionalCalendarSelectorProps): JSX.Element | null => {
|
const AdditionalCalendarSelector = ({ isLoading }: AdditionalCalendarSelectorProps): JSX.Element | null => {
|
||||||
const { t } = useLocale();
|
const { t } = useLocale();
|
||||||
const query = trpc.viewer.integrations.useQuery({ variant: "calendar", onlyInstalled: true });
|
const query = trpc.viewer.integrations.useQuery({ variant: "calendar", onlyInstalled: true });
|
||||||
@@ -59,51 +38,39 @@ const AdditionalCalendarSelector = ({ isLoading }: AdditionalCalendarSelectorPro
|
|||||||
type: "new_other",
|
type: "new_other",
|
||||||
});
|
});
|
||||||
return (
|
return (
|
||||||
<Select
|
<Dropdown modal={false}>
|
||||||
name="additionalCalendar"
|
<DropdownMenuTrigger asChild>
|
||||||
placeholder={
|
<Button StartIcon={Plus} color="secondary" {...(isLoading && { loading: isLoading })}>
|
||||||
<Button StartIcon={FiPlus} color="secondary">
|
|
||||||
{t("add")}
|
{t("add")}
|
||||||
</Button>
|
</Button>
|
||||||
}
|
</DropdownMenuTrigger>
|
||||||
options={options}
|
<DropdownMenuContent>
|
||||||
isSearchable={false}
|
{options.map((data) => (
|
||||||
isLoading={isLoading}
|
<DropdownMenuItem key={data.slug} className="focus:outline-none">
|
||||||
components={{ Option: ImageOption }}
|
{data.slug === "add-new" ? (
|
||||||
styles={{
|
<DropdownItem StartIcon={Plus} color="minimal" href="/apps/categories/calendar">
|
||||||
menu: (defaultStyles) => ({
|
{t("install_new_calendar_app")}
|
||||||
...defaultStyles,
|
</DropdownItem>
|
||||||
"@media only screen and (max-width: 640px)": {
|
) : (
|
||||||
left: "0",
|
<InstallAppButton
|
||||||
},
|
type={data.type}
|
||||||
width: "max-content",
|
render={(installProps) => {
|
||||||
right: "0",
|
const props = { ...installProps } as any;
|
||||||
}),
|
return (
|
||||||
indicatorSeparator: (defaultStyles) => ({
|
<DropdownItem {...props} color="minimal" type="button">
|
||||||
...defaultStyles,
|
<span className="flex items-center gap-x-2">
|
||||||
display: "none",
|
{data.image && <img className="h-5 w-5" src={data.image} alt={data.label} />}
|
||||||
}),
|
{`${t("add")} ${data.label}`}
|
||||||
control: (defaultStyles) => ({
|
</span>
|
||||||
...defaultStyles,
|
</DropdownItem>
|
||||||
padding: "0",
|
);
|
||||||
border: "0",
|
}}
|
||||||
borderRadius: "6px",
|
/>
|
||||||
minHeight: "auto",
|
)}
|
||||||
}),
|
</DropdownMenuItem>
|
||||||
dropdownIndicator: (defaultStyles) => ({
|
))}
|
||||||
...defaultStyles,
|
</DropdownMenuContent>
|
||||||
display: "none",
|
</Dropdown>
|
||||||
}),
|
|
||||||
valueContainer: (defaultStyles) => ({
|
|
||||||
...defaultStyles,
|
|
||||||
padding: "0",
|
|
||||||
}),
|
|
||||||
placeholder: (defaultStyles) => ({
|
|
||||||
...defaultStyles,
|
|
||||||
margin: "0",
|
|
||||||
}),
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
);
|
);
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -1,12 +1,11 @@
|
|||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { useRouter } from "next/router";
|
|
||||||
import type { IframeHTMLAttributes } from "react";
|
import type { IframeHTMLAttributes } from "react";
|
||||||
import React, { useState } from "react";
|
import React, { useState } from "react";
|
||||||
|
|
||||||
import useAddAppMutation from "@calcom/app-store/_utils/useAddAppMutation";
|
import useAddAppMutation from "@calcom/app-store/_utils/useAddAppMutation";
|
||||||
import { InstallAppButton, AppDependencyComponent } from "@calcom/app-store/components";
|
import { InstallAppButton, AppDependencyComponent } from "@calcom/app-store/components";
|
||||||
import DisconnectIntegration from "@calcom/features/apps/components/DisconnectIntegration";
|
import DisconnectIntegration from "@calcom/features/apps/components/DisconnectIntegration";
|
||||||
import LicenseRequired from "@calcom/features/ee/common/components/v2/LicenseRequired";
|
import LicenseRequired from "@calcom/features/ee/common/components/LicenseRequired";
|
||||||
import Shell from "@calcom/features/shell/Shell";
|
import Shell from "@calcom/features/shell/Shell";
|
||||||
import classNames from "@calcom/lib/classNames";
|
import classNames from "@calcom/lib/classNames";
|
||||||
import { APP_NAME, COMPANY_NAME, SUPPORT_MAIL_ADDRESS } from "@calcom/lib/constants";
|
import { APP_NAME, COMPANY_NAME, SUPPORT_MAIL_ADDRESS } from "@calcom/lib/constants";
|
||||||
@@ -14,16 +13,7 @@ import { useLocale } from "@calcom/lib/hooks/useLocale";
|
|||||||
import { trpc } from "@calcom/trpc/react";
|
import { trpc } from "@calcom/trpc/react";
|
||||||
import type { App as AppType } from "@calcom/types/App";
|
import type { App as AppType } from "@calcom/types/App";
|
||||||
import { Button, showToast, SkeletonButton, SkeletonText, HeadSeo, Badge } from "@calcom/ui";
|
import { Button, showToast, SkeletonButton, SkeletonText, HeadSeo, Badge } from "@calcom/ui";
|
||||||
import {
|
import { BookOpen, Check, ExternalLink, File, Flag, Mail, Plus, Shield } from "@calcom/ui/components/icon";
|
||||||
FiBookOpen,
|
|
||||||
FiCheck,
|
|
||||||
FiExternalLink,
|
|
||||||
FiFile,
|
|
||||||
FiFlag,
|
|
||||||
FiMail,
|
|
||||||
FiPlus,
|
|
||||||
FiShield,
|
|
||||||
} from "@calcom/ui/components/icon";
|
|
||||||
|
|
||||||
/* These app slugs all require Google Cal to be installed */
|
/* These app slugs all require Google Cal to be installed */
|
||||||
|
|
||||||
@@ -46,13 +36,13 @@ const Component = ({
|
|||||||
tos,
|
tos,
|
||||||
privacy,
|
privacy,
|
||||||
isProOnly,
|
isProOnly,
|
||||||
|
teamsPlanRequired,
|
||||||
descriptionItems,
|
descriptionItems,
|
||||||
isTemplate,
|
isTemplate,
|
||||||
dependencies,
|
dependencies,
|
||||||
}: Parameters<typeof App>[0]) => {
|
}: Parameters<typeof App>[0]) => {
|
||||||
const { t } = useLocale();
|
const { t, i18n } = useLocale();
|
||||||
const hasDescriptionItems = descriptionItems && descriptionItems.length > 0;
|
const hasDescriptionItems = descriptionItems && descriptionItems.length > 0;
|
||||||
const router = useRouter();
|
|
||||||
|
|
||||||
const mutation = useAddAppMutation(null, {
|
const mutation = useAddAppMutation(null, {
|
||||||
onSuccess: (data) => {
|
onSuccess: (data) => {
|
||||||
@@ -96,7 +86,7 @@ const Component = ({
|
|||||||
return (
|
return (
|
||||||
<div className="relative flex-1 flex-col items-start justify-start px-4 md:flex md:px-8 lg:flex-row lg:px-0">
|
<div className="relative flex-1 flex-col items-start justify-start px-4 md:flex md:px-8 lg:flex-row lg:px-0">
|
||||||
{hasDescriptionItems && (
|
{hasDescriptionItems && (
|
||||||
<div className="align-center mb-4 -ml-4 -mr-4 flex min-h-[450px] w-auto basis-3/5 snap-x snap-mandatory flex-row overflow-auto whitespace-nowrap bg-gray-100 p-4 md:mb-8 md:-ml-8 md:-mr-8 md:p-8 lg:mx-0 lg:mb-0 lg:max-w-2xl lg:flex-col lg:justify-center lg:rounded-md">
|
<div className="align-center bg-subtle mb-4 -ml-4 -mr-4 flex min-h-[450px] w-auto basis-3/5 snap-x snap-mandatory flex-row overflow-auto whitespace-nowrap p-4 md:mb-8 md:-ml-8 md:-mr-8 md:p-8 lg:mx-0 lg:mb-0 lg:max-w-2xl lg:flex-col lg:justify-center lg:rounded-md">
|
||||||
{descriptionItems ? (
|
{descriptionItems ? (
|
||||||
descriptionItems.map((descriptionItem, index) =>
|
descriptionItems.map((descriptionItem, index) =>
|
||||||
typeof descriptionItem === "object" ? (
|
typeof descriptionItem === "object" ? (
|
||||||
@@ -127,13 +117,17 @@ const Component = ({
|
|||||||
<div className="mb-8 flex pt-4">
|
<div className="mb-8 flex pt-4">
|
||||||
<header>
|
<header>
|
||||||
<div className="mb-4 flex items-center">
|
<div className="mb-4 flex items-center">
|
||||||
<img className="min-h-16 min-w-16 h-16 w-16" src={logo} alt={name} />
|
<img
|
||||||
<h1 className="font-cal ml-4 text-3xl text-gray-900">{name}</h1>
|
className={classNames(logo.includes("-dark") && "dark:invert", "min-h-16 min-w-16 h-16 w-16")}
|
||||||
|
src={logo}
|
||||||
|
alt={name}
|
||||||
|
/>
|
||||||
|
<h1 className="font-cal text-emphasis ml-4 text-3xl">{name}</h1>
|
||||||
</div>
|
</div>
|
||||||
<h2 className="text-sm font-medium text-gray-600">
|
<h2 className="text-default text-sm font-medium">
|
||||||
<Link
|
<Link
|
||||||
href={`categories/${categories[0]}`}
|
href={`categories/${categories[0]}`}
|
||||||
className="rounded-md bg-gray-100 p-1 text-xs capitalize text-gray-800">
|
className="bg-subtle text-emphasis rounded-md p-1 text-xs capitalize">
|
||||||
{categories[0]}
|
{categories[0]}
|
||||||
</Link>{" "}
|
</Link>{" "}
|
||||||
• {t("published_by", { author })}
|
• {t("published_by", { author })}
|
||||||
@@ -149,7 +143,7 @@ const Component = ({
|
|||||||
isGlobal ||
|
isGlobal ||
|
||||||
(existingCredentials.length > 0 && allowedMultipleInstalls ? (
|
(existingCredentials.length > 0 && allowedMultipleInstalls ? (
|
||||||
<div className="flex space-x-3">
|
<div className="flex space-x-3">
|
||||||
<Button StartIcon={FiCheck} color="secondary" disabled>
|
<Button StartIcon={Check} color="secondary" disabled>
|
||||||
{existingCredentials.length > 0
|
{existingCredentials.length > 0
|
||||||
? t("active_install", { count: existingCredentials.length })
|
? t("active_install", { count: existingCredentials.length })
|
||||||
: t("default")}
|
: t("default")}
|
||||||
@@ -159,6 +153,7 @@ const Component = ({
|
|||||||
type={type}
|
type={type}
|
||||||
isProOnly={isProOnly}
|
isProOnly={isProOnly}
|
||||||
disableInstall={disableInstall}
|
disableInstall={disableInstall}
|
||||||
|
teamsPlanRequired={teamsPlanRequired}
|
||||||
render={({ useDefaultComponent, ...props }) => {
|
render={({ useDefaultComponent, ...props }) => {
|
||||||
if (useDefaultComponent) {
|
if (useDefaultComponent) {
|
||||||
props = {
|
props = {
|
||||||
@@ -171,7 +166,7 @@ const Component = ({
|
|||||||
}
|
}
|
||||||
return (
|
return (
|
||||||
<Button
|
<Button
|
||||||
StartIcon={FiPlus}
|
StartIcon={Plus}
|
||||||
{...props}
|
{...props}
|
||||||
// @TODO: Overriding color and size prevent us from
|
// @TODO: Overriding color and size prevent us from
|
||||||
// having to duplicate InstallAppButton for now.
|
// having to duplicate InstallAppButton for now.
|
||||||
@@ -191,7 +186,7 @@ const Component = ({
|
|||||||
label={t("disconnect")}
|
label={t("disconnect")}
|
||||||
credentialId={existingCredentials[0]}
|
credentialId={existingCredentials[0]}
|
||||||
onSuccess={() => {
|
onSuccess={() => {
|
||||||
router.replace("/apps/installed");
|
appCredentials.refetch();
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
@@ -199,6 +194,7 @@ const Component = ({
|
|||||||
type={type}
|
type={type}
|
||||||
isProOnly={isProOnly}
|
isProOnly={isProOnly}
|
||||||
disableInstall={disableInstall}
|
disableInstall={disableInstall}
|
||||||
|
teamsPlanRequired={teamsPlanRequired}
|
||||||
render={({ useDefaultComponent, ...props }) => {
|
render={({ useDefaultComponent, ...props }) => {
|
||||||
if (useDefaultComponent) {
|
if (useDefaultComponent) {
|
||||||
props = {
|
props = {
|
||||||
@@ -243,14 +239,18 @@ const Component = ({
|
|||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<div className="prose-sm prose mt-8">{body}</div>
|
<div className="prose-sm prose prose-a:text-default prose-headings:text-emphasis prose-code:text-default prose-strong:text-default text-default mt-8">
|
||||||
<h4 className="mt-8 font-semibold text-gray-900 ">{t("pricing")}</h4>
|
{body}
|
||||||
<span>
|
</div>
|
||||||
{price === 0 ? (
|
<h4 className="text-emphasis mt-8 font-semibold ">{t("pricing")}</h4>
|
||||||
|
<span className="text-default">
|
||||||
|
{teamsPlanRequired ? (
|
||||||
|
t("teams_plan_required")
|
||||||
|
) : price === 0 ? (
|
||||||
t("free_to_use_apps")
|
t("free_to_use_apps")
|
||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
{Intl.NumberFormat("en-US", {
|
{Intl.NumberFormat(i18n.language, {
|
||||||
style: "currency",
|
style: "currency",
|
||||||
currency: "USD",
|
currency: "USD",
|
||||||
useGrouping: false,
|
useGrouping: false,
|
||||||
@@ -260,16 +260,16 @@ const Component = ({
|
|||||||
)}
|
)}
|
||||||
</span>
|
</span>
|
||||||
|
|
||||||
<h4 className="mt-8 mb-2 font-semibold text-gray-900 ">{t("learn_more")}</h4>
|
<h4 className="text-emphasis mt-8 mb-2 font-semibold ">{t("learn_more")}</h4>
|
||||||
<ul className="prose-sm -ml-1 -mr-1 leading-5">
|
<ul className="prose-sm -ml-1 -mr-1 leading-5">
|
||||||
{docs && (
|
{docs && (
|
||||||
<li>
|
<li>
|
||||||
<a
|
<a
|
||||||
target="_blank"
|
target="_blank"
|
||||||
rel="noreferrer"
|
rel="noreferrer"
|
||||||
className="text-sm font-normal text-black no-underline hover:underline"
|
className="text-emphasis text-sm font-normal no-underline hover:underline"
|
||||||
href={docs}>
|
href={docs}>
|
||||||
<FiBookOpen className="mr-1 -mt-1 inline h-4 w-4 text-gray-500" />
|
<BookOpen className="text-subtle mr-1 -mt-1 inline h-4 w-4" />
|
||||||
{t("documentation")}
|
{t("documentation")}
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
@@ -279,9 +279,9 @@ const Component = ({
|
|||||||
<a
|
<a
|
||||||
target="_blank"
|
target="_blank"
|
||||||
rel="noreferrer"
|
rel="noreferrer"
|
||||||
className="font-normal text-black no-underline hover:underline"
|
className="text-emphasis font-normal no-underline hover:underline"
|
||||||
href={website}>
|
href={website}>
|
||||||
<FiExternalLink className="mr-1 -mt-px inline h-4 w-4 text-gray-500" />
|
<ExternalLink className="text-subtle mr-1 -mt-px inline h-4 w-4" />
|
||||||
{website.replace("https://", "")}
|
{website.replace("https://", "")}
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
@@ -291,9 +291,9 @@ const Component = ({
|
|||||||
<a
|
<a
|
||||||
target="_blank"
|
target="_blank"
|
||||||
rel="noreferrer"
|
rel="noreferrer"
|
||||||
className="font-normal text-black no-underline hover:underline"
|
className="text-emphasis font-normal no-underline hover:underline"
|
||||||
href={"mailto:" + email}>
|
href={"mailto:" + email}>
|
||||||
<FiMail className="mr-1 -mt-px inline h-4 w-4 text-gray-500" />
|
<Mail className="text-subtle mr-1 -mt-px inline h-4 w-4" />
|
||||||
|
|
||||||
{email}
|
{email}
|
||||||
</a>
|
</a>
|
||||||
@@ -304,9 +304,9 @@ const Component = ({
|
|||||||
<a
|
<a
|
||||||
target="_blank"
|
target="_blank"
|
||||||
rel="noreferrer"
|
rel="noreferrer"
|
||||||
className="font-normal text-black no-underline hover:underline"
|
className="text-emphasis font-normal no-underline hover:underline"
|
||||||
href={tos}>
|
href={tos}>
|
||||||
<FiFile className="mr-1 -mt-px inline h-4 w-4 text-gray-500" />
|
<File className="text-subtle mr-1 -mt-px inline h-4 w-4" />
|
||||||
{t("terms_of_service")}
|
{t("terms_of_service")}
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
@@ -316,20 +316,20 @@ const Component = ({
|
|||||||
<a
|
<a
|
||||||
target="_blank"
|
target="_blank"
|
||||||
rel="noreferrer"
|
rel="noreferrer"
|
||||||
className="font-normal text-black no-underline hover:underline"
|
className="text-emphasis font-normal no-underline hover:underline"
|
||||||
href={privacy}>
|
href={privacy}>
|
||||||
<FiShield className="mr-1 -mt-px inline h-4 w-4 text-gray-500" />
|
<Shield className="text-subtle mr-1 -mt-px inline h-4 w-4" />
|
||||||
{t("privacy_policy")}
|
{t("privacy_policy")}
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
)}
|
)}
|
||||||
</ul>
|
</ul>
|
||||||
<hr className="my-8" />
|
<hr className="border-subtle my-8 border" />
|
||||||
<span className="leading-1 block text-xs text-gray-500">
|
<span className="leading-1 text-subtle block text-xs">
|
||||||
{t("every_app_published", { appName: APP_NAME, companyName: COMPANY_NAME })}
|
{t("every_app_published", { appName: APP_NAME, companyName: COMPANY_NAME })}
|
||||||
</span>
|
</span>
|
||||||
<a className="mt-2 block text-xs text-red-500" href={`mailto:${SUPPORT_MAIL_ADDRESS}`}>
|
<a className="mt-2 block text-xs text-red-500" href={`mailto:${SUPPORT_MAIL_ADDRESS}`}>
|
||||||
<FiFlag className="inline h-3 w-3" /> {t("report_app")}
|
<Flag className="inline h-3 w-3" /> {t("report_app")}
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -363,13 +363,14 @@ export default function App(props: {
|
|||||||
privacy?: string;
|
privacy?: string;
|
||||||
licenseRequired: AppType["licenseRequired"];
|
licenseRequired: AppType["licenseRequired"];
|
||||||
isProOnly: AppType["isProOnly"];
|
isProOnly: AppType["isProOnly"];
|
||||||
|
teamsPlanRequired: AppType["teamsPlanRequired"];
|
||||||
descriptionItems?: Array<string | { iframe: IframeHTMLAttributes<HTMLIFrameElement> }>;
|
descriptionItems?: Array<string | { iframe: IframeHTMLAttributes<HTMLIFrameElement> }>;
|
||||||
isTemplate?: boolean;
|
isTemplate?: boolean;
|
||||||
disableInstall?: boolean;
|
disableInstall?: boolean;
|
||||||
dependencies?: string[];
|
dependencies?: string[];
|
||||||
}) {
|
}) {
|
||||||
return (
|
return (
|
||||||
<Shell smallHeading isPublic heading={<ShellHeading />} backPath="/apps" withoutSeo>
|
<Shell smallHeading isPublic hideHeadingOnMobile heading={<ShellHeading />} backPath="/apps" withoutSeo>
|
||||||
<HeadSeo
|
<HeadSeo
|
||||||
title={props.name}
|
title={props.name}
|
||||||
description={props.description}
|
description={props.description}
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
import { useMutation } from "@tanstack/react-query";
|
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { Fragment } from "react";
|
import { Fragment } from "react";
|
||||||
|
|
||||||
import { InstallAppButton } from "@calcom/app-store/components";
|
import { InstallAppButton } from "@calcom/app-store/components";
|
||||||
import DisconnectIntegration from "@calcom/features/apps/components/DisconnectIntegration";
|
import DisconnectIntegration from "@calcom/features/apps/components/DisconnectIntegration";
|
||||||
|
import { CalendarSwitch } from "@calcom/features/calendars/CalendarSwitch";
|
||||||
import DestinationCalendarSelector from "@calcom/features/calendars/DestinationCalendarSelector";
|
import DestinationCalendarSelector from "@calcom/features/calendars/DestinationCalendarSelector";
|
||||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||||
import { trpc } from "@calcom/trpc/react";
|
import { trpc } from "@calcom/trpc/react";
|
||||||
@@ -12,12 +12,10 @@ import {
|
|||||||
Button,
|
Button,
|
||||||
EmptyScreen,
|
EmptyScreen,
|
||||||
List,
|
List,
|
||||||
showToast,
|
|
||||||
AppSkeletonLoader as SkeletonLoader,
|
AppSkeletonLoader as SkeletonLoader,
|
||||||
Switch,
|
|
||||||
ShellSubHeading,
|
ShellSubHeading,
|
||||||
} from "@calcom/ui";
|
} from "@calcom/ui";
|
||||||
import { FiArrowLeft, FiCalendar, FiPlus } from "@calcom/ui/components/icon";
|
import { Calendar, Plus } from "@calcom/ui/components/icon";
|
||||||
|
|
||||||
import { QueryCell } from "@lib/QueryCell";
|
import { QueryCell } from "@lib/QueryCell";
|
||||||
|
|
||||||
@@ -31,83 +29,6 @@ type Props = {
|
|||||||
destinationCalendarId?: string;
|
destinationCalendarId?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
function CalendarSwitch(props: {
|
|
||||||
type: string;
|
|
||||||
externalId: string;
|
|
||||||
title: string;
|
|
||||||
defaultSelected: boolean;
|
|
||||||
destination?: boolean;
|
|
||||||
}) {
|
|
||||||
const { t } = useLocale();
|
|
||||||
const utils = trpc.useContext();
|
|
||||||
|
|
||||||
const mutation = useMutation<
|
|
||||||
unknown,
|
|
||||||
unknown,
|
|
||||||
{
|
|
||||||
isOn: boolean;
|
|
||||||
}
|
|
||||||
>(
|
|
||||||
async ({ isOn }) => {
|
|
||||||
const body = {
|
|
||||||
integration: props.type,
|
|
||||||
externalId: props.externalId,
|
|
||||||
};
|
|
||||||
if (isOn) {
|
|
||||||
const res = await fetch("/api/availability/calendar", {
|
|
||||||
method: "POST",
|
|
||||||
headers: {
|
|
||||||
"Content-Type": "application/json",
|
|
||||||
},
|
|
||||||
body: JSON.stringify(body),
|
|
||||||
});
|
|
||||||
if (!res.ok) {
|
|
||||||
throw new Error("Something went wrong");
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
const res = await fetch("/api/availability/calendar", {
|
|
||||||
method: "DELETE",
|
|
||||||
headers: {
|
|
||||||
"Content-Type": "application/json",
|
|
||||||
},
|
|
||||||
body: JSON.stringify(body),
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!res.ok) {
|
|
||||||
throw new Error("Something went wrong");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
async onSettled() {
|
|
||||||
await utils.viewer.integrations.invalidate();
|
|
||||||
},
|
|
||||||
onError() {
|
|
||||||
showToast(`Something went wrong when toggling "${props.title}""`, "error");
|
|
||||||
},
|
|
||||||
}
|
|
||||||
);
|
|
||||||
return (
|
|
||||||
<div className="flex flex-col py-1 sm:flex-row">
|
|
||||||
<Switch
|
|
||||||
key={props.externalId}
|
|
||||||
name="enabled"
|
|
||||||
label={props.title}
|
|
||||||
defaultChecked={props.defaultSelected}
|
|
||||||
onCheckedChange={(isOn: boolean) => {
|
|
||||||
mutation.mutate({ isOn });
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
{!!props.destination && (
|
|
||||||
<span className="ml-8 inline-flex items-center gap-1 rounded-md bg-gray-100 px-2 py-1 text-sm font-normal text-gray-800 sm:ml-4">
|
|
||||||
<FiArrowLeft className="h-4 w-4" />
|
|
||||||
{t("adding_events_to")}
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function CalendarList(props: Props) {
|
function CalendarList(props: Props) {
|
||||||
const { t } = useLocale();
|
const { t } = useLocale();
|
||||||
const query = trpc.viewer.integrations.useQuery({ variant: "calendar", onlyInstalled: false });
|
const query = trpc.viewer.integrations.useQuery({ variant: "calendar", onlyInstalled: false });
|
||||||
@@ -177,22 +98,23 @@ function ConnectedCalendarsList(props: Props) {
|
|||||||
credentialId={item.credentialId}
|
credentialId={item.credentialId}
|
||||||
trashIcon
|
trashIcon
|
||||||
onSuccess={props.onChanged}
|
onSuccess={props.onChanged}
|
||||||
buttonProps={{ className: "border border-gray-300" }}
|
buttonProps={{ className: "border border-default" }}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
}>
|
}>
|
||||||
<div className="border-t border-gray-200">
|
<div className="border-subtle border-t">
|
||||||
{!fromOnboarding && (
|
{!fromOnboarding && (
|
||||||
<>
|
<>
|
||||||
<p className="px-5 pt-4 text-sm text-gray-500">{t("toggle_calendars_conflict")}</p>
|
<p className="text-subtle px-5 pt-4 text-sm">{t("toggle_calendars_conflict")}</p>
|
||||||
<ul className="space-y-2 px-5 py-4">
|
<ul className="space-y-4 px-5 py-4">
|
||||||
{item.calendars.map((cal) => (
|
{item.calendars.map((cal) => (
|
||||||
<CalendarSwitch
|
<CalendarSwitch
|
||||||
key={cal.externalId}
|
key={cal.externalId}
|
||||||
externalId={cal.externalId}
|
externalId={cal.externalId}
|
||||||
title={cal.name || "Nameless calendar"}
|
title={cal.name || "Nameless calendar"}
|
||||||
|
name={cal.name || "Nameless calendar"}
|
||||||
type={item.integration.type}
|
type={item.integration.type}
|
||||||
defaultSelected={cal.isSelected}
|
isChecked={cal.isSelected}
|
||||||
destination={cal.externalId === props.destinationCalendarId}
|
destination={cal.externalId === props.destinationCalendarId}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
@@ -218,7 +140,7 @@ function ConnectedCalendarsList(props: Props) {
|
|||||||
credentialId={item.credentialId}
|
credentialId={item.credentialId}
|
||||||
trashIcon
|
trashIcon
|
||||||
onSuccess={props.onChanged}
|
onSuccess={props.onChanged}
|
||||||
buttonProps={{ className: "border border-gray-300" }}
|
buttonProps={{ className: "border border-default" }}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
@@ -264,7 +186,7 @@ export function CalendarListContainer(props: { heading?: boolean; fromOnboarding
|
|||||||
{!!data.connectedCalendars.length || !!installedCalendars.data?.items.length ? (
|
{!!data.connectedCalendars.length || !!installedCalendars.data?.items.length ? (
|
||||||
<>
|
<>
|
||||||
{heading && (
|
{heading && (
|
||||||
<div className="flex flex-col gap-6 rounded-md border p-7">
|
<div className="border-default flex flex-col gap-6 rounded-md border p-7">
|
||||||
<ShellSubHeading
|
<ShellSubHeading
|
||||||
title={t("calendar")}
|
title={t("calendar")}
|
||||||
subtitle={t("installed_app_calendar_description")}
|
subtitle={t("installed_app_calendar_description")}
|
||||||
@@ -279,18 +201,18 @@ export function CalendarListContainer(props: { heading?: boolean; fromOnboarding
|
|||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
<div className="flex justify-between rounded-md border border-gray-200 bg-gray-50 p-4">
|
<div className="bg-muted border-subtle flex justify-between rounded-md border p-4">
|
||||||
<div className="flex w-full flex-col items-start gap-4 md:flex-row md:items-center">
|
<div className="flex w-full flex-col items-start gap-4 md:flex-row md:items-center">
|
||||||
<div className="relative rounded-md border border-gray-200 bg-white p-1.5">
|
<div className="bg-default border-subtle relative rounded-md border p-1.5">
|
||||||
<FiCalendar className="h-8 w-8" strokeWidth="1" />
|
<Calendar className="text-default h-8 w-8" strokeWidth="1" />
|
||||||
<FiPlus
|
<Plus
|
||||||
className="absolute left-4 top-1/2 ml-0.5 mt-[1px] h-2 w-2 text-black"
|
className="text-emphasis absolute left-4 top-1/2 ml-0.5 mt-[1px] h-2 w-2"
|
||||||
strokeWidth="4"
|
strokeWidth="4"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="md:w-6/12">
|
<div className="md:w-6/12">
|
||||||
<h1 className="text-sm font-semibold">{t("create_events_on")}</h1>
|
<h1 className="text-emphasis text-sm font-semibold">{t("create_events_on")}</h1>
|
||||||
<p className="text-sm font-normal">{t("set_calendar")}</p>
|
<p className="text-default text-sm font-normal">{t("set_calendar")}</p>
|
||||||
</div>
|
</div>
|
||||||
<div className="justify-end md:w-6/12">
|
<div className="justify-end md:w-6/12">
|
||||||
<DestinationCalendarSelector
|
<DestinationCalendarSelector
|
||||||
@@ -322,7 +244,7 @@ export function CalendarListContainer(props: { heading?: boolean; fromOnboarding
|
|||||||
</>
|
</>
|
||||||
) : (
|
) : (
|
||||||
<EmptyScreen
|
<EmptyScreen
|
||||||
Icon={FiCalendar}
|
Icon={Calendar}
|
||||||
headline={t("no_category_apps", {
|
headline={t("no_category_apps", {
|
||||||
category: t("calendar").toLowerCase(),
|
category: t("calendar").toLowerCase(),
|
||||||
})}
|
})}
|
||||||
|
|||||||
@@ -1,139 +0,0 @@
|
|||||||
import classNames from "classnames";
|
|
||||||
import React, { useEffect, useState } from "react";
|
|
||||||
import Select from "react-select";
|
|
||||||
|
|
||||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
|
||||||
import type { DestinationCalendar } from "@calcom/prisma/client";
|
|
||||||
import { trpc } from "@calcom/trpc/react";
|
|
||||||
|
|
||||||
interface Props {
|
|
||||||
onChange: (value: { externalId: string; integration: string }) => void;
|
|
||||||
isLoading?: boolean;
|
|
||||||
hidePlaceholder?: boolean;
|
|
||||||
/** The external Id of the connected calendar */
|
|
||||||
value: string | undefined;
|
|
||||||
destinationCalendar?: DestinationCalendar | null;
|
|
||||||
}
|
|
||||||
|
|
||||||
const DestinationCalendarSelector = ({
|
|
||||||
onChange,
|
|
||||||
isLoading,
|
|
||||||
value,
|
|
||||||
hidePlaceholder,
|
|
||||||
destinationCalendar,
|
|
||||||
}: Props): JSX.Element | null => {
|
|
||||||
const { t } = useLocale();
|
|
||||||
const query = trpc.viewer.connectedCalendars.useQuery();
|
|
||||||
const [selectedOption, setSelectedOption] = useState<{ value: string; label: string } | null>(null);
|
|
||||||
|
|
||||||
// Extra styles to show prefixed text in react-select
|
|
||||||
const content = (hidePlaceholder = false) => {
|
|
||||||
if (!hidePlaceholder) {
|
|
||||||
return {
|
|
||||||
alignItems: "center",
|
|
||||||
display: "flex",
|
|
||||||
":before": {
|
|
||||||
content: `'${t("select_destination_calendar")}:'`,
|
|
||||||
display: "block",
|
|
||||||
marginRight: 8,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}
|
|
||||||
return {};
|
|
||||||
};
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const selected = query.data?.connectedCalendars
|
|
||||||
.map((connected) => connected.calendars ?? [])
|
|
||||||
.flat()
|
|
||||||
.find((cal) => cal.externalId === value);
|
|
||||||
|
|
||||||
if (selected) {
|
|
||||||
setSelectedOption({
|
|
||||||
value: `${selected.integration}:${selected.externalId}`,
|
|
||||||
label: `${selected.name} (${selected.externalId})` || "",
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}, [query.data?.connectedCalendars, value]);
|
|
||||||
|
|
||||||
if (!query.data?.connectedCalendars.length) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
const options =
|
|
||||||
query.data.connectedCalendars.map((selectedCalendar) => ({
|
|
||||||
key: selectedCalendar.credentialId,
|
|
||||||
label: `${selectedCalendar.integration.title} (${selectedCalendar.primary?.name})`,
|
|
||||||
options: (selectedCalendar.calendars ?? [])
|
|
||||||
.filter((cal) => cal.readOnly === false)
|
|
||||||
.map((cal) => ({
|
|
||||||
label: `${cal.name} (${cal.externalId})` || "",
|
|
||||||
value: `${cal.integration}:${cal.externalId}`,
|
|
||||||
})),
|
|
||||||
})) ?? [];
|
|
||||||
const defaultCalendarSelectedString = destinationCalendar?.externalId
|
|
||||||
? `(${
|
|
||||||
destinationCalendar.externalId.length > 15
|
|
||||||
? destinationCalendar.externalId.substring(0, 15) + "..."
|
|
||||||
: destinationCalendar.externalId
|
|
||||||
})`
|
|
||||||
: "";
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
className="relative flex items-center"
|
|
||||||
title={`${t("select_destination_calendar")}: ${selectedOption?.label || ""}`}>
|
|
||||||
<Select
|
|
||||||
name="primarySelectedCalendar"
|
|
||||||
placeholder={
|
|
||||||
!hidePlaceholder ? (
|
|
||||||
`${t("select_destination_calendar")}`
|
|
||||||
) : (
|
|
||||||
<span className="whitespace-nowrap">
|
|
||||||
{t("default_calendar_selected")} {defaultCalendarSelectedString}
|
|
||||||
</span>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
options={options}
|
|
||||||
styles={{
|
|
||||||
placeholder: (styles) => ({ ...styles, ...content(hidePlaceholder) }),
|
|
||||||
singleValue: (styles) => ({ ...styles, ...content(hidePlaceholder) }),
|
|
||||||
control: (defaultStyles) => {
|
|
||||||
return {
|
|
||||||
...defaultStyles,
|
|
||||||
borderRadius: "6px",
|
|
||||||
"@media only screen and (min-width: 640px)": {
|
|
||||||
...(defaultStyles["@media only screen and (min-width: 640px)"] as object),
|
|
||||||
width: "100%",
|
|
||||||
},
|
|
||||||
};
|
|
||||||
},
|
|
||||||
}}
|
|
||||||
isSearchable={false}
|
|
||||||
className={classNames(
|
|
||||||
"block w-full min-w-0 flex-1 rounded-none rounded-r-sm border-gray-300 text-sm",
|
|
||||||
!hidePlaceholder && "font-medium"
|
|
||||||
)}
|
|
||||||
onChange={(option) => {
|
|
||||||
setSelectedOption(option);
|
|
||||||
if (!option) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Split only the first `:`, since Apple uses the full URL as externalId */
|
|
||||||
const [integration, externalId] = option.value.split(/:(.+)/);
|
|
||||||
|
|
||||||
onChange({
|
|
||||||
integration,
|
|
||||||
externalId,
|
|
||||||
});
|
|
||||||
}}
|
|
||||||
isLoading={isLoading}
|
|
||||||
value={selectedOption}
|
|
||||||
components={{
|
|
||||||
IndicatorSeparator: () => null,
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default DestinationCalendarSelector;
|
|
||||||
@@ -6,7 +6,7 @@ import React from "react";
|
|||||||
import Shell from "@calcom/features/shell/Shell";
|
import Shell from "@calcom/features/shell/Shell";
|
||||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||||
import { EmptyScreen } from "@calcom/ui";
|
import { EmptyScreen } from "@calcom/ui";
|
||||||
import { FiAlertCircle } from "@calcom/ui/components/icon";
|
import { AlertCircle } from "@calcom/ui/components/icon";
|
||||||
|
|
||||||
type AppsLayoutProps = {
|
type AppsLayoutProps = {
|
||||||
children: React.ReactNode;
|
children: React.ReactNode;
|
||||||
@@ -18,19 +18,19 @@ export default function AppsLayout({ children, actions, emptyStore, ...rest }: A
|
|||||||
const { t } = useLocale();
|
const { t } = useLocale();
|
||||||
const session = useSession();
|
const session = useSession();
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
const isAdmin = session.data?.user.role === "ADMIN";
|
||||||
|
|
||||||
if (session.status === "loading") return <></>;
|
if (session.status === "loading") return <></>;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Shell {...rest} actions={actions?.("block")}>
|
<Shell {...rest} actions={actions?.("block")} hideHeadingOnMobile>
|
||||||
<div className="flex flex-col xl:flex-row">
|
<div className="flex flex-col xl:flex-row">
|
||||||
<main className="w-full">
|
<main className="w-full">
|
||||||
{emptyStore ? (
|
{emptyStore ? (
|
||||||
<EmptyScreen
|
<EmptyScreen
|
||||||
Icon={FiAlertCircle}
|
Icon={AlertCircle}
|
||||||
headline={t("no_apps")}
|
headline={t("no_apps")}
|
||||||
description={session.data?.user.role === "ADMIN" ? "You can enable apps in the settings" : ""}
|
description={isAdmin ? "You can enable apps in the settings" : ""}
|
||||||
buttonText={session.data?.user.role === "ADMIN" ? t("apps_settings") : ""}
|
buttonText={isAdmin ? t("apps_settings") : ""}
|
||||||
buttonOnClick={() => router.push("/settings/admin/apps/calendar")}
|
buttonOnClick={() => router.push("/settings/admin/apps/calendar")}
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
|
|||||||
@@ -6,38 +6,38 @@ import type { InstalledAppVariants } from "@calcom/app-store/utils";
|
|||||||
import Shell from "@calcom/features/shell/Shell";
|
import Shell from "@calcom/features/shell/Shell";
|
||||||
import { trpc } from "@calcom/trpc/react";
|
import { trpc } from "@calcom/trpc/react";
|
||||||
import type { HorizontalTabItemProps, VerticalTabItemProps } from "@calcom/ui";
|
import type { HorizontalTabItemProps, VerticalTabItemProps } from "@calcom/ui";
|
||||||
import { FiBarChart, FiCalendar, FiCreditCard, FiGrid, FiShare2, FiVideo } from "@calcom/ui/components/icon";
|
import { BarChart, Calendar, CreditCard, Grid, Share2, Video } from "@calcom/ui/components/icon";
|
||||||
|
|
||||||
const tabs: (VerticalTabItemProps | HorizontalTabItemProps)[] = [
|
const tabs: (VerticalTabItemProps | HorizontalTabItemProps)[] = [
|
||||||
{
|
{
|
||||||
name: "calendar",
|
name: "calendar",
|
||||||
href: "/apps/installed/calendar",
|
href: "/apps/installed/calendar",
|
||||||
icon: FiCalendar,
|
icon: Calendar,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "conferencing",
|
name: "conferencing",
|
||||||
href: "/apps/installed/conferencing",
|
href: "/apps/installed/conferencing",
|
||||||
icon: FiVideo,
|
icon: Video,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "payment",
|
name: "payment",
|
||||||
href: "/apps/installed/payment",
|
href: "/apps/installed/payment",
|
||||||
icon: FiCreditCard,
|
icon: CreditCard,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "automation",
|
name: "automation",
|
||||||
href: "/apps/installed/automation",
|
href: "/apps/installed/automation",
|
||||||
icon: FiShare2,
|
icon: Share2,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "analytics",
|
name: "analytics",
|
||||||
href: "/apps/installed/analytics",
|
href: "/apps/installed/analytics",
|
||||||
icon: FiBarChart,
|
icon: BarChart,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "other",
|
name: "other",
|
||||||
href: "/apps/installed/other",
|
href: "/apps/installed/other",
|
||||||
icon: FiGrid,
|
icon: Grid,
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
@@ -57,7 +57,7 @@ export default function InstalledAppsLayout({
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Shell {...rest}>
|
<Shell {...rest} hideHeadingOnMobile>
|
||||||
<AppCategoryNavigation baseURL="/apps/installed" containerClassname="min-w-0 w-full">
|
<AppCategoryNavigation baseURL="/apps/installed" containerClassname="min-w-0 w-full">
|
||||||
{children}
|
{children}
|
||||||
</AppCategoryNavigation>
|
</AppCategoryNavigation>
|
||||||
|
|||||||
@@ -28,7 +28,7 @@ export default function TwoFactor({ center = true }) {
|
|||||||
<div className={center ? "mx-auto !mt-0 max-w-sm" : "!mt-0 max-w-sm"}>
|
<div className={center ? "mx-auto !mt-0 max-w-sm" : "!mt-0 max-w-sm"}>
|
||||||
<Label className="mt-4"> {t("2fa_code")}</Label>
|
<Label className="mt-4"> {t("2fa_code")}</Label>
|
||||||
|
|
||||||
<p className="mb-4 text-sm text-gray-500">{t("2fa_enabled_instructions")}</p>
|
<p className="text-subtle mb-4 text-sm">{t("2fa_enabled_instructions")}</p>
|
||||||
<input hidden type="hidden" value={value} {...methods.register("totpCode")} />
|
<input hidden type="hidden" value={value} {...methods.register("totpCode")} />
|
||||||
<div className="flex flex-row justify-between">
|
<div className="flex flex-row justify-between">
|
||||||
<Input
|
<Input
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
import { UserPermissionRole } from "@prisma/client";
|
|
||||||
import { useSession } from "next-auth/react";
|
import { useSession } from "next-auth/react";
|
||||||
import { useRouter } from "next/router";
|
import { useRouter } from "next/router";
|
||||||
import type { ComponentProps } from "react";
|
import type { ComponentProps } from "react";
|
||||||
@@ -6,6 +5,7 @@ import React, { useEffect } from "react";
|
|||||||
|
|
||||||
import SettingsLayout from "@calcom/features/settings/layouts/SettingsLayout";
|
import SettingsLayout from "@calcom/features/settings/layouts/SettingsLayout";
|
||||||
import type Shell from "@calcom/features/shell/Shell";
|
import type Shell from "@calcom/features/shell/Shell";
|
||||||
|
import { UserPermissionRole } from "@calcom/prisma/enums";
|
||||||
import { ErrorBoundary } from "@calcom/ui";
|
import { ErrorBoundary } from "@calcom/ui";
|
||||||
|
|
||||||
export default function AdminLayout({
|
export default function AdminLayout({
|
||||||
@@ -26,7 +26,7 @@ export default function AdminLayout({
|
|||||||
const isAppsPage = router.asPath.startsWith("/settings/admin/apps");
|
const isAppsPage = router.asPath.startsWith("/settings/admin/apps");
|
||||||
return (
|
return (
|
||||||
<SettingsLayout {...rest}>
|
<SettingsLayout {...rest}>
|
||||||
<div className="mx-auto flex max-w-4xl flex-row divide-y divide-gray-200">
|
<div className="divide-subtle mx-auto flex max-w-4xl flex-row divide-y">
|
||||||
<div className={isAppsPage ? "min-w-0" : "flex flex-1 [&>*]:flex-1"}>
|
<div className={isAppsPage ? "min-w-0" : "flex flex-1 [&>*]:flex-1"}>
|
||||||
<ErrorBoundary>{children}</ErrorBoundary>
|
<ErrorBoundary>{children}</ErrorBoundary>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,13 +1,13 @@
|
|||||||
import React from "react";
|
import React from "react";
|
||||||
|
|
||||||
import { Button, SkeletonText } from "@calcom/ui";
|
import { Button, SkeletonText } from "@calcom/ui";
|
||||||
import { FiMoreHorizontal } from "@calcom/ui/components/icon";
|
import { MoreHorizontal } from "@calcom/ui/components/icon";
|
||||||
|
|
||||||
import classNames from "@lib/classNames";
|
import classNames from "@lib/classNames";
|
||||||
|
|
||||||
function SkeletonLoader() {
|
function SkeletonLoader() {
|
||||||
return (
|
return (
|
||||||
<ul className="animate-pulse divide-y divide-gray-200 rounded-md border border-gray-200 bg-white sm:mx-0 sm:overflow-hidden">
|
<ul className="divide-subtle border-subtle bg-default animate-pulse divide-y rounded-md border sm:mx-0 sm:overflow-hidden">
|
||||||
<SkeletonItem />
|
<SkeletonItem />
|
||||||
<SkeletonItem />
|
<SkeletonItem />
|
||||||
<SkeletonItem />
|
<SkeletonItem />
|
||||||
@@ -20,8 +20,8 @@ export default SkeletonLoader;
|
|||||||
function SkeletonItem() {
|
function SkeletonItem() {
|
||||||
return (
|
return (
|
||||||
<li>
|
<li>
|
||||||
<div className="flex items-center justify-between py-5 hover:bg-neutral-50 ltr:pl-4 rtl:pr-4 sm:ltr:pl-0 sm:rtl:pr-0">
|
<div className="flex items-center justify-between py-5 ltr:pl-4 rtl:pr-4 sm:ltr:pl-0 sm:rtl:pr-0">
|
||||||
<div className="items-between flex w-full flex-col justify-center hover:bg-neutral-50 sm:px-6">
|
<div className="items-between flex w-full flex-col justify-center sm:px-6">
|
||||||
<SkeletonText className="my-1 h-4 w-32" />
|
<SkeletonText className="my-1 h-4 w-32" />
|
||||||
<SkeletonText className="my-1 h-2 w-24" />
|
<SkeletonText className="my-1 h-2 w-24" />
|
||||||
<SkeletonText className="my-1 h-2 w-40" />
|
<SkeletonText className="my-1 h-2 w-40" />
|
||||||
@@ -31,7 +31,7 @@ function SkeletonItem() {
|
|||||||
type="button"
|
type="button"
|
||||||
variant="icon"
|
variant="icon"
|
||||||
color="secondary"
|
color="secondary"
|
||||||
StartIcon={FiMoreHorizontal}
|
StartIcon={MoreHorizontal}
|
||||||
disabled
|
disabled
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@@ -43,7 +43,7 @@ export const SelectSkeletonLoader = ({ className }: { className?: string }) => {
|
|||||||
return (
|
return (
|
||||||
<li
|
<li
|
||||||
className={classNames(
|
className={classNames(
|
||||||
"group flex w-full items-center justify-between rounded-sm border border-gray-200 px-[10px] py-3",
|
"border-subtle group flex w-full items-center justify-between rounded-sm border px-[10px] py-3",
|
||||||
className
|
className
|
||||||
)}>
|
)}>
|
||||||
<div className="flex-grow truncate text-sm">
|
<div className="flex-grow truncate text-sm">
|
||||||
|
|||||||
@@ -4,58 +4,65 @@ import { getEventLocationType, locationKeyToString } from "@calcom/app-store/loc
|
|||||||
import { classNames } from "@calcom/lib";
|
import { classNames } from "@calcom/lib";
|
||||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||||
import { Tooltip } from "@calcom/ui";
|
import { Tooltip } from "@calcom/ui";
|
||||||
import { FiLink } from "@calcom/ui/components/icon";
|
import { Link } from "@calcom/ui/components/icon";
|
||||||
|
|
||||||
import type { Props } from "./pages/AvailabilityPage";
|
import type { Props } from "./pages/AvailabilityPage";
|
||||||
|
|
||||||
|
const excludeNullValues = (value: unknown) => !!value;
|
||||||
|
|
||||||
export function AvailableEventLocations({ locations }: { locations: Props["eventType"]["locations"] }) {
|
export function AvailableEventLocations({ locations }: { locations: Props["eventType"]["locations"] }) {
|
||||||
const { t } = useLocale();
|
const { t } = useLocale();
|
||||||
|
|
||||||
return locations.length ? (
|
const renderLocations = locations.map((location, index) => {
|
||||||
<div className="dark:text-darkgray-600 mr-6 flex w-full flex-col space-y-4 break-words text-sm text-gray-600">
|
const eventLocationType = getEventLocationType(location.type);
|
||||||
{locations.map((location, index) => {
|
if (!eventLocationType) {
|
||||||
const eventLocationType = getEventLocationType(location.type);
|
// It's possible that the location app got uninstalled
|
||||||
if (!eventLocationType) {
|
return null;
|
||||||
// It's possible that the location app got uninstalled
|
}
|
||||||
return null;
|
if (eventLocationType.variable === "hostDefault") {
|
||||||
}
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
const translateAbleKeys = [
|
const translateAbleKeys = [
|
||||||
"attendee_in_person",
|
"attendee_in_person",
|
||||||
"in_person",
|
"in_person",
|
||||||
"attendee_phone_number",
|
"attendee_phone_number",
|
||||||
"link_meeting",
|
"link_meeting",
|
||||||
"organizer_phone_number",
|
"organizer_phone_number",
|
||||||
];
|
];
|
||||||
|
|
||||||
const locationKey = z.string().default("").parse(locationKeyToString(location));
|
const locationKey = z.string().default("").parse(locationKeyToString(location));
|
||||||
|
const translatedLocation = location.type.startsWith("integrations:")
|
||||||
|
? eventLocationType.label
|
||||||
|
: translateAbleKeys.includes(locationKey)
|
||||||
|
? t(locationKey)
|
||||||
|
: locationKey;
|
||||||
|
|
||||||
const translatedLocation = location.type.startsWith("integrations:")
|
return (
|
||||||
? eventLocationType.label
|
<div key={`${location.type}-${index}`} className="flex flex-row items-center text-sm font-medium">
|
||||||
: translateAbleKeys.includes(locationKey)
|
{eventLocationType.iconUrl === "/link.svg" ? (
|
||||||
? t(locationKey)
|
<Link className="text-default ml-[2px] h-4 w-4 ltr:mr-[10px] rtl:ml-[10px] " />
|
||||||
: locationKey;
|
) : (
|
||||||
|
<img
|
||||||
return (
|
src={eventLocationType.iconUrl}
|
||||||
<div key={`${location.type}-${index}`} className="flex flex-row items-center text-sm font-medium">
|
className={classNames(
|
||||||
{eventLocationType.iconUrl === "/link.svg" ? (
|
"ml-[2px] h-4 w-4 opacity-70 ltr:mr-[10px] rtl:ml-[10px] dark:opacity-100 ",
|
||||||
<FiLink className="dark:text-darkgray-600 min-h-4 min-w-4 ml-[2px] opacity-70 ltr:mr-[10px] rtl:ml-[10px] dark:opacity-100 " />
|
!eventLocationType.iconUrl?.startsWith("/app-store") ? "dark:invert-[.65]" : ""
|
||||||
) : (
|
|
||||||
<img
|
|
||||||
src={eventLocationType.iconUrl}
|
|
||||||
className={classNames(
|
|
||||||
"ml-[2px] h-4 w-4 opacity-70 ltr:mr-[10px] rtl:ml-[10px] dark:opacity-100 ",
|
|
||||||
!eventLocationType.iconUrl?.includes("api") ? "dark:invert-[.65]" : ""
|
|
||||||
)}
|
|
||||||
alt={`${eventLocationType.label} icon`}
|
|
||||||
/>
|
|
||||||
)}
|
)}
|
||||||
<Tooltip content={translatedLocation}>
|
alt={`${eventLocationType.label} icon`}
|
||||||
<p className="line-clamp-1">{translatedLocation}</p>
|
/>
|
||||||
</Tooltip>
|
)}
|
||||||
</div>
|
<Tooltip content={translatedLocation}>
|
||||||
);
|
<p className="line-clamp-1">{translatedLocation}</p>
|
||||||
})}
|
</Tooltip>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
const filteredLocations = renderLocations.filter(excludeNullValues) as JSX.Element[];
|
||||||
|
return filteredLocations.length ? (
|
||||||
|
<div className="text-default mr-6 flex w-full flex-col space-y-4 break-words text-sm">
|
||||||
|
{filteredLocations}
|
||||||
</div>
|
</div>
|
||||||
) : null;
|
) : null;
|
||||||
}
|
}
|
||||||
@@ -10,7 +10,8 @@ import { useLocale } from "@calcom/lib/hooks/useLocale";
|
|||||||
import useMediaQuery from "@calcom/lib/hooks/useMediaQuery";
|
import useMediaQuery from "@calcom/lib/hooks/useMediaQuery";
|
||||||
import { TimeFormat } from "@calcom/lib/timeFormat";
|
import { TimeFormat } from "@calcom/lib/timeFormat";
|
||||||
import { nameOfDay } from "@calcom/lib/weekday";
|
import { nameOfDay } from "@calcom/lib/weekday";
|
||||||
import type { Slot } from "@calcom/trpc/server/routers/viewer/slots";
|
import { trpc } from "@calcom/trpc/react";
|
||||||
|
import type { Slot } from "@calcom/trpc/server/routers/viewer/slots/types";
|
||||||
import { SkeletonContainer, SkeletonText, ToggleGroup } from "@calcom/ui";
|
import { SkeletonContainer, SkeletonText, ToggleGroup } from "@calcom/ui";
|
||||||
|
|
||||||
import classNames from "@lib/classNames";
|
import classNames from "@lib/classNames";
|
||||||
@@ -28,6 +29,7 @@ type AvailableTimesProps = {
|
|||||||
slots?: Slot[];
|
slots?: Slot[];
|
||||||
isLoading: boolean;
|
isLoading: boolean;
|
||||||
ethSignature?: string;
|
ethSignature?: string;
|
||||||
|
duration: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
const AvailableTimes: FC<AvailableTimesProps> = ({
|
const AvailableTimes: FC<AvailableTimesProps> = ({
|
||||||
@@ -42,7 +44,9 @@ const AvailableTimes: FC<AvailableTimesProps> = ({
|
|||||||
seatsPerTimeSlot,
|
seatsPerTimeSlot,
|
||||||
bookingAttendees,
|
bookingAttendees,
|
||||||
ethSignature,
|
ethSignature,
|
||||||
|
duration,
|
||||||
}) => {
|
}) => {
|
||||||
|
const reserveSlotMutation = trpc.viewer.public.slots.reserveSlot.useMutation();
|
||||||
const [slotPickerRef] = useAutoAnimate<HTMLDivElement>();
|
const [slotPickerRef] = useAutoAnimate<HTMLDivElement>();
|
||||||
const { t, i18n } = useLocale();
|
const { t, i18n } = useLocale();
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
@@ -63,16 +67,25 @@ const AvailableTimes: FC<AvailableTimesProps> = ({
|
|||||||
[isMobile]
|
[isMobile]
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const reserveSlot = (slot: Slot) => {
|
||||||
|
reserveSlotMutation.mutate({
|
||||||
|
slotUtcStartDate: slot.time,
|
||||||
|
eventTypeId,
|
||||||
|
slotUtcEndDate: dayjs(slot.time).utc().add(duration, "minutes").format(),
|
||||||
|
bookingUid: slot.bookingUid,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div ref={slotPickerRef}>
|
<div ref={slotPickerRef}>
|
||||||
{!!date ? (
|
{!!date ? (
|
||||||
<div className="dark:bg-darkgray-100 mt-8 flex h-full w-full flex-col rounded-md px-4 text-center sm:mt-0 sm:p-4 md:-mb-5 md:min-w-[200px] md:p-4 lg:min-w-[300px]">
|
<div className="mt-8 flex h-full w-full flex-col rounded-md px-4 text-center sm:mt-0 sm:p-4 md:-mb-5 md:min-w-[200px] md:p-4 lg:min-w-[300px]">
|
||||||
<div className="mb-4 flex items-center text-left text-base">
|
<div className="mb-4 flex items-center text-left text-base">
|
||||||
<div className="mr-4">
|
<div className="mr-4">
|
||||||
<span className="text-bookingdarker dark:text-darkgray-800 font-semibold text-gray-900">
|
<span className="text-emphasis font-semibold">
|
||||||
{nameOfDay(i18n.language, Number(date.format("d")), "short")}
|
{nameOfDay(i18n.language, Number(date.format("d")), "short")}
|
||||||
</span>
|
</span>
|
||||||
<span className="text-bookinglight">
|
<span className="text-subtle">
|
||||||
, {date.toDate().toLocaleString(i18n.language, { month: "short" })} {date.format(" D ")}
|
, {date.toDate().toLocaleString(i18n.language, { month: "short" })} {date.format(" D ")}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
@@ -132,7 +145,7 @@ const AvailableTimes: FC<AvailableTimesProps> = ({
|
|||||||
{seatsPerTimeSlot && slot.attendees && (slotFull || notEnoughSeats) ? (
|
{seatsPerTimeSlot && slot.attendees && (slotFull || notEnoughSeats) ? (
|
||||||
<div
|
<div
|
||||||
className={classNames(
|
className={classNames(
|
||||||
"text-primary-500 dark:bg-darkgray-200 dark:text-darkgray-900 mb-2 block rounded-sm border bg-white py-2 font-medium opacity-25 dark:border-transparent ",
|
"text-default bg-default border-subtle mb-2 block rounded-sm border py-2 font-medium opacity-25",
|
||||||
brand === "#fff" || brand === "#ffffff" ? "" : ""
|
brand === "#fff" || brand === "#ffffff" ? "" : ""
|
||||||
)}>
|
)}>
|
||||||
{dayjs(slot.time).tz(timeZone()).format(timeFormat)}
|
{dayjs(slot.time).tz(timeZone()).format(timeFormat)}
|
||||||
@@ -147,10 +160,10 @@ const AvailableTimes: FC<AvailableTimesProps> = ({
|
|||||||
href={bookingUrl}
|
href={bookingUrl}
|
||||||
prefetch={false}
|
prefetch={false}
|
||||||
className={classNames(
|
className={classNames(
|
||||||
"hover:bg-brand hover:border-brand hover:text-brandcontrast dark:hover:text-darkmodebrandcontrast",
|
" bg-default dark:bg-muted border-default hover:bg-subtle hover:border-brand-default text-emphasis mb-2 block rounded-md border py-2 text-sm font-medium",
|
||||||
"dark:bg-darkgray-200 dark:hover:bg-darkmodebrand dark:hover:border-darkmodebrand dark:text-darkgray-800 mb-2 block rounded-md border bg-white py-2 text-sm font-medium dark:border-transparent",
|
|
||||||
brand === "#fff" || brand === "#ffffff" ? "" : ""
|
brand === "#fff" || brand === "#ffffff" ? "" : ""
|
||||||
)}
|
)}
|
||||||
|
onClick={() => reserveSlot(slot)}
|
||||||
data-testid="time">
|
data-testid="time">
|
||||||
{dayjs(slot.time).tz(timeZone()).format(timeFormat)}
|
{dayjs(slot.time).tz(timeZone()).format(timeFormat)}
|
||||||
{!!seatsPerTimeSlot && (
|
{!!seatsPerTimeSlot && (
|
||||||
@@ -174,7 +187,7 @@ const AvailableTimes: FC<AvailableTimesProps> = ({
|
|||||||
|
|
||||||
{!isLoading && !slots.length && (
|
{!isLoading && !slots.length && (
|
||||||
<div className="-mt-4 flex h-full w-full flex-col content-center items-center justify-center">
|
<div className="-mt-4 flex h-full w-full flex-col content-center items-center justify-center">
|
||||||
<h1 className="my-6 text-xl text-black dark:text-white">{t("all_booked_today")}</h1>
|
<h1 className="text-emphasis my-6 text-xl">{t("all_booked_today")}</h1>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
|||||||
@@ -1,12 +1,12 @@
|
|||||||
import { SchedulingType } from "@prisma/client";
|
|
||||||
import type { FC, ReactNode } from "react";
|
import type { FC, ReactNode } from "react";
|
||||||
import { useEffect } from "react";
|
import { useEffect } from "react";
|
||||||
|
|
||||||
import dayjs from "@calcom/dayjs";
|
import dayjs from "@calcom/dayjs";
|
||||||
import classNames from "@calcom/lib/classNames";
|
import classNames from "@calcom/lib/classNames";
|
||||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||||
|
import { SchedulingType } from "@calcom/prisma/enums";
|
||||||
import { Badge } from "@calcom/ui";
|
import { Badge } from "@calcom/ui";
|
||||||
import { FiCheckSquare, FiClock } from "@calcom/ui/components/icon";
|
import { CheckSquare, Clock } from "@calcom/ui/components/icon";
|
||||||
|
|
||||||
import useRouterQuery from "@lib/hooks/useRouterQuery";
|
import useRouterQuery from "@lib/hooks/useRouterQuery";
|
||||||
|
|
||||||
@@ -83,32 +83,27 @@ const BookingDescription: FC<Props> = (props) => {
|
|||||||
size="sm"
|
size="sm"
|
||||||
truncateAfter={3}
|
truncateAfter={3}
|
||||||
/>
|
/>
|
||||||
<h2 className="mt-1 mb-2 break-words text-sm font-medium text-gray-600 dark:text-gray-300">
|
<h2 className="text-default mt-1 mb-2 break-words text-sm font-medium ">{profile.name}</h2>
|
||||||
{profile.name}
|
<h1 className="font-cal text-emphasis mb-6 break-words text-2xl font-semibold leading-none">
|
||||||
</h2>
|
|
||||||
<h1 className="font-cal dark:text-darkgray-900 mb-6 break-words text-2xl font-semibold leading-none text-gray-900">
|
|
||||||
{eventType.title}
|
{eventType.title}
|
||||||
</h1>
|
</h1>
|
||||||
<div className="dark:text-darkgray-600 flex flex-col space-y-4 text-sm font-medium text-gray-600">
|
<div className=" text-default flex flex-col space-y-4 text-sm font-medium">
|
||||||
{eventType?.description && (
|
{eventType?.description && (
|
||||||
<div
|
<div
|
||||||
className={classNames(
|
className={classNames(
|
||||||
"scroll-bar scrollbar-track-w-20 -mx-5 flex max-h-[180px] overflow-y-scroll px-5 ",
|
"scroll-bar scrollbar-track-w-20 -mx-5 flex max-h-[180px] overflow-y-scroll px-5 ",
|
||||||
isBookingPage && "dark:text-darkgray-600 text-sm font-medium text-gray-600"
|
isBookingPage && "text-default text-sm font-medium"
|
||||||
)}>
|
)}>
|
||||||
|
{/* TODO: Fix colors when token is introdcued to DS */}
|
||||||
<div className="max-w-full flex-shrink break-words [&_a]:text-blue-500 [&_a]:underline [&_a]:hover:text-blue-600">
|
<div className="max-w-full flex-shrink break-words [&_a]:text-blue-500 [&_a]:underline [&_a]:hover:text-blue-600">
|
||||||
<EventTypeDescriptionSafeHTML eventType={eventType} />
|
<EventTypeDescriptionSafeHTML eventType={eventType} />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{requiresConfirmation && (
|
{requiresConfirmation && (
|
||||||
<div
|
<div className={classNames("items-top flex", isBookingPage && "text-default text-sm font-medium")}>
|
||||||
className={classNames(
|
|
||||||
"items-top flex",
|
|
||||||
isBookingPage && "dark:text-darkgray-600 text-sm font-medium text-gray-600"
|
|
||||||
)}>
|
|
||||||
<div>
|
<div>
|
||||||
<FiCheckSquare className="ml-[2px] inline-block h-4 w-4 ltr:mr-[10px] rtl:ml-[10px] " />
|
<CheckSquare className="ml-[2px] inline-block h-4 w-4 ltr:mr-[10px] rtl:ml-[10px] " />
|
||||||
</div>
|
</div>
|
||||||
{requiresConfirmationText}
|
{requiresConfirmationText}
|
||||||
</div>
|
</div>
|
||||||
@@ -119,12 +114,12 @@ const BookingDescription: FC<Props> = (props) => {
|
|||||||
<div
|
<div
|
||||||
className={classNames(
|
className={classNames(
|
||||||
"flex flex-nowrap text-sm font-medium",
|
"flex flex-nowrap text-sm font-medium",
|
||||||
isBookingPage && "dark:text-darkgray-600 text-gray-600",
|
isBookingPage && "text-default",
|
||||||
!eventType.metadata?.multipleDuration && "items-center"
|
!eventType.metadata?.multipleDuration && "items-center"
|
||||||
)}>
|
)}>
|
||||||
<FiClock
|
<Clock
|
||||||
className={classNames(
|
className={classNames(
|
||||||
"min-h-4 min-w-4 ml-[2px] inline-block ltr:mr-[10px] rtl:ml-[10px]",
|
"ml-[2px] inline-block h-4 w-4 ltr:mr-[10px] rtl:ml-[10px]",
|
||||||
isBookingPage && "mt-[2px]"
|
isBookingPage && "mt-[2px]"
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
@@ -137,9 +132,9 @@ const BookingDescription: FC<Props> = (props) => {
|
|||||||
variant="gray"
|
variant="gray"
|
||||||
className={classNames(
|
className={classNames(
|
||||||
duration === dur.toString()
|
duration === dur.toString()
|
||||||
? "bg-darkgray-200 text-darkgray-900 dark:bg-darkmodebrand dark:!text-darkmodebrandcontrast"
|
? "bg-emphasis border-emphasis text-emphasis "
|
||||||
: "hover:bg-darkgray-200 dark:hover:bg-darkmodebrand hover:text-darkgray-900 dark:hover:text-darkmodebrandcontrast dark:bg-darkgray-200 bg-gray-200 text-gray-900 dark:text-white",
|
: "bg-subtle text-default border-transparent ",
|
||||||
"cursor-pointer"
|
"cursor-pointer border"
|
||||||
)}
|
)}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setDuration(dur);
|
setDuration(dur);
|
||||||
|
|||||||
@@ -1,22 +1,36 @@
|
|||||||
import { FormattedNumber, IntlProvider } from "react-intl";
|
import { i18n } from "next-i18next";
|
||||||
|
import type { TFunction } from "next-i18next";
|
||||||
|
|
||||||
import getPaymentAppData from "@calcom/lib/getPaymentAppData";
|
import getPaymentAppData from "@calcom/lib/getPaymentAppData";
|
||||||
import { FiCreditCard } from "@calcom/ui/components/icon";
|
import { CreditCard } from "@calcom/ui/components/icon";
|
||||||
|
|
||||||
const BookingDescriptionPayment = (props: { eventType: Parameters<typeof getPaymentAppData>[0] }) => {
|
const BookingDescriptionPayment = (props: {
|
||||||
|
eventType: Parameters<typeof getPaymentAppData>[0];
|
||||||
|
t: TFunction;
|
||||||
|
i18n: typeof i18n;
|
||||||
|
}) => {
|
||||||
const paymentAppData = getPaymentAppData(props.eventType);
|
const paymentAppData = getPaymentAppData(props.eventType);
|
||||||
if (!paymentAppData || paymentAppData.price <= 0) return null;
|
if (!paymentAppData || paymentAppData.price <= 0) return null;
|
||||||
|
|
||||||
|
const params = {
|
||||||
|
amount: paymentAppData.price / 100.0,
|
||||||
|
formatParams: { amount: { currency: paymentAppData.currency } },
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<p className="text-bookinglight -ml-2 px-2 text-sm ">
|
<p className="text-bookinglight -ml-2 px-2 text-sm ">
|
||||||
<FiCreditCard className="ml-[2px] -mt-1 inline-block h-4 w-4 ltr:mr-[10px] rtl:ml-[10px]" />
|
<CreditCard className="ml-[2px] -mt-1 inline-block h-4 w-4 ltr:mr-[10px] rtl:ml-[10px]" />
|
||||||
<IntlProvider locale="en">
|
{paymentAppData.paymentOption === "HOLD" ? (
|
||||||
<FormattedNumber
|
<>{props.t("no_show_fee_amount", params)}</>
|
||||||
value={paymentAppData.price / 100.0}
|
) : (
|
||||||
style="currency"
|
<>
|
||||||
currency={paymentAppData.currency?.toUpperCase()}
|
{/* If undefined this will default to the browser locale */}
|
||||||
/>
|
{new Intl.NumberFormat(i18n?.language, {
|
||||||
</IntlProvider>
|
style: "currency",
|
||||||
|
currency: paymentAppData.currency,
|
||||||
|
}).format(paymentAppData.price / 100)}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
</p>
|
</p>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
import { BookingStatus } from "@prisma/client";
|
|
||||||
import { useRouter } from "next/router";
|
import { useRouter } from "next/router";
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
|
|
||||||
@@ -10,8 +9,10 @@ import "@calcom/dayjs/locales";
|
|||||||
import ViewRecordingsDialog from "@calcom/features/ee/video/ViewRecordingsDialog";
|
import ViewRecordingsDialog from "@calcom/features/ee/video/ViewRecordingsDialog";
|
||||||
import classNames from "@calcom/lib/classNames";
|
import classNames from "@calcom/lib/classNames";
|
||||||
import { formatTime } from "@calcom/lib/date-fns";
|
import { formatTime } from "@calcom/lib/date-fns";
|
||||||
|
import getPaymentAppData from "@calcom/lib/getPaymentAppData";
|
||||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||||
import { getEveryFreqFor } from "@calcom/lib/recurringStrings";
|
import { getEveryFreqFor } from "@calcom/lib/recurringStrings";
|
||||||
|
import { BookingStatus } from "@calcom/prisma/enums";
|
||||||
import type { RouterInputs, RouterOutputs } from "@calcom/trpc/react";
|
import type { RouterInputs, RouterOutputs } from "@calcom/trpc/react";
|
||||||
import { trpc } from "@calcom/trpc/react";
|
import { trpc } from "@calcom/trpc/react";
|
||||||
import type { ActionType } from "@calcom/ui";
|
import type { ActionType } from "@calcom/ui";
|
||||||
@@ -28,10 +29,11 @@ import {
|
|||||||
TableActions,
|
TableActions,
|
||||||
TextAreaField,
|
TextAreaField,
|
||||||
} from "@calcom/ui";
|
} from "@calcom/ui";
|
||||||
import { FiCheck, FiClock, FiMapPin, FiRefreshCcw, FiSend, FiSlash, FiX } from "@calcom/ui/components/icon";
|
import { Check, Clock, MapPin, RefreshCcw, Send, Ban, X, CreditCard } from "@calcom/ui/components/icon";
|
||||||
|
|
||||||
import useMeQuery from "@lib/hooks/useMeQuery";
|
import useMeQuery from "@lib/hooks/useMeQuery";
|
||||||
|
|
||||||
|
import { ChargeCardDialog } from "@components/dialog/ChargeCardDialog";
|
||||||
import { EditLocationDialog } from "@components/dialog/EditLocationDialog";
|
import { EditLocationDialog } from "@components/dialog/EditLocationDialog";
|
||||||
import { RescheduleDialog } from "@components/dialog/RescheduleDialog";
|
import { RescheduleDialog } from "@components/dialog/RescheduleDialog";
|
||||||
|
|
||||||
@@ -56,7 +58,9 @@ function BookingListItem(booking: BookingItemProps) {
|
|||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const [rejectionReason, setRejectionReason] = useState<string>("");
|
const [rejectionReason, setRejectionReason] = useState<string>("");
|
||||||
const [rejectionDialogIsOpen, setRejectionDialogIsOpen] = useState(false);
|
const [rejectionDialogIsOpen, setRejectionDialogIsOpen] = useState(false);
|
||||||
|
const [chargeCardDialogIsOpen, setChargeCardDialogIsOpen] = useState(false);
|
||||||
const [viewRecordingsDialogIsOpen, setViewRecordingsDialogIsOpen] = useState<boolean>(false);
|
const [viewRecordingsDialogIsOpen, setViewRecordingsDialogIsOpen] = useState<boolean>(false);
|
||||||
|
const cardCharged = booking?.payment[0]?.success;
|
||||||
const mutation = trpc.viewer.bookings.confirm.useMutation({
|
const mutation = trpc.viewer.bookings.confirm.useMutation({
|
||||||
onSuccess: (data) => {
|
onSuccess: (data) => {
|
||||||
if (data?.status === BookingStatus.REJECTED) {
|
if (data?.status === BookingStatus.REJECTED) {
|
||||||
@@ -83,6 +87,8 @@ function BookingListItem(booking: BookingItemProps) {
|
|||||||
const isTabRecurring = booking.listingStatus === "recurring";
|
const isTabRecurring = booking.listingStatus === "recurring";
|
||||||
const isTabUnconfirmed = booking.listingStatus === "unconfirmed";
|
const isTabUnconfirmed = booking.listingStatus === "unconfirmed";
|
||||||
|
|
||||||
|
const paymentAppData = getPaymentAppData(booking.eventType);
|
||||||
|
|
||||||
const bookingConfirm = async (confirm: boolean) => {
|
const bookingConfirm = async (confirm: boolean) => {
|
||||||
let body = {
|
let body = {
|
||||||
bookingId: booking.id,
|
bookingId: booking.id,
|
||||||
@@ -113,19 +119,23 @@ function BookingListItem(booking: BookingItemProps) {
|
|||||||
onClick: () => {
|
onClick: () => {
|
||||||
setRejectionDialogIsOpen(true);
|
setRejectionDialogIsOpen(true);
|
||||||
},
|
},
|
||||||
icon: FiSlash,
|
icon: Ban,
|
||||||
disabled: mutation.isLoading,
|
disabled: mutation.isLoading,
|
||||||
},
|
},
|
||||||
{
|
// For bookings with payment, only confirm if the booking is paid for
|
||||||
id: "confirm",
|
...((isPending && !booking?.eventType?.price) || (!!booking?.eventType?.price && booking.paid)
|
||||||
label: (isTabRecurring || isTabUnconfirmed) && isRecurring ? t("confirm_all") : t("confirm"),
|
? [
|
||||||
onClick: () => {
|
{
|
||||||
bookingConfirm(true);
|
id: "confirm",
|
||||||
},
|
label: (isTabRecurring || isTabUnconfirmed) && isRecurring ? t("confirm_all") : t("confirm"),
|
||||||
icon: FiCheck,
|
onClick: () => {
|
||||||
disabled: mutation.isLoading,
|
bookingConfirm(true);
|
||||||
color: "primary",
|
},
|
||||||
},
|
icon: Check,
|
||||||
|
disabled: mutation.isLoading,
|
||||||
|
},
|
||||||
|
]
|
||||||
|
: []),
|
||||||
];
|
];
|
||||||
|
|
||||||
const showRecordingActions: ActionType[] = [
|
const showRecordingActions: ActionType[] = [
|
||||||
@@ -149,7 +159,7 @@ function BookingListItem(booking: BookingItemProps) {
|
|||||||
isTabRecurring && isRecurring ? "&allRemainingBookings=true" : ""
|
isTabRecurring && isRecurring ? "&allRemainingBookings=true" : ""
|
||||||
}${booking.seatsReferences.length ? `&seatReferenceUid=${getSeatReferenceUid()}` : ""}
|
}${booking.seatsReferences.length ? `&seatReferenceUid=${getSeatReferenceUid()}` : ""}
|
||||||
`,
|
`,
|
||||||
icon: FiX,
|
icon: X,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: "edit_booking",
|
id: "edit_booking",
|
||||||
@@ -157,7 +167,7 @@ function BookingListItem(booking: BookingItemProps) {
|
|||||||
actions: [
|
actions: [
|
||||||
{
|
{
|
||||||
id: "reschedule",
|
id: "reschedule",
|
||||||
icon: FiClock,
|
icon: Clock,
|
||||||
label: t("reschedule_booking"),
|
label: t("reschedule_booking"),
|
||||||
href: `/reschedule/${booking.uid}${
|
href: `/reschedule/${booking.uid}${
|
||||||
booking.seatsReferences.length ? `?seatReferenceUid=${getSeatReferenceUid()}` : ""
|
booking.seatsReferences.length ? `?seatReferenceUid=${getSeatReferenceUid()}` : ""
|
||||||
@@ -165,7 +175,7 @@ function BookingListItem(booking: BookingItemProps) {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: "reschedule_request",
|
id: "reschedule_request",
|
||||||
icon: FiSend,
|
icon: Send,
|
||||||
iconClassName: "rotate-45 w-[16px] -translate-x-0.5 ",
|
iconClassName: "rotate-45 w-[16px] -translate-x-0.5 ",
|
||||||
label: t("send_reschedule_request"),
|
label: t("send_reschedule_request"),
|
||||||
onClick: () => {
|
onClick: () => {
|
||||||
@@ -178,12 +188,24 @@ function BookingListItem(booking: BookingItemProps) {
|
|||||||
onClick: () => {
|
onClick: () => {
|
||||||
setIsOpenLocationDialog(true);
|
setIsOpenLocationDialog(true);
|
||||||
},
|
},
|
||||||
icon: FiMapPin,
|
icon: MapPin,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
|
const chargeCardActions: ActionType[] = [
|
||||||
|
{
|
||||||
|
id: "charge_card",
|
||||||
|
label: cardCharged ? t("no_show_fee_charged") : t("collect_no_show_fee"),
|
||||||
|
disabled: cardCharged,
|
||||||
|
onClick: () => {
|
||||||
|
setChargeCardDialogIsOpen(true);
|
||||||
|
},
|
||||||
|
icon: CreditCard,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
if (isTabRecurring && isRecurring) {
|
if (isTabRecurring && isRecurring) {
|
||||||
bookedActions = bookedActions.filter((action) => action.id !== "edit_booking");
|
bookedActions = bookedActions.filter((action) => action.id !== "edit_booking");
|
||||||
}
|
}
|
||||||
@@ -194,7 +216,7 @@ function BookingListItem(booking: BookingItemProps) {
|
|||||||
|
|
||||||
const RequestSentMessage = () => {
|
const RequestSentMessage = () => {
|
||||||
return (
|
return (
|
||||||
<Badge startIcon={FiSend} size="md" variant="gray" data-testid="request_reschedule_sent">
|
<Badge startIcon={Send} size="md" variant="gray" data-testid="request_reschedule_sent">
|
||||||
{t("reschedule_request_sent")}
|
{t("reschedule_request_sent")}
|
||||||
</Badge>
|
</Badge>
|
||||||
);
|
);
|
||||||
@@ -237,10 +259,14 @@ function BookingListItem(booking: BookingItemProps) {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const title = booking.title;
|
||||||
|
// To be used after we run query on legacy bookings
|
||||||
|
// const showRecordingsButtons = booking.isRecorded && isPast && isConfirmed;
|
||||||
|
|
||||||
const showRecordingsButtons =
|
const showRecordingsButtons =
|
||||||
(booking.location === "integrations:daily" || booking?.location?.trim() === "") && isPast && isConfirmed;
|
(booking.location === "integrations:daily" || booking?.location?.trim() === "") && isPast && isConfirmed;
|
||||||
|
|
||||||
const title = booking.title;
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<RescheduleDialog
|
<RescheduleDialog
|
||||||
@@ -254,6 +280,15 @@ function BookingListItem(booking: BookingItemProps) {
|
|||||||
isOpenDialog={isOpenSetLocationDialog}
|
isOpenDialog={isOpenSetLocationDialog}
|
||||||
setShowLocationModal={setIsOpenLocationDialog}
|
setShowLocationModal={setIsOpenLocationDialog}
|
||||||
/>
|
/>
|
||||||
|
{booking.paid && booking.payment[0] && (
|
||||||
|
<ChargeCardDialog
|
||||||
|
isOpenDialog={chargeCardDialogIsOpen}
|
||||||
|
setIsOpenDialog={setChargeCardDialogIsOpen}
|
||||||
|
bookingId={booking.id}
|
||||||
|
paymentAmount={booking.payment[0].amount}
|
||||||
|
paymentCurrency={booking.payment[0].currency}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
{showRecordingsButtons && (
|
{showRecordingsButtons && (
|
||||||
<ViewRecordingsDialog
|
<ViewRecordingsDialog
|
||||||
booking={booking}
|
booking={booking}
|
||||||
@@ -271,7 +306,7 @@ function BookingListItem(booking: BookingItemProps) {
|
|||||||
label={
|
label={
|
||||||
<>
|
<>
|
||||||
{t("rejection_reason")}
|
{t("rejection_reason")}
|
||||||
<span className="font-normal text-gray-500"> (Optional)</span>
|
<span className="text-subtle font-normal"> (Optional)</span>
|
||||||
</>
|
</>
|
||||||
}
|
}
|
||||||
value={rejectionReason}
|
value={rejectionReason}
|
||||||
@@ -292,13 +327,13 @@ function BookingListItem(booking: BookingItemProps) {
|
|||||||
</DialogContent>
|
</DialogContent>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
|
|
||||||
<tr className="group flex flex-col hover:bg-gray-50 sm:flex-row">
|
<tr className="hover:bg-muted group flex flex-col sm:flex-row">
|
||||||
<td
|
<td
|
||||||
className="hidden align-top ltr:pl-6 rtl:pr-6 sm:table-cell sm:min-w-[12rem]"
|
className="hidden align-top ltr:pl-6 rtl:pr-6 sm:table-cell sm:min-w-[12rem]"
|
||||||
onClick={onClickTableData}>
|
onClick={onClickTableData}>
|
||||||
<div className="cursor-pointer py-4">
|
<div className="cursor-pointer py-4">
|
||||||
<div className="text-sm leading-6 text-gray-900">{startTime}</div>
|
<div className="text-emphasis text-sm leading-6">{startTime}</div>
|
||||||
<div className="text-sm text-gray-500">
|
<div className="text-subtle text-sm">
|
||||||
{formatTime(booking.startTime, user?.timeFormat, user?.timeZone)} -{" "}
|
{formatTime(booking.startTime, user?.timeFormat, user?.timeZone)} -{" "}
|
||||||
{formatTime(booking.endTime, user?.timeFormat, user?.timeZone)}
|
{formatTime(booking.endTime, user?.timeFormat, user?.timeZone)}
|
||||||
<MeetingTimeInTimezones
|
<MeetingTimeInTimezones
|
||||||
@@ -319,13 +354,17 @@ function BookingListItem(booking: BookingItemProps) {
|
|||||||
{booking.eventType.team.name}
|
{booking.eventType.team.name}
|
||||||
</Badge>
|
</Badge>
|
||||||
)}
|
)}
|
||||||
{booking.paid && (
|
{booking.paid && !booking.payment[0] ? (
|
||||||
<Badge className="ltr:mr-2 rtl:ml-2" variant="green">
|
<Badge className="ltr:mr-2 rtl:ml-2" variant="orange">
|
||||||
{t("paid")}
|
{t("error_collecting_card")}
|
||||||
</Badge>
|
</Badge>
|
||||||
)}
|
) : booking.paid ? (
|
||||||
|
<Badge className="ltr:mr-2 rtl:ml-2" variant="green">
|
||||||
|
{booking.payment[0].paymentOption === "HOLD" ? t("card_held") : t("paid")}
|
||||||
|
</Badge>
|
||||||
|
) : null}
|
||||||
{recurringDates !== undefined && (
|
{recurringDates !== undefined && (
|
||||||
<div className="mt-2 text-sm text-gray-400">
|
<div className="text-muted mt-2 text-sm">
|
||||||
<RecurringBookingsTooltip booking={booking} recurringDates={recurringDates} />
|
<RecurringBookingsTooltip booking={booking} recurringDates={recurringDates} />
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
@@ -335,8 +374,8 @@ function BookingListItem(booking: BookingItemProps) {
|
|||||||
{/* Time and Badges for mobile */}
|
{/* Time and Badges for mobile */}
|
||||||
<div className="w-full pt-4 pb-2 sm:hidden">
|
<div className="w-full pt-4 pb-2 sm:hidden">
|
||||||
<div className="flex w-full items-center justify-between sm:hidden">
|
<div className="flex w-full items-center justify-between sm:hidden">
|
||||||
<div className="text-sm leading-6 text-gray-900">{startTime}</div>
|
<div className="text-emphasis text-sm leading-6">{startTime}</div>
|
||||||
<div className="pr-2 text-sm text-gray-500">
|
<div className="text-subtle pr-2 text-sm">
|
||||||
{formatTime(booking.startTime, user?.timeFormat, user?.timeZone)} -{" "}
|
{formatTime(booking.startTime, user?.timeFormat, user?.timeZone)} -{" "}
|
||||||
{formatTime(booking.endTime, user?.timeFormat, user?.timeZone)}
|
{formatTime(booking.endTime, user?.timeFormat, user?.timeZone)}
|
||||||
<MeetingTimeInTimezones
|
<MeetingTimeInTimezones
|
||||||
@@ -365,7 +404,7 @@ function BookingListItem(booking: BookingItemProps) {
|
|||||||
</Badge>
|
</Badge>
|
||||||
)}
|
)}
|
||||||
{recurringDates !== undefined && (
|
{recurringDates !== undefined && (
|
||||||
<div className="text-sm text-gray-400 sm:hidden">
|
<div className="text-muted text-sm sm:hidden">
|
||||||
<RecurringBookingsTooltip booking={booking} recurringDates={recurringDates} />
|
<RecurringBookingsTooltip booking={booking} recurringDates={recurringDates} />
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
@@ -375,21 +414,21 @@ function BookingListItem(booking: BookingItemProps) {
|
|||||||
<div
|
<div
|
||||||
title={title}
|
title={title}
|
||||||
className={classNames(
|
className={classNames(
|
||||||
"max-w-10/12 sm:max-w-56 text-sm font-medium leading-6 text-gray-900 md:max-w-full",
|
"max-w-10/12 sm:max-w-56 text-emphasis text-sm font-medium leading-6 md:max-w-full",
|
||||||
isCancelled ? "line-through" : ""
|
isCancelled ? "line-through" : ""
|
||||||
)}>
|
)}>
|
||||||
{title}
|
{title}
|
||||||
<span> </span>
|
<span> </span>
|
||||||
|
|
||||||
{!!booking?.eventType?.price && !booking.paid && (
|
{paymentAppData.enabled && !booking.paid && booking.payment.length && (
|
||||||
<Badge className="hidden ltr:ml-2 ltr:mr-2 rtl:ml-2 sm:inline-flex" variant="orange">
|
<Badge className="ms-2 me-2 hidden sm:inline-flex" variant="orange">
|
||||||
{t("pending_payment")}
|
{t("pending_payment")}
|
||||||
</Badge>
|
</Badge>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
{booking.description && (
|
{booking.description && (
|
||||||
<div
|
<div
|
||||||
className="max-w-10/12 sm:max-w-32 md:max-w-52 xl:max-w-80 truncate text-sm text-gray-600"
|
className="max-w-10/12 sm:max-w-32 md:max-w-52 xl:max-w-80 text-default truncate text-sm"
|
||||||
title={booking.description}>
|
title={booking.description}>
|
||||||
"{booking.description}"
|
"{booking.description}"
|
||||||
</div>
|
</div>
|
||||||
@@ -413,7 +452,7 @@ function BookingListItem(booking: BookingItemProps) {
|
|||||||
<>
|
<>
|
||||||
{isPending && user?.id === booking.user?.id && <TableActions actions={pendingActions} />}
|
{isPending && user?.id === booking.user?.id && <TableActions actions={pendingActions} />}
|
||||||
{isConfirmed && <TableActions actions={bookedActions} />}
|
{isConfirmed && <TableActions actions={bookedActions} />}
|
||||||
{isRejected && <div className="text-sm text-gray-500">{t("rejected")}</div>}
|
{isRejected && <div className="text-subtle text-sm">{t("rejected")}</div>}
|
||||||
</>
|
</>
|
||||||
) : null}
|
) : null}
|
||||||
{isPast && isPending && !isConfirmed ? <TableActions actions={bookedActions} /> : null}
|
{isPast && isPending && !isConfirmed ? <TableActions actions={bookedActions} /> : null}
|
||||||
@@ -423,6 +462,11 @@ function BookingListItem(booking: BookingItemProps) {
|
|||||||
<RequestSentMessage />
|
<RequestSentMessage />
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
{booking.status === "ACCEPTED" && booking.paid && booking.payment[0]?.paymentOption === "HOLD" && (
|
||||||
|
<div className="ml-2">
|
||||||
|
<TableActions actions={chargeCardActions} />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
</>
|
</>
|
||||||
@@ -443,12 +487,12 @@ const RecurringBookingsTooltip = ({ booking, recurringDates }: RecurringBookings
|
|||||||
i18n: { language },
|
i18n: { language },
|
||||||
} = useLocale();
|
} = useLocale();
|
||||||
const now = new Date();
|
const now = new Date();
|
||||||
const recurringCount = recurringDates.filter((date) => {
|
const recurringCount = recurringDates.filter((recurringDate) => {
|
||||||
return (
|
return (
|
||||||
date >= now &&
|
recurringDate >= now &&
|
||||||
!booking.recurringInfo?.bookings[BookingStatus.CANCELLED]
|
!booking.recurringInfo?.bookings[BookingStatus.CANCELLED]
|
||||||
.map((date) => date.toDateString())
|
.map((date) => date.toDateString())
|
||||||
.includes(date.toDateString())
|
.includes(recurringDate.toDateString())
|
||||||
);
|
);
|
||||||
}).length;
|
}).length;
|
||||||
|
|
||||||
@@ -475,10 +519,10 @@ const RecurringBookingsTooltip = ({ booking, recurringDates }: RecurringBookings
|
|||||||
</p>
|
</p>
|
||||||
);
|
);
|
||||||
})}>
|
})}>
|
||||||
<div className="text-gray-600 dark:text-white">
|
<div className="text-default">
|
||||||
<FiRefreshCcw
|
<RefreshCcw
|
||||||
strokeWidth="3"
|
strokeWidth="3"
|
||||||
className="float-left mr-1 mt-1.5 inline-block h-3 w-3 text-gray-400"
|
className="text-muted float-left mr-1 mt-1.5 inline-block h-3 w-3"
|
||||||
/>
|
/>
|
||||||
<p className="mt-1 pl-5 text-xs">
|
<p className="mt-1 pl-5 text-xs">
|
||||||
{booking.status === BookingStatus.ACCEPTED
|
{booking.status === BookingStatus.ACCEPTED
|
||||||
@@ -551,13 +595,13 @@ const DisplayAttendees = ({
|
|||||||
}) => {
|
}) => {
|
||||||
const { t } = useLocale();
|
const { t } = useLocale();
|
||||||
return (
|
return (
|
||||||
<div className="text-sm text-gray-900">
|
<div className="text-emphasis text-sm">
|
||||||
{user && <FirstAttendee user={user} currentEmail={currentEmail} />}
|
{user && <FirstAttendee user={user} currentEmail={currentEmail} />}
|
||||||
{attendees.length > 1 ? <span>, </span> : <span> {t("and")} </span>}
|
{attendees.length > 1 ? <span>, </span> : <span> {t("and")} </span>}
|
||||||
<Attendee {...attendees[0]} />
|
<Attendee {...attendees[0]} />
|
||||||
{attendees.length > 1 && (
|
{attendees.length > 1 && (
|
||||||
<>
|
<>
|
||||||
<div className="inline-block text-sm text-gray-900"> {t("and")} </div>
|
<div className="text-emphasis inline-block text-sm"> {t("and")} </div>
|
||||||
{attendees.length > 2 ? (
|
{attendees.length > 2 ? (
|
||||||
<Tooltip
|
<Tooltip
|
||||||
content={attendees.slice(1).map((attendee) => (
|
content={attendees.slice(1).map((attendee) => (
|
||||||
|
|||||||
@@ -2,11 +2,10 @@ import { useRouter } from "next/router";
|
|||||||
import { useCallback, useState } from "react";
|
import { useCallback, useState } from "react";
|
||||||
|
|
||||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||||
import useTheme from "@calcom/lib/hooks/useTheme";
|
|
||||||
import { collectPageParameters, telemetryEventTypes, useTelemetry } from "@calcom/lib/telemetry";
|
import { collectPageParameters, telemetryEventTypes, useTelemetry } from "@calcom/lib/telemetry";
|
||||||
import type { RecurringEvent } from "@calcom/types/Calendar";
|
import type { RecurringEvent } from "@calcom/types/Calendar";
|
||||||
import { Button, TextArea } from "@calcom/ui";
|
import { Button, TextArea } from "@calcom/ui";
|
||||||
import { FiX } from "@calcom/ui/components/icon";
|
import { X } from "@calcom/ui/components/icon";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
booking: {
|
booking: {
|
||||||
@@ -34,7 +33,6 @@ export default function CancelBooking(props: Props) {
|
|||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
const telemetry = useTelemetry();
|
const telemetry = useTelemetry();
|
||||||
const [error, setError] = useState<string | null>(booking ? null : t("booking_already_cancelled"));
|
const [error, setError] = useState<string | null>(booking ? null : t("booking_already_cancelled"));
|
||||||
useTheme(props.theme);
|
|
||||||
|
|
||||||
const cancelBookingRef = useCallback((node: HTMLTextAreaElement) => {
|
const cancelBookingRef = useCallback((node: HTMLTextAreaElement) => {
|
||||||
if (node !== null) {
|
if (node !== null) {
|
||||||
@@ -47,11 +45,11 @@ export default function CancelBooking(props: Props) {
|
|||||||
<>
|
<>
|
||||||
{error && (
|
{error && (
|
||||||
<div className="mt-8">
|
<div className="mt-8">
|
||||||
<div className="mx-auto flex h-12 w-12 items-center justify-center rounded-full bg-red-100">
|
<div className="bg-error mx-auto flex h-12 w-12 items-center justify-center rounded-full">
|
||||||
<FiX className="h-6 w-6 text-red-600" />
|
<X className="h-6 w-6 text-red-600" />
|
||||||
</div>
|
</div>
|
||||||
<div className="mt-3 text-center sm:mt-5">
|
<div className="mt-3 text-center sm:mt-5">
|
||||||
<h3 className="text-lg font-medium leading-6 text-gray-900" id="modal-title">
|
<h3 className="text-emphasis text-lg font-medium leading-6" id="modal-title">
|
||||||
{error}
|
{error}
|
||||||
</h3>
|
</h3>
|
||||||
</div>
|
</div>
|
||||||
@@ -59,13 +57,13 @@ export default function CancelBooking(props: Props) {
|
|||||||
)}
|
)}
|
||||||
{!error && (
|
{!error && (
|
||||||
<div className="mt-5 sm:mt-6">
|
<div className="mt-5 sm:mt-6">
|
||||||
<label className="text-bookingdark font-medium dark:text-white">{t("cancellation_reason")}</label>
|
<label className="text-default font-medium">{t("cancellation_reason")}</label>
|
||||||
<TextArea
|
<TextArea
|
||||||
ref={cancelBookingRef}
|
ref={cancelBookingRef}
|
||||||
placeholder={t("cancellation_reason_placeholder")}
|
placeholder={t("cancellation_reason_placeholder")}
|
||||||
value={cancellationReason}
|
value={cancellationReason}
|
||||||
onChange={(e) => setCancellationReason(e.target.value)}
|
onChange={(e) => setCancellationReason(e.target.value)}
|
||||||
className="dark:bg-darkgray-100 dark:border-darkgray-400 mt-2 mb-4 w-full dark:text-white "
|
className="mt-2 mb-4 w-full "
|
||||||
rows={3}
|
rows={3}
|
||||||
/>
|
/>
|
||||||
<div className="flex flex-col-reverse rtl:space-x-reverse ">
|
<div className="flex flex-col-reverse rtl:space-x-reverse ">
|
||||||
@@ -77,7 +75,6 @@ export default function CancelBooking(props: Props) {
|
|||||||
{t("nevermind")}
|
{t("nevermind")}
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
className="flex justify-center"
|
|
||||||
data-testid="cancel"
|
data-testid="cancel"
|
||||||
onClick={async () => {
|
onClick={async () => {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import { SkeletonText } from "@calcom/ui";
|
|||||||
|
|
||||||
function SkeletonLoader() {
|
function SkeletonLoader() {
|
||||||
return (
|
return (
|
||||||
<ul className="animate-pulse divide-y divide-gray-200 rounded-md border border-gray-200 bg-white sm:overflow-hidden">
|
<ul className="divide-subtle border-subtle bg-default animate-pulse divide-y rounded-md border sm:overflow-hidden">
|
||||||
<SkeletonItem />
|
<SkeletonItem />
|
||||||
<SkeletonItem />
|
<SkeletonItem />
|
||||||
<SkeletonItem />
|
<SkeletonItem />
|
||||||
|
|||||||
@@ -133,10 +133,7 @@ export const SlotPicker = ({
|
|||||||
eventTypeId: eventType.id,
|
eventTypeId: eventType.id,
|
||||||
eventTypeSlug: eventType.slug,
|
eventTypeSlug: eventType.slug,
|
||||||
usernameList: users,
|
usernameList: users,
|
||||||
startTime:
|
startTime: dayjs().startOf("day"),
|
||||||
browsingDate === undefined || browsingDate.get("month") === dayjs.tz(undefined, timeZone).get("month")
|
|
||||||
? dayjs.tz(undefined, timeZone).subtract(2, "days").startOf("day")
|
|
||||||
: browsingDate?.startOf("month"),
|
|
||||||
endTime: browsingDate?.endOf("month"),
|
endTime: browsingDate?.endOf("month"),
|
||||||
timeZone,
|
timeZone,
|
||||||
duration,
|
duration,
|
||||||
@@ -167,7 +164,7 @@ export const SlotPicker = ({
|
|||||||
isLoading={isLoading}
|
isLoading={isLoading}
|
||||||
className={classNames(
|
className={classNames(
|
||||||
"mt-8 px-4 pb-4 sm:mt-0 md:min-w-[300px] md:px-4 lg:min-w-[455px]",
|
"mt-8 px-4 pb-4 sm:mt-0 md:min-w-[300px] md:px-4 lg:min-w-[455px]",
|
||||||
selectedDate ? "sm:dark:border-darkgray-200 border-gray-200 sm:border-r sm:p-4 sm:pr-6" : "sm:p-4"
|
selectedDate ? " border-subtle sm:border-r sm:p-4 sm:pr-6" : "sm:p-4"
|
||||||
)}
|
)}
|
||||||
includedDates={Object.keys(monthSlots).filter((k) => monthSlots[k].length > 0)}
|
includedDates={Object.keys(monthSlots).filter((k) => monthSlots[k].length > 0)}
|
||||||
locale={isLocaleReady ? i18n.language : "en"}
|
locale={isLocaleReady ? i18n.language : "en"}
|
||||||
@@ -197,6 +194,7 @@ export const SlotPicker = ({
|
|||||||
bookingAttendees={bookingAttendees}
|
bookingAttendees={bookingAttendees}
|
||||||
recurringCount={recurringEventCount}
|
recurringCount={recurringEventCount}
|
||||||
ethSignature={ethSignature}
|
ethSignature={ethSignature}
|
||||||
|
duration={parseInt(duration)}
|
||||||
/>
|
/>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,12 +1,9 @@
|
|||||||
import type { FC } from "react";
|
import type { FC } from "react";
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
|
|
||||||
import useTheme from "@calcom/lib/hooks/useTheme";
|
|
||||||
import type { ITimezoneOption } from "@calcom/ui";
|
import type { ITimezoneOption } from "@calcom/ui";
|
||||||
import { TimezoneSelect } from "@calcom/ui";
|
import { TimezoneSelect } from "@calcom/ui";
|
||||||
|
|
||||||
import useMeQuery from "@lib/hooks/useMeQuery";
|
|
||||||
|
|
||||||
import { timeZone } from "../../lib/clock";
|
import { timeZone } from "../../lib/clock";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
@@ -15,8 +12,6 @@ type Props = {
|
|||||||
|
|
||||||
const TimeOptions: FC<Props> = ({ onSelectTimeZone }) => {
|
const TimeOptions: FC<Props> = ({ onSelectTimeZone }) => {
|
||||||
const [selectedTimeZone, setSelectedTimeZone] = useState("");
|
const [selectedTimeZone, setSelectedTimeZone] = useState("");
|
||||||
const query = useMeQuery();
|
|
||||||
const userTheme = useTheme(query?.data?.theme).resolvedTheme;
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setSelectedTimeZone(timeZone());
|
setSelectedTimeZone(timeZone());
|
||||||
@@ -28,59 +23,17 @@ const TimeOptions: FC<Props> = ({ onSelectTimeZone }) => {
|
|||||||
}
|
}
|
||||||
}, [selectedTimeZone, onSelectTimeZone]);
|
}, [selectedTimeZone, onSelectTimeZone]);
|
||||||
|
|
||||||
const customStyles = {
|
|
||||||
option: (_provided: object, state: { isSelected: boolean; isFocused: boolean }) => ({
|
|
||||||
padding: "10px 12px !important",
|
|
||||||
color: state.isSelected || state.isFocused ? "#101010" : "#374151",
|
|
||||||
backgroundColor: state.isSelected ? "#E5E7EB !important" : "white",
|
|
||||||
...(userTheme === "dark" && {
|
|
||||||
color: state.isSelected || state.isFocused ? "white" : "#80868B",
|
|
||||||
}),
|
|
||||||
}),
|
|
||||||
control: () => ({
|
|
||||||
display: "flex",
|
|
||||||
cursor: "pointer",
|
|
||||||
backgroundColor: "transparent !important",
|
|
||||||
minWidth: "5rem",
|
|
||||||
height: "24px",
|
|
||||||
minHeight: "24px !important",
|
|
||||||
boxShadow: "none !important",
|
|
||||||
}),
|
|
||||||
singleValue: (provided: object, state: { selectProps: { menuIsOpen: boolean } }) => ({
|
|
||||||
...provided,
|
|
||||||
color: state.selectProps.menuIsOpen ? "#111827" : "#4B5563",
|
|
||||||
...(userTheme === "dark" && {
|
|
||||||
color: "#a5a5a5 !important",
|
|
||||||
}),
|
|
||||||
}),
|
|
||||||
menu: (provided: object) => ({
|
|
||||||
...provided,
|
|
||||||
minWidth: "20rem",
|
|
||||||
margin: "8px 0 0 -20px",
|
|
||||||
}),
|
|
||||||
valueContainer: (provided: object) => ({
|
|
||||||
...provided,
|
|
||||||
padding: "0 0 0 6px",
|
|
||||||
}),
|
|
||||||
dropdownIndicator: (provided: object, state: { selectProps: { menuIsOpen: boolean } }) => ({
|
|
||||||
...provided,
|
|
||||||
transform: state.selectProps.menuIsOpen ? "rotate(180deg)" : "",
|
|
||||||
color: state.selectProps.menuIsOpen ? "#111827" : "#4B5563",
|
|
||||||
marginLeft: 4,
|
|
||||||
padding: 0,
|
|
||||||
...(userTheme === "dark" && {
|
|
||||||
color: "#80868B",
|
|
||||||
}),
|
|
||||||
}),
|
|
||||||
};
|
|
||||||
|
|
||||||
return !!selectedTimeZone ? (
|
return !!selectedTimeZone ? (
|
||||||
<TimezoneSelect
|
<TimezoneSelect
|
||||||
id="timeZone"
|
id="timeZone"
|
||||||
|
classNames={{
|
||||||
|
singleValue: () => "text-default",
|
||||||
|
dropdownIndicator: () => "text-default",
|
||||||
|
menu: () => "!w-64 max-w-[90vw] shadow-dropdown bg-default border-subtle border rounded-md mt-1",
|
||||||
|
}}
|
||||||
|
variant="minimal"
|
||||||
value={selectedTimeZone}
|
value={selectedTimeZone}
|
||||||
onChange={(tz: ITimezoneOption) => setSelectedTimeZone(tz.value)}
|
onChange={(tz: ITimezoneOption) => setSelectedTimeZone(tz.value)}
|
||||||
className="flex h-6 text-sm font-medium"
|
|
||||||
styles={customStyles}
|
|
||||||
/>
|
/>
|
||||||
) : null;
|
) : null;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { FiGlobe } from "@calcom/ui/components/icon";
|
import { Globe } from "@calcom/ui/components/icon";
|
||||||
|
|
||||||
import { timeZone as localStorageTimeZone } from "@lib/clock";
|
import { timeZone as localStorageTimeZone } from "@lib/clock";
|
||||||
|
|
||||||
@@ -17,8 +17,8 @@ export function TimezoneDropdown({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div className="dark:focus-within:bg-darkgray-200 dark:bg-darkgray-100 dark:hover:bg-darkgray-200 -mx-[2px] !mt-3 flex w-fit max-w-[20rem] items-center rounded-[4px] px-1 py-[2px] text-sm font-medium focus-within:bg-gray-200 hover:bg-gray-100 lg:max-w-[12rem] [&_p]:focus-within:text-gray-900 dark:[&_p]:focus-within:text-white [&_svg]:focus-within:text-gray-900 dark:[&_svg]:focus-within:text-white">
|
<div className="dark:focus-within:bg-darkgray-200 dark:hover:bg-darkgray-200 !mt-3 flex w-full max-w-[20rem] items-center rounded-[4px] px-1 text-sm font-medium focus-within:bg-gray-200 hover:bg-gray-100 lg:max-w-[12rem] [&_p]:focus-within:text-gray-900 dark:[&_p]:focus-within:text-white [&_svg]:focus-within:text-gray-900 dark:[&_svg]:focus-within:text-white">
|
||||||
<FiGlobe className="dark:text-darkgray-600 flex h-4 w-4 text-gray-600 ltr:mr-[2px] rtl:ml-[2px]" />
|
<Globe className="dark:text-darkgray-600 flex h-4 w-4 text-gray-600 ltr:mr-2 rtl:ml-2" />
|
||||||
<TimeOptions onSelectTimeZone={handleSelectTimeZone} />
|
<TimeOptions onSelectTimeZone={handleSelectTimeZone} />
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
import dynamic from "next/dynamic";
|
import dynamic from "next/dynamic";
|
||||||
import { useRouter } from "next/router";
|
import { useRouter } from "next/router";
|
||||||
import { useEffect, useMemo, useReducer, useState } from "react";
|
import { useEffect, useMemo, useReducer, useState } from "react";
|
||||||
import { FormattedNumber, IntlProvider } from "react-intl";
|
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
|
||||||
import BookingPageTagManager from "@calcom/app-store/BookingPageTagManager";
|
import BookingPageTagManager from "@calcom/app-store/BookingPageTagManager";
|
||||||
@@ -14,18 +13,17 @@ import {
|
|||||||
useIsBackgroundTransparent,
|
useIsBackgroundTransparent,
|
||||||
useIsEmbed,
|
useIsEmbed,
|
||||||
} from "@calcom/embed-core/embed-iframe";
|
} from "@calcom/embed-core/embed-iframe";
|
||||||
import CustomBranding from "@calcom/lib/CustomBranding";
|
|
||||||
import classNames from "@calcom/lib/classNames";
|
import classNames from "@calcom/lib/classNames";
|
||||||
|
import useGetBrandingColours from "@calcom/lib/getBrandColours";
|
||||||
import getPaymentAppData from "@calcom/lib/getPaymentAppData";
|
import getPaymentAppData from "@calcom/lib/getPaymentAppData";
|
||||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||||
import useTheme from "@calcom/lib/hooks/useTheme";
|
import useTheme from "@calcom/lib/hooks/useTheme";
|
||||||
import notEmpty from "@calcom/lib/notEmpty";
|
import notEmpty from "@calcom/lib/notEmpty";
|
||||||
import { getRecurringFreq } from "@calcom/lib/recurringStrings";
|
import { getRecurringFreq } from "@calcom/lib/recurringStrings";
|
||||||
import { collectPageParameters, telemetryEventTypes, useTelemetry } from "@calcom/lib/telemetry";
|
|
||||||
import { detectBrowserTimeFormat, setIs24hClockInLocalStorage, TimeFormat } from "@calcom/lib/timeFormat";
|
import { detectBrowserTimeFormat, setIs24hClockInLocalStorage, TimeFormat } from "@calcom/lib/timeFormat";
|
||||||
import { trpc } from "@calcom/trpc";
|
import { trpc } from "@calcom/trpc";
|
||||||
import { HeadSeo } from "@calcom/ui";
|
import { HeadSeo, NumberInput, useCalcomTheme } from "@calcom/ui";
|
||||||
import { FiCreditCard, FiUser, FiRefreshCcw } from "@calcom/ui/components/icon";
|
import { CreditCard, User, RefreshCcw } from "@calcom/ui/components/icon";
|
||||||
|
|
||||||
import { timeZone as localStorageTimeZone } from "@lib/clock";
|
import { timeZone as localStorageTimeZone } from "@lib/clock";
|
||||||
|
|
||||||
@@ -38,7 +36,7 @@ import type { AvailabilityPageProps } from "../../../pages/[user]/[type]";
|
|||||||
import type { DynamicAvailabilityPageProps } from "../../../pages/d/[link]/[slug]";
|
import type { DynamicAvailabilityPageProps } from "../../../pages/d/[link]/[slug]";
|
||||||
import type { AvailabilityTeamPageProps } from "../../../pages/team/[slug]/[type]";
|
import type { AvailabilityTeamPageProps } from "../../../pages/team/[slug]/[type]";
|
||||||
|
|
||||||
const PoweredByCal = dynamic(() => import("@components/ui/PoweredByCal"));
|
const PoweredBy = dynamic(() => import("@calcom/ee/components/PoweredBy"));
|
||||||
|
|
||||||
const Toaster = dynamic(() => import("react-hot-toast").then((mod) => mod.Toaster), { ssr: false });
|
const Toaster = dynamic(() => import("react-hot-toast").then((mod) => mod.Toaster), { ssr: false });
|
||||||
/*const SlotPicker = dynamic(() => import("../SlotPicker").then((mod) => mod.SlotPicker), {
|
/*const SlotPicker = dynamic(() => import("../SlotPicker").then((mod) => mod.SlotPicker), {
|
||||||
@@ -58,13 +56,25 @@ const dateQuerySchema = z.object({
|
|||||||
|
|
||||||
export type Props = AvailabilityTeamPageProps | AvailabilityPageProps | DynamicAvailabilityPageProps;
|
export type Props = AvailabilityTeamPageProps | AvailabilityPageProps | DynamicAvailabilityPageProps;
|
||||||
|
|
||||||
|
const useBrandColors = ({ brandColor, darkBrandColor }: { brandColor: string; darkBrandColor: string }) => {
|
||||||
|
const brandTheme = useGetBrandingColours({
|
||||||
|
lightVal: brandColor,
|
||||||
|
darkVal: darkBrandColor,
|
||||||
|
});
|
||||||
|
useCalcomTheme(brandTheme);
|
||||||
|
};
|
||||||
|
|
||||||
const AvailabilityPage = ({ profile, eventType, ...restProps }: Props) => {
|
const AvailabilityPage = ({ profile, eventType, ...restProps }: Props) => {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const isEmbed = useIsEmbed(restProps.isEmbed);
|
const isEmbed = useIsEmbed(restProps.isEmbed);
|
||||||
const query = dateQuerySchema.parse(router.query);
|
const query = dateQuerySchema.parse(router.query);
|
||||||
const { rescheduleUid } = query;
|
const { rescheduleUid } = query;
|
||||||
useTheme(profile.theme);
|
useTheme(profile.theme);
|
||||||
const { t } = useLocale();
|
useBrandColors({
|
||||||
|
brandColor: profile.brandColor,
|
||||||
|
darkBrandColor: profile.darkBrandColor,
|
||||||
|
});
|
||||||
|
const { t, i18n } = useLocale();
|
||||||
const availabilityDatePickerEmbedStyles = useEmbedStyles("availabilityDatePicker");
|
const availabilityDatePickerEmbedStyles = useEmbedStyles("availabilityDatePicker");
|
||||||
//TODO: Plan to remove shouldAlignCentrallyInEmbed config
|
//TODO: Plan to remove shouldAlignCentrallyInEmbed config
|
||||||
const shouldAlignCentrallyInEmbed = useEmbedNonStylesConfig("align") !== "left";
|
const shouldAlignCentrallyInEmbed = useEmbedNonStylesConfig("align") !== "left";
|
||||||
@@ -93,8 +103,9 @@ const AvailabilityPage = ({ profile, eventType, ...restProps }: Props) => {
|
|||||||
|
|
||||||
const [recurringEventCount, setRecurringEventCount] = useState(eventType.recurringEvent?.count);
|
const [recurringEventCount, setRecurringEventCount] = useState(eventType.recurringEvent?.count);
|
||||||
|
|
||||||
|
/*
|
||||||
const telemetry = useTelemetry();
|
const telemetry = useTelemetry();
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (top !== window) {
|
if (top !== window) {
|
||||||
//page_view will be collected automatically by _middleware.ts
|
//page_view will be collected automatically by _middleware.ts
|
||||||
telemetry.event(
|
telemetry.event(
|
||||||
@@ -102,7 +113,7 @@ const AvailabilityPage = ({ profile, eventType, ...restProps }: Props) => {
|
|||||||
collectPageParameters("/availability", { isTeamBooking: document.URL.includes("team/") })
|
collectPageParameters("/availability", { isTeamBooking: document.URL.includes("team/") })
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}, [telemetry]);
|
}, [telemetry]); */
|
||||||
const embedUiConfig = useEmbedUiConfig();
|
const embedUiConfig = useEmbedUiConfig();
|
||||||
// get dynamic user list here
|
// get dynamic user list here
|
||||||
const userList = eventType.users ? eventType.users.map((user) => user.username).filter(notEmpty) : [];
|
const userList = eventType.users ? eventType.users.map((user) => user.username).filter(notEmpty) : [];
|
||||||
@@ -112,6 +123,7 @@ const AvailabilityPage = ({ profile, eventType, ...restProps }: Props) => {
|
|||||||
[timeZone]
|
[timeZone]
|
||||||
);
|
);
|
||||||
const paymentAppData = getPaymentAppData(eventType);
|
const paymentAppData = getPaymentAppData(eventType);
|
||||||
|
|
||||||
const rainbowAppData = getEventTypeAppData(eventType, "rainbow") || {};
|
const rainbowAppData = getEventTypeAppData(eventType, "rainbow") || {};
|
||||||
const rawSlug = profile.slug ? profile.slug.split("/") : [];
|
const rawSlug = profile.slug ? profile.slug.split("/") : [];
|
||||||
if (rawSlug.length > 1) rawSlug.pop(); //team events have team name as slug, but user events have [user]/[type] as slug.
|
if (rawSlug.length > 1) rawSlug.pop(); //team events have team name as slug, but user events have [user]/[type] as slug.
|
||||||
@@ -157,30 +169,27 @@ const AvailabilityPage = ({ profile, eventType, ...restProps }: Props) => {
|
|||||||
isBrandingHidden={restProps.isBrandingHidden}
|
isBrandingHidden={restProps.isBrandingHidden}
|
||||||
/>
|
/>
|
||||||
<BookingPageTagManager eventType={eventType} />
|
<BookingPageTagManager eventType={eventType} />
|
||||||
<CustomBranding lightVal={profile.brandColor} darkVal={profile.darkBrandColor} />
|
|
||||||
<div>
|
<div>
|
||||||
<main
|
<main
|
||||||
className={classNames(
|
className={classNames(
|
||||||
"flex flex-col md:mx-4",
|
"flex flex-col md:mx-4",
|
||||||
shouldAlignCentrally ? "items-center" : "items-start",
|
shouldAlignCentrally ? "items-center" : "items-start",
|
||||||
!isEmbed && classNames("mx-auto my-0 ease-in-out md:my-24")
|
!isEmbed && classNames("bg-subtle dark:bg-default mx-auto my-0 ease-in-out md:my-24")
|
||||||
)}>
|
)}>
|
||||||
<div>
|
<div>
|
||||||
<div
|
<div
|
||||||
style={availabilityDatePickerEmbedStyles}
|
style={availabilityDatePickerEmbedStyles}
|
||||||
className={classNames(
|
className={classNames(
|
||||||
isBackgroundTransparent
|
isBackgroundTransparent ? "" : "bg-default dark:bg-muted pb-4 md:pb-0",
|
||||||
? ""
|
"border-booker md:border-booker-width md:rounded-md",
|
||||||
: "dark:bg-darkgray-100 sm:dark:border-darkgray-300 bg-white pb-4 md:pb-0",
|
|
||||||
"border-bookinglightest md:rounded-md md:border",
|
|
||||||
isEmbed && "mx-auto"
|
isEmbed && "mx-auto"
|
||||||
)}>
|
)}>
|
||||||
<div className="md:flex">
|
<div className="md:flex">
|
||||||
{showEventTypeDetails && (
|
{showEventTypeDetails && (
|
||||||
<div
|
<div
|
||||||
className={classNames(
|
className={classNames(
|
||||||
"sm:dark:border-darkgray-200 flex flex-col border-gray-200 p-5 sm:border-r",
|
" border-subtle flex flex-col p-5 sm:border-r",
|
||||||
"min-w-full md:w-[280px] md:min-w-[280px]",
|
"min-w-full md:w-[230px] md:min-w-[230px]",
|
||||||
recurringEventCount && "xl:w-[380px] xl:min-w-[380px]"
|
recurringEventCount && "xl:w-[380px] xl:min-w-[380px]"
|
||||||
)}>
|
)}>
|
||||||
<BookingDescription profile={profile} eventType={eventType} rescheduleUid={rescheduleUid}>
|
<BookingDescription profile={profile} eventType={eventType} rescheduleUid={rescheduleUid}>
|
||||||
@@ -188,10 +197,10 @@ const AvailabilityPage = ({ profile, eventType, ...restProps }: Props) => {
|
|||||||
<div
|
<div
|
||||||
className={classNames(
|
className={classNames(
|
||||||
"flex flex-nowrap items-center text-sm font-medium",
|
"flex flex-nowrap items-center text-sm font-medium",
|
||||||
"dark:text-darkgray-600 text-gray-600",
|
" text-default",
|
||||||
"ltr:mr-[10px] rtl:ml-[10px]"
|
"ltr:mr-[10px] rtl:ml-[10px]"
|
||||||
)}>
|
)}>
|
||||||
<FiUser
|
<User
|
||||||
className={classNames(
|
className={classNames(
|
||||||
"min-h-4 min-w-4 ml-[2px] inline-block ltr:mr-[10px] rtl:ml-[10px]",
|
"min-h-4 min-w-4 ml-[2px] inline-block ltr:mr-[10px] rtl:ml-[10px]",
|
||||||
"mt-[2px]"
|
"mt-[2px]"
|
||||||
@@ -202,21 +211,23 @@ const AvailabilityPage = ({ profile, eventType, ...restProps }: Props) => {
|
|||||||
)}
|
)}
|
||||||
{!rescheduleUid && eventType.recurringEvent && (
|
{!rescheduleUid && eventType.recurringEvent && (
|
||||||
<div className="flex items-start text-sm font-medium">
|
<div className="flex items-start text-sm font-medium">
|
||||||
<FiRefreshCcw className="float-left mt-[7px] ml-[2px] inline-block h-4 w-4 ltr:mr-[10px] rtl:ml-[10px] " />
|
<RefreshCcw className="float-left mt-[7px] ml-[2px] inline-block h-4 w-4 ltr:mr-[10px] rtl:ml-[10px] " />
|
||||||
<div>
|
<div>
|
||||||
<p className="mb-1 -ml-2 inline px-2 py-1">
|
<p className="mb-1 -ml-2 inline px-2 py-1">
|
||||||
{getRecurringFreq({ t, recurringEvent: eventType.recurringEvent })}
|
{getRecurringFreq({ t, recurringEvent: eventType.recurringEvent })}
|
||||||
</p>
|
</p>
|
||||||
<input
|
|
||||||
type="number"
|
<NumberInput
|
||||||
|
defaultValue={eventType.recurringEvent.count}
|
||||||
min="1"
|
min="1"
|
||||||
max={eventType.recurringEvent.count}
|
max={eventType.recurringEvent.count}
|
||||||
className="w-15 dark:bg-darkgray-200 h-7 rounded-sm border-gray-300 bg-white text-sm font-medium [appearance:textfield] ltr:mr-2 rtl:ml-2 dark:border-gray-500"
|
isFullWidth={false}
|
||||||
defaultValue={eventType.recurringEvent.count}
|
className="me-2 inline w-16"
|
||||||
onChange={(event) => {
|
onChange={(event) => {
|
||||||
setRecurringEventCount(parseInt(event?.target.value));
|
setRecurringEventCount(parseInt(event?.target.value));
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<p className="inline">
|
<p className="inline">
|
||||||
{t("occurrence", {
|
{t("occurrence", {
|
||||||
count: recurringEventCount,
|
count: recurringEventCount,
|
||||||
@@ -227,14 +238,22 @@ const AvailabilityPage = ({ profile, eventType, ...restProps }: Props) => {
|
|||||||
)}
|
)}
|
||||||
{paymentAppData.price > 0 && (
|
{paymentAppData.price > 0 && (
|
||||||
<p className="-ml-2 px-2 text-sm font-medium">
|
<p className="-ml-2 px-2 text-sm font-medium">
|
||||||
<FiCreditCard className="ml-[2px] -mt-1 inline-block h-4 w-4 ltr:mr-[10px] rtl:ml-[10px]" />
|
<CreditCard className="ml-[2px] -mt-1 inline-block h-4 w-4 ltr:mr-[10px] rtl:ml-[10px]" />
|
||||||
<IntlProvider locale="en">
|
{paymentAppData.paymentOption === "HOLD" ? (
|
||||||
<FormattedNumber
|
<>
|
||||||
value={paymentAppData.price / 100.0}
|
{t("no_show_fee_amount", {
|
||||||
style="currency"
|
amount: paymentAppData.price / 100.0,
|
||||||
currency={paymentAppData.currency?.toUpperCase()}
|
formatParams: { amount: { currency: paymentAppData.currency } },
|
||||||
/>
|
})}
|
||||||
</IntlProvider>
|
</>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
{new Intl.NumberFormat(i18n.language, {
|
||||||
|
style: "currency",
|
||||||
|
currency: paymentAppData.currency,
|
||||||
|
}).format(paymentAppData.price / 100)}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
</p>
|
</p>
|
||||||
)}
|
)}
|
||||||
{timezoneDropdown}
|
{timezoneDropdown}
|
||||||
@@ -243,12 +262,12 @@ const AvailabilityPage = ({ profile, eventType, ...restProps }: Props) => {
|
|||||||
{/* Temporarily disabled - booking?.startTime && rescheduleUid && (
|
{/* Temporarily disabled - booking?.startTime && rescheduleUid && (
|
||||||
<div>
|
<div>
|
||||||
<p
|
<p
|
||||||
className="mt-4 mb-3 text-gray-600 dark:text-darkgray-600"
|
className="mt-4 mb-3 text-default"
|
||||||
data-testid="former_time_p_desktop">
|
data-testid="former_time_p_desktop">
|
||||||
{t("former_time")}
|
{t("former_time")}
|
||||||
</p>
|
</p>
|
||||||
<p className="text-gray-500 line-through dark:text-darkgray-600">
|
<p className="text-subtle line-through ">
|
||||||
<CalendarIcon className="ltr:mr-[10px] rtl:ml-[10px] -mt-1 inline-block h-4 w-4 text-gray-500" />
|
<CalendarIcon className="ltr:mr-[10px] rtl:ml-[10px] -mt-1 inline-block h-4 w-4 text-subtle" />
|
||||||
{typeof booking.startTime === "string" && parseDate(dayjs(booking.startTime), i18n)}
|
{typeof booking.startTime === "string" && parseDate(dayjs(booking.startTime), i18n)}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
@@ -282,7 +301,7 @@ const AvailabilityPage = ({ profile, eventType, ...restProps }: Props) => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{/* FIXME: We don't show branding in Embed yet because we need to place branding on top of the main content. Keeping it outside the main content would have visibility issues because outside main content background is transparent */}
|
{/* FIXME: We don't show branding in Embed yet because we need to place branding on top of the main content. Keeping it outside the main content would have visibility issues because outside main content background is transparent */}
|
||||||
{!restProps.isBrandingHidden && !isEmbed && <PoweredByCal />}
|
{!restProps.isBrandingHidden && !isEmbed && <PoweredBy />}
|
||||||
</div>
|
</div>
|
||||||
</main>
|
</main>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -11,7 +11,6 @@ import { z } from "zod";
|
|||||||
|
|
||||||
import BookingPageTagManager from "@calcom/app-store/BookingPageTagManager";
|
import BookingPageTagManager from "@calcom/app-store/BookingPageTagManager";
|
||||||
import type { EventLocationType } from "@calcom/app-store/locations";
|
import type { EventLocationType } from "@calcom/app-store/locations";
|
||||||
import { getEventLocationType, locationKeyToString } from "@calcom/app-store/locations";
|
|
||||||
import { createPaymentLink } from "@calcom/app-store/stripepayment/lib/client";
|
import { createPaymentLink } from "@calcom/app-store/stripepayment/lib/client";
|
||||||
import { getEventTypeAppData } from "@calcom/app-store/utils";
|
import { getEventTypeAppData } from "@calcom/app-store/utils";
|
||||||
import type { LocationObject } from "@calcom/core/location";
|
import type { LocationObject } from "@calcom/core/location";
|
||||||
@@ -22,6 +21,7 @@ import {
|
|||||||
useIsBackgroundTransparent,
|
useIsBackgroundTransparent,
|
||||||
useIsEmbed,
|
useIsEmbed,
|
||||||
} from "@calcom/embed-core/embed-iframe";
|
} from "@calcom/embed-core/embed-iframe";
|
||||||
|
import { createBooking, createRecurringBooking } from "@calcom/features/bookings/lib";
|
||||||
import {
|
import {
|
||||||
getBookingFieldsWithSystemFields,
|
getBookingFieldsWithSystemFields,
|
||||||
SystemField,
|
SystemField,
|
||||||
@@ -29,25 +29,26 @@ import {
|
|||||||
import getBookingResponsesSchema, {
|
import getBookingResponsesSchema, {
|
||||||
getBookingResponsesPartialSchema,
|
getBookingResponsesPartialSchema,
|
||||||
} from "@calcom/features/bookings/lib/getBookingResponsesSchema";
|
} from "@calcom/features/bookings/lib/getBookingResponsesSchema";
|
||||||
|
import getLocationOptionsForSelect from "@calcom/features/bookings/lib/getLocationOptionsForSelect";
|
||||||
import { FormBuilderField } from "@calcom/features/form-builder/FormBuilder";
|
import { FormBuilderField } from "@calcom/features/form-builder/FormBuilder";
|
||||||
import CustomBranding from "@calcom/lib/CustomBranding";
|
import { bookingSuccessRedirect } from "@calcom/lib/bookingSuccessRedirect";
|
||||||
import classNames from "@calcom/lib/classNames";
|
import classNames from "@calcom/lib/classNames";
|
||||||
import { APP_NAME } from "@calcom/lib/constants";
|
import { APP_NAME, MINUTES_TO_BOOK } from "@calcom/lib/constants";
|
||||||
|
import useGetBrandingColours from "@calcom/lib/getBrandColours";
|
||||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||||
import useTheme from "@calcom/lib/hooks/useTheme";
|
import useTheme from "@calcom/lib/hooks/useTheme";
|
||||||
import { useTypedQuery } from "@calcom/lib/hooks/useTypedQuery";
|
import { useTypedQuery } from "@calcom/lib/hooks/useTypedQuery";
|
||||||
import { HttpError } from "@calcom/lib/http-error";
|
import { HttpError } from "@calcom/lib/http-error";
|
||||||
|
import { parseDate, parseRecurringDates } from "@calcom/lib/parse-dates";
|
||||||
import { getEveryFreqFor } from "@calcom/lib/recurringStrings";
|
import { getEveryFreqFor } from "@calcom/lib/recurringStrings";
|
||||||
import { collectPageParameters, telemetryEventTypes, useTelemetry } from "@calcom/lib/telemetry";
|
import { telemetryEventTypes, useTelemetry } from "@calcom/lib/telemetry";
|
||||||
import { TimeFormat } from "@calcom/lib/timeFormat";
|
import { TimeFormat } from "@calcom/lib/timeFormat";
|
||||||
import { Button, Form, Tooltip } from "@calcom/ui";
|
import { trpc } from "@calcom/trpc";
|
||||||
import { FiAlertTriangle, FiCalendar, FiRefreshCw, FiUser } from "@calcom/ui/components/icon";
|
import { Button, Form, Tooltip, useCalcomTheme } from "@calcom/ui";
|
||||||
|
import { AlertTriangle, Calendar, RefreshCw, User } from "@calcom/ui/components/icon";
|
||||||
|
|
||||||
import { timeZone } from "@lib/clock";
|
import { timeZone } from "@lib/clock";
|
||||||
import useRouterQuery from "@lib/hooks/useRouterQuery";
|
import useRouterQuery from "@lib/hooks/useRouterQuery";
|
||||||
import createBooking from "@lib/mutations/bookings/create-booking";
|
|
||||||
import createRecurringBooking from "@lib/mutations/bookings/create-recurring-booking";
|
|
||||||
import { parseRecurringDates, parseDate } from "@lib/parseDate";
|
|
||||||
|
|
||||||
import type { Gate, GateState } from "@components/Gates";
|
import type { Gate, GateState } from "@components/Gates";
|
||||||
import Gates from "@components/Gates";
|
import Gates from "@components/Gates";
|
||||||
@@ -64,6 +65,14 @@ const BookingDescriptionPayment = dynamic(
|
|||||||
() => import("@components/booking/BookingDescriptionPayment")
|
() => import("@components/booking/BookingDescriptionPayment")
|
||||||
) as unknown as typeof import("@components/booking/BookingDescriptionPayment").default;
|
) as unknown as typeof import("@components/booking/BookingDescriptionPayment").default;
|
||||||
|
|
||||||
|
const useBrandColors = ({ brandColor, darkBrandColor }: { brandColor?: string; darkBrandColor?: string }) => {
|
||||||
|
const brandTheme = useGetBrandingColours({
|
||||||
|
lightVal: brandColor,
|
||||||
|
darkVal: darkBrandColor,
|
||||||
|
});
|
||||||
|
useCalcomTheme(brandTheme);
|
||||||
|
};
|
||||||
|
|
||||||
type BookingPageProps = BookPageProps | TeamBookingPageProps | HashLinkPageProps;
|
type BookingPageProps = BookPageProps | TeamBookingPageProps | HashLinkPageProps;
|
||||||
const BookingFields = ({
|
const BookingFields = ({
|
||||||
fields,
|
fields,
|
||||||
@@ -135,26 +144,14 @@ const BookingFields = ({
|
|||||||
}
|
}
|
||||||
const optionsInputs = field.optionsInputs;
|
const optionsInputs = field.optionsInputs;
|
||||||
|
|
||||||
const options = locations.map((location) => {
|
// TODO: Instead of `getLocationOptionsForSelect` options should be retrieved from dataStore[field.getOptionsAt]. It would make it agnostic of the `name` of the field.
|
||||||
const eventLocation = getEventLocationType(location.type);
|
const options = getLocationOptionsForSelect(locations, t);
|
||||||
const locationString = locationKeyToString(location);
|
options.forEach((option) => {
|
||||||
|
const optionInput = optionsInputs[option.value as keyof typeof optionsInputs];
|
||||||
if (typeof locationString !== "string" || !eventLocation) {
|
|
||||||
// It's possible that location app got uninstalled
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
const type = eventLocation.type;
|
|
||||||
const optionInput = optionsInputs[type as keyof typeof optionsInputs];
|
|
||||||
if (optionInput) {
|
if (optionInput) {
|
||||||
optionInput.placeholder = t(eventLocation?.attendeeInputPlaceholder || "");
|
optionInput.placeholder = option.inputPlaceholder;
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
|
||||||
label: t(locationString),
|
|
||||||
value: type,
|
|
||||||
};
|
|
||||||
});
|
});
|
||||||
|
|
||||||
field.options = options.filter(
|
field.options = options.filter(
|
||||||
(location): location is NonNullable<(typeof options)[number]> => !!location
|
(location): location is NonNullable<(typeof options)[number]> => !!location
|
||||||
);
|
);
|
||||||
@@ -205,6 +202,7 @@ const routerQuerySchema = z
|
|||||||
const BookingPage = ({
|
const BookingPage = ({
|
||||||
eventType,
|
eventType,
|
||||||
booking,
|
booking,
|
||||||
|
currentSlotBooking,
|
||||||
profile,
|
profile,
|
||||||
isDynamicGroupBooking,
|
isDynamicGroupBooking,
|
||||||
recurringEventCount,
|
recurringEventCount,
|
||||||
@@ -212,8 +210,11 @@ const BookingPage = ({
|
|||||||
hashedLink,
|
hashedLink,
|
||||||
...restProps
|
...restProps
|
||||||
}: BookingPageProps) => {
|
}: BookingPageProps) => {
|
||||||
|
const removeSelectedSlotMarkMutation = trpc.viewer.public.slots.removeSelectedSlotMark.useMutation();
|
||||||
|
const reserveSlotMutation = trpc.viewer.public.slots.reserveSlot.useMutation();
|
||||||
const { t, i18n } = useLocale();
|
const { t, i18n } = useLocale();
|
||||||
const { duration: queryDuration } = useRouterQuery("duration");
|
const { duration: queryDuration } = useRouterQuery("duration");
|
||||||
|
const { date: queryDate } = useRouterQuery("date");
|
||||||
const isEmbed = useIsEmbed(restProps.isEmbed);
|
const isEmbed = useIsEmbed(restProps.isEmbed);
|
||||||
const embedUiConfig = useEmbedUiConfig();
|
const embedUiConfig = useEmbedUiConfig();
|
||||||
const shouldAlignCentrallyInEmbed = useEmbedNonStylesConfig("align") !== "left";
|
const shouldAlignCentrallyInEmbed = useEmbedNonStylesConfig("align") !== "left";
|
||||||
@@ -229,6 +230,17 @@ const BookingPage = ({
|
|||||||
}),
|
}),
|
||||||
{}
|
{}
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const reserveSlot = () => {
|
||||||
|
if (queryDuration) {
|
||||||
|
reserveSlotMutation.mutate({
|
||||||
|
eventTypeId: eventType.id,
|
||||||
|
slotUtcStartDate: dayjs(queryDate).utc().format(),
|
||||||
|
slotUtcEndDate: dayjs(queryDate).utc().add(parseInt(queryDuration), "minutes").format(),
|
||||||
|
bookingUid: currentSlotBooking?.uid,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
// Define duration now that we support multiple duration eventTypes
|
// Define duration now that we support multiple duration eventTypes
|
||||||
let duration = eventType.length;
|
let duration = eventType.length;
|
||||||
if (
|
if (
|
||||||
@@ -241,13 +253,19 @@ const BookingPage = ({
|
|||||||
}
|
}
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (top !== window) {
|
/* if (top !== window) {
|
||||||
//page_view will be collected automatically by _middleware.ts
|
//page_view will be collected automatically by _middleware.ts
|
||||||
telemetry.event(
|
telemetry.event(
|
||||||
telemetryEventTypes.embedView,
|
telemetryEventTypes.embedView,
|
||||||
collectPageParameters("/book", { isTeamBooking: document.URL.includes("team/") })
|
collectPageParameters("/book", { isTeamBooking: document.URL.includes("team/") })
|
||||||
);
|
);
|
||||||
}
|
} */
|
||||||
|
reserveSlot();
|
||||||
|
const interval = setInterval(reserveSlot, parseInt(MINUTES_TO_BOOK) * 60 * 1000 - 2000);
|
||||||
|
return () => {
|
||||||
|
clearInterval(interval);
|
||||||
|
removeSelectedSlotMarkMutation.mutate();
|
||||||
|
};
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
@@ -267,15 +285,19 @@ const BookingPage = ({
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return router.push({
|
const query = {
|
||||||
pathname: `/booking/${uid}`,
|
isSuccessBookingPage: true,
|
||||||
query: {
|
email: bookingForm.getValues("responses.email"),
|
||||||
isSuccessBookingPage: true,
|
eventTypeSlug: eventType.slug,
|
||||||
email: bookingForm.getValues("responses.email"),
|
seatReferenceUid: "seatReferenceUid" in responseData ? responseData.seatReferenceUid : null,
|
||||||
eventTypeSlug: eventType.slug,
|
...(rescheduleUid && booking?.startTime && { formerTime: booking.startTime.toString() }),
|
||||||
seatReferenceUid: "seatReferenceUid" in responseData ? responseData.seatReferenceUid : null,
|
};
|
||||||
...(rescheduleUid && booking?.startTime && { formerTime: booking.startTime.toString() }),
|
|
||||||
},
|
return bookingSuccessRedirect({
|
||||||
|
router,
|
||||||
|
successRedirectUrl: eventType.successRedirectUrl,
|
||||||
|
query,
|
||||||
|
bookingUid: uid,
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
@@ -283,16 +305,18 @@ const BookingPage = ({
|
|||||||
const recurringMutation = useMutation(createRecurringBooking, {
|
const recurringMutation = useMutation(createRecurringBooking, {
|
||||||
onSuccess: async (responseData = []) => {
|
onSuccess: async (responseData = []) => {
|
||||||
const { uid } = responseData[0] || {};
|
const { uid } = responseData[0] || {};
|
||||||
|
const query = {
|
||||||
return router.push({
|
isSuccessBookingPage: true,
|
||||||
pathname: `/booking/${uid}`,
|
allRemainingBookings: true,
|
||||||
query: {
|
email: bookingForm.getValues("responses.email"),
|
||||||
isSuccessBookingPage: true,
|
eventTypeSlug: eventType.slug,
|
||||||
allRemainingBookings: true,
|
formerTime: booking?.startTime.toString(),
|
||||||
email: bookingForm.getValues("responses.email"),
|
};
|
||||||
eventTypeSlug: eventType.slug,
|
return bookingSuccessRedirect({
|
||||||
formerTime: booking?.startTime.toString(),
|
router,
|
||||||
},
|
successRedirectUrl: eventType.successRedirectUrl,
|
||||||
|
query,
|
||||||
|
bookingUid: uid,
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
@@ -302,6 +326,10 @@ const BookingPage = ({
|
|||||||
} = useTypedQuery(routerQuerySchema);
|
} = useTypedQuery(routerQuerySchema);
|
||||||
|
|
||||||
useTheme(profile.theme);
|
useTheme(profile.theme);
|
||||||
|
useBrandColors({
|
||||||
|
brandColor: profile.brandColor,
|
||||||
|
darkBrandColor: profile.darkBrandColor,
|
||||||
|
});
|
||||||
|
|
||||||
const querySchema = getBookingResponsesPartialSchema({
|
const querySchema = getBookingResponsesPartialSchema({
|
||||||
eventType: {
|
eventType: {
|
||||||
@@ -406,7 +434,6 @@ const BookingPage = ({
|
|||||||
// Calculate the booking date(s)
|
// Calculate the booking date(s)
|
||||||
let recurringStrings: string[] = [],
|
let recurringStrings: string[] = [],
|
||||||
recurringDates: Date[] = [];
|
recurringDates: Date[] = [];
|
||||||
|
|
||||||
if (eventType.recurringEvent?.freq && recurringEventCount !== null) {
|
if (eventType.recurringEvent?.freq && recurringEventCount !== null) {
|
||||||
[recurringStrings, recurringDates] = parseRecurringDates(
|
[recurringStrings, recurringDates] = parseRecurringDates(
|
||||||
{
|
{
|
||||||
@@ -416,7 +443,7 @@ const BookingPage = ({
|
|||||||
recurringCount: parseInt(recurringEventCount.toString()),
|
recurringCount: parseInt(recurringEventCount.toString()),
|
||||||
selectedTimeFormat: timeFormat,
|
selectedTimeFormat: timeFormat,
|
||||||
},
|
},
|
||||||
i18n
|
i18n.language
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -511,7 +538,6 @@ const BookingPage = ({
|
|||||||
<link rel="icon" href="/favico.ico" />
|
<link rel="icon" href="/favico.ico" />
|
||||||
</Head>
|
</Head>
|
||||||
<BookingPageTagManager eventType={eventType} />
|
<BookingPageTagManager eventType={eventType} />
|
||||||
<CustomBranding lightVal={profile.brandColor} darkVal={profile.darkBrandColor} />
|
|
||||||
<main
|
<main
|
||||||
className={classNames(
|
className={classNames(
|
||||||
shouldAlignCentrally ? "mx-auto" : "",
|
shouldAlignCentrally ? "mx-auto" : "",
|
||||||
@@ -521,17 +547,17 @@ const BookingPage = ({
|
|||||||
<div
|
<div
|
||||||
className={classNames(
|
className={classNames(
|
||||||
"main",
|
"main",
|
||||||
isBackgroundTransparent ? "" : "dark:bg-darkgray-100 bg-white dark:border",
|
isBackgroundTransparent ? "" : "bg-default dark:bg-muted",
|
||||||
"dark:border-darkgray-300 rounded-md sm:border"
|
"border-booker sm:border-booker-width rounded-md"
|
||||||
)}>
|
)}>
|
||||||
<div className="sm:flex">
|
<div className="sm:flex">
|
||||||
{showEventTypeDetails && (
|
{showEventTypeDetails && (
|
||||||
<div className="sm:dark:border-darkgray-300 dark:text-darkgray-600 flex flex-col px-6 pt-6 pb-0 text-gray-600 sm:w-1/2 sm:border-r sm:pb-6">
|
<div className="sm:border-subtle text-default flex flex-col px-6 pt-6 pb-0 sm:w-1/2 sm:border-r sm:pb-6">
|
||||||
<BookingDescription isBookingPage profile={profile} eventType={eventType}>
|
<BookingDescription isBookingPage profile={profile} eventType={eventType}>
|
||||||
<BookingDescriptionPayment eventType={eventType} />
|
<BookingDescriptionPayment eventType={eventType} t={t} i18n={i18n} />
|
||||||
{!rescheduleUid && eventType.recurringEvent?.freq && recurringEventCount && (
|
{!rescheduleUid && eventType.recurringEvent?.freq && recurringEventCount && (
|
||||||
<div className="items-start text-sm font-medium text-gray-600 dark:text-white">
|
<div className="text-default items-start text-sm font-medium">
|
||||||
<FiRefreshCw className="ml-[2px] inline-block h-4 w-4 ltr:mr-[10px] rtl:ml-[10px]" />
|
<RefreshCw className="ml-[2px] inline-block h-4 w-4 ltr:mr-[10px] rtl:ml-[10px]" />
|
||||||
<p className="-ml-2 inline-block items-center px-2">
|
<p className="-ml-2 inline-block items-center px-2">
|
||||||
{getEveryFreqFor({
|
{getEveryFreqFor({
|
||||||
t,
|
t,
|
||||||
@@ -542,11 +568,11 @@ const BookingPage = ({
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
<div className="text-bookinghighlight flex items-start text-sm">
|
<div className="text-bookinghighlight flex items-start text-sm">
|
||||||
<FiCalendar className="ml-[2px] mt-[2px] inline-block h-4 w-4 ltr:mr-[10px] rtl:ml-[10px]" />
|
<Calendar className="ml-[2px] mt-[2px] inline-block h-4 w-4 ltr:mr-[10px] rtl:ml-[10px]" />
|
||||||
<div className="text-sm font-medium">
|
<div className="text-sm font-medium">
|
||||||
{isClientTimezoneAvailable &&
|
{isClientTimezoneAvailable &&
|
||||||
(rescheduleUid || !eventType.recurringEvent?.freq) &&
|
(rescheduleUid || !eventType.recurringEvent?.freq) &&
|
||||||
`${parseDate(date, i18n, timeFormat)}`}
|
`${parseDate(date, i18n.language, { selectedTimeFormat: timeFormat })}`}
|
||||||
{isClientTimezoneAvailable &&
|
{isClientTimezoneAvailable &&
|
||||||
!rescheduleUid &&
|
!rescheduleUid &&
|
||||||
eventType.recurringEvent?.freq &&
|
eventType.recurringEvent?.freq &&
|
||||||
@@ -559,7 +585,7 @@ const BookingPage = ({
|
|||||||
content={recurringStrings.slice(5).map((timeFormatted, key) => (
|
content={recurringStrings.slice(5).map((timeFormatted, key) => (
|
||||||
<p key={key}>{timeFormatted}</p>
|
<p key={key}>{timeFormatted}</p>
|
||||||
))}>
|
))}>
|
||||||
<p className="dark:text-darkgray-600 text-sm">
|
<p className=" text-sm">
|
||||||
+ {t("plus_more", { count: recurringStrings.length - 5 })}
|
+ {t("plus_more", { count: recurringStrings.length - 5 })}
|
||||||
</p>
|
</p>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
@@ -573,34 +599,40 @@ const BookingPage = ({
|
|||||||
{t("former_time")}
|
{t("former_time")}
|
||||||
</p>
|
</p>
|
||||||
<p className="line-through ">
|
<p className="line-through ">
|
||||||
<FiCalendar className="ml-[2px] -mt-1 inline-block h-4 w-4 ltr:mr-[10px] rtl:ml-[10px]" />
|
<Calendar className="ml-[2px] -mt-1 inline-block h-4 w-4 ltr:mr-[10px] rtl:ml-[10px]" />
|
||||||
{isClientTimezoneAvailable &&
|
{isClientTimezoneAvailable &&
|
||||||
typeof booking.startTime === "string" &&
|
typeof booking.startTime === "string" &&
|
||||||
parseDate(dayjs(booking.startTime), i18n, timeFormat)}
|
parseDate(dayjs(booking.startTime), i18n.language, {
|
||||||
|
selectedTimeFormat: timeFormat,
|
||||||
|
})}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{!!eventType.seatsPerTimeSlot && (
|
{!!eventType.seatsPerTimeSlot && (
|
||||||
<div className="text-bookinghighlight flex items-start text-sm">
|
<div className="text-bookinghighlight flex items-start text-sm">
|
||||||
<FiUser
|
<User
|
||||||
className={`ml-[2px] mt-[2px] inline-block h-4 w-4 ltr:mr-[10px] rtl:ml-[10px] ${
|
className={`ml-[2px] mt-[2px] inline-block h-4 w-4 ltr:mr-[10px] rtl:ml-[10px] ${
|
||||||
booking && booking.attendees.length / eventType.seatsPerTimeSlot >= 0.5
|
currentSlotBooking &&
|
||||||
|
currentSlotBooking.attendees.length / eventType.seatsPerTimeSlot >= 0.5
|
||||||
? "text-rose-600"
|
? "text-rose-600"
|
||||||
: booking && booking.attendees.length / eventType.seatsPerTimeSlot >= 0.33
|
: currentSlotBooking &&
|
||||||
|
currentSlotBooking.attendees.length / eventType.seatsPerTimeSlot >= 0.33
|
||||||
? "text-yellow-500"
|
? "text-yellow-500"
|
||||||
: "text-bookinghighlight"
|
: "text-bookinghighlight"
|
||||||
}`}
|
}`}
|
||||||
/>
|
/>
|
||||||
<p
|
<p
|
||||||
className={`${
|
className={`${
|
||||||
booking && booking.attendees.length / eventType.seatsPerTimeSlot >= 0.5
|
currentSlotBooking &&
|
||||||
|
currentSlotBooking.attendees.length / eventType.seatsPerTimeSlot >= 0.5
|
||||||
? "text-rose-600"
|
? "text-rose-600"
|
||||||
: booking && booking.attendees.length / eventType.seatsPerTimeSlot >= 0.33
|
: currentSlotBooking &&
|
||||||
|
currentSlotBooking.attendees.length / eventType.seatsPerTimeSlot >= 0.33
|
||||||
? "text-yellow-500"
|
? "text-yellow-500"
|
||||||
: "text-bookinghighlight"
|
: "text-bookinghighlight"
|
||||||
} mb-2 font-medium`}>
|
} mb-2 font-medium`}>
|
||||||
{booking
|
{currentSlotBooking
|
||||||
? eventType.seatsPerTimeSlot - booking.attendees.length
|
? eventType.seatsPerTimeSlot - currentSlotBooking.attendees.length
|
||||||
: eventType.seatsPerTimeSlot}{" "}
|
: eventType.seatsPerTimeSlot}{" "}
|
||||||
/ {eventType.seatsPerTimeSlot} {t("seats_available")}
|
/ {eventType.seatsPerTimeSlot} {t("seats_available")}
|
||||||
</p>
|
</p>
|
||||||
@@ -638,9 +670,9 @@ const BookingPage = ({
|
|||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</Form>
|
</Form>
|
||||||
{(mutation.isError || recurringMutation.isError) && (
|
{mutation.isError || recurringMutation.isError ? (
|
||||||
<ErrorMessage error={mutation.error || recurringMutation.error} />
|
<ErrorMessage error={mutation.error || recurringMutation.error} />
|
||||||
)}
|
) : null}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -661,9 +693,9 @@ function ErrorMessage({ error }: { error: unknown }) {
|
|||||||
<div data-testid="booking-fail" className="mt-2 border-l-4 border-blue-400 bg-blue-50 p-4">
|
<div data-testid="booking-fail" className="mt-2 border-l-4 border-blue-400 bg-blue-50 p-4">
|
||||||
<div className="flex">
|
<div className="flex">
|
||||||
<div className="flex-shrink-0">
|
<div className="flex-shrink-0">
|
||||||
<FiAlertTriangle className="h-5 w-5 text-blue-400" aria-hidden="true" />
|
<AlertTriangle className="h-5 w-5 text-blue-400" aria-hidden="true" />
|
||||||
</div>
|
</div>
|
||||||
<div className="ltr:ml-3 rtl:mr-3">
|
<div className="ms-3">
|
||||||
<p className="text-sm text-blue-700">
|
<p className="text-sm text-blue-700">
|
||||||
{rescheduleUid ? t("reschedule_fail") : t("booking_fail")}{" "}
|
{rescheduleUid ? t("reschedule_fail") : t("booking_fail")}{" "}
|
||||||
{error instanceof HttpError || error instanceof Error ? (
|
{error instanceof HttpError || error instanceof Error ? (
|
||||||
|
|||||||
@@ -0,0 +1,82 @@
|
|||||||
|
import { useState } from "react";
|
||||||
|
import type { Dispatch, SetStateAction } from "react";
|
||||||
|
|
||||||
|
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||||
|
import { trpc } from "@calcom/trpc/react";
|
||||||
|
import {
|
||||||
|
Button,
|
||||||
|
Dialog,
|
||||||
|
DialogClose,
|
||||||
|
DialogContent,
|
||||||
|
DialogFooter,
|
||||||
|
DialogHeader,
|
||||||
|
showToast,
|
||||||
|
} from "@calcom/ui";
|
||||||
|
import { CreditCard, AlertTriangle } from "@calcom/ui/components/icon";
|
||||||
|
|
||||||
|
interface IRescheduleDialog {
|
||||||
|
isOpenDialog: boolean;
|
||||||
|
setIsOpenDialog: Dispatch<SetStateAction<boolean>>;
|
||||||
|
bookingId: number;
|
||||||
|
paymentAmount: number;
|
||||||
|
paymentCurrency: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const ChargeCardDialog = (props: IRescheduleDialog) => {
|
||||||
|
const { t } = useLocale();
|
||||||
|
const utils = trpc.useContext();
|
||||||
|
const { isOpenDialog, setIsOpenDialog, bookingId } = props;
|
||||||
|
const [chargeError, setChargeError] = useState(false);
|
||||||
|
const chargeCardMutation = trpc.viewer.payments.chargeCard.useMutation({
|
||||||
|
onSuccess: () => {
|
||||||
|
utils.viewer.bookings.invalidate();
|
||||||
|
setIsOpenDialog(false);
|
||||||
|
showToast("Charge successful", "success");
|
||||||
|
},
|
||||||
|
onError: () => {
|
||||||
|
setChargeError(true);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const currencyStringParams = {
|
||||||
|
amount: props.paymentAmount / 100.0,
|
||||||
|
formatParams: { amount: { currency: props.paymentCurrency } },
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Dialog open={isOpenDialog} onOpenChange={setIsOpenDialog}>
|
||||||
|
<DialogContent>
|
||||||
|
<div className="flex flex-row space-x-3">
|
||||||
|
<div className="flex h-10 w-10 flex-shrink-0 justify-center rounded-full bg-[#FAFAFA]">
|
||||||
|
<CreditCard className="m-auto h-6 w-6" />
|
||||||
|
</div>
|
||||||
|
<div className="pt-1">
|
||||||
|
<DialogHeader title={t("charge_card")} />
|
||||||
|
<p>{t("charge_card_dialog_body", currencyStringParams)}</p>
|
||||||
|
|
||||||
|
{chargeError && (
|
||||||
|
<div className="mt-4 flex text-red-500">
|
||||||
|
<AlertTriangle className="mr-2 h-5 w-5 " aria-hidden="true" />
|
||||||
|
<p className="text-sm">{t("error_charging_card")}</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<DialogFooter>
|
||||||
|
<DialogClose />
|
||||||
|
<Button
|
||||||
|
data-testid="send_request"
|
||||||
|
disabled={chargeCardMutation.isLoading || chargeError}
|
||||||
|
onClick={() =>
|
||||||
|
chargeCardMutation.mutate({
|
||||||
|
bookingId,
|
||||||
|
})
|
||||||
|
}>
|
||||||
|
{t("charge_attendee", currencyStringParams)}
|
||||||
|
</Button>
|
||||||
|
</DialogFooter>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -19,7 +19,7 @@ import type { RouterOutputs } from "@calcom/trpc/react";
|
|||||||
import { trpc } from "@calcom/trpc/react";
|
import { trpc } from "@calcom/trpc/react";
|
||||||
import { Input } from "@calcom/ui";
|
import { Input } from "@calcom/ui";
|
||||||
import { Button, Dialog, DialogContent, DialogFooter, Form, PhoneInput } from "@calcom/ui";
|
import { Button, Dialog, DialogContent, DialogFooter, Form, PhoneInput } from "@calcom/ui";
|
||||||
import { FiMapPin } from "@calcom/ui/components/icon";
|
import { MapPin } from "@calcom/ui/components/icon";
|
||||||
|
|
||||||
import { QueryCell } from "@lib/QueryCell";
|
import { QueryCell } from "@lib/QueryCell";
|
||||||
|
|
||||||
@@ -33,6 +33,7 @@ interface ISetLocationDialog {
|
|||||||
saveLocation: (newLocationType: EventLocationType["type"], details: { [key: string]: string }) => void;
|
saveLocation: (newLocationType: EventLocationType["type"], details: { [key: string]: string }) => void;
|
||||||
selection?: LocationOption;
|
selection?: LocationOption;
|
||||||
booking?: BookingItem;
|
booking?: BookingItem;
|
||||||
|
isTeamEvent?: boolean;
|
||||||
defaultValues?: LocationObject[];
|
defaultValues?: LocationObject[];
|
||||||
setShowLocationModal: React.Dispatch<React.SetStateAction<boolean>>;
|
setShowLocationModal: React.Dispatch<React.SetStateAction<boolean>>;
|
||||||
isOpenDialog: boolean;
|
isOpenDialog: boolean;
|
||||||
@@ -74,6 +75,7 @@ export const EditLocationDialog = (props: ISetLocationDialog) => {
|
|||||||
saveLocation,
|
saveLocation,
|
||||||
selection,
|
selection,
|
||||||
booking,
|
booking,
|
||||||
|
isTeamEvent,
|
||||||
setShowLocationModal,
|
setShowLocationModal,
|
||||||
isOpenDialog,
|
isOpenDialog,
|
||||||
defaultValues,
|
defaultValues,
|
||||||
@@ -162,7 +164,7 @@ export const EditLocationDialog = (props: ISetLocationDialog) => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<label htmlFor="locationInput" className="block text-sm font-medium text-gray-700">
|
<label htmlFor="locationInput" className="text-default block text-sm font-medium">
|
||||||
{t(eventLocationType.messageForOrganizer || "")}
|
{t(eventLocationType.messageForOrganizer || "")}
|
||||||
</label>
|
</label>
|
||||||
<div className="mt-1">
|
<div className="mt-1">
|
||||||
@@ -180,7 +182,7 @@ export const EditLocationDialog = (props: ISetLocationDialog) => {
|
|||||||
<ErrorMessage
|
<ErrorMessage
|
||||||
errors={locationFormMethods.formState.errors}
|
errors={locationFormMethods.formState.errors}
|
||||||
name={eventLocationType.variable}
|
name={eventLocationType.variable}
|
||||||
className="mt-1 text-sm text-red-500"
|
className="text-error mt-1 text-sm"
|
||||||
as="p"
|
as="p"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@@ -206,7 +208,7 @@ export const EditLocationDialog = (props: ISetLocationDialog) => {
|
|||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
return <p className="text-sm">{getMessageForOrganizer(selectedLocation, t)}</p>;
|
return <p className="text-default text-sm">{getMessageForOrganizer(selectedLocation, t)}</p>;
|
||||||
}
|
}
|
||||||
})();
|
})();
|
||||||
|
|
||||||
@@ -214,16 +216,16 @@ export const EditLocationDialog = (props: ISetLocationDialog) => {
|
|||||||
<Dialog open={isOpenDialog} onOpenChange={(open) => setShowLocationModal(open)}>
|
<Dialog open={isOpenDialog} onOpenChange={(open) => setShowLocationModal(open)}>
|
||||||
<DialogContent>
|
<DialogContent>
|
||||||
<div className="flex flex-row space-x-3">
|
<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">
|
<div className="bg-subtle 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">
|
||||||
<FiMapPin className="text-primary-600 h-6 w-6" />
|
<MapPin className="text-emphasis h-6 w-6" />
|
||||||
</div>
|
</div>
|
||||||
<div className="w-full">
|
<div className="w-full">
|
||||||
<div className="mt-3 text-center sm:mt-0 sm:text-left">
|
<div className="mt-3 text-center sm:mt-0 sm:text-left">
|
||||||
<h3 className="text-lg font-medium leading-6 text-gray-900" id="modal-title">
|
<h3 className="text-emphasis text-lg font-medium leading-6" id="modal-title">
|
||||||
{t("edit_location")}
|
{t("edit_location")}
|
||||||
</h3>
|
</h3>
|
||||||
{!booking && (
|
{!booking && (
|
||||||
<p className="text-sm text-gray-400">
|
<p className="text-default text-sm">
|
||||||
<Trans i18nKey="cant_find_the_right_video_app_visit_our_app_store">
|
<Trans i18nKey="cant_find_the_right_video_app_visit_our_app_store">
|
||||||
Can't find the right video app? Visit our
|
Can't find the right video app? Visit our
|
||||||
<Link className="cursor-pointer text-blue-500 underline" href="/apps/categories/video">
|
<Link className="cursor-pointer text-blue-500 underline" href="/apps/categories/video">
|
||||||
@@ -238,8 +240,8 @@ export const EditLocationDialog = (props: ISetLocationDialog) => {
|
|||||||
|
|
||||||
{booking && (
|
{booking && (
|
||||||
<>
|
<>
|
||||||
<p className="mt-6 mb-2 ml-1 text-sm font-bold text-black">{t("current_location")}:</p>
|
<p className="text-emphasis mt-6 mb-2 ml-1 text-sm font-bold">{t("current_location")}:</p>
|
||||||
<p className="mb-2 ml-1 text-sm text-black">
|
<p className="text-emphasis mb-2 ml-1 text-sm">
|
||||||
{getHumanReadableLocationValue(booking.location, t)}
|
{getHumanReadableLocationValue(booking.location, t)}
|
||||||
</p>
|
</p>
|
||||||
</>
|
</>
|
||||||
@@ -290,7 +292,9 @@ export const EditLocationDialog = (props: ISetLocationDialog) => {
|
|||||||
query={locationsQuery}
|
query={locationsQuery}
|
||||||
success={({ data }) => {
|
success={({ data }) => {
|
||||||
if (!data.length) return null;
|
if (!data.length) return null;
|
||||||
const locationOptions = [...data];
|
const locationOptions = [...data].filter((option) => {
|
||||||
|
return !isTeamEvent ? option.label !== "Conferencing" : true;
|
||||||
|
});
|
||||||
if (booking) {
|
if (booking) {
|
||||||
locationOptions.map((location) =>
|
locationOptions.map((location) =>
|
||||||
location.options.filter((l) => !["phone", "attendeeInPerson"].includes(l.value))
|
location.options.filter((l) => !["phone", "attendeeInPerson"].includes(l.value))
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ import {
|
|||||||
showToast,
|
showToast,
|
||||||
TextArea,
|
TextArea,
|
||||||
} from "@calcom/ui";
|
} from "@calcom/ui";
|
||||||
import { FiClock } from "@calcom/ui/components/icon";
|
import { Clock } from "@calcom/ui/components/icon";
|
||||||
|
|
||||||
interface IRescheduleDialog {
|
interface IRescheduleDialog {
|
||||||
isOpenDialog: boolean;
|
isOpenDialog: boolean;
|
||||||
@@ -44,14 +44,14 @@ export const RescheduleDialog = (props: IRescheduleDialog) => {
|
|||||||
<DialogContent>
|
<DialogContent>
|
||||||
<div className="flex flex-row space-x-3">
|
<div className="flex flex-row space-x-3">
|
||||||
<div className="flex h-10 w-10 flex-shrink-0 justify-center rounded-full bg-[#FAFAFA]">
|
<div className="flex h-10 w-10 flex-shrink-0 justify-center rounded-full bg-[#FAFAFA]">
|
||||||
<FiClock className="m-auto h-6 w-6" />
|
<Clock className="m-auto h-6 w-6" />
|
||||||
</div>
|
</div>
|
||||||
<div className="pt-1">
|
<div className="pt-1">
|
||||||
<DialogHeader title={t("send_reschedule_request")} />
|
<DialogHeader title={t("send_reschedule_request")} />
|
||||||
<p className="text-sm text-gray-500">{t("reschedule_modal_description")}</p>
|
<p className="text-subtle text-sm">{t("reschedule_modal_description")}</p>
|
||||||
<p className="mt-6 mb-2 text-sm font-bold text-black">
|
<p className="text-emphasis mt-6 mb-2 text-sm font-bold">
|
||||||
{t("reason_for_reschedule_request")}
|
{t("reason_for_reschedule_request")}
|
||||||
<span className="font-normal text-gray-500"> (Optional)</span>
|
<span className="text-subtle font-normal"> (Optional)</span>
|
||||||
</p>
|
</p>
|
||||||
<TextArea
|
<TextArea
|
||||||
data-testid="reschedule_reason"
|
data-testid="reschedule_reason"
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import React from "react";
|
import React from "react";
|
||||||
|
|
||||||
import { HttpError } from "@lib/core/http/error";
|
import { HttpError } from "@calcom/lib/http-error";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
statusCode?: number | null;
|
statusCode?: number | null;
|
||||||
@@ -29,15 +29,15 @@ const ErrorDebugPanel: React.FC<{ error: Props["error"]; children?: never }> = (
|
|||||||
];
|
];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="overflow-hidden bg-white shadow sm:rounded-lg">
|
<div className="bg-default overflow-hidden shadow sm:rounded-lg">
|
||||||
<div className="border-t border-gray-200 px-4 py-5 sm:p-0">
|
<div className="border-subtle border-t px-4 py-5 sm:p-0">
|
||||||
<dl className="sm:divide-y sm:divide-gray-200">
|
<dl className="sm:divide-subtle sm:divide-y">
|
||||||
{debugMap.map(([key, value]) => {
|
{debugMap.map(([key, value]) => {
|
||||||
if (value !== undefined) {
|
if (value !== undefined) {
|
||||||
return (
|
return (
|
||||||
<div key={key} className="py-4 sm:grid sm:grid-cols-3 sm:gap-4 sm:py-5 sm:px-6">
|
<div key={key} className="py-4 sm:grid sm:grid-cols-3 sm:gap-4 sm:py-5 sm:px-6">
|
||||||
<dt className="text-sm font-bold text-black">{key}</dt>
|
<dt className="text-emphasis text-sm font-bold">{key}</dt>
|
||||||
<dd className="mt-1 text-sm text-black sm:col-span-2 sm:mt-0">{value}</dd>
|
<dd className="text-emphasis mt-1 text-sm sm:col-span-2 sm:mt-0">{value}</dd>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -53,11 +53,11 @@ export const ErrorPage: React.FC<Props> = (props) => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div className="min-h-screen bg-white px-4">
|
<div className="bg-default min-h-screen px-4">
|
||||||
<main className="mx-auto max-w-xl pb-6 pt-16 sm:pt-24">
|
<main className="mx-auto max-w-xl pb-6 pt-16 sm:pt-24">
|
||||||
<div className="text-center">
|
<div className="text-center">
|
||||||
<p className="text-sm font-semibold uppercase tracking-wide text-black">{statusCode}</p>
|
<p className="text-emphasis text-sm font-semibold uppercase tracking-wide">{statusCode}</p>
|
||||||
<h1 className="mt-2 text-4xl font-extrabold tracking-tight text-gray-900 sm:text-5xl">
|
<h1 className="text-emphasis mt-2 text-4xl font-extrabold tracking-tight sm:text-5xl">
|
||||||
{message}
|
{message}
|
||||||
</h1>
|
</h1>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,230 +0,0 @@
|
|||||||
import type { FormValues } from "pages/event-types/[type]";
|
|
||||||
import { Controller, useFormContext } from "react-hook-form";
|
|
||||||
import type { OptionProps, SingleValueProps } from "react-select";
|
|
||||||
import { components } from "react-select";
|
|
||||||
|
|
||||||
import dayjs from "@calcom/dayjs";
|
|
||||||
import classNames from "@calcom/lib/classNames";
|
|
||||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
|
||||||
import { weekdayNames } from "@calcom/lib/weekday";
|
|
||||||
import { trpc } from "@calcom/trpc/react";
|
|
||||||
import useMeQuery from "@calcom/trpc/react/hooks/useMeQuery";
|
|
||||||
import { Badge, Button, Select, SettingsToggle, SkeletonText } from "@calcom/ui";
|
|
||||||
import { FiExternalLink, FiGlobe } from "@calcom/ui/components/icon";
|
|
||||||
|
|
||||||
import { SelectSkeletonLoader } from "@components/availability/SkeletonLoader";
|
|
||||||
|
|
||||||
type AvailabilityOption = {
|
|
||||||
label: string;
|
|
||||||
value: number;
|
|
||||||
isDefault: boolean;
|
|
||||||
};
|
|
||||||
|
|
||||||
const Option = ({ ...props }: OptionProps<AvailabilityOption>) => {
|
|
||||||
const { label, isDefault } = props.data;
|
|
||||||
const { t } = useLocale();
|
|
||||||
return (
|
|
||||||
<components.Option {...props}>
|
|
||||||
<span>{label}</span>
|
|
||||||
{isDefault && (
|
|
||||||
<Badge variant="blue" className="ml-2">
|
|
||||||
{t("default")}
|
|
||||||
</Badge>
|
|
||||||
)}
|
|
||||||
</components.Option>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
const SingleValue = ({ ...props }: SingleValueProps<AvailabilityOption>) => {
|
|
||||||
const { label, isDefault } = props.data;
|
|
||||||
const { t } = useLocale();
|
|
||||||
return (
|
|
||||||
<components.SingleValue {...props}>
|
|
||||||
<span>{label}</span>
|
|
||||||
{isDefault && (
|
|
||||||
<Badge variant="blue" className="ml-2">
|
|
||||||
{t("default")}
|
|
||||||
</Badge>
|
|
||||||
)}
|
|
||||||
</components.SingleValue>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
const AvailabilitySelect = ({
|
|
||||||
className = "",
|
|
||||||
...props
|
|
||||||
}: {
|
|
||||||
className?: string;
|
|
||||||
name: string;
|
|
||||||
value: number;
|
|
||||||
onBlur: () => void;
|
|
||||||
onChange: (value: AvailabilityOption | null) => void;
|
|
||||||
}) => {
|
|
||||||
const { data, isLoading } = trpc.viewer.availability.list.useQuery();
|
|
||||||
const { t } = useLocale();
|
|
||||||
|
|
||||||
if (isLoading) {
|
|
||||||
return <SelectSkeletonLoader />;
|
|
||||||
}
|
|
||||||
|
|
||||||
const schedules = data?.schedules || [];
|
|
||||||
|
|
||||||
const options = schedules.map((schedule) => ({
|
|
||||||
value: schedule.id,
|
|
||||||
label: schedule.name,
|
|
||||||
isDefault: schedule.isDefault,
|
|
||||||
}));
|
|
||||||
|
|
||||||
const value = options.find((option) =>
|
|
||||||
props.value
|
|
||||||
? option.value === props.value
|
|
||||||
: option.value === schedules.find((schedule) => schedule.isDefault)?.id
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Select
|
|
||||||
placeholder={t("select")}
|
|
||||||
options={options}
|
|
||||||
isSearchable={false}
|
|
||||||
onChange={props.onChange}
|
|
||||||
className={classNames("block w-full min-w-0 flex-1 rounded-sm text-sm", className)}
|
|
||||||
value={value}
|
|
||||||
components={{ Option, SingleValue }}
|
|
||||||
isMulti={false}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
const format = (date: Date, hour12: boolean) =>
|
|
||||||
Intl.DateTimeFormat(undefined, { hour: "numeric", minute: "numeric", hour12 }).format(
|
|
||||||
new Date(dayjs.utc(date).format("YYYY-MM-DDTHH:mm:ss"))
|
|
||||||
);
|
|
||||||
|
|
||||||
const EventTypeScheduleDetails = () => {
|
|
||||||
const { data: loggedInUser } = useMeQuery();
|
|
||||||
const timeFormat = loggedInUser?.timeFormat;
|
|
||||||
const { t, i18n } = useLocale();
|
|
||||||
const { watch } = useFormContext<FormValues>();
|
|
||||||
|
|
||||||
const scheduleId = watch("schedule");
|
|
||||||
const { isLoading, data: schedule } = trpc.viewer.availability.schedule.get.useQuery(
|
|
||||||
{ scheduleId: scheduleId || loggedInUser?.defaultScheduleId || undefined },
|
|
||||||
{ enabled: !!scheduleId || !!loggedInUser?.defaultScheduleId }
|
|
||||||
);
|
|
||||||
|
|
||||||
const filterDays = (dayNum: number) =>
|
|
||||||
schedule?.schedule.filter((item) => item.days.includes((dayNum + 1) % 7)) || [];
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="space-y-4 rounded border px-6 pb-4">
|
|
||||||
<ol className="table border-collapse text-sm">
|
|
||||||
{weekdayNames(i18n.language, 1, "long").map((day, index) => {
|
|
||||||
const isAvailable = !!filterDays(index).length;
|
|
||||||
return (
|
|
||||||
<li key={day} className="my-6 flex border-transparent last:mb-2">
|
|
||||||
<span
|
|
||||||
className={classNames(
|
|
||||||
"w-20 font-medium sm:w-32",
|
|
||||||
!isAvailable && "text-gray-500 opacity-50"
|
|
||||||
)}>
|
|
||||||
{day}
|
|
||||||
</span>
|
|
||||||
{isLoading ? (
|
|
||||||
<SkeletonText className="block h-5 w-60" />
|
|
||||||
) : isAvailable ? (
|
|
||||||
<div className="space-y-3 text-right">
|
|
||||||
{filterDays(index).map((dayRange, i) => (
|
|
||||||
<div key={i} className="flex items-center leading-4">
|
|
||||||
<span className="w-16 sm:w-28 sm:text-left">
|
|
||||||
{format(dayRange.startTime, timeFormat === 12)}
|
|
||||||
</span>
|
|
||||||
<span className="ltr:ml-4 rtl:mr-4">-</span>
|
|
||||||
<div className="ml-6">{format(dayRange.endTime, timeFormat === 12)}</div>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
<span className="ml-6 text-gray-500 opacity-50 sm:ml-0">{t("unavailable")}</span>
|
|
||||||
)}
|
|
||||||
</li>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</ol>
|
|
||||||
<hr />
|
|
||||||
<div className="flex flex-col justify-center gap-2 sm:flex-row sm:justify-between">
|
|
||||||
<span className="flex items-center justify-center text-sm text-gray-600 sm:justify-start">
|
|
||||||
<FiGlobe className="ltr:mr-2 rtl:ml-2" />
|
|
||||||
{schedule?.timeZone || <SkeletonText className="block h-5 w-32" />}
|
|
||||||
</span>
|
|
||||||
{!!schedule?.id && (
|
|
||||||
<Button
|
|
||||||
href={`/availability/${schedule.id}`}
|
|
||||||
disabled={isLoading}
|
|
||||||
color="minimal"
|
|
||||||
EndIcon={FiExternalLink}
|
|
||||||
target="_blank"
|
|
||||||
rel="noopener noreferrer">
|
|
||||||
{t("edit_availability")}
|
|
||||||
</Button>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
const EventTypeSchedule = () => {
|
|
||||||
const { t } = useLocale();
|
|
||||||
return (
|
|
||||||
<div className="space-y-4">
|
|
||||||
<div>
|
|
||||||
<label htmlFor="availability" className="mb-2 block text-sm font-medium leading-none text-gray-700">
|
|
||||||
{t("availability")}
|
|
||||||
</label>
|
|
||||||
<Controller
|
|
||||||
name="schedule"
|
|
||||||
render={({ field }) => (
|
|
||||||
<AvailabilitySelect
|
|
||||||
value={field.value}
|
|
||||||
onBlur={field.onBlur}
|
|
||||||
name={field.name}
|
|
||||||
onChange={(selected) => {
|
|
||||||
field.onChange(selected?.value || null);
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<EventTypeScheduleDetails />
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
const UseCommonScheduleSettingsToggle = () => {
|
|
||||||
const { t } = useLocale();
|
|
||||||
const { resetField, setValue } = useFormContext<FormValues>();
|
|
||||||
return (
|
|
||||||
<Controller
|
|
||||||
name="metadata.config.useHostSchedulesForTeamEvent"
|
|
||||||
render={({ field: { value, onChange } }) => (
|
|
||||||
<SettingsToggle
|
|
||||||
checked={!value}
|
|
||||||
onCheckedChange={(checked) => {
|
|
||||||
onChange(!checked);
|
|
||||||
if (checked) {
|
|
||||||
resetField("schedule");
|
|
||||||
} else {
|
|
||||||
setValue("schedule", null);
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
title={t("choose_common_schedule_team_event")}
|
|
||||||
description={t("choose_common_schedule_team_event_description")}>
|
|
||||||
<EventTypeSchedule />
|
|
||||||
</SettingsToggle>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export const AvailabilityTab = ({ isTeamEvent }: { isTeamEvent: boolean }) => {
|
|
||||||
return isTeamEvent ? <UseCommonScheduleSettingsToggle /> : <EventTypeSchedule />;
|
|
||||||
};
|
|
||||||
@@ -58,35 +58,35 @@ const CustomEventTypeModalForm: FC<CustomEventTypeModalFormProps> = (props) => {
|
|||||||
className="mb-0"
|
className="mb-0"
|
||||||
/>
|
/>
|
||||||
<div className="pt-6 text-sm">
|
<div className="pt-6 text-sm">
|
||||||
<div className="mb-6 rounded-md bg-gray-100 p-2">
|
<div className="bg-subtle mb-6 rounded-md p-2">
|
||||||
<h1 className="mb-2 ml-1 font-medium text-gray-900">{t("available_variables")}</h1>
|
<h1 className="text-emphasis mb-2 ml-1 font-medium">{t("available_variables")}</h1>
|
||||||
<div className="mb-2.5 flex font-normal">
|
<div className="mb-2.5 flex font-normal">
|
||||||
<p className="ml-1 mr-5 w-28 text-gray-400">{`{Event type title}`}</p>
|
<p className="text-subtle ml-1 mr-5 w-28">{`{Event type title}`}</p>
|
||||||
<p className="text-gray-900">{t("event_name_info")}</p>
|
<p className="text-emphasis">{t("event_name_info")}</p>
|
||||||
</div>
|
</div>
|
||||||
<div className="mb-2.5 flex font-normal">
|
<div className="mb-2.5 flex font-normal">
|
||||||
<p className="ml-1 mr-5 w-28 text-gray-400">{`{Organiser}`}</p>
|
<p className="text-subtle ml-1 mr-5 w-28">{`{Organiser}`}</p>
|
||||||
<p className="text-gray-900">{t("your_full_name")}</p>
|
<p className="text-emphasis">{t("your_full_name")}</p>
|
||||||
</div>
|
</div>
|
||||||
<div className="mb-2.5 flex font-normal">
|
<div className="mb-2.5 flex font-normal">
|
||||||
<p className="ml-1 mr-5 w-28 text-gray-400">{`{Scheduler}`}</p>
|
<p className="text-subtle ml-1 mr-5 w-28">{`{Scheduler}`}</p>
|
||||||
<p className="text-gray-900">{t("scheduler_full_name")}</p>
|
<p className="text-emphasis">{t("scheduler_full_name")}</p>
|
||||||
</div>
|
</div>
|
||||||
<div className="mb-1 flex font-normal">
|
<div className="mb-1 flex font-normal">
|
||||||
<p className="ml-1 mr-5 w-28 text-gray-400">{`{Location}`}</p>
|
<p className="text-subtle ml-1 mr-5 w-28">{`{Location}`}</p>
|
||||||
<p className="text-gray-900">{t("location_info")}</p>
|
<p className="text-emphasis">{t("location_info")}</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<h1 className="mb-2 text-[14px] font-medium leading-4">{t("preview")}</h1>
|
<h1 className="mb-2 text-[14px] font-medium leading-4">{t("preview")}</h1>
|
||||||
<div
|
<div
|
||||||
className="flex h-[212px] w-full rounded-md border-y bg-cover bg-center"
|
className="flex h-[212px] w-full rounded-md border-y bg-cover bg-center dark:invert"
|
||||||
style={{
|
style={{
|
||||||
backgroundImage: "url(/calendar-preview.svg)",
|
backgroundImage: "url(/calendar-preview.svg)",
|
||||||
}}>
|
}}>
|
||||||
<div className="m-auto flex items-center justify-center self-stretch">
|
<div className="m-auto flex items-center justify-center self-stretch">
|
||||||
<div className="mt-3 ml-11 box-border h-[110px] w-[120px] flex-col items-start gap-1 rounded-md border border-solid border-black bg-gray-100 text-[12px] leading-3">
|
<div className="bg-subtle mt-3 ml-11 box-border h-[110px] w-[120px] flex-col items-start gap-1 rounded-md border border-solid border-black text-[12px] leading-3">
|
||||||
<p className="overflow-hidden text-ellipsis p-1.5 font-medium text-gray-900">{previewText}</p>
|
<p className="text-emphasis overflow-hidden text-ellipsis p-1.5 font-medium">{previewText}</p>
|
||||||
<p className="ml-1.5 text-[10px] font-normal text-gray-600">8 - 10 AM</p>
|
<p className="text-default ml-1.5 text-[10px] font-normal">8 - 10 AM</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -8,14 +8,21 @@ import { v5 as uuidv5 } from "uuid";
|
|||||||
|
|
||||||
import type { EventNameObjectType } from "@calcom/core/event";
|
import type { EventNameObjectType } from "@calcom/core/event";
|
||||||
import { getEventName } from "@calcom/core/event";
|
import { getEventName } from "@calcom/core/event";
|
||||||
|
import getLocationsOptionsForSelect from "@calcom/features/bookings/lib/getLocationOptionsForSelect";
|
||||||
import DestinationCalendarSelector from "@calcom/features/calendars/DestinationCalendarSelector";
|
import DestinationCalendarSelector from "@calcom/features/calendars/DestinationCalendarSelector";
|
||||||
|
import useLockedFieldsManager from "@calcom/features/ee/managed-event-types/hooks/useLockedFieldsManager";
|
||||||
|
import {
|
||||||
|
allowDisablingAttendeeConfirmationEmails,
|
||||||
|
allowDisablingHostConfirmationEmails,
|
||||||
|
} from "@calcom/features/ee/workflows/lib/allowDisablingStandardEmails";
|
||||||
import { FormBuilder } from "@calcom/features/form-builder/FormBuilder";
|
import { FormBuilder } from "@calcom/features/form-builder/FormBuilder";
|
||||||
import { APP_NAME, CAL_URL, IS_SELF_HOSTED } from "@calcom/lib/constants";
|
import { classNames } from "@calcom/lib";
|
||||||
|
import { APP_NAME, CAL_URL } from "@calcom/lib/constants";
|
||||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||||
import type { Prisma } from "@calcom/prisma/client";
|
import type { Prisma } from "@calcom/prisma/client";
|
||||||
import { trpc } from "@calcom/trpc/react";
|
import { trpc } from "@calcom/trpc/react";
|
||||||
import { Badge, Button, Checkbox, Label, SettingsToggle, showToast, TextField, Tooltip } from "@calcom/ui";
|
import { Button, Checkbox, Label, SettingsToggle, showToast, TextField, Tooltip, Alert } from "@calcom/ui";
|
||||||
import { FiEdit, FiCopy } from "@calcom/ui/components/icon";
|
import { Edit, Copy } from "@calcom/ui/components/icon";
|
||||||
|
|
||||||
import RequiresConfirmationController from "./RequiresConfirmationController";
|
import RequiresConfirmationController from "./RequiresConfirmationController";
|
||||||
|
|
||||||
@@ -40,6 +47,8 @@ export const EventAdvancedTab = ({ eventType, team }: Pick<EventTypeSetupProps,
|
|||||||
|
|
||||||
const bookingFields: Prisma.JsonObject = {};
|
const bookingFields: Prisma.JsonObject = {};
|
||||||
|
|
||||||
|
const workflows = eventType.workflows.map((workflowOnEventType) => workflowOnEventType.workflow);
|
||||||
|
|
||||||
eventType.bookingFields.forEach(({ name }) => {
|
eventType.bookingFields.forEach(({ name }) => {
|
||||||
bookingFields[name] = name + " input";
|
bookingFields[name] = name + " input";
|
||||||
});
|
});
|
||||||
@@ -56,6 +65,7 @@ export const EventAdvancedTab = ({ eventType, team }: Pick<EventTypeSetupProps,
|
|||||||
const [requiresConfirmation, setRequiresConfirmation] = useState(eventType.requiresConfirmation);
|
const [requiresConfirmation, setRequiresConfirmation] = useState(eventType.requiresConfirmation);
|
||||||
const placeholderHashedLink = `${CAL_URL}/d/${hashedUrl}/${eventType.slug}`;
|
const placeholderHashedLink = `${CAL_URL}/d/${hashedUrl}/${eventType.slug}`;
|
||||||
const seatsEnabled = formMethods.watch("seatsPerTimeSlotEnabled");
|
const seatsEnabled = formMethods.watch("seatsPerTimeSlotEnabled");
|
||||||
|
const noShowFeeEnabled = eventType.metadata.apps?.stripe?.paymentOption === "HOLD";
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
!hashedUrl && setHashedUrl(generateHashedLink(eventType.users[0]?.id ?? team?.id));
|
!hashedUrl && setHashedUrl(generateHashedLink(eventType.users[0]?.id ?? team?.id));
|
||||||
@@ -77,11 +87,19 @@ export const EventAdvancedTab = ({ eventType, team }: Pick<EventTypeSetupProps,
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const { shouldLockDisableProps } = useLockedFieldsManager(
|
||||||
|
eventType,
|
||||||
|
t("locked_fields_admin_description"),
|
||||||
|
t("locked_fields_member_description")
|
||||||
|
);
|
||||||
const eventNamePlaceholder = getEventName({
|
const eventNamePlaceholder = getEventName({
|
||||||
...eventNameObject,
|
...eventNameObject,
|
||||||
eventName: formMethods.watch("eventName"),
|
eventName: formMethods.watch("eventName"),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const successRedirectUrlLocked = shouldLockDisableProps("successRedirectUrl");
|
||||||
|
const seatsLocked = shouldLockDisableProps("seatsPerTimeSlotEnabled");
|
||||||
|
|
||||||
const closeEventNameTip = () => setShowEventNameTip(false);
|
const closeEventNameTip = () => setShowEventNameTip(false);
|
||||||
|
|
||||||
const setEventName = (value: string) => formMethods.setValue("eventName", value);
|
const setEventName = (value: string) => formMethods.setValue("eventName", value);
|
||||||
@@ -99,7 +117,7 @@ export const EventAdvancedTab = ({ eventType, team }: Pick<EventTypeSetupProps,
|
|||||||
<Link
|
<Link
|
||||||
href="/apps/categories/calendar"
|
href="/apps/categories/calendar"
|
||||||
target="_blank"
|
target="_blank"
|
||||||
className="text-sm text-gray-600 hover:text-gray-900">
|
className="hover:text-emphasis text-default text-sm">
|
||||||
{t("add_another_calendar")}
|
{t("add_another_calendar")}
|
||||||
</Link>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
@@ -118,44 +136,51 @@ export const EventAdvancedTab = ({ eventType, team }: Pick<EventTypeSetupProps,
|
|||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<p className="text-sm text-gray-600">{t("select_which_cal")}</p>
|
<p className="text-default text-sm">{t("select_which_cal")}</p>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
<div className="w-full">
|
<div className="w-full">
|
||||||
<TextField
|
<TextField
|
||||||
label={t("event_name_in_calendar")}
|
label={t("event_name_in_calendar")}
|
||||||
type="text"
|
type="text"
|
||||||
|
{...shouldLockDisableProps("eventName")}
|
||||||
placeholder={eventNamePlaceholder}
|
placeholder={eventNamePlaceholder}
|
||||||
defaultValue={eventType.eventName || ""}
|
defaultValue={eventType.eventName || ""}
|
||||||
{...formMethods.register("eventName")}
|
{...formMethods.register("eventName")}
|
||||||
addOnSuffix={
|
addOnSuffix={
|
||||||
<Button
|
<Button
|
||||||
type="button"
|
|
||||||
StartIcon={FiEdit}
|
|
||||||
variant="icon"
|
|
||||||
color="minimal"
|
color="minimal"
|
||||||
className="hover:stroke-3 min-w-fit px-0 hover:bg-transparent hover:text-black"
|
size="sm"
|
||||||
onClick={() => setShowEventNameTip((old) => !old)}
|
|
||||||
aria-label="edit custom name"
|
aria-label="edit custom name"
|
||||||
/>
|
className="hover:stroke-3 hover:text-emphasis min-w-fit px-0 !py-0 hover:bg-transparent"
|
||||||
|
onClick={() => setShowEventNameTip((old) => !old)}>
|
||||||
|
<Edit className="h-4 w-4" />
|
||||||
|
</Button>
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<hr />
|
<hr className="border-subtle" />
|
||||||
<FormBuilder
|
<FormBuilder
|
||||||
title={t("booking_questions_title")}
|
title={t("booking_questions_title")}
|
||||||
description={t("booking_questions_description")}
|
description={t("booking_questions_description")}
|
||||||
addFieldLabel={t("add_a_booking_question")}
|
addFieldLabel={t("add_a_booking_question")}
|
||||||
formProp="bookingFields"
|
formProp="bookingFields"
|
||||||
|
{...shouldLockDisableProps("bookingFields")}
|
||||||
|
dataStore={{
|
||||||
|
options: {
|
||||||
|
locations: getLocationsOptionsForSelect(eventType?.locations ?? [], t),
|
||||||
|
},
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
<hr />
|
<hr className="border-subtle" />
|
||||||
<RequiresConfirmationController
|
<RequiresConfirmationController
|
||||||
|
eventType={eventType}
|
||||||
seatsEnabled={seatsEnabled}
|
seatsEnabled={seatsEnabled}
|
||||||
metadata={eventType.metadata}
|
metadata={eventType.metadata}
|
||||||
requiresConfirmation={requiresConfirmation}
|
requiresConfirmation={requiresConfirmation}
|
||||||
onRequiresConfirmation={setRequiresConfirmation}
|
onRequiresConfirmation={setRequiresConfirmation}
|
||||||
/>
|
/>
|
||||||
<hr />
|
<hr className="border-subtle" />
|
||||||
<Controller
|
<Controller
|
||||||
name="hideCalendarNotes"
|
name="hideCalendarNotes"
|
||||||
control={formMethods.control}
|
control={formMethods.control}
|
||||||
@@ -163,13 +188,14 @@ export const EventAdvancedTab = ({ eventType, team }: Pick<EventTypeSetupProps,
|
|||||||
render={({ field: { value, onChange } }) => (
|
render={({ field: { value, onChange } }) => (
|
||||||
<SettingsToggle
|
<SettingsToggle
|
||||||
title={t("disable_notes")}
|
title={t("disable_notes")}
|
||||||
|
{...shouldLockDisableProps("hideCalendarNotes")}
|
||||||
description={t("disable_notes_description")}
|
description={t("disable_notes_description")}
|
||||||
checked={value}
|
checked={value}
|
||||||
onCheckedChange={(e) => onChange(e)}
|
onCheckedChange={(e) => onChange(e)}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
<hr />
|
<hr className="border-subtle" />
|
||||||
<Controller
|
<Controller
|
||||||
name="successRedirectUrl"
|
name="successRedirectUrl"
|
||||||
control={formMethods.control}
|
control={formMethods.control}
|
||||||
@@ -177,48 +203,43 @@ export const EventAdvancedTab = ({ eventType, team }: Pick<EventTypeSetupProps,
|
|||||||
<>
|
<>
|
||||||
<SettingsToggle
|
<SettingsToggle
|
||||||
title={t("redirect_success_booking")}
|
title={t("redirect_success_booking")}
|
||||||
|
{...successRedirectUrlLocked}
|
||||||
description={t("redirect_url_description")}
|
description={t("redirect_url_description")}
|
||||||
checked={redirectUrlVisible}
|
checked={redirectUrlVisible}
|
||||||
onCheckedChange={(e) => {
|
onCheckedChange={(e) => {
|
||||||
setRedirectUrlVisible(e);
|
setRedirectUrlVisible(e);
|
||||||
onChange(e ? value : "");
|
onChange(e ? value : "");
|
||||||
}}>
|
}}>
|
||||||
{/* Textfield has some margin by default we remove that so we can keep consitant aligment */}
|
{/* Textfield has some margin by default we remove that so we can keep consistent alignment */}
|
||||||
<div className="lg:-ml-2">
|
<div className="lg:-ml-2 lg:-mb-2">
|
||||||
<TextField
|
<TextField
|
||||||
|
className="w-full"
|
||||||
label={t("redirect_success_booking")}
|
label={t("redirect_success_booking")}
|
||||||
labelSrOnly
|
labelSrOnly
|
||||||
|
disabled={successRedirectUrlLocked.disabled}
|
||||||
placeholder={t("external_redirect_url")}
|
placeholder={t("external_redirect_url")}
|
||||||
required={redirectUrlVisible}
|
required={redirectUrlVisible}
|
||||||
type="text"
|
type="text"
|
||||||
defaultValue={eventType.successRedirectUrl || ""}
|
defaultValue={eventType.successRedirectUrl || ""}
|
||||||
{...formMethods.register("successRedirectUrl")}
|
{...formMethods.register("successRedirectUrl")}
|
||||||
/>
|
/>
|
||||||
<div className="mt-2 flex">
|
<div
|
||||||
<Checkbox
|
className={classNames(
|
||||||
description={t("disable_success_page")}
|
"p-1 text-sm text-orange-600",
|
||||||
// Disable if it's not Self Hosted or if the redirect url is not set
|
formMethods.getValues("successRedirectUrl") ? "block" : "hidden"
|
||||||
disabled={!IS_SELF_HOSTED || !formMethods.watch("successRedirectUrl")}
|
)}>
|
||||||
{...formMethods.register("metadata.disableSuccessPage")}
|
{t("redirect_url_warning")}
|
||||||
/>
|
|
||||||
{/*TODO: Extract it out into a component when used more than once*/}
|
|
||||||
{!IS_SELF_HOSTED && (
|
|
||||||
<Link href="https://cal.com/pricing" target="_blank">
|
|
||||||
<Badge variant="orange" className="ml-2">
|
|
||||||
Platform Only
|
|
||||||
</Badge>
|
|
||||||
</Link>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</SettingsToggle>
|
</SettingsToggle>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
<hr />
|
<hr className="border-subtle" />
|
||||||
<SettingsToggle
|
<SettingsToggle
|
||||||
data-testid="hashedLinkCheck"
|
data-testid="hashedLinkCheck"
|
||||||
title={t("private_link")}
|
title={t("private_link")}
|
||||||
|
{...shouldLockDisableProps("hashedLinkCheck")}
|
||||||
description={t("private_link_description", { appName: APP_NAME })}
|
description={t("private_link_description", { appName: APP_NAME })}
|
||||||
checked={hashedLinkVisible}
|
checked={hashedLinkVisible}
|
||||||
onCheckedChange={(e) => {
|
onCheckedChange={(e) => {
|
||||||
@@ -240,6 +261,10 @@ export const EventAdvancedTab = ({ eventType, team }: Pick<EventTypeSetupProps,
|
|||||||
<Tooltip content={eventType.hashedLink ? t("copy_to_clipboard") : t("enabled_after_update")}>
|
<Tooltip content={eventType.hashedLink ? t("copy_to_clipboard") : t("enabled_after_update")}>
|
||||||
<Button
|
<Button
|
||||||
color="minimal"
|
color="minimal"
|
||||||
|
size="sm"
|
||||||
|
type="button"
|
||||||
|
className="hover:stroke-3 hover:text-emphasis min-w-fit px-0 !py-0 hover:bg-transparent"
|
||||||
|
aria-label="copy link"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
navigator.clipboard.writeText(placeholderHashedLink);
|
navigator.clipboard.writeText(placeholderHashedLink);
|
||||||
if (eventType.hashedLink) {
|
if (eventType.hashedLink) {
|
||||||
@@ -247,73 +272,122 @@ export const EventAdvancedTab = ({ eventType, team }: Pick<EventTypeSetupProps,
|
|||||||
} else {
|
} else {
|
||||||
showToast(t("enabled_after_update_description"), "warning");
|
showToast(t("enabled_after_update_description"), "warning");
|
||||||
}
|
}
|
||||||
}}
|
}}>
|
||||||
className="hover:stroke-3 hover:bg-transparent hover:text-black"
|
<Copy className="h-4 w-4" />
|
||||||
type="button">
|
|
||||||
<FiCopy />
|
|
||||||
</Button>
|
</Button>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</SettingsToggle>
|
</SettingsToggle>
|
||||||
<hr />
|
<hr className="border-subtle" />
|
||||||
<Controller
|
<Controller
|
||||||
name="seatsPerTimeSlotEnabled"
|
name="seatsPerTimeSlotEnabled"
|
||||||
control={formMethods.control}
|
control={formMethods.control}
|
||||||
defaultValue={!!eventType.seatsPerTimeSlot}
|
defaultValue={!!eventType.seatsPerTimeSlot}
|
||||||
render={({ field: { value, onChange } }) => (
|
render={({ field: { value, onChange } }) => (
|
||||||
<SettingsToggle
|
<>
|
||||||
data-testid="offer-seats-toggle"
|
<SettingsToggle
|
||||||
title={t("offer_seats")}
|
data-testid="offer-seats-toggle"
|
||||||
description={t("offer_seats_description")}
|
title={t("offer_seats")}
|
||||||
checked={value}
|
{...seatsLocked}
|
||||||
onCheckedChange={(e) => {
|
description={t("offer_seats_description")}
|
||||||
// Enabling seats will disable guests and requiring confirmation until fully supported
|
checked={value}
|
||||||
if (e) {
|
disabled={noShowFeeEnabled}
|
||||||
toggleGuests(false);
|
onCheckedChange={(e) => {
|
||||||
formMethods.setValue("requiresConfirmation", false);
|
// Enabling seats will disable guests and requiring confirmation until fully supported
|
||||||
setRequiresConfirmation(false);
|
if (e) {
|
||||||
formMethods.setValue("seatsPerTimeSlot", 2);
|
toggleGuests(false);
|
||||||
} else {
|
formMethods.setValue("requiresConfirmation", false);
|
||||||
formMethods.setValue("seatsPerTimeSlot", null);
|
setRequiresConfirmation(false);
|
||||||
toggleGuests(true);
|
formMethods.setValue("seatsPerTimeSlot", 2);
|
||||||
}
|
} else {
|
||||||
onChange(e);
|
formMethods.setValue("seatsPerTimeSlot", null);
|
||||||
}}>
|
toggleGuests(true);
|
||||||
<Controller
|
}
|
||||||
name="seatsPerTimeSlot"
|
onChange(e);
|
||||||
control={formMethods.control}
|
}}>
|
||||||
defaultValue={eventType.seatsPerTimeSlot}
|
<Controller
|
||||||
render={({ field: { value, onChange } }) => (
|
name="seatsPerTimeSlot"
|
||||||
<div className="lg:-ml-2">
|
control={formMethods.control}
|
||||||
<TextField
|
defaultValue={eventType.seatsPerTimeSlot}
|
||||||
required
|
render={({ field: { value, onChange } }) => (
|
||||||
name="seatsPerTimeSlot"
|
<div className="lg:-ml-2">
|
||||||
labelSrOnly
|
<TextField
|
||||||
label={t("number_of_seats")}
|
required
|
||||||
type="number"
|
name="seatsPerTimeSlot"
|
||||||
defaultValue={value || 2}
|
labelSrOnly
|
||||||
min={1}
|
label={t("number_of_seats")}
|
||||||
addOnSuffix={<>{t("seats")}</>}
|
type="number"
|
||||||
onChange={(e) => {
|
disabled={seatsLocked.disabled}
|
||||||
onChange(Math.abs(Number(e.target.value)));
|
defaultValue={value || 2}
|
||||||
}}
|
min={1}
|
||||||
/>
|
addOnSuffix={<>{t("seats")}</>}
|
||||||
<div className="mt-2">
|
onChange={(e) => {
|
||||||
<Checkbox
|
onChange(Math.abs(Number(e.target.value)));
|
||||||
description={t("show_attendees")}
|
}}
|
||||||
onChange={(e) => formMethods.setValue("seatsShowAttendees", e.target.checked)}
|
|
||||||
defaultChecked={!!eventType.seatsShowAttendees}
|
|
||||||
/>
|
/>
|
||||||
|
<div className="mt-2">
|
||||||
|
<Checkbox
|
||||||
|
description={t("show_attendees")}
|
||||||
|
disabled={seatsLocked.disabled}
|
||||||
|
onChange={(e) => formMethods.setValue("seatsShowAttendees", e.target.checked)}
|
||||||
|
defaultChecked={!!eventType.seatsShowAttendees}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
)}
|
||||||
)}
|
/>
|
||||||
/>
|
</SettingsToggle>
|
||||||
</SettingsToggle>
|
{noShowFeeEnabled && <Alert severity="warning" title={t("seats_and_no_show_fee_error")} />}
|
||||||
|
</>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
|
{allowDisablingAttendeeConfirmationEmails(workflows) && (
|
||||||
|
<>
|
||||||
|
<hr className="border-subtle" />
|
||||||
|
<Controller
|
||||||
|
name="metadata.disableStandardEmails.confirmation.attendee"
|
||||||
|
control={formMethods.control}
|
||||||
|
render={({ field: { value, onChange } }) => (
|
||||||
|
<>
|
||||||
|
<SettingsToggle
|
||||||
|
title={t("disable_attendees_confirmation_emails")}
|
||||||
|
description={t("disable_attendees_confirmation_emails_description")}
|
||||||
|
checked={value || false}
|
||||||
|
onCheckedChange={(e) => {
|
||||||
|
formMethods.setValue("metadata.disableStandardEmails.confirmation.attendee", e);
|
||||||
|
onChange(e);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
{allowDisablingHostConfirmationEmails(workflows) && (
|
||||||
|
<>
|
||||||
|
<hr className="border-subtle" />
|
||||||
|
<Controller
|
||||||
|
name="metadata.disableStandardEmails.confirmation.host"
|
||||||
|
control={formMethods.control}
|
||||||
|
defaultValue={!!eventType.seatsPerTimeSlot}
|
||||||
|
render={({ field: { value, onChange } }) => (
|
||||||
|
<>
|
||||||
|
<SettingsToggle
|
||||||
|
title={t("disable_host_confirmation_emails")}
|
||||||
|
description={t("disable_host_confirmation_emails_description")}
|
||||||
|
checked={value || false}
|
||||||
|
onCheckedChange={(e) => {
|
||||||
|
formMethods.setValue("metadata.disableStandardEmails.confirmation.host", e);
|
||||||
|
onChange(e);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
{showEventNameTip && (
|
{showEventNameTip && (
|
||||||
<CustomEventTypeModal
|
<CustomEventTypeModal
|
||||||
close={closeEventNameTip}
|
close={closeEventNameTip}
|
||||||
|
|||||||
@@ -5,10 +5,11 @@ import type { GetAppData, SetAppData } from "@calcom/app-store/EventTypeAppConte
|
|||||||
import { EventTypeAppCard } from "@calcom/app-store/_components/EventTypeAppCardInterface";
|
import { EventTypeAppCard } from "@calcom/app-store/_components/EventTypeAppCardInterface";
|
||||||
import type { EventTypeAppCardComponentProps } from "@calcom/app-store/types";
|
import type { EventTypeAppCardComponentProps } from "@calcom/app-store/types";
|
||||||
import type { EventTypeAppsList } from "@calcom/app-store/utils";
|
import type { EventTypeAppsList } from "@calcom/app-store/utils";
|
||||||
|
import useLockedFieldsManager from "@calcom/features/ee/managed-event-types/hooks/useLockedFieldsManager";
|
||||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||||
import { trpc } from "@calcom/trpc/react";
|
import { trpc } from "@calcom/trpc/react";
|
||||||
import { Button, EmptyScreen } from "@calcom/ui";
|
import { Button, EmptyScreen, Alert } from "@calcom/ui";
|
||||||
import { FiGrid } from "@calcom/ui/components/icon";
|
import { Grid, Lock } from "@calcom/ui/components/icon";
|
||||||
|
|
||||||
export type EventType = Pick<EventTypeSetupProps, "eventType">["eventType"] &
|
export type EventType = Pick<EventTypeSetupProps, "eventType">["eventType"] &
|
||||||
EventTypeAppCardComponentProps["eventType"];
|
EventTypeAppCardComponentProps["eventType"];
|
||||||
@@ -55,19 +56,39 @@ export const EventAppsTab = ({ eventType }: { eventType: EventType }) => {
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const { shouldLockDisableProps, isManagedEventType, isChildrenManagedEventType } = useLockedFieldsManager(
|
||||||
|
eventType,
|
||||||
|
t("locked_fields_admin_description"),
|
||||||
|
t("locked_fields_member_description")
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div>
|
<div>
|
||||||
<div className="before:border-0">
|
<div className="before:border-0">
|
||||||
|
{!installedApps?.length && isManagedEventType && (
|
||||||
|
<Alert
|
||||||
|
severity="neutral"
|
||||||
|
className="mb-2"
|
||||||
|
title={t("locked_for_members")}
|
||||||
|
message={t("locked_apps_description")}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
{!isLoading && !installedApps?.length ? (
|
{!isLoading && !installedApps?.length ? (
|
||||||
<EmptyScreen
|
<EmptyScreen
|
||||||
Icon={FiGrid}
|
Icon={Grid}
|
||||||
headline={t("empty_installed_apps_headline")}
|
headline={t("empty_installed_apps_headline")}
|
||||||
description={t("empty_installed_apps_description")}
|
description={t("empty_installed_apps_description")}
|
||||||
buttonRaw={
|
buttonRaw={
|
||||||
<Button target="_blank" color="secondary" href="/apps">
|
isChildrenManagedEventType && !isManagedEventType ? (
|
||||||
{t("empty_installed_apps_button")}{" "}
|
<Button StartIcon={Lock} color="secondary" disabled>
|
||||||
</Button>
|
{t("locked_by_admin")}
|
||||||
|
</Button>
|
||||||
|
) : (
|
||||||
|
<Button target="_blank" color="secondary" href="/apps">
|
||||||
|
{t("empty_installed_apps_button")}{" "}
|
||||||
|
</Button>
|
||||||
|
)
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
) : null}
|
) : null}
|
||||||
@@ -82,22 +103,24 @@ export const EventAppsTab = ({ eventType }: { eventType: EventType }) => {
|
|||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
{!shouldLockDisableProps("apps").disabled && (
|
||||||
{!isLoading && notInstalledApps?.length ? (
|
<div>
|
||||||
<h2 className="mt-0 mb-2 text-lg font-semibold text-gray-900">Available Apps</h2>
|
{!isLoading && notInstalledApps?.length ? (
|
||||||
) : null}
|
<h2 className="text-emphasis my-2 text-lg font-semibold">{t("available_apps")}</h2>
|
||||||
<div className="before:border-0">
|
) : null}
|
||||||
{notInstalledApps?.map((app) => (
|
<div className="before:border-0">
|
||||||
<EventTypeAppCard
|
{notInstalledApps?.map((app) => (
|
||||||
getAppData={getAppDataGetter(app.slug as EventTypeAppsList)}
|
<EventTypeAppCard
|
||||||
setAppData={getAppDataSetter(app.slug as EventTypeAppsList)}
|
getAppData={getAppDataGetter(app.slug as EventTypeAppsList)}
|
||||||
key={app.slug}
|
setAppData={getAppDataSetter(app.slug as EventTypeAppsList)}
|
||||||
app={app}
|
key={app.slug}
|
||||||
eventType={eventType}
|
app={app}
|
||||||
/>
|
eventType={eventType}
|
||||||
))}
|
/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
)}
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
@@ -0,0 +1,301 @@
|
|||||||
|
import type { EventTypeSetup, FormValues } from "pages/event-types/[type]";
|
||||||
|
import { useState } from "react";
|
||||||
|
import { Controller, useFormContext } from "react-hook-form";
|
||||||
|
import type { OptionProps, SingleValueProps } from "react-select";
|
||||||
|
import { components } from "react-select";
|
||||||
|
|
||||||
|
import dayjs from "@calcom/dayjs";
|
||||||
|
import useLockedFieldsManager from "@calcom/features/ee/managed-event-types/hooks/useLockedFieldsManager";
|
||||||
|
import classNames from "@calcom/lib/classNames";
|
||||||
|
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||||
|
import { weekdayNames } from "@calcom/lib/weekday";
|
||||||
|
import { SchedulingType } from "@calcom/prisma/enums";
|
||||||
|
import { trpc } from "@calcom/trpc/react";
|
||||||
|
import useMeQuery from "@calcom/trpc/react/hooks/useMeQuery";
|
||||||
|
import { Badge, Button, Select, SettingsToggle, SkeletonText } from "@calcom/ui";
|
||||||
|
import { ExternalLink, Globe } from "@calcom/ui/components/icon";
|
||||||
|
|
||||||
|
import { SelectSkeletonLoader } from "@components/availability/SkeletonLoader";
|
||||||
|
|
||||||
|
export type AvailabilityOption = {
|
||||||
|
label: string;
|
||||||
|
value: number;
|
||||||
|
isDefault: boolean;
|
||||||
|
isManaged?: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
const Option = ({ ...props }: OptionProps<AvailabilityOption>) => {
|
||||||
|
const { label, isDefault, isManaged = false } = props.data;
|
||||||
|
const { t } = useLocale();
|
||||||
|
return (
|
||||||
|
<components.Option {...props}>
|
||||||
|
<span>{label}</span>
|
||||||
|
{isDefault && (
|
||||||
|
<Badge variant="blue" className="ml-2">
|
||||||
|
{t("default")}
|
||||||
|
</Badge>
|
||||||
|
)}
|
||||||
|
{isManaged && (
|
||||||
|
<Badge variant="gray" className="ml-2">
|
||||||
|
{t("managed")}
|
||||||
|
</Badge>
|
||||||
|
)}
|
||||||
|
</components.Option>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const SingleValue = ({ ...props }: SingleValueProps<AvailabilityOption>) => {
|
||||||
|
const { label, isDefault, isManaged = false } = props.data;
|
||||||
|
const { t } = useLocale();
|
||||||
|
return (
|
||||||
|
<components.SingleValue {...props}>
|
||||||
|
<span>{label}</span>
|
||||||
|
{isDefault && (
|
||||||
|
<Badge variant="blue" className="ml-2">
|
||||||
|
{t("default")}
|
||||||
|
</Badge>
|
||||||
|
)}
|
||||||
|
{isManaged && (
|
||||||
|
<Badge variant="gray" className="ml-2">
|
||||||
|
{t("managed")}
|
||||||
|
</Badge>
|
||||||
|
)}
|
||||||
|
</components.SingleValue>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const format = (date: Date, hour12: boolean) =>
|
||||||
|
Intl.DateTimeFormat(undefined, {
|
||||||
|
hour: "numeric",
|
||||||
|
minute: "numeric",
|
||||||
|
hourCycle: hour12 ? "h12" : "h24",
|
||||||
|
}).format(new Date(dayjs.utc(date).format("YYYY-MM-DDTHH:mm:ss")));
|
||||||
|
|
||||||
|
const EventTypeScheduleDetails = ({
|
||||||
|
isManagedEventType,
|
||||||
|
selectedScheduleValue,
|
||||||
|
}: {
|
||||||
|
isManagedEventType: boolean;
|
||||||
|
selectedScheduleValue: AvailabilityOption | undefined;
|
||||||
|
}) => {
|
||||||
|
const { data: loggedInUser } = useMeQuery();
|
||||||
|
const timeFormat = loggedInUser?.timeFormat;
|
||||||
|
const { t, i18n } = useLocale();
|
||||||
|
const { watch } = useFormContext<FormValues>();
|
||||||
|
|
||||||
|
const scheduleId = watch("schedule");
|
||||||
|
const { isLoading, data: schedule } = trpc.viewer.availability.schedule.get.useQuery(
|
||||||
|
{
|
||||||
|
scheduleId: scheduleId || loggedInUser?.defaultScheduleId || selectedScheduleValue?.value || undefined,
|
||||||
|
isManagedEventType,
|
||||||
|
},
|
||||||
|
{ enabled: !!scheduleId || !!loggedInUser?.defaultScheduleId || !!selectedScheduleValue }
|
||||||
|
);
|
||||||
|
|
||||||
|
const filterDays = (dayNum: number) =>
|
||||||
|
schedule?.schedule.filter((item) => item.days.includes((dayNum + 1) % 7)) || [];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="border-default space-y-4 rounded border px-6 pb-4">
|
||||||
|
<ol className="table border-collapse text-sm">
|
||||||
|
{weekdayNames(i18n.language, 1, "long").map((day, index) => {
|
||||||
|
const isAvailable = !!filterDays(index).length;
|
||||||
|
return (
|
||||||
|
<li key={day} className="my-6 flex border-transparent last:mb-2">
|
||||||
|
<span
|
||||||
|
className={classNames(
|
||||||
|
"w-20 font-medium sm:w-32 ",
|
||||||
|
!isAvailable ? "text-subtle line-through" : "text-default"
|
||||||
|
)}>
|
||||||
|
{day}
|
||||||
|
</span>
|
||||||
|
{isLoading ? (
|
||||||
|
<SkeletonText className="block h-5 w-60" />
|
||||||
|
) : isAvailable ? (
|
||||||
|
<div className="space-y-3 text-right">
|
||||||
|
{filterDays(index).map((dayRange, i) => (
|
||||||
|
<div key={i} className="text-default flex items-center leading-4">
|
||||||
|
<span className="w-16 sm:w-28 sm:text-left">
|
||||||
|
{format(dayRange.startTime, timeFormat === 12)}
|
||||||
|
</span>
|
||||||
|
<span className="ms-4">-</span>
|
||||||
|
<div className="ml-6 sm:w-28">{format(dayRange.endTime, timeFormat === 12)}</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<span className="text-subtle ml-6 sm:ml-0">{t("unavailable")}</span>
|
||||||
|
)}
|
||||||
|
</li>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</ol>
|
||||||
|
<hr className="border-subtle" />
|
||||||
|
<div className="flex flex-col justify-center gap-2 sm:flex-row sm:justify-between">
|
||||||
|
<span className="text-default flex items-center justify-center text-sm sm:justify-start">
|
||||||
|
<Globe className="h-3.5 w-3.5 ltr:mr-2 rtl:ml-2" />
|
||||||
|
{schedule?.timeZone || <SkeletonText className="block h-5 w-32" />}
|
||||||
|
</span>
|
||||||
|
{!!schedule?.id && !schedule.isManaged && (
|
||||||
|
<Button
|
||||||
|
href={`/availability/${schedule.id}`}
|
||||||
|
disabled={isLoading}
|
||||||
|
color="minimal"
|
||||||
|
EndIcon={ExternalLink}
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer">
|
||||||
|
{t("edit_availability")}
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const EventTypeSchedule = ({ eventType }: { eventType: EventTypeSetup }) => {
|
||||||
|
const { t } = useLocale();
|
||||||
|
const { shouldLockIndicator, isManagedEventType, isChildrenManagedEventType } = useLockedFieldsManager(
|
||||||
|
eventType,
|
||||||
|
t("locked_fields_admin_description"),
|
||||||
|
t("locked_fields_member_description")
|
||||||
|
);
|
||||||
|
const { watch } = useFormContext<FormValues>();
|
||||||
|
const watchSchedule = watch("schedule");
|
||||||
|
const formMethods = useFormContext<FormValues>();
|
||||||
|
const [options, setOptions] = useState<AvailabilityOption[]>([]);
|
||||||
|
|
||||||
|
const { isLoading } = trpc.viewer.availability.list.useQuery(undefined, {
|
||||||
|
onSuccess: ({ schedules }) => {
|
||||||
|
const options = schedules.map((schedule) => ({
|
||||||
|
value: schedule.id,
|
||||||
|
label: schedule.name,
|
||||||
|
isDefault: schedule.isDefault,
|
||||||
|
isManaged: false,
|
||||||
|
}));
|
||||||
|
|
||||||
|
// We are showing a managed event for a team admin, so adding the option to let members choose their schedule
|
||||||
|
if (isManagedEventType) {
|
||||||
|
options.push({
|
||||||
|
value: 0,
|
||||||
|
label: t("members_default_schedule"),
|
||||||
|
isDefault: false,
|
||||||
|
isManaged: false,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// We are showing a managed event for a member and team owner selected their own schedule, so adding
|
||||||
|
// the managed schedule option
|
||||||
|
if (
|
||||||
|
isChildrenManagedEventType &&
|
||||||
|
watchSchedule &&
|
||||||
|
!schedules.find((schedule) => schedule.id === watchSchedule)
|
||||||
|
) {
|
||||||
|
options.push({
|
||||||
|
value: watchSchedule,
|
||||||
|
label: eventType.scheduleName ?? t("default_schedule_name"),
|
||||||
|
isDefault: false,
|
||||||
|
isManaged: false,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
setOptions(options);
|
||||||
|
|
||||||
|
const scheduleId = formMethods.getValues("schedule");
|
||||||
|
const value = options.find((option) =>
|
||||||
|
scheduleId
|
||||||
|
? option.value === scheduleId
|
||||||
|
: isManagedEventType
|
||||||
|
? option.value === 0
|
||||||
|
: option.value === schedules.find((schedule) => schedule.isDefault)?.id
|
||||||
|
);
|
||||||
|
|
||||||
|
formMethods.setValue("availability", value);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const availabilityValue = formMethods.watch("availability");
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="space-y-4">
|
||||||
|
<div>
|
||||||
|
<label htmlFor="availability" className="text-default mb-2 block text-sm font-medium leading-none">
|
||||||
|
{t("availability")}
|
||||||
|
{shouldLockIndicator("availability")}
|
||||||
|
</label>
|
||||||
|
{isLoading && <SelectSkeletonLoader />}
|
||||||
|
{!isLoading && (
|
||||||
|
<Controller
|
||||||
|
name="schedule"
|
||||||
|
render={({ field }) => {
|
||||||
|
return (
|
||||||
|
<Select
|
||||||
|
placeholder={t("select")}
|
||||||
|
options={options}
|
||||||
|
isSearchable={false}
|
||||||
|
onChange={(selected) => {
|
||||||
|
field.onChange(selected?.value || null);
|
||||||
|
if (selected?.value) formMethods.setValue("availability", selected);
|
||||||
|
}}
|
||||||
|
className="block w-full min-w-0 flex-1 rounded-sm text-sm"
|
||||||
|
value={availabilityValue}
|
||||||
|
components={{ Option, SingleValue }}
|
||||||
|
isMulti={false}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
{availabilityValue?.value !== 0 ? (
|
||||||
|
<EventTypeScheduleDetails
|
||||||
|
selectedScheduleValue={availabilityValue}
|
||||||
|
isManagedEventType={isManagedEventType || isChildrenManagedEventType}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
isManagedEventType && (
|
||||||
|
<p className="!mt-2 ml-1 text-sm text-gray-600">{t("members_default_schedule_description")}</p>
|
||||||
|
)
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const UseCommonScheduleSettingsToggle = ({ eventType }: { eventType: EventTypeSetup }) => {
|
||||||
|
const { t } = useLocale();
|
||||||
|
const { resetField, setValue } = useFormContext<FormValues>();
|
||||||
|
return (
|
||||||
|
<Controller
|
||||||
|
name="metadata.config.useHostSchedulesForTeamEvent"
|
||||||
|
render={({ field: { value, onChange } }) => (
|
||||||
|
<SettingsToggle
|
||||||
|
checked={!value}
|
||||||
|
onCheckedChange={(checked) => {
|
||||||
|
onChange(!checked);
|
||||||
|
if (checked) {
|
||||||
|
resetField("schedule");
|
||||||
|
} else {
|
||||||
|
setValue("schedule", null);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
title={t("choose_common_schedule_team_event")}
|
||||||
|
description={t("choose_common_schedule_team_event_description")}>
|
||||||
|
<EventTypeSchedule eventType={eventType} />
|
||||||
|
</SettingsToggle>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const EventAvailabilityTab = ({
|
||||||
|
eventType,
|
||||||
|
isTeamEvent,
|
||||||
|
}: {
|
||||||
|
eventType: EventTypeSetup;
|
||||||
|
isTeamEvent: boolean;
|
||||||
|
}) => {
|
||||||
|
return isTeamEvent && eventType.schedulingType !== SchedulingType.MANAGED ? (
|
||||||
|
<UseCommonScheduleSettingsToggle eventType={eventType} />
|
||||||
|
) : (
|
||||||
|
<EventTypeSchedule eventType={eventType} />
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -7,24 +7,16 @@ import type { UseFormRegisterReturn } from "react-hook-form";
|
|||||||
import { Controller, useFormContext, useWatch } from "react-hook-form";
|
import { Controller, useFormContext, useWatch } from "react-hook-form";
|
||||||
import type { SingleValue } from "react-select";
|
import type { SingleValue } from "react-select";
|
||||||
|
|
||||||
|
import useLockedFieldsManager from "@calcom/features/ee/managed-event-types/hooks/useLockedFieldsManager";
|
||||||
import { classNames } from "@calcom/lib";
|
import { classNames } from "@calcom/lib";
|
||||||
import type { DurationType } from "@calcom/lib/convertToNewDurationType";
|
import type { DurationType } from "@calcom/lib/convertToNewDurationType";
|
||||||
import convertToNewDurationType from "@calcom/lib/convertToNewDurationType";
|
import convertToNewDurationType from "@calcom/lib/convertToNewDurationType";
|
||||||
import findDurationType from "@calcom/lib/findDurationType";
|
import findDurationType from "@calcom/lib/findDurationType";
|
||||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||||
import type { PeriodType } from "@calcom/prisma/client";
|
import type { PeriodType } from "@calcom/prisma/enums";
|
||||||
import type { IntervalLimit } from "@calcom/types/Calendar";
|
import type { IntervalLimit } from "@calcom/types/Calendar";
|
||||||
import {
|
import { Button, DateRangePicker, InputField, Label, Select, SettingsToggle, TextField } from "@calcom/ui";
|
||||||
Button,
|
import { Plus, Trash } from "@calcom/ui/components/icon";
|
||||||
DateRangePicker,
|
|
||||||
Input,
|
|
||||||
InputField,
|
|
||||||
Label,
|
|
||||||
Select,
|
|
||||||
SettingsToggle,
|
|
||||||
TextField,
|
|
||||||
} from "@calcom/ui";
|
|
||||||
import { FiPlus, FiTrash } from "@calcom/ui/components/icon";
|
|
||||||
|
|
||||||
const MinimumBookingNoticeInput = React.forwardRef<
|
const MinimumBookingNoticeInput = React.forwardRef<
|
||||||
HTMLInputElement,
|
HTMLInputElement,
|
||||||
@@ -78,6 +70,7 @@ const MinimumBookingNoticeInput = React.forwardRef<
|
|||||||
<div className="w-1/2 md:w-full">
|
<div className="w-1/2 md:w-full">
|
||||||
<InputField
|
<InputField
|
||||||
required
|
required
|
||||||
|
disabled={passThroughProps.disabled}
|
||||||
defaultValue={minimumBookingNoticeDisplayValues.value}
|
defaultValue={minimumBookingNoticeDisplayValues.value}
|
||||||
onChange={(e) =>
|
onChange={(e) =>
|
||||||
setMinimumBookingNoticeDisplayValues({
|
setMinimumBookingNoticeDisplayValues({
|
||||||
@@ -88,12 +81,14 @@ const MinimumBookingNoticeInput = React.forwardRef<
|
|||||||
label={t("minimum_booking_notice")}
|
label={t("minimum_booking_notice")}
|
||||||
type="number"
|
type="number"
|
||||||
placeholder="0"
|
placeholder="0"
|
||||||
|
min={0}
|
||||||
className="mb-0 h-[38px] rounded-[4px] ltr:mr-2 rtl:ml-2"
|
className="mb-0 h-[38px] rounded-[4px] ltr:mr-2 rtl:ml-2"
|
||||||
/>
|
/>
|
||||||
<input type="hidden" ref={ref} {...passThroughProps} />
|
<input type="hidden" ref={ref} {...passThroughProps} />
|
||||||
</div>
|
</div>
|
||||||
<Select
|
<Select
|
||||||
isSearchable={false}
|
isSearchable={false}
|
||||||
|
isDisabled={passThroughProps.disabled}
|
||||||
className="mb-0 ml-2 h-[38px] w-full capitalize md:min-w-[150px] md:max-w-[200px]"
|
className="mb-0 ml-2 h-[38px] w-full capitalize md:min-w-[150px] md:max-w-[200px]"
|
||||||
defaultValue={durationTypeOptions.find(
|
defaultValue={durationTypeOptions.find(
|
||||||
(option) => option.value === minimumBookingNoticeDisplayValues.type
|
(option) => option.value === minimumBookingNoticeDisplayValues.type
|
||||||
@@ -113,7 +108,7 @@ const MinimumBookingNoticeInput = React.forwardRef<
|
|||||||
});
|
});
|
||||||
|
|
||||||
export const EventLimitsTab = ({ eventType }: Pick<EventTypeSetupProps, "eventType">) => {
|
export const EventLimitsTab = ({ eventType }: Pick<EventTypeSetupProps, "eventType">) => {
|
||||||
const { t } = useLocale();
|
const { t, i18n } = useLocale();
|
||||||
const formMethods = useFormContext<FormValues>();
|
const formMethods = useFormContext<FormValues>();
|
||||||
|
|
||||||
const PERIOD_TYPES = [
|
const PERIOD_TYPES = [
|
||||||
@@ -145,12 +140,43 @@ export const EventLimitsTab = ({ eventType }: Pick<EventTypeSetupProps, "eventTy
|
|||||||
defaultValue: periodType?.type,
|
defaultValue: periodType?.type,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const { shouldLockIndicator, shouldLockDisableProps } = useLockedFieldsManager(
|
||||||
|
eventType,
|
||||||
|
t("locked_fields_admin_description"),
|
||||||
|
t("locked_fields_member_description")
|
||||||
|
);
|
||||||
|
|
||||||
|
const bookingLimitsLocked = shouldLockDisableProps("bookingLimits");
|
||||||
|
const durationLimitsLocked = shouldLockDisableProps("durationLimits");
|
||||||
|
const periodTypeLocked = shouldLockDisableProps("periodType");
|
||||||
|
const offsetStartLockedProps = shouldLockDisableProps("offsetStart");
|
||||||
|
|
||||||
|
const optionsPeriod = [
|
||||||
|
{ value: 1, label: t("calendar_days") },
|
||||||
|
{ value: 0, label: t("business_days") },
|
||||||
|
];
|
||||||
|
|
||||||
|
// offsetStart toggle is client-side only, opened by default if offsetStart is set
|
||||||
|
const offsetStartValue = useWatch({
|
||||||
|
control: formMethods.control,
|
||||||
|
name: "offsetStart",
|
||||||
|
});
|
||||||
|
const [offsetToggle, setOffsetToggle] = useState(() => offsetStartValue > 0);
|
||||||
|
|
||||||
|
// Preview how the offset will affect start times
|
||||||
|
const offsetOriginalTime = new Date();
|
||||||
|
offsetOriginalTime.setHours(9, 0, 0, 0);
|
||||||
|
const offsetAdjustedTime = new Date(offsetOriginalTime.getTime() + offsetStartValue * 60 * 1000);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="space-y-8">
|
<div className="space-y-8">
|
||||||
<div className="space-y-4 lg: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="flex flex-col space-y-4 lg:flex-row lg:space-y-0 lg:space-x-4">
|
||||||
<div className="w-full">
|
<div className="w-full">
|
||||||
<Label htmlFor="beforeBufferTime">{t("before_event")} </Label>
|
<Label htmlFor="beforeBufferTime">
|
||||||
|
{t("before_event")}
|
||||||
|
{shouldLockIndicator("bookingLimits")}
|
||||||
|
</Label>
|
||||||
<Controller
|
<Controller
|
||||||
name="beforeBufferTime"
|
name="beforeBufferTime"
|
||||||
control={formMethods.control}
|
control={formMethods.control}
|
||||||
@@ -169,6 +195,7 @@ export const EventLimitsTab = ({ eventType }: Pick<EventTypeSetupProps, "eventTy
|
|||||||
return (
|
return (
|
||||||
<Select
|
<Select
|
||||||
isSearchable={false}
|
isSearchable={false}
|
||||||
|
isDisabled={shouldLockDisableProps("bookingLimits").disabled}
|
||||||
onChange={(val) => {
|
onChange={(val) => {
|
||||||
if (val) onChange(val.value);
|
if (val) onChange(val.value);
|
||||||
}}
|
}}
|
||||||
@@ -182,7 +209,10 @@ export const EventLimitsTab = ({ eventType }: Pick<EventTypeSetupProps, "eventTy
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="w-full">
|
<div className="w-full">
|
||||||
<Label htmlFor="afterBufferTime">{t("after_event")} </Label>
|
<Label htmlFor="afterBufferTime">
|
||||||
|
{t("after_event")}
|
||||||
|
{shouldLockIndicator("bookingLimits")}
|
||||||
|
</Label>
|
||||||
<Controller
|
<Controller
|
||||||
name="afterBufferTime"
|
name="afterBufferTime"
|
||||||
control={formMethods.control}
|
control={formMethods.control}
|
||||||
@@ -201,6 +231,7 @@ export const EventLimitsTab = ({ eventType }: Pick<EventTypeSetupProps, "eventTy
|
|||||||
return (
|
return (
|
||||||
<Select
|
<Select
|
||||||
isSearchable={false}
|
isSearchable={false}
|
||||||
|
isDisabled={shouldLockDisableProps("bookingLimits").disabled}
|
||||||
onChange={(val) => {
|
onChange={(val) => {
|
||||||
if (val) onChange(val.value);
|
if (val) onChange(val.value);
|
||||||
}}
|
}}
|
||||||
@@ -216,11 +247,20 @@ export const EventLimitsTab = ({ eventType }: Pick<EventTypeSetupProps, "eventTy
|
|||||||
</div>
|
</div>
|
||||||
<div className="flex flex-col space-y-4 lg:flex-row lg:space-y-0 lg:space-x-4">
|
<div className="flex flex-col space-y-4 lg:flex-row lg:space-y-0 lg:space-x-4">
|
||||||
<div className="w-full">
|
<div className="w-full">
|
||||||
<Label htmlFor="minimumBookingNotice">{t("minimum_booking_notice")} </Label>
|
<Label htmlFor="minimumBookingNotice">
|
||||||
<MinimumBookingNoticeInput {...formMethods.register("minimumBookingNotice")} />
|
{t("minimum_booking_notice")}
|
||||||
|
{shouldLockIndicator("minimumBookingNotice")}
|
||||||
|
</Label>
|
||||||
|
<MinimumBookingNoticeInput
|
||||||
|
disabled={shouldLockDisableProps("minimumBookingNotice").disabled}
|
||||||
|
{...formMethods.register("minimumBookingNotice")}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="w-full">
|
<div className="w-full">
|
||||||
<Label htmlFor="slotInterval">{t("slot_interval")} </Label>
|
<Label htmlFor="slotInterval">
|
||||||
|
{t("slot_interval")}
|
||||||
|
{shouldLockIndicator("slotInterval")}
|
||||||
|
</Label>
|
||||||
<Controller
|
<Controller
|
||||||
name="slotInterval"
|
name="slotInterval"
|
||||||
control={formMethods.control}
|
control={formMethods.control}
|
||||||
@@ -238,6 +278,7 @@ export const EventLimitsTab = ({ eventType }: Pick<EventTypeSetupProps, "eventTy
|
|||||||
return (
|
return (
|
||||||
<Select
|
<Select
|
||||||
isSearchable={false}
|
isSearchable={false}
|
||||||
|
isDisabled={shouldLockDisableProps("slotInterval").disabled}
|
||||||
onChange={(val) => {
|
onChange={(val) => {
|
||||||
formMethods.setValue("slotInterval", val && (val.value || 0) > 0 ? val.value : null);
|
formMethods.setValue("slotInterval", val && (val.value || 0) > 0 ? val.value : null);
|
||||||
}}
|
}}
|
||||||
@@ -253,13 +294,14 @@ export const EventLimitsTab = ({ eventType }: Pick<EventTypeSetupProps, "eventTy
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<hr />
|
<hr className="border-subtle" />
|
||||||
<Controller
|
<Controller
|
||||||
name="bookingLimits"
|
name="bookingLimits"
|
||||||
control={formMethods.control}
|
control={formMethods.control}
|
||||||
render={({ field: { value } }) => (
|
render={({ field: { value } }) => (
|
||||||
<SettingsToggle
|
<SettingsToggle
|
||||||
title={t("limit_booking_frequency")}
|
title={t("limit_booking_frequency")}
|
||||||
|
{...bookingLimitsLocked}
|
||||||
description={t("limit_booking_frequency_description")}
|
description={t("limit_booking_frequency_description")}
|
||||||
checked={Object.keys(value ?? {}).length > 0}
|
checked={Object.keys(value ?? {}).length > 0}
|
||||||
onCheckedChange={(active) => {
|
onCheckedChange={(active) => {
|
||||||
@@ -271,11 +313,16 @@ export const EventLimitsTab = ({ eventType }: Pick<EventTypeSetupProps, "eventTy
|
|||||||
formMethods.setValue("bookingLimits", {});
|
formMethods.setValue("bookingLimits", {});
|
||||||
}
|
}
|
||||||
}}>
|
}}>
|
||||||
<IntervalLimitsManager propertyName="bookingLimits" defaultLimit={1} step={1} />
|
<IntervalLimitsManager
|
||||||
|
disabled={bookingLimitsLocked.disabled}
|
||||||
|
propertyName="bookingLimits"
|
||||||
|
defaultLimit={1}
|
||||||
|
step={1}
|
||||||
|
/>
|
||||||
</SettingsToggle>
|
</SettingsToggle>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
<hr />
|
<hr className="border-subtle" />
|
||||||
<Controller
|
<Controller
|
||||||
name="durationLimits"
|
name="durationLimits"
|
||||||
control={formMethods.control}
|
control={formMethods.control}
|
||||||
@@ -283,6 +330,7 @@ export const EventLimitsTab = ({ eventType }: Pick<EventTypeSetupProps, "eventTy
|
|||||||
<SettingsToggle
|
<SettingsToggle
|
||||||
title={t("limit_total_booking_duration")}
|
title={t("limit_total_booking_duration")}
|
||||||
description={t("limit_total_booking_duration_description")}
|
description={t("limit_total_booking_duration_description")}
|
||||||
|
{...durationLimitsLocked}
|
||||||
checked={Object.keys(value ?? {}).length > 0}
|
checked={Object.keys(value ?? {}).length > 0}
|
||||||
onCheckedChange={(active) => {
|
onCheckedChange={(active) => {
|
||||||
if (active) {
|
if (active) {
|
||||||
@@ -296,13 +344,14 @@ export const EventLimitsTab = ({ eventType }: Pick<EventTypeSetupProps, "eventTy
|
|||||||
<IntervalLimitsManager
|
<IntervalLimitsManager
|
||||||
propertyName="durationLimits"
|
propertyName="durationLimits"
|
||||||
defaultLimit={60}
|
defaultLimit={60}
|
||||||
|
disabled={durationLimitsLocked.disabled}
|
||||||
step={15}
|
step={15}
|
||||||
textFieldSuffix={t("minutes")}
|
textFieldSuffix={t("minutes")}
|
||||||
/>
|
/>
|
||||||
</SettingsToggle>
|
</SettingsToggle>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
<hr />
|
<hr className="border-subtle" />
|
||||||
<Controller
|
<Controller
|
||||||
name="periodType"
|
name="periodType"
|
||||||
control={formMethods.control}
|
control={formMethods.control}
|
||||||
@@ -310,49 +359,64 @@ export const EventLimitsTab = ({ eventType }: Pick<EventTypeSetupProps, "eventTy
|
|||||||
<SettingsToggle
|
<SettingsToggle
|
||||||
title={t("limit_future_bookings")}
|
title={t("limit_future_bookings")}
|
||||||
description={t("limit_future_bookings_description")}
|
description={t("limit_future_bookings_description")}
|
||||||
|
{...periodTypeLocked}
|
||||||
checked={value && value !== "UNLIMITED"}
|
checked={value && value !== "UNLIMITED"}
|
||||||
onCheckedChange={(bool) => formMethods.setValue("periodType", bool ? "ROLLING" : "UNLIMITED")}>
|
onCheckedChange={(bool) => formMethods.setValue("periodType", bool ? "ROLLING" : "UNLIMITED")}>
|
||||||
<RadioGroup.Root
|
<RadioGroup.Root
|
||||||
defaultValue={watchPeriodType}
|
defaultValue={watchPeriodType}
|
||||||
value={watchPeriodType}
|
value={watchPeriodType}
|
||||||
onValueChange={(val) => formMethods.setValue("periodType", val as PeriodType)}>
|
onValueChange={(val) => formMethods.setValue("periodType", val as PeriodType)}>
|
||||||
{PERIOD_TYPES.map((period) => {
|
{PERIOD_TYPES.filter((opt) =>
|
||||||
|
periodTypeLocked.disabled ? watchPeriodType === opt.type : true
|
||||||
|
).map((period) => {
|
||||||
if (period.type === "UNLIMITED") return null;
|
if (period.type === "UNLIMITED") return null;
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={classNames(
|
className={classNames(
|
||||||
"mb-2 flex flex-wrap items-center text-sm",
|
"text-default mb-2 flex flex-wrap items-center text-sm",
|
||||||
watchPeriodType === "UNLIMITED" && "pointer-events-none opacity-30"
|
watchPeriodType === "UNLIMITED" && "pointer-events-none opacity-30"
|
||||||
)}
|
)}
|
||||||
key={period.type}>
|
key={period.type}>
|
||||||
<RadioGroup.Item
|
{!periodTypeLocked.disabled && (
|
||||||
id={period.type}
|
<RadioGroup.Item
|
||||||
value={period.type}
|
id={period.type}
|
||||||
className="min-w-4 flex h-4 w-4 cursor-pointer items-center rounded-full border border-black bg-white focus:border-2 focus:outline-none ltr:mr-2 rtl:ml-2">
|
value={period.type}
|
||||||
<RadioGroup.Indicator className="relative flex h-4 w-4 items-center justify-center after:block after:h-2 after:w-2 after:rounded-full after:bg-black" />
|
className="min-w-4 bg-default border-default flex h-4 w-4 cursor-pointer items-center rounded-full border focus:border-2 focus:outline-none ltr:mr-2 rtl:ml-2">
|
||||||
</RadioGroup.Item>
|
<RadioGroup.Indicator className="after:bg-inverted relative flex h-4 w-4 items-center justify-center after:block after:h-2 after:w-2 after:rounded-full" />
|
||||||
|
</RadioGroup.Item>
|
||||||
|
)}
|
||||||
{period.prefix ? <span>{period.prefix} </span> : null}
|
{period.prefix ? <span>{period.prefix} </span> : null}
|
||||||
{period.type === "ROLLING" && (
|
{period.type === "ROLLING" && (
|
||||||
<div className="flex h-9">
|
<div className="flex items-center">
|
||||||
<Input
|
<TextField
|
||||||
|
labelSrOnly
|
||||||
type="number"
|
type="number"
|
||||||
className="block w-16 rounded-md border-gray-300 py-3 text-sm [appearance:textfield] ltr:mr-2 rtl:ml-2"
|
className="border-default my-0 block w-16 text-sm [appearance:textfield] ltr:mr-2 rtl:ml-2"
|
||||||
placeholder="30"
|
placeholder="30"
|
||||||
|
disabled={periodTypeLocked.disabled}
|
||||||
{...formMethods.register("periodDays", { valueAsNumber: true })}
|
{...formMethods.register("periodDays", { valueAsNumber: true })}
|
||||||
defaultValue={eventType.periodDays || 30}
|
defaultValue={eventType.periodDays || 30}
|
||||||
/>
|
/>
|
||||||
<select
|
<Select
|
||||||
id=""
|
options={optionsPeriod}
|
||||||
className="block h-9 w-full rounded-md border-gray-300 py-2 pl-3 pr-10 text-sm focus:outline-none"
|
isSearchable={false}
|
||||||
{...formMethods.register("periodCountCalendarDays")}
|
isDisabled={periodTypeLocked.disabled}
|
||||||
defaultValue={eventType.periodCountCalendarDays ? "1" : "0"}>
|
onChange={(opt) => {
|
||||||
<option value="1">{t("calendar_days")}</option>
|
formMethods.setValue(
|
||||||
<option value="0">{t("business_days")}</option>
|
"periodCountCalendarDays",
|
||||||
</select>
|
opt?.value.toString() as "0" | "1"
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
defaultValue={
|
||||||
|
optionsPeriod.find(
|
||||||
|
(opt) => opt.value === (eventType.periodCountCalendarDays ? 1 : 0)
|
||||||
|
) ?? optionsPeriod[0]
|
||||||
|
}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{period.type === "RANGE" && (
|
{period.type === "RANGE" && (
|
||||||
<div className="inline-flex space-x-2 ltr:ml-2 ltr:mr-2 rtl:ml-2 rtl:space-x-reverse">
|
<div className="ms-2 me-2 inline-flex space-x-2 rtl:space-x-reverse">
|
||||||
<Controller
|
<Controller
|
||||||
name="periodDates"
|
name="periodDates"
|
||||||
control={formMethods.control}
|
control={formMethods.control}
|
||||||
@@ -361,6 +425,7 @@ export const EventLimitsTab = ({ eventType }: Pick<EventTypeSetupProps, "eventTy
|
|||||||
<DateRangePicker
|
<DateRangePicker
|
||||||
startDate={formMethods.getValues("periodDates").startDate}
|
startDate={formMethods.getValues("periodDates").startDate}
|
||||||
endDate={formMethods.getValues("periodDates").endDate}
|
endDate={formMethods.getValues("periodDates").endDate}
|
||||||
|
disabled={periodTypeLocked.disabled}
|
||||||
onDatesChange={({ startDate, endDate }) => {
|
onDatesChange={({ startDate, endDate }) => {
|
||||||
formMethods.setValue("periodDates", {
|
formMethods.setValue("periodDates", {
|
||||||
startDate,
|
startDate,
|
||||||
@@ -372,9 +437,7 @@ export const EventLimitsTab = ({ eventType }: Pick<EventTypeSetupProps, "eventTy
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{period.suffix ? (
|
{period.suffix ? <span className="ms-2 me-2"> {period.suffix}</span> : null}
|
||||||
<span className="ltr:ml-2 ltr:mr-2 rtl:ml-2"> {period.suffix}</span>
|
|
||||||
) : null}
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
@@ -382,6 +445,32 @@ export const EventLimitsTab = ({ eventType }: Pick<EventTypeSetupProps, "eventTy
|
|||||||
</SettingsToggle>
|
</SettingsToggle>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
|
<hr className="border-subtle" />
|
||||||
|
<SettingsToggle
|
||||||
|
title={t("offset_toggle")}
|
||||||
|
description={t("offset_toggle_description")}
|
||||||
|
{...offsetStartLockedProps}
|
||||||
|
checked={offsetToggle}
|
||||||
|
onCheckedChange={(active) => {
|
||||||
|
setOffsetToggle(active);
|
||||||
|
if (!active) {
|
||||||
|
formMethods.setValue("offsetStart", 0);
|
||||||
|
}
|
||||||
|
}}>
|
||||||
|
<TextField
|
||||||
|
required
|
||||||
|
type="number"
|
||||||
|
{...offsetStartLockedProps}
|
||||||
|
label={t("offset_start")}
|
||||||
|
defaultValue={eventType.offsetStart}
|
||||||
|
{...formMethods.register("offsetStart")}
|
||||||
|
addOnSuffix={<>{t("minutes")}</>}
|
||||||
|
hint={t("offset_start_description", {
|
||||||
|
originalTime: offsetOriginalTime.toLocaleTimeString(i18n.language, { timeStyle: "short" }),
|
||||||
|
adjustedTime: offsetAdjustedTime.toLocaleTimeString(i18n.language, { timeStyle: "short" }),
|
||||||
|
})}
|
||||||
|
/>
|
||||||
|
</SettingsToggle>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
@@ -401,6 +490,7 @@ type IntervalLimitItemProps = {
|
|||||||
step: number;
|
step: number;
|
||||||
value: number;
|
value: number;
|
||||||
textFieldSuffix?: string;
|
textFieldSuffix?: string;
|
||||||
|
disabled?: boolean;
|
||||||
selectOptions: { value: keyof IntervalLimit; label: string }[];
|
selectOptions: { value: keyof IntervalLimit; label: string }[];
|
||||||
hasDeleteButton?: boolean;
|
hasDeleteButton?: boolean;
|
||||||
onDelete: (intervalLimitsKey: IntervalLimitsKey) => void;
|
onDelete: (intervalLimitsKey: IntervalLimitsKey) => void;
|
||||||
@@ -415,6 +505,7 @@ const IntervalLimitItem = ({
|
|||||||
textFieldSuffix,
|
textFieldSuffix,
|
||||||
selectOptions,
|
selectOptions,
|
||||||
hasDeleteButton,
|
hasDeleteButton,
|
||||||
|
disabled,
|
||||||
onDelete,
|
onDelete,
|
||||||
onLimitChange,
|
onLimitChange,
|
||||||
onIntervalSelect,
|
onIntervalSelect,
|
||||||
@@ -425,7 +516,9 @@ const IntervalLimitItem = ({
|
|||||||
required
|
required
|
||||||
type="number"
|
type="number"
|
||||||
containerClassName={textFieldSuffix ? "w-44 -mb-1" : "w-16 mb-0"}
|
containerClassName={textFieldSuffix ? "w-44 -mb-1" : "w-16 mb-0"}
|
||||||
|
className="mb-0 !h-auto"
|
||||||
placeholder={`${value}`}
|
placeholder={`${value}`}
|
||||||
|
disabled={disabled}
|
||||||
min={step}
|
min={step}
|
||||||
step={step}
|
step={step}
|
||||||
defaultValue={value}
|
defaultValue={value}
|
||||||
@@ -435,11 +528,12 @@ const IntervalLimitItem = ({
|
|||||||
<Select
|
<Select
|
||||||
options={selectOptions}
|
options={selectOptions}
|
||||||
isSearchable={false}
|
isSearchable={false}
|
||||||
|
isDisabled={disabled}
|
||||||
defaultValue={INTERVAL_LIMIT_OPTIONS.find((option) => option.value === limitKey)}
|
defaultValue={INTERVAL_LIMIT_OPTIONS.find((option) => option.value === limitKey)}
|
||||||
onChange={onIntervalSelect}
|
onChange={onIntervalSelect}
|
||||||
/>
|
/>
|
||||||
{hasDeleteButton && (
|
{hasDeleteButton && !disabled && (
|
||||||
<Button variant="icon" StartIcon={FiTrash} color="destructive" onClick={() => onDelete(limitKey)} />
|
<Button variant="icon" StartIcon={Trash} color="destructive" onClick={() => onDelete(limitKey)} />
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
@@ -450,6 +544,7 @@ type IntervalLimitsManagerProps<K extends "durationLimits" | "bookingLimits"> =
|
|||||||
defaultLimit: number;
|
defaultLimit: number;
|
||||||
step: number;
|
step: number;
|
||||||
textFieldSuffix?: string;
|
textFieldSuffix?: string;
|
||||||
|
disabled?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
const IntervalLimitsManager = <K extends "durationLimits" | "bookingLimits">({
|
const IntervalLimitsManager = <K extends "durationLimits" | "bookingLimits">({
|
||||||
@@ -457,6 +552,7 @@ const IntervalLimitsManager = <K extends "durationLimits" | "bookingLimits">({
|
|||||||
defaultLimit,
|
defaultLimit,
|
||||||
step,
|
step,
|
||||||
textFieldSuffix,
|
textFieldSuffix,
|
||||||
|
disabled,
|
||||||
}: IntervalLimitsManagerProps<K>) => {
|
}: IntervalLimitsManagerProps<K>) => {
|
||||||
const { watch, setValue, control } = useFormContext<FormValues>();
|
const { watch, setValue, control } = useFormContext<FormValues>();
|
||||||
const watchIntervalLimits = watch(propertyName);
|
const watchIntervalLimits = watch(propertyName);
|
||||||
@@ -506,6 +602,7 @@ const IntervalLimitsManager = <K extends "durationLimits" | "bookingLimits">({
|
|||||||
limitKey={limitKey}
|
limitKey={limitKey}
|
||||||
step={step}
|
step={step}
|
||||||
value={value}
|
value={value}
|
||||||
|
disabled={disabled}
|
||||||
textFieldSuffix={textFieldSuffix}
|
textFieldSuffix={textFieldSuffix}
|
||||||
hasDeleteButton={Object.keys(currentIntervalLimits).length > 1}
|
hasDeleteButton={Object.keys(currentIntervalLimits).length > 1}
|
||||||
selectOptions={INTERVAL_LIMIT_OPTIONS.filter(
|
selectOptions={INTERVAL_LIMIT_OPTIONS.filter(
|
||||||
@@ -536,8 +633,8 @@ const IntervalLimitsManager = <K extends "durationLimits" | "bookingLimits">({
|
|||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
{currentIntervalLimits && Object.keys(currentIntervalLimits).length <= 3 && (
|
{currentIntervalLimits && Object.keys(currentIntervalLimits).length <= 3 && !disabled && (
|
||||||
<Button color="minimal" StartIcon={FiPlus} onClick={addLimit}>
|
<Button color="minimal" StartIcon={Plus} onClick={addLimit}>
|
||||||
{t("add_limit")}
|
{t("add_limit")}
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ export const EventRecurringTab = ({ eventType }: Pick<EventTypeSetupProps, "even
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="">
|
<div className="">
|
||||||
<RecurringEventController paymentEnabled={requirePayment} recurringEvent={eventType.recurringEvent} />
|
<RecurringEventController paymentEnabled={requirePayment} eventType={eventType} />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
@@ -4,20 +4,32 @@ import { isValidPhoneNumber } from "libphonenumber-js";
|
|||||||
import { Trans } from "next-i18next";
|
import { Trans } from "next-i18next";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import type { EventTypeSetupProps, FormValues } from "pages/event-types/[type]";
|
import type { EventTypeSetupProps, FormValues } from "pages/event-types/[type]";
|
||||||
import { useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import { Controller, useForm, useFormContext } from "react-hook-form";
|
import { Controller, useForm, useFormContext } from "react-hook-form";
|
||||||
import type { MultiValue } from "react-select";
|
import type { MultiValue } from "react-select";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
|
||||||
import type { EventLocationType } from "@calcom/app-store/locations";
|
import type { EventLocationType } from "@calcom/app-store/locations";
|
||||||
import { getEventLocationType, MeetLocationType, LocationType } from "@calcom/app-store/locations";
|
import { getEventLocationType, MeetLocationType, LocationType } from "@calcom/app-store/locations";
|
||||||
|
import useLockedFieldsManager from "@calcom/features/ee/managed-event-types/hooks/useLockedFieldsManager";
|
||||||
|
import cx from "@calcom/lib/classNames";
|
||||||
import { CAL_URL } from "@calcom/lib/constants";
|
import { CAL_URL } from "@calcom/lib/constants";
|
||||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||||
import { md } from "@calcom/lib/markdownIt";
|
import { md } from "@calcom/lib/markdownIt";
|
||||||
import { slugify } from "@calcom/lib/slugify";
|
import { slugify } from "@calcom/lib/slugify";
|
||||||
import turndown from "@calcom/lib/turndownService";
|
import turndown from "@calcom/lib/turndownService";
|
||||||
import { Button, Editor, Label, Select, SettingsToggle, Skeleton, TextField } from "@calcom/ui";
|
import {
|
||||||
import { FiEdit2, FiCheck, FiX, FiPlus } from "@calcom/ui/components/icon";
|
Button,
|
||||||
|
Label,
|
||||||
|
Select,
|
||||||
|
SettingsToggle,
|
||||||
|
Skeleton,
|
||||||
|
TextField,
|
||||||
|
Editor,
|
||||||
|
SkeletonContainer,
|
||||||
|
SkeletonText,
|
||||||
|
} from "@calcom/ui";
|
||||||
|
import { Edit2, Check, X, Plus } from "@calcom/ui/components/icon";
|
||||||
|
|
||||||
import { EditLocationDialog } from "@components/dialog/EditLocationDialog";
|
import { EditLocationDialog } from "@components/dialog/EditLocationDialog";
|
||||||
import type { SingleValueLocationOption, LocationOption } from "@components/ui/form/LocationSelect";
|
import type { SingleValueLocationOption, LocationOption } from "@components/ui/form/LocationSelect";
|
||||||
@@ -35,14 +47,60 @@ const getLocationFromType = (
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const getDefaultLocationValue = (options: EventTypeSetupProps["locationOptions"], type: string) => {
|
const getLocationInfo = (props: Pick<EventTypeSetupProps, "eventType" | "locationOptions">) => {
|
||||||
for (const locationType of options) {
|
const locationAvailable =
|
||||||
for (const location of locationType.options) {
|
props.eventType.locations &&
|
||||||
if (location.value === type && location.disabled === false) {
|
props.eventType.locations.length > 0 &&
|
||||||
return location;
|
props.locationOptions.some((op) =>
|
||||||
}
|
op.options.find((opt) => opt.value === props.eventType.locations[0].type)
|
||||||
}
|
);
|
||||||
}
|
const locationDetails = props.eventType.locations &&
|
||||||
|
props.eventType.locations.length > 0 &&
|
||||||
|
!locationAvailable && {
|
||||||
|
slug: props.eventType.locations[0].type
|
||||||
|
.replace("integrations:", "")
|
||||||
|
.replace(":", "-")
|
||||||
|
.replace("_video", ""),
|
||||||
|
name: props.eventType.locations[0].type
|
||||||
|
.replace("integrations:", "")
|
||||||
|
.replace(":", " ")
|
||||||
|
.replace("_video", "")
|
||||||
|
.split(" ")
|
||||||
|
.map((word) => word[0].toUpperCase() + word.slice(1))
|
||||||
|
.join(" "),
|
||||||
|
};
|
||||||
|
return { locationAvailable, locationDetails };
|
||||||
|
};
|
||||||
|
interface DescriptionEditorProps {
|
||||||
|
description?: string | null;
|
||||||
|
editable?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
const DescriptionEditor = (props: DescriptionEditorProps) => {
|
||||||
|
const formMethods = useFormContext<FormValues>();
|
||||||
|
const [mounted, setIsMounted] = useState(false);
|
||||||
|
const { t } = useLocale();
|
||||||
|
const { description } = props;
|
||||||
|
const [firstRender, setFirstRender] = useState(true);
|
||||||
|
useEffect(() => {
|
||||||
|
setIsMounted(true);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return mounted ? (
|
||||||
|
<Editor
|
||||||
|
getText={() => md.render(formMethods.getValues("description") || description || "")}
|
||||||
|
setText={(value: string) => formMethods.setValue("description", turndown(value))}
|
||||||
|
excludedToolbarItems={["blockType"]}
|
||||||
|
placeholder={t("quick_video_meeting")}
|
||||||
|
editable={props.editable}
|
||||||
|
firstRender={firstRender}
|
||||||
|
setFirstRender={setFirstRender}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<SkeletonContainer>
|
||||||
|
<SkeletonText className="block h-24 w-full" />
|
||||||
|
</SkeletonContainer>
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const EventSetupTab = (
|
export const EventSetupTab = (
|
||||||
@@ -53,12 +111,16 @@ export const EventSetupTab = (
|
|||||||
) => {
|
) => {
|
||||||
const { t } = useLocale();
|
const { t } = useLocale();
|
||||||
const formMethods = useFormContext<FormValues>();
|
const formMethods = useFormContext<FormValues>();
|
||||||
const { eventType, locationOptions, team, destinationCalendar } = props;
|
const { eventType, team, destinationCalendar } = props;
|
||||||
const [showLocationModal, setShowLocationModal] = useState(false);
|
const [showLocationModal, setShowLocationModal] = useState(false);
|
||||||
const [editingLocationType, setEditingLocationType] = useState<string>("");
|
const [editingLocationType, setEditingLocationType] = useState<string>("");
|
||||||
const [selectedLocation, setSelectedLocation] = useState<LocationOption | undefined>(undefined);
|
const [selectedLocation, setSelectedLocation] = useState<LocationOption | undefined>(undefined);
|
||||||
const [multipleDuration, setMultipleDuration] = useState(eventType.metadata.multipleDuration);
|
const [multipleDuration, setMultipleDuration] = useState(eventType.metadata.multipleDuration);
|
||||||
|
|
||||||
|
const locationOptions = props.locationOptions.filter((option) => {
|
||||||
|
return !team ? option.label !== "Conferencing" : true;
|
||||||
|
});
|
||||||
|
|
||||||
const multipleDurationOptions = [5, 10, 15, 20, 25, 30, 45, 50, 60, 75, 80, 90, 120, 180].map((mins) => ({
|
const multipleDurationOptions = [5, 10, 15, 20, 25, 30, 45, 50, 60, 75, 80, 90, 120, 180].map((mins) => ({
|
||||||
value: mins,
|
value: mins,
|
||||||
label: t("multiple_duration_mins", { count: mins }),
|
label: t("multiple_duration_mins", { count: mins }),
|
||||||
@@ -143,6 +205,13 @@ export const EventSetupTab = (
|
|||||||
resolver: zodResolver(locationFormSchema),
|
resolver: zodResolver(locationFormSchema),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const { isChildrenManagedEventType, isManagedEventType, shouldLockIndicator, shouldLockDisableProps } =
|
||||||
|
useLockedFieldsManager(
|
||||||
|
eventType,
|
||||||
|
t("locked_fields_admin_description"),
|
||||||
|
t("locked_fields_member_description")
|
||||||
|
);
|
||||||
|
|
||||||
const Locations = () => {
|
const Locations = () => {
|
||||||
const { t } = useLocale();
|
const { t } = useLocale();
|
||||||
|
|
||||||
@@ -157,6 +226,12 @@ export const EventSetupTab = (
|
|||||||
return true;
|
return true;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const defaultValue = isManagedEventType
|
||||||
|
? locationOptions.find((op) => op.label === t("default"))?.options[0]
|
||||||
|
: undefined;
|
||||||
|
|
||||||
|
const { locationDetails, locationAvailable } = getLocationInfo(props);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="w-full">
|
<div className="w-full">
|
||||||
{validLocations.length === 0 && (
|
{validLocations.length === 0 && (
|
||||||
@@ -164,6 +239,8 @@ export const EventSetupTab = (
|
|||||||
<LocationSelect
|
<LocationSelect
|
||||||
placeholder={t("select")}
|
placeholder={t("select")}
|
||||||
options={locationOptions}
|
options={locationOptions}
|
||||||
|
isDisabled={shouldLockDisableProps("locations").disabled}
|
||||||
|
defaultValue={defaultValue}
|
||||||
isSearchable={false}
|
isSearchable={false}
|
||||||
className="block w-full min-w-0 flex-1 rounded-sm text-sm"
|
className="block w-full min-w-0 flex-1 rounded-sm text-sm"
|
||||||
menuPlacement="auto"
|
menuPlacement="auto"
|
||||||
@@ -198,24 +275,22 @@ export const EventSetupTab = (
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<li
|
<li
|
||||||
onClick={() => {
|
|
||||||
locationFormMethods.setValue("locationType", location.type);
|
|
||||||
locationFormMethods.unregister("locationLink");
|
|
||||||
locationFormMethods.unregister("locationAddress");
|
|
||||||
locationFormMethods.unregister("locationPhoneNumber");
|
|
||||||
setEditingLocationType(location.type);
|
|
||||||
openLocationModal(location.type);
|
|
||||||
}}
|
|
||||||
key={`${location.type}${index}`}
|
key={`${location.type}${index}`}
|
||||||
className="mb-2 rounded-md border border-gray-300 py-1.5 px-2 hover:cursor-pointer">
|
className="border-default text-default mb-2 h-9 rounded-md border py-1.5 px-2 hover:cursor-pointer">
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<div className="flex items-center">
|
<div className="flex items-center">
|
||||||
<img
|
<img
|
||||||
src={eventLocationType.iconUrl}
|
src={eventLocationType.iconUrl}
|
||||||
className="h-4 w-4"
|
className={cx(
|
||||||
|
"h-4 w-4",
|
||||||
|
// invert all the icons except app icons
|
||||||
|
eventLocationType.iconUrl &&
|
||||||
|
!eventLocationType.iconUrl.startsWith("/app-store") &&
|
||||||
|
"dark:invert"
|
||||||
|
)}
|
||||||
alt={`${eventLocationType.label} logo`}
|
alt={`${eventLocationType.label} logo`}
|
||||||
/>
|
/>
|
||||||
<span className="line-clamp-1 text-sm ltr:ml-1 rtl:mr-1">{eventLabel}</span>
|
<span className="line-clamp-1 ms-1 text-sm">{eventLabel}</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex">
|
<div className="flex">
|
||||||
<button
|
<button
|
||||||
@@ -229,11 +304,11 @@ export const EventSetupTab = (
|
|||||||
openLocationModal(location.type);
|
openLocationModal(location.type);
|
||||||
}}
|
}}
|
||||||
aria-label={t("edit")}
|
aria-label={t("edit")}
|
||||||
className="mr-1 p-1 text-gray-500 hover:text-gray-900">
|
className="hover:text-emphasis text-subtle mr-1 p-1">
|
||||||
<FiEdit2 className="h-4 w-4" />
|
<Edit2 className="h-4 w-4" />
|
||||||
</button>
|
</button>
|
||||||
<button type="button" onClick={() => removeLocation(location)} aria-label={t("remove")}>
|
<button type="button" onClick={() => removeLocation(location)} aria-label={t("remove")}>
|
||||||
<FiX className="border-l-1 h-6 w-6 pl-1 text-gray-500 hover:text-gray-900 " />
|
<X className="border-l-1 hover:text-emphasis text-subtle h-6 w-6 pl-1 " />
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -244,8 +319,8 @@ export const EventSetupTab = (
|
|||||||
(location) =>
|
(location) =>
|
||||||
location.type === MeetLocationType && destinationCalendar?.integration !== "google_calendar"
|
location.type === MeetLocationType && destinationCalendar?.integration !== "google_calendar"
|
||||||
) && (
|
) && (
|
||||||
<div className="flex text-sm text-gray-600">
|
<div className="text-default flex text-sm">
|
||||||
<FiCheck className="mt-0.5 mr-1.5 h-2 w-2.5" />
|
<Check className="mt-0.5 mr-1.5 h-2 w-2.5" />
|
||||||
<Trans i18nKey="event_type_requres_google_cal">
|
<Trans i18nKey="event_type_requres_google_cal">
|
||||||
<p>
|
<p>
|
||||||
The “Add to calendar” for this event type needs to be a Google Calendar for Meet to work.
|
The “Add to calendar” for this event type needs to be a Google Calendar for Meet to work.
|
||||||
@@ -259,11 +334,19 @@ export const EventSetupTab = (
|
|||||||
</Trans>
|
</Trans>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{validLocations.length > 0 && (
|
{isChildrenManagedEventType && !locationAvailable && locationDetails && (
|
||||||
|
<p className="pl-1 text-sm leading-none text-red-600">
|
||||||
|
{t("app_not_connected", { appName: locationDetails.name })}{" "}
|
||||||
|
<a className="underline" href={`${CAL_URL}/apps/${locationDetails.slug}`}>
|
||||||
|
{t("connect_now")}
|
||||||
|
</a>
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
{validLocations.length > 0 && !isManagedEventType && !isChildrenManagedEventType && (
|
||||||
<li>
|
<li>
|
||||||
<Button
|
<Button
|
||||||
data-testid="add-location"
|
data-testid="add-location"
|
||||||
StartIcon={FiPlus}
|
StartIcon={Plus}
|
||||||
color="minimal"
|
color="minimal"
|
||||||
onClick={() => setShowLocationModal(true)}>
|
onClick={() => setShowLocationModal(true)}>
|
||||||
{t("add_location")}
|
{t("add_location")}
|
||||||
@@ -276,32 +359,43 @@ export const EventSetupTab = (
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const lengthLockedProps = shouldLockDisableProps("length");
|
||||||
|
const descriptionLockedProps = shouldLockDisableProps("description");
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<div className="space-y-8">
|
<div className="space-y-8">
|
||||||
<TextField
|
<TextField
|
||||||
required
|
required
|
||||||
label={t("title")}
|
label={t("title")}
|
||||||
|
{...shouldLockDisableProps("title")}
|
||||||
defaultValue={eventType.title}
|
defaultValue={eventType.title}
|
||||||
{...formMethods.register("title")}
|
{...formMethods.register("title")}
|
||||||
/>
|
/>
|
||||||
<div>
|
<div>
|
||||||
<Label>{t("description")}</Label>
|
<Label>
|
||||||
<Editor
|
{t("description")}
|
||||||
getText={() => md.render(formMethods.getValues("description") || eventType.description || "")}
|
{shouldLockIndicator("description")}
|
||||||
setText={(value: string) => formMethods.setValue("description", turndown(value))}
|
</Label>
|
||||||
excludedToolbarItems={["blockType"]}
|
<DescriptionEditor
|
||||||
placeholder={t("quick_video_meeting")}
|
description={eventType?.description}
|
||||||
|
editable={!descriptionLockedProps.disabled}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<TextField
|
<TextField
|
||||||
required
|
required
|
||||||
label={t("URL")}
|
label={t("URL")}
|
||||||
|
{...shouldLockDisableProps("slug")}
|
||||||
defaultValue={eventType.slug}
|
defaultValue={eventType.slug}
|
||||||
addOnLeading={
|
addOnLeading={
|
||||||
<>
|
<>
|
||||||
{CAL_URL?.replace(/^(https?:|)\/\//, "")}/
|
{CAL_URL?.replace(/^(https?:|)\/\//, "")}/
|
||||||
{team ? "team/" + team.slug : eventType.users[0].username}/
|
{!isManagedEventType
|
||||||
|
? team
|
||||||
|
? "team/" + team.slug
|
||||||
|
: eventType.users[0].username
|
||||||
|
: t("username_placeholder")}
|
||||||
|
/
|
||||||
</>
|
</>
|
||||||
}
|
}
|
||||||
{...formMethods.register("slug", {
|
{...formMethods.register("slug", {
|
||||||
@@ -349,12 +443,14 @@ export const EventSetupTab = (
|
|||||||
<div>
|
<div>
|
||||||
<Skeleton as={Label} loadingClassName="w-16">
|
<Skeleton as={Label} loadingClassName="w-16">
|
||||||
{t("default_duration")}
|
{t("default_duration")}
|
||||||
|
{shouldLockIndicator("length")}
|
||||||
</Skeleton>
|
</Skeleton>
|
||||||
<Select
|
<Select
|
||||||
value={defaultDuration}
|
value={defaultDuration}
|
||||||
isSearchable={false}
|
isSearchable={false}
|
||||||
name="length"
|
name="length"
|
||||||
className="text-sm"
|
className="text-sm"
|
||||||
|
isDisabled={lengthLockedProps.disabled}
|
||||||
noOptionsMessage={() => t("default_duration_no_options")}
|
noOptionsMessage={() => t("default_duration_no_options")}
|
||||||
options={selectedMultipleDuration}
|
options={selectedMultipleDuration}
|
||||||
onChange={(option) => {
|
onChange={(option) => {
|
||||||
@@ -370,32 +466,37 @@ export const EventSetupTab = (
|
|||||||
<TextField
|
<TextField
|
||||||
required
|
required
|
||||||
type="number"
|
type="number"
|
||||||
|
{...lengthLockedProps}
|
||||||
label={t("duration")}
|
label={t("duration")}
|
||||||
defaultValue={eventType.length ?? 15}
|
defaultValue={eventType.length ?? 15}
|
||||||
{...formMethods.register("length")}
|
{...formMethods.register("length")}
|
||||||
addOnSuffix={<>{t("minutes")}</>}
|
addOnSuffix={<>{t("minutes")}</>}
|
||||||
|
min={1}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
<div className="!mt-4 [&_label]:my-1 [&_label]:font-normal">
|
{!lengthLockedProps.disabled && (
|
||||||
<SettingsToggle
|
<div className="!mt-4 [&_label]:my-1 [&_label]:font-normal">
|
||||||
title={t("allow_booker_to_select_duration")}
|
<SettingsToggle
|
||||||
checked={multipleDuration !== undefined}
|
title={t("allow_booker_to_select_duration")}
|
||||||
onCheckedChange={() => {
|
checked={multipleDuration !== undefined}
|
||||||
if (multipleDuration !== undefined) {
|
onCheckedChange={() => {
|
||||||
setMultipleDuration(undefined);
|
if (multipleDuration !== undefined) {
|
||||||
formMethods.setValue("metadata.multipleDuration", undefined);
|
setMultipleDuration(undefined);
|
||||||
formMethods.setValue("length", eventType.length);
|
formMethods.setValue("metadata.multipleDuration", undefined);
|
||||||
} else {
|
formMethods.setValue("length", eventType.length);
|
||||||
setMultipleDuration([]);
|
} else {
|
||||||
formMethods.setValue("metadata.multipleDuration", []);
|
setMultipleDuration([]);
|
||||||
formMethods.setValue("length", 0);
|
formMethods.setValue("metadata.multipleDuration", []);
|
||||||
}
|
formMethods.setValue("length", 0);
|
||||||
}}
|
}
|
||||||
/>
|
}}
|
||||||
</div>
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
<div>
|
<div>
|
||||||
<Skeleton as={Label} loadingClassName="w-16">
|
<Skeleton as={Label} loadingClassName="w-16">
|
||||||
{t("location")}
|
{t("location")}
|
||||||
|
{shouldLockIndicator("locations")}
|
||||||
</Skeleton>
|
</Skeleton>
|
||||||
|
|
||||||
<Controller
|
<Controller
|
||||||
@@ -409,6 +510,7 @@ export const EventSetupTab = (
|
|||||||
|
|
||||||
{/* We portal this modal so we can submit the form inside. Otherwise we get issues submitting two forms at once */}
|
{/* We portal this modal so we can submit the form inside. Otherwise we get issues submitting two forms at once */}
|
||||||
<EditLocationDialog
|
<EditLocationDialog
|
||||||
|
isTeamEvent={!!team}
|
||||||
isOpenDialog={showLocationModal}
|
isOpenDialog={showLocationModal}
|
||||||
setShowLocationModal={setShowLocationModal}
|
setShowLocationModal={setShowLocationModal}
|
||||||
saveLocation={saveLocation}
|
saveLocation={saveLocation}
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
import type { SchedulingType } from "@prisma/client";
|
|
||||||
import type { EventTypeSetupProps, FormValues } from "pages/event-types/[type]";
|
import type { EventTypeSetupProps, FormValues } from "pages/event-types/[type]";
|
||||||
import { useEffect, useRef } from "react";
|
import { useEffect, useRef } from "react";
|
||||||
import type { ComponentProps } from "react";
|
import type { ComponentProps } from "react";
|
||||||
@@ -7,24 +6,47 @@ import type { Options } from "react-select";
|
|||||||
|
|
||||||
import type { CheckedSelectOption } from "@calcom/features/eventtypes/components/CheckedTeamSelect";
|
import type { CheckedSelectOption } from "@calcom/features/eventtypes/components/CheckedTeamSelect";
|
||||||
import CheckedTeamSelect from "@calcom/features/eventtypes/components/CheckedTeamSelect";
|
import CheckedTeamSelect from "@calcom/features/eventtypes/components/CheckedTeamSelect";
|
||||||
|
import ChildrenEventTypeSelect from "@calcom/features/eventtypes/components/ChildrenEventTypeSelect";
|
||||||
import { WEBAPP_URL } from "@calcom/lib/constants";
|
import { WEBAPP_URL } from "@calcom/lib/constants";
|
||||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||||
|
import { SchedulingType } from "@calcom/prisma/enums";
|
||||||
import { Label, Select } from "@calcom/ui";
|
import { Label, Select } from "@calcom/ui";
|
||||||
|
|
||||||
interface IMemberToValue {
|
interface IUserToValue {
|
||||||
id: number | null;
|
id: number | null;
|
||||||
name: string | null;
|
name: string | null;
|
||||||
username: string | null;
|
username: string | null;
|
||||||
email: string;
|
email: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
const mapUserToValue = ({ id, name, username, email }: IMemberToValue) => ({
|
const mapUserToValue = ({ id, name, username, email }: IUserToValue) => ({
|
||||||
value: `${id || ""}`,
|
value: `${id || ""}`,
|
||||||
label: `${name || ""}`,
|
label: `${name || ""}`,
|
||||||
avatar: `${WEBAPP_URL}/${username}/avatar.png`,
|
avatar: `${WEBAPP_URL}/${username}/avatar.png`,
|
||||||
email,
|
email,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
export const mapMemberToChildrenOption = (
|
||||||
|
member: EventTypeSetupProps["teamMembers"][number],
|
||||||
|
slug: string
|
||||||
|
) => {
|
||||||
|
return {
|
||||||
|
slug,
|
||||||
|
hidden: false,
|
||||||
|
created: false,
|
||||||
|
owner: {
|
||||||
|
id: member.id,
|
||||||
|
name: member.name ?? "",
|
||||||
|
email: member.email,
|
||||||
|
username: member.username ?? "",
|
||||||
|
membership: member.membership,
|
||||||
|
eventTypeSlugs: member.eventTypes ?? [],
|
||||||
|
},
|
||||||
|
value: `${member.id ?? ""}`,
|
||||||
|
label: member.name ?? "",
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
const sortByLabel = (a: ReturnType<typeof mapUserToValue>, b: ReturnType<typeof mapUserToValue>) => {
|
const sortByLabel = (a: ReturnType<typeof mapUserToValue>, b: ReturnType<typeof mapUserToValue>) => {
|
||||||
if (a.label < b.label) {
|
if (a.label < b.label) {
|
||||||
return -1;
|
return -1;
|
||||||
@@ -35,6 +57,40 @@ const sortByLabel = (a: ReturnType<typeof mapUserToValue>, b: ReturnType<typeof
|
|||||||
return 0;
|
return 0;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const ChildrenEventTypesList = ({
|
||||||
|
options = [],
|
||||||
|
value,
|
||||||
|
onChange,
|
||||||
|
...rest
|
||||||
|
}: {
|
||||||
|
value: ReturnType<typeof mapMemberToChildrenOption>[];
|
||||||
|
onChange?: (options: ReturnType<typeof mapMemberToChildrenOption>[]) => void;
|
||||||
|
options?: Options<ReturnType<typeof mapMemberToChildrenOption>>;
|
||||||
|
} & Omit<Partial<ComponentProps<typeof ChildrenEventTypeSelect>>, "onChange" | "value">) => {
|
||||||
|
const { t } = useLocale();
|
||||||
|
return (
|
||||||
|
<div className="flex flex-col space-y-5">
|
||||||
|
<div>
|
||||||
|
<Label>{t("assign_to")}</Label>
|
||||||
|
<ChildrenEventTypeSelect
|
||||||
|
onChange={(options) => {
|
||||||
|
onChange &&
|
||||||
|
onChange(
|
||||||
|
options.map((option) => ({
|
||||||
|
...option,
|
||||||
|
}))
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
value={value}
|
||||||
|
options={options.filter((opt) => !value.find((val) => val.owner.id.toString() === opt.value))}
|
||||||
|
controlShouldRenderValue={false}
|
||||||
|
{...rest}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
const CheckedHostField = ({
|
const CheckedHostField = ({
|
||||||
labelText,
|
labelText,
|
||||||
placeholder,
|
placeholder,
|
||||||
@@ -52,7 +108,7 @@ const CheckedHostField = ({
|
|||||||
options?: Options<CheckedSelectOption>;
|
options?: Options<CheckedSelectOption>;
|
||||||
} & Omit<Partial<ComponentProps<typeof CheckedTeamSelect>>, "onChange" | "value">) => {
|
} & Omit<Partial<ComponentProps<typeof CheckedTeamSelect>>, "onChange" | "value">) => {
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col space-y-5 bg-gray-50 p-4">
|
<div className="bg-muted flex flex-col space-y-5 p-4">
|
||||||
<div>
|
<div>
|
||||||
<Label>{labelText}</Label>
|
<Label>{labelText}</Label>
|
||||||
<CheckedTeamSelect
|
<CheckedTeamSelect
|
||||||
@@ -123,6 +179,21 @@ const RoundRobinHosts = ({
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const ChildrenEventTypes = ({
|
||||||
|
childrenEventTypeOptions,
|
||||||
|
}: {
|
||||||
|
childrenEventTypeOptions: ReturnType<typeof mapMemberToChildrenOption>[];
|
||||||
|
}) => {
|
||||||
|
return (
|
||||||
|
<Controller<FormValues>
|
||||||
|
name="children"
|
||||||
|
render={({ field: { onChange, value } }) => (
|
||||||
|
<ChildrenEventTypesList value={value} options={childrenEventTypeOptions} onChange={onChange} />
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
const Hosts = ({
|
const Hosts = ({
|
||||||
teamMembers,
|
teamMembers,
|
||||||
}: {
|
}: {
|
||||||
@@ -189,6 +260,7 @@ const Hosts = ({
|
|||||||
/>*/}
|
/>*/}
|
||||||
</>
|
</>
|
||||||
),
|
),
|
||||||
|
MANAGED: <></>,
|
||||||
};
|
};
|
||||||
return !!schedulingType ? schedulingTypeRender[schedulingType] : <></>;
|
return !!schedulingType ? schedulingTypeRender[schedulingType] : <></>;
|
||||||
}}
|
}}
|
||||||
@@ -196,7 +268,11 @@ const Hosts = ({
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const EventTeamTab = ({ team, teamMembers }: Pick<EventTypeSetupProps, "teamMembers" | "team">) => {
|
export const EventTeamTab = ({
|
||||||
|
team,
|
||||||
|
teamMembers,
|
||||||
|
eventType,
|
||||||
|
}: Pick<EventTypeSetupProps, "teamMembers" | "team" | "eventType">) => {
|
||||||
const { t } = useLocale();
|
const { t } = useLocale();
|
||||||
|
|
||||||
const schedulingTypeOptions: {
|
const schedulingTypeOptions: {
|
||||||
@@ -216,9 +292,13 @@ export const EventTeamTab = ({ team, teamMembers }: Pick<EventTypeSetupProps, "t
|
|||||||
},
|
},
|
||||||
];
|
];
|
||||||
const teamMembersOptions = teamMembers.map(mapUserToValue);
|
const teamMembersOptions = teamMembers.map(mapUserToValue);
|
||||||
|
const childrenEventTypeOptions = teamMembers.map((member) => {
|
||||||
|
return mapMemberToChildrenOption(member, eventType.slug);
|
||||||
|
});
|
||||||
|
const isManagedEventType = eventType.schedulingType === SchedulingType.MANAGED;
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
{team && (
|
{team && !isManagedEventType && (
|
||||||
<div className="space-y-5">
|
<div className="space-y-5">
|
||||||
<div className="flex flex-col">
|
<div className="flex flex-col">
|
||||||
<Label>{t("scheduling_type")}</Label>
|
<Label>{t("scheduling_type")}</Label>
|
||||||
@@ -239,6 +319,9 @@ export const EventTeamTab = ({ team, teamMembers }: Pick<EventTypeSetupProps, "t
|
|||||||
<Hosts teamMembers={teamMembersOptions} />
|
<Hosts teamMembers={teamMembersOptions} />
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
{team && isManagedEventType && (
|
||||||
|
<ChildrenEventTypes childrenEventTypeOptions={childrenEventTypeOptions} />
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
@@ -1,16 +1,17 @@
|
|||||||
|
import type { Webhook } from "@prisma/client";
|
||||||
|
import { Webhook as TbWebhook } from "lucide-react";
|
||||||
import type { EventTypeSetupProps } from "pages/event-types/[type]";
|
import type { EventTypeSetupProps } from "pages/event-types/[type]";
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import { TbWebhook } from "react-icons/tb";
|
|
||||||
|
|
||||||
|
import useLockedFieldsManager from "@calcom/features/ee/managed-event-types/hooks/useLockedFieldsManager";
|
||||||
import { WebhookForm } from "@calcom/features/webhooks/components";
|
import { WebhookForm } from "@calcom/features/webhooks/components";
|
||||||
import type { WebhookFormSubmitData } from "@calcom/features/webhooks/components/WebhookForm";
|
import type { WebhookFormSubmitData } from "@calcom/features/webhooks/components/WebhookForm";
|
||||||
import WebhookListItem from "@calcom/features/webhooks/components/WebhookListItem";
|
import WebhookListItem from "@calcom/features/webhooks/components/WebhookListItem";
|
||||||
import { APP_NAME } from "@calcom/lib/constants";
|
import { APP_NAME } from "@calcom/lib/constants";
|
||||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||||
import type { Webhook } from "@calcom/prisma/client";
|
|
||||||
import { trpc } from "@calcom/trpc/react";
|
import { trpc } from "@calcom/trpc/react";
|
||||||
import { Button, Dialog, DialogContent, EmptyScreen, showToast } from "@calcom/ui";
|
import { Alert, Button, Dialog, DialogContent, EmptyScreen, showToast } from "@calcom/ui";
|
||||||
import { FiPlus } from "@calcom/ui/components/icon";
|
import { Plus, Lock } from "@calcom/ui/components/icon";
|
||||||
|
|
||||||
export const EventTeamWebhooksTab = ({
|
export const EventTeamWebhooksTab = ({
|
||||||
eventType,
|
eventType,
|
||||||
@@ -85,12 +86,20 @@ export const EventTeamWebhooksTab = ({
|
|||||||
<Button
|
<Button
|
||||||
color="secondary"
|
color="secondary"
|
||||||
data-testid="new_webhook"
|
data-testid="new_webhook"
|
||||||
StartIcon={FiPlus}
|
StartIcon={Plus}
|
||||||
onClick={() => setCreateModalOpen(true)}>
|
onClick={() => setCreateModalOpen(true)}>
|
||||||
{t("new_webhook")}
|
{t("new_webhook")}
|
||||||
</Button>
|
</Button>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const { shouldLockDisableProps, isChildrenManagedEventType, isManagedEventType } = useLockedFieldsManager(
|
||||||
|
eventType,
|
||||||
|
t("locked_fields_admin_description"),
|
||||||
|
t("locked_fields_member_description")
|
||||||
|
);
|
||||||
|
const webhookLockedStatus = shouldLockDisableProps("webhooks");
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
{team && webhooks && !isLoading && (
|
{team && webhooks && !isLoading && (
|
||||||
@@ -98,15 +107,24 @@ export const EventTeamWebhooksTab = ({
|
|||||||
<div>
|
<div>
|
||||||
<div>
|
<div>
|
||||||
<>
|
<>
|
||||||
|
{isManagedEventType && (
|
||||||
|
<Alert
|
||||||
|
severity="neutral"
|
||||||
|
className="mb-2"
|
||||||
|
title={t("locked_for_members")}
|
||||||
|
message={t("locked_webhooks_description")}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
{webhooks.length ? (
|
{webhooks.length ? (
|
||||||
<>
|
<>
|
||||||
<div className="mb-8 rounded-md border">
|
<div className="mb-2 rounded-md border">
|
||||||
{webhooks.map((webhook, index) => {
|
{webhooks.map((webhook, index) => {
|
||||||
return (
|
return (
|
||||||
<WebhookListItem
|
<WebhookListItem
|
||||||
key={webhook.id}
|
key={webhook.id}
|
||||||
webhook={webhook}
|
webhook={webhook}
|
||||||
lastItem={webhooks.length === index + 1}
|
lastItem={webhooks.length === index + 1}
|
||||||
|
canEditWebhook={!webhookLockedStatus.disabled}
|
||||||
onEditWebhook={() => {
|
onEditWebhook={() => {
|
||||||
setEditModalOpen(true);
|
setEditModalOpen(true);
|
||||||
setWebhookToEdit(webhook);
|
setWebhookToEdit(webhook);
|
||||||
@@ -122,7 +140,15 @@ export const EventTeamWebhooksTab = ({
|
|||||||
Icon={TbWebhook}
|
Icon={TbWebhook}
|
||||||
headline={t("create_your_first_webhook")}
|
headline={t("create_your_first_webhook")}
|
||||||
description={t("create_your_first_team_webhook_description", { appName: APP_NAME })}
|
description={t("create_your_first_team_webhook_description", { appName: APP_NAME })}
|
||||||
buttonRaw={<NewWebhookButton />}
|
buttonRaw={
|
||||||
|
isChildrenManagedEventType && !isManagedEventType ? (
|
||||||
|
<Button StartIcon={Lock} color="secondary" disabled>
|
||||||
|
{t("locked_by_admin")}
|
||||||
|
</Button>
|
||||||
|
) : (
|
||||||
|
<NewWebhookButton />
|
||||||
|
)
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
@@ -144,7 +170,7 @@ export const EventTeamWebhooksTab = ({
|
|||||||
</Dialog>
|
</Dialog>
|
||||||
{/* Edit webhook dialog */}
|
{/* Edit webhook dialog */}
|
||||||
<Dialog open={editModalOpen} onOpenChange={(isOpen) => !isOpen && setEditModalOpen(false)}>
|
<Dialog open={editModalOpen} onOpenChange={(isOpen) => !isOpen && setEditModalOpen(false)}>
|
||||||
<DialogContent title={t("edit_webhook")}>
|
<DialogContent enableOverflow title={t("edit_webhook")}>
|
||||||
<WebhookForm
|
<WebhookForm
|
||||||
webhook={webhookToEdit}
|
webhook={webhookToEdit}
|
||||||
apps={installedApps?.items.map((app) => app.slug)}
|
apps={installedApps?.items.map((app) => app.slug)}
|
||||||
|
|||||||
@@ -1,15 +1,18 @@
|
|||||||
|
import { Webhook as TbWebhook } from "lucide-react";
|
||||||
import type { TFunction } from "next-i18next";
|
import type { TFunction } from "next-i18next";
|
||||||
|
import { Trans } from "next-i18next";
|
||||||
import { useRouter } from "next/router";
|
import { useRouter } from "next/router";
|
||||||
import type { EventTypeSetupProps, FormValues } from "pages/event-types/[type]";
|
import type { EventTypeSetupProps, FormValues } from "pages/event-types/[type]";
|
||||||
import { useMemo, useState, Suspense } from "react";
|
import { useMemo, useState, Suspense } from "react";
|
||||||
import type { UseFormReturn } from "react-hook-form";
|
import type { UseFormReturn } from "react-hook-form";
|
||||||
import { TbWebhook } from "react-icons/tb";
|
|
||||||
|
|
||||||
|
import useLockedFieldsManager from "@calcom/features/ee/managed-event-types/hooks/useLockedFieldsManager";
|
||||||
import Shell from "@calcom/features/shell/Shell";
|
import Shell from "@calcom/features/shell/Shell";
|
||||||
import { classNames } from "@calcom/lib";
|
import { classNames } from "@calcom/lib";
|
||||||
import { CAL_URL } from "@calcom/lib/constants";
|
import { CAL_URL } from "@calcom/lib/constants";
|
||||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||||
import { HttpError } from "@calcom/lib/http-error";
|
import { HttpError } from "@calcom/lib/http-error";
|
||||||
|
import { SchedulingType } from "@calcom/prisma/enums";
|
||||||
import { trpc, TRPCClientError } from "@calcom/trpc/react";
|
import { trpc, TRPCClientError } from "@calcom/trpc/react";
|
||||||
import {
|
import {
|
||||||
Button,
|
Button,
|
||||||
@@ -32,22 +35,23 @@ import {
|
|||||||
VerticalTabs,
|
VerticalTabs,
|
||||||
} from "@calcom/ui";
|
} from "@calcom/ui";
|
||||||
import {
|
import {
|
||||||
FiLink,
|
Link as LinkIcon,
|
||||||
FiCalendar,
|
Calendar,
|
||||||
FiClock,
|
Clock,
|
||||||
FiSliders,
|
Sliders,
|
||||||
FiRepeat,
|
Repeat,
|
||||||
FiGrid,
|
Grid,
|
||||||
FiZap,
|
Zap,
|
||||||
FiUsers,
|
Users,
|
||||||
FiExternalLink,
|
ExternalLink,
|
||||||
FiCode,
|
Code,
|
||||||
FiTrash,
|
Trash,
|
||||||
FiMoreHorizontal,
|
MoreHorizontal,
|
||||||
FiLoader,
|
Loader,
|
||||||
} from "@calcom/ui/components/icon";
|
} from "@calcom/ui/components/icon";
|
||||||
|
|
||||||
import { EmbedButton, EmbedDialog } from "@components/Embed";
|
import { EmbedButton, EmbedDialog } from "@components/Embed";
|
||||||
|
import type { AvailabilityOption } from "@components/eventtype/EventAvailabilityTab";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
children: React.ReactNode;
|
children: React.ReactNode;
|
||||||
@@ -60,6 +64,7 @@ type Props = {
|
|||||||
enabledWorkflowsNumber: number;
|
enabledWorkflowsNumber: number;
|
||||||
formMethods: UseFormReturn<FormValues>;
|
formMethods: UseFormReturn<FormValues>;
|
||||||
isUpdateMutationLoading?: boolean;
|
isUpdateMutationLoading?: boolean;
|
||||||
|
availability?: AvailabilityOption;
|
||||||
};
|
};
|
||||||
|
|
||||||
function getNavigation(props: {
|
function getNavigation(props: {
|
||||||
@@ -68,8 +73,10 @@ function getNavigation(props: {
|
|||||||
enabledAppsNumber: number;
|
enabledAppsNumber: number;
|
||||||
enabledWorkflowsNumber: number;
|
enabledWorkflowsNumber: number;
|
||||||
installedAppsNumber: number;
|
installedAppsNumber: number;
|
||||||
|
availability: AvailabilityOption | undefined;
|
||||||
}) {
|
}) {
|
||||||
const { eventType, t, enabledAppsNumber, installedAppsNumber, enabledWorkflowsNumber } = props;
|
const { eventType, t, enabledAppsNumber, installedAppsNumber, enabledWorkflowsNumber, availability } =
|
||||||
|
props;
|
||||||
const duration =
|
const duration =
|
||||||
eventType.metadata?.multipleDuration?.map((duration) => ` ${duration}`) || eventType.length;
|
eventType.metadata?.multipleDuration?.map((duration) => ` ${duration}`) || eventType.length;
|
||||||
|
|
||||||
@@ -77,44 +84,38 @@ function getNavigation(props: {
|
|||||||
{
|
{
|
||||||
name: "event_setup_tab_title",
|
name: "event_setup_tab_title",
|
||||||
href: `/event-types/${eventType.id}?tabName=setup`,
|
href: `/event-types/${eventType.id}?tabName=setup`,
|
||||||
icon: FiLink,
|
icon: LinkIcon,
|
||||||
info: `${duration} ${t("minute_timeUnit")}`, // TODO: Get this from props
|
info: `${duration} ${t("minute_timeUnit")}`, // TODO: Get this from props
|
||||||
},
|
},
|
||||||
{
|
|
||||||
name: "availability",
|
|
||||||
href: `/event-types/${eventType.id}?tabName=availability`,
|
|
||||||
icon: FiCalendar,
|
|
||||||
info: `default_schedule_name`, // TODO: Get this from props
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
name: "event_limit_tab_title",
|
name: "event_limit_tab_title",
|
||||||
href: `/event-types/${eventType.id}?tabName=limits`,
|
href: `/event-types/${eventType.id}?tabName=limits`,
|
||||||
icon: FiClock,
|
icon: Clock,
|
||||||
info: `event_limit_tab_description`,
|
info: `event_limit_tab_description`,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "event_advanced_tab_title",
|
name: "event_advanced_tab_title",
|
||||||
href: `/event-types/${eventType.id}?tabName=advanced`,
|
href: `/event-types/${eventType.id}?tabName=advanced`,
|
||||||
icon: FiSliders,
|
icon: Sliders,
|
||||||
info: `event_advanced_tab_description`,
|
info: `event_advanced_tab_description`,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "recurring",
|
name: "recurring",
|
||||||
href: `/event-types/${eventType.id}?tabName=recurring`,
|
href: `/event-types/${eventType.id}?tabName=recurring`,
|
||||||
icon: FiRepeat,
|
icon: Repeat,
|
||||||
info: `recurring_event_tab_description`,
|
info: `recurring_event_tab_description`,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "apps",
|
name: "apps",
|
||||||
href: `/event-types/${eventType.id}?tabName=apps`,
|
href: `/event-types/${eventType.id}?tabName=apps`,
|
||||||
icon: FiGrid,
|
icon: Grid,
|
||||||
//TODO: Handle proper translation with count handling
|
//TODO: Handle proper translation with count handling
|
||||||
info: `${installedAppsNumber} apps, ${enabledAppsNumber} ${t("active")}`,
|
info: `${installedAppsNumber} apps, ${enabledAppsNumber} ${t("active")}`,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "workflows",
|
name: "workflows",
|
||||||
href: `/event-types/${eventType.id}?tabName=workflows`,
|
href: `/event-types/${eventType.id}?tabName=workflows`,
|
||||||
icon: FiZap,
|
icon: Zap,
|
||||||
info: `${enabledWorkflowsNumber} ${t("active")}`,
|
info: `${enabledWorkflowsNumber} ${t("active")}`,
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
@@ -131,13 +132,17 @@ function EventTypeSingleLayout({
|
|||||||
enabledWorkflowsNumber,
|
enabledWorkflowsNumber,
|
||||||
isUpdateMutationLoading,
|
isUpdateMutationLoading,
|
||||||
formMethods,
|
formMethods,
|
||||||
|
availability,
|
||||||
}: Props) {
|
}: Props) {
|
||||||
const utils = trpc.useContext();
|
const utils = trpc.useContext();
|
||||||
const { t } = useLocale();
|
const { t } = useLocale();
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const [deleteDialogOpen, setDeleteDialogOpen] = useState(false);
|
const [deleteDialogOpen, setDeleteDialogOpen] = useState(false);
|
||||||
|
|
||||||
const hasPermsToDelete = currentUserMembership?.role !== "MEMBER" || !currentUserMembership;
|
const hasPermsToDelete =
|
||||||
|
currentUserMembership?.role !== "MEMBER" ||
|
||||||
|
!currentUserMembership ||
|
||||||
|
eventType.schedulingType === SchedulingType.MANAGED;
|
||||||
|
|
||||||
const deleteMutation = trpc.viewer.eventTypes.delete.useMutation({
|
const deleteMutation = trpc.viewer.eventTypes.delete.useMutation({
|
||||||
onSuccess: async () => {
|
onSuccess: async () => {
|
||||||
@@ -157,6 +162,12 @@ function EventTypeSingleLayout({
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const { isManagedEventType, isChildrenManagedEventType } = useLockedFieldsManager(
|
||||||
|
eventType,
|
||||||
|
t("locked_fields_admin_description"),
|
||||||
|
t("locked_fields_member_description")
|
||||||
|
);
|
||||||
|
|
||||||
// Define tab navigation here
|
// Define tab navigation here
|
||||||
const EventTypeTabs = useMemo(() => {
|
const EventTypeTabs = useMemo(() => {
|
||||||
const navigation = getNavigation({
|
const navigation = getNavigation({
|
||||||
@@ -165,15 +176,42 @@ function EventTypeSingleLayout({
|
|||||||
enabledAppsNumber,
|
enabledAppsNumber,
|
||||||
installedAppsNumber,
|
installedAppsNumber,
|
||||||
enabledWorkflowsNumber,
|
enabledWorkflowsNumber,
|
||||||
|
availability,
|
||||||
|
});
|
||||||
|
navigation.splice(1, 0, {
|
||||||
|
name: "availability",
|
||||||
|
href: `/event-types/${eventType.id}?tabName=availability`,
|
||||||
|
icon: Calendar,
|
||||||
|
info:
|
||||||
|
isManagedEventType || isChildrenManagedEventType
|
||||||
|
? eventType.schedule === null
|
||||||
|
? "Member's default schedule"
|
||||||
|
: isChildrenManagedEventType
|
||||||
|
? `${
|
||||||
|
eventType.scheduleName
|
||||||
|
? `${eventType.scheduleName} - ${t("managed")}`
|
||||||
|
: `default_schedule_name`
|
||||||
|
}`
|
||||||
|
: eventType.scheduleName ?? `default_schedule_name`
|
||||||
|
: `default_schedule_name`,
|
||||||
});
|
});
|
||||||
// If there is a team put this navigation item within the tabs
|
// If there is a team put this navigation item within the tabs
|
||||||
if (team) {
|
if (team) {
|
||||||
navigation.splice(2, 0, {
|
navigation.splice(2, 0, {
|
||||||
name: "assignment",
|
name: "assignment",
|
||||||
href: `/event-types/${eventType.id}?tabName=team`,
|
href: `/event-types/${eventType.id}?tabName=team`,
|
||||||
icon: FiUsers,
|
icon: Users,
|
||||||
info: eventType.schedulingType === "COLLECTIVE" ? "collective" : "round_robin",
|
info: `${t(eventType.schedulingType?.toLowerCase() ?? "")}${
|
||||||
|
isManagedEventType
|
||||||
|
? ` - ${t("count_members", { count: formMethods.watch("children").length || 0 })}`
|
||||||
|
: ""
|
||||||
|
}`,
|
||||||
});
|
});
|
||||||
|
}
|
||||||
|
if (isManagedEventType || isChildrenManagedEventType) {
|
||||||
|
// Removing apps and workflows for manageg event types by admins v1
|
||||||
|
navigation.splice(-2, 1);
|
||||||
|
} else {
|
||||||
navigation.push({
|
navigation.push({
|
||||||
name: "webhooks",
|
name: "webhooks",
|
||||||
href: `/event-types/${eventType.id}?tabName=webhooks`,
|
href: `/event-types/${eventType.id}?tabName=webhooks`,
|
||||||
@@ -182,13 +220,14 @@ function EventTypeSingleLayout({
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
return navigation;
|
return navigation;
|
||||||
}, [t, eventType, installedAppsNumber, enabledAppsNumber, enabledWorkflowsNumber, team]);
|
}, [t, eventType, installedAppsNumber, enabledAppsNumber, enabledWorkflowsNumber, team, availability]);
|
||||||
|
|
||||||
const permalink = `${CAL_URL}/${team ? `team/${team.slug}` : eventType.users[0].username}/${
|
const permalink = `${CAL_URL}/${team ? `team/${team.slug}` : eventType.users[0].username}/${
|
||||||
eventType.slug
|
eventType.slug
|
||||||
}`;
|
}`;
|
||||||
|
|
||||||
const embedLink = `${team ? `team/${team.slug}` : eventType.users[0].username}/${eventType.slug}`;
|
const embedLink = `${team ? `team/${team.slug}` : eventType.users[0].username}/${eventType.slug}`;
|
||||||
|
const isManagedEvent = eventType.schedulingType === SchedulingType.MANAGED ? "_managed" : "";
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Shell
|
<Shell
|
||||||
@@ -197,86 +236,96 @@ function EventTypeSingleLayout({
|
|||||||
heading={eventType.title}
|
heading={eventType.title}
|
||||||
CTA={
|
CTA={
|
||||||
<div className="flex items-center justify-end">
|
<div className="flex items-center justify-end">
|
||||||
<div className="hidden items-center rounded-md px-2 sm:hover:bg-gray-100 lg:flex">
|
{!eventType.metadata.managedEventConfig && (
|
||||||
<Skeleton
|
<>
|
||||||
as={Label}
|
<div className="sm:hover:bg-muted hidden items-center rounded-md px-2 lg:flex">
|
||||||
htmlFor="hiddenSwitch"
|
<Skeleton
|
||||||
className="mt-2 hidden cursor-pointer self-center whitespace-nowrap pr-2 sm:inline">
|
as={Label}
|
||||||
{t("hide_from_profile")}
|
htmlFor="hiddenSwitch"
|
||||||
</Skeleton>
|
className="mt-2 hidden cursor-pointer self-center whitespace-nowrap pr-2 sm:inline">
|
||||||
<Switch
|
{t("hide_from_profile")}
|
||||||
id="hiddenSwitch"
|
</Skeleton>
|
||||||
checked={formMethods.watch("hidden")}
|
<Switch
|
||||||
onCheckedChange={(e) => {
|
id="hiddenSwitch"
|
||||||
formMethods.setValue("hidden", e);
|
checked={formMethods.watch("hidden")}
|
||||||
}}
|
onCheckedChange={(e) => {
|
||||||
/>
|
formMethods.setValue("hidden", e);
|
||||||
</div>
|
}}
|
||||||
<VerticalDivider className="hidden lg:block" />
|
/>
|
||||||
|
</div>
|
||||||
|
<VerticalDivider className="hidden lg:block" />
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
|
||||||
{/* TODO: Figure out why combined isnt working - works in storybook */}
|
{/* TODO: Figure out why combined isnt working - works in storybook */}
|
||||||
<ButtonGroup combined containerProps={{ className: "border-gray-300 hidden lg:flex" }}>
|
<ButtonGroup combined containerProps={{ className: "border-default hidden lg:flex" }}>
|
||||||
{/* We have to warp this in tooltip as it has a href which disabels the tooltip on buttons */}
|
{!isManagedEventType && (
|
||||||
<Tooltip content={t("preview")}>
|
<>
|
||||||
<Button
|
{/* We have to warp this in tooltip as it has a href which disabels the tooltip on buttons */}
|
||||||
color="secondary"
|
<Tooltip content={t("preview")}>
|
||||||
data-testid="preview-button"
|
<Button
|
||||||
target="_blank"
|
color="secondary"
|
||||||
variant="icon"
|
data-testid="preview-button"
|
||||||
href={permalink}
|
target="_blank"
|
||||||
rel="noreferrer"
|
variant="icon"
|
||||||
StartIcon={FiExternalLink}
|
href={permalink}
|
||||||
/>
|
rel="noreferrer"
|
||||||
</Tooltip>
|
StartIcon={ExternalLink}
|
||||||
|
/>
|
||||||
|
</Tooltip>
|
||||||
|
|
||||||
<Button
|
<Button
|
||||||
color="secondary"
|
color="secondary"
|
||||||
variant="icon"
|
variant="icon"
|
||||||
StartIcon={FiLink}
|
StartIcon={LinkIcon}
|
||||||
tooltip={t("copy_link")}
|
tooltip={t("copy_link")}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
navigator.clipboard.writeText(permalink);
|
navigator.clipboard.writeText(permalink);
|
||||||
showToast("Link copied!", "success");
|
showToast("Link copied!", "success");
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<EmbedButton
|
<EmbedButton
|
||||||
embedUrl={encodeURIComponent(embedLink)}
|
embedUrl={encodeURIComponent(embedLink)}
|
||||||
StartIcon={FiCode}
|
StartIcon={Code}
|
||||||
color="secondary"
|
color="secondary"
|
||||||
variant="icon"
|
variant="icon"
|
||||||
tooltip={t("embed")}
|
tooltip={t("embed")}
|
||||||
/>
|
/>
|
||||||
<Button
|
</>
|
||||||
color="secondary"
|
)}
|
||||||
variant="icon"
|
{!isChildrenManagedEventType && (
|
||||||
StartIcon={FiTrash}
|
<Button
|
||||||
tooltip={t("delete")}
|
color="destructive"
|
||||||
disabled={!hasPermsToDelete}
|
variant="icon"
|
||||||
onClick={() => setDeleteDialogOpen(true)}
|
StartIcon={Trash}
|
||||||
/>
|
tooltip={t("delete")}
|
||||||
|
disabled={!hasPermsToDelete}
|
||||||
|
onClick={() => setDeleteDialogOpen(true)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</ButtonGroup>
|
</ButtonGroup>
|
||||||
|
|
||||||
<VerticalDivider className="hidden lg:block" />
|
<VerticalDivider className="hidden lg:block" />
|
||||||
|
|
||||||
<Dropdown>
|
<Dropdown>
|
||||||
<DropdownMenuTrigger asChild>
|
<DropdownMenuTrigger asChild>
|
||||||
<Button className="lg:hidden" StartIcon={FiMoreHorizontal} variant="icon" color="secondary" />
|
<Button className="lg:hidden" StartIcon={MoreHorizontal} variant="icon" color="secondary" />
|
||||||
</DropdownMenuTrigger>
|
</DropdownMenuTrigger>
|
||||||
<DropdownMenuContent style={{ minWidth: "200px" }}>
|
<DropdownMenuContent style={{ minWidth: "200px" }}>
|
||||||
<DropdownMenuItem className="focus:ring-gray-100">
|
<DropdownMenuItem className="focus:ring-muted">
|
||||||
<DropdownItem
|
<DropdownItem
|
||||||
target="_blank"
|
target="_blank"
|
||||||
type="button"
|
type="button"
|
||||||
StartIcon={FiExternalLink}
|
StartIcon={ExternalLink}
|
||||||
href={permalink}
|
href={permalink}
|
||||||
rel="noreferrer">
|
rel="noreferrer">
|
||||||
{t("preview")}
|
{t("preview")}
|
||||||
</DropdownItem>
|
</DropdownItem>
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
<DropdownMenuItem className="focus:ring-gray-100">
|
<DropdownMenuItem className="focus:ring-muted">
|
||||||
<DropdownItem
|
<DropdownItem
|
||||||
type="button"
|
type="button"
|
||||||
StartIcon={FiLink}
|
StartIcon={LinkIcon}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
navigator.clipboard.writeText(permalink);
|
navigator.clipboard.writeText(permalink);
|
||||||
showToast("Link copied!", "success");
|
showToast("Link copied!", "success");
|
||||||
@@ -284,18 +333,18 @@ function EventTypeSingleLayout({
|
|||||||
{t("copy_link")}
|
{t("copy_link")}
|
||||||
</DropdownItem>
|
</DropdownItem>
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
<DropdownMenuItem className="focus:ring-gray-100">
|
<DropdownMenuItem className="focus:ring-muted">
|
||||||
<DropdownItem
|
<DropdownItem
|
||||||
type="button"
|
type="button"
|
||||||
color="destructive"
|
color="destructive"
|
||||||
StartIcon={FiTrash}
|
StartIcon={Trash}
|
||||||
disabled={!hasPermsToDelete}
|
disabled={!hasPermsToDelete}
|
||||||
onClick={() => setDeleteDialogOpen(true)}>
|
onClick={() => setDeleteDialogOpen(true)}>
|
||||||
{t("delete")}
|
{t("delete")}
|
||||||
</DropdownItem>
|
</DropdownItem>
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
<DropdownMenuSeparator />
|
<DropdownMenuSeparator />
|
||||||
<div className="flex h-9 flex-row items-center justify-between py-2 px-4 sm:hover:bg-gray-100">
|
<div className="sm:hover:bg-subtle flex h-9 flex-row items-center justify-between py-2 px-4">
|
||||||
<Skeleton
|
<Skeleton
|
||||||
as={Label}
|
as={Label}
|
||||||
htmlFor="hiddenSwitch"
|
htmlFor="hiddenSwitch"
|
||||||
@@ -312,7 +361,7 @@ function EventTypeSingleLayout({
|
|||||||
</div>
|
</div>
|
||||||
</DropdownMenuContent>
|
</DropdownMenuContent>
|
||||||
</Dropdown>
|
</Dropdown>
|
||||||
<div className="border-l-2 border-gray-300" />
|
<div className="border-default border-l-2" />
|
||||||
<Button
|
<Button
|
||||||
className="ml-4 lg:ml-0"
|
className="ml-4 lg:ml-0"
|
||||||
type="submit"
|
type="submit"
|
||||||
@@ -323,7 +372,7 @@ function EventTypeSingleLayout({
|
|||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
}>
|
}>
|
||||||
<Suspense fallback={<FiLoader />}>
|
<Suspense fallback={<Loader />}>
|
||||||
<div className="flex flex-col xl:flex-row xl:space-x-6">
|
<div className="flex flex-col xl:flex-row xl:space-x-6">
|
||||||
<div className="hidden xl:block">
|
<div className="hidden xl:block">
|
||||||
<VerticalTabs
|
<VerticalTabs
|
||||||
@@ -331,6 +380,8 @@ function EventTypeSingleLayout({
|
|||||||
tabs={EventTypeTabs}
|
tabs={EventTypeTabs}
|
||||||
sticky
|
sticky
|
||||||
linkProps={{ shallow: true }}
|
linkProps={{ shallow: true }}
|
||||||
|
itemClassname="items-start"
|
||||||
|
iconClassName="md:mt-px"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="p-2 md:mx-0 md:p-0 xl:hidden">
|
<div className="p-2 md:mx-0 md:p-0 xl:hidden">
|
||||||
@@ -339,7 +390,7 @@ function EventTypeSingleLayout({
|
|||||||
<div className="w-full ltr:mr-2 rtl:ml-2">
|
<div className="w-full ltr:mr-2 rtl:ml-2">
|
||||||
<div
|
<div
|
||||||
className={classNames(
|
className={classNames(
|
||||||
"mt-4 rounded-md border-gray-200 bg-white sm:mx-0 xl:mt-0",
|
"bg-default border-subtle mt-4 rounded-md sm:mx-0 xl:mt-0",
|
||||||
disableBorder ? "border-0 " : "p-2 md:border md:p-6"
|
disableBorder ? "border-0 " : "p-2 md:border md:p-6"
|
||||||
)}>
|
)}>
|
||||||
{children}
|
{children}
|
||||||
@@ -351,14 +402,25 @@ function EventTypeSingleLayout({
|
|||||||
<ConfirmationDialogContent
|
<ConfirmationDialogContent
|
||||||
isLoading={deleteMutation.isLoading}
|
isLoading={deleteMutation.isLoading}
|
||||||
variety="danger"
|
variety="danger"
|
||||||
title={t("delete_event_type")}
|
title={t(`delete${isManagedEvent}_event_type`)}
|
||||||
confirmBtnText={t("confirm_delete_event_type")}
|
confirmBtnText={t(`confirm_delete_event_type`)}
|
||||||
loadingText={t("confirm_delete_event_type")}
|
loadingText={t(`confirm_delete_event_type`)}
|
||||||
onConfirm={(e) => {
|
onConfirm={(e) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
deleteMutation.mutate({ id: eventType.id });
|
deleteMutation.mutate({ id: eventType.id });
|
||||||
}}>
|
}}>
|
||||||
{t("delete_event_type_description")}
|
<p className="mt-5">
|
||||||
|
<Trans
|
||||||
|
i18nKey={`delete${isManagedEvent}_event_type_description`}
|
||||||
|
components={{ li: <li />, ul: <ul className="ml-4 list-disc" /> }}>
|
||||||
|
<ul>
|
||||||
|
<li>Members assigned to this event type will also have their event types deleted.</li>
|
||||||
|
<li>
|
||||||
|
Anyone who they've shared their link with will no longer be able to book using it.
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</Trans>
|
||||||
|
</p>
|
||||||
</ConfirmationDialogContent>
|
</ConfirmationDialogContent>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
<EmbedDialog />
|
<EmbedDialog />
|
||||||
|
|||||||
@@ -1,23 +1,26 @@
|
|||||||
import type { FormValues } from "pages/event-types/[type]";
|
import type { EventTypeSetup, FormValues } from "pages/event-types/[type]";
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import { useFormContext } from "react-hook-form";
|
import { useFormContext } from "react-hook-form";
|
||||||
|
|
||||||
|
import useLockedFieldsManager from "@calcom/features/ee/managed-event-types/hooks/useLockedFieldsManager";
|
||||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||||
import { Frequency } from "@calcom/prisma/zod-utils";
|
import { Frequency } from "@calcom/prisma/zod-utils";
|
||||||
import type { RecurringEvent } from "@calcom/types/Calendar";
|
import type { RecurringEvent } from "@calcom/types/Calendar";
|
||||||
import { Alert, Select, SettingsToggle } from "@calcom/ui";
|
import { Alert, Select, SettingsToggle, TextField } from "@calcom/ui";
|
||||||
|
|
||||||
type RecurringEventControllerProps = {
|
type RecurringEventControllerProps = {
|
||||||
recurringEvent: RecurringEvent | null;
|
eventType: EventTypeSetup;
|
||||||
paymentEnabled: boolean;
|
paymentEnabled: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
export default function RecurringEventController({
|
export default function RecurringEventController({
|
||||||
recurringEvent,
|
eventType,
|
||||||
paymentEnabled,
|
paymentEnabled,
|
||||||
}: RecurringEventControllerProps) {
|
}: RecurringEventControllerProps) {
|
||||||
const { t } = useLocale();
|
const { t } = useLocale();
|
||||||
const [recurringEventState, setRecurringEventState] = useState<RecurringEvent | null>(recurringEvent);
|
const [recurringEventState, setRecurringEventState] = useState<RecurringEvent | null>(
|
||||||
|
eventType.recurringEvent
|
||||||
|
);
|
||||||
const formMethods = useFormContext<FormValues>();
|
const formMethods = useFormContext<FormValues>();
|
||||||
|
|
||||||
/* Just yearly-0, monthly-1 and weekly-2 */
|
/* Just yearly-0, monthly-1 and weekly-2 */
|
||||||
@@ -28,6 +31,14 @@ export default function RecurringEventController({
|
|||||||
value: value.toString(),
|
value: value.toString(),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
const { shouldLockDisableProps } = useLockedFieldsManager(
|
||||||
|
eventType,
|
||||||
|
t("locked_fields_admin_description"),
|
||||||
|
t("locked_fields_member_description")
|
||||||
|
);
|
||||||
|
|
||||||
|
const recurringLocked = shouldLockDisableProps("recurringEvent");
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="block items-start sm:flex">
|
<div className="block items-start sm:flex">
|
||||||
<div className={!paymentEnabled ? "w-full" : ""}>
|
<div className={!paymentEnabled ? "w-full" : ""}>
|
||||||
@@ -37,6 +48,7 @@ export default function RecurringEventController({
|
|||||||
<>
|
<>
|
||||||
<SettingsToggle
|
<SettingsToggle
|
||||||
title={t("recurring_event")}
|
title={t("recurring_event")}
|
||||||
|
{...recurringLocked}
|
||||||
description={t("recurring_event_description")}
|
description={t("recurring_event_description")}
|
||||||
checked={recurringEventState !== null}
|
checked={recurringEventState !== null}
|
||||||
data-testid="recurring-event-check"
|
data-testid="recurring-event-check"
|
||||||
@@ -45,7 +57,7 @@ export default function RecurringEventController({
|
|||||||
formMethods.setValue("recurringEvent", null);
|
formMethods.setValue("recurringEvent", null);
|
||||||
setRecurringEventState(null);
|
setRecurringEventState(null);
|
||||||
} else {
|
} else {
|
||||||
const newVal = recurringEvent || {
|
const newVal = eventType.recurringEvent || {
|
||||||
interval: 1,
|
interval: 1,
|
||||||
count: 12,
|
count: 12,
|
||||||
freq: Frequency.WEEKLY,
|
freq: Frequency.WEEKLY,
|
||||||
@@ -57,12 +69,13 @@ export default function RecurringEventController({
|
|||||||
{recurringEventState && (
|
{recurringEventState && (
|
||||||
<div data-testid="recurring-event-collapsible" className="text-sm">
|
<div data-testid="recurring-event-collapsible" className="text-sm">
|
||||||
<div className="flex items-center">
|
<div className="flex items-center">
|
||||||
<p className="text-gray-900 ltr:mr-2 rtl:ml-2">{t("repeats_every")}</p>
|
<p className="text-emphasis ltr:mr-2 rtl:ml-2">{t("repeats_every")}</p>
|
||||||
<input
|
<TextField
|
||||||
|
disabled={recurringLocked.disabled}
|
||||||
type="number"
|
type="number"
|
||||||
min="1"
|
min="1"
|
||||||
max="20"
|
max="20"
|
||||||
className="block h-[36px] w-16 rounded-md border-gray-300 text-sm [appearance:textfield] ltr:mr-2 rtl:ml-2"
|
className="mb-0"
|
||||||
defaultValue={recurringEventState.interval}
|
defaultValue={recurringEventState.interval}
|
||||||
onChange={(event) => {
|
onChange={(event) => {
|
||||||
const newVal = {
|
const newVal = {
|
||||||
@@ -77,7 +90,8 @@ export default function RecurringEventController({
|
|||||||
options={recurringEventFreqOptions}
|
options={recurringEventFreqOptions}
|
||||||
value={recurringEventFreqOptions[recurringEventState.freq]}
|
value={recurringEventFreqOptions[recurringEventState.freq]}
|
||||||
isSearchable={false}
|
isSearchable={false}
|
||||||
className="w-18 block h-[36px] min-w-0 rounded-md text-sm"
|
className="w-18 ml-2 block min-w-0 rounded-md text-sm"
|
||||||
|
isDisabled={recurringLocked.disabled}
|
||||||
onChange={(event) => {
|
onChange={(event) => {
|
||||||
const newVal = {
|
const newVal = {
|
||||||
...recurringEventState,
|
...recurringEventState,
|
||||||
@@ -89,13 +103,14 @@ export default function RecurringEventController({
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="mt-4 flex items-center">
|
<div className="mt-4 flex items-center">
|
||||||
<p className="text-gray-900 ltr:mr-2 rtl:ml-2">{t("for_a_maximum_of")}</p>
|
<p className="text-emphasis ltr:mr-2 rtl:ml-2">{t("for_a_maximum_of")}</p>
|
||||||
<input
|
<TextField
|
||||||
|
disabled={recurringLocked.disabled}
|
||||||
type="number"
|
type="number"
|
||||||
min="1"
|
min="1"
|
||||||
max="20"
|
max="20"
|
||||||
className="block h-[36px] w-16 rounded-md border-gray-300 text-sm [appearance:textfield] ltr:mr-2 rtl:ml-2"
|
|
||||||
defaultValue={recurringEventState.count}
|
defaultValue={recurringEventState.count}
|
||||||
|
className="mb-0"
|
||||||
onChange={(event) => {
|
onChange={(event) => {
|
||||||
const newVal = {
|
const newVal = {
|
||||||
...recurringEventState,
|
...recurringEventState,
|
||||||
@@ -105,7 +120,7 @@ export default function RecurringEventController({
|
|||||||
setRecurringEventState(newVal);
|
setRecurringEventState(newVal);
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<p className="text-gray-900 ltr:mr-2 rtl:ml-2">
|
<p className="text-emphasis ltr:ml-2 rtl:mr-2">
|
||||||
{t("events", {
|
{t("events", {
|
||||||
count: recurringEventState.count,
|
count: recurringEventState.count,
|
||||||
})}
|
})}
|
||||||
|
|||||||
@@ -1,25 +1,29 @@
|
|||||||
import * as RadioGroup from "@radix-ui/react-radio-group";
|
import * as RadioGroup from "@radix-ui/react-radio-group";
|
||||||
import type { UnitTypeLongPlural } from "dayjs";
|
import type { UnitTypeLongPlural } from "dayjs";
|
||||||
import { Trans } from "next-i18next";
|
import { Trans } from "next-i18next";
|
||||||
import type { FormValues } from "pages/event-types/[type]";
|
import type { EventTypeSetup, FormValues } from "pages/event-types/[type]";
|
||||||
import type { Dispatch, SetStateAction } from "react";
|
import type { Dispatch, SetStateAction } from "react";
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import { Controller, useFormContext } from "react-hook-form";
|
import { Controller, useFormContext } from "react-hook-form";
|
||||||
import type z from "zod";
|
import type z from "zod";
|
||||||
|
|
||||||
|
import useLockedFieldsManager from "@calcom/features/ee/managed-event-types/hooks/useLockedFieldsManager";
|
||||||
|
import { classNames } from "@calcom/lib";
|
||||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||||
import type { EventTypeMetaDataSchema } from "@calcom/prisma/zod-utils";
|
import type { EventTypeMetaDataSchema } from "@calcom/prisma/zod-utils";
|
||||||
import { Input, Label, SettingsToggle } from "@calcom/ui";
|
import { Input, SettingsToggle, RadioField, Select } from "@calcom/ui";
|
||||||
|
|
||||||
type RequiresConfirmationControllerProps = {
|
type RequiresConfirmationControllerProps = {
|
||||||
metadata: z.infer<typeof EventTypeMetaDataSchema>;
|
metadata: z.infer<typeof EventTypeMetaDataSchema>;
|
||||||
requiresConfirmation: boolean;
|
requiresConfirmation: boolean;
|
||||||
onRequiresConfirmation: Dispatch<SetStateAction<boolean>>;
|
onRequiresConfirmation: Dispatch<SetStateAction<boolean>>;
|
||||||
seatsEnabled: boolean;
|
seatsEnabled: boolean;
|
||||||
|
eventType: EventTypeSetup;
|
||||||
};
|
};
|
||||||
|
|
||||||
export default function RequiresConfirmationController({
|
export default function RequiresConfirmationController({
|
||||||
metadata,
|
metadata,
|
||||||
|
eventType,
|
||||||
requiresConfirmation,
|
requiresConfirmation,
|
||||||
onRequiresConfirmation,
|
onRequiresConfirmation,
|
||||||
seatsEnabled,
|
seatsEnabled,
|
||||||
@@ -37,6 +41,23 @@ export default function RequiresConfirmationController({
|
|||||||
}
|
}
|
||||||
}, [requiresConfirmation]);
|
}, [requiresConfirmation]);
|
||||||
|
|
||||||
|
const { shouldLockDisableProps } = useLockedFieldsManager(
|
||||||
|
eventType,
|
||||||
|
t("locked_fields_admin_description"),
|
||||||
|
t("locked_fields_member_description")
|
||||||
|
);
|
||||||
|
const requiresConfirmationLockedProps = shouldLockDisableProps("requiresConfirmation");
|
||||||
|
|
||||||
|
const options = [
|
||||||
|
{ label: t("minute_timeUnit"), value: "minutes" },
|
||||||
|
{ label: t("hour_timeUnit"), value: "hours" },
|
||||||
|
];
|
||||||
|
|
||||||
|
const defaultValue = options.find(
|
||||||
|
(opt) =>
|
||||||
|
opt.value === (metadata?.requiresConfirmationThreshold?.unit ?? defaultRequiresConfirmationSetup.unit)
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="block items-start sm:flex">
|
<div className="block items-start sm:flex">
|
||||||
<div className="w-full">
|
<div className="w-full">
|
||||||
@@ -46,10 +67,11 @@ export default function RequiresConfirmationController({
|
|||||||
render={() => (
|
render={() => (
|
||||||
<SettingsToggle
|
<SettingsToggle
|
||||||
title={t("requires_confirmation")}
|
title={t("requires_confirmation")}
|
||||||
disabled={seatsEnabled}
|
disabled={seatsEnabled || requiresConfirmationLockedProps.disabled}
|
||||||
tooltip={seatsEnabled ? t("seat_options_doesnt_support_confirmation") : undefined}
|
tooltip={seatsEnabled ? t("seat_options_doesnt_support_confirmation") : undefined}
|
||||||
description={t("requires_confirmation_description")}
|
description={t("requires_confirmation_description")}
|
||||||
checked={requiresConfirmation}
|
checked={requiresConfirmation}
|
||||||
|
LockedIcon={requiresConfirmationLockedProps.LockedIcon}
|
||||||
onCheckedChange={(val) => {
|
onCheckedChange={(val) => {
|
||||||
formMethods.setValue("requiresConfirmation", val);
|
formMethods.setValue("requiresConfirmation", val);
|
||||||
onRequiresConfirmation(val);
|
onRequiresConfirmation(val);
|
||||||
@@ -77,73 +99,82 @@ export default function RequiresConfirmationController({
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
}}>
|
}}>
|
||||||
<div className="-ml-2 flex flex-col flex-wrap justify-start gap-y-2 space-y-2">
|
<div className="flex flex-col flex-wrap justify-start gap-y-2">
|
||||||
<div className="flex items-center">
|
{(requiresConfirmationSetup === undefined || !requiresConfirmationLockedProps.disabled) && (
|
||||||
<RadioGroup.Item
|
<RadioField
|
||||||
|
label={t("always_requires_confirmation")}
|
||||||
|
disabled={requiresConfirmationLockedProps.disabled}
|
||||||
id="always"
|
id="always"
|
||||||
value="always"
|
value="always"
|
||||||
className="min-w-4 flex h-4 w-4 cursor-pointer items-center rounded-full border border-black bg-white focus:border-2 focus:outline-none ltr:mr-2 rtl:ml-2">
|
/>
|
||||||
<RadioGroup.Indicator className="relative flex h-4 w-4 items-center justify-center after:block after:h-2 after:w-2 after:rounded-full after:bg-black" />
|
)}
|
||||||
</RadioGroup.Item>
|
{(requiresConfirmationSetup !== undefined || !requiresConfirmationLockedProps.disabled) && (
|
||||||
<Label htmlFor="always" className="!m-0 flex items-center">
|
<RadioField
|
||||||
{t("always_requires_confirmation")}
|
disabled={requiresConfirmationLockedProps.disabled}
|
||||||
</Label>
|
className="items-center"
|
||||||
</div>
|
label={
|
||||||
<div className="flex items-center">
|
<>
|
||||||
<RadioGroup.Item
|
<Trans
|
||||||
|
i18nKey="when_booked_with_less_than_notice"
|
||||||
|
defaults="When booked with less than <time></time> notice"
|
||||||
|
components={{
|
||||||
|
time: (
|
||||||
|
<div className="mx-2 inline-flex">
|
||||||
|
<Input
|
||||||
|
type="number"
|
||||||
|
min={1}
|
||||||
|
disabled={requiresConfirmationLockedProps.disabled}
|
||||||
|
onChange={(evt) => {
|
||||||
|
const val = Number(evt.target?.value);
|
||||||
|
setRequiresConfirmationSetup({
|
||||||
|
unit:
|
||||||
|
requiresConfirmationSetup?.unit ??
|
||||||
|
defaultRequiresConfirmationSetup.unit,
|
||||||
|
time: val,
|
||||||
|
});
|
||||||
|
formMethods.setValue(
|
||||||
|
"metadata.requiresConfirmationThreshold.time",
|
||||||
|
val
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
className="border-default !m-0 block w-16 rounded-md text-sm [appearance:textfield]"
|
||||||
|
defaultValue={metadata?.requiresConfirmationThreshold?.time || 30}
|
||||||
|
/>
|
||||||
|
<label
|
||||||
|
className={classNames(
|
||||||
|
requiresConfirmationLockedProps.disabled && "cursor-not-allowed"
|
||||||
|
)}>
|
||||||
|
<Select
|
||||||
|
inputId="notice"
|
||||||
|
options={options}
|
||||||
|
isSearchable={false}
|
||||||
|
isDisabled={requiresConfirmationLockedProps.disabled}
|
||||||
|
className="ml-2"
|
||||||
|
onChange={(opt) => {
|
||||||
|
setRequiresConfirmationSetup({
|
||||||
|
time:
|
||||||
|
requiresConfirmationSetup?.time ??
|
||||||
|
defaultRequiresConfirmationSetup.time,
|
||||||
|
unit: opt?.value as UnitTypeLongPlural,
|
||||||
|
});
|
||||||
|
formMethods.setValue(
|
||||||
|
"metadata.requiresConfirmationThreshold.unit",
|
||||||
|
opt?.value as UnitTypeLongPlural
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
defaultValue={defaultValue}
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
),
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
}
|
||||||
id="notice"
|
id="notice"
|
||||||
value="notice"
|
value="notice"
|
||||||
className="min-w-4 flex h-4 w-4 cursor-pointer items-center rounded-full border border-black bg-white focus:border-2 focus:outline-none ltr:mr-2 rtl:ml-2">
|
/>
|
||||||
<RadioGroup.Indicator className="relative flex h-4 w-4 items-center justify-center after:block after:h-2 after:w-2 after:rounded-full after:bg-black" />
|
)}
|
||||||
</RadioGroup.Item>
|
|
||||||
<Label htmlFor="notice" className="!m-0 flex items-center">
|
|
||||||
<Trans
|
|
||||||
i18nKey="when_booked_with_less_than_notice"
|
|
||||||
defaults="When booked with less than <time></time> notice"
|
|
||||||
components={{
|
|
||||||
time: (
|
|
||||||
<div className="mx-2 flex">
|
|
||||||
<Input
|
|
||||||
type="number"
|
|
||||||
min={1}
|
|
||||||
onChange={(evt) => {
|
|
||||||
const val = Number(evt.target?.value);
|
|
||||||
setRequiresConfirmationSetup({
|
|
||||||
unit:
|
|
||||||
requiresConfirmationSetup?.unit ??
|
|
||||||
defaultRequiresConfirmationSetup.unit,
|
|
||||||
time: val,
|
|
||||||
});
|
|
||||||
formMethods.setValue("metadata.requiresConfirmationThreshold.time", val);
|
|
||||||
}}
|
|
||||||
className="!m-0 block w-16 rounded-md border-gray-300 text-sm [appearance:textfield]"
|
|
||||||
defaultValue={metadata?.requiresConfirmationThreshold?.time || 30}
|
|
||||||
/>
|
|
||||||
<select
|
|
||||||
onChange={(evt) => {
|
|
||||||
const val = evt.target.value as UnitTypeLongPlural;
|
|
||||||
setRequiresConfirmationSetup({
|
|
||||||
time:
|
|
||||||
requiresConfirmationSetup?.time ??
|
|
||||||
defaultRequiresConfirmationSetup.time,
|
|
||||||
unit: val,
|
|
||||||
});
|
|
||||||
formMethods.setValue("metadata.requiresConfirmationThreshold.unit", val);
|
|
||||||
}}
|
|
||||||
className="ml-2 block h-9 rounded-md border-gray-300 py-2 pl-3 pr-10 text-sm focus:outline-none"
|
|
||||||
defaultValue={
|
|
||||||
metadata?.requiresConfirmationThreshold?.unit ||
|
|
||||||
defaultRequiresConfirmationSetup.unit
|
|
||||||
}>
|
|
||||||
<option value="minutes">{t("minute_timeUnit")}</option>
|
|
||||||
<option value="hours">{t("hour_timeUnit")}</option>
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
),
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</Label>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</RadioGroup.Root>
|
</RadioGroup.Root>
|
||||||
</SettingsToggle>
|
</SettingsToggle>
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { SkeletonAvatar, SkeletonContainer, SkeletonText } from "@calcom/ui";
|
import { SkeletonAvatar, SkeletonContainer, SkeletonText } from "@calcom/ui";
|
||||||
import { FiClock, FiUser } from "@calcom/ui/components/icon";
|
import { Clock, User } from "@calcom/ui/components/icon";
|
||||||
|
|
||||||
function SkeletonLoader() {
|
function SkeletonLoader() {
|
||||||
return (
|
return (
|
||||||
@@ -11,7 +11,7 @@ function SkeletonLoader() {
|
|||||||
<SkeletonText className="h-4 w-24" />
|
<SkeletonText className="h-4 w-24" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<ul className="divide-y divide-gray-200 rounded-md border border-gray-200 bg-white sm:mx-0 sm:overflow-hidden">
|
<ul className="border-subtle bg-default divide-subtle divide-y rounded-md border sm:mx-0 sm:overflow-hidden">
|
||||||
<SkeletonItem />
|
<SkeletonItem />
|
||||||
<SkeletonItem />
|
<SkeletonItem />
|
||||||
<SkeletonItem />
|
<SkeletonItem />
|
||||||
@@ -32,11 +32,11 @@ function SkeletonItem() {
|
|||||||
<div className="">
|
<div className="">
|
||||||
<ul className="mt-2 flex space-x-4 rtl:space-x-reverse ">
|
<ul className="mt-2 flex space-x-4 rtl:space-x-reverse ">
|
||||||
<li className="flex items-center whitespace-nowrap">
|
<li className="flex items-center whitespace-nowrap">
|
||||||
<FiClock className="mt-0.5 mr-1.5 inline h-4 w-4 text-gray-200" />
|
<Clock className="text-subtle mt-0.5 mr-1.5 inline h-4 w-4" />
|
||||||
<SkeletonText className="h-4 w-12" />
|
<SkeletonText className="h-4 w-12" />
|
||||||
</li>
|
</li>
|
||||||
<li className="flex items-center whitespace-nowrap">
|
<li className="flex items-center whitespace-nowrap">
|
||||||
<FiUser className="mt-0.5 mr-1.5 inline h-4 w-4 text-gray-200" />
|
<User className="text-subtle mt-0.5 mr-1.5 inline h-4 w-4" />
|
||||||
<SkeletonText className="h-4 w-12" />
|
<SkeletonText className="h-4 w-12" />
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
|
|||||||
+9
-7
@@ -3,20 +3,21 @@ import { useLocale } from "@calcom/lib/hooks/useLocale";
|
|||||||
import type { App } from "@calcom/types/App";
|
import type { App } from "@calcom/types/App";
|
||||||
import { Button } from "@calcom/ui";
|
import { Button } from "@calcom/ui";
|
||||||
|
|
||||||
interface ICalendarItem {
|
interface IAppConnectionItem {
|
||||||
title: string;
|
title: string;
|
||||||
description?: string;
|
description?: string;
|
||||||
imageSrc: string;
|
logo: string;
|
||||||
type: App["type"];
|
type: App["type"];
|
||||||
|
installed?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
const CalendarItem = (props: ICalendarItem) => {
|
const AppConnectionItem = (props: IAppConnectionItem) => {
|
||||||
const { title, imageSrc, type } = props;
|
const { title, logo, type, installed } = props;
|
||||||
const { t } = useLocale();
|
const { t } = useLocale();
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-row items-center justify-between p-5">
|
<div className="flex flex-row items-center justify-between p-5">
|
||||||
<div className="flex items-center space-x-3">
|
<div className="flex items-center space-x-3">
|
||||||
<img src={imageSrc} alt={title} className="h-8 w-8" />
|
<img src={logo} alt={title} className="h-8 w-8" />
|
||||||
<p className="text-sm font-bold">{title}</p>
|
<p className="text-sm font-bold">{title}</p>
|
||||||
</div>
|
</div>
|
||||||
<InstallAppButtonWithoutPlanCheck
|
<InstallAppButtonWithoutPlanCheck
|
||||||
@@ -25,13 +26,14 @@ const CalendarItem = (props: ICalendarItem) => {
|
|||||||
<Button
|
<Button
|
||||||
{...buttonProps}
|
{...buttonProps}
|
||||||
color="secondary"
|
color="secondary"
|
||||||
|
disabled={installed}
|
||||||
type="button"
|
type="button"
|
||||||
onClick={(event) => {
|
onClick={(event) => {
|
||||||
// Save cookie key to return url step
|
// Save cookie key to return url step
|
||||||
document.cookie = `return-to=${window.location.href};path=/;max-age=3600;SameSite=Lax`;
|
document.cookie = `return-to=${window.location.href};path=/;max-age=3600;SameSite=Lax`;
|
||||||
buttonProps && buttonProps.onClick && buttonProps?.onClick(event);
|
buttonProps && buttonProps.onClick && buttonProps?.onClick(event);
|
||||||
}}>
|
}}>
|
||||||
{t("connect")}
|
{installed ? t("installed") : t("connect")}
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
@@ -39,4 +41,4 @@ const CalendarItem = (props: ICalendarItem) => {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export { CalendarItem };
|
export { AppConnectionItem };
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
import { CalendarSwitch } from "./CalendarSwitch";
|
import { CalendarSwitch } from "@calcom/features/calendars/CalendarSwitch";
|
||||||
|
|
||||||
interface IConnectedCalendarItem {
|
interface IConnectedCalendarItem {
|
||||||
name: string;
|
name: string;
|
||||||
@@ -28,14 +28,14 @@ const ConnectedCalendarItem = (prop: IConnectedCalendarItem) => {
|
|||||||
<p className="font-sans text-sm font-bold leading-5">
|
<p className="font-sans text-sm font-bold leading-5">
|
||||||
{name}
|
{name}
|
||||||
{/* Temporarily removed till we use it on another place */}
|
{/* Temporarily removed till we use it on another place */}
|
||||||
{/* <span className="mx-1 rounded-[4px] bg-green-100 py-[2px] px-[6px] font-sans text-xs font-medium text-green-600">
|
{/* <span className="mx-1 rounded-[4px] bg-success py-[2px] px-[6px] font-sans text-xs font-medium text-green-600">
|
||||||
{t("default")}
|
{t("default")}
|
||||||
</span> */}
|
</span> */}
|
||||||
</p>
|
</p>
|
||||||
<div className="fle-row flex">
|
<div className="fle-row flex">
|
||||||
<span
|
<span
|
||||||
title={externalId}
|
title={externalId}
|
||||||
className="max-w-44 mt-1 overflow-hidden text-ellipsis whitespace-nowrap font-sans text-sm text-gray-500">
|
className="max-w-44 text-subtle mt-1 overflow-hidden text-ellipsis whitespace-nowrap font-sans text-sm">
|
||||||
{externalId}{" "}
|
{externalId}{" "}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
@@ -44,11 +44,11 @@ const ConnectedCalendarItem = (prop: IConnectedCalendarItem) => {
|
|||||||
{/* <Button
|
{/* <Button
|
||||||
color="minimal"
|
color="minimal"
|
||||||
type="button"
|
type="button"
|
||||||
className="ml-auto flex rounded-md border border-gray-200 py-[10x] px-4 font-sans text-sm">
|
className="ml-auto flex rounded-md border border-subtle py-[10x] px-4 font-sans text-sm">
|
||||||
{t("edit")}
|
{t("edit")}
|
||||||
</Button> */}
|
</Button> */}
|
||||||
</div>
|
</div>
|
||||||
<div className="h-[1px] w-full border-b border-gray-200" />
|
<div className="border-subtle h-[1px] w-full border-b" />
|
||||||
<div>
|
<div>
|
||||||
<ul className="p-4">
|
<ul className="p-4">
|
||||||
{calendars?.map((calendar, i) => (
|
{calendars?.map((calendar, i) => (
|
||||||
|
|||||||
@@ -1,9 +1,8 @@
|
|||||||
|
import DestinationCalendarSelector from "@calcom/features/calendars/DestinationCalendarSelector";
|
||||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||||
import type { RouterInputs } from "@calcom/trpc/react";
|
import type { RouterInputs } from "@calcom/trpc/react";
|
||||||
import { trpc } from "@calcom/trpc/react";
|
import { trpc } from "@calcom/trpc/react";
|
||||||
|
|
||||||
import DestinationCalendarSelector from "@components/DestinationCalendarSelector";
|
|
||||||
|
|
||||||
interface ICreateEventsOnCalendarSelectProps {
|
interface ICreateEventsOnCalendarSelectProps {
|
||||||
calendar?: RouterInputs["viewer"]["setDestinationCalendar"] | null;
|
calendar?: RouterInputs["viewer"]["setDestinationCalendar"] | null;
|
||||||
}
|
}
|
||||||
@@ -17,7 +16,7 @@ const CreateEventsOnCalendarSelect = (props: ICreateEventsOnCalendarSelectProps)
|
|||||||
<>
|
<>
|
||||||
<div className="mt-6 flex flex-row">
|
<div className="mt-6 flex flex-row">
|
||||||
<div className="w-full">
|
<div className="w-full">
|
||||||
<label htmlFor="createEventsOn" className="flex text-sm font-medium text-gray-700">
|
<label htmlFor="createEventsOn" className="text-default flex text-sm font-medium">
|
||||||
{t("create_events_on")}
|
{t("create_events_on")}
|
||||||
</label>
|
</label>
|
||||||
<div className="mt-2">
|
<div className="mt-2">
|
||||||
|
|||||||
@@ -0,0 +1,17 @@
|
|||||||
|
import { SkeletonAvatar, SkeletonText, SkeletonButton } from "@calcom/ui";
|
||||||
|
|
||||||
|
export function StepConnectionLoader() {
|
||||||
|
return (
|
||||||
|
<ul className="bg-default divide-subtle border-subtle divide-y rounded-md border p-0 dark:bg-black">
|
||||||
|
{Array.from({ length: 4 }).map((_item, index) => {
|
||||||
|
return (
|
||||||
|
<li className="flex w-full flex-row justify-center border-b-0 py-6" key={index}>
|
||||||
|
<SkeletonAvatar className="mx-6 h-8 w-8 px-4" />
|
||||||
|
<SkeletonText className="ml-1 mr-4 mt-3 h-5 w-full" />
|
||||||
|
<SkeletonButton className="mr-6 h-8 w-20 rounded-md p-5" />
|
||||||
|
</li>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</ul>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,13 +1,13 @@
|
|||||||
import { ArrowRightIcon } from "@heroicons/react/solid";
|
|
||||||
|
|
||||||
import classNames from "@calcom/lib/classNames";
|
import classNames from "@calcom/lib/classNames";
|
||||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||||
import { trpc } from "@calcom/trpc/react";
|
import { trpc } from "@calcom/trpc/react";
|
||||||
import { SkeletonAvatar, SkeletonText, SkeletonButton, List } from "@calcom/ui";
|
import { List } from "@calcom/ui";
|
||||||
|
import { ArrowRight } from "@calcom/ui/components/icon";
|
||||||
|
|
||||||
import { CalendarItem } from "../components/CalendarItem";
|
import { AppConnectionItem } from "../components/AppConnectionItem";
|
||||||
import { ConnectedCalendarItem } from "../components/ConnectedCalendarItem";
|
import { ConnectedCalendarItem } from "../components/ConnectedCalendarItem";
|
||||||
import { CreateEventsOnCalendarSelect } from "../components/CreateEventsOnCalendarSelect";
|
import { CreateEventsOnCalendarSelect } from "../components/CreateEventsOnCalendarSelect";
|
||||||
|
import { StepConnectionLoader } from "../components/StepConnectionLoader";
|
||||||
|
|
||||||
interface IConnectCalendarsProps {
|
interface IConnectCalendarsProps {
|
||||||
nextStep: () => void;
|
nextStep: () => void;
|
||||||
@@ -31,13 +31,13 @@ const ConnectedCalendars = (props: IConnectCalendarsProps) => {
|
|||||||
firstCalendar &&
|
firstCalendar &&
|
||||||
firstCalendar.integration &&
|
firstCalendar.integration &&
|
||||||
firstCalendar.integration.title &&
|
firstCalendar.integration.title &&
|
||||||
firstCalendar.integration.imageSrc && (
|
firstCalendar.integration.logo && (
|
||||||
<>
|
<>
|
||||||
<List className="rounded-md border border-gray-200 bg-white p-0 dark:bg-black">
|
<List className="bg-default border-subtle rounded-md border p-0 dark:bg-black ">
|
||||||
<ConnectedCalendarItem
|
<ConnectedCalendarItem
|
||||||
key={firstCalendar.integration.title}
|
key={firstCalendar.integration.title}
|
||||||
name={firstCalendar.integration.title}
|
name={firstCalendar.integration.title}
|
||||||
logo={firstCalendar.integration.imageSrc}
|
logo={firstCalendar.integration.logo}
|
||||||
externalId={
|
externalId={
|
||||||
firstCalendar && firstCalendar.calendars && firstCalendar.calendars.length > 0
|
firstCalendar && firstCalendar.calendars && firstCalendar.calendars.length > 0
|
||||||
? firstCalendar.calendars[0].externalId
|
? firstCalendar.calendars[0].externalId
|
||||||
@@ -49,22 +49,22 @@ const ConnectedCalendars = (props: IConnectCalendarsProps) => {
|
|||||||
</List>
|
</List>
|
||||||
{/* Create event on selected calendar */}
|
{/* Create event on selected calendar */}
|
||||||
<CreateEventsOnCalendarSelect calendar={destinationCalendar} />
|
<CreateEventsOnCalendarSelect calendar={destinationCalendar} />
|
||||||
<p className="mt-4 text-sm text-gray-500">{t("connect_calendars_from_app_store")}</p>
|
<p className="text-subtle mt-4 text-sm">{t("connect_calendars_from_app_store")}</p>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Connect calendars list */}
|
{/* Connect calendars list */}
|
||||||
{firstCalendar === undefined && queryIntegrations.data && queryIntegrations.data.items.length > 0 && (
|
{firstCalendar === undefined && queryIntegrations.data && queryIntegrations.data.items.length > 0 && (
|
||||||
<List className="mx-1 divide-y divide-gray-200 rounded-md border border-gray-200 bg-white p-0 dark:bg-black sm:mx-0">
|
<List className="bg-default divide-subtle border-subtle mx-1 divide-y rounded-md border p-0 dark:bg-black sm:mx-0">
|
||||||
{queryIntegrations.data &&
|
{queryIntegrations.data &&
|
||||||
queryIntegrations.data.items.map((item) => (
|
queryIntegrations.data.items.map((item) => (
|
||||||
<li key={item.title}>
|
<li key={item.title}>
|
||||||
{item.title && item.imageSrc && (
|
{item.title && item.logo && (
|
||||||
<CalendarItem
|
<AppConnectionItem
|
||||||
type={item.type}
|
type={item.type}
|
||||||
title={item.title}
|
title={item.title}
|
||||||
description={item.description}
|
description={item.description}
|
||||||
imageSrc={item.imageSrc}
|
logo={item.logo}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</li>
|
</li>
|
||||||
@@ -72,30 +72,19 @@ const ConnectedCalendars = (props: IConnectCalendarsProps) => {
|
|||||||
</List>
|
</List>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{queryIntegrations.isLoading && (
|
{queryIntegrations.isLoading && <StepConnectionLoader />}
|
||||||
<ul className="divide-y divide-gray-200 rounded-md border border-gray-200 bg-white p-0 dark:bg-black">
|
|
||||||
{[0, 0, 0, 0].map((_item, index) => {
|
|
||||||
return (
|
|
||||||
<li className="flex w-full flex-row justify-center border-b-0 py-6" key={index}>
|
|
||||||
<SkeletonAvatar className="mx-6 h-8 w-8 px-4" />
|
|
||||||
<SkeletonText className="ml-1 mr-4 mt-3 h-5 w-full" />
|
|
||||||
<SkeletonButton className="mr-6 h-8 w-20 rounded-md p-5" />
|
|
||||||
</li>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</ul>
|
|
||||||
)}
|
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
data-testid="save-calendar-button"
|
data-testid="save-calendar-button"
|
||||||
className={classNames(
|
className={classNames(
|
||||||
"mt-8 flex w-full flex-row justify-center rounded-md border border-black bg-black p-2 text-center text-sm text-white",
|
"text-inverted mt-8 flex w-full flex-row justify-center rounded-md border border-black bg-black p-2 text-center text-sm",
|
||||||
disabledNextButton ? "cursor-not-allowed opacity-20" : ""
|
disabledNextButton ? "cursor-not-allowed opacity-20" : ""
|
||||||
)}
|
)}
|
||||||
onClick={() => nextStep()}
|
onClick={() => nextStep()}
|
||||||
disabled={disabledNextButton}>
|
disabled={disabledNextButton}>
|
||||||
{firstCalendar ? `${t("continue")}` : `${t("next_step_text")}`}
|
{firstCalendar ? `${t("continue")}` : `${t("next_step_text")}`}
|
||||||
<ArrowRightIcon className="ml-2 h-4 w-4 self-center" aria-hidden="true" />
|
<ArrowRight className="ml-2 h-4 w-4 self-center" aria-hidden="true" />
|
||||||
</button>
|
</button>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -0,0 +1,67 @@
|
|||||||
|
import classNames from "@calcom/lib/classNames";
|
||||||
|
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||||
|
import { trpc } from "@calcom/trpc/react";
|
||||||
|
import { List } from "@calcom/ui";
|
||||||
|
import { ArrowRight } from "@calcom/ui/components/icon";
|
||||||
|
|
||||||
|
import { AppConnectionItem } from "../components/AppConnectionItem";
|
||||||
|
import { StepConnectionLoader } from "../components/StepConnectionLoader";
|
||||||
|
|
||||||
|
interface ConnectedAppStepProps {
|
||||||
|
nextStep: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
const ConnectedVideoStep = (props: ConnectedAppStepProps) => {
|
||||||
|
const { nextStep } = props;
|
||||||
|
const { data: queryConnectedVideoApps, isLoading } = trpc.viewer.integrations.useQuery({
|
||||||
|
variant: "conferencing",
|
||||||
|
onlyInstalled: false,
|
||||||
|
});
|
||||||
|
const { t } = useLocale();
|
||||||
|
|
||||||
|
const hasAnyInstalledVideoApps = queryConnectedVideoApps?.items.some(
|
||||||
|
(item) => item.credentialIds.length > 0
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{!isLoading && (
|
||||||
|
<List className="bg-default border-subtle divide-subtle scroll-bar mx-1 max-h-[45vh] divide-y !overflow-y-scroll rounded-md border p-0 sm:mx-0">
|
||||||
|
{queryConnectedVideoApps?.items &&
|
||||||
|
queryConnectedVideoApps?.items.map((item) => {
|
||||||
|
if (item.slug === "daily-video") return null; // we dont want to show daily here as it is installed by default
|
||||||
|
return (
|
||||||
|
<li key={item.name}>
|
||||||
|
{item.name && item.logo && (
|
||||||
|
<AppConnectionItem
|
||||||
|
type={item.type}
|
||||||
|
title={item.name}
|
||||||
|
description={item.description}
|
||||||
|
logo={item.logo}
|
||||||
|
installed={item.credentialIds.length > 0}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</li>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</List>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{isLoading && <StepConnectionLoader />}
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
data-testid="save-video-button"
|
||||||
|
className={classNames(
|
||||||
|
"text-inverted mt-8 flex w-full flex-row justify-center rounded-md border border-black bg-black p-2 text-center text-sm",
|
||||||
|
!hasAnyInstalledVideoApps ? "cursor-not-allowed opacity-20" : ""
|
||||||
|
)}
|
||||||
|
disabled={!hasAnyInstalledVideoApps}
|
||||||
|
onClick={() => nextStep()}>
|
||||||
|
{t("next_step_text")}
|
||||||
|
<ArrowRight className="ml-2 h-4 w-4 self-center" aria-hidden="true" />
|
||||||
|
</button>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export { ConnectedVideoStep };
|
||||||
@@ -1,4 +1,3 @@
|
|||||||
import { ArrowRightIcon } from "@heroicons/react/solid";
|
|
||||||
import { useRouter } from "next/router";
|
import { useRouter } from "next/router";
|
||||||
import { useForm } from "react-hook-form";
|
import { useForm } from "react-hook-form";
|
||||||
|
|
||||||
@@ -9,6 +8,7 @@ import type { TRPCClientErrorLike } from "@calcom/trpc/react";
|
|||||||
import { trpc } from "@calcom/trpc/react";
|
import { trpc } from "@calcom/trpc/react";
|
||||||
import type { AppRouter } from "@calcom/trpc/server/routers/_app";
|
import type { AppRouter } from "@calcom/trpc/server/routers/_app";
|
||||||
import { Button, Form } from "@calcom/ui";
|
import { Button, Form } from "@calcom/ui";
|
||||||
|
import { ArrowRight } from "@calcom/ui/components/icon";
|
||||||
|
|
||||||
interface ISetupAvailabilityProps {
|
interface ISetupAvailabilityProps {
|
||||||
nextStep: () => void;
|
nextStep: () => void;
|
||||||
@@ -50,7 +50,7 @@ const SetupAvailability = (props: ISetupAvailabilityProps) => {
|
|||||||
const updateSchedule = trpc.viewer.availability.schedule.update.useMutation(mutationOptions);
|
const updateSchedule = trpc.viewer.availability.schedule.update.useMutation(mutationOptions);
|
||||||
return (
|
return (
|
||||||
<Form
|
<Form
|
||||||
className="w-full bg-white text-black dark:bg-opacity-5 dark:text-white"
|
className="bg-default dark:text-inverted text-emphasis w-full [--cal-brand-accent:#fafafa] dark:bg-opacity-5"
|
||||||
form={availabilityForm}
|
form={availabilityForm}
|
||||||
handleSubmit={async (values) => {
|
handleSubmit={async (values) => {
|
||||||
try {
|
try {
|
||||||
@@ -81,7 +81,7 @@ const SetupAvailability = (props: ISetupAvailabilityProps) => {
|
|||||||
type="submit"
|
type="submit"
|
||||||
className="mt-2 w-full justify-center p-2 text-sm sm:mt-8"
|
className="mt-2 w-full justify-center p-2 text-sm sm:mt-8"
|
||||||
disabled={availabilityForm.formState.isSubmitting}>
|
disabled={availabilityForm.formState.isSubmitting}>
|
||||||
{t("next_step_text")} <ArrowRightIcon className="ml-2 h-4 w-4 self-center" />
|
{t("next_step_text")} <ArrowRight className="ml-2 h-4 w-4 self-center" />
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</Form>
|
</Form>
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
import { ArrowRightIcon } from "@heroicons/react/solid";
|
|
||||||
import { useRouter } from "next/router";
|
import { useRouter } from "next/router";
|
||||||
import type { FormEvent } from "react";
|
import type { FormEvent } from "react";
|
||||||
import { useRef, useState } from "react";
|
import { useRef, useState } from "react";
|
||||||
@@ -11,6 +10,7 @@ import turndown from "@calcom/lib/turndownService";
|
|||||||
import { trpc } from "@calcom/trpc/react";
|
import { trpc } from "@calcom/trpc/react";
|
||||||
import { Button, Editor, ImageUploader, Label, showToast } from "@calcom/ui";
|
import { Button, Editor, ImageUploader, Label, showToast } from "@calcom/ui";
|
||||||
import { Avatar } from "@calcom/ui";
|
import { Avatar } from "@calcom/ui";
|
||||||
|
import { ArrowRight } from "@calcom/ui/components/icon";
|
||||||
|
|
||||||
import type { IOnboardingPageProps } from "../../../pages/getting-started/[[...step]]";
|
import type { IOnboardingPageProps } from "../../../pages/getting-started/[[...step]]";
|
||||||
|
|
||||||
@@ -35,6 +35,7 @@ const UserProfile = (props: IUserProfileProps) => {
|
|||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const createEventType = trpc.viewer.eventTypes.create.useMutation();
|
const createEventType = trpc.viewer.eventTypes.create.useMutation();
|
||||||
const telemetry = useTelemetry();
|
const telemetry = useTelemetry();
|
||||||
|
const [firstRender, setFirstRender] = useState(true);
|
||||||
|
|
||||||
const mutation = trpc.viewer.updateProfile.useMutation({
|
const mutation = trpc.viewer.updateProfile.useMutation({
|
||||||
onSuccess: async (_data, context) => {
|
onSuccess: async (_data, context) => {
|
||||||
@@ -117,7 +118,7 @@ const UserProfile = (props: IUserProfileProps) => {
|
|||||||
name="avatar"
|
name="avatar"
|
||||||
id="avatar"
|
id="avatar"
|
||||||
placeholder="URL"
|
placeholder="URL"
|
||||||
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"
|
className="border-default focus:ring-empthasis mt-1 block w-full rounded-sm border px-3 py-2 text-sm focus:border-gray-800 focus:outline-none"
|
||||||
defaultValue={imageSrc}
|
defaultValue={imageSrc}
|
||||||
/>
|
/>
|
||||||
<div className="flex items-center px-4">
|
<div className="flex items-center px-4">
|
||||||
@@ -142,21 +143,23 @@ const UserProfile = (props: IUserProfileProps) => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<fieldset className="mt-8">
|
<fieldset className="mt-8">
|
||||||
<Label className="mb-2 block text-sm font-medium text-gray-700">{t("about")}</Label>
|
<Label className="text-default mb-2 block text-sm font-medium">{t("about")}</Label>
|
||||||
<Editor
|
<Editor
|
||||||
getText={() => md.render(getValues("bio") || user?.bio || "")}
|
getText={() => md.render(getValues("bio") || user?.bio || "")}
|
||||||
setText={(value: string) => setValue("bio", turndown(value))}
|
setText={(value: string) => setValue("bio", turndown(value))}
|
||||||
excludedToolbarItems={["blockType"]}
|
excludedToolbarItems={["blockType"]}
|
||||||
|
firstRender={firstRender}
|
||||||
|
setFirstRender={setFirstRender}
|
||||||
/>
|
/>
|
||||||
<p className="mt-2 font-sans text-sm font-normal text-gray-600 dark:text-white">
|
<p className="dark:text-inverted text-default mt-2 font-sans text-sm font-normal">
|
||||||
{t("few_sentences_about_yourself")}
|
{t("few_sentences_about_yourself")}
|
||||||
</p>
|
</p>
|
||||||
</fieldset>
|
</fieldset>
|
||||||
<Button
|
<Button
|
||||||
type="submit"
|
type="submit"
|
||||||
className="mt-8 flex w-full flex-row justify-center rounded-md border border-black bg-black p-2 text-center text-sm text-white">
|
className="text-inverted mt-8 flex w-full flex-row justify-center rounded-md border border-black bg-black p-2 text-center text-sm">
|
||||||
{t("finish")}
|
{t("finish")}
|
||||||
<ArrowRightIcon className="ml-2 h-4 w-4 self-center" aria-hidden="true" />
|
<ArrowRight className="ml-2 h-4 w-4 self-center" aria-hidden="true" />
|
||||||
</Button>
|
</Button>
|
||||||
</form>
|
</form>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
import { ArrowRightIcon } from "@heroicons/react/outline";
|
|
||||||
import { zodResolver } from "@hookform/resolvers/zod";
|
import { zodResolver } from "@hookform/resolvers/zod";
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import { useForm } from "react-hook-form";
|
import { useForm } from "react-hook-form";
|
||||||
@@ -10,6 +9,7 @@ import { useLocale } from "@calcom/lib/hooks/useLocale";
|
|||||||
import { telemetryEventTypes, useTelemetry } from "@calcom/lib/telemetry";
|
import { telemetryEventTypes, useTelemetry } from "@calcom/lib/telemetry";
|
||||||
import { trpc } from "@calcom/trpc/react";
|
import { trpc } from "@calcom/trpc/react";
|
||||||
import { Button, TimezoneSelect } from "@calcom/ui";
|
import { Button, TimezoneSelect } from "@calcom/ui";
|
||||||
|
import { ArrowRight } from "@calcom/ui/components/icon";
|
||||||
|
|
||||||
import { UsernameAvailabilityField } from "@components/ui/UsernameAvailability";
|
import { UsernameAvailabilityField } from "@components/ui/UsernameAvailability";
|
||||||
|
|
||||||
@@ -73,7 +73,7 @@ const UserSettings = (props: IUserSettingsProps) => {
|
|||||||
|
|
||||||
{/* Full name textfield */}
|
{/* Full name textfield */}
|
||||||
<div className="w-full">
|
<div className="w-full">
|
||||||
<label htmlFor="name" className="mb-2 block text-sm font-medium text-gray-700">
|
<label htmlFor="name" className="text-default mb-2 block text-sm font-medium">
|
||||||
{t("full_name")}
|
{t("full_name")}
|
||||||
</label>
|
</label>
|
||||||
<input
|
<input
|
||||||
@@ -85,7 +85,7 @@ const UserSettings = (props: IUserSettingsProps) => {
|
|||||||
type="text"
|
type="text"
|
||||||
autoComplete="off"
|
autoComplete="off"
|
||||||
autoCorrect="off"
|
autoCorrect="off"
|
||||||
className="w-full rounded-md border border-gray-300 text-sm"
|
className="border-default w-full rounded-md border text-sm"
|
||||||
/>
|
/>
|
||||||
{errors.name && (
|
{errors.name && (
|
||||||
<p data-testid="required" className="py-2 text-xs text-red-500">
|
<p data-testid="required" className="py-2 text-xs text-red-500">
|
||||||
@@ -95,7 +95,7 @@ const UserSettings = (props: IUserSettingsProps) => {
|
|||||||
</div>
|
</div>
|
||||||
{/* Timezone select field */}
|
{/* Timezone select field */}
|
||||||
<div className="w-full">
|
<div className="w-full">
|
||||||
<label htmlFor="timeZone" className="block text-sm font-medium text-gray-700">
|
<label htmlFor="timeZone" className="text-default block text-sm font-medium">
|
||||||
{t("timezone")}
|
{t("timezone")}
|
||||||
</label>
|
</label>
|
||||||
|
|
||||||
@@ -106,7 +106,7 @@ const UserSettings = (props: IUserSettingsProps) => {
|
|||||||
className="mt-2 w-full rounded-md text-sm"
|
className="mt-2 w-full rounded-md text-sm"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<p className="mt-3 flex flex-row font-sans text-xs leading-tight text-gray-500 dark:text-white">
|
<p className="text-subtle dark:text-inverted mt-3 flex flex-row font-sans text-xs leading-tight">
|
||||||
{t("current_time")} {dayjs().tz(selectedTimeZone).format("LT").toString().toLowerCase()}
|
{t("current_time")} {dayjs().tz(selectedTimeZone).format("LT").toString().toLowerCase()}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
@@ -116,7 +116,7 @@ const UserSettings = (props: IUserSettingsProps) => {
|
|||||||
className="mt-8 flex w-full flex-row justify-center"
|
className="mt-8 flex w-full flex-row justify-center"
|
||||||
disabled={mutation.isLoading}>
|
disabled={mutation.isLoading}>
|
||||||
{t("next_step_text")}
|
{t("next_step_text")}
|
||||||
<ArrowRightIcon className="ml-2 h-4 w-4 self-center" aria-hidden="true" />
|
<ArrowRight className="ml-2 h-4 w-4 self-center" aria-hidden="true" />
|
||||||
</Button>
|
</Button>
|
||||||
</form>
|
</form>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -24,7 +24,9 @@ export default function WizardLayout({
|
|||||||
}, [router.asPath]);
|
}, [router.asPath]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="dark:bg-brand dark:text-brand-contrast min-h-screen text-black" data-testid="onboarding">
|
<div
|
||||||
|
className="dark:bg-brand dark:text-brand-contrast text-emphasis min-h-screen"
|
||||||
|
data-testid="onboarding">
|
||||||
<div>
|
<div>
|
||||||
<Toaster position="bottom-right" />
|
<Toaster position="bottom-right" />
|
||||||
</div>
|
</div>
|
||||||
@@ -34,7 +36,7 @@ export default function WizardLayout({
|
|||||||
<div className="mx-auto sm:max-w-[520px]">
|
<div className="mx-auto sm:max-w-[520px]">
|
||||||
<header>
|
<header>
|
||||||
<p className="font-cal mb-3 text-[28px] font-medium leading-7">{title} </p>
|
<p className="font-cal mb-3 text-[28px] font-medium leading-7">{title} </p>
|
||||||
<p className="font-sans text-sm font-normal text-gray-500">{subtitle} </p>
|
<p className="text-subtle font-sans text-sm font-normal">{subtitle} </p>
|
||||||
</header>
|
</header>
|
||||||
<Steps maxSteps={maxSteps} currentStep={currentStep} navigateToStep={noop} />
|
<Steps maxSteps={maxSteps} currentStep={currentStep} navigateToStep={noop} />
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -59,14 +59,14 @@ const ChangePasswordSection = () => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<form className="divide-y divide-gray-200 lg:col-span-9" onSubmit={changePasswordHandler}>
|
<form className="divide-subtle divide-y lg:col-span-9" onSubmit={changePasswordHandler}>
|
||||||
<div className="py-6 lg:pb-5">
|
<div className="py-6 lg:pb-5">
|
||||||
<div className="my-3">
|
<div className="my-3">
|
||||||
<h2 className="font-cal text-lg font-medium leading-6 text-gray-900">{t("change_password")}</h2>
|
<h2 className="font-cal text-emphasis text-lg font-medium leading-6">{t("change_password")}</h2>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex flex-col space-y-2 sm:flex-row sm:space-y-0">
|
<div className="flex flex-col space-y-2 sm:flex-row sm:space-y-0">
|
||||||
<div className="w-full ltr:mr-2 rtl:ml-2 sm:w-1/2">
|
<div className="w-full ltr:mr-2 rtl:ml-2 sm:w-1/2">
|
||||||
<label htmlFor="current_password" className="block text-sm font-medium text-gray-700">
|
<label htmlFor="current_password" className="text-default block text-sm font-medium">
|
||||||
{t("current_password")}
|
{t("current_password")}
|
||||||
</label>
|
</label>
|
||||||
<div className="mt-1">
|
<div className="mt-1">
|
||||||
@@ -77,13 +77,13 @@ const ChangePasswordSection = () => {
|
|||||||
name="current_password"
|
name="current_password"
|
||||||
id="current_password"
|
id="current_password"
|
||||||
required
|
required
|
||||||
className="block w-full rounded-sm border-gray-300 text-sm"
|
className="border-default block w-full rounded-sm text-sm"
|
||||||
placeholder={t("your_old_password")}
|
placeholder={t("your_old_password")}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="w-full sm:w-1/2">
|
<div className="w-full sm:w-1/2">
|
||||||
<label htmlFor="new_password" className="block text-sm font-medium text-gray-700">
|
<label htmlFor="new_password" className="text-default block text-sm font-medium">
|
||||||
{t("new_password")}
|
{t("new_password")}
|
||||||
</label>
|
</label>
|
||||||
<div className="mt-1">
|
<div className="mt-1">
|
||||||
@@ -94,7 +94,7 @@ const ChangePasswordSection = () => {
|
|||||||
value={newPassword}
|
value={newPassword}
|
||||||
required
|
required
|
||||||
onInput={(e) => setNewPassword(e.currentTarget.value)}
|
onInput={(e) => setNewPassword(e.currentTarget.value)}
|
||||||
className="block w-full rounded-sm border-gray-300 text-sm"
|
className="border-default block w-full rounded-sm text-sm"
|
||||||
placeholder={t("super_secure_new_password")}
|
placeholder={t("super_secure_new_password")}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -70,10 +70,10 @@ const DisableTwoFactorAuthModal = ({ onDisable, onCancel }: DisableTwoFactorAuth
|
|||||||
<div className="mb-4">
|
<div className="mb-4">
|
||||||
<PasswordField
|
<PasswordField
|
||||||
labelProps={{
|
labelProps={{
|
||||||
className: "block text-sm font-medium text-gray-700",
|
className: "block text-sm font-medium text-default",
|
||||||
}}
|
}}
|
||||||
{...form.register("password")}
|
{...form.register("password")}
|
||||||
className="mt-1 block w-full rounded-md border border-gray-300 px-3 py-2 text-sm focus:border-black focus:outline-none focus:ring-black"
|
className="border-default mt-1 block w-full rounded-md border px-3 py-2 text-sm focus:border-black focus:outline-none focus:ring-black"
|
||||||
/>
|
/>
|
||||||
<Label className="mt-4"> {t("2fa_code")}</Label>
|
<Label className="mt-4"> {t("2fa_code")}</Label>
|
||||||
|
|
||||||
@@ -82,7 +82,7 @@ const DisableTwoFactorAuthModal = ({ onDisable, onCancel }: DisableTwoFactorAuth
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="mt-5 sm:mt-4 sm:flex sm:flex-row-reverse">
|
<div className="mt-5 sm:mt-4 sm:flex sm:flex-row-reverse">
|
||||||
<Button type="submit" className="ltr:ml-2 ltr:mr-2 rtl:ml-2" disabled={isDisabling}>
|
<Button type="submit" className="ms-2 me-2" disabled={isDisabling}>
|
||||||
{t("disable")}
|
{t("disable")}
|
||||||
</Button>
|
</Button>
|
||||||
<Button color="secondary" onClick={onCancel}>
|
<Button color="secondary" onClick={onCancel}>
|
||||||
|
|||||||
@@ -22,14 +22,14 @@ const DisableUserImpersonation = ({ disableImpersonation }: { disableImpersonati
|
|||||||
<div className="flex flex-col justify-between pt-9 pl-2 sm:flex-row">
|
<div className="flex flex-col justify-between pt-9 pl-2 sm:flex-row">
|
||||||
<div>
|
<div>
|
||||||
<div className="flex flex-row items-center">
|
<div className="flex flex-row items-center">
|
||||||
<h2 className="font-cal text-lg font-medium leading-6 text-gray-900">
|
<h2 className="font-cal text-emphasis text-lg font-medium leading-6">
|
||||||
{t("user_impersonation_heading")}
|
{t("user_impersonation_heading")}
|
||||||
</h2>
|
</h2>
|
||||||
<Badge className="ml-2 text-xs" variant={!disableImpersonation ? "success" : "gray"}>
|
<Badge className="ml-2 text-xs" variant={!disableImpersonation ? "success" : "gray"}>
|
||||||
{!disableImpersonation ? t("enabled") : t("disabled")}
|
{!disableImpersonation ? t("enabled") : t("disabled")}
|
||||||
</Badge>
|
</Badge>
|
||||||
</div>
|
</div>
|
||||||
<p className="mt-1 text-sm text-gray-500">{t("user_impersonation_description")}</p>
|
<p className="text-subtle mt-1 text-sm">{t("user_impersonation_description")}</p>
|
||||||
</div>
|
</div>
|
||||||
<div className="mt-5 sm:mt-0 sm:self-center">
|
<div className="mt-5 sm:mt-0 sm:self-center">
|
||||||
<Button
|
<Button
|
||||||
|
|||||||
@@ -135,7 +135,7 @@ const EnableTwoFactorModal = ({ onEnable, onCancel }: EnableTwoFactorModalProps)
|
|||||||
<WithStep step={SetupStep.ConfirmPassword} current={step}>
|
<WithStep step={SetupStep.ConfirmPassword} current={step}>
|
||||||
<form onSubmit={handleSetup}>
|
<form onSubmit={handleSetup}>
|
||||||
<div className="mb-4">
|
<div className="mb-4">
|
||||||
<label htmlFor="password" className="mt-4 block text-sm font-medium text-gray-700">
|
<label htmlFor="password" className="text-default mt-4 block text-sm font-medium">
|
||||||
{t("password")}
|
{t("password")}
|
||||||
</label>
|
</label>
|
||||||
<div className="mt-1">
|
<div className="mt-1">
|
||||||
@@ -146,7 +146,7 @@ const EnableTwoFactorModal = ({ onEnable, onCancel }: EnableTwoFactorModalProps)
|
|||||||
required
|
required
|
||||||
value={password}
|
value={password}
|
||||||
onInput={(e) => setPassword(e.currentTarget.value)}
|
onInput={(e) => setPassword(e.currentTarget.value)}
|
||||||
className="block w-full rounded-sm border-gray-300 text-sm"
|
className="border-default block w-full rounded-sm text-sm"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -178,22 +178,19 @@ const EnableTwoFactorModal = ({ onEnable, onCancel }: EnableTwoFactorModalProps)
|
|||||||
<WithStep step={SetupStep.ConfirmPassword} current={step}>
|
<WithStep step={SetupStep.ConfirmPassword} current={step}>
|
||||||
<Button
|
<Button
|
||||||
type="submit"
|
type="submit"
|
||||||
className="ltr:ml-2 ltr:mr-2 rtl:ml-2"
|
className="ms-2 me-2"
|
||||||
onClick={handleSetup}
|
onClick={handleSetup}
|
||||||
disabled={password.length === 0 || isSubmitting}>
|
disabled={password.length === 0 || isSubmitting}>
|
||||||
{t("continue")}
|
{t("continue")}
|
||||||
</Button>
|
</Button>
|
||||||
</WithStep>
|
</WithStep>
|
||||||
<WithStep step={SetupStep.DisplayQrCode} current={step}>
|
<WithStep step={SetupStep.DisplayQrCode} current={step}>
|
||||||
<Button
|
<Button type="submit" className="ms-2 me-2" onClick={() => setStep(SetupStep.EnterTotpCode)}>
|
||||||
type="submit"
|
|
||||||
className="ltr:ml-2 ltr:mr-2 rtl:ml-2"
|
|
||||||
onClick={() => setStep(SetupStep.EnterTotpCode)}>
|
|
||||||
{t("continue")}
|
{t("continue")}
|
||||||
</Button>
|
</Button>
|
||||||
</WithStep>
|
</WithStep>
|
||||||
<WithStep step={SetupStep.EnterTotpCode} current={step}>
|
<WithStep step={SetupStep.EnterTotpCode} current={step}>
|
||||||
<Button type="submit" className="ltr:ml-2 ltr:mr-2 rtl:ml-2" disabled={isSubmitting}>
|
<Button type="submit" className="ms-2 me-2" disabled={isSubmitting}>
|
||||||
{t("enable")}
|
{t("enable")}
|
||||||
</Button>
|
</Button>
|
||||||
</WithStep>
|
</WithStep>
|
||||||
|
|||||||
@@ -17,12 +17,12 @@ const TwoFactorAuthSection = ({ twoFactorEnabled }: { twoFactorEnabled: boolean
|
|||||||
<div className="flex flex-col justify-between pt-9 pl-2 sm:flex-row">
|
<div className="flex flex-col justify-between pt-9 pl-2 sm:flex-row">
|
||||||
<div>
|
<div>
|
||||||
<div className="flex flex-row items-center">
|
<div className="flex flex-row items-center">
|
||||||
<h2 className="font-cal text-lg font-medium leading-6 text-gray-900">{t("2fa")}</h2>
|
<h2 className="font-cal text-emphasis text-lg font-medium leading-6">{t("2fa")}</h2>
|
||||||
<Badge className="ml-2 text-xs" variant={enabled ? "success" : "gray"}>
|
<Badge className="ml-2 text-xs" variant={enabled ? "success" : "gray"}>
|
||||||
{enabled ? t("enabled") : t("disabled")}
|
{enabled ? t("enabled") : t("disabled")}
|
||||||
</Badge>
|
</Badge>
|
||||||
</div>
|
</div>
|
||||||
<p className="mt-1 text-sm text-gray-500">{t("add_an_extra_layer_of_security")}</p>
|
<p className="text-subtle mt-1 text-sm">{t("add_an_extra_layer_of_security")}</p>
|
||||||
</div>
|
</div>
|
||||||
<div className="mt-5 sm:mt-0 sm:self-center">
|
<div className="mt-5 sm:mt-0 sm:self-center">
|
||||||
<Button
|
<Button
|
||||||
|
|||||||
@@ -1,16 +1,16 @@
|
|||||||
import { FiShield } from "@calcom/ui/components/icon";
|
import { Shield } from "@calcom/ui/components/icon";
|
||||||
|
|
||||||
const TwoFactorModalHeader = ({ title, description }: { title: string; description: string }) => {
|
const TwoFactorModalHeader = ({ title, description }: { title: string; description: string }) => {
|
||||||
return (
|
return (
|
||||||
<div className="mb-4 sm:flex sm:items-start">
|
<div className="mb-4 sm:flex sm:items-start">
|
||||||
<div className="bg-brand text-brandcontrast dark:bg-darkmodebrand dark:text-darkmodebrandcontrast mx-auto flex h-12 w-12 flex-shrink-0 items-center justify-center rounded-full bg-opacity-5 sm:mx-0 sm:h-10 sm:w-10">
|
<div className="bg-brand text-brandcontrast dark:bg-darkmodebrand dark:text-darkmodebrandcontrast mx-auto flex h-12 w-12 flex-shrink-0 items-center justify-center rounded-full bg-opacity-5 sm:mx-0 sm:h-10 sm:w-10">
|
||||||
<FiShield className="h-6 w-6 text-white" />
|
<Shield className="text-inverted h-6 w-6" />
|
||||||
</div>
|
</div>
|
||||||
<div className="mt-3 text-center sm:mt-0 sm:ml-4 sm:text-left">
|
<div className="mt-3 text-center sm:mt-0 sm:ml-4 sm:text-left">
|
||||||
<h3 className="font-cal text-lg font-medium leading-6 text-gray-900" id="modal-title">
|
<h3 className="font-cal text-emphasis text-lg font-medium leading-6" id="modal-title">
|
||||||
{title}
|
{title}
|
||||||
</h3>
|
</h3>
|
||||||
<p className="text-sm text-gray-400">{description}</p>
|
<p className="text-muted text-sm">{description}</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,85 +0,0 @@
|
|||||||
import { useMutation } from "@tanstack/react-query";
|
|
||||||
|
|
||||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
|
||||||
import { trpc } from "@calcom/trpc/react";
|
|
||||||
import { Badge, showToast, Switch } from "@calcom/ui";
|
|
||||||
import { FiArrowLeft } from "@calcom/ui/components/icon";
|
|
||||||
|
|
||||||
export function CalendarSwitch(props: {
|
|
||||||
type: string;
|
|
||||||
externalId: string;
|
|
||||||
title: string;
|
|
||||||
defaultSelected: boolean;
|
|
||||||
isSelected: boolean;
|
|
||||||
}) {
|
|
||||||
const { t } = useLocale();
|
|
||||||
|
|
||||||
const utils = trpc.useContext();
|
|
||||||
|
|
||||||
const mutation = useMutation<
|
|
||||||
unknown,
|
|
||||||
unknown,
|
|
||||||
{
|
|
||||||
isOn: boolean;
|
|
||||||
}
|
|
||||||
>(
|
|
||||||
async ({ isOn }) => {
|
|
||||||
const body = {
|
|
||||||
integration: props.type,
|
|
||||||
externalId: props.externalId,
|
|
||||||
};
|
|
||||||
if (isOn) {
|
|
||||||
const res = await fetch("/api/availability/calendar", {
|
|
||||||
method: "POST",
|
|
||||||
headers: {
|
|
||||||
"Content-Type": "application/json",
|
|
||||||
},
|
|
||||||
body: JSON.stringify(body),
|
|
||||||
});
|
|
||||||
if (!res.ok) {
|
|
||||||
throw new Error("Something went wrong");
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
const res = await fetch("/api/availability/calendar", {
|
|
||||||
method: "DELETE",
|
|
||||||
headers: {
|
|
||||||
"Content-Type": "application/json",
|
|
||||||
},
|
|
||||||
body: JSON.stringify(body),
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!res.ok) {
|
|
||||||
throw new Error("Something went wrong");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
async onSettled() {
|
|
||||||
await utils.viewer.integrations.invalidate();
|
|
||||||
},
|
|
||||||
onError() {
|
|
||||||
showToast(`Something went wrong when toggling "${props.title}""`, "error");
|
|
||||||
},
|
|
||||||
}
|
|
||||||
);
|
|
||||||
return (
|
|
||||||
<div className="flex space-x-2 py-1 rtl:space-x-reverse">
|
|
||||||
<Switch
|
|
||||||
key={props.externalId}
|
|
||||||
name="enabled"
|
|
||||||
label={props.title}
|
|
||||||
defaultChecked={props.isSelected}
|
|
||||||
onCheckedChange={(isOn: boolean) => {
|
|
||||||
mutation.mutate({ isOn });
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
{props.defaultSelected && (
|
|
||||||
<Badge variant="gray">
|
|
||||||
<div className="flex">
|
|
||||||
<FiArrowLeft className="mr-1" /> {t("adding_events_to")}
|
|
||||||
</div>
|
|
||||||
</Badge>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
Loaded 100 of 1461 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user