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 |
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,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"]}]'
|
|
||||||
+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
|
||||||
@@ -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.
|
||||||
|
|
||||||
|
|||||||
@@ -158,6 +158,15 @@ yarn dx
|
|||||||
```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
|
||||||
|
|
||||||
@@ -166,34 +175,29 @@ echo 'NEXT_PUBLIC_DEBUG=1' >> .env
|
|||||||
```
|
```
|
||||||
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://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>
|
||||||
|
|
||||||
|
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 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`)
|
||||||
|
|
||||||
@@ -460,6 +464,10 @@ following
|
|||||||
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.
|
||||||
|
|||||||
@@ -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": {
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -36,6 +36,7 @@ const Component = ({
|
|||||||
tos,
|
tos,
|
||||||
privacy,
|
privacy,
|
||||||
isProOnly,
|
isProOnly,
|
||||||
|
teamsPlanRequired,
|
||||||
descriptionItems,
|
descriptionItems,
|
||||||
isTemplate,
|
isTemplate,
|
||||||
dependencies,
|
dependencies,
|
||||||
@@ -152,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 = {
|
||||||
@@ -192,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 = {
|
||||||
@@ -241,7 +244,9 @@ const Component = ({
|
|||||||
</div>
|
</div>
|
||||||
<h4 className="text-emphasis mt-8 font-semibold ">{t("pricing")}</h4>
|
<h4 className="text-emphasis mt-8 font-semibold ">{t("pricing")}</h4>
|
||||||
<span className="text-default">
|
<span className="text-default">
|
||||||
{price === 0 ? (
|
{teamsPlanRequired ? (
|
||||||
|
t("teams_plan_required")
|
||||||
|
) : price === 0 ? (
|
||||||
t("free_to_use_apps")
|
t("free_to_use_apps")
|
||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
@@ -358,6 +363,7 @@ 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;
|
||||||
|
|||||||
@@ -18,9 +18,9 @@ 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")} hideHeadingOnMobile>
|
<Shell {...rest} actions={actions?.("block")} hideHeadingOnMobile>
|
||||||
<div className="flex flex-col xl:flex-row">
|
<div className="flex flex-col xl:flex-row">
|
||||||
@@ -29,8 +29,8 @@ export default function AppsLayout({ children, actions, emptyStore, ...rest }: A
|
|||||||
<EmptyScreen
|
<EmptyScreen
|
||||||
Icon={AlertCircle}
|
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")}
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
|
|||||||
@@ -72,7 +72,7 @@ const AvailableTimes: FC<AvailableTimesProps> = ({
|
|||||||
slotUtcStartDate: slot.time,
|
slotUtcStartDate: slot.time,
|
||||||
eventTypeId,
|
eventTypeId,
|
||||||
slotUtcEndDate: dayjs(slot.time).utc().add(duration, "minutes").format(),
|
slotUtcEndDate: dayjs(slot.time).utc().add(duration, "minutes").format(),
|
||||||
bookingAttendees: bookingAttendees || undefined,
|
bookingUid: slot.bookingUid,
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -29,7 +29,7 @@ import {
|
|||||||
TableActions,
|
TableActions,
|
||||||
TextAreaField,
|
TextAreaField,
|
||||||
} from "@calcom/ui";
|
} from "@calcom/ui";
|
||||||
import { Check, Clock, MapPin, RefreshCcw, Send, Slash, X, CreditCard } 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";
|
||||||
|
|
||||||
@@ -119,7 +119,7 @@ function BookingListItem(booking: BookingItemProps) {
|
|||||||
onClick: () => {
|
onClick: () => {
|
||||||
setRejectionDialogIsOpen(true);
|
setRejectionDialogIsOpen(true);
|
||||||
},
|
},
|
||||||
icon: Slash,
|
icon: Ban,
|
||||||
disabled: mutation.isLoading,
|
disabled: mutation.isLoading,
|
||||||
},
|
},
|
||||||
// For bookings with payment, only confirm if the booking is paid for
|
// For bookings with payment, only confirm if the booking is paid for
|
||||||
|
|||||||
@@ -237,7 +237,7 @@ const BookingPage = ({
|
|||||||
eventTypeId: eventType.id,
|
eventTypeId: eventType.id,
|
||||||
slotUtcStartDate: dayjs(queryDate).utc().format(),
|
slotUtcStartDate: dayjs(queryDate).utc().format(),
|
||||||
slotUtcEndDate: dayjs(queryDate).utc().add(parseInt(queryDuration), "minutes").format(),
|
slotUtcEndDate: dayjs(queryDate).utc().add(parseInt(queryDuration), "minutes").format(),
|
||||||
bookingAttendees: currentSlotBooking ? currentSlotBooking.attendees.length : undefined,
|
bookingUid: currentSlotBooking?.uid,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -322,7 +322,6 @@ export const EventAdvancedTab = ({ eventType, team }: Pick<EventTypeSetupProps,
|
|||||||
disabled={seatsLocked.disabled}
|
disabled={seatsLocked.disabled}
|
||||||
defaultValue={value || 2}
|
defaultValue={value || 2}
|
||||||
min={1}
|
min={1}
|
||||||
className="w-24"
|
|
||||||
addOnSuffix={<>{t("seats")}</>}
|
addOnSuffix={<>{t("seats")}</>}
|
||||||
onChange={(e) => {
|
onChange={(e) => {
|
||||||
onChange(Math.abs(Number(e.target.value)));
|
onChange(Math.abs(Number(e.target.value)));
|
||||||
|
|||||||
@@ -119,7 +119,7 @@ const EventTypeScheduleDetails = ({
|
|||||||
{format(dayRange.startTime, timeFormat === 12)}
|
{format(dayRange.startTime, timeFormat === 12)}
|
||||||
</span>
|
</span>
|
||||||
<span className="ms-4">-</span>
|
<span className="ms-4">-</span>
|
||||||
<div className="ml-6">{format(dayRange.endTime, timeFormat === 12)}</div>
|
<div className="ml-6 sm:w-28">{format(dayRange.endTime, timeFormat === 12)}</div>
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -108,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 = [
|
||||||
@@ -149,12 +149,25 @@ export const EventLimitsTab = ({ eventType }: Pick<EventTypeSetupProps, "eventTy
|
|||||||
const bookingLimitsLocked = shouldLockDisableProps("bookingLimits");
|
const bookingLimitsLocked = shouldLockDisableProps("bookingLimits");
|
||||||
const durationLimitsLocked = shouldLockDisableProps("durationLimits");
|
const durationLimitsLocked = shouldLockDisableProps("durationLimits");
|
||||||
const periodTypeLocked = shouldLockDisableProps("periodType");
|
const periodTypeLocked = shouldLockDisableProps("periodType");
|
||||||
|
const offsetStartLockedProps = shouldLockDisableProps("offsetStart");
|
||||||
|
|
||||||
const optionsPeriod = [
|
const optionsPeriod = [
|
||||||
{ value: 1, label: t("calendar_days") },
|
{ value: 1, label: t("calendar_days") },
|
||||||
{ value: 0, label: t("business_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">
|
||||||
@@ -432,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>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -390,7 +390,12 @@ export const EventSetupTab = (
|
|||||||
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", {
|
||||||
|
|||||||
@@ -170,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)}
|
||||||
|
|||||||
@@ -170,7 +170,7 @@ function EventTypeSingleLayout({
|
|||||||
|
|
||||||
// Define tab navigation here
|
// Define tab navigation here
|
||||||
const EventTypeTabs = useMemo(() => {
|
const EventTypeTabs = useMemo(() => {
|
||||||
let navigation = getNavigation({
|
const navigation = getNavigation({
|
||||||
t,
|
t,
|
||||||
eventType,
|
eventType,
|
||||||
enabledAppsNumber,
|
enabledAppsNumber,
|
||||||
@@ -210,7 +210,7 @@ function EventTypeSingleLayout({
|
|||||||
}
|
}
|
||||||
if (isManagedEventType || isChildrenManagedEventType) {
|
if (isManagedEventType || isChildrenManagedEventType) {
|
||||||
// Removing apps and workflows for manageg event types by admins v1
|
// Removing apps and workflows for manageg event types by admins v1
|
||||||
navigation = navigation.slice(0, -2);
|
navigation.splice(-2, 1);
|
||||||
} else {
|
} else {
|
||||||
navigation.push({
|
navigation.push({
|
||||||
name: "webhooks",
|
name: "webhooks",
|
||||||
|
|||||||
+7
-5
@@ -3,15 +3,16 @@ 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;
|
||||||
logo: string;
|
logo: string;
|
||||||
type: App["type"];
|
type: App["type"];
|
||||||
|
installed?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
const CalendarItem = (props: ICalendarItem) => {
|
const AppConnectionItem = (props: IAppConnectionItem) => {
|
||||||
const { title, logo, 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">
|
||||||
@@ -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 };
|
||||||
@@ -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;
|
||||||
@@ -33,7 +33,7 @@ const ConnectedCalendars = (props: IConnectCalendarsProps) => {
|
|||||||
firstCalendar.integration.title &&
|
firstCalendar.integration.title &&
|
||||||
firstCalendar.integration.logo && (
|
firstCalendar.integration.logo && (
|
||||||
<>
|
<>
|
||||||
<List className="bg-default border-subtle rounded-md border 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}
|
||||||
@@ -60,7 +60,7 @@ const ConnectedCalendars = (props: IConnectCalendarsProps) => {
|
|||||||
queryIntegrations.data.items.map((item) => (
|
queryIntegrations.data.items.map((item) => (
|
||||||
<li key={item.title}>
|
<li key={item.title}>
|
||||||
{item.title && item.logo && (
|
{item.title && item.logo && (
|
||||||
<CalendarItem
|
<AppConnectionItem
|
||||||
type={item.type}
|
type={item.type}
|
||||||
title={item.title}
|
title={item.title}
|
||||||
description={item.description}
|
description={item.description}
|
||||||
@@ -72,19 +72,8 @@ const ConnectedCalendars = (props: IConnectCalendarsProps) => {
|
|||||||
</List>
|
</List>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{queryIntegrations.isLoading && (
|
{queryIntegrations.isLoading && <StepConnectionLoader />}
|
||||||
<ul className="bg-default divide-subtle border-subtle divide-y rounded-md border 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"
|
||||||
@@ -95,7 +84,7 @@ const ConnectedCalendars = (props: IConnectCalendarsProps) => {
|
|||||||
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;
|
||||||
@@ -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]]";
|
||||||
|
|
||||||
@@ -159,7 +159,7 @@ const UserProfile = (props: IUserProfileProps) => {
|
|||||||
type="submit"
|
type="submit"
|
||||||
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">
|
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";
|
||||||
|
|
||||||
@@ -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>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -31,7 +31,7 @@ const Member = ({ member, teamName }: { member: MemberType; teamName: string | n
|
|||||||
{!isBioEmpty ? (
|
{!isBioEmpty ? (
|
||||||
<>
|
<>
|
||||||
<div
|
<div
|
||||||
className=" text-subtle text-sm [&_a]:text-blue-500 [&_a]:underline [&_a]:hover:text-blue-600"
|
className=" text-subtle break-words text-sm [&_a]:text-blue-500 [&_a]:underline [&_a]:hover:text-blue-600"
|
||||||
dangerouslySetInnerHTML={{ __html: md.render(member.bio || "") }}
|
dangerouslySetInnerHTML={{ __html: md.render(member.bio || "") }}
|
||||||
/>
|
/>
|
||||||
</>
|
</>
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
import { StarIcon as StarIconSolid } from "@heroicons/react/solid";
|
|
||||||
import classNames from "classnames";
|
import classNames from "classnames";
|
||||||
import { debounce, noop } from "lodash";
|
import { debounce, noop } from "lodash";
|
||||||
import { useRouter } from "next/router";
|
import { useRouter } from "next/router";
|
||||||
@@ -15,6 +14,7 @@ import type { RouterOutputs } 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, Dialog, DialogClose, DialogContent, Input, Label } from "@calcom/ui";
|
import { Button, Dialog, DialogClose, DialogContent, Input, Label } from "@calcom/ui";
|
||||||
|
import { Star as StarSolid } from "@calcom/ui/components/icon";
|
||||||
import { Check, Edit2, ExternalLink } from "@calcom/ui/components/icon";
|
import { Check, Edit2, ExternalLink } from "@calcom/ui/components/icon";
|
||||||
|
|
||||||
export enum UsernameChangeStatusEnum {
|
export enum UsernameChangeStatusEnum {
|
||||||
@@ -230,12 +230,16 @@ const PremiumTextfield = (props: ICustomUsernameProps) => {
|
|||||||
<div className="absolute top-0 right-2 flex flex-row">
|
<div className="absolute top-0 right-2 flex flex-row">
|
||||||
<span
|
<span
|
||||||
className={classNames(
|
className={classNames(
|
||||||
"mx-2 py-1",
|
"mx-2 py-2",
|
||||||
isInputUsernamePremium ? "text-orange-400" : "",
|
isInputUsernamePremium ? "text-transparent" : "",
|
||||||
usernameIsAvailable ? "" : ""
|
usernameIsAvailable ? "" : ""
|
||||||
)}>
|
)}>
|
||||||
{isInputUsernamePremium ? <StarIconSolid className="mt-[2px] h-4 w-4" /> : <></>}
|
{isInputUsernamePremium ? <StarSolid className="mt-[2px] h-4 w-4 fill-orange-400" /> : <></>}
|
||||||
{!isInputUsernamePremium && usernameIsAvailable ? <Check className="mt-2 h-4 w-4" /> : <></>}
|
{!isInputUsernamePremium && usernameIsAvailable ? (
|
||||||
|
<Check className="mt-[2px] h-4 w-4" />
|
||||||
|
) : (
|
||||||
|
<></>
|
||||||
|
)}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
+105
-44
@@ -26,7 +26,10 @@ const I18nextAdapter = appWithTranslation<NextJsAppProps<SSRConfig> & { children
|
|||||||
);
|
);
|
||||||
|
|
||||||
// Workaround for https://github.com/vercel/next.js/issues/8592
|
// Workaround for https://github.com/vercel/next.js/issues/8592
|
||||||
export type AppProps = Omit<NextAppProps<WithNonceProps & Record<string, unknown>>, "Component"> & {
|
export type AppProps = Omit<
|
||||||
|
NextAppProps<WithNonceProps & { themeBasis?: string } & Record<string, unknown>>,
|
||||||
|
"Component"
|
||||||
|
> & {
|
||||||
Component: NextAppProps["Component"] & {
|
Component: NextAppProps["Component"] & {
|
||||||
requiresLicense?: boolean;
|
requiresLicense?: boolean;
|
||||||
isThemeSupported?: boolean;
|
isThemeSupported?: boolean;
|
||||||
@@ -72,58 +75,21 @@ const enum ThemeSupport {
|
|||||||
Booking = "userConfigured",
|
Booking = "userConfigured",
|
||||||
}
|
}
|
||||||
|
|
||||||
const CalcomThemeProvider = (
|
type CalcomThemeProps = PropsWithChildren<
|
||||||
props: PropsWithChildren<
|
Pick<AppProps["pageProps"], "nonce" | "themeBasis"> &
|
||||||
WithNonceProps & {
|
Pick<AppProps["Component"], "isBookingPage" | "isThemeSupported">
|
||||||
isBookingPage?: boolean | ((arg: { router: NextRouter }) => boolean);
|
>;
|
||||||
isThemeSupported?: boolean;
|
const CalcomThemeProvider = (props: CalcomThemeProps) => {
|
||||||
}
|
|
||||||
>
|
|
||||||
) => {
|
|
||||||
// We now support the inverse of how we handled it in the past. Setting this to false will disable theme.
|
|
||||||
// undefined or true means we use system theme
|
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const isBookingPage = (() => {
|
|
||||||
if (typeof props.isBookingPage === "function") {
|
|
||||||
return props.isBookingPage({ router: router });
|
|
||||||
}
|
|
||||||
|
|
||||||
return props.isBookingPage;
|
|
||||||
})();
|
|
||||||
|
|
||||||
const themeSupport = isBookingPage
|
|
||||||
? ThemeSupport.Booking
|
|
||||||
: // if isThemeSupported is explicitly false, we don't use theme there
|
|
||||||
props.isThemeSupported === false
|
|
||||||
? ThemeSupport.None
|
|
||||||
: ThemeSupport.App;
|
|
||||||
|
|
||||||
const forcedTheme = themeSupport === ThemeSupport.None ? "light" : undefined;
|
|
||||||
// Use namespace of embed to ensure same namespaced embed are displayed with same theme. This allows different embeds on the same website to be themed differently
|
// Use namespace of embed to ensure same namespaced embed are displayed with same theme. This allows different embeds on the same website to be themed differently
|
||||||
// One such example is our Embeds Demo and Testing page at http://localhost:3100
|
// One such example is our Embeds Demo and Testing page at http://localhost:3100
|
||||||
// Having `getEmbedNamespace` defined on window before react initializes the app, ensures that embedNamespace is available on the first mount and can be used as part of storageKey
|
// Having `getEmbedNamespace` defined on window before react initializes the app, ensures that embedNamespace is available on the first mount and can be used as part of storageKey
|
||||||
const embedNamespace = typeof window !== "undefined" ? window.getEmbedNamespace() : null;
|
const embedNamespace = typeof window !== "undefined" ? window.getEmbedNamespace() : null;
|
||||||
const isEmbedMode = typeof embedNamespace === "string";
|
const isEmbedMode = typeof embedNamespace === "string";
|
||||||
|
|
||||||
const storageKey = isEmbedMode
|
|
||||||
? `embed-theme-${embedNamespace}`
|
|
||||||
: themeSupport === ThemeSupport.App
|
|
||||||
? "app-theme"
|
|
||||||
: themeSupport === ThemeSupport.Booking
|
|
||||||
? "booking-theme"
|
|
||||||
: undefined;
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ThemeProvider
|
<ThemeProvider {...getThemeProviderProps({ props, isEmbedMode, embedNamespace, router })}>
|
||||||
nonce={props.nonce}
|
|
||||||
enableColorScheme={false}
|
|
||||||
enableSystem={themeSupport !== ThemeSupport.None}
|
|
||||||
forcedTheme={forcedTheme}
|
|
||||||
storageKey={storageKey}
|
|
||||||
// next-themes doesn't listen to changes on storageKey. So we need to force a re-render when storageKey changes
|
|
||||||
// This is how login to dashboard soft navigation changes theme from light to dark
|
|
||||||
key={storageKey}
|
|
||||||
attribute="class">
|
|
||||||
{/* Embed Mode can be detected reliably only on client side here as there can be static generated pages as well which can't determine if it's embed mode at backend */}
|
{/* Embed Mode can be detected reliably only on client side here as there can be static generated pages as well which can't determine if it's embed mode at backend */}
|
||||||
{/* color-scheme makes background:transparent not work in iframe which is required by embed. */}
|
{/* color-scheme makes background:transparent not work in iframe which is required by embed. */}
|
||||||
{typeof window !== "undefined" && !isEmbedMode && (
|
{typeof window !== "undefined" && !isEmbedMode && (
|
||||||
@@ -140,6 +106,100 @@ const CalcomThemeProvider = (
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The most important job for this fn is to generate correct storageKey for theme persistenc.
|
||||||
|
* `storageKey` is important because that key is listened for changes(using [`storage`](https://developer.mozilla.org/en-US/docs/Web/API/Window/storage_event) event) and any pages opened will change it's theme based on that(as part of next-themes implementation).
|
||||||
|
* Choosing the right storageKey avoids theme flickering caused by another page using different theme
|
||||||
|
* So, we handle all the cases here namely,
|
||||||
|
* - Both Booking Pages, /free/30min and /pro/30min but configured with different themes but being operated together.
|
||||||
|
* - Embeds using different namespace. They can be completely themed different on the same page.
|
||||||
|
* - Embeds using the same namespace but showing different cal.com links with different themes
|
||||||
|
* - Embeds using the same namespace and showing same cal.com links with different themes(Different theme is possible for same cal.com link in case of embed because of theme config available in embed)
|
||||||
|
* - App has different theme then Booking Pages.
|
||||||
|
*
|
||||||
|
* All the above cases have one thing in common, which is the origin and thus localStorage is shared and thus `storageKey` is critical to avoid theme flickering.
|
||||||
|
*
|
||||||
|
* Some things to note:
|
||||||
|
* - There is a side effect of so many factors in `storageKey` that many localStorage keys will be created if a user goes through all these scenarios(e.g like booking a lot of different users)
|
||||||
|
* - Some might recommend disabling localStorage persistence but that doesn't give good UX as then we would default to light theme always for a few seconds before switching to dark theme(if that's the user's preference).
|
||||||
|
* - We can't disable [`storage`](https://developer.mozilla.org/en-US/docs/Web/API/Window/storage_event) event handling as well because changing theme in one tab won't change the theme without refresh in other tabs. That's again a bad UX
|
||||||
|
* - Theme flickering becomes infinitely ongoing in case of embeds because of the browser's delay in processing `storage` event within iframes. Consider two embeds simulatenously opened with pages A and B. Note the timeline and keep in mind that it happened
|
||||||
|
* because 'setItem(A)' and 'Receives storageEvent(A)' allowed executing setItem(B) in b/w because of the delay.
|
||||||
|
* - t1 -> setItem(A) & Fires storageEvent(A) - On Page A) - Current State(A)
|
||||||
|
* - t2 -> setItem(B) & Fires storageEvent(B) - On Page B) - Current State(B)
|
||||||
|
* - t3 -> Receives storageEvent(A) & thus setItem(A) & thus fires storageEvent(A) (On Page B) - Current State(A)
|
||||||
|
* - t4 -> Receives storageEvent(B) & thus setItem(B) & thus fires storageEvent(B) (On Page A) - Current State(B)
|
||||||
|
* - ... and so on ...
|
||||||
|
*/
|
||||||
|
function getThemeProviderProps({
|
||||||
|
props,
|
||||||
|
isEmbedMode,
|
||||||
|
embedNamespace,
|
||||||
|
router,
|
||||||
|
}: {
|
||||||
|
props: Omit<CalcomThemeProps, "children">;
|
||||||
|
isEmbedMode: boolean;
|
||||||
|
embedNamespace: string | null;
|
||||||
|
router: NextRouter;
|
||||||
|
}) {
|
||||||
|
const isBookingPage = (() => {
|
||||||
|
if (typeof props.isBookingPage === "function") {
|
||||||
|
return props.isBookingPage({ router: router });
|
||||||
|
}
|
||||||
|
return props.isBookingPage;
|
||||||
|
})();
|
||||||
|
|
||||||
|
const themeSupport = isBookingPage
|
||||||
|
? ThemeSupport.Booking
|
||||||
|
: // if isThemeSupported is explicitly false, we don't use theme there
|
||||||
|
props.isThemeSupported === false
|
||||||
|
? ThemeSupport.None
|
||||||
|
: ThemeSupport.App;
|
||||||
|
|
||||||
|
const isBookingPageThemSupportRequired = themeSupport === ThemeSupport.Booking;
|
||||||
|
const themeBasis = props.themeBasis;
|
||||||
|
|
||||||
|
if ((isBookingPageThemSupportRequired || isEmbedMode) && !themeBasis) {
|
||||||
|
console.warn(
|
||||||
|
"`themeBasis` is required for booking page theme support. Not providing it will cause theme flicker."
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const appearanceIdSuffix = themeBasis ? ":" + themeBasis : "";
|
||||||
|
const forcedTheme = themeSupport === ThemeSupport.None ? "light" : undefined;
|
||||||
|
let embedExplicitlySetThemeSuffix = "";
|
||||||
|
|
||||||
|
if (typeof window !== "undefined") {
|
||||||
|
const embedTheme = window.getEmbedTheme();
|
||||||
|
if (embedTheme) {
|
||||||
|
embedExplicitlySetThemeSuffix = ":" + embedTheme;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const storageKey = isEmbedMode
|
||||||
|
? // Same Namespace, Same Organizer but different themes would still work seamless and not cause theme flicker
|
||||||
|
// Even though it's recommended to use different namespaces when you want to theme differently on the same page but if the embeds are on different pages, the problem can still arise
|
||||||
|
`embed-theme-${embedNamespace}${appearanceIdSuffix}${embedExplicitlySetThemeSuffix}`
|
||||||
|
: themeSupport === ThemeSupport.App
|
||||||
|
? "app-theme"
|
||||||
|
: isBookingPageThemSupportRequired
|
||||||
|
? `booking-theme${appearanceIdSuffix}`
|
||||||
|
: undefined;
|
||||||
|
|
||||||
|
return {
|
||||||
|
storageKey,
|
||||||
|
forcedTheme,
|
||||||
|
themeSupport,
|
||||||
|
nonce: props.nonce,
|
||||||
|
enableColorScheme: false,
|
||||||
|
enableSystem: themeSupport !== ThemeSupport.None,
|
||||||
|
// next-themes doesn't listen to changes on storageKey. So we need to force a re-render when storageKey changes
|
||||||
|
// This is how login to dashboard soft navigation changes theme from light to dark
|
||||||
|
key: storageKey,
|
||||||
|
attribute: "class",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
function FeatureFlagsProvider({ children }: { children: React.ReactNode }) {
|
function FeatureFlagsProvider({ children }: { children: React.ReactNode }) {
|
||||||
const flags = useFlags();
|
const flags = useFlags();
|
||||||
return <FeatureProvider value={flags}>{children}</FeatureProvider>;
|
return <FeatureProvider value={flags}>{children}</FeatureProvider>;
|
||||||
@@ -157,6 +217,7 @@ const AppProviders = (props: AppPropsWithChildren) => {
|
|||||||
<TooltipProvider>
|
<TooltipProvider>
|
||||||
{/* color-scheme makes background:transparent not work which is required by embed. We need to ensure next-theme adds color-scheme to `body` instead of `html`(https://github.com/pacocoursey/next-themes/blob/main/src/index.tsx#L74). Once that's done we can enable color-scheme support */}
|
{/* color-scheme makes background:transparent not work which is required by embed. We need to ensure next-theme adds color-scheme to `body` instead of `html`(https://github.com/pacocoursey/next-themes/blob/main/src/index.tsx#L74). Once that's done we can enable color-scheme support */}
|
||||||
<CalcomThemeProvider
|
<CalcomThemeProvider
|
||||||
|
themeBasis={props.pageProps.themeBasis}
|
||||||
nonce={props.pageProps.nonce}
|
nonce={props.pageProps.nonce}
|
||||||
isThemeSupported={props.Component.isThemeSupported}
|
isThemeSupported={props.Component.isThemeSupported}
|
||||||
isBookingPage={props.Component.isBookingPage}>
|
isBookingPage={props.Component.isBookingPage}>
|
||||||
|
|||||||
@@ -111,12 +111,6 @@ const nextConfig = {
|
|||||||
transform: "lucide-react/dist/esm/icons/{{ kebabCase member }}",
|
transform: "lucide-react/dist/esm/icons/{{ kebabCase member }}",
|
||||||
preventFullImport: true,
|
preventFullImport: true,
|
||||||
},
|
},
|
||||||
"@heroicons/react/solid": {
|
|
||||||
transform: "@heroicons/react/solid/esm/{{ member }}",
|
|
||||||
},
|
|
||||||
"@heroicons/react/outline": {
|
|
||||||
transform: "@heroicons/react/outline/esm/{{ member }}",
|
|
||||||
},
|
|
||||||
"@calcom/features/insights/components": {
|
"@calcom/features/insights/components": {
|
||||||
transform: "@calcom/features/insights/components/{{member}}",
|
transform: "@calcom/features/insights/components/{{member}}",
|
||||||
skipDefaultConversion: true,
|
skipDefaultConversion: true,
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@calcom/web",
|
"name": "@calcom/web",
|
||||||
"version": "2.8.13",
|
"version": "2.9.2",
|
||||||
"private": true,
|
"private": true,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"analyze": "ANALYZE=true next build",
|
"analyze": "ANALYZE=true next build",
|
||||||
@@ -41,7 +41,6 @@
|
|||||||
"@daily-co/daily-js": "^0.37.0",
|
"@daily-co/daily-js": "^0.37.0",
|
||||||
"@formkit/auto-animate": "^1.0.0-beta.5",
|
"@formkit/auto-animate": "^1.0.0-beta.5",
|
||||||
"@glidejs/glide": "^3.5.2",
|
"@glidejs/glide": "^3.5.2",
|
||||||
"@heroicons/react": "^1.0.6",
|
|
||||||
"@hookform/error-message": "^2.0.0",
|
"@hookform/error-message": "^2.0.0",
|
||||||
"@hookform/resolvers": "^2.9.7",
|
"@hookform/resolvers": "^2.9.7",
|
||||||
"@next-auth/prisma-adapter": "^1.0.4",
|
"@next-auth/prisma-adapter": "^1.0.4",
|
||||||
@@ -115,12 +114,11 @@
|
|||||||
"react-select": "^5.7.0",
|
"react-select": "^5.7.0",
|
||||||
"react-timezone-select": "^1.4.0",
|
"react-timezone-select": "^1.4.0",
|
||||||
"react-use-intercom": "1.5.1",
|
"react-use-intercom": "1.5.1",
|
||||||
"remark": "^14.0.2",
|
"remove-markdown": "^0.5.0",
|
||||||
"rrule": "^2.7.1",
|
"rrule": "^2.7.1",
|
||||||
"sanitize-html": "^2.10.0",
|
"sanitize-html": "^2.10.0",
|
||||||
"schema-dts": "^1.1.0",
|
"schema-dts": "^1.1.0",
|
||||||
"short-uuid": "^4.2.0",
|
"short-uuid": "^4.2.0",
|
||||||
"strip-markdown": "^5.0.0",
|
|
||||||
"stripe": "^9.16.0",
|
"stripe": "^9.16.0",
|
||||||
"superjson": "1.9.1",
|
"superjson": "1.9.1",
|
||||||
"tailwindcss-radix": "^2.6.0",
|
"tailwindcss-radix": "^2.6.0",
|
||||||
@@ -153,6 +151,7 @@
|
|||||||
"@types/qrcode": "^1.4.3",
|
"@types/qrcode": "^1.4.3",
|
||||||
"@types/react": "18.0.26",
|
"@types/react": "18.0.26",
|
||||||
"@types/react-phone-number-input": "^3.0.14",
|
"@types/react-phone-number-input": "^3.0.14",
|
||||||
|
"@types/remove-markdown": "^0.3.1",
|
||||||
"@types/sanitize-html": "^2.9.0",
|
"@types/sanitize-html": "^2.9.0",
|
||||||
"@types/stripe": "^8.0.417",
|
"@types/stripe": "^8.0.417",
|
||||||
"@types/uuid": "8.3.1",
|
"@types/uuid": "8.3.1",
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
import { BadgeCheckIcon } from "@heroicons/react/solid";
|
|
||||||
import classNames from "classnames";
|
import classNames from "classnames";
|
||||||
import type { GetServerSidePropsContext } from "next";
|
import type { GetServerSidePropsContext } from "next";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
@@ -23,11 +22,12 @@ import defaultEvents, {
|
|||||||
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 { markdownToSafeHTML } from "@calcom/lib/markdownToSafeHTML";
|
import { markdownToSafeHTML } from "@calcom/lib/markdownToSafeHTML";
|
||||||
|
import { stripMarkdown } from "@calcom/lib/stripMarkdown";
|
||||||
import prisma from "@calcom/prisma";
|
import prisma from "@calcom/prisma";
|
||||||
import { baseEventTypeSelect } from "@calcom/prisma/selects";
|
import { baseEventTypeSelect } from "@calcom/prisma/selects";
|
||||||
import { EventTypeMetaDataSchema } from "@calcom/prisma/zod-utils";
|
import { EventTypeMetaDataSchema } from "@calcom/prisma/zod-utils";
|
||||||
import { Avatar, AvatarGroup, HeadSeo } from "@calcom/ui";
|
import { Avatar, AvatarGroup, HeadSeo } from "@calcom/ui";
|
||||||
import { ArrowRight } from "@calcom/ui/components/icon";
|
import { Verified, ArrowRight } from "@calcom/ui/components/icon";
|
||||||
|
|
||||||
import type { inferSSRProps } from "@lib/types/inferSSRProps";
|
import type { inferSSRProps } from "@lib/types/inferSSRProps";
|
||||||
import type { EmbedProps } from "@lib/withEmbedSsr";
|
import type { EmbedProps } from "@lib/withEmbedSsr";
|
||||||
@@ -37,7 +37,16 @@ import PageWrapper from "@components/PageWrapper";
|
|||||||
import { ssrInit } from "@server/lib/ssr";
|
import { ssrInit } from "@server/lib/ssr";
|
||||||
|
|
||||||
export default function User(props: inferSSRProps<typeof getServerSideProps> & EmbedProps) {
|
export default function User(props: inferSSRProps<typeof getServerSideProps> & EmbedProps) {
|
||||||
const { users, profile, eventTypes, isDynamicGroup, dynamicNames, dynamicUsernames, isSingleUser } = props;
|
const {
|
||||||
|
users,
|
||||||
|
profile,
|
||||||
|
eventTypes,
|
||||||
|
isDynamicGroup,
|
||||||
|
dynamicNames,
|
||||||
|
dynamicUsernames,
|
||||||
|
isSingleUser,
|
||||||
|
markdownStrippedBio,
|
||||||
|
} = props;
|
||||||
const [user] = users; //To be used when we only have a single user, not dynamic group
|
const [user] = users; //To be used when we only have a single user, not dynamic group
|
||||||
useTheme(user.theme);
|
useTheme(user.theme);
|
||||||
const { t } = useLocale();
|
const { t } = useLocale();
|
||||||
@@ -107,11 +116,9 @@ export default function User(props: inferSSRProps<typeof getServerSideProps> & E
|
|||||||
<>
|
<>
|
||||||
<HeadSeo
|
<HeadSeo
|
||||||
title={isDynamicGroup ? dynamicNames.join(", ") : nameOrUsername}
|
title={isDynamicGroup ? dynamicNames.join(", ") : nameOrUsername}
|
||||||
description={
|
description={isDynamicGroup ? `Book events with ${dynamicUsernames.join(", ")}` : markdownStrippedBio}
|
||||||
isDynamicGroup ? `Book events with ${dynamicUsernames.join(", ")}` : (user.bio as string) || ""
|
|
||||||
}
|
|
||||||
meeting={{
|
meeting={{
|
||||||
title: isDynamicGroup ? "" : `${user.bio}`,
|
title: isDynamicGroup ? "" : markdownStrippedBio,
|
||||||
profile: { name: `${profile.name}`, image: null },
|
profile: { name: `${profile.name}`, image: null },
|
||||||
users: isDynamicGroup
|
users: isDynamicGroup
|
||||||
? dynamicUsernames.map((username, index) => ({ username, name: dynamicNames[index] }))
|
? dynamicUsernames.map((username, index) => ({ username, name: dynamicNames[index] }))
|
||||||
@@ -131,12 +138,14 @@ export default function User(props: inferSSRProps<typeof getServerSideProps> & E
|
|||||||
<Avatar imageSrc={user.avatar} size="xl" alt={nameOrUsername} />
|
<Avatar imageSrc={user.avatar} size="xl" alt={nameOrUsername} />
|
||||||
<h1 className="font-cal text-emphasis mb-1 text-3xl">
|
<h1 className="font-cal text-emphasis mb-1 text-3xl">
|
||||||
{nameOrUsername}
|
{nameOrUsername}
|
||||||
{user.verified && <BadgeCheckIcon className=" mx-1 -mt-1 inline h-6 w-6 text-blue-500" />}
|
{user.verified && (
|
||||||
|
<Verified className=" mx-1 -mt-1 inline h-6 w-6 fill-blue-500 text-white dark:text-black" />
|
||||||
|
)}
|
||||||
</h1>
|
</h1>
|
||||||
{!isBioEmpty && (
|
{!isBioEmpty && (
|
||||||
<>
|
<>
|
||||||
<div
|
<div
|
||||||
className=" text-subtle text-sm [&_a]:text-blue-500 [&_a]:underline [&_a]:hover:text-blue-600"
|
className=" text-subtle break-words text-sm [&_a]:text-blue-500 [&_a]:underline [&_a]:hover:text-blue-600"
|
||||||
dangerouslySetInnerHTML={{ __html: props.safeBio }}
|
dangerouslySetInnerHTML={{ __html: props.safeBio }}
|
||||||
/>
|
/>
|
||||||
</>
|
</>
|
||||||
@@ -342,11 +351,14 @@ export const getServerSideProps = async (context: GetServerSidePropsContext) =>
|
|||||||
|
|
||||||
const safeBio = markdownToSafeHTML(user.bio) || "";
|
const safeBio = markdownToSafeHTML(user.bio) || "";
|
||||||
|
|
||||||
|
const markdownStrippedBio = stripMarkdown(user?.bio || "");
|
||||||
return {
|
return {
|
||||||
props: {
|
props: {
|
||||||
users,
|
users,
|
||||||
safeBio,
|
safeBio,
|
||||||
profile,
|
profile,
|
||||||
|
// Dynamic group has no theme preference right now. It uses system theme.
|
||||||
|
themeBasis: isDynamicGroup ? null : user.username,
|
||||||
user: {
|
user: {
|
||||||
emailMd5: crypto.createHash("md5").update(user.email).digest("hex"),
|
emailMd5: crypto.createHash("md5").update(user.email).digest("hex"),
|
||||||
},
|
},
|
||||||
@@ -361,6 +373,7 @@ export const getServerSideProps = async (context: GetServerSidePropsContext) =>
|
|||||||
dynamicNames,
|
dynamicNames,
|
||||||
dynamicUsernames,
|
dynamicUsernames,
|
||||||
isSingleUser,
|
isSingleUser,
|
||||||
|
markdownStrippedBio,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
@@ -177,6 +177,8 @@ async function getUserPageProps(context: GetStaticPropsContext) {
|
|||||||
slug: `${user.username}/${eventType.slug}`,
|
slug: `${user.username}/${eventType.slug}`,
|
||||||
image: `${WEBAPP_URL}/${user.username}/avatar.png`,
|
image: `${WEBAPP_URL}/${user.username}/avatar.png`,
|
||||||
},
|
},
|
||||||
|
// Dynamic group has no theme preference right now. It uses system theme.
|
||||||
|
themeBasis: user.username,
|
||||||
away: user?.away,
|
away: user?.away,
|
||||||
isDynamic: false,
|
isDynamic: false,
|
||||||
trpcState: ssg.dehydrate(),
|
trpcState: ssg.dehydrate(),
|
||||||
@@ -307,6 +309,8 @@ async function getDynamicGroupPageProps(context: GetStaticPropsContext) {
|
|||||||
props: {
|
props: {
|
||||||
eventType: eventTypeObject,
|
eventType: eventTypeObject,
|
||||||
profile,
|
profile,
|
||||||
|
// Dynamic group has no theme preference right now. It uses system theme.
|
||||||
|
themeBasis: null,
|
||||||
isDynamic: true,
|
isDynamic: true,
|
||||||
away: false,
|
away: false,
|
||||||
trpcState: ssg.dehydrate(),
|
trpcState: ssg.dehydrate(),
|
||||||
|
|||||||
@@ -293,6 +293,8 @@ export async function getServerSideProps(context: GetServerSidePropsContext) {
|
|||||||
props: {
|
props: {
|
||||||
away: user.away,
|
away: user.away,
|
||||||
profile,
|
profile,
|
||||||
|
// Dynamic group has no theme preference right now. It uses system theme.
|
||||||
|
themeBasis: isDynamicGroupBooking ? null : user.username,
|
||||||
eventType: eventTypeObject,
|
eventType: eventTypeObject,
|
||||||
booking,
|
booking,
|
||||||
currentSlotBooking: currentSlotBooking,
|
currentSlotBooking: currentSlotBooking,
|
||||||
|
|||||||
@@ -46,7 +46,8 @@ class MyDocument extends Document<Props> {
|
|||||||
<link rel="manifest" href="/site.webmanifest" />
|
<link rel="manifest" href="/site.webmanifest" />
|
||||||
<link rel="mask-icon" href="/safari-pinned-tab.svg" color="#000000" />
|
<link rel="mask-icon" href="/safari-pinned-tab.svg" color="#000000" />
|
||||||
<meta name="msapplication-TileColor" content="#ff0000" />
|
<meta name="msapplication-TileColor" content="#ff0000" />
|
||||||
<meta name="theme-color" content="var(--cal-bg)" />
|
<meta name="theme-color" media="(prefers-color-scheme: light)" content="#f9fafb" />
|
||||||
|
<meta name="theme-color" media="(prefers-color-scheme: dark)" content="#1C1C1C" />
|
||||||
</Head>
|
</Head>
|
||||||
|
|
||||||
<body
|
<body
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import type { WebhookTriggerEvents } from "@prisma/client";
|
||||||
import type { NextApiRequest, NextApiResponse } from "next";
|
import type { NextApiRequest, NextApiResponse } from "next";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
|
||||||
@@ -5,6 +6,8 @@ import { DailyLocationType } from "@calcom/app-store/locations";
|
|||||||
import { getDownloadLinkOfCalVideoByRecordingId } from "@calcom/core/videoClient";
|
import { getDownloadLinkOfCalVideoByRecordingId } from "@calcom/core/videoClient";
|
||||||
import { sendDailyVideoRecordingEmails } from "@calcom/emails";
|
import { sendDailyVideoRecordingEmails } from "@calcom/emails";
|
||||||
import { getServerSession } from "@calcom/features/auth/lib/getServerSession";
|
import { getServerSession } from "@calcom/features/auth/lib/getServerSession";
|
||||||
|
import getWebhooks from "@calcom/features/webhooks/lib/getWebhooks";
|
||||||
|
import sendPayload from "@calcom/features/webhooks/lib/sendPayload";
|
||||||
import { IS_SELF_HOSTED } from "@calcom/lib/constants";
|
import { IS_SELF_HOSTED } from "@calcom/lib/constants";
|
||||||
import { defaultHandler } from "@calcom/lib/server";
|
import { defaultHandler } from "@calcom/lib/server";
|
||||||
import { getTranslation } from "@calcom/lib/server/i18n";
|
import { getTranslation } from "@calcom/lib/server/i18n";
|
||||||
@@ -20,6 +23,38 @@ const downloadLinkSchema = z.object({
|
|||||||
download_link: z.string(),
|
download_link: z.string(),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const triggerWebhook = async ({
|
||||||
|
evt,
|
||||||
|
downloadLink,
|
||||||
|
booking,
|
||||||
|
}: {
|
||||||
|
evt: CalendarEvent;
|
||||||
|
downloadLink: string;
|
||||||
|
booking: {
|
||||||
|
userId: number | undefined;
|
||||||
|
eventTypeId: number | null;
|
||||||
|
};
|
||||||
|
}) => {
|
||||||
|
const eventTrigger: WebhookTriggerEvents = "RECORDING_READY";
|
||||||
|
// Send Webhook call if hooked to BOOKING.RECORDING_READY
|
||||||
|
const subscriberOptions = {
|
||||||
|
userId: booking.userId ?? 0,
|
||||||
|
eventTypeId: booking.eventTypeId ?? 0,
|
||||||
|
triggerEvent: eventTrigger,
|
||||||
|
};
|
||||||
|
const webhooks = await getWebhooks(subscriberOptions);
|
||||||
|
|
||||||
|
const promises = webhooks.map((webhook) =>
|
||||||
|
sendPayload(webhook.secret, eventTrigger, new Date().toISOString(), webhook, {
|
||||||
|
...evt,
|
||||||
|
downloadLink,
|
||||||
|
}).catch((e) => {
|
||||||
|
console.error(`Error executing webhook for event: ${eventTrigger}, URL: ${webhook.subscriberUrl}`, e);
|
||||||
|
})
|
||||||
|
);
|
||||||
|
await Promise.all(promises);
|
||||||
|
};
|
||||||
|
|
||||||
async function handler(req: NextApiRequest, res: NextApiResponse) {
|
async function handler(req: NextApiRequest, res: NextApiResponse) {
|
||||||
if (!process.env.SENDGRID_API_KEY || !process.env.SENDGRID_EMAIL) {
|
if (!process.env.SENDGRID_API_KEY || !process.env.SENDGRID_EMAIL) {
|
||||||
return res.status(405).json({ message: "No SendGrid API key or email" });
|
return res.status(405).json({ message: "No SendGrid API key or email" });
|
||||||
@@ -51,6 +86,7 @@ async function handler(req: NextApiRequest, res: NextApiResponse) {
|
|||||||
uid: true,
|
uid: true,
|
||||||
location: true,
|
location: true,
|
||||||
isRecorded: true,
|
isRecorded: true,
|
||||||
|
eventTypeId: true,
|
||||||
user: {
|
user: {
|
||||||
select: {
|
select: {
|
||||||
id: true,
|
id: true,
|
||||||
@@ -105,31 +141,36 @@ async function handler(req: NextApiRequest, res: NextApiResponse) {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const response = await getDownloadLinkOfCalVideoByRecordingId(recordingId);
|
||||||
|
const downloadLinkResponse = downloadLinkSchema.parse(response);
|
||||||
|
const downloadLink = downloadLinkResponse.download_link;
|
||||||
|
|
||||||
|
const evt: CalendarEvent = {
|
||||||
|
type: booking.title,
|
||||||
|
title: booking.title,
|
||||||
|
description: booking.description || undefined,
|
||||||
|
startTime: booking.startTime.toISOString(),
|
||||||
|
endTime: booking.endTime.toISOString(),
|
||||||
|
organizer: {
|
||||||
|
email: booking.user?.email || "Email-less",
|
||||||
|
name: booking.user?.name || "Nameless",
|
||||||
|
timeZone: booking.user?.timeZone || "Europe/London",
|
||||||
|
language: { translate: t, locale: booking?.user?.locale ?? "en" },
|
||||||
|
},
|
||||||
|
attendees: attendeesList,
|
||||||
|
uid: booking.uid,
|
||||||
|
};
|
||||||
|
|
||||||
|
await triggerWebhook({
|
||||||
|
evt,
|
||||||
|
downloadLink,
|
||||||
|
booking: { userId: booking?.user?.id, eventTypeId: booking.eventTypeId },
|
||||||
|
});
|
||||||
|
|
||||||
const isSendingEmailsAllowed = IS_SELF_HOSTED || session?.user?.belongsToActiveTeam;
|
const isSendingEmailsAllowed = IS_SELF_HOSTED || session?.user?.belongsToActiveTeam;
|
||||||
|
|
||||||
// send emails to all attendees only when user has team plan
|
// send emails to all attendees only when user has team plan
|
||||||
if (isSendingEmailsAllowed) {
|
if (isSendingEmailsAllowed) {
|
||||||
const response = await getDownloadLinkOfCalVideoByRecordingId(recordingId);
|
|
||||||
|
|
||||||
const downloadLinkResponse = downloadLinkSchema.parse(response);
|
|
||||||
const downloadLink = downloadLinkResponse.download_link;
|
|
||||||
|
|
||||||
const evt: CalendarEvent = {
|
|
||||||
type: booking.title,
|
|
||||||
title: booking.title,
|
|
||||||
description: booking.description || undefined,
|
|
||||||
startTime: booking.startTime.toISOString(),
|
|
||||||
endTime: booking.endTime.toISOString(),
|
|
||||||
organizer: {
|
|
||||||
email: booking.user?.email || "Email-less",
|
|
||||||
name: booking.user?.name || "Nameless",
|
|
||||||
timeZone: booking.user?.timeZone || "Europe/London",
|
|
||||||
language: { translate: t, locale: booking?.user?.locale ?? "en" },
|
|
||||||
},
|
|
||||||
attendees: attendeesList,
|
|
||||||
uid: booking.uid,
|
|
||||||
};
|
|
||||||
|
|
||||||
await sendDailyVideoRecordingEmails(evt, downloadLink);
|
await sendDailyVideoRecordingEmails(evt, downloadLink);
|
||||||
return res.status(200).json({ message: "Success" });
|
return res.status(200).json({ message: "Success" });
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,7 +4,6 @@ import type { SatoriOptions } from "satori";
|
|||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
|
||||||
import { Meeting, App, Generic } from "@calcom/lib/OgImages";
|
import { Meeting, App, Generic } from "@calcom/lib/OgImages";
|
||||||
import { md } from "@calcom/lib/markdownIt";
|
|
||||||
|
|
||||||
const calFont = fetch(new URL("../../../../public/fonts/cal.ttf", import.meta.url)).then((res) =>
|
const calFont = fetch(new URL("../../../../public/fonts/cal.ttf", import.meta.url)).then((res) =>
|
||||||
res.arrayBuffer()
|
res.arrayBuffer()
|
||||||
@@ -75,12 +74,10 @@ export default async function handler(req: NextApiRequest) {
|
|||||||
imageType,
|
imageType,
|
||||||
});
|
});
|
||||||
|
|
||||||
const title_ = md.render(title).replace(/(<([^>]+)>)/gi, "");
|
|
||||||
|
|
||||||
const img = new ImageResponse(
|
const img = new ImageResponse(
|
||||||
(
|
(
|
||||||
<Meeting
|
<Meeting
|
||||||
title={title_}
|
title={title}
|
||||||
profile={{ name: meetingProfileName, image: meetingImage }}
|
profile={{ name: meetingProfileName, image: meetingImage }}
|
||||||
users={names.map((name, index) => ({ name, username: usernames[index] }))}
|
users={names.map((name, index) => ({ name, username: usernames[index] }))}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import fs from "fs";
|
|||||||
import matter from "gray-matter";
|
import matter from "gray-matter";
|
||||||
import MarkdownIt from "markdown-it";
|
import MarkdownIt from "markdown-it";
|
||||||
import type { GetStaticPaths, GetStaticPropsContext } from "next";
|
import type { GetStaticPaths, GetStaticPropsContext } from "next";
|
||||||
|
import Link from "next/link";
|
||||||
import path from "path";
|
import path from "path";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
|
||||||
@@ -33,7 +34,26 @@ const sourceSchema = z.object({
|
|||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
|
|
||||||
function SingleAppPage({ data, source }: inferSSRProps<typeof getStaticProps>) {
|
function SingleAppPage(props: inferSSRProps<typeof getStaticProps>) {
|
||||||
|
// If it's not production environment, it would be a better idea to inform that the App is disabled.
|
||||||
|
if (props.isAppDisabled) {
|
||||||
|
if (process.env.NODE_ENV !== "production") {
|
||||||
|
// TODO: Improve disabled App UI. This is just a placeholder.
|
||||||
|
return (
|
||||||
|
<div className="p-2">
|
||||||
|
This App seems to be disabled. If you are an admin, you can enable this app from{" "}
|
||||||
|
<Link href="/settings/admin/apps" className="cursor-pointer text-blue-500 underline">
|
||||||
|
here
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Disabled App should give 404 any ways in production.
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { source, data } = props;
|
||||||
return (
|
return (
|
||||||
<App
|
<App
|
||||||
name={data.name}
|
name={data.name}
|
||||||
@@ -52,6 +72,7 @@ function SingleAppPage({ data, source }: inferSSRProps<typeof getStaticProps>) {
|
|||||||
website={data.url}
|
website={data.url}
|
||||||
email={data.email}
|
email={data.email}
|
||||||
licenseRequired={data.licenseRequired}
|
licenseRequired={data.licenseRequired}
|
||||||
|
teamsPlanRequired={data.teamsPlanRequired}
|
||||||
isProOnly={data.isProOnly}
|
isProOnly={data.isProOnly}
|
||||||
descriptionItems={source.data?.items as string[] | undefined}
|
descriptionItems={source.data?.items as string[] | undefined}
|
||||||
isTemplate={data.isTemplate}
|
isTemplate={data.isTemplate}
|
||||||
@@ -80,29 +101,43 @@ export const getStaticPaths: GetStaticPaths<{ slug: string }> = async () => {
|
|||||||
export const getStaticProps = async (ctx: GetStaticPropsContext) => {
|
export const getStaticProps = async (ctx: GetStaticPropsContext) => {
|
||||||
if (typeof ctx.params?.slug !== "string") return { notFound: true };
|
if (typeof ctx.params?.slug !== "string") return { notFound: true };
|
||||||
|
|
||||||
const app = await prisma.app.findUnique({
|
const appMeta = await getAppWithMetadata({
|
||||||
|
slug: ctx.params?.slug,
|
||||||
|
});
|
||||||
|
|
||||||
|
const appFromDb = await prisma.app.findUnique({
|
||||||
where: { slug: ctx.params.slug.toLowerCase() },
|
where: { slug: ctx.params.slug.toLowerCase() },
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!app) return { notFound: true };
|
const isAppAvailableInFileSystem = appMeta;
|
||||||
|
const isAppDisabled = isAppAvailableInFileSystem && (!appFromDb || !appFromDb.enabled);
|
||||||
|
|
||||||
const singleApp = await getAppWithMetadata(app);
|
if (process.env.NODE_ENV !== "production" && isAppDisabled) {
|
||||||
|
return {
|
||||||
|
props: {
|
||||||
|
isAppDisabled: true as const,
|
||||||
|
data: {
|
||||||
|
...appMeta,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
if (!singleApp) return { notFound: true };
|
if (!appFromDb || !appMeta || isAppDisabled) return { notFound: true };
|
||||||
|
|
||||||
const isTemplate = singleApp.isTemplate;
|
const isTemplate = appMeta.isTemplate;
|
||||||
const appDirname = path.join(isTemplate ? "templates" : "", app.dirName);
|
const appDirname = path.join(isTemplate ? "templates" : "", appFromDb.dirName);
|
||||||
const README_PATH = path.join(process.cwd(), "..", "..", `packages/app-store/${appDirname}/DESCRIPTION.md`);
|
const README_PATH = path.join(process.cwd(), "..", "..", `packages/app-store/${appDirname}/DESCRIPTION.md`);
|
||||||
const postFilePath = path.join(README_PATH);
|
const postFilePath = path.join(README_PATH);
|
||||||
let source = "";
|
let source = "";
|
||||||
|
|
||||||
try {
|
try {
|
||||||
source = fs.readFileSync(postFilePath).toString();
|
source = fs.readFileSync(postFilePath).toString();
|
||||||
source = source.replace(/{DESCRIPTION}/g, singleApp.description);
|
source = source.replace(/{DESCRIPTION}/g, appMeta.description);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
/* If the app doesn't have a README we fallback to the package description */
|
/* If the app doesn't have a README we fallback to the package description */
|
||||||
console.log(`No DESCRIPTION.md provided for: ${appDirname}`);
|
console.log(`No DESCRIPTION.md provided for: ${appDirname}`);
|
||||||
source = singleApp.description;
|
source = appMeta.description;
|
||||||
}
|
}
|
||||||
|
|
||||||
const result = matter(source);
|
const result = matter(source);
|
||||||
@@ -111,8 +146,8 @@ export const getStaticProps = async (ctx: GetStaticPropsContext) => {
|
|||||||
data.items = data.items.map((item) => {
|
data.items = data.items.map((item) => {
|
||||||
if (typeof item === "string") {
|
if (typeof item === "string") {
|
||||||
return getAppAssetFullPath(item, {
|
return getAppAssetFullPath(item, {
|
||||||
dirName: singleApp.dirName,
|
dirName: appMeta.dirName,
|
||||||
isTemplate: singleApp.isTemplate,
|
isTemplate: appMeta.isTemplate,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
return item;
|
return item;
|
||||||
@@ -120,8 +155,9 @@ export const getStaticProps = async (ctx: GetStaticPropsContext) => {
|
|||||||
}
|
}
|
||||||
return {
|
return {
|
||||||
props: {
|
props: {
|
||||||
|
isAppDisabled: false as const,
|
||||||
source: { content, data },
|
source: { content, data },
|
||||||
data: singleApp,
|
data: appMeta,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -172,7 +172,7 @@ export default function Login({
|
|||||||
className="mb-0"
|
className="mb-0"
|
||||||
{...register("password")}
|
{...register("password")}
|
||||||
/>
|
/>
|
||||||
<div className="absolute -top-1.5 ltr:right-0 rtl:left-0">
|
<div className="absolute -top-[2px] ltr:right-0 rtl:left-0">
|
||||||
<Link
|
<Link
|
||||||
href="/auth/forgot-password"
|
href="/auth/forgot-password"
|
||||||
tabIndex={-1}
|
tabIndex={-1}
|
||||||
|
|||||||
@@ -157,10 +157,13 @@ export const getServerSideProps = async (context: GetServerSidePropsContext) =>
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
let deploymentKey = await getDeploymentKey(prisma);
|
const deploymentKey = await prisma.deployment.findUnique({
|
||||||
|
where: { id: 1 },
|
||||||
|
select: { licenseKey: true },
|
||||||
|
});
|
||||||
|
|
||||||
// Check existant CALCOM_LICENSE_KEY env var and acccount for it
|
// Check existant CALCOM_LICENSE_KEY env var and acccount for it
|
||||||
if (!!process.env.CALCOM_LICENSE_KEY && !deploymentKey) {
|
if (!!process.env.CALCOM_LICENSE_KEY && !deploymentKey?.licenseKey) {
|
||||||
await prisma.deployment.upsert({
|
await prisma.deployment.upsert({
|
||||||
where: { id: 1 },
|
where: { id: 1 },
|
||||||
update: {
|
update: {
|
||||||
@@ -172,10 +175,9 @@ export const getServerSideProps = async (context: GetServerSidePropsContext) =>
|
|||||||
agreedLicenseAt: new Date(),
|
agreedLicenseAt: new Date(),
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
deploymentKey = await getDeploymentKey(prisma);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const isFreeLicense = deploymentKey === "";
|
const isFreeLicense = (await getDeploymentKey(prisma)) === "";
|
||||||
|
|
||||||
return {
|
return {
|
||||||
props: {
|
props: {
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
import { CheckIcon, MailOpenIcon, ExclamationIcon } from "@heroicons/react/outline";
|
|
||||||
import { signIn } from "next-auth/react";
|
import { signIn } from "next-auth/react";
|
||||||
import Head from "next/head";
|
import Head from "next/head";
|
||||||
import { useRouter } from "next/router";
|
import { useRouter } from "next/router";
|
||||||
@@ -9,6 +8,7 @@ import z from "zod";
|
|||||||
import { APP_NAME, WEBAPP_URL } from "@calcom/lib/constants";
|
import { APP_NAME, WEBAPP_URL } from "@calcom/lib/constants";
|
||||||
import { trpc } from "@calcom/trpc/react";
|
import { trpc } from "@calcom/trpc/react";
|
||||||
import { Button, showToast } from "@calcom/ui";
|
import { Button, showToast } from "@calcom/ui";
|
||||||
|
import { Check, MailOpen, AlertTriangle } from "@calcom/ui/components/icon";
|
||||||
|
|
||||||
import Loader from "@components/Loader";
|
import Loader from "@components/Loader";
|
||||||
import PageWrapper from "@components/PageWrapper";
|
import PageWrapper from "@components/PageWrapper";
|
||||||
@@ -118,11 +118,11 @@ export default function Verify() {
|
|||||||
<div className="m-10 flex max-w-2xl flex-col items-start border border-white p-12 text-left">
|
<div className="m-10 flex max-w-2xl flex-col items-start border border-white p-12 text-left">
|
||||||
<div className="rounded-full border border-white p-3">
|
<div className="rounded-full border border-white p-3">
|
||||||
{hasPaymentFailed ? (
|
{hasPaymentFailed ? (
|
||||||
<ExclamationIcon className="text-inverted h-12 w-12 flex-shrink-0 p-0.5 font-extralight" />
|
<AlertTriangle className="text-inverted h-12 w-12 flex-shrink-0 p-0.5 font-extralight" />
|
||||||
) : sessionId ? (
|
) : sessionId ? (
|
||||||
<CheckIcon className="text-inverted h-12 w-12 flex-shrink-0 p-0.5 font-extralight" />
|
<Check className="text-inverted h-12 w-12 flex-shrink-0 p-0.5 font-extralight" />
|
||||||
) : (
|
) : (
|
||||||
<MailOpenIcon className="text-inverted h-12 w-12 flex-shrink-0 p-0.5 font-extralight" />
|
<MailOpen className="text-inverted h-12 w-12 flex-shrink-0 p-0.5 font-extralight" />
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<h3 className="font-cal my-6 text-3xl font-normal">
|
<h3 className="font-cal my-6 text-3xl font-normal">
|
||||||
|
|||||||
@@ -62,7 +62,7 @@ const DateOverride = ({ workingHours }: { workingHours: WorkingHours[] }) => {
|
|||||||
<h3 className="text-emphasis font-medium leading-6">
|
<h3 className="text-emphasis font-medium leading-6">
|
||||||
{t("date_overrides")}{" "}
|
{t("date_overrides")}{" "}
|
||||||
<Tooltip content={t("date_overrides_info")}>
|
<Tooltip content={t("date_overrides_info")}>
|
||||||
<span className="inline-block">
|
<span className="inline-block align-middle">
|
||||||
<Info className="h-4 w-4" />
|
<Info className="h-4 w-4" />
|
||||||
</span>
|
</span>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
@@ -212,7 +212,7 @@ export default function Availability() {
|
|||||||
aria-label={t("delete")}
|
aria-label={t("delete")}
|
||||||
className="hidden sm:inline"
|
className="hidden sm:inline"
|
||||||
disabled={schedule?.isLastSchedule}
|
disabled={schedule?.isLastSchedule}
|
||||||
tooltip={t("requires_at_least_one_schedule")}
|
tooltip={schedule?.isLastSchedule ? t("requires_at_least_one_schedule") : t("delete")}
|
||||||
/>
|
/>
|
||||||
</DialogTrigger>
|
</DialogTrigger>
|
||||||
<ConfirmationDialogContent
|
<ConfirmationDialogContent
|
||||||
|
|||||||
@@ -53,7 +53,7 @@ const AvailabilityView = ({ user }: { user: User }) => {
|
|||||||
{t("overview_of_day")}{" "}
|
{t("overview_of_day")}{" "}
|
||||||
<input
|
<input
|
||||||
type="date"
|
type="date"
|
||||||
className="inline h-8 border-none p-0"
|
className="inline h-8 border-none bg-inherit p-0"
|
||||||
defaultValue={formattedSelectedDate}
|
defaultValue={formattedSelectedDate}
|
||||||
onChange={(e) => {
|
onChange={(e) => {
|
||||||
if (e.target.value) setSelectedDate(e.target.value);
|
if (e.target.value) setSelectedDate(e.target.value);
|
||||||
|
|||||||
@@ -443,8 +443,10 @@ export default function Success(props: SuccessProps) {
|
|||||||
)}
|
)}
|
||||||
{bookingInfo?.attendees.map((attendee) => (
|
{bookingInfo?.attendees.map((attendee) => (
|
||||||
<div key={attendee.name + attendee.email} className="mb-3 last:mb-0">
|
<div key={attendee.name + attendee.email} className="mb-3 last:mb-0">
|
||||||
{attendee.name && <p>{attendee.name}</p>}
|
{attendee.name && (
|
||||||
<p data-testid={`attendee-${attendee.email}`}>{attendee.email}</p>
|
<p data-testid={`attendee-name-${attendee.name}`}>{attendee.name}</p>
|
||||||
|
)}
|
||||||
|
<p data-testid={`attendee-email-${attendee.email}`}>{attendee.email}</p>
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
@@ -1092,6 +1094,7 @@ export async function getServerSideProps(context: GetServerSidePropsContext) {
|
|||||||
|
|
||||||
return {
|
return {
|
||||||
props: {
|
props: {
|
||||||
|
themeBasis: eventType.team ? eventType.team.slug : eventType.users[0]?.username,
|
||||||
hideBranding: eventType.team ? eventType.team.hideBranding : eventType.users[0].hideBranding,
|
hideBranding: eventType.team ? eventType.team.hideBranding : eventType.users[0].hideBranding,
|
||||||
profile,
|
profile,
|
||||||
eventType,
|
eventType,
|
||||||
|
|||||||
@@ -160,6 +160,7 @@ export const getServerSideProps = async (context: GetServerSidePropsContext) =>
|
|||||||
return {
|
return {
|
||||||
props: {
|
props: {
|
||||||
away: user.away,
|
away: user.away,
|
||||||
|
themeBasis: user.username,
|
||||||
isDynamicGroup: false,
|
isDynamicGroup: false,
|
||||||
profile,
|
profile,
|
||||||
date,
|
date,
|
||||||
|
|||||||
@@ -122,6 +122,7 @@ export async function getServerSideProps(context: GetServerSidePropsContext) {
|
|||||||
return {
|
return {
|
||||||
props: {
|
props: {
|
||||||
profile,
|
profile,
|
||||||
|
themeBasis: user.username,
|
||||||
eventType: eventTypeObject,
|
eventType: eventTypeObject,
|
||||||
booking: null,
|
booking: null,
|
||||||
currentSlotBooking: null,
|
currentSlotBooking: null,
|
||||||
|
|||||||
@@ -51,6 +51,7 @@ export type FormValues = {
|
|||||||
eventName: string;
|
eventName: string;
|
||||||
slug: string;
|
slug: string;
|
||||||
length: number;
|
length: number;
|
||||||
|
offsetStart: number;
|
||||||
description: string;
|
description: string;
|
||||||
disableGuests: boolean;
|
disableGuests: boolean;
|
||||||
requiresConfirmation: boolean;
|
requiresConfirmation: boolean;
|
||||||
@@ -211,6 +212,7 @@ const EventTypePage = (props: EventTypeSetupProps) => {
|
|||||||
bookingLimits: eventType.bookingLimits || undefined,
|
bookingLimits: eventType.bookingLimits || undefined,
|
||||||
durationLimits: eventType.durationLimits || undefined,
|
durationLimits: eventType.durationLimits || undefined,
|
||||||
length: eventType.length,
|
length: eventType.length,
|
||||||
|
offsetStart: eventType.offsetStart,
|
||||||
hidden: eventType.hidden,
|
hidden: eventType.hidden,
|
||||||
periodDates: {
|
periodDates: {
|
||||||
startDate: periodDates.startDate,
|
startDate: periodDates.startDate,
|
||||||
@@ -255,6 +257,7 @@ const EventTypePage = (props: EventTypeSetupProps) => {
|
|||||||
)
|
)
|
||||||
.optional(),
|
.optional(),
|
||||||
length: z.union([z.string().transform((val) => +val), z.number()]).optional(),
|
length: z.union([z.string().transform((val) => +val), z.number()]).optional(),
|
||||||
|
offsetStart: z.union([z.string().transform((val) => +val), z.number()]).optional(),
|
||||||
bookingFields: eventTypeBookingFields,
|
bookingFields: eventTypeBookingFields,
|
||||||
})
|
})
|
||||||
// TODO: Add schema for other fields later.
|
// TODO: Add schema for other fields later.
|
||||||
|
|||||||
@@ -490,18 +490,20 @@ export const EventTypeList = ({ group, groupIndex, readOnly, types }: EventTypeL
|
|||||||
{t("duplicate")}
|
{t("duplicate")}
|
||||||
</DropdownItem>
|
</DropdownItem>
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
<DropdownMenuItem className="outline-none">
|
|
||||||
<EmbedButton
|
|
||||||
as={DropdownItem}
|
|
||||||
type="button"
|
|
||||||
StartIcon={Code}
|
|
||||||
className="w-full rounded-none"
|
|
||||||
embedUrl={encodeURIComponent(embedLink)}>
|
|
||||||
{t("embed")}
|
|
||||||
</EmbedButton>
|
|
||||||
</DropdownMenuItem>
|
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
{!isManagedEventType && (
|
||||||
|
<DropdownMenuItem className="outline-none">
|
||||||
|
<EmbedButton
|
||||||
|
as={DropdownItem}
|
||||||
|
type="button"
|
||||||
|
StartIcon={Code}
|
||||||
|
className="w-full rounded-none"
|
||||||
|
embedUrl={encodeURIComponent(embedLink)}>
|
||||||
|
{t("embed")}
|
||||||
|
</EmbedButton>
|
||||||
|
</DropdownMenuItem>
|
||||||
|
)}
|
||||||
{/* readonly is only set when we are on a team - if we are on a user event type null will be the value. */}
|
{/* readonly is only set when we are on a team - if we are on a user event type null will be the value. */}
|
||||||
{(group.metadata?.readOnly === false || group.metadata.readOnly === null) &&
|
{(group.metadata?.readOnly === false || group.metadata.readOnly === null) &&
|
||||||
!isChildrenManagedEventType && (
|
!isChildrenManagedEventType && (
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ import type { inferSSRProps } from "@lib/types/inferSSRProps";
|
|||||||
|
|
||||||
import PageWrapper from "@components/PageWrapper";
|
import PageWrapper from "@components/PageWrapper";
|
||||||
import { ConnectedCalendars } from "@components/getting-started/steps-views/ConnectCalendars";
|
import { ConnectedCalendars } from "@components/getting-started/steps-views/ConnectCalendars";
|
||||||
|
import { ConnectedVideoStep } from "@components/getting-started/steps-views/ConnectedVideoStep";
|
||||||
import { SetupAvailability } from "@components/getting-started/steps-views/SetupAvailability";
|
import { SetupAvailability } from "@components/getting-started/steps-views/SetupAvailability";
|
||||||
import UserProfile from "@components/getting-started/steps-views/UserProfile";
|
import UserProfile from "@components/getting-started/steps-views/UserProfile";
|
||||||
import { UserSettings } from "@components/getting-started/steps-views/UserSettings";
|
import { UserSettings } from "@components/getting-started/steps-views/UserSettings";
|
||||||
@@ -22,7 +23,13 @@ import { UserSettings } from "@components/getting-started/steps-views/UserSettin
|
|||||||
export type IOnboardingPageProps = inferSSRProps<typeof getServerSideProps>;
|
export type IOnboardingPageProps = inferSSRProps<typeof getServerSideProps>;
|
||||||
|
|
||||||
const INITIAL_STEP = "user-settings";
|
const INITIAL_STEP = "user-settings";
|
||||||
const steps = ["user-settings", "connected-calendar", "setup-availability", "user-profile"] as const;
|
const steps = [
|
||||||
|
"user-settings",
|
||||||
|
"connected-calendar",
|
||||||
|
"connected-video",
|
||||||
|
"setup-availability",
|
||||||
|
"user-profile",
|
||||||
|
] as const;
|
||||||
|
|
||||||
const stepTransform = (step: (typeof steps)[number]) => {
|
const stepTransform = (step: (typeof steps)[number]) => {
|
||||||
const stepIndex = steps.indexOf(step);
|
const stepIndex = steps.indexOf(step);
|
||||||
@@ -36,9 +43,9 @@ const stepRouteSchema = z.object({
|
|||||||
step: z.array(z.enum(steps)).default([INITIAL_STEP]),
|
step: z.array(z.enum(steps)).default([INITIAL_STEP]),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// TODO: Refactor how steps work to be contained in one array/object. Currently we have steps,initalsteps,headers etc. These can all be in one place
|
||||||
const OnboardingPage = (props: IOnboardingPageProps) => {
|
const OnboardingPage = (props: IOnboardingPageProps) => {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
|
||||||
const { user } = props;
|
const { user } = props;
|
||||||
const { t } = useLocale();
|
const { t } = useLocale();
|
||||||
|
|
||||||
@@ -55,6 +62,11 @@ const OnboardingPage = (props: IOnboardingPageProps) => {
|
|||||||
subtitle: [`${t("connect_your_calendar_instructions")}`],
|
subtitle: [`${t("connect_your_calendar_instructions")}`],
|
||||||
skipText: `${t("connect_calendar_later")}`,
|
skipText: `${t("connect_calendar_later")}`,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
title: `${t("connect_your_video_app")}`,
|
||||||
|
subtitle: [`${t("connect_your_video_app_instructions")}`],
|
||||||
|
skipText: `${t("set_up_later")}`,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
title: `${t("set_availability")}`,
|
title: `${t("set_availability")}`,
|
||||||
subtitle: [
|
subtitle: [
|
||||||
@@ -68,6 +80,17 @@ const OnboardingPage = (props: IOnboardingPageProps) => {
|
|||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
|
// TODO: Add this in when we have solved the ability to move to tokens accept invite and note invitedto
|
||||||
|
// Ability to accept other pending invites if any (low priority)
|
||||||
|
// if (props.hasPendingInvites) {
|
||||||
|
// headers.unshift(
|
||||||
|
// props.hasPendingInvites && {
|
||||||
|
// title: `${t("email_no_user_invite_heading", { appName: APP_NAME })}`,
|
||||||
|
// subtitle: [], // TODO: come up with some subtitle text here
|
||||||
|
// }
|
||||||
|
// );
|
||||||
|
// }
|
||||||
|
|
||||||
const goToIndex = (index: number) => {
|
const goToIndex = (index: number) => {
|
||||||
const newStep = steps[index];
|
const newStep = steps[index];
|
||||||
router.push(
|
router.push(
|
||||||
@@ -122,12 +145,15 @@ const OnboardingPage = (props: IOnboardingPageProps) => {
|
|||||||
|
|
||||||
{currentStep === "connected-calendar" && <ConnectedCalendars nextStep={() => goToIndex(2)} />}
|
{currentStep === "connected-calendar" && <ConnectedCalendars nextStep={() => goToIndex(2)} />}
|
||||||
|
|
||||||
|
{currentStep === "connected-video" && <ConnectedVideoStep nextStep={() => goToIndex(3)} />}
|
||||||
|
|
||||||
{currentStep === "setup-availability" && (
|
{currentStep === "setup-availability" && (
|
||||||
<SetupAvailability nextStep={() => goToIndex(3)} defaultScheduleId={user.defaultScheduleId} />
|
<SetupAvailability nextStep={() => goToIndex(4)} defaultScheduleId={user.defaultScheduleId} />
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{currentStep === "user-profile" && <UserProfile user={user} />}
|
{currentStep === "user-profile" && <UserProfile user={user} />}
|
||||||
</StepCard>
|
</StepCard>
|
||||||
|
|
||||||
{headers[currentStepIndex]?.skipText && (
|
{headers[currentStepIndex]?.skipText && (
|
||||||
<div className="flex w-full flex-row justify-center">
|
<div className="flex w-full flex-row justify-center">
|
||||||
<Button
|
<Button
|
||||||
@@ -181,6 +207,18 @@ export const getServerSideProps = async (context: GetServerSidePropsContext) =>
|
|||||||
allowDynamicBooking: true,
|
allowDynamicBooking: true,
|
||||||
defaultScheduleId: true,
|
defaultScheduleId: true,
|
||||||
completedOnboarding: true,
|
completedOnboarding: true,
|
||||||
|
teams: {
|
||||||
|
select: {
|
||||||
|
accepted: true,
|
||||||
|
team: {
|
||||||
|
select: {
|
||||||
|
id: true,
|
||||||
|
name: true,
|
||||||
|
logo: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -199,6 +237,7 @@ export const getServerSideProps = async (context: GetServerSidePropsContext) =>
|
|||||||
...user,
|
...user,
|
||||||
emailMd5: crypto.createHash("md5").update(user.email).digest("hex"),
|
emailMd5: crypto.createHash("md5").update(user.email).digest("hex"),
|
||||||
},
|
},
|
||||||
|
hasPendingInvites: user.teams.find((team) => team.accepted === false) ?? false,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import type { GetServerSidePropsContext } from "next";
|
|||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
|
||||||
import { Booker } from "@calcom/atoms";
|
import { Booker } from "@calcom/atoms";
|
||||||
|
import { BookerSeo } from "@calcom/features/bookings/components/BookerSeo";
|
||||||
import { getBookingByUidOrRescheduleUid } from "@calcom/features/bookings/lib/get-booking";
|
import { getBookingByUidOrRescheduleUid } from "@calcom/features/bookings/lib/get-booking";
|
||||||
import type { GetBookingType } from "@calcom/features/bookings/lib/get-booking";
|
import type { GetBookingType } from "@calcom/features/bookings/lib/get-booking";
|
||||||
import { getUsernameList } from "@calcom/lib/defaultEvents";
|
import { getUsernameList } from "@calcom/lib/defaultEvents";
|
||||||
@@ -13,10 +14,22 @@ import PageWrapper from "@components/PageWrapper";
|
|||||||
|
|
||||||
type PageProps = inferSSRProps<typeof getServerSideProps>;
|
type PageProps = inferSSRProps<typeof getServerSideProps>;
|
||||||
|
|
||||||
export default function Type({ slug, user, booking, away }: PageProps) {
|
export default function Type({ slug, user, booking, away, isBrandingHidden }: PageProps) {
|
||||||
return (
|
return (
|
||||||
<main className="flex h-full min-h-[100dvh] items-center justify-center">
|
<main className="flex h-full min-h-[100dvh] items-center justify-center">
|
||||||
<Booker username={user} eventSlug={slug} rescheduleBooking={booking} isAway={away} />
|
<BookerSeo
|
||||||
|
username={user}
|
||||||
|
eventSlug={slug}
|
||||||
|
rescheduleUid={booking?.uid}
|
||||||
|
hideBranding={isBrandingHidden}
|
||||||
|
/>
|
||||||
|
<Booker
|
||||||
|
username={user}
|
||||||
|
eventSlug={slug}
|
||||||
|
rescheduleBooking={booking}
|
||||||
|
isAway={away}
|
||||||
|
hideBranding={isBrandingHidden}
|
||||||
|
/>
|
||||||
</main>
|
</main>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -27,8 +40,8 @@ async function getDynamicGroupPageProps(context: GetServerSidePropsContext) {
|
|||||||
const { user, type: slug } = paramsSchema.parse(context.params);
|
const { user, type: slug } = paramsSchema.parse(context.params);
|
||||||
const { rescheduleUid } = context.query;
|
const { rescheduleUid } = context.query;
|
||||||
|
|
||||||
const { ssgInit } = await import("@server/lib/ssg");
|
const { ssrInit } = await import("@server/lib/ssr");
|
||||||
const ssg = await ssgInit(context);
|
const ssr = await ssrInit(context);
|
||||||
const usernameList = getUsernameList(user);
|
const usernameList = getUsernameList(user);
|
||||||
|
|
||||||
const users = await prisma.user.findMany({
|
const users = await prisma.user.findMany({
|
||||||
@@ -53,13 +66,16 @@ async function getDynamicGroupPageProps(context: GetServerSidePropsContext) {
|
|||||||
booking = await getBookingByUidOrRescheduleUid(`${rescheduleUid}`);
|
booking = await getBookingByUidOrRescheduleUid(`${rescheduleUid}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
await ssr.viewer.public.event.prefetch({ username: user, eventSlug: slug });
|
||||||
|
|
||||||
return {
|
return {
|
||||||
props: {
|
props: {
|
||||||
booking,
|
booking,
|
||||||
user,
|
user,
|
||||||
slug,
|
slug,
|
||||||
away: false,
|
away: false,
|
||||||
trpcState: ssg.dehydrate(),
|
trpcState: ssr.dehydrate(),
|
||||||
|
isBrandingHidden: false,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -67,14 +83,16 @@ async function getDynamicGroupPageProps(context: GetServerSidePropsContext) {
|
|||||||
async function getUserPageProps(context: GetServerSidePropsContext) {
|
async function getUserPageProps(context: GetServerSidePropsContext) {
|
||||||
const { user: username, type: slug } = paramsSchema.parse(context.params);
|
const { user: username, type: slug } = paramsSchema.parse(context.params);
|
||||||
const { rescheduleUid } = context.query;
|
const { rescheduleUid } = context.query;
|
||||||
const { ssgInit } = await import("@server/lib/ssg");
|
|
||||||
const ssg = await ssgInit(context);
|
const { ssrInit } = await import("@server/lib/ssr");
|
||||||
|
const ssr = await ssrInit(context);
|
||||||
const user = await prisma.user.findUnique({
|
const user = await prisma.user.findUnique({
|
||||||
where: {
|
where: {
|
||||||
username,
|
username,
|
||||||
},
|
},
|
||||||
select: {
|
select: {
|
||||||
away: true,
|
away: true,
|
||||||
|
hideBranding: true,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -89,13 +107,16 @@ async function getUserPageProps(context: GetServerSidePropsContext) {
|
|||||||
booking = await getBookingByUidOrRescheduleUid(`${rescheduleUid}`);
|
booking = await getBookingByUidOrRescheduleUid(`${rescheduleUid}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
await ssr.viewer.public.event.prefetch({ username, eventSlug: slug });
|
||||||
|
|
||||||
return {
|
return {
|
||||||
props: {
|
props: {
|
||||||
booking,
|
booking,
|
||||||
away: user?.away,
|
away: user?.away,
|
||||||
user: username,
|
user: username,
|
||||||
slug,
|
slug,
|
||||||
trpcState: ssg.dehydrate(),
|
trpcState: ssr.dehydrate(),
|
||||||
|
isBrandingHidden: user?.hideBranding,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import type { GetServerSidePropsContext } from "next";
|
|||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
|
||||||
import { Booker } from "@calcom/atoms";
|
import { Booker } from "@calcom/atoms";
|
||||||
|
import { BookerSeo } from "@calcom/features/bookings/components/BookerSeo";
|
||||||
import { getBookingByUidOrRescheduleUid } from "@calcom/features/bookings/lib/get-booking";
|
import { getBookingByUidOrRescheduleUid } from "@calcom/features/bookings/lib/get-booking";
|
||||||
import type { GetBookingType } from "@calcom/features/bookings/lib/get-booking";
|
import type { GetBookingType } from "@calcom/features/bookings/lib/get-booking";
|
||||||
import prisma from "@calcom/prisma";
|
import prisma from "@calcom/prisma";
|
||||||
@@ -12,10 +13,22 @@ import PageWrapper from "@components/PageWrapper";
|
|||||||
|
|
||||||
type PageProps = inferSSRProps<typeof getServerSideProps>;
|
type PageProps = inferSSRProps<typeof getServerSideProps>;
|
||||||
|
|
||||||
export default function Type({ slug, user, booking, away }: PageProps) {
|
export default function Type({ slug, user, booking, away, isBrandingHidden }: PageProps) {
|
||||||
return (
|
return (
|
||||||
<main className="flex h-full min-h-[100dvh] items-center justify-center">
|
<main className="flex h-full min-h-[100dvh] items-center justify-center">
|
||||||
<Booker username={user} eventSlug={slug} rescheduleBooking={booking} isAway={away} />
|
<BookerSeo
|
||||||
|
username={user}
|
||||||
|
eventSlug={slug}
|
||||||
|
rescheduleUid={booking?.uid}
|
||||||
|
hideBranding={isBrandingHidden}
|
||||||
|
/>
|
||||||
|
<Booker
|
||||||
|
username={user}
|
||||||
|
eventSlug={slug}
|
||||||
|
rescheduleBooking={booking}
|
||||||
|
isAway={away}
|
||||||
|
hideBranding={isBrandingHidden}
|
||||||
|
/>
|
||||||
</main>
|
</main>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -30,8 +43,8 @@ const paramsSchema = z.object({ type: z.string(), slug: z.string() });
|
|||||||
export const getServerSideProps = async (context: GetServerSidePropsContext) => {
|
export const getServerSideProps = async (context: GetServerSidePropsContext) => {
|
||||||
const { slug: teamSlug, type: meetingSlug } = paramsSchema.parse(context.params);
|
const { slug: teamSlug, type: meetingSlug } = paramsSchema.parse(context.params);
|
||||||
const { rescheduleUid } = context.query;
|
const { rescheduleUid } = context.query;
|
||||||
const { ssgInit } = await import("@server/lib/ssg");
|
const { ssrInit } = await import("@server/lib/ssr");
|
||||||
const ssg = await ssgInit(context);
|
const ssr = await ssrInit(context);
|
||||||
|
|
||||||
const team = await prisma.team.findFirst({
|
const team = await prisma.team.findFirst({
|
||||||
where: {
|
where: {
|
||||||
@@ -39,6 +52,7 @@ export const getServerSideProps = async (context: GetServerSidePropsContext) =>
|
|||||||
},
|
},
|
||||||
select: {
|
select: {
|
||||||
id: true,
|
id: true,
|
||||||
|
hideBranding: true,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -52,14 +66,15 @@ export const getServerSideProps = async (context: GetServerSidePropsContext) =>
|
|||||||
if (rescheduleUid) {
|
if (rescheduleUid) {
|
||||||
booking = await getBookingByUidOrRescheduleUid(`${rescheduleUid}`);
|
booking = await getBookingByUidOrRescheduleUid(`${rescheduleUid}`);
|
||||||
}
|
}
|
||||||
|
await ssr.viewer.public.event.prefetch({ username: teamSlug, eventSlug: meetingSlug });
|
||||||
return {
|
return {
|
||||||
props: {
|
props: {
|
||||||
booking,
|
booking,
|
||||||
away: false,
|
away: false,
|
||||||
user: teamSlug,
|
user: teamSlug,
|
||||||
slug: meetingSlug,
|
slug: meetingSlug,
|
||||||
trpcState: ssg.dehydrate(),
|
trpcState: ssr.dehydrate(),
|
||||||
|
isBrandingHidden: team?.hideBranding,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
@@ -50,6 +50,7 @@ const ProfileImpersonationView = () => {
|
|||||||
setValue,
|
setValue,
|
||||||
reset,
|
reset,
|
||||||
getValues,
|
getValues,
|
||||||
|
watch,
|
||||||
} = formMethods;
|
} = formMethods;
|
||||||
|
|
||||||
const isDisabled = isSubmitting || !isDirty;
|
const isDisabled = isSubmitting || !isDirty;
|
||||||
@@ -63,11 +64,11 @@ const ProfileImpersonationView = () => {
|
|||||||
}}>
|
}}>
|
||||||
<div className="flex space-x-3">
|
<div className="flex space-x-3">
|
||||||
<Switch
|
<Switch
|
||||||
defaultChecked={!user?.disableImpersonation}
|
|
||||||
onCheckedChange={(e) => {
|
onCheckedChange={(e) => {
|
||||||
setValue("disableImpersonation", !e, { shouldDirty: true });
|
setValue("disableImpersonation", !e, { shouldDirty: true });
|
||||||
}}
|
}}
|
||||||
fitToHeight={true}
|
fitToHeight={true}
|
||||||
|
checked={!watch("disableImpersonation")}
|
||||||
/>
|
/>
|
||||||
<div className="flex flex-col">
|
<div className="flex flex-col">
|
||||||
<Skeleton as={Label} className="text-emphasis text-sm font-semibold leading-none">
|
<Skeleton as={Label} className="text-emphasis text-sm font-semibold leading-none">
|
||||||
|
|||||||
+40
-28
@@ -4,10 +4,12 @@ import { useRouter } from "next/router";
|
|||||||
import type { CSSProperties } from "react";
|
import type { CSSProperties } from "react";
|
||||||
import type { SubmitHandler } from "react-hook-form";
|
import type { SubmitHandler } from "react-hook-form";
|
||||||
import { FormProvider, useForm } from "react-hook-form";
|
import { FormProvider, useForm } from "react-hook-form";
|
||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
import LicenseRequired from "@calcom/features/ee/common/components/LicenseRequired";
|
import LicenseRequired from "@calcom/features/ee/common/components/LicenseRequired";
|
||||||
|
import { checkPremiumUsername } from "@calcom/features/ee/common/lib/checkPremiumUsername";
|
||||||
import { isSAMLLoginEnabled } from "@calcom/features/ee/sso/lib/saml";
|
import { isSAMLLoginEnabled } from "@calcom/features/ee/sso/lib/saml";
|
||||||
import { WEBAPP_URL } from "@calcom/lib/constants";
|
import { IS_SELF_HOSTED, WEBAPP_URL } from "@calcom/lib/constants";
|
||||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||||
import { collectPageParameters, telemetryEventTypes, useTelemetry } from "@calcom/lib/telemetry";
|
import { collectPageParameters, telemetryEventTypes, useTelemetry } from "@calcom/lib/telemetry";
|
||||||
import prisma from "@calcom/prisma";
|
import prisma from "@calcom/prisma";
|
||||||
@@ -16,7 +18,6 @@ import { Alert, Button, EmailField, HeadSeo, PasswordField, TextField } from "@c
|
|||||||
|
|
||||||
import PageWrapper from "@components/PageWrapper";
|
import PageWrapper from "@components/PageWrapper";
|
||||||
|
|
||||||
import { asStringOrNull } from "../lib/asStringOrNull";
|
|
||||||
import { IS_GOOGLE_LOGIN_ENABLED } from "../server/lib/constants";
|
import { IS_GOOGLE_LOGIN_ENABLED } from "../server/lib/constants";
|
||||||
import { ssrInit } from "../server/lib/ssr";
|
import { ssrInit } from "../server/lib/ssr";
|
||||||
|
|
||||||
@@ -24,11 +25,10 @@ type FormValues = {
|
|||||||
username: string;
|
username: string;
|
||||||
email: string;
|
email: string;
|
||||||
password: string;
|
password: string;
|
||||||
passwordcheck: string;
|
|
||||||
apiError: string;
|
apiError: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export default function Signup({ prepopulateFormValues }: inferSSRProps<typeof getServerSideProps>) {
|
export default function Signup({ prepopulateFormValues, token }: inferSSRProps<typeof getServerSideProps>) {
|
||||||
const { t } = useLocale();
|
const { t } = useLocale();
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const telemetry = useTelemetry();
|
const telemetry = useTelemetry();
|
||||||
@@ -76,7 +76,7 @@ export default function Signup({ prepopulateFormValues }: inferSSRProps<typeof g
|
|||||||
return (
|
return (
|
||||||
<LicenseRequired>
|
<LicenseRequired>
|
||||||
<div
|
<div
|
||||||
className="bg-muted flex min-h-screen flex-col justify-center py-12 sm:px-6 lg:px-8"
|
className="bg-muted flex min-h-screen flex-col justify-center "
|
||||||
style={
|
style={
|
||||||
{
|
{
|
||||||
"--cal-brand": "#111827",
|
"--cal-brand": "#111827",
|
||||||
@@ -95,7 +95,7 @@ export default function Signup({ prepopulateFormValues }: inferSSRProps<typeof g
|
|||||||
</h2>
|
</h2>
|
||||||
</div>
|
</div>
|
||||||
<div className="mt-8 sm:mx-auto sm:w-full sm:max-w-md">
|
<div className="mt-8 sm:mx-auto sm:w-full sm:max-w-md">
|
||||||
<div className="bg-default mx-2 px-4 py-8 shadow sm:rounded-lg sm:px-10">
|
<div className="bg-default mx-2 p-6 shadow sm:rounded-lg lg:p-8">
|
||||||
<FormProvider {...methods}>
|
<FormProvider {...methods}>
|
||||||
<form
|
<form
|
||||||
onSubmit={(event) => {
|
onSubmit={(event) => {
|
||||||
@@ -109,7 +109,7 @@ export default function Signup({ prepopulateFormValues }: inferSSRProps<typeof g
|
|||||||
}}
|
}}
|
||||||
className="bg-default space-y-6">
|
className="bg-default space-y-6">
|
||||||
{errors.apiError && <Alert severity="error" message={errors.apiError?.message} />}
|
{errors.apiError && <Alert severity="error" message={errors.apiError?.message} />}
|
||||||
<div className="space-y-2">
|
<div className="space-y-4">
|
||||||
<TextField
|
<TextField
|
||||||
addOnLeading={`${process.env.NEXT_PUBLIC_WEBSITE_URL}/`}
|
addOnLeading={`${process.env.NEXT_PUBLIC_WEBSITE_URL}/`}
|
||||||
{...register("username")}
|
{...register("username")}
|
||||||
@@ -125,32 +125,28 @@ export default function Signup({ prepopulateFormValues }: inferSSRProps<typeof g
|
|||||||
className: "block text-sm font-medium text-default",
|
className: "block text-sm font-medium text-default",
|
||||||
}}
|
}}
|
||||||
{...register("password")}
|
{...register("password")}
|
||||||
|
hintErrors={["caplow", "min", "num"]}
|
||||||
className="border-default mt-1 block w-full rounded-md border px-3 py-2 shadow-sm focus:border-black focus:outline-none focus:ring-black sm:text-sm"
|
className="border-default mt-1 block w-full rounded-md border px-3 py-2 shadow-sm focus:border-black focus:outline-none focus:ring-black sm:text-sm"
|
||||||
/>
|
/>
|
||||||
<PasswordField
|
|
||||||
label={t("confirm_password")}
|
|
||||||
{...register("passwordcheck", {
|
|
||||||
validate: (value) =>
|
|
||||||
value === methods.watch("password") || (t("error_password_mismatch") as string),
|
|
||||||
})}
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
<div className="flex space-x-2 rtl:space-x-reverse">
|
<div className="flex space-x-2 rtl:space-x-reverse">
|
||||||
<Button type="submit" loading={isSubmitting} className="w-7/12 justify-center">
|
<Button type="submit" loading={isSubmitting} className="w-full justify-center">
|
||||||
{t("create_account")}
|
{t("create_account")}
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
{!token && (
|
||||||
color="secondary"
|
<Button
|
||||||
className="w-5/12 justify-center"
|
color="secondary"
|
||||||
onClick={() =>
|
className="w-full justify-center"
|
||||||
signIn("Cal.com", {
|
onClick={() =>
|
||||||
callbackUrl: router.query.callbackUrl
|
signIn("Cal.com", {
|
||||||
? `${WEBAPP_URL}/${router.query.callbackUrl}`
|
callbackUrl: router.query.callbackUrl
|
||||||
: `${WEBAPP_URL}/getting-started`,
|
? `${WEBAPP_URL}/${router.query.callbackUrl}`
|
||||||
})
|
: `${WEBAPP_URL}/getting-started`,
|
||||||
}>
|
})
|
||||||
{t("login_instead")}
|
}>
|
||||||
</Button>
|
{t("login_instead")}
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</FormProvider>
|
</FormProvider>
|
||||||
@@ -163,7 +159,7 @@ export default function Signup({ prepopulateFormValues }: inferSSRProps<typeof g
|
|||||||
|
|
||||||
export const getServerSideProps = async (ctx: GetServerSidePropsContext) => {
|
export const getServerSideProps = async (ctx: GetServerSidePropsContext) => {
|
||||||
const ssr = await ssrInit(ctx);
|
const ssr = await ssrInit(ctx);
|
||||||
const token = asStringOrNull(ctx.query.token);
|
const token = z.string().optional().parse(ctx.query.token);
|
||||||
|
|
||||||
const props = {
|
const props = {
|
||||||
isGoogleLoginEnabled: IS_GOOGLE_LOGIN_ENABLED,
|
isGoogleLoginEnabled: IS_GOOGLE_LOGIN_ENABLED,
|
||||||
@@ -221,11 +217,27 @@ export const getServerSideProps = async (ctx: GetServerSidePropsContext) => {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const guessUsernameFromEmail = (email: string) => {
|
||||||
|
const [username] = email.split("@");
|
||||||
|
return username;
|
||||||
|
};
|
||||||
|
|
||||||
|
let username = guessUsernameFromEmail(verificationToken.identifier);
|
||||||
|
|
||||||
|
if (!IS_SELF_HOSTED) {
|
||||||
|
// Im not sure we actually hit this because of next redirects signup to website repo - but just in case this is pretty cool :)
|
||||||
|
const { available, suggestion } = await checkPremiumUsername(username);
|
||||||
|
|
||||||
|
username = available ? username : suggestion || username;
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
props: {
|
props: {
|
||||||
...props,
|
...props,
|
||||||
|
token,
|
||||||
prepopulateFormValues: {
|
prepopulateFormValues: {
|
||||||
email: verificationToken.identifier,
|
email: verificationToken.identifier,
|
||||||
|
username,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import Link from "next/link";
|
|||||||
import { useRouter } from "next/router";
|
import { useRouter } from "next/router";
|
||||||
import { useEffect } from "react";
|
import { useEffect } from "react";
|
||||||
|
|
||||||
import { useIsEmbed } from "@calcom/embed-core/embed-iframe";
|
import { sdkActionManager, useIsEmbed } from "@calcom/embed-core/embed-iframe";
|
||||||
import EventTypeDescription from "@calcom/features/eventtypes/components/EventTypeDescription";
|
import EventTypeDescription from "@calcom/features/eventtypes/components/EventTypeDescription";
|
||||||
import { CAL_URL } from "@calcom/lib/constants";
|
import { CAL_URL } from "@calcom/lib/constants";
|
||||||
import { getPlaceholderAvatar } from "@calcom/lib/defaultAvatarImage";
|
import { getPlaceholderAvatar } from "@calcom/lib/defaultAvatarImage";
|
||||||
@@ -12,6 +12,7 @@ import { useLocale } from "@calcom/lib/hooks/useLocale";
|
|||||||
import useTheme from "@calcom/lib/hooks/useTheme";
|
import useTheme from "@calcom/lib/hooks/useTheme";
|
||||||
import { markdownToSafeHTML } from "@calcom/lib/markdownToSafeHTML";
|
import { markdownToSafeHTML } from "@calcom/lib/markdownToSafeHTML";
|
||||||
import { getTeamWithMembers } from "@calcom/lib/server/queries/teams";
|
import { getTeamWithMembers } from "@calcom/lib/server/queries/teams";
|
||||||
|
import { stripMarkdown } from "@calcom/lib/stripMarkdown";
|
||||||
import { collectPageParameters, telemetryEventTypes, useTelemetry } from "@calcom/lib/telemetry";
|
import { collectPageParameters, telemetryEventTypes, useTelemetry } from "@calcom/lib/telemetry";
|
||||||
import prisma from "@calcom/prisma";
|
import prisma from "@calcom/prisma";
|
||||||
import { Avatar, AvatarGroup, Button, EmptyScreen, HeadSeo } from "@calcom/ui";
|
import { Avatar, AvatarGroup, Button, EmptyScreen, HeadSeo } from "@calcom/ui";
|
||||||
@@ -26,7 +27,7 @@ import Team from "@components/team/screens/Team";
|
|||||||
import { ssrInit } from "@server/lib/ssr";
|
import { ssrInit } from "@server/lib/ssr";
|
||||||
|
|
||||||
export type TeamPageProps = inferSSRProps<typeof getServerSideProps>;
|
export type TeamPageProps = inferSSRProps<typeof getServerSideProps>;
|
||||||
function TeamPage({ team, isUnpublished }: TeamPageProps) {
|
function TeamPage({ team, isUnpublished, markdownStrippedBio }: TeamPageProps) {
|
||||||
useTheme(team.theme);
|
useTheme(team.theme);
|
||||||
const showMembers = useToggleQuery("members");
|
const showMembers = useToggleQuery("members");
|
||||||
const { t } = useLocale();
|
const { t } = useLocale();
|
||||||
@@ -67,6 +68,11 @@ function TeamPage({ team, isUnpublished }: TeamPageProps) {
|
|||||||
<div className="px-6 py-4 ">
|
<div className="px-6 py-4 ">
|
||||||
<Link
|
<Link
|
||||||
href={`/team/${team.slug}/${type.slug}`}
|
href={`/team/${team.slug}/${type.slug}`}
|
||||||
|
onClick={async () => {
|
||||||
|
sdkActionManager?.fire("eventTypeSelected", {
|
||||||
|
eventType: type,
|
||||||
|
});
|
||||||
|
}}
|
||||||
data-testid="event-type-link"
|
data-testid="event-type-link"
|
||||||
className="flex justify-between">
|
className="flex justify-between">
|
||||||
<div className="flex-shrink">
|
<div className="flex-shrink">
|
||||||
@@ -100,7 +106,7 @@ function TeamPage({ team, isUnpublished }: TeamPageProps) {
|
|||||||
title={teamName}
|
title={teamName}
|
||||||
description={teamName}
|
description={teamName}
|
||||||
meeting={{
|
meeting={{
|
||||||
title: team?.bio || "",
|
title: markdownStrippedBio,
|
||||||
profile: { name: `${team.name}`, image: getPlaceholderAvatar(team.logo, team.name) },
|
profile: { name: `${team.name}`, image: getPlaceholderAvatar(team.logo, team.name) },
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
@@ -111,7 +117,7 @@ function TeamPage({ team, isUnpublished }: TeamPageProps) {
|
|||||||
{!isBioEmpty && (
|
{!isBioEmpty && (
|
||||||
<>
|
<>
|
||||||
<div
|
<div
|
||||||
className=" text-subtle text-sm [&_a]:text-blue-500 [&_a]:underline [&_a]:hover:text-blue-600"
|
className=" text-subtle break-words text-sm [&_a]:text-blue-500 [&_a]:underline [&_a]:hover:text-blue-600"
|
||||||
dangerouslySetInnerHTML={{ __html: team.safeBio }}
|
dangerouslySetInnerHTML={{ __html: team.safeBio }}
|
||||||
/>
|
/>
|
||||||
</>
|
</>
|
||||||
@@ -196,10 +202,14 @@ export const getServerSideProps = async (context: GetServerSidePropsContext) =>
|
|||||||
return { ...member, safeBio: markdownToSafeHTML(member.bio || "") };
|
return { ...member, safeBio: markdownToSafeHTML(member.bio || "") };
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const markdownStrippedBio = stripMarkdown(team?.bio || "");
|
||||||
|
|
||||||
return {
|
return {
|
||||||
props: {
|
props: {
|
||||||
team: { ...team, safeBio, members },
|
team: { ...team, safeBio, members },
|
||||||
|
themeBasis: team.slug,
|
||||||
trpcState: ssr.dehydrate(),
|
trpcState: ssr.dehydrate(),
|
||||||
|
markdownStrippedBio,
|
||||||
},
|
},
|
||||||
} as const;
|
} as const;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -196,6 +196,7 @@ export const getServerSideProps = async (context: GetServerSidePropsContext) =>
|
|||||||
brandColor: team.brandColor,
|
brandColor: team.brandColor,
|
||||||
darkBrandColor: team.darkBrandColor,
|
darkBrandColor: team.darkBrandColor,
|
||||||
},
|
},
|
||||||
|
themeBasis: team.slug,
|
||||||
date: dateParam,
|
date: dateParam,
|
||||||
eventType: eventTypeObject,
|
eventType: eventTypeObject,
|
||||||
workingHours,
|
workingHours,
|
||||||
|
|||||||
@@ -153,6 +153,7 @@ export async function getServerSideProps(context: GetServerSidePropsContext) {
|
|||||||
image: eventTypeObject.team?.logo || null,
|
image: eventTypeObject.team?.logo || null,
|
||||||
eventName: null,
|
eventName: null,
|
||||||
},
|
},
|
||||||
|
themeBasis: eventTypeObject.team?.slug,
|
||||||
eventType: eventTypeObject,
|
eventType: eventTypeObject,
|
||||||
recurringEventCount,
|
recurringEventCount,
|
||||||
booking,
|
booking,
|
||||||
|
|||||||
@@ -8,6 +8,8 @@ import {
|
|||||||
bookTimeSlot,
|
bookTimeSlot,
|
||||||
selectFirstAvailableTimeSlotNextMonth,
|
selectFirstAvailableTimeSlotNextMonth,
|
||||||
selectSecondAvailableTimeSlotNextMonth,
|
selectSecondAvailableTimeSlotNextMonth,
|
||||||
|
testEmail,
|
||||||
|
testName,
|
||||||
} from "./lib/testUtils";
|
} from "./lib/testUtils";
|
||||||
|
|
||||||
test.describe.configure({ mode: "parallel" });
|
test.describe.configure({ mode: "parallel" });
|
||||||
@@ -101,6 +103,8 @@ testBothBookers.describe("pro user", () => {
|
|||||||
// Because it tests the entire booking flow + the cancellation + rebooking
|
// Because it tests the entire booking flow + the cancellation + rebooking
|
||||||
test.setTimeout(testInfo.timeout * 3);
|
test.setTimeout(testInfo.timeout * 3);
|
||||||
await bookFirstEvent(page);
|
await bookFirstEvent(page);
|
||||||
|
await expect(page.locator(`[data-testid="attendee-email-${testEmail}"]`)).toHaveText(testEmail);
|
||||||
|
await expect(page.locator(`[data-testid="attendee-name-${testName}"]`)).toHaveText(testName);
|
||||||
|
|
||||||
const [pro] = users.get();
|
const [pro] = users.get();
|
||||||
await pro.login();
|
await pro.login();
|
||||||
@@ -114,7 +118,10 @@ testBothBookers.describe("pro user", () => {
|
|||||||
|
|
||||||
const cancelledHeadline = await page.locator('[data-testid="cancelled-headline"]').innerText();
|
const cancelledHeadline = await page.locator('[data-testid="cancelled-headline"]').innerText();
|
||||||
|
|
||||||
await expect(cancelledHeadline).toBe("This event is cancelled");
|
expect(cancelledHeadline).toBe("This event is cancelled");
|
||||||
|
|
||||||
|
await expect(page.locator(`[data-testid="attendee-email-${testEmail}"]`)).toHaveText(testEmail);
|
||||||
|
await expect(page.locator(`[data-testid="attendee-name-${testName}"]`)).toHaveText(testName);
|
||||||
|
|
||||||
await page.goto(`/${pro.username}`);
|
await page.goto(`/${pro.username}`);
|
||||||
await bookFirstEvent(page);
|
await bookFirstEvent(page);
|
||||||
@@ -155,7 +162,7 @@ testBothBookers.describe("pro user", () => {
|
|||||||
await expect(page.locator("[data-testid=success-page]")).toBeVisible();
|
await expect(page.locator("[data-testid=success-page]")).toBeVisible();
|
||||||
|
|
||||||
additionalGuests.forEach(async (email) => {
|
additionalGuests.forEach(async (email) => {
|
||||||
await expect(page.locator(`[data-testid="attendee-${email}"]`)).toHaveText(email);
|
await expect(page.locator(`[data-testid="attendee-email-${email}"]`)).toHaveText(email);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -419,9 +419,13 @@ testBothBookers.describe("Booking with Seats", (bookerVariant) => {
|
|||||||
// Go to cancel page and see that attendees are listed and myself as I'm owner of the booking
|
// Go to cancel page and see that attendees are listed and myself as I'm owner of the booking
|
||||||
await page.goto(`/booking/${booking.uid}?cancel=true&allRemainingBookings=false`);
|
await page.goto(`/booking/${booking.uid}?cancel=true&allRemainingBookings=false`);
|
||||||
|
|
||||||
const foundFirstAttendeeAsOwner = await page.locator('p[data-testid="attendee-first+seats@cal.com"]');
|
const foundFirstAttendeeAsOwner = await page.locator(
|
||||||
|
'p[data-testid="attendee-email-first+seats@cal.com"]'
|
||||||
|
);
|
||||||
await expect(foundFirstAttendeeAsOwner).toHaveCount(1);
|
await expect(foundFirstAttendeeAsOwner).toHaveCount(1);
|
||||||
const foundSecondAttendeeAsOwner = await page.locator('p[data-testid="attendee-second+seats@cal.com"]');
|
const foundSecondAttendeeAsOwner = await page.locator(
|
||||||
|
'p[data-testid="attendee-email-second+seats@cal.com"]'
|
||||||
|
);
|
||||||
await expect(foundSecondAttendeeAsOwner).toHaveCount(1);
|
await expect(foundSecondAttendeeAsOwner).toHaveCount(1);
|
||||||
await page.pause();
|
await page.pause();
|
||||||
await page.goto("auth/logout");
|
await page.goto("auth/logout");
|
||||||
@@ -433,10 +437,12 @@ testBothBookers.describe("Booking with Seats", (bookerVariant) => {
|
|||||||
);
|
);
|
||||||
|
|
||||||
// No attendees should be displayed only the one that it's cancelling
|
// No attendees should be displayed only the one that it's cancelling
|
||||||
const notFoundSecondAttendee = await page.locator('p[data-testid="attendee-second+seats@cal.com"]');
|
const notFoundSecondAttendee = await page.locator(
|
||||||
|
'p[data-testid="attendee-email-second+seats@cal.com"]'
|
||||||
|
);
|
||||||
|
|
||||||
await expect(notFoundSecondAttendee).toHaveCount(0);
|
await expect(notFoundSecondAttendee).toHaveCount(0);
|
||||||
const foundFirstAttendee = await page.locator('p[data-testid="attendee-first+seats@cal.com"]');
|
const foundFirstAttendee = await page.locator('p[data-testid="attendee-email-first+seats@cal.com"]');
|
||||||
await expect(foundFirstAttendee).toHaveCount(1);
|
await expect(foundFirstAttendee).toHaveCount(1);
|
||||||
|
|
||||||
await prisma.eventType.update({
|
await prisma.eventType.update({
|
||||||
@@ -453,11 +459,11 @@ testBothBookers.describe("Booking with Seats", (bookerVariant) => {
|
|||||||
);
|
);
|
||||||
|
|
||||||
// Now attendees should be displayed
|
// Now attendees should be displayed
|
||||||
const foundSecondAttendee = await page.locator('p[data-testid="attendee-second+seats@cal.com"]');
|
const foundSecondAttendee = await page.locator('p[data-testid="attendee-email-second+seats@cal.com"]');
|
||||||
|
|
||||||
await expect(foundSecondAttendee).toHaveCount(1);
|
await expect(foundSecondAttendee).toHaveCount(1);
|
||||||
const foundFirstAttendeeAgain = await page
|
const foundFirstAttendeeAgain = await page
|
||||||
.locator('p[data-testid="attendee-first+seats@cal.com"]')
|
.locator('p[data-testid="attendee-email-first+seats@cal.com"]')
|
||||||
.first();
|
.first();
|
||||||
await expect(foundFirstAttendeeAgain).toHaveCount(1);
|
await expect(foundFirstAttendeeAgain).toHaveCount(1);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -15,6 +15,9 @@ type Request = IncomingMessage & { body?: unknown };
|
|||||||
type RequestHandlerOptions = { req: Request; res: ServerResponse };
|
type RequestHandlerOptions = { req: Request; res: ServerResponse };
|
||||||
type RequestHandler = (opts: RequestHandlerOptions) => void;
|
type RequestHandler = (opts: RequestHandlerOptions) => void;
|
||||||
|
|
||||||
|
export const testEmail = "test@example.com";
|
||||||
|
export const testName = "Test Testson";
|
||||||
|
|
||||||
export function createHttpServer(opts: { requestHandler?: RequestHandler } = {}) {
|
export function createHttpServer(opts: { requestHandler?: RequestHandler } = {}) {
|
||||||
const {
|
const {
|
||||||
requestHandler = ({ res }) => {
|
requestHandler = ({ res }) => {
|
||||||
@@ -133,8 +136,8 @@ export async function bookFirstEvent(page: Page) {
|
|||||||
|
|
||||||
export const bookTimeSlot = async (page: Page, opts?: { name?: string; email?: string }) => {
|
export const bookTimeSlot = async (page: Page, opts?: { name?: string; email?: string }) => {
|
||||||
// --- fill form
|
// --- fill form
|
||||||
await page.fill('[name="name"]', opts?.name ?? "Test Testson");
|
await page.fill('[name="name"]', opts?.name ?? testName);
|
||||||
await page.fill('[name="email"]', opts?.email ?? "test@example.com");
|
await page.fill('[name="email"]', opts?.email ?? testEmail);
|
||||||
await page.press('[name="email"]', "Enter");
|
await page.press('[name="email"]', "Enter");
|
||||||
};
|
};
|
||||||
// Provide an standalone localize utility not managed by next-i18n
|
// Provide an standalone localize utility not managed by next-i18n
|
||||||
|
|||||||
@@ -47,10 +47,19 @@ test.describe("Onboarding", () => {
|
|||||||
// tests skip button, we don't want to test entire flow.
|
// tests skip button, we don't want to test entire flow.
|
||||||
await page.locator("button[data-testid=skip-step]").click();
|
await page.locator("button[data-testid=skip-step]").click();
|
||||||
|
|
||||||
await expect(page).toHaveURL(/.*setup-availability/);
|
await expect(page).toHaveURL(/.*connected-video/);
|
||||||
});
|
});
|
||||||
|
|
||||||
await test.step("step 3", async () => {
|
await test.step("step 3", async () => {
|
||||||
|
const isDisabled = await page.locator("button[data-testid=save-video-button]").isDisabled();
|
||||||
|
await expect(isDisabled).toBe(true);
|
||||||
|
// tests skip button, we don't want to test entire flow.
|
||||||
|
await page.locator("button[data-testid=skip-step]").click();
|
||||||
|
|
||||||
|
await expect(page).toHaveURL(/.*setup-availability/);
|
||||||
|
});
|
||||||
|
|
||||||
|
await test.step("step 4", async () => {
|
||||||
const isDisabled = await page.locator("button[data-testid=save-availability]").isDisabled();
|
const isDisabled = await page.locator("button[data-testid=save-availability]").isDisabled();
|
||||||
await expect(isDisabled).toBe(false);
|
await expect(isDisabled).toBe(false);
|
||||||
// same here, skip this step.
|
// same here, skip this step.
|
||||||
@@ -59,7 +68,7 @@ test.describe("Onboarding", () => {
|
|||||||
await expect(page).toHaveURL(/.*user-profile/);
|
await expect(page).toHaveURL(/.*user-profile/);
|
||||||
});
|
});
|
||||||
|
|
||||||
await test.step("step 4", async () => {
|
await test.step("step 5", async () => {
|
||||||
await page.locator("button[type=submit]").click();
|
await page.locator("button[type=submit]").click();
|
||||||
|
|
||||||
// should redirect to /event-types after onboarding
|
// should redirect to /event-types after onboarding
|
||||||
|
|||||||
Binary file not shown.
|
Before Width: | Height: | Size: 485 B After Width: | Height: | Size: 743 B |
@@ -1,3 +1 @@
|
|||||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-6 h-6">
|
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-ban"><circle cx="12" cy="12" r="10"></circle><line x1="4.93" x2="19.07" y1="4.93" y2="19.07"></line></svg>
|
||||||
<path stroke="white" stroke-linecap="round" stroke-linejoin="round" d="M18.364 18.364A9 9 0 005.636 5.636m12.728 12.728A9 9 0 015.636 5.636m12.728 12.728L5.636 5.636" />
|
|
||||||
</svg>
|
|
||||||
|
Before Width: | Height: | Size: 309 B After Width: | Height: | Size: 308 B |
@@ -1572,7 +1572,7 @@
|
|||||||
"sender_name": "اسم المرسل",
|
"sender_name": "اسم المرسل",
|
||||||
"already_invited": "تمت دعوة هذا الحاضر بالفعل",
|
"already_invited": "تمت دعوة هذا الحاضر بالفعل",
|
||||||
"no_recordings_found": "لم يتم العثور على تسجيلات",
|
"no_recordings_found": "لم يتم العثور على تسجيلات",
|
||||||
"new_workflow_subtitle": "سير عمل جديد من أجل...",
|
"new_workflow_subtitle": "سير عمل جديد لـ...",
|
||||||
"reporting": "تقديم التقارير",
|
"reporting": "تقديم التقارير",
|
||||||
"reporting_feature": "عرض جميع البيانات الواردة وتنزيلها كملف CSV",
|
"reporting_feature": "عرض جميع البيانات الواردة وتنزيلها كملف CSV",
|
||||||
"teams_plan_required": "خطة Teams مطلوبة",
|
"teams_plan_required": "خطة Teams مطلوبة",
|
||||||
@@ -1724,7 +1724,6 @@
|
|||||||
"locked_apps_description": "سيكون الأعضاء قادرين على رؤية التطبيقات النشطة، ولكن من دون القدرة على تعديل أي إعدادات للتطبيق",
|
"locked_apps_description": "سيكون الأعضاء قادرين على رؤية التطبيقات النشطة، ولكن من دون القدرة على تعديل أي إعدادات للتطبيق",
|
||||||
"locked_webhooks_description": "سيكون الأعضاء قادرين على رؤية قوالب الويب النشطة، ولكن من دون القدرة على تعديل أي إعدادات للقوالب",
|
"locked_webhooks_description": "سيكون الأعضاء قادرين على رؤية قوالب الويب النشطة، ولكن من دون القدرة على تعديل أي إعدادات للقوالب",
|
||||||
"locked_workflows_description": "سيكون الأعضاء قادرين على رؤية مسارات العمل النشطة، ولكن من دون القدرة على تعديل أي إعدادات لمسار العمل",
|
"locked_workflows_description": "سيكون الأعضاء قادرين على رؤية مسارات العمل النشطة، ولكن من دون القدرة على تعديل أي إعدادات لمسار العمل",
|
||||||
"locked_by_admin": "مقفل من قبل المشرف",
|
|
||||||
"app_not_connected": "أنت غير متصل بحساب {{appName}}.",
|
"app_not_connected": "أنت غير متصل بحساب {{appName}}.",
|
||||||
"connect_now": "اتصل الآن",
|
"connect_now": "اتصل الآن",
|
||||||
"managed_event_dialog_confirm_button_one": "استبدال وإشعار العضو {{count}}",
|
"managed_event_dialog_confirm_button_one": "استبدال وإشعار العضو {{count}}",
|
||||||
|
|||||||
@@ -1724,7 +1724,6 @@
|
|||||||
"locked_apps_description": "Členové budou vidět aktivní aplikace, ale nastavení aplikací upravovat moci nebudou",
|
"locked_apps_description": "Členové budou vidět aktivní aplikace, ale nastavení aplikací upravovat moci nebudou",
|
||||||
"locked_webhooks_description": "Členové budou vidět aktivní webhooky, ale nastavení webhooků upravovat moci nebudou",
|
"locked_webhooks_description": "Členové budou vidět aktivní webhooky, ale nastavení webhooků upravovat moci nebudou",
|
||||||
"locked_workflows_description": "Členové budou vidět aktivní pracovní postupy, ale nastavení pracovních postupů upravovat moci nebudou",
|
"locked_workflows_description": "Členové budou vidět aktivní pracovní postupy, ale nastavení pracovních postupů upravovat moci nebudou",
|
||||||
"locked_by_admin": "Uzamčeno správcem",
|
|
||||||
"app_not_connected": "Nemáte připojený účet {{appName}}.",
|
"app_not_connected": "Nemáte připojený účet {{appName}}.",
|
||||||
"connect_now": "Připojte se",
|
"connect_now": "Připojte se",
|
||||||
"managed_event_dialog_confirm_button_one": "Nahradit a upozornit {{count}} člena",
|
"managed_event_dialog_confirm_button_one": "Nahradit a upozornit {{count}} člena",
|
||||||
|
|||||||
@@ -1724,7 +1724,6 @@
|
|||||||
"locked_apps_description": "Mitglieder können die aktiven Apps sehen, können aber keine App-Einstellungen anpassen",
|
"locked_apps_description": "Mitglieder können die aktiven Apps sehen, können aber keine App-Einstellungen anpassen",
|
||||||
"locked_webhooks_description": "Mitglieder können die aktiven Webhooks sehen, können aber keine Webhook-Einstellungen anpassen",
|
"locked_webhooks_description": "Mitglieder können die aktiven Webhooks sehen, können aber keine Webhook-Einstellungen anpassen",
|
||||||
"locked_workflows_description": "Mitglieder können die aktiven Workflows sehen, können aber keine Workflow-Einstellungen anpassen",
|
"locked_workflows_description": "Mitglieder können die aktiven Workflows sehen, können aber keine Workflow-Einstellungen anpassen",
|
||||||
"locked_by_admin": "Von Admin gesperrt",
|
|
||||||
"app_not_connected": "Sie haben kein {{appName}}-Konto verbunden.",
|
"app_not_connected": "Sie haben kein {{appName}}-Konto verbunden.",
|
||||||
"connect_now": "Jetzt verbinden",
|
"connect_now": "Jetzt verbinden",
|
||||||
"managed_event_dialog_confirm_button_one": "{{count}} Mitglied ersetzen & benachrichtigen",
|
"managed_event_dialog_confirm_button_one": "{{count}} Mitglied ersetzen & benachrichtigen",
|
||||||
|
|||||||
@@ -237,6 +237,8 @@
|
|||||||
"set_availability": "Set your availability",
|
"set_availability": "Set your availability",
|
||||||
"continue_without_calendar": "Continue without calendar",
|
"continue_without_calendar": "Continue without calendar",
|
||||||
"connect_your_calendar": "Connect your calendar",
|
"connect_your_calendar": "Connect your calendar",
|
||||||
|
"connect_your_video_app":"Connect your video apps",
|
||||||
|
"connect_your_video_app_instructions":"Connect your video apps to use them on your event types.",
|
||||||
"connect_your_calendar_instructions": "Connect your calendar to automatically check for busy times and new events as they’re scheduled.",
|
"connect_your_calendar_instructions": "Connect your calendar to automatically check for busy times and new events as they’re scheduled.",
|
||||||
"set_up_later": "Set up later",
|
"set_up_later": "Set up later",
|
||||||
"current_time": "Current time",
|
"current_time": "Current time",
|
||||||
@@ -367,6 +369,7 @@
|
|||||||
"create_webhook": "Create Webhook",
|
"create_webhook": "Create Webhook",
|
||||||
"booking_cancelled": "Booking Cancelled",
|
"booking_cancelled": "Booking Cancelled",
|
||||||
"booking_rescheduled": "Booking Rescheduled",
|
"booking_rescheduled": "Booking Rescheduled",
|
||||||
|
"recording_ready":"Recording Download Link Ready",
|
||||||
"booking_created": "Booking Created",
|
"booking_created": "Booking Created",
|
||||||
"meeting_ended": "Meeting Ended",
|
"meeting_ended": "Meeting Ended",
|
||||||
"form_submitted": "Form Submitted",
|
"form_submitted": "Form Submitted",
|
||||||
@@ -753,6 +756,10 @@
|
|||||||
"new_event_type_to_book_description": "Create a new event type for people to book times with.",
|
"new_event_type_to_book_description": "Create a new event type for people to book times with.",
|
||||||
"length": "Length",
|
"length": "Length",
|
||||||
"minimum_booking_notice": "Minimum Notice",
|
"minimum_booking_notice": "Minimum Notice",
|
||||||
|
"offset_toggle": "Offset start times",
|
||||||
|
"offset_toggle_description": "Offset timeslots shown to bookers by a specified number of minutes",
|
||||||
|
"offset_start": "Offset by",
|
||||||
|
"offset_start_description": "e.g. this will show time slots to your bookers at {{ adjustedTime }} instead of {{ originalTime }}",
|
||||||
"slot_interval": "Time-slot intervals",
|
"slot_interval": "Time-slot intervals",
|
||||||
"slot_interval_default": "Use event length (default)",
|
"slot_interval_default": "Use event length (default)",
|
||||||
"delete_event_type": "Delete event type?",
|
"delete_event_type": "Delete event type?",
|
||||||
@@ -1192,6 +1199,7 @@
|
|||||||
"create_workflow": "Create a workflow",
|
"create_workflow": "Create a workflow",
|
||||||
"do_this": "Do this",
|
"do_this": "Do this",
|
||||||
"turn_off": "Turn off",
|
"turn_off": "Turn off",
|
||||||
|
"turn_on": "Turn on",
|
||||||
"settings_updated_successfully": "Settings updated successfully",
|
"settings_updated_successfully": "Settings updated successfully",
|
||||||
"error_updating_settings": "Error updating settings",
|
"error_updating_settings": "Error updating settings",
|
||||||
"personal_cal_url": "My personal {{appName}} URL",
|
"personal_cal_url": "My personal {{appName}} URL",
|
||||||
@@ -1623,6 +1631,7 @@
|
|||||||
"email_user_cta": "View Invitation",
|
"email_user_cta": "View Invitation",
|
||||||
"email_no_user_invite_heading": "You’ve been invited to join a team on {{appName}}",
|
"email_no_user_invite_heading": "You’ve been invited to join a team on {{appName}}",
|
||||||
"email_no_user_invite_subheading": "{{invitedBy}} has invited you to join their team on {{appName}}. {{appName}} is the event-juggling scheduler that enables you and your team to schedule meetings without the email tennis.",
|
"email_no_user_invite_subheading": "{{invitedBy}} has invited you to join their team on {{appName}}. {{appName}} is the event-juggling scheduler that enables you and your team to schedule meetings without the email tennis.",
|
||||||
|
"email_user_invite_subheading": "{{invitedBy}} has invited you to join their team `{{teamName}}` on {{appName}}. {{appName}} is the event-juggling scheduler that enables you and your team to schedule meetings without the email tennis.",
|
||||||
"email_no_user_invite_steps_intro": "We’ll walk you through a few short steps and you’ll be enjoying stress free scheduling with your team in no time.",
|
"email_no_user_invite_steps_intro": "We’ll walk you through a few short steps and you’ll be enjoying stress free scheduling with your team in no time.",
|
||||||
"email_no_user_step_one": "Choose your username",
|
"email_no_user_step_one": "Choose your username",
|
||||||
"email_no_user_step_two": "Connect your calendar account",
|
"email_no_user_step_two": "Connect your calendar account",
|
||||||
@@ -1736,7 +1745,7 @@
|
|||||||
"locked_apps_description": "Members will be able to see the active apps but will not be able to edit any app settings",
|
"locked_apps_description": "Members will be able to see the active apps but will not be able to edit any app settings",
|
||||||
"locked_webhooks_description": "Members will be able to see the active webhooks but will not be able to edit any webhook settings",
|
"locked_webhooks_description": "Members will be able to see the active webhooks but will not be able to edit any webhook settings",
|
||||||
"locked_workflows_description": "Members will be able to see the active workflows but will not be able to edit any workflow settings",
|
"locked_workflows_description": "Members will be able to see the active workflows but will not be able to edit any workflow settings",
|
||||||
"locked_by_admin": "Locked by admin",
|
"locked_by_admin": "Locked by team admin",
|
||||||
"app_not_connected": "You have not connected a {{appName}} account.",
|
"app_not_connected": "You have not connected a {{appName}} account.",
|
||||||
"connect_now": "Connect now",
|
"connect_now": "Connect now",
|
||||||
"managed_event_dialog_confirm_button_one": "Replace & notify {{count}} member",
|
"managed_event_dialog_confirm_button_one": "Replace & notify {{count}} member",
|
||||||
@@ -1810,6 +1819,8 @@
|
|||||||
"open_dialog_with_element_click": "Open your Cal dialog when someone clicks an element.",
|
"open_dialog_with_element_click": "Open your Cal dialog when someone clicks an element.",
|
||||||
"need_help_embedding": "Need help? See our guides for embedding Cal on Wix, Squarespace, or WordPress, check our common questions, or explore advanced embed options.",
|
"need_help_embedding": "Need help? See our guides for embedding Cal on Wix, Squarespace, or WordPress, check our common questions, or explore advanced embed options.",
|
||||||
"book_my_cal": "Book my Cal",
|
"book_my_cal": "Book my Cal",
|
||||||
|
"form_updated_successfully":"Form updated successfully.",
|
||||||
|
"email_not_cal_member_cta": "Join your team",
|
||||||
"disable_attendees_confirmation_emails": "Disable default confirmation emails for attendees",
|
"disable_attendees_confirmation_emails": "Disable default confirmation emails for attendees",
|
||||||
"disable_attendees_confirmation_emails_description": "At least one workflow is active on this event type that sends an email to the attendees when the event is booked.",
|
"disable_attendees_confirmation_emails_description": "At least one workflow is active on this event type that sends an email to the attendees when the event is booked.",
|
||||||
"disable_host_confirmation_emails": "Disable default confirmation emails for host",
|
"disable_host_confirmation_emails": "Disable default confirmation emails for host",
|
||||||
|
|||||||
@@ -1724,7 +1724,6 @@
|
|||||||
"locked_apps_description": "Los miembros podrán ver las aplicaciones activas pero no podrán editar ninguna configuración de la aplicación",
|
"locked_apps_description": "Los miembros podrán ver las aplicaciones activas pero no podrán editar ninguna configuración de la aplicación",
|
||||||
"locked_webhooks_description": "Los miembros podrán ver los webhooks activos, pero no podrán editar ninguna configuración de webhooks",
|
"locked_webhooks_description": "Los miembros podrán ver los webhooks activos, pero no podrán editar ninguna configuración de webhooks",
|
||||||
"locked_workflows_description": "Los miembros podrán ver los flujos de trabajo activos, pero no podrán editar ninguna configuración de flujo de trabajo",
|
"locked_workflows_description": "Los miembros podrán ver los flujos de trabajo activos, pero no podrán editar ninguna configuración de flujo de trabajo",
|
||||||
"locked_by_admin": "Bloqueado por el administrador",
|
|
||||||
"app_not_connected": "No ha conectado una cuenta de {{appName}}.",
|
"app_not_connected": "No ha conectado una cuenta de {{appName}}.",
|
||||||
"connect_now": "Conectar ahora",
|
"connect_now": "Conectar ahora",
|
||||||
"managed_event_dialog_confirm_button_one": "Reemplazar y notificar a {{count}} miembro",
|
"managed_event_dialog_confirm_button_one": "Reemplazar y notificar a {{count}} miembro",
|
||||||
|
|||||||
@@ -237,6 +237,8 @@
|
|||||||
"set_availability": "Définissez vos disponibilités",
|
"set_availability": "Définissez vos disponibilités",
|
||||||
"continue_without_calendar": "Continuer sans calendrier",
|
"continue_without_calendar": "Continuer sans calendrier",
|
||||||
"connect_your_calendar": "Connectez votre calendrier",
|
"connect_your_calendar": "Connectez votre calendrier",
|
||||||
|
"connect_your_video_app": "Connectez vos applications vidéo",
|
||||||
|
"connect_your_video_app_instructions": "Connectez vos applications vidéo pour les utiliser sur vos types d'événements.",
|
||||||
"connect_your_calendar_instructions": "Connectez votre calendrier pour vérifier automatiquement vos disponibilités ainsi que les nouveaux événements lorsqu'ils sont planifiés.",
|
"connect_your_calendar_instructions": "Connectez votre calendrier pour vérifier automatiquement vos disponibilités ainsi que les nouveaux événements lorsqu'ils sont planifiés.",
|
||||||
"set_up_later": "Configurer plus tard",
|
"set_up_later": "Configurer plus tard",
|
||||||
"current_time": "Heure actuelle",
|
"current_time": "Heure actuelle",
|
||||||
@@ -367,6 +369,7 @@
|
|||||||
"create_webhook": "Créer un Webhook",
|
"create_webhook": "Créer un Webhook",
|
||||||
"booking_cancelled": "Réservation annulée",
|
"booking_cancelled": "Réservation annulée",
|
||||||
"booking_rescheduled": "Réservation replanifiée",
|
"booking_rescheduled": "Réservation replanifiée",
|
||||||
|
"recording_ready": "Lien de téléchargement d'enregistrement prêt",
|
||||||
"booking_created": "Réservation créée",
|
"booking_created": "Réservation créée",
|
||||||
"meeting_ended": "Rendez-vous terminé",
|
"meeting_ended": "Rendez-vous terminé",
|
||||||
"form_submitted": "Formulaire envoyé",
|
"form_submitted": "Formulaire envoyé",
|
||||||
@@ -1192,6 +1195,7 @@
|
|||||||
"create_workflow": "Créer un workflow",
|
"create_workflow": "Créer un workflow",
|
||||||
"do_this": "Effectuer ceci",
|
"do_this": "Effectuer ceci",
|
||||||
"turn_off": "Désactiver",
|
"turn_off": "Désactiver",
|
||||||
|
"turn_on": "Activer",
|
||||||
"settings_updated_successfully": "Paramètres mis à jour avec succès",
|
"settings_updated_successfully": "Paramètres mis à jour avec succès",
|
||||||
"error_updating_settings": "Erreur lors de la mise à jour des paramètres",
|
"error_updating_settings": "Erreur lors de la mise à jour des paramètres",
|
||||||
"personal_cal_url": "Mon lien {{appName}} personnel",
|
"personal_cal_url": "Mon lien {{appName}} personnel",
|
||||||
@@ -1623,6 +1627,7 @@
|
|||||||
"email_user_cta": "Voir l'invitation",
|
"email_user_cta": "Voir l'invitation",
|
||||||
"email_no_user_invite_heading": "Vous avez été invité(e) à rejoindre une équipe sur {{appName}}",
|
"email_no_user_invite_heading": "Vous avez été invité(e) à rejoindre une équipe sur {{appName}}",
|
||||||
"email_no_user_invite_subheading": "{{invitedBy}} a été invité à rejoindre son équipe sur {{appName}}. {{appName}} est le planificateur d'événements qui vous permet, à vous et à votre équipe, de planifier des rendez-vous sans les allers-retours par e-mail.",
|
"email_no_user_invite_subheading": "{{invitedBy}} a été invité à rejoindre son équipe sur {{appName}}. {{appName}} est le planificateur d'événements qui vous permet, à vous et à votre équipe, de planifier des rendez-vous sans les allers-retours par e-mail.",
|
||||||
|
"email_user_invite_subheading": "{{invitedBy}} vous a invité à rejoindre son équipe « {{teamName}} » sur {{appName}}. {{appName}} est le planificateur d'événements qui vous permet, à vous et à votre équipe, de planifier des réunions sans les allers-retours par e-mail.",
|
||||||
"email_no_user_invite_steps_intro": "Nous vous guiderons à travers quelques étapes courtes et vous profiterez d'une planification sans stress avec votre équipe en un rien de temps.",
|
"email_no_user_invite_steps_intro": "Nous vous guiderons à travers quelques étapes courtes et vous profiterez d'une planification sans stress avec votre équipe en un rien de temps.",
|
||||||
"email_no_user_step_one": "Choisissez votre nom d'utilisateur",
|
"email_no_user_step_one": "Choisissez votre nom d'utilisateur",
|
||||||
"email_no_user_step_two": "Connectez votre compte de calendrier",
|
"email_no_user_step_two": "Connectez votre compte de calendrier",
|
||||||
@@ -1736,7 +1741,7 @@
|
|||||||
"locked_apps_description": "Les membres pourront voir les applications actives, mais ne pourront pas modifier leurs paramètres.",
|
"locked_apps_description": "Les membres pourront voir les applications actives, mais ne pourront pas modifier leurs paramètres.",
|
||||||
"locked_webhooks_description": "Les membres pourront voir les webhooks actifs, mais ne pourront pas modifier leurs paramètres.",
|
"locked_webhooks_description": "Les membres pourront voir les webhooks actifs, mais ne pourront pas modifier leurs paramètres.",
|
||||||
"locked_workflows_description": "Les membres pourront voir les workflows actifs, mais ne pourront pas modifier leurs paramètres.",
|
"locked_workflows_description": "Les membres pourront voir les workflows actifs, mais ne pourront pas modifier leurs paramètres.",
|
||||||
"locked_by_admin": "Verrouillé par l'administrateur",
|
"locked_by_admin": "Verrouillé par l'administrateur de l'équipe",
|
||||||
"app_not_connected": "Vous n'avez pas connecté de compte {{appName}}.",
|
"app_not_connected": "Vous n'avez pas connecté de compte {{appName}}.",
|
||||||
"connect_now": "Connecter maintenant",
|
"connect_now": "Connecter maintenant",
|
||||||
"managed_event_dialog_confirm_button_one": "Remplacer et notifier {{count}} membre",
|
"managed_event_dialog_confirm_button_one": "Remplacer et notifier {{count}} membre",
|
||||||
@@ -1805,6 +1810,7 @@
|
|||||||
"inline_embed": "Intégration en ligne",
|
"inline_embed": "Intégration en ligne",
|
||||||
"load_inline_content": "Charge votre type d'événement directement en ligne avec le contenu de votre site web.",
|
"load_inline_content": "Charge votre type d'événement directement en ligne avec le contenu de votre site web.",
|
||||||
"floating_pop_up_button": "Bouton pop-up flottant",
|
"floating_pop_up_button": "Bouton pop-up flottant",
|
||||||
|
"email_not_cal_member_cta": "Rejoignez votre équipe",
|
||||||
"disable_attendees_confirmation_emails": "Désactiver les e-mails de confirmation par défaut pour les participants",
|
"disable_attendees_confirmation_emails": "Désactiver les e-mails de confirmation par défaut pour les participants",
|
||||||
"disable_attendees_confirmation_emails_description": "Au moins un workflow est actif sur ce type d'événement qui envoie un e-mail aux participants lorsque l'événement est réservé.",
|
"disable_attendees_confirmation_emails_description": "Au moins un workflow est actif sur ce type d'événement qui envoie un e-mail aux participants lorsque l'événement est réservé.",
|
||||||
"disable_host_confirmation_emails": "Désactiver les e-mails de confirmation par défaut pour l'hôte",
|
"disable_host_confirmation_emails": "Désactiver les e-mails de confirmation par défaut pour l'hôte",
|
||||||
|
|||||||
@@ -1724,7 +1724,6 @@
|
|||||||
"locked_apps_description": "I membri saranno in grado di vedere le app attive, ma non saranno in grado di modificare le impostazioni delle app",
|
"locked_apps_description": "I membri saranno in grado di vedere le app attive, ma non saranno in grado di modificare le impostazioni delle app",
|
||||||
"locked_webhooks_description": "I membri saranno in grado di vedere i webhook attivi, ma non saranno in grado di modificare le impostazioni dei webhook",
|
"locked_webhooks_description": "I membri saranno in grado di vedere i webhook attivi, ma non saranno in grado di modificare le impostazioni dei webhook",
|
||||||
"locked_workflows_description": "I membri saranno in grado di vedere i flussi di lavoro attivi, ma non saranno in grado di modificare le impostazioni dei flussi di lavoro",
|
"locked_workflows_description": "I membri saranno in grado di vedere i flussi di lavoro attivi, ma non saranno in grado di modificare le impostazioni dei flussi di lavoro",
|
||||||
"locked_by_admin": "Bloccato dall'amministratore",
|
|
||||||
"app_not_connected": "Non è stato connesso un account di {{appName}}.",
|
"app_not_connected": "Non è stato connesso un account di {{appName}}.",
|
||||||
"connect_now": "Connetti ora",
|
"connect_now": "Connetti ora",
|
||||||
"managed_event_dialog_confirm_button_one": "Sostituisci e invia notifica a {{count}} membro",
|
"managed_event_dialog_confirm_button_one": "Sostituisci e invia notifica a {{count}} membro",
|
||||||
|
|||||||
@@ -1724,7 +1724,6 @@
|
|||||||
"locked_apps_description": "メンバーは有効なアプリを確認できますが、アプリの設定を編集することはできません",
|
"locked_apps_description": "メンバーは有効なアプリを確認できますが、アプリの設定を編集することはできません",
|
||||||
"locked_webhooks_description": "メンバーは有効なウェブフックを確認できますが、ウェブフックの設定を編集することはできません",
|
"locked_webhooks_description": "メンバーは有効なウェブフックを確認できますが、ウェブフックの設定を編集することはできません",
|
||||||
"locked_workflows_description": "メンバーは有効なワークフローを確認できますが、ワークフローの設定を編集することはできません",
|
"locked_workflows_description": "メンバーは有効なワークフローを確認できますが、ワークフローの設定を編集することはできません",
|
||||||
"locked_by_admin": "管理者によりロックされています",
|
|
||||||
"app_not_connected": "{{appName}} のアカウントに接続していません。",
|
"app_not_connected": "{{appName}} のアカウントに接続していません。",
|
||||||
"connect_now": "今すぐ接続",
|
"connect_now": "今すぐ接続",
|
||||||
"managed_event_dialog_confirm_button_one": "{{count}} 人のメンバーを置き換えて通知する",
|
"managed_event_dialog_confirm_button_one": "{{count}} 人のメンバーを置き換えて通知する",
|
||||||
|
|||||||
@@ -237,6 +237,7 @@
|
|||||||
"set_availability": "언제 여유가 있는지 설정하세요.",
|
"set_availability": "언제 여유가 있는지 설정하세요.",
|
||||||
"continue_without_calendar": "캘린더 없이 계속하기",
|
"continue_without_calendar": "캘린더 없이 계속하기",
|
||||||
"connect_your_calendar": "캘린더 연결하기",
|
"connect_your_calendar": "캘린더 연결하기",
|
||||||
|
"connect_your_video_app_instructions": "이벤트 타입에서 사용하기 위한 비디오 앱을 연결해주세요.",
|
||||||
"connect_your_calendar_instructions": "바쁜 시간과 새 이벤트 일정을 자동으로 확인할 수 있도록 캘린더 연결",
|
"connect_your_calendar_instructions": "바쁜 시간과 새 이벤트 일정을 자동으로 확인할 수 있도록 캘린더 연결",
|
||||||
"set_up_later": "나중에 하기",
|
"set_up_later": "나중에 하기",
|
||||||
"current_time": "현재 시각",
|
"current_time": "현재 시각",
|
||||||
@@ -465,6 +466,7 @@
|
|||||||
"friday": "금요일",
|
"friday": "금요일",
|
||||||
"saturday": "토요일",
|
"saturday": "토요일",
|
||||||
"sunday": "일요일",
|
"sunday": "일요일",
|
||||||
|
"all_booked_today": "모두 예약됨.",
|
||||||
"slots_load_fail": "사용 가능한 시간 슬롯을 로드할 수 없습니다.",
|
"slots_load_fail": "사용 가능한 시간 슬롯을 로드할 수 없습니다.",
|
||||||
"additional_guests": "게스트 추가",
|
"additional_guests": "게스트 추가",
|
||||||
"your_name": "당신의 이름",
|
"your_name": "당신의 이름",
|
||||||
@@ -1038,6 +1040,7 @@
|
|||||||
"event_cancelled_trigger": "이벤트 취소 시",
|
"event_cancelled_trigger": "이벤트 취소 시",
|
||||||
"new_event_trigger": "새 이벤트 예약 시",
|
"new_event_trigger": "새 이벤트 예약 시",
|
||||||
"email_host_action": "호스트에게 이메일 발송",
|
"email_host_action": "호스트에게 이메일 발송",
|
||||||
|
"email_attendee_action": "참석자들에게 이메일 전송",
|
||||||
"sms_attendee_action": "참석자에게 SMS 발송",
|
"sms_attendee_action": "참석자에게 SMS 발송",
|
||||||
"sms_number_action": "특정 번호로 SMS 발송",
|
"sms_number_action": "특정 번호로 SMS 발송",
|
||||||
"workflows": "워크플로",
|
"workflows": "워크플로",
|
||||||
@@ -1246,6 +1249,7 @@
|
|||||||
"calendars_description": "이벤트 타입이 캘린더와 상호 작용하는 방식 구성",
|
"calendars_description": "이벤트 타입이 캘린더와 상호 작용하는 방식 구성",
|
||||||
"appearance_description": "예약 모양 설정 관리",
|
"appearance_description": "예약 모양 설정 관리",
|
||||||
"conferencing_description": "회의에 즐겨 사용하는 화상 회의 앱 추가",
|
"conferencing_description": "회의에 즐겨 사용하는 화상 회의 앱 추가",
|
||||||
|
"add_conferencing_app": "회의 앱 추가",
|
||||||
"password_description": "계정 비밀번호 설정 관리",
|
"password_description": "계정 비밀번호 설정 관리",
|
||||||
"2fa_description": "계정 비밀번호 설정 관리",
|
"2fa_description": "계정 비밀번호 설정 관리",
|
||||||
"we_just_need_basic_info": "프로필 설정을 위해 몇 가지 기본 정보만 있으면 됩니다.",
|
"we_just_need_basic_info": "프로필 설정을 위해 몇 가지 기본 정보만 있으면 됩니다.",
|
||||||
@@ -1620,6 +1624,7 @@
|
|||||||
"email_user_cta": "초대 보기",
|
"email_user_cta": "초대 보기",
|
||||||
"email_no_user_invite_heading": "{{appName}} 팀에 초대되었습니다",
|
"email_no_user_invite_heading": "{{appName}} 팀에 초대되었습니다",
|
||||||
"email_no_user_invite_subheading": "{{invitedBy}} 님이 귀하를 {{appName}}에서 자신의 팀에 초대했습니다. {{appName}} 앱은 귀하와 귀하의 팀이 이메일을 주고 받지 않고 회의 일정을 잡을 수 있게 해주는 이벤트 정리 스케줄러입니다.",
|
"email_no_user_invite_subheading": "{{invitedBy}} 님이 귀하를 {{appName}}에서 자신의 팀에 초대했습니다. {{appName}} 앱은 귀하와 귀하의 팀이 이메일을 주고 받지 않고 회의 일정을 잡을 수 있게 해주는 이벤트 정리 스케줄러입니다.",
|
||||||
|
"email_user_invite_subheading": "{{invitedBy}}님이 {{appName}}에서 자신의 `{{teamName}}` 팀에 가입하도록 당신을 초대했습니다. {{appName}}은 유저와 팀이 이메일을 주고 받지 않고도 회의 일정을 잡을 수 있게 하는 이벤트 조율 스케줄러입니다.",
|
||||||
"email_no_user_invite_steps_intro": "안내해 드릴 몇 가지 간단한 단계만 거치면 곧 팀과 함께 스트레스 없이 편리하게 일정을 관리할 수 있게 될 것입니다.",
|
"email_no_user_invite_steps_intro": "안내해 드릴 몇 가지 간단한 단계만 거치면 곧 팀과 함께 스트레스 없이 편리하게 일정을 관리할 수 있게 될 것입니다.",
|
||||||
"email_no_user_step_one": "사용자 이름 선택",
|
"email_no_user_step_one": "사용자 이름 선택",
|
||||||
"email_no_user_step_two": "캘린더 계정 연결",
|
"email_no_user_step_two": "캘린더 계정 연결",
|
||||||
@@ -1695,6 +1700,14 @@
|
|||||||
"spot_popular_event_types_description": "어떤 이벤트 타입이 가장 많은 클릭을 받고 예약이 되고 있는지 확인해보세요",
|
"spot_popular_event_types_description": "어떤 이벤트 타입이 가장 많은 클릭을 받고 예약이 되고 있는지 확인해보세요",
|
||||||
"no_responses_yet": "아직 응답 없음",
|
"no_responses_yet": "아직 응답 없음",
|
||||||
"this_will_be_the_placeholder": "이것은 자리 표시자가 됩니다",
|
"this_will_be_the_placeholder": "이것은 자리 표시자가 됩니다",
|
||||||
|
"error_booking_event": "이벤트를 예약하는 도중 오류가 발생했습니다. 페이지를 새로고침한 후 다시 시도해주세요.",
|
||||||
|
"timeslot_missing_title": "시간대가 선택되지 않았습니다",
|
||||||
|
"timeslot_missing_description": "이벤트를 예약 할 시간대를 선택해주세요.",
|
||||||
|
"timeslot_missing_cta": "시간대 선택",
|
||||||
|
"switch_monthly": "월별 보기로 전환",
|
||||||
|
"switch_weekly": "주간 보기로 전환",
|
||||||
|
"switch_multiday": "일간 보기로 전환",
|
||||||
|
"num_locations": "{{num}}개의 위치 옵션",
|
||||||
"this_meeting_has_not_started_yet": "이 회의는 아직 시작되지 않았습니다",
|
"this_meeting_has_not_started_yet": "이 회의는 아직 시작되지 않았습니다",
|
||||||
"this_app_requires_connected_account": "{{appName}}에 연결된 {{dependencyName}} 계정이 필요합니다",
|
"this_app_requires_connected_account": "{{appName}}에 연결된 {{dependencyName}} 계정이 필요합니다",
|
||||||
"connect_app": "{{dependencyName}} 연결",
|
"connect_app": "{{dependencyName}} 연결",
|
||||||
@@ -1724,7 +1737,6 @@
|
|||||||
"locked_apps_description": "회원은 활성 앱을 볼 수 있지만 앱 설정을 편집할 수는 없습니다",
|
"locked_apps_description": "회원은 활성 앱을 볼 수 있지만 앱 설정을 편집할 수는 없습니다",
|
||||||
"locked_webhooks_description": "회원은 활성 웹훅을 볼 수 있지만 웹훅 설정을 편집할 수는 없습니다",
|
"locked_webhooks_description": "회원은 활성 웹훅을 볼 수 있지만 웹훅 설정을 편집할 수는 없습니다",
|
||||||
"locked_workflows_description": "회원은 활성 워크플로를 볼 수 있지만 워크플로 설정을 편집할 수는 없습니다",
|
"locked_workflows_description": "회원은 활성 워크플로를 볼 수 있지만 워크플로 설정을 편집할 수는 없습니다",
|
||||||
"locked_by_admin": "관리자에 의해 잠금 됨",
|
|
||||||
"app_not_connected": "{{appName}} 계정을 연결하지 않으셨습니다.",
|
"app_not_connected": "{{appName}} 계정을 연결하지 않으셨습니다.",
|
||||||
"connect_now": "지금 연결",
|
"connect_now": "지금 연결",
|
||||||
"managed_event_dialog_confirm_button_one": "회원 {{count}}명 바꾸기 및 알림",
|
"managed_event_dialog_confirm_button_one": "회원 {{count}}명 바꾸기 및 알림",
|
||||||
@@ -1782,5 +1794,15 @@
|
|||||||
"seats_and_no_show_fee_error": "현재 좌석을 활성화할 수 없으므로 노쇼 수수료를 부과할 수 없습니다",
|
"seats_and_no_show_fee_error": "현재 좌석을 활성화할 수 없으므로 노쇼 수수료를 부과할 수 없습니다",
|
||||||
"complete_your_booking": "예약을 완료하세요",
|
"complete_your_booking": "예약을 완료하세요",
|
||||||
"complete_your_booking_subject": "예약 완료: {{title}} 날짜 {{date}}",
|
"complete_your_booking_subject": "예약 완료: {{title}} 날짜 {{date}}",
|
||||||
"email_invite_team": "{{email}} 이 초대되었습니다."
|
"currency_string": "{{amount, currency}}",
|
||||||
|
"charge_card_dialog_body": "참석자에게 {{amount, currency}}을 청구하려고 합니다. 계속 진행하시겠습니까?",
|
||||||
|
"charge_attendee": "참석자에게 {{amount, currency}}을 청구합니다.",
|
||||||
|
"payment_app_commission": "결제가 필요합니다 (건당 수수료 {{paymentFeePercentage}}% + {{fee, currency}})",
|
||||||
|
"email_invite_team": "{{email}} 이 초대되었습니다.",
|
||||||
|
"image_size_limit_exceed": "업로드되는 이미지의 크기는 5MB를 초과하면 안 됩니다.",
|
||||||
|
"need_help_embedding": "도움이 필요하신가요? Wix, Squarespace, 또는 WordPress에서 Cal을 임베드하는 가이드를 참조하거나, 자주 묻는 질문을 확인하거나, 고급 임베드 옵션을 살펴보세요.",
|
||||||
|
"disable_attendees_confirmation_emails": "참석자에 대한 기본 확인 이메일 비활성화",
|
||||||
|
"disable_attendees_confirmation_emails_description": "이 이벤트 유형에는 이벤트가 예약되었을 때 참석자들에게 이메일을 보내는 최소한 하나의 워크플로가 활성화되어 있습니다.",
|
||||||
|
"disable_host_confirmation_emails": "호스트에 대한 기본 확인 이메일 비활성화",
|
||||||
|
"disable_host_confirmation_emails_description": "이 이벤트 유형에는 이벤트가 예약될 때 호스트에게 이메일을 보내는 최소한 하나의 워크플로가 활성화되어 있습니다."
|
||||||
}
|
}
|
||||||
@@ -1724,7 +1724,6 @@
|
|||||||
"locked_apps_description": "Leden kunnen de actieve apps zien, maar kunnen geen appinstellingen bewerken",
|
"locked_apps_description": "Leden kunnen de actieve apps zien, maar kunnen geen appinstellingen bewerken",
|
||||||
"locked_webhooks_description": "Leden kunnen de actieve webhooks zien, maar kunnen geen webhookinstellingen bewerken",
|
"locked_webhooks_description": "Leden kunnen de actieve webhooks zien, maar kunnen geen webhookinstellingen bewerken",
|
||||||
"locked_workflows_description": "Leden kunnen de actieve werkstromen zien, maar kunnen geen werkstroominstellingen bewerken",
|
"locked_workflows_description": "Leden kunnen de actieve werkstromen zien, maar kunnen geen werkstroominstellingen bewerken",
|
||||||
"locked_by_admin": "Vergrendeld door beheerder",
|
|
||||||
"app_not_connected": "U heeft geen {{appName}}-account gekoppeld.",
|
"app_not_connected": "U heeft geen {{appName}}-account gekoppeld.",
|
||||||
"connect_now": "Nu koppelen",
|
"connect_now": "Nu koppelen",
|
||||||
"managed_event_dialog_confirm_button_one": "{{count}} lid vervangen en informeren",
|
"managed_event_dialog_confirm_button_one": "{{count}} lid vervangen en informeren",
|
||||||
|
|||||||
@@ -1724,7 +1724,6 @@
|
|||||||
"locked_apps_description": "Członkowie będą mogli zobaczyć aktywne aplikacje, ale nie będą mogli zmieniać ich ustawień.",
|
"locked_apps_description": "Członkowie będą mogli zobaczyć aktywne aplikacje, ale nie będą mogli zmieniać ich ustawień.",
|
||||||
"locked_webhooks_description": "Członkowie będą mogli zobaczyć aktywne webhooki, ale nie będą mogli zmieniać ich ustawień.",
|
"locked_webhooks_description": "Członkowie będą mogli zobaczyć aktywne webhooki, ale nie będą mogli zmieniać ich ustawień.",
|
||||||
"locked_workflows_description": "Członkowie będą mogli zobaczyć aktywne przepływy pracy, ale nie będą mogli zmieniać ich ustawień.",
|
"locked_workflows_description": "Członkowie będą mogli zobaczyć aktywne przepływy pracy, ale nie będą mogli zmieniać ich ustawień.",
|
||||||
"locked_by_admin": "Zablokowane przez administratora",
|
|
||||||
"app_not_connected": "Nie połączono konta aplikacji {{appName}}.",
|
"app_not_connected": "Nie połączono konta aplikacji {{appName}}.",
|
||||||
"connect_now": "Połącz teraz",
|
"connect_now": "Połącz teraz",
|
||||||
"managed_event_dialog_confirm_button_one": "Zastąp i poinformuj {{count}} członka",
|
"managed_event_dialog_confirm_button_one": "Zastąp i poinformuj {{count}} członka",
|
||||||
|
|||||||
@@ -1724,7 +1724,6 @@
|
|||||||
"locked_apps_description": "Os membros poderão ver os aplicativos ativos, mas não poderão editar as configurações",
|
"locked_apps_description": "Os membros poderão ver os aplicativos ativos, mas não poderão editar as configurações",
|
||||||
"locked_webhooks_description": "Os membros poderão ver os webhooks ativos, mas não poderão editar as configurações",
|
"locked_webhooks_description": "Os membros poderão ver os webhooks ativos, mas não poderão editar as configurações",
|
||||||
"locked_workflows_description": "Os membros poderão ver os fluxos de trabalho ativos, mas não poderão editar as configurações",
|
"locked_workflows_description": "Os membros poderão ver os fluxos de trabalho ativos, mas não poderão editar as configurações",
|
||||||
"locked_by_admin": "Bloqueado pelo administrador",
|
|
||||||
"app_not_connected": "Você não conectou uma conta do {{appName}}.",
|
"app_not_connected": "Você não conectou uma conta do {{appName}}.",
|
||||||
"connect_now": "Conectar agora",
|
"connect_now": "Conectar agora",
|
||||||
"managed_event_dialog_confirm_button_one": "Substituir e notificar {{count}} membro",
|
"managed_event_dialog_confirm_button_one": "Substituir e notificar {{count}} membro",
|
||||||
|
|||||||
@@ -1724,7 +1724,6 @@
|
|||||||
"locked_apps_description": "Os membros poderão ver as aplicações ativas, mas não poderão editar quaisquer configurações das aplicações",
|
"locked_apps_description": "Os membros poderão ver as aplicações ativas, mas não poderão editar quaisquer configurações das aplicações",
|
||||||
"locked_webhooks_description": "Os membros poderão ver os webhooks ativos, mas não poderão editar quaisquer configurações dos webhooks",
|
"locked_webhooks_description": "Os membros poderão ver os webhooks ativos, mas não poderão editar quaisquer configurações dos webhooks",
|
||||||
"locked_workflows_description": "Os membros poderão ver os fluxos de trabalho ativos, mas não poderão editar quaisquer configurações dos fluxos de trabalho",
|
"locked_workflows_description": "Os membros poderão ver os fluxos de trabalho ativos, mas não poderão editar quaisquer configurações dos fluxos de trabalho",
|
||||||
"locked_by_admin": "Bloqueado pelo administrador",
|
|
||||||
"app_not_connected": "Não associou uma conta {{appName}}.",
|
"app_not_connected": "Não associou uma conta {{appName}}.",
|
||||||
"connect_now": "Associar agora",
|
"connect_now": "Associar agora",
|
||||||
"managed_event_dialog_confirm_button_one": "Substituir e notificar {{count}} membro",
|
"managed_event_dialog_confirm_button_one": "Substituir e notificar {{count}} membro",
|
||||||
|
|||||||
@@ -1724,7 +1724,6 @@
|
|||||||
"locked_apps_description": "Membrii vor putea vedea aplicațiile active, însă nu le vor putea modifica deloc setările",
|
"locked_apps_description": "Membrii vor putea vedea aplicațiile active, însă nu le vor putea modifica deloc setările",
|
||||||
"locked_webhooks_description": "Membrii vor putea vedea webhook-urile active, însă nu le vor putea modifica deloc setările",
|
"locked_webhooks_description": "Membrii vor putea vedea webhook-urile active, însă nu le vor putea modifica deloc setările",
|
||||||
"locked_workflows_description": "Membrii vor putea vedea fluxurile de lucru active, însă nu le vor putea modifica deloc setările",
|
"locked_workflows_description": "Membrii vor putea vedea fluxurile de lucru active, însă nu le vor putea modifica deloc setările",
|
||||||
"locked_by_admin": "Blocat de administrator",
|
|
||||||
"app_not_connected": "Nu ați conectat un cont {{appName}}.",
|
"app_not_connected": "Nu ați conectat un cont {{appName}}.",
|
||||||
"connect_now": "Conectați-l acum",
|
"connect_now": "Conectați-l acum",
|
||||||
"managed_event_dialog_confirm_button_one": "Înlocuiți și anunțați {{count}} membru",
|
"managed_event_dialog_confirm_button_one": "Înlocuiți și anunțați {{count}} membru",
|
||||||
|
|||||||
@@ -1724,7 +1724,6 @@
|
|||||||
"locked_apps_description": "Участники смогут видеть активные приложения, но не смогут редактировать их настройки",
|
"locked_apps_description": "Участники смогут видеть активные приложения, но не смогут редактировать их настройки",
|
||||||
"locked_webhooks_description": "Участники смогут видеть активные вебхуки, но не смогут редактировать их настройки",
|
"locked_webhooks_description": "Участники смогут видеть активные вебхуки, но не смогут редактировать их настройки",
|
||||||
"locked_workflows_description": "Участники смогут видеть активные рабочие процессы, но не смогут редактировать их настройки",
|
"locked_workflows_description": "Участники смогут видеть активные рабочие процессы, но не смогут редактировать их настройки",
|
||||||
"locked_by_admin": "Заблокировано администратором",
|
|
||||||
"app_not_connected": "Вы не подключили аккаунт {{appName}}.",
|
"app_not_connected": "Вы не подключили аккаунт {{appName}}.",
|
||||||
"connect_now": "Подключить сейчас",
|
"connect_now": "Подключить сейчас",
|
||||||
"managed_event_dialog_confirm_button_one": "Заменить и уведомить {{count}} участника",
|
"managed_event_dialog_confirm_button_one": "Заменить и уведомить {{count}} участника",
|
||||||
|
|||||||
@@ -1724,7 +1724,6 @@
|
|||||||
"locked_apps_description": "Članovi će moći da vide aktivne aplikacije, ali neće moći da uređuju bilo kakva podešavanja aplikacije",
|
"locked_apps_description": "Članovi će moći da vide aktivne aplikacije, ali neće moći da uređuju bilo kakva podešavanja aplikacije",
|
||||||
"locked_webhooks_description": "Članovi će moći da vide aktivne webhook-ove, ali neće moći da uređuju bilo kakva podešavanja webhook-ova",
|
"locked_webhooks_description": "Članovi će moći da vide aktivne webhook-ove, ali neće moći da uređuju bilo kakva podešavanja webhook-ova",
|
||||||
"locked_workflows_description": "Članovi će moći da vide aktivne radne tokove, ali neće moći da uređuju bilo kakva podešavanja radnih tokova",
|
"locked_workflows_description": "Članovi će moći da vide aktivne radne tokove, ali neće moći da uređuju bilo kakva podešavanja radnih tokova",
|
||||||
"locked_by_admin": "Zaključao administrator",
|
|
||||||
"app_not_connected": "Niste povezali {{appName}} nalog.",
|
"app_not_connected": "Niste povezali {{appName}} nalog.",
|
||||||
"connect_now": "Povežite odmah",
|
"connect_now": "Povežite odmah",
|
||||||
"managed_event_dialog_confirm_button_one": "Zameni i obavesti {{count}} člana",
|
"managed_event_dialog_confirm_button_one": "Zameni i obavesti {{count}} člana",
|
||||||
|
|||||||
@@ -1724,7 +1724,6 @@
|
|||||||
"locked_apps_description": "Medlemmarna kommer att kunna se de aktiva apparna, men de kommer inte att kunna ändra appinställningarna",
|
"locked_apps_description": "Medlemmarna kommer att kunna se de aktiva apparna, men de kommer inte att kunna ändra appinställningarna",
|
||||||
"locked_webhooks_description": "Medlemmarna kommer att kunna se aktiva webhooks men inte kunna redigera inställningar för webhooks",
|
"locked_webhooks_description": "Medlemmarna kommer att kunna se aktiva webhooks men inte kunna redigera inställningar för webhooks",
|
||||||
"locked_workflows_description": "Medlemmarna kommer att kunna se de aktiva arbetsflödena, men de kommer inte att kunna ändra några arbetsflödesinställningar",
|
"locked_workflows_description": "Medlemmarna kommer att kunna se de aktiva arbetsflödena, men de kommer inte att kunna ändra några arbetsflödesinställningar",
|
||||||
"locked_by_admin": "Låst av administratör",
|
|
||||||
"app_not_connected": "Du har inte anslutit ett {{appName}}-konto.",
|
"app_not_connected": "Du har inte anslutit ett {{appName}}-konto.",
|
||||||
"connect_now": "Anslut nu",
|
"connect_now": "Anslut nu",
|
||||||
"managed_event_dialog_confirm_button_one": "Ersätt och meddela {{count}} medlem",
|
"managed_event_dialog_confirm_button_one": "Ersätt och meddela {{count}} medlem",
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
{
|
{
|
||||||
|
"identity_provider": "அடையாள வழங்குபவர்",
|
||||||
"trial_days_left": "உங்கள் மேம்படுத்தப்பட்ட பயன்பாட்டு சோதனையில் $t(day, {\"count\": {{days}} }) மீதமுள்ளது",
|
"trial_days_left": "உங்கள் மேம்படுத்தப்பட்ட பயன்பாட்டு சோதனையில் $t(day, {\"count\": {{days}} }) மீதமுள்ளது",
|
||||||
"day_one": "{{count}} நாள்",
|
"day_one": "{{count}} நாள்",
|
||||||
"day_other": "{{count}} நாட்கள்",
|
"day_other": "{{count}} நாட்கள்",
|
||||||
@@ -7,6 +8,7 @@
|
|||||||
"upgrade_now": "மேம்படுத்தவும்",
|
"upgrade_now": "மேம்படுத்தவும்",
|
||||||
"accept_invitation": "அழைப்பை ஏற்கவும்",
|
"accept_invitation": "அழைப்பை ஏற்கவும்",
|
||||||
"calcom_explained": "{{appName}} அனைவருக்கும் திட்டமிடல் உள்கட்டமைப்பை வழங்குகிறது.",
|
"calcom_explained": "{{appName}} அனைவருக்கும் திட்டமிடல் உள்கட்டமைப்பை வழங்குகிறது.",
|
||||||
|
"calcom_explained_new_user": "உங்கள் {{appName}} கணக்கை அமைக்கவும்! உங்கள் திட்டமிடல் சிக்கல்கள் அனைத்தையும் தீர்க்க சில படிகள் மட்டுமே உள்ளன.",
|
||||||
"have_any_questions": "கேள்விகள் உள்ளதா? நாங்கள் உதவ தயாராக இருக்கிறோம்.",
|
"have_any_questions": "கேள்விகள் உள்ளதா? நாங்கள் உதவ தயாராக இருக்கிறோம்.",
|
||||||
"reset_password_subject": "{{appName}}: கடவுச்சொல் மீட்டமைக்கும் வழிமுறைகள்",
|
"reset_password_subject": "{{appName}}: கடவுச்சொல் மீட்டமைக்கும் வழிமுறைகள்",
|
||||||
"event_declined_subject": "{{title}} {{date}} அன்று நிராகரிக்கப்பட்டது",
|
"event_declined_subject": "{{title}} {{date}} அன்று நிராகரிக்கப்பட்டது",
|
||||||
@@ -28,8 +30,58 @@
|
|||||||
"your_event_has_been_scheduled": "உங்கள் நிகழ்வு திட்டமிடப்பட்டது",
|
"your_event_has_been_scheduled": "உங்கள் நிகழ்வு திட்டமிடப்பட்டது",
|
||||||
"your_event_has_been_scheduled_recurring": "உங்கள் தொடர் நிகழ்வு திட்டமிடப்பட்டுள்ளது",
|
"your_event_has_been_scheduled_recurring": "உங்கள் தொடர் நிகழ்வு திட்டமிடப்பட்டுள்ளது",
|
||||||
"accept_our_license": ".env மாறியை மாற்றுவதன் மூலம் எங்கள் உரிமத்தை ஏற்கவும்\n<1>NEXT_PUBLIC_LICENSE_CONSENT</1> to '{{agree}}'.",
|
"accept_our_license": ".env மாறியை மாற்றுவதன் மூலம் எங்கள் உரிமத்தை ஏற்கவும்\n<1>NEXT_PUBLIC_LICENSE_CONSENT</1> to '{{agree}}'.",
|
||||||
|
"error_message": "பிழை செய்தி: '{{errorMessage}}'",
|
||||||
|
"refund_failed_subject": "பணத்தைத் திரும்பப் பெற முடியவில்லை: {{name}} - {{date}} - {{eventType}}",
|
||||||
"refund_failed": "{{date}} அன்று {{userName}} உடன் நடந்த {{eventType}} நிகழ்விற்கான பணத்தைத் திரும்பி அனுப்ப இயலவில்லை.",
|
"refund_failed": "{{date}} அன்று {{userName}} உடன் நடந்த {{eventType}} நிகழ்விற்கான பணத்தைத் திரும்பி அனுப்ப இயலவில்லை.",
|
||||||
"check_with_provider_and_user": "உங்கள் கட்டண வழங்குநர் மற்றும் {{user}} உடன் இனைந்து இதை எவ்வாறு கையாள்வது என்பதைச் சரிபார்க்கவும்.",
|
"check_with_provider_and_user": "உங்கள் கட்டண வழங்குநர் மற்றும் {{user}} உடன் இனைந்து இதை எவ்வாறு கையாள்வது என்பதைச் சரிபார்க்கவும்.",
|
||||||
|
"a_refund_failed": "பணத்தைத் திரும்பப் பெற முடியவில்லை",
|
||||||
|
"awaiting_payment_subject": "பணம் செலுத்த காத்திருக்கிறது: {{date}} அன்று {{title}}",
|
||||||
|
"meeting_awaiting_payment": "உங்கள் மீட்டிங் கட்டணம் செலுத்த காத்திருக்கிறது",
|
||||||
|
"help": "உதவி",
|
||||||
|
"price": "விலை",
|
||||||
|
"paid": "பணம் செலுத்தப்பட்டது",
|
||||||
|
"refunded": "பணம் திருப்பிக் கொடுக்கப்பட்டது",
|
||||||
|
"payment": "பணம் செலுத்துதல்",
|
||||||
|
"missing_card_fields": "கார்டு புலங்கள் இல்லை",
|
||||||
|
"pay_now": "இப்போது பணம் செலுத்த",
|
||||||
|
"codebase_has_to_stay_opensource": "கோட்பேஸ் மாற்றப்பட்டாலும் இல்லாவிட்டாலும் திறந்த மூலமாக இருக்க வேண்டும்",
|
||||||
|
"cannot_repackage_codebase": "நீங்கள் கோட்பேஸை மீண்டும் பேக்கேஜ் செய்யவோ விற்கவோ முடியாது",
|
||||||
|
"acquire_license": "மின்னஞ்சல்மூலம் இந்த விதிமுறைகளை அகற்ற வணிக உரிமத்தைப் பெறவும்",
|
||||||
|
"terms_summary": "விதிமுறைகளின் சுருக்கம்",
|
||||||
|
"open_env": ".env ஐத் திறந்து எங்கள் உரிமத்தை ஏற்கவும்",
|
||||||
|
"env_changed": "நான் என் .env ஐ மாற்றிவிட்டேன்",
|
||||||
|
"accept_license": "உரிமத்தை ஏற்கவும்",
|
||||||
|
"still_waiting_for_approval": "ஒரு நிகழ்வு இன்னும் ஒப்புதலுக்காகக் காத்திருக்கிறது",
|
||||||
|
"event_is_still_waiting": "நிகழ்வு கோரிக்கை இன்னும் காத்திருக்கிறது: {{attendeeName}} - {{date}} - {{eventType}}",
|
||||||
|
"no_more_results": "மேலும் முடிவுகள் இல்லை",
|
||||||
|
"no_results": "முடிவுகள் இல்லை",
|
||||||
|
"load_more_results": "மேலும் முடிவுகளை ஏற்றவும்",
|
||||||
|
"integration_meeting_id": "{{integrationName}} சந்திப்பு ஐடி: {{meetingId}}",
|
||||||
|
"confirmed_event_type_subject": "உறுதிப்படுத்தப்பட்டது: {{date}} அன்று {{name}} உடன் {{eventType}}",
|
||||||
|
"new_event_request": "புதிய நிகழ்வு கோரிக்கை: {{attendeeName}} - {{date}} - {{eventType}}",
|
||||||
|
"confirm_or_reject_request": "கோரிக்கையை உறுதிப்படுத்தவும் அல்லது நிராகரிக்கவும்",
|
||||||
|
"check_bookings_page_to_confirm_or_reject": "முன்பதிவை உறுதிப்படுத்த அல்லது நிராகரிக்க உங்கள் முன்பதிவு பக்கத்தைப் பார்க்கவும்.",
|
||||||
|
"event_awaiting_approval": "உங்கள் ஒப்புதலுக்காக ஒரு நிகழ்வு காத்திருக்கிறது",
|
||||||
|
"event_awaiting_approval_recurring": "தொடர்ச்சியான நிகழ்வு உங்கள் ஒப்புதலுக்காகக் காத்திருக்கிறது",
|
||||||
|
"someone_requested_an_event": "உங்கள் காலெண்டரில் ஒரு நிகழ்வைத் திட்டமிடுமாறு ஒருவர் கோரியுள்ளார்.",
|
||||||
|
"someone_requested_password_reset": "உங்கள் கடவுச்சொல்லை மாற்ற யாரோ ஒரு இணைப்பைக் கோரியுள்ளனர்.",
|
||||||
|
"password_reset_instructions": "நீங்கள் இதைக் கோரவில்லை எனில், இந்த மின்னஞ்சலைப் பாதுகாப்பாகப் புறக்கணிக்கலாம், மேலும் உங்கள் கடவுச்சொல் மாற்றப்படாது.",
|
||||||
|
"event_awaiting_approval_subject": "ஒப்புதலுக்காகக் காத்திருக்கிறது: {{title}} {{date}} இல்",
|
||||||
|
"event_still_awaiting_approval": "உங்கள் ஒப்புதலுக்காக ஒரு நிகழ்வு இன்னும் காத்திருக்கிறது",
|
||||||
|
"booking_submitted_subject": "முன்பதிவு சமர்ப்பிக்கப்பட்டது: {{title}} {{date}} அன்று",
|
||||||
|
"download_your_recording": "உங்கள் பதிவைப் பதிவிறக்கவும்",
|
||||||
|
"your_meeting_has_been_booked": "உங்கள் சந்திப்பு பதிவு செய்யப்பட்டுள்ளது",
|
||||||
|
"event_type_has_been_rescheduled_on_time_date": "உங்கள் {{title}}, {{date}} க்கு மாற்றியமைக்கப்பட்டுள்ளது.",
|
||||||
|
"event_has_been_rescheduled": "புதுப்பிக்கப்பட்டது - உங்கள் நிகழ்வு மீண்டும் திட்டமிடப்பட்டது",
|
||||||
|
"request_reschedule_title_attendee": "உங்கள் முன்பதிவை மாற்றியமைக்க கோரிக்கை",
|
||||||
|
"request_reschedule_subtitle": "{{organizer}} முன்பதிவை ரத்துசெய்துவிட்டு, மற்றொரு நேரத்தைத் தேர்ந்தெடுக்கும்படி கோரியுள்ளார்.",
|
||||||
|
"request_reschedule_title_organizer": "நீங்கள் {{attendee}} ஐ மறுஅட்டவணைக்குக் கோரியுள்ளீர்கள்",
|
||||||
|
"request_reschedule_subtitle_organizer": "நீங்கள் முன்பதிவை ரத்து செய்துவிட்டீர்கள், {{attendee}} உங்களுடன் புதிய முன்பதிவு நேரத்தைத் தேர்ந்தெடுக்க வேண்டும்.",
|
||||||
|
"rescheduled_event_type_subject": "மறுஅட்டவணைக்கான கோரிக்கை அனுப்பப்பட்டது: {{eventType}} உடன் {{name}} உடன் {{date}}",
|
||||||
|
"requested_to_reschedule_subject_attendee": "மறுஅட்டவணை செயல் தேவை: {{eventType}} க்கு {{name}} உடன் புதிய நேரத்தை முன்பதிவு செய்யவும்",
|
||||||
|
"reschedule_reason": "மறு அட்டவணைக்கான காரணம்",
|
||||||
|
"hi_user_name": "வணக்கம் {{name}}",
|
||||||
|
"turn_on": "இயக்கவும்",
|
||||||
"email_no_user_invite_heading": "நீங்கள் {{appName}} குழுவில் சேர அழைக்கப்பட்டுள்ளீர்கள்",
|
"email_no_user_invite_heading": "நீங்கள் {{appName}} குழுவில் சேர அழைக்கப்பட்டுள்ளீர்கள்",
|
||||||
"email_no_user_invite_subheading": "{{invitedBy}} உங்களை {{appName}} குழுவில் சேர அழைத்துள்ளார். {{appName}} என்பது ஈமெயில் இல்லாமலேயே உங்களையும் உங்கள் குழுவையும் கூட்டங்களைத் திட்டமிடுவதற்கு உதவும் திட்டமிடல் பயன்பாடு.",
|
"email_no_user_invite_subheading": "{{invitedBy}} உங்களை {{appName}} குழுவில் சேர அழைத்துள்ளார். {{appName}} என்பது ஈமெயில் இல்லாமலேயே உங்களையும் உங்கள் குழுவையும் கூட்டங்களைத் திட்டமிடுவதற்கு உதவும் திட்டமிடல் பயன்பாடு.",
|
||||||
"email_no_user_invite_steps_intro": "சில சிறிய முயர்ச்சித்தல் மூலம் நாங்கள் உங்களை அழைத்துச் செல்வோம், எந்த நேரத்திலும் உங்கள் குழுவுடன் மன அழுத்தமில்லாத திட்டமிடலை அனுபவிப்பீர்கள்.",
|
"email_no_user_invite_steps_intro": "சில சிறிய முயர்ச்சித்தல் மூலம் நாங்கள் உங்களை அழைத்துச் செல்வோம், எந்த நேரத்திலும் உங்கள் குழுவுடன் மன அழுத்தமில்லாத திட்டமிடலை அனுபவிப்பீர்கள்.",
|
||||||
@@ -114,6 +166,7 @@
|
|||||||
"select_all": "அனைத்தையும் தேர்வுசெய்",
|
"select_all": "அனைத்தையும் தேர்வுசெய்",
|
||||||
"default_conferencing_bulk_title": "தற்போதுள்ள நிகழ்வு வகைகளை மொத்தமாகப் புதுப்பிக்கவும்",
|
"default_conferencing_bulk_title": "தற்போதுள்ள நிகழ்வு வகைகளை மொத்தமாகப் புதுப்பிக்கவும்",
|
||||||
"default_conferencing_bulk_description": "தேர்ந்தெடுக்கப்பட்ட நிகழ்வு வகைகளுக்கான இருப்பிடங்களைப் புதுப்பிக்கவும்",
|
"default_conferencing_bulk_description": "தேர்ந்தெடுக்கப்பட்ட நிகழ்வு வகைகளுக்கான இருப்பிடங்களைப் புதுப்பிக்கவும்",
|
||||||
|
"locked_by_admin": "குழு நிர்வாகியால் பூட்டப்பட்டது",
|
||||||
"looking_for_more_analytics": "மேலும் பகுப்பாய்வுகளைத் தேடுகிறீர்களா?",
|
"looking_for_more_analytics": "மேலும் பகுப்பாய்வுகளைத் தேடுகிறீர்களா?",
|
||||||
"add_filter": "வடிகட்டிச் சேர்க்கவும்",
|
"add_filter": "வடிகட்டிச் சேர்க்கவும்",
|
||||||
"select_user": "பயனரைத் தேர்ந்தெடுக்கவும்",
|
"select_user": "பயனரைத் தேர்ந்தெடுக்கவும்",
|
||||||
|
|||||||
@@ -1724,7 +1724,6 @@
|
|||||||
"locked_apps_description": "Üyeler aktif uygulamaları görebilir, ancak hiçbir uygulama ayarını düzenleyemez",
|
"locked_apps_description": "Üyeler aktif uygulamaları görebilir, ancak hiçbir uygulama ayarını düzenleyemez",
|
||||||
"locked_webhooks_description": "Üyeler aktif web kancalarını görebilir, ancak hiçbir web kancası ayarını düzenleyemez",
|
"locked_webhooks_description": "Üyeler aktif web kancalarını görebilir, ancak hiçbir web kancası ayarını düzenleyemez",
|
||||||
"locked_workflows_description": "Üyeler aktif iş akışlarını görebilir, ancak hiçbir iş akışı ayarını düzenleyemez",
|
"locked_workflows_description": "Üyeler aktif iş akışlarını görebilir, ancak hiçbir iş akışı ayarını düzenleyemez",
|
||||||
"locked_by_admin": "Yönetici tarafından kilitlendi",
|
|
||||||
"app_not_connected": "Bir {{appName}} hesabı bağlamadınız.",
|
"app_not_connected": "Bir {{appName}} hesabı bağlamadınız.",
|
||||||
"connect_now": "Hemen bağlan",
|
"connect_now": "Hemen bağlan",
|
||||||
"managed_event_dialog_confirm_button_one": "{{count}} üyeyi değiştirin ve bilgilendirin",
|
"managed_event_dialog_confirm_button_one": "{{count}} üyeyi değiştirin ve bilgilendirin",
|
||||||
|
|||||||
@@ -1724,7 +1724,6 @@
|
|||||||
"locked_apps_description": "Учасники зможуть бачити активні додатки, але не зможуть редагувати налаштування додатка",
|
"locked_apps_description": "Учасники зможуть бачити активні додатки, але не зможуть редагувати налаштування додатка",
|
||||||
"locked_webhooks_description": "Учасники бачитимуть активні вебгуки, але не зможуть змінювати їх налаштування",
|
"locked_webhooks_description": "Учасники бачитимуть активні вебгуки, але не зможуть змінювати їх налаштування",
|
||||||
"locked_workflows_description": "Учасники бачитимуть активні робочі процеси, але не зможуть змінювати їх налаштування",
|
"locked_workflows_description": "Учасники бачитимуть активні робочі процеси, але не зможуть змінювати їх налаштування",
|
||||||
"locked_by_admin": "Заблоковано адміністратором",
|
|
||||||
"app_not_connected": "Ви не під’єднали обліковий запис {{appName}}.",
|
"app_not_connected": "Ви не під’єднали обліковий запис {{appName}}.",
|
||||||
"connect_now": "Під’єднати",
|
"connect_now": "Під’єднати",
|
||||||
"managed_event_dialog_confirm_button_one": "Замінити й повідомити {{count}} учасника",
|
"managed_event_dialog_confirm_button_one": "Замінити й повідомити {{count}} учасника",
|
||||||
|
|||||||
@@ -1724,7 +1724,6 @@
|
|||||||
"locked_apps_description": "Các thành viên có thể thấy được những ứng dụng đang hoạt động nhưng không thể sửa bất kỳ thiết lập nào của ứng dụng",
|
"locked_apps_description": "Các thành viên có thể thấy được những ứng dụng đang hoạt động nhưng không thể sửa bất kỳ thiết lập nào của ứng dụng",
|
||||||
"locked_webhooks_description": "Các thành viên có thể thấy được những webhook đang hoạt động nhưng không thể sửa bất kỳ thiết lập nào của webhook",
|
"locked_webhooks_description": "Các thành viên có thể thấy được những webhook đang hoạt động nhưng không thể sửa bất kỳ thiết lập nào của webhook",
|
||||||
"locked_workflows_description": "Các thành viên có thể thấy được những tiến độ công việc đang hoạt động nhưng không thể sửa bất kỳ thiết lập nào của tiến độ công việc",
|
"locked_workflows_description": "Các thành viên có thể thấy được những tiến độ công việc đang hoạt động nhưng không thể sửa bất kỳ thiết lập nào của tiến độ công việc",
|
||||||
"locked_by_admin": "Bị khoá bởi quản trị viên",
|
|
||||||
"app_not_connected": "Bạn không có tài khoản {{appName}} đã được kết nối.",
|
"app_not_connected": "Bạn không có tài khoản {{appName}} đã được kết nối.",
|
||||||
"connect_now": "Kết nối ngay",
|
"connect_now": "Kết nối ngay",
|
||||||
"managed_event_dialog_confirm_button_one": "Thay thế & thông báo cho {{count}} thành viên",
|
"managed_event_dialog_confirm_button_one": "Thay thế & thông báo cho {{count}} thành viên",
|
||||||
|
|||||||
@@ -1191,6 +1191,7 @@
|
|||||||
"create_workflow": "创建工作流程",
|
"create_workflow": "创建工作流程",
|
||||||
"do_this": "执行此操作",
|
"do_this": "执行此操作",
|
||||||
"turn_off": "关闭",
|
"turn_off": "关闭",
|
||||||
|
"turn_on": "打开",
|
||||||
"settings_updated_successfully": "设置已成功更新",
|
"settings_updated_successfully": "设置已成功更新",
|
||||||
"error_updating_settings": "更新设置时出错",
|
"error_updating_settings": "更新设置时出错",
|
||||||
"personal_cal_url": "我的个人 {{appName}} URL",
|
"personal_cal_url": "我的个人 {{appName}} URL",
|
||||||
@@ -1726,7 +1727,7 @@
|
|||||||
"locked_apps_description": "成员将能够查看活动的应用,但不能编辑任何应用设置",
|
"locked_apps_description": "成员将能够查看活动的应用,但不能编辑任何应用设置",
|
||||||
"locked_webhooks_description": "成员将能够查看活动的 Webhook,但不能编辑任何 Webhook 设置",
|
"locked_webhooks_description": "成员将能够查看活动的 Webhook,但不能编辑任何 Webhook 设置",
|
||||||
"locked_workflows_description": "成员将能够查看活动的工作流程,但不能编辑任何工作流程设置",
|
"locked_workflows_description": "成员将能够查看活动的工作流程,但不能编辑任何工作流程设置",
|
||||||
"locked_by_admin": "被管理员锁定",
|
"locked_by_admin": "被团队管理员锁定",
|
||||||
"app_not_connected": "您尚未连接 {{appName}} 账户。",
|
"app_not_connected": "您尚未连接 {{appName}} 账户。",
|
||||||
"connect_now": "立即连接",
|
"connect_now": "立即连接",
|
||||||
"managed_event_dialog_confirm_button_one": "替换并通知 {{count}} 个成员",
|
"managed_event_dialog_confirm_button_one": "替换并通知 {{count}} 个成员",
|
||||||
|
|||||||
@@ -1724,7 +1724,6 @@
|
|||||||
"locked_apps_description": "成員將能查看啟用的應用程式,但無法編輯任何應用程式設定",
|
"locked_apps_description": "成員將能查看啟用的應用程式,但無法編輯任何應用程式設定",
|
||||||
"locked_webhooks_description": "成員將能查看啟用的 Webhook,但無法編輯任何 Webhook 設定",
|
"locked_webhooks_description": "成員將能查看啟用的 Webhook,但無法編輯任何 Webhook 設定",
|
||||||
"locked_workflows_description": "成員將能查看啟用的工作流程,但無法編輯任何工作流程設定",
|
"locked_workflows_description": "成員將能查看啟用的工作流程,但無法編輯任何工作流程設定",
|
||||||
"locked_by_admin": "已由管理員鎖定",
|
|
||||||
"app_not_connected": "您尚未連結 {{appName}} 帳號。",
|
"app_not_connected": "您尚未連結 {{appName}} 帳號。",
|
||||||
"connect_now": "立即連結",
|
"connect_now": "立即連結",
|
||||||
"managed_event_dialog_confirm_button_one": "取代並通知 {{count}} 位成員",
|
"managed_event_dialog_confirm_button_one": "取代並通知 {{count}} 位成員",
|
||||||
|
|||||||
@@ -151,10 +151,6 @@ const TestData = {
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
const ctx = {
|
|
||||||
prisma: prismaMock,
|
|
||||||
};
|
|
||||||
|
|
||||||
type App = {
|
type App = {
|
||||||
slug: string;
|
slug: string;
|
||||||
dirName: string;
|
dirName: string;
|
||||||
@@ -186,6 +182,7 @@ type InputEventType = {
|
|||||||
id: number;
|
id: number;
|
||||||
title?: string;
|
title?: string;
|
||||||
length?: number;
|
length?: number;
|
||||||
|
offsetStart?: number;
|
||||||
slotInterval?: number;
|
slotInterval?: number;
|
||||||
minimumBookingNotice?: number;
|
minimumBookingNotice?: number;
|
||||||
users?: { id: number }[];
|
users?: { id: number }[];
|
||||||
@@ -711,6 +708,72 @@ describe("getSchedule", () => {
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test("Start times are offset (offsetStart)", async () => {
|
||||||
|
const { dateString: plus1DateString } = getDate({ dateIncrement: 1 });
|
||||||
|
const { dateString: plus2DateString } = getDate({ dateIncrement: 2 });
|
||||||
|
|
||||||
|
const scenarioData = {
|
||||||
|
eventTypes: [
|
||||||
|
{
|
||||||
|
id: 1,
|
||||||
|
length: 25,
|
||||||
|
offsetStart: 5,
|
||||||
|
users: [
|
||||||
|
{
|
||||||
|
id: 101,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
users: [
|
||||||
|
{
|
||||||
|
...TestData.users.example,
|
||||||
|
id: 101,
|
||||||
|
schedules: [TestData.schedules.IstWorkHours],
|
||||||
|
credentials: [getGoogleCalendarCredential()],
|
||||||
|
selectedCalendars: [TestData.selectedCalendars.google],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
hosts: [],
|
||||||
|
apps: [TestData.apps.googleCalendar],
|
||||||
|
};
|
||||||
|
|
||||||
|
createBookingScenario(scenarioData);
|
||||||
|
|
||||||
|
const schedule = await getSchedule({
|
||||||
|
eventTypeId: 1,
|
||||||
|
eventTypeSlug: "",
|
||||||
|
startTime: `${plus1DateString}T18:30:00.000Z`,
|
||||||
|
endTime: `${plus2DateString}T18:29:59.999Z`,
|
||||||
|
timeZone: Timezones["+5:30"],
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(schedule).toHaveTimeSlots(
|
||||||
|
[
|
||||||
|
`04:05:00.000Z`,
|
||||||
|
`04:35:00.000Z`,
|
||||||
|
`05:05:00.000Z`,
|
||||||
|
`05:35:00.000Z`,
|
||||||
|
`06:05:00.000Z`,
|
||||||
|
`06:35:00.000Z`,
|
||||||
|
`07:05:00.000Z`,
|
||||||
|
`07:35:00.000Z`,
|
||||||
|
`08:05:00.000Z`,
|
||||||
|
`08:35:00.000Z`,
|
||||||
|
`09:05:00.000Z`,
|
||||||
|
`09:35:00.000Z`,
|
||||||
|
`10:05:00.000Z`,
|
||||||
|
`10:35:00.000Z`,
|
||||||
|
`11:05:00.000Z`,
|
||||||
|
`11:35:00.000Z`,
|
||||||
|
`12:05:00.000Z`,
|
||||||
|
],
|
||||||
|
{
|
||||||
|
dateString: plus2DateString,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
test("Check for Date overrides", async () => {
|
test("Check for Date overrides", async () => {
|
||||||
const { dateString: plus1DateString } = getDate({ dateIncrement: 1 });
|
const { dateString: plus1DateString } = getDate({ dateIncrement: 1 });
|
||||||
const { dateString: plus2DateString } = getDate({ dateIncrement: 2 });
|
const { dateString: plus2DateString } = getDate({ dateIncrement: 2 });
|
||||||
@@ -1132,6 +1195,7 @@ function addEventTypes(eventTypes: InputEventType[], usersStore: InputUser[]) {
|
|||||||
seatsPerTimeSlot: null,
|
seatsPerTimeSlot: null,
|
||||||
metadata: {},
|
metadata: {},
|
||||||
minimumBookingNotice: 0,
|
minimumBookingNotice: 0,
|
||||||
|
offsetStart: 0,
|
||||||
};
|
};
|
||||||
const foundEvents: Record<number, boolean> = {};
|
const foundEvents: Record<number, boolean> = {};
|
||||||
const eventTypesWithUsers = eventTypes.map((eventType) => {
|
const eventTypesWithUsers = eventTypes.map((eventType) => {
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import type { Prisma } from "@prisma/client";
|
|||||||
|
|
||||||
import updateChildrenEventTypes from "@calcom/features/ee/managed-event-types/lib/handleChildrenEventTypes";
|
import updateChildrenEventTypes from "@calcom/features/ee/managed-event-types/lib/handleChildrenEventTypes";
|
||||||
import { buildEventType } from "@calcom/lib/test/builder";
|
import { buildEventType } from "@calcom/lib/test/builder";
|
||||||
import type { CompleteEventType } from "@calcom/prisma/zod";
|
import type { CompleteEventType, CompleteWorkflowsOnEventTypes } from "@calcom/prisma/zod";
|
||||||
|
|
||||||
import { prismaMock } from "../../../../tests/config/singleton";
|
import { prismaMock } from "../../../../tests/config/singleton";
|
||||||
|
|
||||||
@@ -291,4 +291,85 @@ describe("handleChildrenEventTypes", () => {
|
|||||||
expect(result.deletedExistentEventTypes).toEqual([123]);
|
expect(result.deletedExistentEventTypes).toEqual([123]);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe("Workflows", () => {
|
||||||
|
it("Links workflows to new and existing assigned members", async () => {
|
||||||
|
const { schedulingType, id, teamId, locations, timeZone, parentId, userId, ...evType } =
|
||||||
|
mockFindFirstEventType({
|
||||||
|
metadata: { managedEventConfig: {} },
|
||||||
|
locations: [],
|
||||||
|
workflows: [
|
||||||
|
{
|
||||||
|
workflowId: 11,
|
||||||
|
} as CompleteWorkflowsOnEventTypes,
|
||||||
|
],
|
||||||
|
});
|
||||||
|
prismaMock.$transaction.mockResolvedValue([{ id: 2 }]);
|
||||||
|
await updateChildrenEventTypes({
|
||||||
|
eventTypeId: 1,
|
||||||
|
oldEventType: { children: [{ userId: 4 }], team: { name: "" } },
|
||||||
|
children: [
|
||||||
|
{ hidden: false, owner: { id: 4, name: "", email: "", eventTypeSlugs: [] } },
|
||||||
|
{ hidden: false, owner: { id: 5, name: "", email: "", eventTypeSlugs: [] } },
|
||||||
|
],
|
||||||
|
updatedEventType: { schedulingType: "MANAGED", slug: "something" },
|
||||||
|
currentUserId: 1,
|
||||||
|
hashedLink: undefined,
|
||||||
|
connectedLink: null,
|
||||||
|
prisma: prismaMock,
|
||||||
|
});
|
||||||
|
expect(prismaMock.eventType.create).toHaveBeenCalledWith({
|
||||||
|
data: {
|
||||||
|
...evType,
|
||||||
|
bookingLimits: undefined,
|
||||||
|
durationLimits: undefined,
|
||||||
|
recurringEvent: undefined,
|
||||||
|
hashedLink: undefined,
|
||||||
|
locations: [],
|
||||||
|
parentId: 1,
|
||||||
|
userId: 5,
|
||||||
|
users: {
|
||||||
|
connect: [
|
||||||
|
{
|
||||||
|
id: 5,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
workflows: {
|
||||||
|
create: [{ workflowId: 11 }],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
expect(prismaMock.eventType.update).toHaveBeenCalledWith({
|
||||||
|
data: {
|
||||||
|
...evType,
|
||||||
|
bookingLimits: undefined,
|
||||||
|
durationLimits: undefined,
|
||||||
|
recurringEvent: undefined,
|
||||||
|
hashedLink: undefined,
|
||||||
|
workflows: undefined,
|
||||||
|
scheduleId: undefined,
|
||||||
|
},
|
||||||
|
where: {
|
||||||
|
userId_parentId: {
|
||||||
|
userId: 4,
|
||||||
|
parentId: 1,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
expect(prismaMock.workflowsOnEventTypes.upsert).toHaveBeenCalledWith({
|
||||||
|
create: {
|
||||||
|
eventTypeId: 2,
|
||||||
|
workflowId: 11,
|
||||||
|
},
|
||||||
|
update: {},
|
||||||
|
where: {
|
||||||
|
workflowId_eventTypeId: {
|
||||||
|
eventTypeId: 2,
|
||||||
|
workflowId: 11,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
@@ -24,6 +24,7 @@ describe("Tests the slot logic", () => {
|
|||||||
},
|
},
|
||||||
],
|
],
|
||||||
eventLength: 60,
|
eventLength: 60,
|
||||||
|
offsetStart: 0,
|
||||||
organizerTimeZone: "America/Toronto",
|
organizerTimeZone: "America/Toronto",
|
||||||
})
|
})
|
||||||
).toHaveLength(24);
|
).toHaveLength(24);
|
||||||
@@ -46,6 +47,7 @@ describe("Tests the slot logic", () => {
|
|||||||
},
|
},
|
||||||
],
|
],
|
||||||
eventLength: 60,
|
eventLength: 60,
|
||||||
|
offsetStart: 0,
|
||||||
organizerTimeZone: "America/Toronto",
|
organizerTimeZone: "America/Toronto",
|
||||||
})
|
})
|
||||||
).toHaveLength(12);
|
).toHaveLength(12);
|
||||||
@@ -66,6 +68,7 @@ describe("Tests the slot logic", () => {
|
|||||||
},
|
},
|
||||||
],
|
],
|
||||||
eventLength: 60,
|
eventLength: 60,
|
||||||
|
offsetStart: 0,
|
||||||
organizerTimeZone: "America/Toronto",
|
organizerTimeZone: "America/Toronto",
|
||||||
})
|
})
|
||||||
).toHaveLength(0);
|
).toHaveLength(0);
|
||||||
@@ -87,6 +90,7 @@ describe("Tests the slot logic", () => {
|
|||||||
minimumBookingNotice: 0,
|
minimumBookingNotice: 0,
|
||||||
workingHours,
|
workingHours,
|
||||||
eventLength: 60,
|
eventLength: 60,
|
||||||
|
offsetStart: 0,
|
||||||
organizerTimeZone: "America/Toronto",
|
organizerTimeZone: "America/Toronto",
|
||||||
})
|
})
|
||||||
).toHaveLength(0);
|
).toHaveLength(0);
|
||||||
@@ -108,6 +112,7 @@ describe("Tests the slot logic", () => {
|
|||||||
},
|
},
|
||||||
],
|
],
|
||||||
eventLength: 60,
|
eventLength: 60,
|
||||||
|
offsetStart: 0,
|
||||||
organizerTimeZone: "America/Toronto",
|
organizerTimeZone: "America/Toronto",
|
||||||
})
|
})
|
||||||
).toHaveLength(11);
|
).toHaveLength(11);
|
||||||
@@ -129,8 +134,30 @@ describe("Tests the slot logic", () => {
|
|||||||
},
|
},
|
||||||
],
|
],
|
||||||
eventLength: 20,
|
eventLength: 20,
|
||||||
|
offsetStart: 0,
|
||||||
organizerTimeZone: "America/Toronto",
|
organizerTimeZone: "America/Toronto",
|
||||||
})
|
})
|
||||||
).toHaveLength(71);
|
).toHaveLength(71);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("can fit 48 25 minute slots with a 5 minute offset for an empty day", async () => {
|
||||||
|
expect(
|
||||||
|
getSlots({
|
||||||
|
inviteeDate: dayjs.utc().add(1, "day"),
|
||||||
|
frequency: 25,
|
||||||
|
minimumBookingNotice: 0,
|
||||||
|
workingHours: [
|
||||||
|
{
|
||||||
|
userId: 1,
|
||||||
|
days: Array.from(Array(7).keys()),
|
||||||
|
startTime: MINUTES_DAY_START,
|
||||||
|
endTime: MINUTES_DAY_END,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
eventLength: 25,
|
||||||
|
offsetStart: 5,
|
||||||
|
organizerTimeZone: "America/Toronto",
|
||||||
|
})
|
||||||
|
).toHaveLength(48);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
+7
-2
@@ -23,8 +23,12 @@ for module in "$@"; do
|
|||||||
git submodule add --force $project "apps/$module"
|
git submodule add --force $project "apps/$module"
|
||||||
# Set the default branch to main
|
# Set the default branch to main
|
||||||
git config -f .gitmodules --add "submodule.apps/$module.branch" main
|
git config -f .gitmodules --add "submodule.apps/$module.branch" main
|
||||||
# Adding the subdmoule ignores the `.gitignore` so a reset is needed
|
|
||||||
git reset
|
# Update to the latest from main in that submodule
|
||||||
|
cd apps/$module && git pull origin main && cd ../..
|
||||||
|
|
||||||
|
# We forcefully added the subdmoule which was in .gitignore, so unstage it.
|
||||||
|
git restore --staged apps/$module
|
||||||
else
|
else
|
||||||
# If the module is the API, display a link to request access
|
# If the module is the API, display a link to request access
|
||||||
if [ "$module" = "api" ]; then
|
if [ "$module" = "api" ]; then
|
||||||
@@ -35,3 +39,4 @@ for module in "$@"; do
|
|||||||
fi
|
fi
|
||||||
fi
|
fi
|
||||||
done
|
done
|
||||||
|
git restore --staged .gitmodules
|
||||||
+3
-1
@@ -17,6 +17,7 @@
|
|||||||
"app-store": "yarn app-store-cli cli",
|
"app-store": "yarn app-store-cli cli",
|
||||||
"create-app": "yarn app-store create",
|
"create-app": "yarn app-store create",
|
||||||
"edit-app": "yarn app-store edit",
|
"edit-app": "yarn app-store edit",
|
||||||
|
"publish-embed": "yarn workspaces foreach --from=\"@calcom/embed*\" npm publish --access=public",
|
||||||
"delete-app": "yarn app-store delete",
|
"delete-app": "yarn app-store delete",
|
||||||
"create-app-template": "yarn app-store create-template",
|
"create-app-template": "yarn app-store create-template",
|
||||||
"edit-app-template": "yarn app-store edit-template",
|
"edit-app-template": "yarn app-store edit-template",
|
||||||
@@ -74,6 +75,7 @@
|
|||||||
"web": "yarn workspace @calcom/web"
|
"web": "yarn workspace @calcom/web"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
"@changesets/cli": "^2.26.1",
|
||||||
"@deploysentinel/playwright": "^0.3.3",
|
"@deploysentinel/playwright": "^0.3.3",
|
||||||
"@playwright/test": "^1.31.2",
|
"@playwright/test": "^1.31.2",
|
||||||
"@snaplet/copycat": "^0.3.0",
|
"@snaplet/copycat": "^0.3.0",
|
||||||
@@ -95,7 +97,7 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"city-timezones": "^1.2.1",
|
"city-timezones": "^1.2.1",
|
||||||
"eslint": "^8.34.0",
|
"eslint": "^8.34.0",
|
||||||
"lucide-react": "^0.130.0",
|
"lucide-react": "^0.171.0",
|
||||||
"turbo": "^1.8.3"
|
"turbo": "^1.8.3"
|
||||||
},
|
},
|
||||||
"resolutions": {
|
"resolutions": {
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@calcom/app-store-cli",
|
"name": "@calcom/app-store-cli",
|
||||||
|
"private": true,
|
||||||
"sideEffects": false,
|
"sideEffects": false,
|
||||||
"version": "0.0.0",
|
"version": "0.0.0",
|
||||||
"bin": "dist/cli.js",
|
"bin": "dist/cli.js",
|
||||||
|
|||||||
@@ -4,9 +4,9 @@ import SelectInput from "ink-select-input";
|
|||||||
import TextInput from "ink-text-input";
|
import TextInput from "ink-text-input";
|
||||||
import React, { useEffect, useState } from "react";
|
import React, { useEffect, useState } from "react";
|
||||||
|
|
||||||
import { AppMeta } from "@calcom/types/App";
|
import type { AppMeta } from "@calcom/types/App";
|
||||||
|
|
||||||
import { getSlugFromAppName, BaseAppFork, Seed, generateAppFiles, getAppDirPath } from "../core";
|
import { getSlugFromAppName, BaseAppFork, generateAppFiles, getAppDirPath } from "../core";
|
||||||
import { getApp } from "../utils/getApp";
|
import { getApp } from "../utils/getApp";
|
||||||
import Templates from "../utils/templates";
|
import Templates from "../utils/templates";
|
||||||
import Label from "./Label";
|
import Label from "./Label";
|
||||||
@@ -148,8 +148,6 @@ export const AppForm = ({
|
|||||||
oldSlug: givenSlug,
|
oldSlug: givenSlug,
|
||||||
});
|
});
|
||||||
|
|
||||||
await Seed.update({ slug, category: category, oldSlug: givenSlug, isTemplate });
|
|
||||||
|
|
||||||
await generateAppFiles();
|
await generateAppFiles();
|
||||||
|
|
||||||
// FIXME: Even after CLI showing this message, it is stuck doing work before exiting
|
// FIXME: Even after CLI showing this message, it is stuck doing work before exiting
|
||||||
@@ -242,6 +240,10 @@ export const AppForm = ({
|
|||||||
<Text color="green">Publisher Email: </Text>
|
<Text color="green">Publisher Email: </Text>
|
||||||
<Text>{email}</Text>
|
<Text>{email}</Text>
|
||||||
</Box>
|
</Box>
|
||||||
|
<Text bold>
|
||||||
|
Next Step: Enable the app from http://localhost:3000/settings/admin/apps as admin user (Email:
|
||||||
|
admin@example.com, Pass: ADMINadmin2022!)
|
||||||
|
</Text>
|
||||||
</Box>
|
</Box>
|
||||||
</Box>
|
</Box>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import React, { useEffect, useState } from "react";
|
|||||||
|
|
||||||
import { ImportantText } from "../components/ImportantText";
|
import { ImportantText } from "../components/ImportantText";
|
||||||
import { Message } from "../components/Message";
|
import { Message } from "../components/Message";
|
||||||
import { BaseAppFork, Seed, generateAppFiles } from "../core";
|
import { BaseAppFork, generateAppFiles } from "../core";
|
||||||
import { getApp } from "../utils/getApp";
|
import { getApp } from "../utils/getApp";
|
||||||
|
|
||||||
export default function DeleteForm({ slug, action }: { slug: string; action: "delete" | "delete-template" }) {
|
export default function DeleteForm({ slug, action }: { slug: string; action: "delete" | "delete-template" }) {
|
||||||
@@ -28,7 +28,6 @@ export default function DeleteForm({ slug, action }: { slug: string; action: "de
|
|||||||
if (state === "DELETION_CONFIRMATION_SUCCESSFUL") {
|
if (state === "DELETION_CONFIRMATION_SUCCESSFUL") {
|
||||||
(async () => {
|
(async () => {
|
||||||
await BaseAppFork.delete({ slug, isTemplate });
|
await BaseAppFork.delete({ slug, isTemplate });
|
||||||
Seed.revert({ slug });
|
|
||||||
await generateAppFiles();
|
await generateAppFiles();
|
||||||
// successMsg({ text: `App with slug ${slug} has been deleted`, done: true });
|
// successMsg({ text: `App with slug ${slug} has been deleted`, done: true });
|
||||||
setState("DELETION_COMPLETED");
|
setState("DELETION_COMPLETED");
|
||||||
|
|||||||
@@ -1,8 +1,6 @@
|
|||||||
import fs from "fs";
|
import fs from "fs";
|
||||||
import path from "path";
|
import path from "path";
|
||||||
|
|
||||||
import type seedAppStoreConfig from "@calcom/prisma/seed-app-store.config.json";
|
|
||||||
|
|
||||||
import { APP_STORE_PATH, TEMPLATES_PATH } from "./constants";
|
import { APP_STORE_PATH, TEMPLATES_PATH } from "./constants";
|
||||||
import execSync from "./utils/execSync";
|
import execSync from "./utils/execSync";
|
||||||
|
|
||||||
@@ -27,10 +25,6 @@ export function getAppDirPath(slug: string, isTemplate: boolean) {
|
|||||||
return path.join(TEMPLATES_PATH, `${slug}`);
|
return path.join(TEMPLATES_PATH, `${slug}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
function absolutePath(appRelativePath: string) {
|
|
||||||
return path.join(APP_STORE_PATH, appRelativePath);
|
|
||||||
}
|
|
||||||
|
|
||||||
const updatePackageJson = ({
|
const updatePackageJson = ({
|
||||||
slug,
|
slug,
|
||||||
appDescription,
|
appDescription,
|
||||||
@@ -138,58 +132,6 @@ export const BaseAppFork = {
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
export const Seed = {
|
|
||||||
seedConfigPath: absolutePath("../prisma/seed-app-store.config.json"),
|
|
||||||
update: async function ({
|
|
||||||
slug,
|
|
||||||
category,
|
|
||||||
oldSlug,
|
|
||||||
isTemplate,
|
|
||||||
}: {
|
|
||||||
slug: string;
|
|
||||||
category: string;
|
|
||||||
oldSlug: string;
|
|
||||||
isTemplate: boolean;
|
|
||||||
}) {
|
|
||||||
let configContent = "[]";
|
|
||||||
try {
|
|
||||||
if (fs.statSync(this.seedConfigPath)) {
|
|
||||||
configContent = fs.readFileSync(this.seedConfigPath).toString();
|
|
||||||
}
|
|
||||||
} catch (e) {}
|
|
||||||
|
|
||||||
let seedConfig: typeof seedAppStoreConfig = JSON.parse(configContent);
|
|
||||||
seedConfig = seedConfig.filter((app) => app.slug !== oldSlug);
|
|
||||||
|
|
||||||
if (!seedConfig.find((app) => app.slug === slug)) {
|
|
||||||
seedConfig.push({
|
|
||||||
dirName: slug,
|
|
||||||
categories: [category],
|
|
||||||
slug: slug,
|
|
||||||
type: `${slug}_${category}`,
|
|
||||||
isTemplate: isTemplate,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add the message as a property to first item so that it stays always at the top
|
|
||||||
seedConfig[0]["/*"] =
|
|
||||||
"This file is auto-generated and updated by `yarn app-store create/edit`. Don't edit it manually";
|
|
||||||
|
|
||||||
// Add the message as a property to first item so that it stays always at the top
|
|
||||||
seedConfig[0]["/*"] =
|
|
||||||
"This file is auto-generated and updated by `yarn app-store create/edit`. Don't edit it manually";
|
|
||||||
|
|
||||||
fs.writeFileSync(this.seedConfigPath, JSON.stringify(seedConfig, null, 2));
|
|
||||||
await execSync(`cd ${workspaceDir}/packages/prisma && yarn seed-app-store seed-templates`);
|
|
||||||
},
|
|
||||||
revert: async function ({ slug }: { slug: string }) {
|
|
||||||
let seedConfig: typeof seedAppStoreConfig = JSON.parse(fs.readFileSync(this.seedConfigPath).toString());
|
|
||||||
seedConfig = seedConfig.filter((app) => app.slug !== slug);
|
|
||||||
fs.writeFileSync(this.seedConfigPath, JSON.stringify(seedConfig, null, 2));
|
|
||||||
await execSync(`yarn workspace @calcom/prisma delete-app ${slug}`);
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
export const generateAppFiles = async () => {
|
export const generateAppFiles = async () => {
|
||||||
await execSync(`yarn ts-node --transpile-only src/build.ts`);
|
await execSync(`yarn ts-node --transpile-only src/build.ts`);
|
||||||
};
|
};
|
||||||
@@ -5,8 +5,22 @@ import { userMetadata } from "@calcom/prisma/zod-utils";
|
|||||||
import type { AppFrontendPayload as App } from "@calcom/types/App";
|
import type { AppFrontendPayload as App } from "@calcom/types/App";
|
||||||
import type { CredentialFrontendPayload as Credential } from "@calcom/types/Credential";
|
import type { CredentialFrontendPayload as Credential } from "@calcom/types/Credential";
|
||||||
|
|
||||||
export async function getAppWithMetadata(app: { dirName: string }) {
|
/**
|
||||||
const appMetadata: App | null = appStoreMetadata[app.dirName as keyof typeof appStoreMetadata] as App;
|
* Get App metdata either using dirName or slug
|
||||||
|
*/
|
||||||
|
export async function getAppWithMetadata(app: { dirName: string } | { slug: string }) {
|
||||||
|
let appMetadata: App | null;
|
||||||
|
|
||||||
|
if ("dirName" in app) {
|
||||||
|
appMetadata = appStoreMetadata[app.dirName as keyof typeof appStoreMetadata] as App;
|
||||||
|
} else {
|
||||||
|
const foundEntry = Object.entries(appStoreMetadata).find(([, meta]) => {
|
||||||
|
return meta.slug === app.slug;
|
||||||
|
});
|
||||||
|
if (!foundEntry) return null;
|
||||||
|
appMetadata = foundEntry[1] as App;
|
||||||
|
}
|
||||||
|
|
||||||
if (!appMetadata) return null;
|
if (!appMetadata) return null;
|
||||||
// Let's not leak api keys to the front end
|
// Let's not leak api keys to the front end
|
||||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||||
|
|||||||
@@ -47,6 +47,7 @@ export default function OmniInstallAppButton({
|
|||||||
<InstallAppButton
|
<InstallAppButton
|
||||||
type={app.type}
|
type={app.type}
|
||||||
isProOnly={app.isProOnly}
|
isProOnly={app.isProOnly}
|
||||||
|
teamsPlanRequired={app.teamsPlanRequired}
|
||||||
wrapperClassName={classNames("[@media(max-width:260px)]:w-full", className)}
|
wrapperClassName={classNames("[@media(max-width:260px)]:w-full", className)}
|
||||||
render={({ useDefaultComponent, ...props }) => {
|
render={({ useDefaultComponent, ...props }) => {
|
||||||
if (useDefaultComponent) {
|
if (useDefaultComponent) {
|
||||||
|
|||||||
@@ -12,7 +12,14 @@ export const getCalendar = async (credential: CredentialPayload | null): Promise
|
|||||||
if (calendarType?.endsWith("_other_calendar")) {
|
if (calendarType?.endsWith("_other_calendar")) {
|
||||||
calendarType = calendarType.split("_other_calendar")[0];
|
calendarType = calendarType.split("_other_calendar")[0];
|
||||||
}
|
}
|
||||||
const calendarApp = await appStore[calendarType.split("_").join("") as keyof typeof appStore]();
|
const calendarAppImportFn = appStore[calendarType.split("_").join("") as keyof typeof appStore];
|
||||||
|
|
||||||
|
if (!calendarAppImportFn) {
|
||||||
|
log.warn(`calendar of type ${calendarType} is not implemented`);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const calendarApp = await calendarAppImportFn();
|
||||||
if (!(calendarApp && "lib" in calendarApp && "CalendarService" in calendarApp.lib)) {
|
if (!(calendarApp && "lib" in calendarApp && "CalendarService" in calendarApp.lib)) {
|
||||||
log.warn(`calendar of type ${calendarType} is not implemented`);
|
log.warn(`calendar of type ${calendarType} is not implemented`);
|
||||||
return null;
|
return null;
|
||||||
|
|||||||
Loaded 100 of 301 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user