diff --git a/.changeset/README.md b/.changeset/README.md new file mode 100644 index 0000000000..e5b6d8d6a6 --- /dev/null +++ b/.changeset/README.md @@ -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) diff --git a/.changeset/config.json b/.changeset/config.json new file mode 100644 index 0000000000..cee6df899d --- /dev/null +++ b/.changeset/config.json @@ -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": [] +} diff --git a/.gitpod.yml b/.gitpod.yml new file mode 100644 index 0000000000..027cfc2798 --- /dev/null +++ b/.gitpod.yml @@ -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 \ No newline at end of file diff --git a/.yarn/plugins/@yarnpkg/plugin-workspace-tools.cjs b/.yarn/plugins/@yarnpkg/plugin-workspace-tools.cjs new file mode 100644 index 0000000000..4e89c7c352 --- /dev/null +++ b/.yarn/plugins/@yarnpkg/plugin-workspace-tools.cjs @@ -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{"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;e1&&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;nr?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.isPadded)return e;let n=Math.abs(r.maxLen-String(e).length),s=t.relaxZeros!==!1;switch(n){case 0:return"";case 1:return s?"0?":"0";case 2:return s?"0{0,2}":"00";default:return s?`0{0,${n}}`:`0{${n}}`}}le.cache={};le.clearCache=()=>le.cache={};pt.exports=le});var Ue=q((es,Et)=>{"use strict";var Or=W("util"),At=ht(),dt=e=>e!==null&&typeof e=="object"&&!Array.isArray(e),Nr=e=>r=>e===!0?Number(r):String(r),Me=e=>typeof e=="number"||typeof e=="string"&&e!=="",Ae=e=>Number.isInteger(+e),De=e=>{let r=`${e}`,t=-1;if(r[0]==="-"&&(r=r.slice(1)),r==="0")return!1;for(;r[++t]==="0";);return t>0},Ir=(e,r,t)=>typeof e=="string"||typeof r=="string"?!0:t.stringify===!0,Br=(e,r,t)=>{if(r>0){let n=e[0]==="-"?"-":"";n&&(e=e.slice(1)),e=n+e.padStart(n?r-1:r,"0")}return t===!1?String(e):e},gt=(e,r)=>{let t=e[0]==="-"?"-":"";for(t&&(e=e.slice(1),r--);e.length{e.negatives.sort((a,c)=>ac?1:0),e.positives.sort((a,c)=>ac?1:0);let t=r.capture?"":"?:",n="",s="",i;return e.positives.length&&(n=e.positives.join("|")),e.negatives.length&&(s=`-(${t}${e.negatives.join("|")})`),n&&s?i=`${n}|${s}`:i=n||s,r.wrap?`(${t}${i})`:i},mt=(e,r,t,n)=>{if(t)return At(e,r,{wrap:!1,...n});let s=String.fromCharCode(e);if(e===r)return s;let i=String.fromCharCode(r);return`[${s}-${i}]`},Rt=(e,r,t)=>{if(Array.isArray(e)){let n=t.wrap===!0,s=t.capture?"":"?:";return n?`(${s}${e.join("|")})`:e.join("|")}return At(e,r,t)},yt=(...e)=>new RangeError("Invalid range arguments: "+Or.inspect(...e)),_t=(e,r,t)=>{if(t.strictRanges===!0)throw yt([e,r]);return[]},Mr=(e,r)=>{if(r.strictRanges===!0)throw new TypeError(`Expected step "${e}" to be a number`);return[]},Dr=(e,r,t=1,n={})=>{let s=Number(e),i=Number(r);if(!Number.isInteger(s)||!Number.isInteger(i)){if(n.strictRanges===!0)throw yt([e,r]);return[]}s===0&&(s=0),i===0&&(i=0);let a=s>i,c=String(e),p=String(r),m=String(t);t=Math.max(Math.abs(t),1);let h=De(c)||De(p)||De(m),R=h?Math.max(c.length,p.length,m.length):0,f=h===!1&&Ir(e,r,n)===!1,$=n.transform||Nr(f);if(n.toRegex&&t===1)return mt(gt(e,R),gt(r,R),!0,n);let _={negatives:[],positives:[]},y=T=>_[T<0?"negatives":"positives"].push(Math.abs(T)),E=[],S=0;for(;a?s>=i:s<=i;)n.toRegex===!0&&t>1?y(s):E.push(Br($(s,S),R,f)),s=a?s-t:s+t,S++;return n.toRegex===!0?t>1?Pr(_,n):Rt(E,null,{wrap:!1,...n}):E},Ur=(e,r,t=1,n={})=>{if(!Ae(e)&&e.length>1||!Ae(r)&&r.length>1)return _t(e,r,n);let s=n.transform||(f=>String.fromCharCode(f)),i=`${e}`.charCodeAt(0),a=`${r}`.charCodeAt(0),c=i>a,p=Math.min(i,a),m=Math.max(i,a);if(n.toRegex&&t===1)return mt(p,m,!1,n);let h=[],R=0;for(;c?i>=a:i<=a;)h.push(s(i,R)),i=c?i-t:i+t,R++;return n.toRegex===!0?Rt(h,null,{wrap:!1,options:n}):h},$e=(e,r,t,n={})=>{if(r==null&&Me(e))return[e];if(!Me(e)||!Me(r))return _t(e,r,n);if(typeof t=="function")return $e(e,r,1,{transform:t});if(dt(t))return $e(e,r,0,t);let s={...n};return s.capture===!0&&(s.wrap=!0),t=t||s.step||1,Ae(t)?Ae(e)&&Ae(r)?Dr(e,r,t,s):Ur(e,r,Math.max(Math.abs(t),1),s):t!=null&&!dt(t)?Mr(t,s):$e(e,r,1,t)};Et.exports=$e});var Ct=q((ts,xt)=>{"use strict";var Gr=Ue(),bt=ve(),qr=(e,r={})=>{let t=(n,s={})=>{let i=bt.isInvalidBrace(s),a=n.invalid===!0&&r.escapeInvalid===!0,c=i===!0||a===!0,p=r.escapeInvalid===!0?"\\":"",m="";if(n.isOpen===!0||n.isClose===!0)return p+n.value;if(n.type==="open")return c?p+n.value:"(";if(n.type==="close")return c?p+n.value:")";if(n.type==="comma")return n.prev.type==="comma"?"":c?n.value:"|";if(n.value)return n.value;if(n.nodes&&n.ranges>0){let h=bt.reduce(n.nodes),R=Gr(...h,{...r,wrap:!1,toRegex:!0});if(R.length!==0)return h.length>1&&R.length>1?`(${R})`:R}if(n.nodes)for(let h of n.nodes)m+=t(h,n);return m};return t(e)};xt.exports=qr});var vt=q((rs,St)=>{"use strict";var Kr=Ue(),wt=He(),he=ve(),fe=(e="",r="",t=!1)=>{let n=[];if(e=[].concat(e),r=[].concat(r),!r.length)return e;if(!e.length)return t?he.flatten(r).map(s=>`{${s}}`):r;for(let s of e)if(Array.isArray(s))for(let i of s)n.push(fe(i,r,t));else for(let i of r)t===!0&&typeof i=="string"&&(i=`{${i}}`),n.push(Array.isArray(i)?fe(s,i,t):s+i);return he.flatten(n)},Wr=(e,r={})=>{let t=r.rangeLimit===void 0?1e3:r.rangeLimit,n=(s,i={})=>{s.queue=[];let a=i,c=i.queue;for(;a.type!=="brace"&&a.type!=="root"&&a.parent;)a=a.parent,c=a.queue;if(s.invalid||s.dollar){c.push(fe(c.pop(),wt(s,r)));return}if(s.type==="brace"&&s.invalid!==!0&&s.nodes.length===2){c.push(fe(c.pop(),["{}"]));return}if(s.nodes&&s.ranges>0){let R=he.reduce(s.nodes);if(he.exceedsLimit(...R,r.step,t))throw new RangeError("expanded array length exceeds range limit. Use options.rangeLimit to increase or disable the limit.");let f=Kr(...R,r);f.length===0&&(f=wt(s,r)),c.push(fe(c.pop(),f)),s.nodes=[];return}let p=he.encloseBrace(s),m=s.queue,h=s;for(;h.type!=="brace"&&h.type!=="root"&&h.parent;)h=h.parent,m=h.queue;for(let R=0;R{"use strict";Ht.exports={MAX_LENGTH:1024*64,CHAR_0:"0",CHAR_9:"9",CHAR_UPPERCASE_A:"A",CHAR_LOWERCASE_A:"a",CHAR_UPPERCASE_Z:"Z",CHAR_LOWERCASE_Z:"z",CHAR_LEFT_PARENTHESES:"(",CHAR_RIGHT_PARENTHESES:")",CHAR_ASTERISK:"*",CHAR_AMPERSAND:"&",CHAR_AT:"@",CHAR_BACKSLASH:"\\",CHAR_BACKTICK:"`",CHAR_CARRIAGE_RETURN:"\r",CHAR_CIRCUMFLEX_ACCENT:"^",CHAR_COLON:":",CHAR_COMMA:",",CHAR_DOLLAR:"$",CHAR_DOT:".",CHAR_DOUBLE_QUOTE:'"',CHAR_EQUAL:"=",CHAR_EXCLAMATION_MARK:"!",CHAR_FORM_FEED:"\f",CHAR_FORWARD_SLASH:"/",CHAR_HASH:"#",CHAR_HYPHEN_MINUS:"-",CHAR_LEFT_ANGLE_BRACKET:"<",CHAR_LEFT_CURLY_BRACE:"{",CHAR_LEFT_SQUARE_BRACKET:"[",CHAR_LINE_FEED:` +`,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"});h0){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"};qt.exports={MAX_LENGTH:1024*64,POSIX_REGEX_SOURCE:yn,REGEX_BACKSLASH:/\\(?![*+?^${}(|)[\]])/g,REGEX_NON_SPECIAL_CHARS:/^[^@![\].,$*+?^{}()|\\/]+/,REGEX_SPECIAL_CHARS:/[-*+?.^${}(|)[\]]/,REGEX_SPECIAL_CHARS_BACKREF:/(\\?)((\W)(\3*))/g,REGEX_SPECIAL_CHARS_GLOBAL:/([-*+?.^${}(|)[\]])/g,REGEX_REMOVE_BACKSLASH:/(?:\[.*?[^\\]\]|\\(?=.))/g,REPLACEMENTS:{"***":"*","**/**":"**","**/**/**":"**"},CHAR_0:48,CHAR_9:57,CHAR_UPPERCASE_A:65,CHAR_LOWERCASE_A:97,CHAR_UPPERCASE_Z:90,CHAR_LOWERCASE_Z:122,CHAR_LEFT_PARENTHESES:40,CHAR_RIGHT_PARENTHESES:41,CHAR_ASTERISK:42,CHAR_AMPERSAND:38,CHAR_AT:64,CHAR_BACKWARD_SLASH:92,CHAR_CARRIAGE_RETURN:13,CHAR_CIRCUMFLEX_ACCENT:94,CHAR_COLON:58,CHAR_COMMA:44,CHAR_DOT:46,CHAR_DOUBLE_QUOTE:34,CHAR_EQUAL:61,CHAR_EXCLAMATION_MARK:33,CHAR_FORM_FEED:12,CHAR_FORWARD_SLASH:47,CHAR_GRAVE_ACCENT:96,CHAR_HASH:35,CHAR_HYPHEN_MINUS:45,CHAR_LEFT_ANGLE_BRACKET:60,CHAR_LEFT_CURLY_BRACE:123,CHAR_LEFT_SQUARE_BRACKET:91,CHAR_LINE_FEED:10,CHAR_NO_BREAK_SPACE:160,CHAR_PERCENT:37,CHAR_PLUS:43,CHAR_QUESTION_MARK:63,CHAR_RIGHT_ANGLE_BRACKET:62,CHAR_RIGHT_CURLY_BRACE:125,CHAR_RIGHT_SQUARE_BRACKET:93,CHAR_SEMICOLON:59,CHAR_SINGLE_QUOTE:39,CHAR_SPACE:32,CHAR_TAB:9,CHAR_UNDERSCORE:95,CHAR_VERTICAL_LINE:124,CHAR_ZERO_WIDTH_NOBREAK_SPACE:65279,SEP:un.sep,extglobChars(e){return{"!":{type:"negate",open:"(?:(?!(?:",close:`))${e.STAR})`},"?":{type:"qmark",open:"(?:",close:")?"},"+":{type:"plus",open:"(?:",close:")+"},"*":{type:"star",open:"(?:",close:")*"},"@":{type:"at",open:"(?:",close:")"}}},globChars(e){return e===!0?Rn:Gt}}});var Re=q(Q=>{"use strict";var _n=W("path"),En=process.platform==="win32",{REGEX_BACKSLASH:bn,REGEX_REMOVE_BACKSLASH:xn,REGEX_SPECIAL_CHARS:Cn,REGEX_SPECIAL_CHARS_GLOBAL:wn}=me();Q.isObject=e=>e!==null&&typeof e=="object"&&!Array.isArray(e);Q.hasRegexChars=e=>Cn.test(e);Q.isRegexChar=e=>e.length===1&&Q.hasRegexChars(e);Q.escapeRegex=e=>e.replace(wn,"\\$1");Q.toPosixSlashes=e=>e.replace(bn,"/");Q.removeBackslashes=e=>e.replace(xn,r=>r==="\\"?"":r);Q.supportsLookbehinds=()=>{let e=process.version.slice(1).split(".").map(Number);return e.length===3&&e[0]>=9||e[0]===8&&e[1]>=10};Q.isWindows=e=>e&&typeof e.windows=="boolean"?e.windows:En===!0||_n.sep==="\\";Q.escapeLast=(e,r,t)=>{let n=e.lastIndexOf(r,t);return n===-1?e:e[n-1]==="\\"?Q.escapeLast(e,r,n-1):`${e.slice(0,n)}\\${e.slice(n)}`};Q.removePrefix=(e,r={})=>{let t=e;return t.startsWith("./")&&(t=t.slice(2),r.prefix="./"),t};Q.wrapOutput=(e,r={},t={})=>{let n=t.contains?"":"^",s=t.contains?"":"$",i=`${n}(?:${e})${s}`;return r.negated===!0&&(i=`(?:^(?!${i}).*$)`),i}});var Yt=q((us,Zt)=>{"use strict";var Kt=Re(),{CHAR_ASTERISK:We,CHAR_AT:Sn,CHAR_BACKWARD_SLASH:ye,CHAR_COMMA:vn,CHAR_DOT:je,CHAR_EXCLAMATION_MARK:Fe,CHAR_FORWARD_SLASH:Xt,CHAR_LEFT_CURLY_BRACE:Qe,CHAR_LEFT_PARENTHESES:Xe,CHAR_LEFT_SQUARE_BRACKET:Hn,CHAR_PLUS:$n,CHAR_QUESTION_MARK:Wt,CHAR_RIGHT_CURLY_BRACE:Tn,CHAR_RIGHT_PARENTHESES:jt,CHAR_RIGHT_SQUARE_BRACKET:kn}=me(),Ft=e=>e===Xt||e===ye,Qt=e=>{e.isPrefix!==!0&&(e.depth=e.isGlobstar?1/0:1)},Ln=(e,r)=>{let t=r||{},n=e.length-1,s=t.parts===!0||t.scanToEnd===!0,i=[],a=[],c=[],p=e,m=-1,h=0,R=0,f=!1,$=!1,_=!1,y=!1,E=!1,S=!1,T=!1,L=!1,z=!1,I=!1,re=0,K,g,v={value:"",depth:0,isGlob:!1},k=()=>m>=n,l=()=>p.charCodeAt(m+1),H=()=>(K=g,p.charCodeAt(++m));for(;m0&&(B=p.slice(0,h),p=p.slice(h),R-=h),w&&_===!0&&R>0?(w=p.slice(0,R),o=p.slice(R)):_===!0?(w="",o=p):w=p,w&&w!==""&&w!=="/"&&w!==p&&Ft(w.charCodeAt(w.length-1))&&(w=w.slice(0,-1)),t.unescape===!0&&(o&&(o=Kt.removeBackslashes(o)),w&&T===!0&&(w=Kt.removeBackslashes(w)));let u={prefix:B,input:e,start:h,base:w,glob:o,isBrace:f,isBracket:$,isGlob:_,isExtglob:y,isGlobstar:E,negated:L,negatedExtglob:z};if(t.tokens===!0&&(u.maxDepth=0,Ft(g)||a.push(v),u.tokens=a),t.parts===!0||t.tokens===!0){let P;for(let b=0;b{"use strict";var ke=me(),Y=Re(),{MAX_LENGTH:Le,POSIX_REGEX_SOURCE:On,REGEX_NON_SPECIAL_CHARS:Nn,REGEX_SPECIAL_CHARS_BACKREF:In,REPLACEMENTS:zt}=ke,Bn=(e,r)=>{if(typeof r.expandRange=="function")return r.expandRange(...e,r);e.sort();let t=`[${e.join("-")}]`;try{new RegExp(t)}catch{return e.map(s=>Y.escapeRegex(s)).join("..")}return t},de=(e,r)=>`Missing ${e}: "${r}" - use "\\\\${r}" to match literal characters`,Vt=(e,r)=>{if(typeof e!="string")throw new TypeError("Expected a string");e=zt[e]||e;let t={...r},n=typeof t.maxLength=="number"?Math.min(Le,t.maxLength):Le,s=e.length;if(s>n)throw new SyntaxError(`Input length: ${s}, exceeds maximum allowed length: ${n}`);let i={type:"bos",value:"",output:t.prepend||""},a=[i],c=t.capture?"":"?:",p=Y.isWindows(r),m=ke.globChars(p),h=ke.extglobChars(m),{DOT_LITERAL:R,PLUS_LITERAL:f,SLASH_LITERAL:$,ONE_CHAR:_,DOTS_SLASH:y,NO_DOT:E,NO_DOT_SLASH:S,NO_DOTS_SLASH:T,QMARK:L,QMARK_NO_DOT:z,STAR:I,START_ANCHOR:re}=m,K=A=>`(${c}(?:(?!${re}${A.dot?y:R}).)*?)`,g=t.dot?"":E,v=t.dot?L:z,k=t.bash===!0?K(t):I;t.capture&&(k=`(${k})`),typeof t.noext=="boolean"&&(t.noextglob=t.noext);let l={input:e,index:-1,start:0,dot:t.dot===!0,consumed:"",output:"",prefix:"",backtrack:!1,negated:!1,brackets:0,braces:0,parens:0,quotes:0,globstar:!1,tokens:a};e=Y.removePrefix(e,l),s=e.length;let H=[],w=[],B=[],o=i,u,P=()=>l.index===s-1,b=l.peek=(A=1)=>e[l.index+A],V=l.advance=()=>e[++l.index]||"",J=()=>e.slice(l.index+1),X=(A="",O=0)=>{l.consumed+=A,l.index+=O},Ee=A=>{l.output+=A.output!=null?A.output:A.value,X(A.value)},mr=()=>{let A=1;for(;b()==="!"&&(b(2)!=="("||b(3)==="?");)V(),l.start++,A++;return A%2===0?!1:(l.negated=!0,l.start++,!0)},be=A=>{l[A]++,B.push(A)},oe=A=>{l[A]--,B.pop()},C=A=>{if(o.type==="globstar"){let O=l.braces>0&&(A.type==="comma"||A.type==="brace"),d=A.extglob===!0||H.length&&(A.type==="pipe"||A.type==="paren");A.type!=="slash"&&A.type!=="paren"&&!O&&!d&&(l.output=l.output.slice(0,-o.output.length),o.type="star",o.value="*",o.output=k,l.output+=o.output)}if(H.length&&A.type!=="paren"&&(H[H.length-1].inner+=A.value),(A.value||A.output)&&Ee(A),o&&o.type==="text"&&A.type==="text"){o.value+=A.value,o.output=(o.output||"")+A.value;return}A.prev=o,a.push(A),o=A},xe=(A,O)=>{let d={...h[O],conditions:1,inner:""};d.prev=o,d.parens=l.parens,d.output=l.output;let x=(t.capture?"(":"")+d.open;be("parens"),C({type:A,value:O,output:l.output?"":_}),C({type:"paren",extglob:!0,value:V(),output:x}),H.push(d)},Rr=A=>{let O=A.close+(t.capture?")":""),d;if(A.type==="negate"){let x=k;A.inner&&A.inner.length>1&&A.inner.includes("/")&&(x=K(t)),(x!==k||P()||/^\)+$/.test(J()))&&(O=A.close=`)$))${x}`),A.inner.includes("*")&&(d=J())&&/^\.[^\\/.]+$/.test(d)&&(O=A.close=`)${d})${x})`),A.prev.type==="bos"&&(l.negatedExtglob=!0)}C({type:"paren",extglob:!0,value:u,output:O}),oe("parens")};if(t.fastpaths!==!1&&!/(^[*!]|[/()[\]{}"])/.test(e)){let A=!1,O=e.replace(In,(d,x,M,j,G,Ie)=>j==="\\"?(A=!0,d):j==="?"?x?x+j+(G?L.repeat(G.length):""):Ie===0?v+(G?L.repeat(G.length):""):L.repeat(M.length):j==="."?R.repeat(M.length):j==="*"?x?x+j+(G?k:""):k:x?d:`\\${d}`);return A===!0&&(t.unescape===!0?O=O.replace(/\\/g,""):O=O.replace(/\\+/g,d=>d.length%2===0?"\\\\":d?"\\":"")),O===e&&t.contains===!0?(l.output=e,l):(l.output=Y.wrapOutput(O,l,r),l)}for(;!P();){if(u=V(),u==="\0")continue;if(u==="\\"){let d=b();if(d==="/"&&t.bash!==!0||d==="."||d===";")continue;if(!d){u+="\\",C({type:"text",value:u});continue}let x=/^\\+/.exec(J()),M=0;if(x&&x[0].length>2&&(M=x[0].length,l.index+=M,M%2!==0&&(u+="\\")),t.unescape===!0?u=V():u+=V(),l.brackets===0){C({type:"text",value:u});continue}}if(l.brackets>0&&(u!=="]"||o.value==="["||o.value==="[^")){if(t.posix!==!1&&u===":"){let d=o.value.slice(1);if(d.includes("[")&&(o.posix=!0,d.includes(":"))){let x=o.value.lastIndexOf("["),M=o.value.slice(0,x),j=o.value.slice(x+2),G=On[j];if(G){o.value=M+G,l.backtrack=!0,V(),!i.output&&a.indexOf(o)===1&&(i.output=_);continue}}}(u==="["&&b()!==":"||u==="-"&&b()==="]")&&(u=`\\${u}`),u==="]"&&(o.value==="["||o.value==="[^")&&(u=`\\${u}`),t.posix===!0&&u==="!"&&o.value==="["&&(u="^"),o.value+=u,Ee({value:u});continue}if(l.quotes===1&&u!=='"'){u=Y.escapeRegex(u),o.value+=u,Ee({value:u});continue}if(u==='"'){l.quotes=l.quotes===1?0:1,t.keepQuotes===!0&&C({type:"text",value:u});continue}if(u==="("){be("parens"),C({type:"paren",value:u});continue}if(u===")"){if(l.parens===0&&t.strictBrackets===!0)throw new SyntaxError(de("opening","("));let d=H[H.length-1];if(d&&l.parens===d.parens+1){Rr(H.pop());continue}C({type:"paren",value:u,output:l.parens?")":"\\)"}),oe("parens");continue}if(u==="["){if(t.nobracket===!0||!J().includes("]")){if(t.nobracket!==!0&&t.strictBrackets===!0)throw new SyntaxError(de("closing","]"));u=`\\${u}`}else be("brackets");C({type:"bracket",value:u});continue}if(u==="]"){if(t.nobracket===!0||o&&o.type==="bracket"&&o.value.length===1){C({type:"text",value:u,output:`\\${u}`});continue}if(l.brackets===0){if(t.strictBrackets===!0)throw new SyntaxError(de("opening","["));C({type:"text",value:u,output:`\\${u}`});continue}oe("brackets");let d=o.value.slice(1);if(o.posix!==!0&&d[0]==="^"&&!d.includes("/")&&(u=`/${u}`),o.value+=u,Ee({value:u}),t.literalBrackets===!1||Y.hasRegexChars(d))continue;let x=Y.escapeRegex(o.value);if(l.output=l.output.slice(0,-o.value.length),t.literalBrackets===!0){l.output+=x,o.value=x;continue}o.value=`(${c}${x}|${o.value})`,l.output+=o.value;continue}if(u==="{"&&t.nobrace!==!0){be("braces");let d={type:"brace",value:u,output:"(",outputIndex:l.output.length,tokensIndex:l.tokens.length};w.push(d),C(d);continue}if(u==="}"){let d=w[w.length-1];if(t.nobrace===!0||!d){C({type:"text",value:u,output:u});continue}let x=")";if(d.dots===!0){let M=a.slice(),j=[];for(let G=M.length-1;G>=0&&(a.pop(),M[G].type!=="brace");G--)M[G].type!=="dots"&&j.unshift(M[G].value);x=Bn(j,t),l.backtrack=!0}if(d.comma!==!0&&d.dots!==!0){let M=l.output.slice(0,d.outputIndex),j=l.tokens.slice(d.tokensIndex);d.value=d.output="\\{",u=x="\\}",l.output=M;for(let G of j)l.output+=G.output||G.value}C({type:"brace",value:u,output:x}),oe("braces"),w.pop();continue}if(u==="|"){H.length>0&&H[H.length-1].conditions++,C({type:"text",value:u});continue}if(u===","){let d=u,x=w[w.length-1];x&&B[B.length-1]==="braces"&&(x.comma=!0,d="|"),C({type:"comma",value:u,output:d});continue}if(u==="/"){if(o.type==="dot"&&l.index===l.start+1){l.start=l.index+1,l.consumed="",l.output="",a.pop(),o=i;continue}C({type:"slash",value:u,output:$});continue}if(u==="."){if(l.braces>0&&o.type==="dot"){o.value==="."&&(o.output=R);let d=w[w.length-1];o.type="dots",o.output+=u,o.value+=u,d.dots=!0;continue}if(l.braces+l.parens===0&&o.type!=="bos"&&o.type!=="slash"){C({type:"text",value:u,output:R});continue}C({type:"dot",value:u,output:R});continue}if(u==="?"){if(!(o&&o.value==="(")&&t.noextglob!==!0&&b()==="("&&b(2)!=="?"){xe("qmark",u);continue}if(o&&o.type==="paren"){let x=b(),M=u;if(x==="<"&&!Y.supportsLookbehinds())throw new Error("Node.js v10 or higher is required for regex lookbehinds");(o.value==="("&&!/[!=<:]/.test(x)||x==="<"&&!/<([!=]|\w+>)/.test(J()))&&(M=`\\${u}`),C({type:"text",value:u,output:M});continue}if(t.dot!==!0&&(o.type==="slash"||o.type==="bos")){C({type:"qmark",value:u,output:z});continue}C({type:"qmark",value:u,output:L});continue}if(u==="!"){if(t.noextglob!==!0&&b()==="("&&(b(2)!=="?"||!/[!=<:]/.test(b(3)))){xe("negate",u);continue}if(t.nonegate!==!0&&l.index===0){mr();continue}}if(u==="+"){if(t.noextglob!==!0&&b()==="("&&b(2)!=="?"){xe("plus",u);continue}if(o&&o.value==="("||t.regex===!1){C({type:"plus",value:u,output:f});continue}if(o&&(o.type==="bracket"||o.type==="paren"||o.type==="brace")||l.parens>0){C({type:"plus",value:u});continue}C({type:"plus",value:f});continue}if(u==="@"){if(t.noextglob!==!0&&b()==="("&&b(2)!=="?"){C({type:"at",extglob:!0,value:u,output:""});continue}C({type:"text",value:u});continue}if(u!=="*"){(u==="$"||u==="^")&&(u=`\\${u}`);let d=Nn.exec(J());d&&(u+=d[0],l.index+=d[0].length),C({type:"text",value:u});continue}if(o&&(o.type==="globstar"||o.star===!0)){o.type="star",o.star=!0,o.value+=u,o.output=k,l.backtrack=!0,l.globstar=!0,X(u);continue}let A=J();if(t.noextglob!==!0&&/^\([^?]/.test(A)){xe("star",u);continue}if(o.type==="star"){if(t.noglobstar===!0){X(u);continue}let d=o.prev,x=d.prev,M=d.type==="slash"||d.type==="bos",j=x&&(x.type==="star"||x.type==="globstar");if(t.bash===!0&&(!M||A[0]&&A[0]!=="/")){C({type:"star",value:u,output:""});continue}let G=l.braces>0&&(d.type==="comma"||d.type==="brace"),Ie=H.length&&(d.type==="pipe"||d.type==="paren");if(!M&&d.type!=="paren"&&!G&&!Ie){C({type:"star",value:u,output:""});continue}for(;A.slice(0,3)==="/**";){let Ce=e[l.index+4];if(Ce&&Ce!=="/")break;A=A.slice(3),X("/**",3)}if(d.type==="bos"&&P()){o.type="globstar",o.value+=u,o.output=K(t),l.output=o.output,l.globstar=!0,X(u);continue}if(d.type==="slash"&&d.prev.type!=="bos"&&!j&&P()){l.output=l.output.slice(0,-(d.output+o.output).length),d.output=`(?:${d.output}`,o.type="globstar",o.output=K(t)+(t.strictSlashes?")":"|$)"),o.value+=u,l.globstar=!0,l.output+=d.output+o.output,X(u);continue}if(d.type==="slash"&&d.prev.type!=="bos"&&A[0]==="/"){let Ce=A[1]!==void 0?"|$":"";l.output=l.output.slice(0,-(d.output+o.output).length),d.output=`(?:${d.output}`,o.type="globstar",o.output=`${K(t)}${$}|${$}${Ce})`,o.value+=u,l.output+=d.output+o.output,l.globstar=!0,X(u+V()),C({type:"slash",value:"/",output:""});continue}if(d.type==="bos"&&A[0]==="/"){o.type="globstar",o.value+=u,o.output=`(?:^|${$}|${K(t)}${$})`,l.output=o.output,l.globstar=!0,X(u+V()),C({type:"slash",value:"/",output:""});continue}l.output=l.output.slice(0,-o.output.length),o.type="globstar",o.output=K(t),o.value+=u,l.output+=o.output,l.globstar=!0,X(u);continue}let O={type:"star",value:u,output:k};if(t.bash===!0){O.output=".*?",(o.type==="bos"||o.type==="slash")&&(O.output=g+O.output),C(O);continue}if(o&&(o.type==="bracket"||o.type==="paren")&&t.regex===!0){O.output=u,C(O);continue}(l.index===l.start||o.type==="slash"||o.type==="dot")&&(o.type==="dot"?(l.output+=S,o.output+=S):t.dot===!0?(l.output+=T,o.output+=T):(l.output+=g,o.output+=g),b()!=="*"&&(l.output+=_,o.output+=_)),C(O)}for(;l.brackets>0;){if(t.strictBrackets===!0)throw new SyntaxError(de("closing","]"));l.output=Y.escapeLast(l.output,"["),oe("brackets")}for(;l.parens>0;){if(t.strictBrackets===!0)throw new SyntaxError(de("closing",")"));l.output=Y.escapeLast(l.output,"("),oe("parens")}for(;l.braces>0;){if(t.strictBrackets===!0)throw new SyntaxError(de("closing","}"));l.output=Y.escapeLast(l.output,"{"),oe("braces")}if(t.strictSlashes!==!0&&(o.type==="star"||o.type==="bracket")&&C({type:"maybe_slash",value:"",output:`${$}?`}),l.backtrack===!0){l.output="";for(let A of l.tokens)l.output+=A.output!=null?A.output:A.value,A.suffix&&(l.output+=A.suffix)}return l};Vt.fastpaths=(e,r)=>{let t={...r},n=typeof t.maxLength=="number"?Math.min(Le,t.maxLength):Le,s=e.length;if(s>n)throw new SyntaxError(`Input length: ${s}, exceeds maximum allowed length: ${n}`);e=zt[e]||e;let i=Y.isWindows(r),{DOT_LITERAL:a,SLASH_LITERAL:c,ONE_CHAR:p,DOTS_SLASH:m,NO_DOT:h,NO_DOTS:R,NO_DOTS_SLASH:f,STAR:$,START_ANCHOR:_}=ke.globChars(i),y=t.dot?R:h,E=t.dot?f:h,S=t.capture?"":"?:",T={negated:!1,prefix:""},L=t.bash===!0?".*?":$;t.capture&&(L=`(${L})`);let z=g=>g.noglobstar===!0?L:`(${S}(?:(?!${_}${g.dot?m:a}).)*?)`,I=g=>{switch(g){case"*":return`${y}${p}${L}`;case".*":return`${a}${p}${L}`;case"*.*":return`${y}${L}${a}${p}${L}`;case"*/*":return`${y}${L}${c}${p}${E}${L}`;case"**":return y+z(t);case"**/*":return`(?:${y}${z(t)}${c})?${E}${p}${L}`;case"**/*.*":return`(?:${y}${z(t)}${c})?${E}${L}${a}${p}${L}`;case"**/.*":return`(?:${y}${z(t)}${c})?${a}${p}${L}`;default:{let v=/^(.*?)\.(\w+)$/.exec(g);if(!v)return;let k=I(v[1]);return k?k+a+v[2]:void 0}}},re=Y.removePrefix(e,T),K=I(re);return K&&t.strictSlashes!==!0&&(K+=`${c}?`),K};Jt.exports=Vt});var rr=q((ls,tr)=>{"use strict";var Pn=W("path"),Mn=Yt(),Ze=er(),Ye=Re(),Dn=me(),Un=e=>e&&typeof e=="object"&&!Array.isArray(e),D=(e,r,t=!1)=>{if(Array.isArray(e)){let h=e.map(f=>D(f,r,t));return f=>{for(let $ of h){let _=$(f);if(_)return _}return!1}}let n=Un(e)&&e.tokens&&e.input;if(e===""||typeof e!="string"&&!n)throw new TypeError("Expected pattern to be a non-empty string");let s=r||{},i=Ye.isWindows(r),a=n?D.compileRe(e,r):D.makeRe(e,r,!1,!0),c=a.state;delete a.state;let p=()=>!1;if(s.ignore){let h={...r,ignore:null,onMatch:null,onResult:null};p=D(s.ignore,h,t)}let m=(h,R=!1)=>{let{isMatch:f,match:$,output:_}=D.test(h,a,r,{glob:e,posix:i}),y={glob:e,state:c,regex:a,posix:i,input:h,output:_,match:$,isMatch:f};return typeof s.onResult=="function"&&s.onResult(y),f===!1?(y.isMatch=!1,R?y:!1):p(h)?(typeof s.onIgnore=="function"&&s.onIgnore(y),y.isMatch=!1,R?y:!1):(typeof s.onMatch=="function"&&s.onMatch(y),R?y:!0)};return t&&(m.state=c),m};D.test=(e,r,t,{glob:n,posix:s}={})=>{if(typeof e!="string")throw new TypeError("Expected input to be a string");if(e==="")return{isMatch:!1,output:""};let i=t||{},a=i.format||(s?Ye.toPosixSlashes:null),c=e===n,p=c&&a?a(e):e;return c===!1&&(p=a?a(e):e,c=p===n),(c===!1||i.capture===!0)&&(i.matchBase===!0||i.basename===!0?c=D.matchBase(e,r,t,s):c=r.exec(p)),{isMatch:Boolean(c),match:c,output:p}};D.matchBase=(e,r,t,n=Ye.isWindows(t))=>(r instanceof RegExp?r:D.makeRe(r,t)).test(Pn.basename(e));D.isMatch=(e,r,t)=>D(r,t)(e);D.parse=(e,r)=>Array.isArray(e)?e.map(t=>D.parse(t,r)):Ze(e,{...r,fastpaths:!1});D.scan=(e,r)=>Mn(e,r);D.compileRe=(e,r,t=!1,n=!1)=>{if(t===!0)return e.output;let s=r||{},i=s.contains?"":"^",a=s.contains?"":"$",c=`${i}(?:${e.output})${a}`;e&&e.negated===!0&&(c=`^(?!${c}).*$`);let p=D.toRegex(c,r);return n===!0&&(p.state=e),p};D.makeRe=(e,r={},t=!1,n=!1)=>{if(!e||typeof e!="string")throw new TypeError("Expected a non-empty string");let s={negated:!1,fastpaths:!0};return r.fastpaths!==!1&&(e[0]==="."||e[0]==="*")&&(s.output=Ze.fastpaths(e,r)),s.output||(s=Ze(e,r)),D.compileRe(s,r,t,n)};D.toRegex=(e,r)=>{try{let t=r||{};return new RegExp(e,t.flags||(t.nocase?"i":""))}catch(t){if(r&&r.debug===!0)throw t;return/$^/}};D.constants=Dn;tr.exports=D});var sr=q((fs,nr)=>{"use strict";nr.exports=rr()});var cr=q((ps,ur)=>{"use strict";var ir=W("util"),or=Pt(),ae=sr(),ze=Re(),ar=e=>e===""||e==="./",N=(e,r,t)=>{r=[].concat(r),e=[].concat(e);let n=new Set,s=new Set,i=new Set,a=0,c=h=>{i.add(h.output),t&&t.onResult&&t.onResult(h)};for(let h=0;h!n.has(h));if(t&&m.length===0){if(t.failglob===!0)throw new Error(`No matches found for "${r.join(", ")}"`);if(t.nonull===!0||t.nullglob===!0)return t.unescape?r.map(h=>h.replace(/\\/g,"")):r}return m};N.match=N;N.matcher=(e,r)=>ae(e,r);N.isMatch=(e,r,t)=>ae(r,t)(e);N.any=N.isMatch;N.not=(e,r,t={})=>{r=[].concat(r).map(String);let n=new Set,s=[],a=N(e,r,{...t,onResult:c=>{t.onResult&&t.onResult(c),s.push(c.output)}});for(let c of s)a.includes(c)||n.add(c);return[...n]};N.contains=(e,r,t)=>{if(typeof e!="string")throw new TypeError(`Expected a string: "${ir.inspect(e)}"`);if(Array.isArray(r))return r.some(n=>N.contains(e,n,t));if(typeof r=="string"){if(ar(e)||ar(r))return!1;if(e.includes(r)||e.startsWith("./")&&e.slice(2).includes(r))return!0}return N.isMatch(e,r,{...t,contains:!0})};N.matchKeys=(e,r,t)=>{if(!ze.isObject(e))throw new TypeError("Expected the first argument to be an object");let n=N(Object.keys(e),r,t),s={};for(let i of n)s[i]=e[i];return s};N.some=(e,r,t)=>{let n=[].concat(e);for(let s of[].concat(r)){let i=ae(String(s),t);if(n.some(a=>i(a)))return!0}return!1};N.every=(e,r,t)=>{let n=[].concat(e);for(let s of[].concat(r)){let i=ae(String(s),t);if(!n.every(a=>i(a)))return!1}return!0};N.all=(e,r,t)=>{if(typeof e!="string")throw new TypeError(`Expected a string: "${ir.inspect(e)}"`);return[].concat(r).every(n=>ae(n,t)(e))};N.capture=(e,r,t)=>{let n=ze.isWindows(t),i=ae.makeRe(String(e),{...t,capture:!0}).exec(n?ze.toPosixSlashes(r):r);if(i)return i.slice(1).map(a=>a===void 0?"":a)};N.makeRe=(...e)=>ae.makeRe(...e);N.scan=(...e)=>ae.scan(...e);N.parse=(e,r)=>{let t=[];for(let n of[].concat(e||[]))for(let s of or(String(n),r))t.push(ae.parse(s,r));return t};N.braces=(e,r)=>{if(typeof e!="string")throw new TypeError("Expected a string");return r&&r.nobrace===!0||!/\{.*\}/.test(e)?[e]:or(e,r)};N.braceExpand=(e,r)=>{if(typeof e!="string")throw new TypeError("Expected a string");return N.braces(e,{...r,expand:!0})};ur.exports=N});var fr=q((hs,lr)=>{"use strict";lr.exports=(e,...r)=>new Promise(t=>{t(e(...r))})});var hr=q((ds,Ve)=>{"use strict";var Gn=fr(),pr=e=>{if(e<1)throw new TypeError("Expected `concurrency` to be a number from 1 and up");let r=[],t=0,n=()=>{t--,r.length>0&&r.shift()()},s=(c,p,...m)=>{t++;let h=Gn(c,...m);p(h),h.then(n,n)},i=(c,p,...m)=>{tnew Promise(m=>i(c,m,...p));return Object.defineProperties(a,{activeCount:{get:()=>t},pendingCount:{get:()=>r.length}}),a};Ve.exports=pr;Ve.exports.default=pr});var jn={};Cr(jn,{default:()=>Wn});var Se=W("@yarnpkg/cli"),ne=W("@yarnpkg/core"),et=W("@yarnpkg/core"),ue=W("clipanion"),ce=class extends Se.BaseCommand{constructor(){super(...arguments);this.json=ue.Option.Boolean("--json",!1,{description:"Format the output as an NDJSON stream"});this.production=ue.Option.Boolean("--production",!1,{description:"Only install regular dependencies by omitting dev dependencies"});this.all=ue.Option.Boolean("-A,--all",!1,{description:"Install the entire project"});this.workspaces=ue.Option.Rest()}async execute(){let t=await ne.Configuration.find(this.context.cwd,this.context.plugins),{project:n,workspace:s}=await ne.Project.find(t,this.context.cwd),i=await ne.Cache.find(t);await n.restoreInstallState({restoreResolutions:!1});let a;if(this.all)a=new Set(n.workspaces);else if(this.workspaces.length===0){if(!s)throw new Se.WorkspaceRequiredError(n.cwd,this.context.cwd);a=new Set([s])}else a=new Set(this.workspaces.map(p=>n.getWorkspaceByIdent(et.structUtils.parseIdent(p))));for(let p of a)for(let m of this.production?["dependencies"]:ne.Manifest.hardDependencies)for(let h of p.manifest.getForScope(m).values()){let R=n.tryWorkspaceByDescriptor(h);R!==null&&a.add(R)}for(let p of n.workspaces)a.has(p)?this.production&&p.manifest.devDependencies.clear():(p.manifest.installConfig=p.manifest.installConfig||{},p.manifest.installConfig.selfReferences=!1,p.manifest.dependencies.clear(),p.manifest.devDependencies.clear(),p.manifest.peerDependencies.clear(),p.manifest.scripts.clear());return(await ne.StreamReport.start({configuration:t,json:this.json,stdout:this.context.stdout,includeLogs:!0},async p=>{await n.install({cache:i,report:p,persistProject:!1})})).exitCode()}};ce.paths=[["workspaces","focus"]],ce.usage=ue.Command.Usage({category:"Workspace-related commands",description:"install a single workspace and its dependencies",details:"\n This command will run an install as if the specified workspaces (and all other workspaces they depend on) were the only ones in the project. If no workspaces are explicitly listed, the active one will be assumed.\n\n Note that this command is only very moderately useful when using zero-installs, since the cache will contain all the packages anyway - meaning that the only difference between a full install and a focused install would just be a few extra lines in the `.pnp.cjs` file, at the cost of introducing an extra complexity.\n\n If the `-A,--all` flag is set, the entire project will be installed. Combine with `--production` to replicate the old `yarn install --production`.\n "});var Ne=W("@yarnpkg/cli"),ge=W("@yarnpkg/core"),_e=W("@yarnpkg/core"),F=W("@yarnpkg/core"),gr=W("@yarnpkg/plugin-git"),U=W("clipanion"),Oe=Be(cr()),Ar=Be(hr()),te=Be(W("typanion")),pe=class extends Ne.BaseCommand{constructor(){super(...arguments);this.recursive=U.Option.Boolean("-R,--recursive",!1,{description:"Find packages via dependencies/devDependencies instead of using the workspaces field"});this.from=U.Option.Array("--from",[],{description:"An array of glob pattern idents from which to base any recursion"});this.all=U.Option.Boolean("-A,--all",!1,{description:"Run the command on all workspaces of a project"});this.verbose=U.Option.Boolean("-v,--verbose",!1,{description:"Prefix each output line with the name of the originating workspace"});this.parallel=U.Option.Boolean("-p,--parallel",!1,{description:"Run the commands in parallel"});this.interlaced=U.Option.Boolean("-i,--interlaced",!1,{description:"Print the output of commands in real-time instead of buffering it"});this.jobs=U.Option.String("-j,--jobs",{description:"The maximum number of parallel tasks that the execution will be limited to; or `unlimited`",validator:te.isOneOf([te.isEnum(["unlimited"]),te.applyCascade(te.isNumber(),[te.isInteger(),te.isAtLeast(1)])])});this.topological=U.Option.Boolean("-t,--topological",!1,{description:"Run the command after all workspaces it depends on (regular) have finished"});this.topologicalDev=U.Option.Boolean("--topological-dev",!1,{description:"Run the command after all workspaces it depends on (regular + dev) have finished"});this.include=U.Option.Array("--include",[],{description:"An array of glob pattern idents; only matching workspaces will be traversed"});this.exclude=U.Option.Array("--exclude",[],{description:"An array of glob pattern idents; matching workspaces won't be traversed"});this.publicOnly=U.Option.Boolean("--no-private",{description:"Avoid running the command on private workspaces"});this.since=U.Option.String("--since",{description:"Only include workspaces that have been changed since the specified ref.",tolerateBoolean:!0});this.commandName=U.Option.String();this.args=U.Option.Proxy()}async execute(){let t=await ge.Configuration.find(this.context.cwd,this.context.plugins),{project:n,workspace:s}=await ge.Project.find(t,this.context.cwd);if(!this.all&&!s)throw new Ne.WorkspaceRequiredError(n.cwd,this.context.cwd);await n.restoreInstallState();let i=this.cli.process([this.commandName,...this.args]),a=i.path.length===1&&i.path[0]==="run"&&typeof i.scriptName<"u"?i.scriptName:null;if(i.path.length===0)throw new U.UsageError("Invalid subcommand name for iteration - use the 'run' keyword if you wish to execute a script");let c=this.all?n.topLevelWorkspace:s,p=this.since?Array.from(await gr.gitUtils.fetchChangedWorkspaces({ref:this.since,project:n})):[c,...this.from.length>0?c.getRecursiveWorkspaceChildren():[]],m=g=>Oe.default.isMatch(F.structUtils.stringifyIdent(g.locator),this.from),h=this.from.length>0?p.filter(m):p,R=new Set([...h,...h.map(g=>[...this.recursive?this.since?g.getRecursiveWorkspaceDependents():g.getRecursiveWorkspaceDependencies():g.getRecursiveWorkspaceChildren()]).flat()]),f=[],$=!1;if(a!=null&&a.includes(":")){for(let g of n.workspaces)if(g.manifest.scripts.has(a)&&($=!$,$===!1))break}for(let g of R)a&&!g.manifest.scripts.has(a)&&!$&&!(await ge.scriptUtils.getWorkspaceAccessibleBinaries(g)).has(a)||a===process.env.npm_lifecycle_event&&g.cwd===s.cwd||this.include.length>0&&!Oe.default.isMatch(F.structUtils.stringifyIdent(g.locator),this.include)||this.exclude.length>0&&Oe.default.isMatch(F.structUtils.stringifyIdent(g.locator),this.exclude)||this.publicOnly&&g.manifest.private===!0||f.push(g);let _=this.parallel?this.jobs==="unlimited"?1/0:Number(this.jobs)||Math.ceil(F.nodeUtils.availableParallelism()/2):1,y=_===1?!1:this.parallel,E=y?this.interlaced:!0,S=(0,Ar.default)(_),T=new Map,L=new Set,z=0,I=null,re=!1,K=await _e.StreamReport.start({configuration:t,stdout:this.context.stdout,includePrefix:!1},async g=>{let v=async(k,{commandIndex:l})=>{if(re)return-1;!y&&this.verbose&&l>1&&g.reportSeparator();let H=qn(k,{configuration:t,verbose:this.verbose,commandIndex:l}),[w,B]=dr(g,{prefix:H,interlaced:E}),[o,u]=dr(g,{prefix:H,interlaced:E});try{this.verbose&&g.reportInfo(null,`${H} Process started`);let P=Date.now(),b=await this.cli.run([this.commandName,...this.args],{cwd:k.cwd,stdout:w,stderr:o})||0;w.end(),o.end(),await B,await u;let V=Date.now();if(this.verbose){let J=t.get("enableTimers")?`, completed in ${F.formatUtils.pretty(t,V-P,F.formatUtils.Type.DURATION)}`:"";g.reportInfo(null,`${H} Process exited (exit code ${b})${J}`)}return b===130&&(re=!0,I=b),b}catch(P){throw w.end(),o.end(),await B,await u,P}};for(let k of f)T.set(k.anchoredLocator.locatorHash,k);for(;T.size>0&&!g.hasErrors();){let k=[];for(let[w,B]of T){if(L.has(B.anchoredDescriptor.descriptorHash))continue;let o=!0;if(this.topological||this.topologicalDev){let u=this.topologicalDev?new Map([...B.manifest.dependencies,...B.manifest.devDependencies]):B.manifest.dependencies;for(let P of u.values()){let b=n.tryWorkspaceByDescriptor(P);if(o=b===null||!T.has(b.anchoredLocator.locatorHash),!o)break}}if(!!o&&(L.add(B.anchoredDescriptor.descriptorHash),k.push(S(async()=>{let u=await v(B,{commandIndex:++z});return T.delete(w),L.delete(B.anchoredDescriptor.descriptorHash),u})),!y))break}if(k.length===0){let w=Array.from(T.values()).map(B=>F.structUtils.prettyLocator(t,B.anchoredLocator)).join(", ");g.reportError(_e.MessageName.CYCLIC_DEPENDENCIES,`Dependency cycle detected (${w})`);return}let H=(await Promise.all(k)).find(w=>w!==0);I===null&&(I=typeof H<"u"?1:I),(this.topological||this.topologicalDev)&&typeof H<"u"&&g.reportError(_e.MessageName.UNNAMED,"The command failed for workspaces that are depended upon by other workspaces; can't satisfy the dependency graph")}});return I!==null?I:K.exitCode()}};pe.paths=[["workspaces","foreach"]],pe.usage=U.Command.Usage({category:"Workspace-related commands",description:"run a command on all workspaces",details:"\n This command will run a given sub-command on current and all its descendant workspaces. Various flags can alter the exact behavior of the command:\n\n - If `-p,--parallel` is set, the commands will be ran in parallel; they'll by default be limited to a number of parallel tasks roughly equal to half your core number, but that can be overridden via `-j,--jobs`, or disabled by setting `-j unlimited`.\n\n - If `-p,--parallel` and `-i,--interlaced` are both set, Yarn will print the lines from the output as it receives them. If `-i,--interlaced` wasn't set, it would instead buffer the output from each process and print the resulting buffers only after their source processes have exited.\n\n - If `-t,--topological` is set, Yarn will only run the command after all workspaces that it depends on through the `dependencies` field have successfully finished executing. If `--topological-dev` is set, both the `dependencies` and `devDependencies` fields will be considered when figuring out the wait points.\n\n - If `-A,--all` is set, Yarn will run the command on all the workspaces of a project. By default yarn runs the command only on current and all its descendant workspaces.\n\n - If `-R,--recursive` is set, Yarn will find workspaces to run the command on by recursively evaluating `dependencies` and `devDependencies` fields, instead of looking at the `workspaces` fields.\n\n - If `--from` is set, Yarn will use the packages matching the 'from' glob as the starting point for any recursive search.\n\n - If `--since` is set, Yarn will only run the command on workspaces that have been modified since the specified ref. By default Yarn will use the refs specified by the `changesetBaseRefs` configuration option.\n\n - The command may apply to only some workspaces through the use of `--include` which acts as a whitelist. The `--exclude` flag will do the opposite and will be a list of packages that mustn't execute the script. Both flags accept glob patterns (if valid Idents and supported by [micromatch](https://github.com/micromatch/micromatch)). Make sure to escape the patterns, to prevent your own shell from trying to expand them.\n\n Adding the `-v,--verbose` flag will cause Yarn to print more information; in particular the name of the workspace that generated the output will be printed at the front of each line.\n\n If the command is `run` and the script being run does not exist the child workspace will be skipped without error.\n ",examples:[["Publish current and all descendant packages","yarn workspaces foreach npm publish --tolerate-republish"],["Run build script on current and all descendant packages","yarn workspaces foreach run build"],["Run build script on current and all descendant packages in parallel, building package dependencies first","yarn workspaces foreach -pt run build"],["Run build script on several packages and all their dependencies, building dependencies first","yarn workspaces foreach -ptR --from '{workspace-a,workspace-b}' run build"]]});function dr(e,{prefix:r,interlaced:t}){let n=e.createStreamReporter(r),s=new F.miscUtils.DefaultStream;s.pipe(n,{end:!1}),s.on("finish",()=>{n.end()});let i=new Promise(c=>{n.on("finish",()=>{c(s.active)})});if(t)return[s,i];let a=new F.miscUtils.BufferStream;return a.pipe(s,{end:!1}),a.on("finish",()=>{s.end()}),[a,i]}function qn(e,{configuration:r,commandIndex:t,verbose:n}){if(!n)return null;let i=`[${F.structUtils.stringifyIdent(e.locator)}]:`,a=["#2E86AB","#A23B72","#F18F01","#C73E1D","#CCE2A3"],c=a[t%a.length];return F.formatUtils.pretty(r,i,c)}var Kn={commands:[ce,pe]},Wn=Kn;return wr(jn);})(); +/*! + * fill-range + * + * Copyright (c) 2014-present, Jon Schlinkert. + * Licensed under the MIT License. + */ +/*! + * is-number + * + * Copyright (c) 2014-present, Jon Schlinkert. + * Released under the MIT License. + */ +/*! + * to-regex-range + * + * Copyright (c) 2015-present, Jon Schlinkert. + * Released under the MIT License. + */ +return plugin; +} +}; diff --git a/.yarnrc.yml b/.yarnrc.yml index fd204f8708..5f26ff096f 100644 --- a/.yarnrc.yml +++ b/.yarnrc.yml @@ -3,5 +3,7 @@ nodeLinker: node-modules plugins: - path: .yarn/plugins/@yarnpkg/plugin-interactive-tools.cjs 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 diff --git a/README.md b/README.md index dd0fd9a1b5..d7fc9c6244 100644 --- a/README.md +++ b/README.md @@ -158,6 +158,15 @@ yarn dx ```sh 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. + +[![Open in Gitpod](https://gitpod.io/button/open-in-gitpod.svg)](https://gitpod.io/#https://github.com/calcom/cal.com) + + #### Manual setup @@ -166,34 +175,29 @@ echo 'NEXT_PUBLIC_DEBUG=1' >> .env ``` DATABASE_URL='postgresql://:@:' ``` -
- If you don't know how to configure the DATABASE_URL, then follow the steps here to create a quick DB using Heroku. + If you don't know how to configure the DATABASE_URL, then follow the steps here to create a quick local DB - 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. - Create an App + 2. Create your own local db by executing `createDB ` - 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. - ![image](https://user-images.githubusercontent.com/16905768/115323232-a53ba480-a17f-11eb-98db-58e2f8c52426.png) + 3. Now open your psql shell with the DB you created: `psql -h localhost -U postgres -d ` - 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. - ![image](https://user-images.githubusercontent.com/16905768/115323126-5beb5500-a17f-11eb-8030-7380310807a9.png) + 4. Inside the psql shell execute `\conninfo`. And you will get the following info. + ![image](https://user-images.githubusercontent.com/39329182/236612291-51d87f69-6dc1-4a23-bf4d-1ca1754e0a35.png) - 5. Once the pop-up appears, click `Submit Order Form` - plan name should be `Hobby Dev - Free`. - Submit Order Form + 5. Now extract all the info and add it to your DATABASE_URL. The url would look something like this + `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`. - ![image](https://user-images.githubusercontent.com/16905768/115323367-e92ea980-a17f-11eb-9ff4-dec95f2ec349.png) - - 7. In `Settings`, copy your URI to your Cal.com `.env` file and replace the `postgresql://:@:` with it. - ![image](https://user-images.githubusercontent.com/16905768/115323556-4591c900-a180-11eb-9808-2f55d2aa3995.png) - ![image](https://user-images.githubusercontent.com/16905768/115323697-7a9e1b80-a180-11eb-9f08-a742b1037f90.png) - - 8. To view your DB, once you add new data in Prisma, you can use [Heroku Data Explorer](https://heroku-data-explorer.herokuapp.com).
+ If you don't want to create a local DB. Then you can also consider using services like railway.app or render. + - [Setup postgres DB with railway.app](https://arctype.com/postgres/setup/railway-postgres) + - [Setup postgres DB with render](https://render.com/docs/databases) + +1. Copy and paste your `DATABASE_URL` from `.env` to `.env.appStore`. + 1. Set a 32 character random string in your `.env` file for the `CALENDSO_ENCRYPTION_KEY` (You can use a command like `openssl rand -base64 24` to generate one). 1. Set up the database using the Prisma schema (found in `packages/prisma/schema.prisma`) @@ -460,6 +464,10 @@ following 9. Click the "Save" button at the bottom footer. 10. You're good to go. Now you can see any booking in Cal.com created as a meeting in HubSpot for your contacts. +### Obtaining Webex Client ID and Secret + +[See Webex Readme](./packages/app-store/webex/) + ### 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. diff --git a/apps/docs/package.json b/apps/docs/package.json index 19b87989d3..236c09a272 100644 --- a/apps/docs/package.json +++ b/apps/docs/package.json @@ -1,6 +1,7 @@ { "name": "@calcom/docs", "version": "1.0.0", + "private": true, "description": "", "main": "index.js", "scripts": { diff --git a/apps/web/CHANGELOG.md b/apps/web/CHANGELOG.md new file mode 100644 index 0000000000..c4c7804863 --- /dev/null +++ b/apps/web/CHANGELOG.md @@ -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 diff --git a/apps/web/components/eventtype/EventAvailabilityTab.tsx b/apps/web/components/eventtype/EventAvailabilityTab.tsx index 9b20648df7..8d89fecc37 100644 --- a/apps/web/components/eventtype/EventAvailabilityTab.tsx +++ b/apps/web/components/eventtype/EventAvailabilityTab.tsx @@ -119,7 +119,7 @@ const EventTypeScheduleDetails = ({ {format(dayRange.startTime, timeFormat === 12)} - -
{format(dayRange.endTime, timeFormat === 12)}
+
{format(dayRange.endTime, timeFormat === 12)}
))} diff --git a/apps/web/components/eventtype/EventSetupTab.tsx b/apps/web/components/eventtype/EventSetupTab.tsx index 222a8312d5..82ba7366da 100644 --- a/apps/web/components/eventtype/EventSetupTab.tsx +++ b/apps/web/components/eventtype/EventSetupTab.tsx @@ -390,7 +390,12 @@ export const EventSetupTab = ( addOnLeading={ <> {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", { diff --git a/apps/web/components/eventtype/EventTeamWebhooksTab.tsx b/apps/web/components/eventtype/EventTeamWebhooksTab.tsx index f2685037fa..ec1a30ad3d 100644 --- a/apps/web/components/eventtype/EventTeamWebhooksTab.tsx +++ b/apps/web/components/eventtype/EventTeamWebhooksTab.tsx @@ -170,7 +170,7 @@ export const EventTeamWebhooksTab = ({ {/* Edit webhook dialog */} !isOpen && setEditModalOpen(false)}> - + app.slug)} diff --git a/apps/web/components/eventtype/EventTypeSingleLayout.tsx b/apps/web/components/eventtype/EventTypeSingleLayout.tsx index 66b0ff7667..72a1778f4f 100644 --- a/apps/web/components/eventtype/EventTypeSingleLayout.tsx +++ b/apps/web/components/eventtype/EventTypeSingleLayout.tsx @@ -170,7 +170,7 @@ function EventTypeSingleLayout({ // Define tab navigation here const EventTypeTabs = useMemo(() => { - let navigation = getNavigation({ + const navigation = getNavigation({ t, eventType, enabledAppsNumber, @@ -210,7 +210,7 @@ function EventTypeSingleLayout({ } if (isManagedEventType || isChildrenManagedEventType) { // Removing apps and workflows for manageg event types by admins v1 - navigation = navigation.slice(0, -2); + navigation.splice(-2, 1); } else { navigation.push({ name: "webhooks", diff --git a/apps/web/components/getting-started/components/CalendarItem.tsx b/apps/web/components/getting-started/components/AppConnectionItem.tsx similarity index 80% rename from apps/web/components/getting-started/components/CalendarItem.tsx rename to apps/web/components/getting-started/components/AppConnectionItem.tsx index f04c503877..48358a189c 100644 --- a/apps/web/components/getting-started/components/CalendarItem.tsx +++ b/apps/web/components/getting-started/components/AppConnectionItem.tsx @@ -3,15 +3,16 @@ import { useLocale } from "@calcom/lib/hooks/useLocale"; import type { App } from "@calcom/types/App"; import { Button } from "@calcom/ui"; -interface ICalendarItem { +interface IAppConnectionItem { title: string; description?: string; logo: string; type: App["type"]; + installed?: boolean; } -const CalendarItem = (props: ICalendarItem) => { - const { title, logo, type } = props; +const AppConnectionItem = (props: IAppConnectionItem) => { + const { title, logo, type, installed } = props; const { t } = useLocale(); return (
@@ -25,13 +26,14 @@ const CalendarItem = (props: ICalendarItem) => { )} /> @@ -39,4 +41,4 @@ const CalendarItem = (props: ICalendarItem) => { ); }; -export { CalendarItem }; +export { AppConnectionItem }; diff --git a/apps/web/components/getting-started/components/StepConnectionLoader.tsx b/apps/web/components/getting-started/components/StepConnectionLoader.tsx new file mode 100644 index 0000000000..64dae1e62a --- /dev/null +++ b/apps/web/components/getting-started/components/StepConnectionLoader.tsx @@ -0,0 +1,17 @@ +import { SkeletonAvatar, SkeletonText, SkeletonButton } from "@calcom/ui"; + +export function StepConnectionLoader() { + return ( +
    + {Array.from({ length: 4 }).map((_item, index) => { + return ( +
  • + + + +
  • + ); + })} +
+ ); +} diff --git a/apps/web/components/getting-started/steps-views/ConnectCalendars.tsx b/apps/web/components/getting-started/steps-views/ConnectCalendars.tsx index d1d7196899..d2e91f7c09 100644 --- a/apps/web/components/getting-started/steps-views/ConnectCalendars.tsx +++ b/apps/web/components/getting-started/steps-views/ConnectCalendars.tsx @@ -3,11 +3,12 @@ import { ArrowRightIcon } from "@heroicons/react/solid"; import classNames from "@calcom/lib/classNames"; import { useLocale } from "@calcom/lib/hooks/useLocale"; import { trpc } from "@calcom/trpc/react"; -import { SkeletonAvatar, SkeletonText, SkeletonButton, List } from "@calcom/ui"; +import { List } from "@calcom/ui"; -import { CalendarItem } from "../components/CalendarItem"; +import { AppConnectionItem } from "../components/AppConnectionItem"; import { ConnectedCalendarItem } from "../components/ConnectedCalendarItem"; import { CreateEventsOnCalendarSelect } from "../components/CreateEventsOnCalendarSelect"; +import { StepConnectionLoader } from "../components/StepConnectionLoader"; interface IConnectCalendarsProps { nextStep: () => void; @@ -33,7 +34,7 @@ const ConnectedCalendars = (props: IConnectCalendarsProps) => { firstCalendar.integration.title && firstCalendar.integration.logo && ( <> - + { queryIntegrations.data.items.map((item) => (
  • {item.title && item.logo && ( - { )} - {queryIntegrations.isLoading && ( -
      - {[0, 0, 0, 0].map((_item, index) => { - return ( -
    • - - - -
    • - ); - })} -
    - )} + {queryIntegrations.isLoading && } + + + ); +}; + +export { ConnectedVideoStep }; diff --git a/apps/web/components/team/screens/Team.tsx b/apps/web/components/team/screens/Team.tsx index 888593f559..5cbf3fe8ef 100644 --- a/apps/web/components/team/screens/Team.tsx +++ b/apps/web/components/team/screens/Team.tsx @@ -31,7 +31,7 @@ const Member = ({ member, teamName }: { member: MemberType; teamName: string | n {!isBioEmpty ? ( <>
    diff --git a/apps/web/components/ui/UsernameAvailability/PremiumTextfield.tsx b/apps/web/components/ui/UsernameAvailability/PremiumTextfield.tsx index f6d39ba2d8..52d0cbbae2 100644 --- a/apps/web/components/ui/UsernameAvailability/PremiumTextfield.tsx +++ b/apps/web/components/ui/UsernameAvailability/PremiumTextfield.tsx @@ -230,7 +230,7 @@ const PremiumTextfield = (props: ICustomUsernameProps) => {
    diff --git a/apps/web/package.json b/apps/web/package.json index 4554331b6d..da2db5a857 100644 --- a/apps/web/package.json +++ b/apps/web/package.json @@ -1,6 +1,6 @@ { "name": "@calcom/web", - "version": "2.8.12", + "version": "2.9.0", "private": true, "scripts": { "analyze": "ANALYZE=true next build", @@ -115,12 +115,11 @@ "react-select": "^5.7.0", "react-timezone-select": "^1.4.0", "react-use-intercom": "1.5.1", - "remark": "^14.0.2", + "remove-markdown": "^0.5.0", "rrule": "^2.7.1", "sanitize-html": "^2.10.0", "schema-dts": "^1.1.0", "short-uuid": "^4.2.0", - "strip-markdown": "^5.0.0", "stripe": "^9.16.0", "superjson": "1.9.1", "tailwindcss-radix": "^2.6.0", @@ -153,6 +152,7 @@ "@types/qrcode": "^1.4.3", "@types/react": "18.0.26", "@types/react-phone-number-input": "^3.0.14", + "@types/remove-markdown": "^0.3.1", "@types/sanitize-html": "^2.9.0", "@types/stripe": "^8.0.417", "@types/uuid": "8.3.1", diff --git a/apps/web/pages/[user].tsx b/apps/web/pages/[user].tsx index 2ec25163fd..8d0b41a7d6 100644 --- a/apps/web/pages/[user].tsx +++ b/apps/web/pages/[user].tsx @@ -23,6 +23,7 @@ import defaultEvents, { import { useLocale } from "@calcom/lib/hooks/useLocale"; import useTheme from "@calcom/lib/hooks/useTheme"; import { markdownToSafeHTML } from "@calcom/lib/markdownToSafeHTML"; +import { stripMarkdown } from "@calcom/lib/stripMarkdown"; import prisma from "@calcom/prisma"; import { baseEventTypeSelect } from "@calcom/prisma/selects"; import { EventTypeMetaDataSchema } from "@calcom/prisma/zod-utils"; @@ -37,7 +38,16 @@ import PageWrapper from "@components/PageWrapper"; import { ssrInit } from "@server/lib/ssr"; export default function User(props: inferSSRProps & 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 useTheme(user.theme); const { t } = useLocale(); @@ -107,11 +117,9 @@ export default function User(props: inferSSRProps & E <> ({ username, name: dynamicNames[index] })) @@ -136,7 +144,7 @@ export default function User(props: inferSSRProps & E {!isBioEmpty && ( <>
    @@ -342,6 +350,7 @@ export const getServerSideProps = async (context: GetServerSidePropsContext) => const safeBio = markdownToSafeHTML(user.bio) || ""; + const markdownStrippedBio = stripMarkdown(user?.bio || ""); return { props: { users, @@ -361,6 +370,7 @@ export const getServerSideProps = async (context: GetServerSidePropsContext) => dynamicNames, dynamicUsernames, isSingleUser, + markdownStrippedBio, }, }; }; diff --git a/apps/web/pages/_document.tsx b/apps/web/pages/_document.tsx index 9efa2cedd2..dcc53b486c 100644 --- a/apps/web/pages/_document.tsx +++ b/apps/web/pages/_document.tsx @@ -46,7 +46,8 @@ class MyDocument extends Document { - + + { + 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) { if (!process.env.SENDGRID_API_KEY || !process.env.SENDGRID_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, location: true, isRecorded: true, + eventTypeId: true, user: { select: { 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; // send emails to all attendees only when user has team plan 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); return res.status(200).json({ message: "Success" }); } diff --git a/apps/web/pages/api/social/og/image.tsx b/apps/web/pages/api/social/og/image.tsx index 39bb80e722..b3078fcb8b 100644 --- a/apps/web/pages/api/social/og/image.tsx +++ b/apps/web/pages/api/social/og/image.tsx @@ -4,7 +4,6 @@ import type { SatoriOptions } from "satori"; import { z } from "zod"; 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) => res.arrayBuffer() @@ -75,12 +74,10 @@ export default async function handler(req: NextApiRequest) { imageType, }); - const title_ = md.render(title).replace(/(<([^>]+)>)/gi, ""); - const img = new ImageResponse( ( ({ name, username: usernames[index] }))} /> diff --git a/apps/web/pages/apps/[slug]/index.tsx b/apps/web/pages/apps/[slug]/index.tsx index c961f547e2..854f022616 100644 --- a/apps/web/pages/apps/[slug]/index.tsx +++ b/apps/web/pages/apps/[slug]/index.tsx @@ -2,6 +2,7 @@ import fs from "fs"; import matter from "gray-matter"; import MarkdownIt from "markdown-it"; import type { GetStaticPaths, GetStaticPropsContext } from "next"; +import Link from "next/link"; import path from "path"; import { z } from "zod"; @@ -33,7 +34,26 @@ const sourceSchema = z.object({ }), }); -function SingleAppPage({ data, source }: inferSSRProps) { +function SingleAppPage(props: inferSSRProps) { + // 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 ( +
    + This App seems to be disabled. If you are an admin, you can enable this app from{" "} + + here + +
    + ); + } + + // Disabled App should give 404 any ways in production. + return null; + } + + const { source, data } = props; return ( = async () => { export const getStaticProps = async (ctx: GetStaticPropsContext) => { 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() }, }); - 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 appDirname = path.join(isTemplate ? "templates" : "", app.dirName); + const isTemplate = appMeta.isTemplate; + const appDirname = path.join(isTemplate ? "templates" : "", appFromDb.dirName); const README_PATH = path.join(process.cwd(), "..", "..", `packages/app-store/${appDirname}/DESCRIPTION.md`); const postFilePath = path.join(README_PATH); let source = ""; try { source = fs.readFileSync(postFilePath).toString(); - source = source.replace(/{DESCRIPTION}/g, singleApp.description); + source = source.replace(/{DESCRIPTION}/g, appMeta.description); } catch (error) { /* If the app doesn't have a README we fallback to the package description */ console.log(`No DESCRIPTION.md provided for: ${appDirname}`); - source = singleApp.description; + source = appMeta.description; } const result = matter(source); @@ -111,8 +145,8 @@ export const getStaticProps = async (ctx: GetStaticPropsContext) => { data.items = data.items.map((item) => { if (typeof item === "string") { return getAppAssetFullPath(item, { - dirName: singleApp.dirName, - isTemplate: singleApp.isTemplate, + dirName: appMeta.dirName, + isTemplate: appMeta.isTemplate, }); } return item; @@ -120,8 +154,9 @@ export const getStaticProps = async (ctx: GetStaticPropsContext) => { } return { props: { + isAppDisabled: false as const, source: { content, data }, - data: singleApp, + data: appMeta, }, }; }; diff --git a/apps/web/pages/auth/setup/index.tsx b/apps/web/pages/auth/setup/index.tsx index 80214751d2..f9c3074a36 100644 --- a/apps/web/pages/auth/setup/index.tsx +++ b/apps/web/pages/auth/setup/index.tsx @@ -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 - if (!!process.env.CALCOM_LICENSE_KEY && !deploymentKey) { + if (!!process.env.CALCOM_LICENSE_KEY && !deploymentKey?.licenseKey) { await prisma.deployment.upsert({ where: { id: 1 }, update: { @@ -172,10 +175,9 @@ export const getServerSideProps = async (context: GetServerSidePropsContext) => agreedLicenseAt: new Date(), }, }); - deploymentKey = await getDeploymentKey(prisma); } - const isFreeLicense = deploymentKey === ""; + const isFreeLicense = (await getDeploymentKey(prisma)) === ""; return { props: { diff --git a/apps/web/pages/availability/[schedule].tsx b/apps/web/pages/availability/[schedule].tsx index 7af085f6ed..41861fdf52 100644 --- a/apps/web/pages/availability/[schedule].tsx +++ b/apps/web/pages/availability/[schedule].tsx @@ -62,7 +62,7 @@ const DateOverride = ({ workingHours }: { workingHours: WorkingHours[] }) => {

    {t("date_overrides")}{" "} - + @@ -212,7 +212,7 @@ export default function Availability() { aria-label={t("delete")} className="hidden sm:inline" disabled={schedule?.isLastSchedule} - tooltip={t("requires_at_least_one_schedule")} + tooltip={schedule?.isLastSchedule ? t("requires_at_least_one_schedule") : t("delete")} /> - - - {t("embed")} - - )} + {!isManagedEventType && ( + + + {t("embed")} + + + )} {/* 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) && !isChildrenManagedEventType && ( diff --git a/apps/web/pages/getting-started/[[...step]].tsx b/apps/web/pages/getting-started/[[...step]].tsx index f68a9bd32b..1c53282b29 100644 --- a/apps/web/pages/getting-started/[[...step]].tsx +++ b/apps/web/pages/getting-started/[[...step]].tsx @@ -15,6 +15,7 @@ import type { inferSSRProps } from "@lib/types/inferSSRProps"; import PageWrapper from "@components/PageWrapper"; 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 UserProfile from "@components/getting-started/steps-views/UserProfile"; 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; 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 stepIndex = steps.indexOf(step); @@ -36,9 +43,9 @@ const stepRouteSchema = z.object({ 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 router = useRouter(); - const { user } = props; const { t } = useLocale(); @@ -55,6 +62,11 @@ const OnboardingPage = (props: IOnboardingPageProps) => { subtitle: [`${t("connect_your_calendar_instructions")}`], 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")}`, 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 newStep = steps[index]; router.push( @@ -122,12 +145,15 @@ const OnboardingPage = (props: IOnboardingPageProps) => { {currentStep === "connected-calendar" && goToIndex(2)} />} + {currentStep === "connected-video" && goToIndex(3)} />} + {currentStep === "setup-availability" && ( - goToIndex(3)} defaultScheduleId={user.defaultScheduleId} /> + goToIndex(4)} defaultScheduleId={user.defaultScheduleId} /> )} {currentStep === "user-profile" && } + {headers[currentStepIndex]?.skipText && (
    - + {!token && ( + + )}
    @@ -163,7 +159,7 @@ export default function Signup({ prepopulateFormValues }: inferSSRProps { const ssr = await ssrInit(ctx); - const token = asStringOrNull(ctx.query.token); + const token = z.string().optional().parse(ctx.query.token); const props = { 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 { props: { ...props, + token, prepopulateFormValues: { email: verificationToken.identifier, + username, }, }, }; diff --git a/apps/web/pages/team/[slug].tsx b/apps/web/pages/team/[slug].tsx index f38b1b11c4..08bc04ca58 100644 --- a/apps/web/pages/team/[slug].tsx +++ b/apps/web/pages/team/[slug].tsx @@ -4,7 +4,7 @@ import Link from "next/link"; import { useRouter } from "next/router"; 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 { CAL_URL } from "@calcom/lib/constants"; 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 { markdownToSafeHTML } from "@calcom/lib/markdownToSafeHTML"; import { getTeamWithMembers } from "@calcom/lib/server/queries/teams"; +import { stripMarkdown } from "@calcom/lib/stripMarkdown"; import { collectPageParameters, telemetryEventTypes, useTelemetry } from "@calcom/lib/telemetry"; import prisma from "@calcom/prisma"; 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"; export type TeamPageProps = inferSSRProps; -function TeamPage({ team, isUnpublished }: TeamPageProps) { +function TeamPage({ team, isUnpublished, markdownStrippedBio }: TeamPageProps) { useTheme(team.theme); const showMembers = useToggleQuery("members"); const { t } = useLocale(); @@ -67,6 +68,11 @@ function TeamPage({ team, isUnpublished }: TeamPageProps) {
    { + sdkActionManager?.fire("eventTypeSelected", { + eventType: type, + }); + }} data-testid="event-type-link" className="flex justify-between">
    @@ -100,7 +106,7 @@ function TeamPage({ team, isUnpublished }: TeamPageProps) { title={teamName} description={teamName} meeting={{ - title: team?.bio || "", + title: markdownStrippedBio, profile: { name: `${team.name}`, image: getPlaceholderAvatar(team.logo, team.name) }, }} /> @@ -111,7 +117,7 @@ function TeamPage({ team, isUnpublished }: TeamPageProps) { {!isBioEmpty && ( <>
    @@ -196,10 +202,13 @@ export const getServerSideProps = async (context: GetServerSidePropsContext) => return { ...member, safeBio: markdownToSafeHTML(member.bio || "") }; }); + const markdownStrippedBio = stripMarkdown(team?.bio || ""); + return { props: { team: { ...team, safeBio, members }, trpcState: ssr.dehydrate(), + markdownStrippedBio, }, } as const; }; diff --git a/apps/web/playwright/onboarding.e2e.ts b/apps/web/playwright/onboarding.e2e.ts index 060a0609d9..06c3fa4a2f 100644 --- a/apps/web/playwright/onboarding.e2e.ts +++ b/apps/web/playwright/onboarding.e2e.ts @@ -47,10 +47,19 @@ test.describe("Onboarding", () => { // 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 expect(page).toHaveURL(/.*connected-video/); }); 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(); await expect(isDisabled).toBe(false); // same here, skip this step. @@ -59,7 +68,7 @@ test.describe("Onboarding", () => { 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(); // should redirect to /event-types after onboarding diff --git a/apps/web/public/static/locales/ar/common.json b/apps/web/public/static/locales/ar/common.json index 963961bd37..4c0a2d2605 100644 --- a/apps/web/public/static/locales/ar/common.json +++ b/apps/web/public/static/locales/ar/common.json @@ -1724,7 +1724,6 @@ "locked_apps_description": "سيكون الأعضاء قادرين على رؤية التطبيقات النشطة، ولكن من دون القدرة على تعديل أي إعدادات للتطبيق", "locked_webhooks_description": "سيكون الأعضاء قادرين على رؤية قوالب الويب النشطة، ولكن من دون القدرة على تعديل أي إعدادات للقوالب", "locked_workflows_description": "سيكون الأعضاء قادرين على رؤية مسارات العمل النشطة، ولكن من دون القدرة على تعديل أي إعدادات لمسار العمل", - "locked_by_admin": "مقفل من قبل المشرف", "app_not_connected": "أنت غير متصل بحساب {{appName}}.", "connect_now": "اتصل الآن", "managed_event_dialog_confirm_button_one": "استبدال وإشعار العضو {{count}}", diff --git a/apps/web/public/static/locales/cs/common.json b/apps/web/public/static/locales/cs/common.json index 1ffe4544f5..d61b5515cf 100644 --- a/apps/web/public/static/locales/cs/common.json +++ b/apps/web/public/static/locales/cs/common.json @@ -1724,7 +1724,6 @@ "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_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}}.", "connect_now": "Připojte se", "managed_event_dialog_confirm_button_one": "Nahradit a upozornit {{count}} člena", diff --git a/apps/web/public/static/locales/de/common.json b/apps/web/public/static/locales/de/common.json index 53cfea5d8e..3e6525a7c2 100644 --- a/apps/web/public/static/locales/de/common.json +++ b/apps/web/public/static/locales/de/common.json @@ -1724,7 +1724,6 @@ "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_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.", "connect_now": "Jetzt verbinden", "managed_event_dialog_confirm_button_one": "{{count}} Mitglied ersetzen & benachrichtigen", diff --git a/apps/web/public/static/locales/en/common.json b/apps/web/public/static/locales/en/common.json index 9190aa6da3..529d56c259 100644 --- a/apps/web/public/static/locales/en/common.json +++ b/apps/web/public/static/locales/en/common.json @@ -237,6 +237,8 @@ "set_availability": "Set your availability", "continue_without_calendar": "Continue without 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.", "set_up_later": "Set up later", "current_time": "Current time", @@ -367,6 +369,7 @@ "create_webhook": "Create Webhook", "booking_cancelled": "Booking Cancelled", "booking_rescheduled": "Booking Rescheduled", + "recording_ready":"Recording Download Link Ready", "booking_created": "Booking Created", "meeting_ended": "Meeting Ended", "form_submitted": "Form Submitted", @@ -1192,6 +1195,7 @@ "create_workflow": "Create a workflow", "do_this": "Do this", "turn_off": "Turn off", + "turn_on": "Turn on", "settings_updated_successfully": "Settings updated successfully", "error_updating_settings": "Error updating settings", "personal_cal_url": "My personal {{appName}} URL", @@ -1623,6 +1627,7 @@ "email_user_cta": "View Invitation", "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_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_step_one": "Choose your username", "email_no_user_step_two": "Connect your calendar account", @@ -1736,7 +1741,7 @@ "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_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.", "connect_now": "Connect now", "managed_event_dialog_confirm_button_one": "Replace & notify {{count}} member", @@ -1810,6 +1815,7 @@ "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.", "book_my_cal": "Book my Cal", + "email_not_cal_member_cta": "Join your team", "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_host_confirmation_emails": "Disable default confirmation emails for host", diff --git a/apps/web/public/static/locales/es/common.json b/apps/web/public/static/locales/es/common.json index e47ab44879..fd98748da5 100644 --- a/apps/web/public/static/locales/es/common.json +++ b/apps/web/public/static/locales/es/common.json @@ -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_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_by_admin": "Bloqueado por el administrador", "app_not_connected": "No ha conectado una cuenta de {{appName}}.", "connect_now": "Conectar ahora", "managed_event_dialog_confirm_button_one": "Reemplazar y notificar a {{count}} miembro", diff --git a/apps/web/public/static/locales/fr/common.json b/apps/web/public/static/locales/fr/common.json index 8196453bf5..e058ebaf12 100644 --- a/apps/web/public/static/locales/fr/common.json +++ b/apps/web/public/static/locales/fr/common.json @@ -237,6 +237,8 @@ "set_availability": "Définissez vos disponibilités", "continue_without_calendar": "Continuer sans 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.", "set_up_later": "Configurer plus tard", "current_time": "Heure actuelle", @@ -367,6 +369,7 @@ "create_webhook": "Créer un Webhook", "booking_cancelled": "Réservation annulée", "booking_rescheduled": "Réservation replanifiée", + "recording_ready": "Lien de téléchargement d'enregistrement prêt", "booking_created": "Réservation créée", "meeting_ended": "Rendez-vous terminé", "form_submitted": "Formulaire envoyé", @@ -1192,6 +1195,7 @@ "create_workflow": "Créer un workflow", "do_this": "Effectuer ceci", "turn_off": "Désactiver", + "turn_on": "Activer", "settings_updated_successfully": "Paramètres mis à jour avec succès", "error_updating_settings": "Erreur lors de la mise à jour des paramètres", "personal_cal_url": "Mon lien {{appName}} personnel", @@ -1623,6 +1627,7 @@ "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_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_step_one": "Choisissez votre nom d'utilisateur", "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_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_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}}.", "connect_now": "Connecter maintenant", "managed_event_dialog_confirm_button_one": "Remplacer et notifier {{count}} membre", @@ -1805,6 +1810,7 @@ "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.", "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_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", diff --git a/apps/web/public/static/locales/it/common.json b/apps/web/public/static/locales/it/common.json index 2320b55c54..2afc031a68 100644 --- a/apps/web/public/static/locales/it/common.json +++ b/apps/web/public/static/locales/it/common.json @@ -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_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_by_admin": "Bloccato dall'amministratore", "app_not_connected": "Non è stato connesso un account di {{appName}}.", "connect_now": "Connetti ora", "managed_event_dialog_confirm_button_one": "Sostituisci e invia notifica a {{count}} membro", diff --git a/apps/web/public/static/locales/ja/common.json b/apps/web/public/static/locales/ja/common.json index 634a63d7eb..c33a7ef8e7 100644 --- a/apps/web/public/static/locales/ja/common.json +++ b/apps/web/public/static/locales/ja/common.json @@ -1724,7 +1724,6 @@ "locked_apps_description": "メンバーは有効なアプリを確認できますが、アプリの設定を編集することはできません", "locked_webhooks_description": "メンバーは有効なウェブフックを確認できますが、ウェブフックの設定を編集することはできません", "locked_workflows_description": "メンバーは有効なワークフローを確認できますが、ワークフローの設定を編集することはできません", - "locked_by_admin": "管理者によりロックされています", "app_not_connected": "{{appName}} のアカウントに接続していません。", "connect_now": "今すぐ接続", "managed_event_dialog_confirm_button_one": "{{count}} 人のメンバーを置き換えて通知する", diff --git a/apps/web/public/static/locales/ko/common.json b/apps/web/public/static/locales/ko/common.json index baad64ccd6..61ec386b46 100644 --- a/apps/web/public/static/locales/ko/common.json +++ b/apps/web/public/static/locales/ko/common.json @@ -237,6 +237,7 @@ "set_availability": "언제 여유가 있는지 설정하세요.", "continue_without_calendar": "캘린더 없이 계속하기", "connect_your_calendar": "캘린더 연결하기", + "connect_your_video_app_instructions": "이벤트 타입에서 사용하기 위한 비디오 앱을 연결해주세요.", "connect_your_calendar_instructions": "바쁜 시간과 새 이벤트 일정을 자동으로 확인할 수 있도록 캘린더 연결", "set_up_later": "나중에 하기", "current_time": "현재 시각", @@ -465,6 +466,7 @@ "friday": "금요일", "saturday": "토요일", "sunday": "일요일", + "all_booked_today": "모두 예약됨.", "slots_load_fail": "사용 가능한 시간 슬롯을 로드할 수 없습니다.", "additional_guests": "게스트 추가", "your_name": "당신의 이름", @@ -1038,6 +1040,7 @@ "event_cancelled_trigger": "이벤트 취소 시", "new_event_trigger": "새 이벤트 예약 시", "email_host_action": "호스트에게 이메일 발송", + "email_attendee_action": "참석자들에게 이메일 전송", "sms_attendee_action": "참석자에게 SMS 발송", "sms_number_action": "특정 번호로 SMS 발송", "workflows": "워크플로", @@ -1246,6 +1249,7 @@ "calendars_description": "이벤트 타입이 캘린더와 상호 작용하는 방식 구성", "appearance_description": "예약 모양 설정 관리", "conferencing_description": "회의에 즐겨 사용하는 화상 회의 앱 추가", + "add_conferencing_app": "회의 앱 추가", "password_description": "계정 비밀번호 설정 관리", "2fa_description": "계정 비밀번호 설정 관리", "we_just_need_basic_info": "프로필 설정을 위해 몇 가지 기본 정보만 있으면 됩니다.", @@ -1620,6 +1624,7 @@ "email_user_cta": "초대 보기", "email_no_user_invite_heading": "{{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_step_one": "사용자 이름 선택", "email_no_user_step_two": "캘린더 계정 연결", @@ -1695,6 +1700,14 @@ "spot_popular_event_types_description": "어떤 이벤트 타입이 가장 많은 클릭을 받고 예약이 되고 있는지 확인해보세요", "no_responses_yet": "아직 응답 없음", "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_app_requires_connected_account": "{{appName}}에 연결된 {{dependencyName}} 계정이 필요합니다", "connect_app": "{{dependencyName}} 연결", @@ -1724,7 +1737,6 @@ "locked_apps_description": "회원은 활성 앱을 볼 수 있지만 앱 설정을 편집할 수는 없습니다", "locked_webhooks_description": "회원은 활성 웹훅을 볼 수 있지만 웹훅 설정을 편집할 수는 없습니다", "locked_workflows_description": "회원은 활성 워크플로를 볼 수 있지만 워크플로 설정을 편집할 수는 없습니다", - "locked_by_admin": "관리자에 의해 잠금 됨", "app_not_connected": "{{appName}} 계정을 연결하지 않으셨습니다.", "connect_now": "지금 연결", "managed_event_dialog_confirm_button_one": "회원 {{count}}명 바꾸기 및 알림", @@ -1782,5 +1794,15 @@ "seats_and_no_show_fee_error": "현재 좌석을 활성화할 수 없으므로 노쇼 수수료를 부과할 수 없습니다", "complete_your_booking": "예약을 완료하세요", "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": "이 이벤트 유형에는 이벤트가 예약될 때 호스트에게 이메일을 보내는 최소한 하나의 워크플로가 활성화되어 있습니다." } diff --git a/apps/web/public/static/locales/nl/common.json b/apps/web/public/static/locales/nl/common.json index f94c2bd5a2..8d73f1ad84 100644 --- a/apps/web/public/static/locales/nl/common.json +++ b/apps/web/public/static/locales/nl/common.json @@ -1724,7 +1724,6 @@ "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_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.", "connect_now": "Nu koppelen", "managed_event_dialog_confirm_button_one": "{{count}} lid vervangen en informeren", diff --git a/apps/web/public/static/locales/pl/common.json b/apps/web/public/static/locales/pl/common.json index 4a6ebb6f1c..fc86885e8e 100644 --- a/apps/web/public/static/locales/pl/common.json +++ b/apps/web/public/static/locales/pl/common.json @@ -1724,7 +1724,6 @@ "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_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}}.", "connect_now": "Połącz teraz", "managed_event_dialog_confirm_button_one": "Zastąp i poinformuj {{count}} członka", diff --git a/apps/web/public/static/locales/pt-BR/common.json b/apps/web/public/static/locales/pt-BR/common.json index c082e22892..7befbd7f5c 100644 --- a/apps/web/public/static/locales/pt-BR/common.json +++ b/apps/web/public/static/locales/pt-BR/common.json @@ -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_webhooks_description": "Os membros poderão ver os webhooks ativos, mas não poderão editar as configurações", "locked_workflows_description": "Os membros poderão ver os fluxos de trabalho ativos, mas não poderão editar as configurações", - "locked_by_admin": "Bloqueado pelo administrador", "app_not_connected": "Você não conectou uma conta do {{appName}}.", "connect_now": "Conectar agora", "managed_event_dialog_confirm_button_one": "Substituir e notificar {{count}} membro", diff --git a/apps/web/public/static/locales/pt/common.json b/apps/web/public/static/locales/pt/common.json index a09ad159db..b6ae5d2220 100644 --- a/apps/web/public/static/locales/pt/common.json +++ b/apps/web/public/static/locales/pt/common.json @@ -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_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_by_admin": "Bloqueado pelo administrador", "app_not_connected": "Não associou uma conta {{appName}}.", "connect_now": "Associar agora", "managed_event_dialog_confirm_button_one": "Substituir e notificar {{count}} membro", diff --git a/apps/web/public/static/locales/ro/common.json b/apps/web/public/static/locales/ro/common.json index bd35210afc..00c718ee4f 100644 --- a/apps/web/public/static/locales/ro/common.json +++ b/apps/web/public/static/locales/ro/common.json @@ -1724,7 +1724,6 @@ "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_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}}.", "connect_now": "Conectați-l acum", "managed_event_dialog_confirm_button_one": "Înlocuiți și anunțați {{count}} membru", diff --git a/apps/web/public/static/locales/ru/common.json b/apps/web/public/static/locales/ru/common.json index 68376a4264..8affc056cd 100644 --- a/apps/web/public/static/locales/ru/common.json +++ b/apps/web/public/static/locales/ru/common.json @@ -1724,7 +1724,6 @@ "locked_apps_description": "Участники смогут видеть активные приложения, но не смогут редактировать их настройки", "locked_webhooks_description": "Участники смогут видеть активные вебхуки, но не смогут редактировать их настройки", "locked_workflows_description": "Участники смогут видеть активные рабочие процессы, но не смогут редактировать их настройки", - "locked_by_admin": "Заблокировано администратором", "app_not_connected": "Вы не подключили аккаунт {{appName}}.", "connect_now": "Подключить сейчас", "managed_event_dialog_confirm_button_one": "Заменить и уведомить {{count}} участника", diff --git a/apps/web/public/static/locales/sr/common.json b/apps/web/public/static/locales/sr/common.json index 716b625429..60596c4c11 100644 --- a/apps/web/public/static/locales/sr/common.json +++ b/apps/web/public/static/locales/sr/common.json @@ -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_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_by_admin": "Zaključao administrator", "app_not_connected": "Niste povezali {{appName}} nalog.", "connect_now": "Povežite odmah", "managed_event_dialog_confirm_button_one": "Zameni i obavesti {{count}} člana", diff --git a/apps/web/public/static/locales/sv/common.json b/apps/web/public/static/locales/sv/common.json index 516b8cd887..9fb50e2812 100644 --- a/apps/web/public/static/locales/sv/common.json +++ b/apps/web/public/static/locales/sv/common.json @@ -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_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_by_admin": "Låst av administratör", "app_not_connected": "Du har inte anslutit ett {{appName}}-konto.", "connect_now": "Anslut nu", "managed_event_dialog_confirm_button_one": "Ersätt och meddela {{count}} medlem", diff --git a/apps/web/public/static/locales/tr/common.json b/apps/web/public/static/locales/tr/common.json index 525a10c8fe..3a5d622e62 100644 --- a/apps/web/public/static/locales/tr/common.json +++ b/apps/web/public/static/locales/tr/common.json @@ -1724,7 +1724,6 @@ "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_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.", "connect_now": "Hemen bağlan", "managed_event_dialog_confirm_button_one": "{{count}} üyeyi değiştirin ve bilgilendirin", diff --git a/apps/web/public/static/locales/uk/common.json b/apps/web/public/static/locales/uk/common.json index dea6b56fb4..4521b177bc 100644 --- a/apps/web/public/static/locales/uk/common.json +++ b/apps/web/public/static/locales/uk/common.json @@ -1724,7 +1724,6 @@ "locked_apps_description": "Учасники зможуть бачити активні додатки, але не зможуть редагувати налаштування додатка", "locked_webhooks_description": "Учасники бачитимуть активні вебгуки, але не зможуть змінювати їх налаштування", "locked_workflows_description": "Учасники бачитимуть активні робочі процеси, але не зможуть змінювати їх налаштування", - "locked_by_admin": "Заблоковано адміністратором", "app_not_connected": "Ви не під’єднали обліковий запис {{appName}}.", "connect_now": "Під’єднати", "managed_event_dialog_confirm_button_one": "Замінити й повідомити {{count}} учасника", diff --git a/apps/web/public/static/locales/vi/common.json b/apps/web/public/static/locales/vi/common.json index 9490052fd0..c33b42d771 100644 --- a/apps/web/public/static/locales/vi/common.json +++ b/apps/web/public/static/locales/vi/common.json @@ -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_webhooks_description": "Các thành viên có thể thấy được những webhook đang hoạt động nhưng không thể sửa bất kỳ thiết lập nào của webhook", "locked_workflows_description": "Các thành viên có thể thấy được những tiến độ công việc đang hoạt động nhưng không thể sửa bất kỳ thiết lập nào của tiến độ công việc", - "locked_by_admin": "Bị khoá bởi quản trị viên", "app_not_connected": "Bạn không có tài khoản {{appName}} đã được kết nối.", "connect_now": "Kết nối ngay", "managed_event_dialog_confirm_button_one": "Thay thế & thông báo cho {{count}} thành viên", diff --git a/apps/web/public/static/locales/zh-CN/common.json b/apps/web/public/static/locales/zh-CN/common.json index a4acd53bce..beb1c929aa 100644 --- a/apps/web/public/static/locales/zh-CN/common.json +++ b/apps/web/public/static/locales/zh-CN/common.json @@ -1191,6 +1191,7 @@ "create_workflow": "创建工作流程", "do_this": "执行此操作", "turn_off": "关闭", + "turn_on": "打开", "settings_updated_successfully": "设置已成功更新", "error_updating_settings": "更新设置时出错", "personal_cal_url": "我的个人 {{appName}} URL", @@ -1726,7 +1727,7 @@ "locked_apps_description": "成员将能够查看活动的应用,但不能编辑任何应用设置", "locked_webhooks_description": "成员将能够查看活动的 Webhook,但不能编辑任何 Webhook 设置", "locked_workflows_description": "成员将能够查看活动的工作流程,但不能编辑任何工作流程设置", - "locked_by_admin": "被管理员锁定", + "locked_by_admin": "被团队管理员锁定", "app_not_connected": "您尚未连接 {{appName}} 账户。", "connect_now": "立即连接", "managed_event_dialog_confirm_button_one": "替换并通知 {{count}} 个成员", diff --git a/apps/web/public/static/locales/zh-TW/common.json b/apps/web/public/static/locales/zh-TW/common.json index 30642a56bd..c1e4469c9d 100644 --- a/apps/web/public/static/locales/zh-TW/common.json +++ b/apps/web/public/static/locales/zh-TW/common.json @@ -1724,7 +1724,6 @@ "locked_apps_description": "成員將能查看啟用的應用程式,但無法編輯任何應用程式設定", "locked_webhooks_description": "成員將能查看啟用的 Webhook,但無法編輯任何 Webhook 設定", "locked_workflows_description": "成員將能查看啟用的工作流程,但無法編輯任何工作流程設定", - "locked_by_admin": "已由管理員鎖定", "app_not_connected": "您尚未連結 {{appName}} 帳號。", "connect_now": "立即連結", "managed_event_dialog_confirm_button_one": "取代並通知 {{count}} 位成員", diff --git a/apps/web/test/lib/handleChildrenEventTypes.test.ts b/apps/web/test/lib/handleChildrenEventTypes.test.ts index e88e89cc9c..d02d168100 100644 --- a/apps/web/test/lib/handleChildrenEventTypes.test.ts +++ b/apps/web/test/lib/handleChildrenEventTypes.test.ts @@ -3,7 +3,7 @@ import type { Prisma } from "@prisma/client"; import updateChildrenEventTypes from "@calcom/features/ee/managed-event-types/lib/handleChildrenEventTypes"; 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"; @@ -291,4 +291,85 @@ describe("handleChildrenEventTypes", () => { 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, + }, + }, + }); + }); + }); }); diff --git a/git-setup.sh b/git-setup.sh index 5f8b782058..9524ce21af 100755 --- a/git-setup.sh +++ b/git-setup.sh @@ -23,8 +23,12 @@ for module in "$@"; do git submodule add --force $project "apps/$module" # Set the default branch to main git config -f .gitmodules --add "submodule.apps/$module.branch" main - # Adding the subdmoule ignores the `.gitignore` so a reset is needed - git reset + + # 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 # If the module is the API, display a link to request access if [ "$module" = "api" ]; then @@ -35,3 +39,4 @@ for module in "$@"; do fi fi done +git restore --staged .gitmodules diff --git a/package.json b/package.json index cba8b795df..e3856d2561 100644 --- a/package.json +++ b/package.json @@ -17,6 +17,7 @@ "app-store": "yarn app-store-cli cli", "create-app": "yarn app-store create", "edit-app": "yarn app-store edit", + "publish-embed": "yarn workspaces foreach --from=\"@calcom/embed*\" npm publish --access=public", "delete-app": "yarn app-store delete", "create-app-template": "yarn app-store create-template", "edit-app-template": "yarn app-store edit-template", @@ -74,6 +75,7 @@ "web": "yarn workspace @calcom/web" }, "devDependencies": { + "@changesets/cli": "^2.26.1", "@deploysentinel/playwright": "^0.3.3", "@playwright/test": "^1.31.2", "@snaplet/copycat": "^0.3.0", diff --git a/packages/app-store-cli/package.json b/packages/app-store-cli/package.json index 20085091a7..00927b79f1 100644 --- a/packages/app-store-cli/package.json +++ b/packages/app-store-cli/package.json @@ -1,5 +1,6 @@ { "name": "@calcom/app-store-cli", + "private": true, "sideEffects": false, "version": "0.0.0", "bin": "dist/cli.js", diff --git a/packages/app-store-cli/src/components/AppCreateUpdateForm.tsx b/packages/app-store-cli/src/components/AppCreateUpdateForm.tsx index ede4e13935..f3e385a935 100644 --- a/packages/app-store-cli/src/components/AppCreateUpdateForm.tsx +++ b/packages/app-store-cli/src/components/AppCreateUpdateForm.tsx @@ -4,9 +4,9 @@ import SelectInput from "ink-select-input"; import TextInput from "ink-text-input"; 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 Templates from "../utils/templates"; import Label from "./Label"; @@ -148,8 +148,6 @@ export const AppForm = ({ oldSlug: givenSlug, }); - await Seed.update({ slug, category: category, oldSlug: givenSlug, isTemplate }); - await generateAppFiles(); // FIXME: Even after CLI showing this message, it is stuck doing work before exiting @@ -242,6 +240,10 @@ export const AppForm = ({ Publisher Email: {email} + + Next Step: Enable the app from http://localhost:3000/settings/admin/apps as admin user (Email: + admin@example.com, Pass: ADMINadmin2022!) + )} diff --git a/packages/app-store-cli/src/components/DeleteForm.tsx b/packages/app-store-cli/src/components/DeleteForm.tsx index b1a3768f8c..54012cc159 100644 --- a/packages/app-store-cli/src/components/DeleteForm.tsx +++ b/packages/app-store-cli/src/components/DeleteForm.tsx @@ -4,7 +4,7 @@ import React, { useEffect, useState } from "react"; import { ImportantText } from "../components/ImportantText"; import { Message } from "../components/Message"; -import { BaseAppFork, Seed, generateAppFiles } from "../core"; +import { BaseAppFork, generateAppFiles } from "../core"; import { getApp } from "../utils/getApp"; 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") { (async () => { await BaseAppFork.delete({ slug, isTemplate }); - Seed.revert({ slug }); await generateAppFiles(); // successMsg({ text: `App with slug ${slug} has been deleted`, done: true }); setState("DELETION_COMPLETED"); diff --git a/packages/app-store-cli/src/core.ts b/packages/app-store-cli/src/core.ts index 7c06fbf172..5255c4884e 100644 --- a/packages/app-store-cli/src/core.ts +++ b/packages/app-store-cli/src/core.ts @@ -1,8 +1,6 @@ import fs from "fs"; import path from "path"; -import type seedAppStoreConfig from "@calcom/prisma/seed-app-store.config.json"; - import { APP_STORE_PATH, TEMPLATES_PATH } from "./constants"; import execSync from "./utils/execSync"; @@ -27,10 +25,6 @@ export function getAppDirPath(slug: string, isTemplate: boolean) { return path.join(TEMPLATES_PATH, `${slug}`); } -function absolutePath(appRelativePath: string) { - return path.join(APP_STORE_PATH, appRelativePath); -} - const updatePackageJson = ({ slug, 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 () => { await execSync(`yarn ts-node --transpile-only src/build.ts`); }; diff --git a/packages/app-store/_appRegistry.ts b/packages/app-store/_appRegistry.ts index b8b979d3cf..709b665ddb 100644 --- a/packages/app-store/_appRegistry.ts +++ b/packages/app-store/_appRegistry.ts @@ -5,8 +5,22 @@ import { userMetadata } from "@calcom/prisma/zod-utils"; import type { AppFrontendPayload as App } from "@calcom/types/App"; 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; // Let's not leak api keys to the front end // eslint-disable-next-line @typescript-eslint/no-unused-vars diff --git a/packages/app-store/_utils/getCalendar.ts b/packages/app-store/_utils/getCalendar.ts index 5fddb8b419..b2ed77fdce 100644 --- a/packages/app-store/_utils/getCalendar.ts +++ b/packages/app-store/_utils/getCalendar.ts @@ -12,7 +12,14 @@ export const getCalendar = async (credential: CredentialPayload | null): Promise if (calendarType?.endsWith("_other_calendar")) { 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)) { log.warn(`calendar of type ${calendarType} is not implemented`); return null; diff --git a/packages/app-store/apps.keys-schemas.generated.ts b/packages/app-store/apps.keys-schemas.generated.ts index fe57bb2d9d..dff887f123 100644 --- a/packages/app-store/apps.keys-schemas.generated.ts +++ b/packages/app-store/apps.keys-schemas.generated.ts @@ -9,6 +9,7 @@ import { appKeysSchema as giphy_zod_ts } from "./giphy/zod"; import { appKeysSchema as googlecalendar_zod_ts } from "./googlecalendar/zod"; import { appKeysSchema as gtm_zod_ts } from "./gtm/zod"; import { appKeysSchema as hubspot_zod_ts } from "./hubspot/zod"; +import { appKeysSchema as jitsivideo_zod_ts } from "./jitsivideo/zod"; import { appKeysSchema as larkcalendar_zod_ts } from "./larkcalendar/zod"; import { appKeysSchema as metapixel_zod_ts } from "./metapixel/zod"; import { appKeysSchema as office365calendar_zod_ts } from "./office365calendar/zod"; @@ -23,6 +24,7 @@ import { appKeysSchema as tandemvideo_zod_ts } from "./tandemvideo/zod"; import { appKeysSchema as booking_pages_tag_zod_ts } from "./templates/booking-pages-tag/zod"; import { appKeysSchema as event_type_app_card_zod_ts } from "./templates/event-type-app-card/zod"; import { appKeysSchema as vital_zod_ts } from "./vital/zod"; +import { appKeysSchema as webex_zod_ts } from "./webex/zod"; import { appKeysSchema as wordpress_zod_ts } from "./wordpress/zod"; import { appKeysSchema as zapier_zod_ts } from "./zapier/zod"; import { appKeysSchema as zoho_bigin_zod_ts } from "./zoho-bigin/zod"; @@ -37,6 +39,7 @@ export const appKeysSchemas = { googlecalendar: googlecalendar_zod_ts, gtm: gtm_zod_ts, hubspot: hubspot_zod_ts, + jitsivideo: jitsivideo_zod_ts, larkcalendar: larkcalendar_zod_ts, metapixel: metapixel_zod_ts, office365calendar: office365calendar_zod_ts, @@ -51,6 +54,7 @@ export const appKeysSchemas = { "booking-pages-tag": booking_pages_tag_zod_ts, "event-type-app-card": event_type_app_card_zod_ts, vital: vital_zod_ts, + webex: webex_zod_ts, wordpress: wordpress_zod_ts, zapier: zapier_zod_ts, "zoho-bigin": zoho_bigin_zod_ts, diff --git a/packages/app-store/apps.metadata.generated.ts b/packages/app-store/apps.metadata.generated.ts index bb402e60a3..db829a26f2 100644 --- a/packages/app-store/apps.metadata.generated.ts +++ b/packages/app-store/apps.metadata.generated.ts @@ -55,6 +55,7 @@ import typeform_config_json from "./typeform/config.json"; import vimcal_config_json from "./vimcal/config.json"; import { metadata as vital__metadata_ts } from "./vital/_metadata"; import weather_in_your_calendar_config_json from "./weather_in_your_calendar/config.json"; +import webex_config_json from "./webex/config.json"; import whatsapp_config_json from "./whatsapp/config.json"; import whereby_config_json from "./whereby/config.json"; import { metadata as wipemycalother__metadata_ts } from "./wipemycalother/_metadata"; @@ -118,6 +119,7 @@ export const appStoreMetadata = { vimcal: vimcal_config_json, vital: vital__metadata_ts, weather_in_your_calendar: weather_in_your_calendar_config_json, + webex: webex_config_json, whatsapp: whatsapp_config_json, whereby: whereby_config_json, wipemycalother: wipemycalother__metadata_ts, diff --git a/packages/app-store/apps.schemas.generated.ts b/packages/app-store/apps.schemas.generated.ts index 4362a31744..0caa880e9b 100644 --- a/packages/app-store/apps.schemas.generated.ts +++ b/packages/app-store/apps.schemas.generated.ts @@ -9,6 +9,7 @@ import { appDataSchema as giphy_zod_ts } from "./giphy/zod"; import { appDataSchema as googlecalendar_zod_ts } from "./googlecalendar/zod"; import { appDataSchema as gtm_zod_ts } from "./gtm/zod"; import { appDataSchema as hubspot_zod_ts } from "./hubspot/zod"; +import { appDataSchema as jitsivideo_zod_ts } from "./jitsivideo/zod"; import { appDataSchema as larkcalendar_zod_ts } from "./larkcalendar/zod"; import { appDataSchema as metapixel_zod_ts } from "./metapixel/zod"; import { appDataSchema as office365calendar_zod_ts } from "./office365calendar/zod"; @@ -23,6 +24,7 @@ import { appDataSchema as tandemvideo_zod_ts } from "./tandemvideo/zod"; import { appDataSchema as booking_pages_tag_zod_ts } from "./templates/booking-pages-tag/zod"; import { appDataSchema as event_type_app_card_zod_ts } from "./templates/event-type-app-card/zod"; import { appDataSchema as vital_zod_ts } from "./vital/zod"; +import { appDataSchema as webex_zod_ts } from "./webex/zod"; import { appDataSchema as wordpress_zod_ts } from "./wordpress/zod"; import { appDataSchema as zapier_zod_ts } from "./zapier/zod"; import { appDataSchema as zoho_bigin_zod_ts } from "./zoho-bigin/zod"; @@ -37,6 +39,7 @@ export const appDataSchemas = { googlecalendar: googlecalendar_zod_ts, gtm: gtm_zod_ts, hubspot: hubspot_zod_ts, + jitsivideo: jitsivideo_zod_ts, larkcalendar: larkcalendar_zod_ts, metapixel: metapixel_zod_ts, office365calendar: office365calendar_zod_ts, @@ -51,6 +54,7 @@ export const appDataSchemas = { "booking-pages-tag": booking_pages_tag_zod_ts, "event-type-app-card": event_type_app_card_zod_ts, vital: vital_zod_ts, + webex: webex_zod_ts, wordpress: wordpress_zod_ts, zapier: zapier_zod_ts, "zoho-bigin": zoho_bigin_zod_ts, diff --git a/packages/app-store/apps.server.generated.ts b/packages/app-store/apps.server.generated.ts index 7b35ab5809..cabd37632f 100644 --- a/packages/app-store/apps.server.generated.ts +++ b/packages/app-store/apps.server.generated.ts @@ -55,6 +55,7 @@ export const apiHandlers = { vimcal: import("./vimcal/api"), vital: import("./vital/api"), weather_in_your_calendar: import("./weather_in_your_calendar/api"), + webex: import("./webex/api"), whatsapp: import("./whatsapp/api"), whereby: import("./whereby/api"), wipemycalother: import("./wipemycalother/api"), diff --git a/packages/app-store/dailyvideo/lib/VideoApiAdapter.ts b/packages/app-store/dailyvideo/lib/VideoApiAdapter.ts index a7076b832f..e0a988f5e2 100644 --- a/packages/app-store/dailyvideo/lib/VideoApiAdapter.ts +++ b/packages/app-store/dailyvideo/lib/VideoApiAdapter.ts @@ -93,7 +93,7 @@ const DailyVideoApiAdapter = (): VideoApiAdapter => { const body = await translateEvent(event); const dailyEvent = await postToDailyAPI(endpoint, body).then(dailyReturnTypeSchema.parse); const meetingToken = await postToDailyAPI("/meeting-tokens", { - properties: { room_name: dailyEvent.name, is_owner: true }, + properties: { room_name: dailyEvent.name, exp: dailyEvent.config.exp, is_owner: true }, }).then(meetingTokenSchema.parse); return Promise.resolve({ diff --git a/packages/app-store/index.ts b/packages/app-store/index.ts index 1453334b4c..6007299922 100644 --- a/packages/app-store/index.ts +++ b/packages/app-store/index.ts @@ -21,6 +21,7 @@ const appStore = { vital: () => import("./vital"), zoomvideo: () => import("./zoomvideo"), wipemycalother: () => import("./wipemycalother"), + webexvideo: () => import("./webex"), giphy: () => import("./giphy"), zapier: () => import("./zapier"), exchange2013calendar: () => import("./exchange2013calendar"), @@ -28,6 +29,7 @@ const appStore = { exchangecalendar: () => import("./exchangecalendar"), facetime: () => import("./facetime"), sylapsvideo: () => import("./sylapsvideo"), + "zoho-bigin": () => import("./zoho-bigin"), }; export default appStore; diff --git a/packages/app-store/jitsivideo/DESCRIPTION.md b/packages/app-store/jitsivideo/DESCRIPTION.md index ee6231b3bb..596f97de6f 100644 --- a/packages/app-store/jitsivideo/DESCRIPTION.md +++ b/packages/app-store/jitsivideo/DESCRIPTION.md @@ -3,4 +3,4 @@ items: - jitsi1.jpg --- -Jitsi is a free open-source video conferencing software for web and mobile. Make a call, launch on your own servers, integrate into your app, and more. +Jitsi is a free open-source video conferencing software for web and mobile. Make a call, launch on your own servers, integrate into your app, and more. \ No newline at end of file diff --git a/packages/app-store/jitsivideo/lib/VideoApiAdapter.ts b/packages/app-store/jitsivideo/lib/VideoApiAdapter.ts index cc11262572..9513e96005 100644 --- a/packages/app-store/jitsivideo/lib/VideoApiAdapter.ts +++ b/packages/app-store/jitsivideo/lib/VideoApiAdapter.ts @@ -3,18 +3,27 @@ import { v4 as uuidv4 } from "uuid"; import type { PartialReference } from "@calcom/types/EventManager"; import type { VideoApiAdapter, VideoCallData } from "@calcom/types/VideoApiAdapter"; +import getAppKeysFromSlug from "../../_utils/getAppKeysFromSlug"; +import { metadata } from "../_metadata"; + const JitsiVideoApiAdapter = (): VideoApiAdapter => { return { getAvailability: () => { return Promise.resolve([]); }, createMeeting: async (): Promise => { + const appKeys = await getAppKeysFromSlug(metadata.slug); + const meetingID = uuidv4(); + + // Default Value + const hostUrl = appKeys.jitsiHost || "https://meet.jit.si/cal"; + return Promise.resolve({ - type: "jitsi_video", + type: metadata.type, id: meetingID, password: "", - url: "https://meet.jit.si/cal/" + meetingID, + url: hostUrl + "/" + meetingID, }); }, deleteMeeting: async (): Promise => { diff --git a/packages/app-store/jitsivideo/zod.ts b/packages/app-store/jitsivideo/zod.ts new file mode 100644 index 0000000000..825da51594 --- /dev/null +++ b/packages/app-store/jitsivideo/zod.ts @@ -0,0 +1,7 @@ +import { z } from "zod"; + +export const appKeysSchema = z.object({ + jitsiHost: z.string().optional(), +}); + +export const appDataSchema = z.object({}); diff --git a/packages/app-store/package.json b/packages/app-store/package.json index bc34fa030d..994ccc8a12 100644 --- a/packages/app-store/package.json +++ b/packages/app-store/package.json @@ -1,5 +1,6 @@ { "name": "@calcom/app-store", + "private": true, "sideEffects": false, "version": "0.0.0", "main": "./index.ts", diff --git a/packages/app-store/routing-forms/trpc/_router.ts b/packages/app-store/routing-forms/trpc/_router.ts index 1830454f06..82a3c6560a 100644 --- a/packages/app-store/routing-forms/trpc/_router.ts +++ b/packages/app-store/routing-forms/trpc/_router.ts @@ -1,667 +1,75 @@ -import type { App_RoutingForms_Form, User } from "@prisma/client"; -import { Prisma } from "@prisma/client"; -import { z } from "zod"; - -import getWebhooks from "@calcom/features/webhooks/lib/getWebhooks"; -import { sendGenericWebhookPayload } from "@calcom/features/webhooks/lib/sendPayload"; -import logger from "@calcom/lib/logger"; -import { WebhookTriggerEvents } from "@calcom/prisma/enums"; -import { RoutingFormSettings } from "@calcom/prisma/zod-utils"; -import { TRPCError } from "@calcom/trpc/server"; import authedProcedure from "@calcom/trpc/server/procedures/authedProcedure"; import publicProcedure from "@calcom/trpc/server/procedures/publicProcedure"; import { router } from "@calcom/trpc/server/trpc"; -import type { Ensure } from "@calcom/types/utils"; -import ResponseEmail from "../emails/templates/response-email"; -import { jsonLogicToPrisma } from "../jsonLogicToPrisma"; -import { createFallbackRoute } from "../lib/createFallbackRoute"; -import getConnectedForms from "../lib/getConnectedForms"; -import { getSerializableForm } from "../lib/getSerializableForm"; -import { isFallbackRoute } from "../lib/isFallbackRoute"; -import { isFormEditAllowed } from "../lib/isFormEditAllowed"; -import isRouter from "../lib/isRouter"; -import isRouterLinkedField from "../lib/isRouterLinkedField"; -import type { Response, SerializableForm } from "../types/types"; -import { zodFields, zodRouterRoute, zodRoutes } from "../zod"; +import { ZDeleteFormInputSchema } from "./deleteForm.schema"; +import { ZFormMutationInputSchema } from "./formMutation.schema"; +import { ZFormQueryInputSchema } from "./formQuery.schema"; +import { ZReportInputSchema } from "./report.schema"; +import { ZResponseInputSchema } from "./response.schema"; -async function onFormSubmission( - form: Ensure & { user: User }, "fields">, - response: Response -) { - const fieldResponsesByName: Record = {}; +// eslint-disable-next-line @typescript-eslint/ban-types +const UNSTABLE_HANDLER_CACHE: Record = {}; - for (const [fieldId, fieldResponse] of Object.entries(response)) { - // Use the label lowercased as the key to identify a field. - const key = - form.fields.find((f) => f.id === fieldId)?.identifier || - (fieldResponse.label as keyof typeof fieldResponsesByName); - fieldResponsesByName[key] = fieldResponse.value; +// TODO: Move getHandler and UNSTABLE_HANDLER_CACHE to a common utils file making sure that there is no name collision across routes +/** + * This function will import the module defined in importer just once and then cache the default export of that module. + * + * It gives you the default export of the module. + * + * **Note: It is your job to ensure that the name provided is unique across all routes.** + */ +const getHandler = async < + T extends { + // eslint-disable-next-line @typescript-eslint/ban-types + default: Function; } - - const subscriberOptions = { - userId: form.user.id, - // It isn't an eventType webhook - eventTypeId: -1, - triggerEvent: WebhookTriggerEvents.FORM_SUBMITTED, - }; - - const webhooks = await getWebhooks(subscriberOptions); - const promises = webhooks.map((webhook) => { - sendGenericWebhookPayload( - webhook.secret, - "FORM_SUBMITTED", - new Date().toISOString(), - webhook, - fieldResponsesByName - ).catch((e) => { - console.error(`Error executing routing form webhook`, webhook, e); - }); - }); - - await Promise.all(promises); - if (form.settings?.emailOwnerOnSubmission) { - logger.debug( - `Preparing to send Form Response email for Form:${form.id} to form owner: ${form.user.email}` - ); - await sendResponseEmail(form, response, form.user.email); - } -} - -const sendResponseEmail = async ( - form: Pick, - response: Response, - ownerEmail: string +>( + /** + * The name of the handler in cache. It has to be unique across all routes + */ + name: string, + importer: () => Promise ) => { - try { - const email = new ResponseEmail({ form: form, toAddresses: [ownerEmail], response: response }); - await email.sendEmail(); - } catch (e) { - logger.error("Error sending response email", e); + const nameInCache = name as keyof typeof UNSTABLE_HANDLER_CACHE; + + if (!UNSTABLE_HANDLER_CACHE[nameInCache]) { + const importedModule = await importer(); + UNSTABLE_HANDLER_CACHE[nameInCache] = importedModule.default; + return importedModule.default as T["default"]; } + + return UNSTABLE_HANDLER_CACHE[nameInCache] as unknown as T["default"]; }; const appRoutingForms = router({ public: router({ - response: publicProcedure - .input( - z.object({ - formId: z.string(), - formFillerId: z.string(), - response: z.record( - z.object({ - label: z.string(), - value: z.union([z.string(), z.array(z.string())]), - }) - ), - }) - ) - .mutation(async ({ ctx, input }) => { - const { prisma } = ctx; - try { - const { response, formId } = input; - const form = await prisma.app_RoutingForms_Form.findFirst({ - where: { - id: formId, - }, - include: { - user: true, - }, - }); - if (!form) { - throw new TRPCError({ - code: "NOT_FOUND", - }); - } - - const serializableForm = await getSerializableForm(form); - if (!serializableForm.fields) { - // There is no point in submitting a form that doesn't have fields defined - throw new TRPCError({ - code: "BAD_REQUEST", - }); - } - - const serializableFormWithFields = { - ...serializableForm, - fields: serializableForm.fields, - }; - - const missingFields = serializableFormWithFields.fields - .filter((field) => !(field.required ? response[field.id]?.value : true)) - .map((f) => f.label); - - if (missingFields.length) { - throw new TRPCError({ - code: "BAD_REQUEST", - message: `Missing required fields ${missingFields.join(", ")}`, - }); - } - const invalidFields = serializableFormWithFields.fields - .filter((field) => { - const fieldValue = response[field.id]?.value; - // The field isn't required at this point. Validate only if it's set - if (!fieldValue) { - return false; - } - let schema; - if (field.type === "email") { - schema = z.string().email(); - } else if (field.type === "phone") { - schema = z.any(); - } else { - schema = z.any(); - } - return !schema.safeParse(fieldValue).success; - }) - .map((f) => ({ label: f.label, type: f.type })); - - if (invalidFields.length) { - throw new TRPCError({ - code: "BAD_REQUEST", - message: `Invalid fields ${invalidFields.map((f) => `${f.label}: ${f.type}`)}`, - }); - } - - const dbFormResponse = await prisma.app_RoutingForms_FormResponse.create({ - data: input, - }); - - await onFormSubmission(serializableFormWithFields, dbFormResponse.response as Response); - return dbFormResponse; - } catch (e) { - if (e instanceof Prisma.PrismaClientKnownRequestError) { - if (e.code === "P2002") { - throw new TRPCError({ - code: "CONFLICT", - }); - } - } - throw e; - } - }), + response: publicProcedure.input(ZResponseInputSchema).mutation(async ({ ctx, input }) => { + const handler = await getHandler("response", () => import("./response.handler")); + return handler({ ctx, input }); + }), }), forms: authedProcedure.query(async ({ ctx }) => { - const { prisma, user } = ctx; - const forms = await prisma.app_RoutingForms_Form.findMany({ - where: { - userId: user.id, - }, - orderBy: { - createdAt: "desc", - }, - include: { - _count: { - select: { - responses: true, - }, - }, - }, - }); - - const serializableForms = []; - for (let i = 0; i < forms.length; i++) { - serializableForms.push(await getSerializableForm(forms[i])); - } - return serializableForms; + const handler = await getHandler("forms", () => import("./forms.handler")); + return handler({ ctx }); + }), + formQuery: authedProcedure.input(ZFormQueryInputSchema).query(async ({ ctx, input }) => { + const handler = await getHandler("formQuery", () => import("./formQuery.handler")); + return handler({ ctx, input }); + }), + formMutation: authedProcedure.input(ZFormMutationInputSchema).mutation(async ({ ctx, input }) => { + const handler = await getHandler("formMutation", () => import("./formMutation.handler")); + return handler({ ctx, input }); + }), + deleteForm: authedProcedure.input(ZDeleteFormInputSchema).mutation(async ({ ctx, input }) => { + const handler = await getHandler("deleteForm", () => import("./deleteForm.handler")); + return handler({ ctx, input }); }), - formQuery: authedProcedure - .input( - z.object({ - id: z.string(), - }) - ) - .query(async ({ ctx, input }) => { - const { prisma, user } = ctx; - const form = await prisma.app_RoutingForms_Form.findFirst({ - where: { - userId: user.id, - id: input.id, - }, - include: { - _count: { - select: { - responses: true, - }, - }, - }, - }); - if (!form) { - return null; - } - - return await getSerializableForm(form); - }), - formMutation: authedProcedure - .input( - z.object({ - id: z.string(), - name: z.string(), - description: z.string().nullable().optional(), - disabled: z.boolean().optional(), - fields: zodFields, - routes: zodRoutes, - addFallback: z.boolean().optional(), - duplicateFrom: z.string().nullable().optional(), - shouldConnect: z.boolean().optional(), - settings: RoutingFormSettings.optional(), - }) - ) - .mutation(async ({ ctx, input }) => { - const { user, prisma } = ctx; - const { name, id, description, settings, disabled, addFallback, duplicateFrom, shouldConnect } = input; - if (!(await isFormEditAllowed({ userId: user.id, formId: id }))) { - throw new TRPCError({ - code: "FORBIDDEN", - }); - } - let { routes: inputRoutes } = input; - let { fields: inputFields } = input; - inputFields = inputFields || []; - inputRoutes = inputRoutes || []; - type InputFields = typeof inputFields; - type InputRoutes = typeof inputRoutes; - let routes: InputRoutes; - let fields: InputFields; - type DuplicateFrom = NonNullable; - - const dbForm = await prisma.app_RoutingForms_Form.findUnique({ - where: { - id: id, - }, - select: { - id: true, - user: true, - name: true, - description: true, - userId: true, - disabled: true, - createdAt: true, - updatedAt: true, - routes: true, - fields: true, - settings: true, - }, - }); - - const dbSerializedForm = dbForm ? await getSerializableForm(dbForm, true) : null; - - if (duplicateFrom) { - ({ routes, fields } = await getRoutesAndFieldsForDuplication(duplicateFrom)); - } else { - [fields, routes] = [inputFields, inputRoutes]; - if (dbSerializedForm) { - fields = markMissingFieldsDeleted(dbSerializedForm, fields); - } - } - - if (dbSerializedForm) { - // If it's an existing form being mutated, update fields in the connected forms(if any). - await updateFieldsInConnectedForms(dbSerializedForm, inputFields); - } - - fields = await getUpdatedRouterLinkedFields(fields, routes); - - if (addFallback) { - // Add a fallback route if there is none - if (!routes.find(isFallbackRoute)) { - routes.push(createFallbackRoute()); - } - } - - return await prisma.app_RoutingForms_Form.upsert({ - where: { - id: id, - }, - create: { - user: { - connect: { - id: user.id, - }, - }, - fields, - name: name, - description, - // Prisma doesn't allow setting null value directly for JSON. It recommends using JsonNull for that case. - routes: routes === null ? Prisma.JsonNull : routes, - id: id, - }, - update: { - disabled: disabled, - fields, - name: name, - description, - settings: settings === null ? Prisma.JsonNull : settings, - routes: routes === null ? Prisma.JsonNull : routes, - }, - }); - - /** - * If Form has Router Linked fields, enrich them with the latest info from the Router - * If Form doesn't have Router fields but there is a Router used in routes, add all the fields from the Router - */ - async function getUpdatedRouterLinkedFields(fields: InputFields, routes: InputRoutes) { - const routerLinkedFields: Record = {}; - for (const [, field] of Object.entries(fields)) { - if (!isRouterLinkedField(field)) { - continue; - } - routerLinkedFields[field.routerId] = true; - - if (!routes.some((route) => route.id === field.routerId)) { - // If the field is from a router that is not available anymore, mark it as deleted - field.deleted = true; - continue; - } - // Get back deleted field as now the Router is there for it. - if (field.deleted) field.deleted = false; - const router = await prisma.app_RoutingForms_Form.findFirst({ - where: { - id: field.routerId, - userId: user.id, - }, - }); - if (router) { - assertIfInvalidRouter(router); - const parsedRouterFields = zodFields.parse(router.fields); - - // There is a field from some router available, make sure that the field has up-to-date info from the router - const routerField = parsedRouterFields?.find((f) => f.id === field.id); - // Update local field(cache) with router field on every mutation - Object.assign(field, routerField); - } - } - - for (const [, route] of Object.entries(routes)) { - if (!isRouter(route)) { - continue; - } - - // If there is a field that belongs to router, then all fields must be there already. So, need to add Router fields - if (routerLinkedFields[route.id]) { - continue; - } - - const router = await prisma.app_RoutingForms_Form.findFirst({ - where: { - id: route.id, - userId: user.id, - }, - }); - if (router) { - assertIfInvalidRouter(router); - const parsedRouterFields = zodFields.parse(router.fields); - const fieldsFromRouter = parsedRouterFields - ?.filter((f) => !f.deleted) - .map((f) => { - return { - ...f, - routerId: route.id, - }; - }); - - if (fieldsFromRouter) { - fields = fields.concat(fieldsFromRouter); - } - } - } - return fields; - } - - function findFieldWithId(id: string, fields: InputFields) { - return fields.find((field) => field.id === id); - } - - /** - * Update fields in connected forms as per the inputFields - */ - async function updateFieldsInConnectedForms( - serializedForm: SerializableForm, - inputFields: InputFields - ) { - for (const [, connectedForm] of Object.entries(serializedForm.connectedForms)) { - const connectedFormDb = await prisma.app_RoutingForms_Form.findFirst({ - where: { - id: connectedForm.id, - }, - }); - if (!connectedFormDb) { - continue; - } - const connectedFormFields = zodFields.parse(connectedFormDb.fields); - - const fieldsThatAreNotInConnectedForm = ( - inputFields?.filter((f) => !findFieldWithId(f.id, connectedFormFields || [])) || [] - ).map((f) => ({ - ...f, - routerId: serializedForm.id, - })); - - const updatedConnectedFormFields = connectedFormFields - // Update fields that are already in connected form - ?.map((field) => { - if (isRouterLinkedField(field) && field.routerId === serializedForm.id) { - return { - ...field, - ...findFieldWithId(field.id, inputFields || []), - }; - } - return field; - }) - // Add fields that are not there - .concat(fieldsThatAreNotInConnectedForm); - - await prisma.app_RoutingForms_Form.update({ - where: { - id: connectedForm.id, - }, - data: { - fields: updatedConnectedFormFields, - }, - }); - } - } - - async function getRoutesAndFieldsForDuplication(duplicateFrom: DuplicateFrom) { - const sourceForm = await prisma.app_RoutingForms_Form.findFirst({ - where: { - userId: user.id, - id: duplicateFrom, - }, - select: { - id: true, - fields: true, - routes: true, - }, - }); - if (!sourceForm) { - throw new TRPCError({ - code: "BAD_REQUEST", - message: `Form to duplicate: ${duplicateFrom} not found`, - }); - } - //TODO: Instead of parsing separately, use getSerializableForm. That would automatically remove deleted fields as well. - const fieldsParsed = zodFields.safeParse(sourceForm.fields); - const routesParsed = zodRoutes.safeParse(sourceForm.routes); - if (!fieldsParsed.success || !routesParsed.success) { - throw new TRPCError({ - code: "INTERNAL_SERVER_ERROR", - message: "Could not parse source form's fields or routes", - }); - } - - let fields, routes; - if (shouldConnect) { - routes = [ - // This connected route would automatically link the fields - zodRouterRoute.parse({ - id: sourceForm.id, - isRouter: true, - }), - ]; - fields = - fieldsParsed.data - // Deleted fields in the form shouldn't be added to the new form - ?.filter((f) => !f.deleted) - .map((f) => { - return { - id: f.id, - routerId: sourceForm.id, - label: "", - type: "", - }; - }) || []; - } else { - // Duplicate just routes and fields - // We don't want name, description and responses to be copied - routes = routesParsed.data || []; - // FIXME: Deleted fields shouldn't come in duplicate - fields = fieldsParsed.data || []; - } - return { routes, fields }; - } - - function markMissingFieldsDeleted( - serializedForm: SerializableForm, - fields: InputFields - ) { - // Find all fields that are in DB(including deleted) but not in the mutation - // e.g. inputFields is [A,B,C]. DB is [A,B,C,D,E,F]. It means D,E,F got deleted - const deletedFields = - serializedForm.fields?.filter((f) => !fields.find((field) => field.id === f.id)) || []; - - // Add back deleted fields in the end and mark them deleted. - // Fields mustn't be deleted, to make sure columns never decrease which hugely simplifies CSV generation - fields = fields.concat( - deletedFields.map((f) => { - f.deleted = true; - return f; - }) - ); - return fields; - } - function assertIfInvalidRouter(router: App_RoutingForms_Form) { - const routesOfRouter = zodRoutes.parse(router.routes); - if (routesOfRouter) { - if (routesOfRouter.find(isRouter)) { - throw new TRPCError({ - code: "BAD_REQUEST", - message: - "A form being used as a Router must be a Origin form. It must not be using any other Router.", - }); - } - } - } - }), - deleteForm: authedProcedure - .input( - z.object({ - id: z.string(), - }) - ) - .mutation(async ({ ctx, input }) => { - const { user, prisma } = ctx; - if (!(await isFormEditAllowed({ userId: user.id, formId: input.id }))) { - throw new TRPCError({ - code: "FORBIDDEN", - }); - } - - const areFormsUsingIt = ( - await getConnectedForms(prisma, { - id: input.id, - userId: user.id, - }) - ).length; - if (areFormsUsingIt) { - throw new TRPCError({ - code: "BAD_REQUEST", - message: "This form is being used by other forms. Please remove it's usage from there first.", - }); - } - return await prisma.app_RoutingForms_Form.deleteMany({ - where: { - id: input.id, - userId: user.id, - }, - }); - }), - - report: authedProcedure - .input( - z.object({ - formId: z.string(), - jsonLogicQuery: z.object({ - logic: z.union([z.record(z.any()), z.null()]), - }), - cursor: z.number().nullish(), // <-- "cursor" needs to exist when using useInfiniteQuery, but can be any type - }) - ) - .query(async ({ ctx: { prisma }, input }) => { - // Can be any prisma `where` clause - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const prismaWhere: Record = input.jsonLogicQuery - ? jsonLogicToPrisma(input.jsonLogicQuery) - : {}; - const skip = input.cursor ?? 0; - const take = 50; - logger.debug( - `Built Prisma where ${JSON.stringify(prismaWhere)} from jsonLogicQuery ${JSON.stringify( - input.jsonLogicQuery - )}` - ); - const form = await prisma.app_RoutingForms_Form.findUnique({ - where: { - id: input.formId, - }, - }); - - if (!form) { - throw new TRPCError({ - code: "NOT_FOUND", - message: "Form not found", - }); - } - // TODO: Second argument is required to return deleted operators. - const serializedForm = await getSerializableForm(form, true); - - const rows = await prisma.app_RoutingForms_FormResponse.findMany({ - where: { - formId: input.formId, - ...prismaWhere, - }, - take, - skip, - }); - const fields = serializedForm?.fields || []; - const headers = fields.map((f) => f.label + (f.deleted ? "(Deleted)" : "")); - const responses: string[][] = []; - rows.forEach((r) => { - const rowResponses: string[] = []; - responses.push(rowResponses); - fields.forEach((field) => { - if (!r.response) { - return; - } - const response = r.response as Response; - const value = response[field.id]?.value || ""; - let stringValue = ""; - if (value instanceof Array) { - stringValue = value.join(", "); - } else { - stringValue = value; - } - rowResponses.push(stringValue); - }); - }); - const areThereNoResultsOrLessThanAskedFor = !rows.length || rows.length < take; - return { - headers, - responses, - nextCursor: areThereNoResultsOrLessThanAskedFor ? null : skip + rows.length, - }; - }), + report: authedProcedure.input(ZReportInputSchema).query(async ({ ctx, input }) => { + const handler = await getHandler("report", () => import("./report.handler")); + return handler({ ctx, input }); + }), }); export default appRoutingForms; diff --git a/packages/app-store/routing-forms/trpc/deleteForm.handler.ts b/packages/app-store/routing-forms/trpc/deleteForm.handler.ts index cc5e936e3c..49979dd6e7 100644 --- a/packages/app-store/routing-forms/trpc/deleteForm.handler.ts +++ b/packages/app-store/routing-forms/trpc/deleteForm.handler.ts @@ -41,3 +41,5 @@ export const deleteFormHandler = async ({ ctx, input }: DeleteFormHandlerOptions }, }); }; + +export default deleteFormHandler; diff --git a/packages/app-store/routing-forms/trpc/formMutation.handler.ts b/packages/app-store/routing-forms/trpc/formMutation.handler.ts index 1455613d49..8329abdd00 100644 --- a/packages/app-store/routing-forms/trpc/formMutation.handler.ts +++ b/packages/app-store/routing-forms/trpc/formMutation.handler.ts @@ -291,7 +291,7 @@ export const formMutationHandler = async ({ ctx, input }: FormMutationHandlerOpt // We don't want name, description and responses to be copied routes = routesParsed.data || []; // FIXME: Deleted fields shouldn't come in duplicate - fields = fieldsParsed.data || []; + fields = fieldsParsed.data ? fieldsParsed.data.filter((f) => !f.deleted) : []; } return { routes, fields }; } @@ -328,3 +328,5 @@ export const formMutationHandler = async ({ ctx, input }: FormMutationHandlerOpt } } }; + +export default formMutationHandler; diff --git a/packages/app-store/routing-forms/trpc/formQuery.handler.ts b/packages/app-store/routing-forms/trpc/formQuery.handler.ts index 61b8a30e54..277836683b 100644 --- a/packages/app-store/routing-forms/trpc/formQuery.handler.ts +++ b/packages/app-store/routing-forms/trpc/formQuery.handler.ts @@ -34,3 +34,5 @@ export const formQueryHandler = async ({ ctx, input }: FormsHandlerOptions) => { return await getSerializableForm(form); }; + +export default formQueryHandler; diff --git a/packages/app-store/routing-forms/trpc/forms.handler.ts b/packages/app-store/routing-forms/trpc/forms.handler.ts index b992b6ab1e..c8d7045a34 100644 --- a/packages/app-store/routing-forms/trpc/forms.handler.ts +++ b/packages/app-store/routing-forms/trpc/forms.handler.ts @@ -35,3 +35,5 @@ export const formsHandler = async ({ ctx }: FormsHandlerOptions) => { } return serializableForms; }; + +export default formsHandler; diff --git a/packages/app-store/routing-forms/trpc/report.handler.ts b/packages/app-store/routing-forms/trpc/report.handler.ts index b2b80b2518..365c20d9da 100644 --- a/packages/app-store/routing-forms/trpc/report.handler.ts +++ b/packages/app-store/routing-forms/trpc/report.handler.ts @@ -78,3 +78,5 @@ export const reportHandler = async ({ ctx: { prisma }, input }: ReportHandlerOpt nextCursor: areThereNoResultsOrLessThanAskedFor ? null : skip + rows.length, }; }; + +export default reportHandler; diff --git a/packages/app-store/routing-forms/trpc/response.handler.ts b/packages/app-store/routing-forms/trpc/response.handler.ts index 7a8c1d4ef5..c49c11011c 100644 --- a/packages/app-store/routing-forms/trpc/response.handler.ts +++ b/packages/app-store/routing-forms/trpc/response.handler.ts @@ -99,3 +99,5 @@ export const responseHandler = async ({ ctx, input }: ResponseHandlerOptions) => throw e; } }; + +export default responseHandler; diff --git a/packages/app-store/sendgrid/api/_postAdd.ts b/packages/app-store/sendgrid/api/_postAdd.ts index 1cf672fb95..b34826d5d9 100644 --- a/packages/app-store/sendgrid/api/_postAdd.ts +++ b/packages/app-store/sendgrid/api/_postAdd.ts @@ -15,7 +15,13 @@ export async function getHandler(req: NextApiRequest) { const { api_key } = req.body; if (!api_key) throw new HttpError({ statusCode: 400, message: "No Api Key provided to check" }); - const encrypted = symmetricEncrypt(JSON.stringify({ api_key }), process.env.CALENDSO_ENCRYPTION_KEY || ""); + let encrypted; + try { + encrypted = symmetricEncrypt(JSON.stringify({ api_key }), process.env.CALENDSO_ENCRYPTION_KEY || ""); + } catch (reason) { + logger.error("Could not add Sendgrid app", reason); + throw new HttpError({ statusCode: 500, message: "Invalid length - CALENDSO_ENCRYPTION_KEY" }); + } const data = { type: "sendgrid_other_calendar", diff --git a/packages/app-store/sendgrid/pages/setup/index.tsx b/packages/app-store/sendgrid/pages/setup/index.tsx index bd59a3aa7b..0575763810 100644 --- a/packages/app-store/sendgrid/pages/setup/index.tsx +++ b/packages/app-store/sendgrid/pages/setup/index.tsx @@ -84,7 +84,7 @@ export default function SendgridSetup() { onBlur={onBlur} disabled={testPassed === true} name="api_key" - placeholder="api_xyz..." + placeholder="SG.xxxxxx..." onChange={async (e) => { onChange(e.target.value); form.setValue("api_key", e.target.value); diff --git a/packages/app-store/weather_in_your_calendar/config.json b/packages/app-store/weather_in_your_calendar/config.json index d5c58c7a1e..751057a54c 100644 --- a/packages/app-store/weather_in_your_calendar/config.json +++ b/packages/app-store/weather_in_your_calendar/config.json @@ -3,7 +3,7 @@ "name": "Weather in your Calendar", "slug": "weather_in_your_calendar", "type": "weather_in_your_calendar_other", - "logo": "icon.gif", + "logo": "icon.svg", "url": "https://weather-in-calendar.com", "variant": "other", "categories": ["other"], diff --git a/packages/app-store/weather_in_your_calendar/static/icon.gif b/packages/app-store/weather_in_your_calendar/static/icon.gif deleted file mode 100644 index 87957d7e03..0000000000 Binary files a/packages/app-store/weather_in_your_calendar/static/icon.gif and /dev/null differ diff --git a/packages/app-store/weather_in_your_calendar/static/icon.svg b/packages/app-store/weather_in_your_calendar/static/icon.svg new file mode 100644 index 0000000000..959aa09809 --- /dev/null +++ b/packages/app-store/weather_in_your_calendar/static/icon.svg @@ -0,0 +1,40 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/packages/app-store/webex/DESCRIPTION.md b/packages/app-store/webex/DESCRIPTION.md new file mode 100644 index 0000000000..7a8120560e --- /dev/null +++ b/packages/app-store/webex/DESCRIPTION.md @@ -0,0 +1,9 @@ +--- +items: + - 1.jpeg + - 2.jpeg + - 3.jpeg + - 4.jpeg +--- + +{DESCRIPTION} diff --git a/packages/app-store/webex/README.md b/packages/app-store/webex/README.md new file mode 100644 index 0000000000..f72dc04a23 --- /dev/null +++ b/packages/app-store/webex/README.md @@ -0,0 +1,14 @@ +### Obtaining Webex Client ID and Secret + +1. Create a [Webex](https://www.webex.com/) acount, if you don't already have one. +2. Go to [Webex for Developers](https://developer.webex.com/) and sign into to your Webex account. (Note: If you're creating a new account, create it on [Webex](https://www.webex.com/), not on [Webex for Developers](https://developer.webex.com/)) +3. On the upper right, click the profile icon and go to ["My Webex Apps"](https://developer.webex.com/my-apps) +4. Click on "Create a New App" and select ["Integration"](https://developer.webex.com/my-apps/new/integration) +5. Choose "No" for "Will this use a mobile SDK?" +6. Give your app a name. +7. Upload an icon or choose one of the default icons. +8. Give your app a short description. +9. Set the Redirect URI as `/api/integrations/webex/callback` replacing Cal.com URL with the URI at which your application runs. +10. Select the following scopes: "meeting:schedules_read", "meeting:schedules_write". +11. Click "Add Integration". +12. Copy the Client ID and Client Secret and add these while enabling the app through Settings -> Admin -> Apps interface diff --git a/packages/app-store/webex/api/add.ts b/packages/app-store/webex/api/add.ts new file mode 100644 index 0000000000..3bc0f8a806 --- /dev/null +++ b/packages/app-store/webex/api/add.ts @@ -0,0 +1,39 @@ +import type { NextApiRequest } from "next"; +import { stringify } from "querystring"; + +import { WEBAPP_URL } from "@calcom/lib/constants"; +import { defaultHandler, defaultResponder } from "@calcom/lib/server"; +import prisma from "@calcom/prisma"; + +import config from "../config.json"; +import { getWebexAppKeys } from "../lib/getWebexAppKeys"; + +async function handler(req: NextApiRequest) { + // Get user + await prisma.user.findFirstOrThrow({ + where: { + id: req.session?.user?.id, + }, + select: { + id: true, + }, + }); + + const { client_id } = await getWebexAppKeys(); + + /** @link https://developer.webex.com/docs/integrations#requesting-permission */ + const params = { + response_type: "code", + client_id, + redirect_uri: `${WEBAPP_URL}/api/integrations/${config.slug}/callback`, + scope: "spark:kms meeting:schedules_read meeting:schedules_write", //should be "A space-separated list of scopes being requested by your integration" + state: "", + }; + const query = stringify(params).replaceAll("+", "%20"); + const url = `https://webexapis.com/v1/authorize?${query}`; + return { url }; +} + +export default defaultHandler({ + GET: Promise.resolve({ default: defaultResponder(handler) }), +}); diff --git a/packages/app-store/webex/api/callback.ts b/packages/app-store/webex/api/callback.ts new file mode 100644 index 0000000000..f5ca865423 --- /dev/null +++ b/packages/app-store/webex/api/callback.ts @@ -0,0 +1,99 @@ +import type { NextApiRequest, NextApiResponse } from "next"; + +import { WEBAPP_URL } from "@calcom/lib/constants"; +import prisma from "@calcom/prisma"; + +import getInstalledAppPath from "../../_utils/getInstalledAppPath"; +import config from "../config.json"; +import { getWebexAppKeys } from "../lib/getWebexAppKeys"; + +export default async function handler(req: NextApiRequest, res: NextApiResponse) { + const { code } = req.query; + const { client_id, client_secret } = await getWebexAppKeys(); + + /** @link https://developer.webex.com/docs/integrations#getting-an-access-token **/ + + const redirectUri = encodeURI(`${WEBAPP_URL}/api/integrations/${config.slug}/callback`); + const authHeader = "Basic " + Buffer.from(client_id + ":" + client_secret).toString("base64"); + const result = await fetch( + "https://webexapis.com/v1/access_token?grant_type=authorization_code&client_id" + + client_id + + "&client_secret=" + + client_secret + + "&code=" + + code + + "&redirect_uri=" + + redirectUri, + { + method: "POST", + headers: { + Authorization: authHeader, + "Content-Type": "application/x-www-form-urlencoded", + }, + } + ); + + if (result.status !== 200) { + let errorMessage = "Something is wrong with Webex API"; + try { + const responseBody = await result.json(); + errorMessage = responseBody.error; + } catch (e) {} + + res.status(400).json({ message: errorMessage }); + return; + } + + const responseBody = await result.json(); + + if (responseBody.error) { + res.status(400).json({ message: responseBody.error }); + return; + } + + responseBody.expiry_date = Math.round(Date.now() + responseBody.expires_in * 1000); + delete responseBody.expires_in; + + const userId = req.session?.user.id; + if (!userId) { + return res.status(404).json({ message: "No user found" }); + } + /** + * With this we take care of no duplicate webex key for a single user + * when creating a video room we only do findFirst so the if they have more than 1 + * others get ignored + * */ + const existingCredentialWebexVideo = await prisma.credential.findMany({ + select: { + id: true, + }, + where: { + type: config.type, + userId: req.session?.user.id, + appId: config.slug, + }, + }); + + // Making sure we only delete webex_video + const credentialIdsToDelete = existingCredentialWebexVideo.map((item) => item.id); + if (credentialIdsToDelete.length > 0) { + await prisma.credential.deleteMany({ where: { id: { in: credentialIdsToDelete }, userId } }); + } + + await prisma.user.update({ + where: { + id: req.session?.user.id, + }, + data: { + credentials: { + create: { + type: config.type, + key: responseBody, + appId: config.slug, + }, + }, + }, + }); + + res.redirect(getInstalledAppPath({ variant: config.variant, slug: config.slug })); +} diff --git a/packages/app-store/webex/api/index.ts b/packages/app-store/webex/api/index.ts new file mode 100644 index 0000000000..eb12c1b4ed --- /dev/null +++ b/packages/app-store/webex/api/index.ts @@ -0,0 +1,2 @@ +export { default as add } from "./add"; +export { default as callback } from "./callback"; diff --git a/packages/app-store/webex/config.json b/packages/app-store/webex/config.json new file mode 100644 index 0000000000..c2737bad52 --- /dev/null +++ b/packages/app-store/webex/config.json @@ -0,0 +1,25 @@ +{ + "/*": "Don't modify slug - If required, do it using cli edit command", + "name": "Webex", + "title": "Webex", + "slug": "webex", + "type": "webex_video", + "imageSrc": "/icon.ico", + "logo": "/icon.ico", + "url": "https://cal.com/apps/webex", + "variant": "conferencing", + "categories": ["video"], + "publisher": "Cal.com, Inc.", + "email": "support@cal.com", + "description": "Create meetings with Cisco Webex", + "appData": { + "location": { + "linkType": "dynamic", + "type": "integrations:webex_video", + "label": "Webex" + } + }, + "isTemplate": false, + "__createdUsingCli": true, + "__template": "basic" +} diff --git a/packages/app-store/webex/index.ts b/packages/app-store/webex/index.ts new file mode 100644 index 0000000000..e2e9d7b029 --- /dev/null +++ b/packages/app-store/webex/index.ts @@ -0,0 +1,2 @@ +export * as api from "./api"; +export * as lib from "./lib"; diff --git a/packages/app-store/webex/lib/VideoApiAdapter.ts b/packages/app-store/webex/lib/VideoApiAdapter.ts new file mode 100644 index 0000000000..f7f4729114 --- /dev/null +++ b/packages/app-store/webex/lib/VideoApiAdapter.ts @@ -0,0 +1,304 @@ +import { z } from "zod"; + +import dayjs from "@calcom/dayjs"; +import prisma from "@calcom/prisma"; +import type { Credential } from "@calcom/prisma/client"; +import type { CalendarEvent } from "@calcom/types/Calendar"; +import type { CredentialPayload } from "@calcom/types/Credential"; +import type { PartialReference } from "@calcom/types/EventManager"; +import type { VideoApiAdapter, VideoCallData } from "@calcom/types/VideoApiAdapter"; + +import { getWebexAppKeys } from "./getWebexAppKeys"; + +/** @link https://developer.webex.com/docs/meetings **/ +const webexEventResultSchema = z.object({ + id: z.string(), + webLink: z.string(), + siteUrl: z.string(), + password: z.string().optional().default(""), +}); + +export type WebexEventResult = z.infer; + +/** @link https://developer.webex.com/docs/api/v1/meetings/create-a-meeting */ +export const webexMeetingSchema = z.object({ + start: z.date(), + end: z.date(), +}); + +/** @link https://developer.webex.com/docs/api/v1/meetings/list-meetings */ +export const webexMeetingsSchema = z.object({ + items: z.array(webexMeetingSchema), +}); + +/** @link https://developer.webex.com/docs/integrations#getting-an-access-token */ +const webexTokenSchema = z.object({ + scope: z.literal("spark:kms meeting:schedules_read meeting:schedules_write"), + token_type: z.literal("Bearer"), + access_token: z.string(), + expires_in: z.number().optional(), + refresh_token: z.string(), + refresh_token_expires_in: z.number(), + expiry_date: z.number(), +}); +type WebexToken = z.infer; +const isTokenValid = (token: WebexToken) => token.expiry_date < Date.now(); + +/** @link https://developer.webex.com/docs/integrations#using-the-refresh-token */ +const webexRefreshedTokenSchema = z.object({ + scope: z.literal("spark:kms meeting:schedules_read meeting:schedules_write"), + token_type: z.literal("Bearer"), + access_token: z.string(), + expires_in: z.number().optional(), + refresh_token: z.string(), + refresh_token_expires_in: z.number(), +}); + +const webexAuth = (credential: CredentialPayload) => { + const refreshAccessToken = async (refreshToken: string) => { + const { client_id, client_secret } = await getWebexAppKeys(); + + const response = await fetch("https://webexapis.com/v1/access_token", { + method: "POST", + headers: { + "Content-type": "application/x-www-form-urlencoded", + }, + body: new URLSearchParams({ + grant_type: "refresh_token", + client_id: client_id, + client_secret: client_secret, + refresh_token: refreshToken, + }), + }); + + const responseBody = await handleWebexResponse(response, credential.id); + + if (responseBody.error) { + if (responseBody.error === "invalid_grant") { + return Promise.reject(new Error("Invalid grant for Cal.com webex app")); + } + } + // We check the if the new credentials matches the expected response structure + const parsedToken = webexRefreshedTokenSchema.safeParse(responseBody); + if (!parsedToken.success) { + return Promise.reject(new Error("Invalid refreshed tokens were returned")); + } + const newTokens = parsedToken.data; + const oldCredential = await prisma.credential.findUniqueOrThrow({ where: { id: credential.id } }); + const parsedKey = webexTokenSchema.safeParse(oldCredential.key); + if (!parsedKey.success) { + return Promise.reject(new Error("Invalid credentials were saved in the DB")); + } + + const key = parsedKey.data; + key.access_token = newTokens.access_token; + key.refresh_token = newTokens.refresh_token; + // set expiry date as offset from current time. + if (newTokens.expires_in) { + key.expiry_date = Math.round(Date.now() + newTokens.expires_in * 1000); + } + // Store new tokens in database. + await prisma.credential.update({ where: { id: credential.id }, data: { key } }); + return newTokens.access_token; + }; + return { + getToken: async () => { + let credentialKey: WebexToken | null = null; + try { + credentialKey = webexTokenSchema.parse(credential.key); + } catch (error) { + return Promise.reject("Webex credential keys parsing error"); + } + + return !isTokenValid(credentialKey) + ? Promise.resolve(credentialKey.access_token) + : refreshAccessToken(credentialKey.refresh_token); + }, + }; +}; + +const WebexVideoApiAdapter = (credential: CredentialPayload): VideoApiAdapter => { + //TODO implement translateEvent for recurring events + const translateEvent = (event: CalendarEvent) => { + //To convert the Cal's CalendarEvent type to a webex meeting type + /** @link https://developer.webex.com/docs/api/v1/meetings/create-a-meeting */ + //Required params - title, start, end + + return { + title: event.title, + start: dayjs(event.startTime).utc().format(), + end: dayjs(event.endTime).utc().format(), + recurrence: event.recurrence, //Follows RFC 2445 https://www.ietf.org/rfc/rfc2445.txt, TODO check if needs conversion + // timezone: event.organizer.timeZone, // Comment this out for now + agenda: event.description, + enableJoinBeforeHost: true, //this is true in zoom's api, do we need it here? + invitees: event.attendees.map((attendee) => ({ + email: attendee.email, + })), + sendEmail: true, + }; + }; + + const fetchWebexApi = async (endpoint: string, options?: RequestInit) => { + const auth = webexAuth(credential); + const accessToken = await auth.getToken(); + console.log("result of accessToken in fetchWebexApi", accessToken); + console.log("createMeeting options in fetchWebexApi", options); + const response = await fetch(`https://webexapis.com/v1/${endpoint}`, { + method: "GET", + ...options, + headers: { + Authorization: "Bearer " + accessToken, + ...options?.headers, + }, + }); + const responseBody = await handleWebexResponse(response, credential.id); + return responseBody; + }; + + return { + getAvailability: async () => { + try { + const responseBody = await fetchWebexApi("meetings"); + + const data = webexMeetingsSchema.passthrough().parse(responseBody); + return data.items.map((meeting) => ({ + start: meeting.start, + end: meeting.end, + })); + } catch (err) { + console.error(err); + + return []; + } + }, + createMeeting: async (event: CalendarEvent): Promise => { + /** @link https://developer.webex.com/docs/api/v1/meetings/create-a-meeting */ + try { + console.log("Creating meeting", event); + console.log("meting body", translateEvent(event)); + console.log("request body in createMeeting", JSON.stringify(translateEvent(event))); + const response = await fetchWebexApi("meetings", { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify(translateEvent(event)), + }); + console.log("Webex create meeting response", response); + if (response.error) { + if (response.error === "invalid_grant") { + await invalidateCredential(credential.id); + return Promise.reject(new Error("Invalid grant for Cal.com webex app")); + } + } + + const result = webexEventResultSchema.parse(response); + if (result.id && result.webLink) { + return { + type: "webex_video", + id: result.id.toString(), + password: result.password || "", + url: result.webLink, + }; + } + throw new Error("Failed to create meeting. Response is " + JSON.stringify(result)); + } catch (err) { + console.error(err); + throw new Error("Unexpected error"); + } + }, + deleteMeeting: async (uid: string): Promise => { + /** @link https://developer.webex.com/docs/api/v1/meetings/delete-a-meeting */ + try { + const response = await fetchWebexApi(`meetings/${uid}`, { + method: "DELETE", + }); + console.log("Webex delete meeting response", response); + if (response.error) { + if (response.error === "invalid_grant") { + await invalidateCredential(credential.id); + return Promise.reject(new Error("Invalid grant for Cal.com webex app")); + } + } + return Promise.resolve(); + } catch (err) { + return Promise.reject(new Error("Failed to delete meeting")); + } + }, + updateMeeting: async (bookingRef: PartialReference, event: CalendarEvent): Promise => { + /** @link https://developer.webex.com/docs/api/v1/meetings/update-a-meeting */ + try { + const response = await fetchWebexApi(`meetings/${bookingRef.uid}`, { + method: "PATCH", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify(translateEvent(event)), + }); + if (response.error) { + if (response.error === "invalid_grant") { + await invalidateCredential(credential.id); + return Promise.reject(new Error("Invalid grant for Cal.com webex app")); + } + } + + const result = webexEventResultSchema.parse(response); + if (result.id && result.webLink) { + return { + type: "webex_video", + id: bookingRef.meetingId as string, + password: result.password || "", + url: result.webLink, + }; + } + throw new Error("Failed to create meeting. Response is " + JSON.stringify(result)); + } catch (err) { + console.error(err); + throw new Error("Unexpected error"); + } + }, + }; +}; + +const handleWebexResponse = async (response: Response, credentialId: Credential["id"]) => { + let _response = response.clone(); + const responseClone = response.clone(); + if (_response.headers.get("content-encoding") === "gzip") { + const responseString = await response.text(); + _response = JSON.parse(responseString); + } + if (!response.ok || (response.status < 200 && response.status >= 300)) { + const responseBody = await _response.json(); + + if ((response && response.status === 124) || responseBody.error === "invalid_grant") { + await invalidateCredential(credentialId); + } + throw Error(response.statusText); + } + // handle 204 response code with empty response (causes crash otherwise as "" is invalid JSON) + if (response.status === 204) { + return; + } + return responseClone.json(); +}; + +const invalidateCredential = async (credentialId: Credential["id"]) => { + const credential = await prisma.credential.findUnique({ + where: { + id: credentialId, + }, + }); + + if (credential) { + await prisma.credential.update({ + where: { + id: credentialId, + }, + data: { + invalid: true, + }, + }); + } +}; +export default WebexVideoApiAdapter; diff --git a/packages/app-store/webex/lib/getWebexAppKeys.ts b/packages/app-store/webex/lib/getWebexAppKeys.ts new file mode 100644 index 0000000000..c76fd6df9f --- /dev/null +++ b/packages/app-store/webex/lib/getWebexAppKeys.ts @@ -0,0 +1,13 @@ +import { z } from "zod"; + +import getAppKeysFromSlug from "../../_utils/getAppKeysFromSlug"; + +const webexAppKeysSchema = z.object({ + client_id: z.string(), + client_secret: z.string(), +}); + +export const getWebexAppKeys = async () => { + const appKeys = await getAppKeysFromSlug("webex"); + return webexAppKeysSchema.parse(appKeys); +}; diff --git a/packages/app-store/webex/lib/index.ts b/packages/app-store/webex/lib/index.ts new file mode 100644 index 0000000000..8dae91448a --- /dev/null +++ b/packages/app-store/webex/lib/index.ts @@ -0,0 +1,2 @@ +export { getWebexAppKeys } from "./getWebexAppKeys"; +export { default as VideoApiAdapter } from "./VideoApiAdapter"; diff --git a/packages/app-store/webex/package.json b/packages/app-store/webex/package.json new file mode 100644 index 0000000000..d441e53448 --- /dev/null +++ b/packages/app-store/webex/package.json @@ -0,0 +1,14 @@ +{ + "$schema": "https://json.schemastore.org/package.json", + "private": true, + "name": "@calcom/webex", + "version": "0.0.0", + "main": "./index.ts", + "dependencies": { + "@calcom/lib": "*" + }, + "devDependencies": { + "@calcom/types": "*" + }, + "description": "Create meetings with Cisco Webex" +} diff --git a/packages/app-store/webex/static/1.jpeg b/packages/app-store/webex/static/1.jpeg new file mode 100644 index 0000000000..14947282da Binary files /dev/null and b/packages/app-store/webex/static/1.jpeg differ diff --git a/packages/app-store/webex/static/2.jpeg b/packages/app-store/webex/static/2.jpeg new file mode 100644 index 0000000000..f26c664426 Binary files /dev/null and b/packages/app-store/webex/static/2.jpeg differ diff --git a/packages/app-store/webex/static/3.jpeg b/packages/app-store/webex/static/3.jpeg new file mode 100644 index 0000000000..cc190ed6a3 Binary files /dev/null and b/packages/app-store/webex/static/3.jpeg differ diff --git a/packages/app-store/webex/static/4.jpeg b/packages/app-store/webex/static/4.jpeg new file mode 100644 index 0000000000..6242781fde Binary files /dev/null and b/packages/app-store/webex/static/4.jpeg differ diff --git a/packages/app-store/webex/static/icon.ico b/packages/app-store/webex/static/icon.ico new file mode 100644 index 0000000000..2fd572c67e Binary files /dev/null and b/packages/app-store/webex/static/icon.ico differ diff --git a/packages/app-store/webex/zod.ts b/packages/app-store/webex/zod.ts new file mode 100644 index 0000000000..8523421dce --- /dev/null +++ b/packages/app-store/webex/zod.ts @@ -0,0 +1,8 @@ +import { z } from "zod"; + +export const appDataSchema = z.object({}); + +export const appKeysSchema = z.object({ + client_id: z.string().min(64), + client_secret: z.string().min(64), +}); diff --git a/packages/app-store/zoomvideo/lib/VideoApiAdapter.ts b/packages/app-store/zoomvideo/lib/VideoApiAdapter.ts index 2b12f84969..ef227596c7 100644 --- a/packages/app-store/zoomvideo/lib/VideoApiAdapter.ts +++ b/packages/app-store/zoomvideo/lib/VideoApiAdapter.ts @@ -20,7 +20,7 @@ const zoomEventResultSchema = z.object({ export type ZoomEventResult = z.infer; -// @TODO: add link to the docs +/** @link https://marketplace.zoom.us/docs/api-reference/zoom-api/methods/#operation/meetings */ export const zoomMeetingsSchema = z.object({ next_page_token: z.string(), page_count: z.number(), diff --git a/packages/config/package.json b/packages/config/package.json index 77ceef3747..e7561183df 100644 --- a/packages/config/package.json +++ b/packages/config/package.json @@ -1,6 +1,7 @@ { "name": "@calcom/config", "sideEffects": false, + "private": true, "version": "0.0.0", "main": "index.js", "license": "MIT", diff --git a/packages/core/videoClient.ts b/packages/core/videoClient.ts index 91c48dff89..244354967d 100644 --- a/packages/core/videoClient.ts +++ b/packages/core/videoClient.ts @@ -142,6 +142,7 @@ const updateMeeting = async ( const deleteMeeting = async (credential: CredentialPayload, uid: string): Promise => { if (credential) { const videoAdapter = (await getVideoAdapters([credential]))[0]; + logger.debug("videoAdapter inside deleteMeeting", { credential, uid }); // There are certain video apps with no video adapter defined. e.g. riverby,whereby if (videoAdapter) { return videoAdapter.deleteMeeting(uid); diff --git a/packages/dayjs/package.json b/packages/dayjs/package.json index 77a9a60dcd..8aa9fa6602 100644 --- a/packages/dayjs/package.json +++ b/packages/dayjs/package.json @@ -1,6 +1,7 @@ { "name": "@calcom/dayjs", "description": "Cal.com's Day.js shared library", + "private": true, "version": "1.0.0", "main": "./index.ts", "dependencies": { diff --git a/packages/emails/src/templates/TeamInviteEmail.tsx b/packages/emails/src/templates/TeamInviteEmail.tsx index a99b805c51..fa0a288a97 100644 --- a/packages/emails/src/templates/TeamInviteEmail.tsx +++ b/packages/emails/src/templates/TeamInviteEmail.tsx @@ -49,11 +49,17 @@ export const TeamInviteEmail = ( marginTop: "32px", lineHeightStep: "24px", }}> - <>{props.language("email_no_user_invite_subheading", { invitedBy: props.from, appName: APP_NAME })} + <> + {props.language("email_user_invite_subheading", { + invitedBy: props.from, + appName: APP_NAME, + teamName: props.teamName, + })} +

    diff --git a/packages/embeds/README.md b/packages/embeds/README.md index 084336b38b..fd486535d3 100644 --- a/packages/embeds/README.md +++ b/packages/embeds/README.md @@ -6,3 +6,11 @@ This folder contains all the various flavours of embeds. `snippet` contains the Vanilla JS Code Snippet that can be installed on any website and would automatically fetch the `core` library. Please see the respective folder READMEs for details on them. + +## Publishing to NPM - Soon to be automated using changesets github action +To publish the packages. Following steps should be followed. All commands are to be run at the root. + +1. `yarn changeset` -> Creates changelog files and adds summary to changelog. Select embed packages only here. +2. `yarn changeset version` -> Bumps the versions as required +3. Get the PR reviewed and merged +4. `yarn publish-embed` -> Releases all packages. We can't use `yarn changeset publish` because it doesn't support workspace: prefix removal yet. See https://github.com/changesets/changesets/issues/432#issuecomment-1016365428 \ No newline at end of file diff --git a/packages/embeds/embed-core/CHANGELOG.md b/packages/embeds/embed-core/CHANGELOG.md new file mode 100644 index 0000000000..2055b3019b --- /dev/null +++ b/packages/embeds/embed-core/CHANGELOG.md @@ -0,0 +1,7 @@ +# @calcom/embed-core + +## 1.1.5 + +### Patch Changes + +- Add changesets. Use prepack instead of prePublish and prepublish only as that works with both yarn and npm diff --git a/packages/embeds/embed-core/package.json b/packages/embeds/embed-core/package.json index 948bd273b9..d182a37faa 100644 --- a/packages/embeds/embed-core/package.json +++ b/packages/embeds/embed-core/package.json @@ -1,6 +1,6 @@ { "name": "@calcom/embed-core", - "version": "1.1.3", + "version": "1.1.5", "description": "This is the vanilla JS core script that embeds Cal Link", "main": "./dist/embed/embed.js", "types": "./dist/index.d.ts", @@ -29,8 +29,7 @@ "embed-tests": "yarn playwright test --config=playwright/config/playwright.config.ts", "embed-tests-quick": "QUICK=true yarn embed-tests", "embed-tests-update-snapshots:ci": "yarn embed-tests-quick --update-snapshots", - "prepare": "NEXT_PUBLIC_WEBAPP_URL='https://app.cal.com' yarn build", - "publish-prod:patch": "yarn publish --access public --patch", + "prepack": "NEXT_PUBLIC_WEBAPP_URL='https://app.cal.com' yarn build", "clean": "rm -rf .turbo && rm -rf node_modules && rm -rf ../../../apps/web/public/embed" }, "files": [ diff --git a/packages/embeds/embed-react/CHANGELOG.md b/packages/embeds/embed-react/CHANGELOG.md new file mode 100644 index 0000000000..161790a99a --- /dev/null +++ b/packages/embeds/embed-react/CHANGELOG.md @@ -0,0 +1,10 @@ +# @calcom/embed-react + +## 1.0.12 + +### Patch Changes + +- Add changesets. Use prepack instead of prePublish and prepublish only as that works with both yarn and npm +- Updated dependencies + - @calcom/embed-snippet@1.0.7 + - @calcom/embed-core@1.1.5 diff --git a/packages/embeds/embed-react/package.json b/packages/embeds/embed-react/package.json index 0c5c240917..6424ed38c1 100644 --- a/packages/embeds/embed-react/package.json +++ b/packages/embeds/embed-react/package.json @@ -1,7 +1,7 @@ { "name": "@calcom/embed-react", "sideEffects": false, - "version": "1.0.10", + "version": "1.0.12", "description": "Embed Cal Link as a React Component", "license": "SEE LICENSE IN LICENSE", "repository": { @@ -20,9 +20,9 @@ "embed-tests": "yarn playwright test --config=./playwright/config/playwright.config.ts", "embed-tests-quick": "QUICK=true yarn embed-tests", "embed-tests-update-snapshots:ci": "yarn embed-tests-quick --update-snapshots", + "prepack": "NEXT_PUBLIC_EMBED_LIB_URL='https://app.cal.com/embed/embed.js' NEXT_PUBLIC_WEBAPP_URL='https://app.cal.com' yarn build", "embed-web-start": "yarn workspace @calcom/web start", "embed-dev": "yarn workspace @calcom/embed-react dev", - "publish-prod:patch": "yarn publish --access public --patch", "clean": "rm -rf .turbo && rm -rf node_modules && rm -rf dist" }, "main": "./dist/Cal.umd.js", diff --git a/packages/embeds/embed-snippet/CHANGELOG.md b/packages/embeds/embed-snippet/CHANGELOG.md new file mode 100644 index 0000000000..c43c154aaf --- /dev/null +++ b/packages/embeds/embed-snippet/CHANGELOG.md @@ -0,0 +1,9 @@ +# @calcom/embed-snippet + +## 1.0.7 + +### Patch Changes + +- Add changesets. Use prepack instead of prePublish and prepublish only as that works with both yarn and npm +- Updated dependencies + - @calcom/embed-core@1.1.5 diff --git a/packages/embeds/embed-snippet/package.json b/packages/embeds/embed-snippet/package.json index 57096b0199..61ed67a5f4 100644 --- a/packages/embeds/embed-snippet/package.json +++ b/packages/embeds/embed-snippet/package.json @@ -1,7 +1,7 @@ { "name": "@calcom/embed-snippet", "sideEffects": false, - "version": "1.0.5", + "version": "1.0.7", "main": "./dist/snippet.umd.js", "module": "./dist/snippet.es.js", "description": "Vanilla JS embed snippet that is responsible to fetch @calcom/embed-core and thus show Cal Link as an embed on a page.", @@ -16,8 +16,7 @@ "type-check": "tsc --pretty --noEmit", "type-check:ci": "tsc-absolute --pretty --noEmit", "lint": "eslint --ext .ts,.js src", - "prepare": "NEXT_PUBLIC_EMBED_LIB_URL='https://app.cal.com/embed/embed.js' NEXT_PUBLIC_WEBAPP_URL='https://app.cal.com' yarn build", - "publish-prod:patch": "yarn publish --access public --patch", + "prepack": "NEXT_PUBLIC_EMBED_LIB_URL='https://app.cal.com/embed/embed.js' NEXT_PUBLIC_WEBAPP_URL='https://app.cal.com' yarn build", "clean": "rm -rf .turbo && rm -rf node_modules && rm -rf dist" }, "files": [ diff --git a/packages/eslint-plugin/package.json b/packages/eslint-plugin/package.json index dadb76eb88..211d191b5d 100644 --- a/packages/eslint-plugin/package.json +++ b/packages/eslint-plugin/package.json @@ -1,6 +1,7 @@ { "name": "@calcom/eslint-plugin-eslint", "sideEffects": false, + "private": true, "version": "0.1.0", "main": "./src/index.js", "dependencies": { diff --git a/packages/features/auth/package.json b/packages/features/auth/package.json index e4a1262564..5948ed54b8 100644 --- a/packages/features/auth/package.json +++ b/packages/features/auth/package.json @@ -1,6 +1,7 @@ { "name": "@calcom/feature-auth", "sideEffects": false, + "private": true, "description": "Cal.com's main auth code", "authors": "Cal.com, Inc.", "version": "1.0.0", diff --git a/packages/features/bookings/Booker/Booker.tsx b/packages/features/bookings/Booker/Booker.tsx index 3a88bff122..796129c920 100644 --- a/packages/features/bookings/Booker/Booker.tsx +++ b/packages/features/bookings/Booker/Booker.tsx @@ -19,7 +19,7 @@ import { EventMeta } from "./components/EventMeta"; import { LargeCalendar } from "./components/LargeCalendar"; import { LargeViewHeader } from "./components/LargeViewHeader"; import { BookerSection } from "./components/Section"; -import { fadeInLeft, useBookerResizeAnimation } from "./config"; +import { fadeInLeft, getBookerSizeClassNames, useBookerResizeAnimation } from "./config"; import { useBookerStore, useInitializeBookerStore } from "./store"; import type { BookerLayout, BookerProps } from "./types"; import { useEvent } from "./utils/event"; @@ -128,11 +128,8 @@ const BookerComponent = ({ username, eventSlug, month, rescheduleBooking }: Book
    diff --git a/packages/features/bookings/Booker/components/BookEventForm/BookEventForm.tsx b/packages/features/bookings/Booker/components/BookEventForm/BookEventForm.tsx index 507aecffa9..bd0917e4e0 100644 --- a/packages/features/bookings/Booker/components/BookEventForm/BookEventForm.tsx +++ b/packages/features/bookings/Booker/components/BookEventForm/BookEventForm.tsx @@ -2,7 +2,7 @@ import { zodResolver } from "@hookform/resolvers/zod"; import type { UseMutationResult } from "@tanstack/react-query"; import { useMutation } from "@tanstack/react-query"; import { useRouter } from "next/router"; -import { useMemo } from "react"; +import { useMemo, useRef } from "react"; import type { FieldError } from "react-hook-form"; import { useForm } from "react-hook-form"; import type { TFunction } from "react-i18next"; @@ -61,6 +61,8 @@ export const BookEventForm = ({ onCancel }: BookEventFormProps) => { const router = useRouter(); const { t, i18n } = useLocale(); const { timezone } = useTimePreferences(); + const errorRef = useRef(null); + const rescheduleUid = useBookerStore((state) => state.rescheduleUid); const rescheduleBooking = useBookerStore((state) => state.rescheduleBooking); const eventSlug = useBookerStore((state) => state.eventSlug); @@ -185,6 +187,9 @@ export const BookEventForm = ({ onCancel }: BookEventFormProps) => { }) ); }, + onError: () => { + errorRef && errorRef.current?.scrollIntoView({ behavior: "smooth" }); + }, }); const createRecurringBookingMutation = useMutation(createRecurringBooking, { @@ -282,8 +287,25 @@ export const BookEventForm = ({ onCancel }: BookEventFormProps) => { locations={eventType.locations} rescheduleUid={rescheduleUid || undefined} /> - -
    + {(createBookingMutation.isError || + createRecurringBookingMutation.isError || + bookingForm.formState.errors["globalError"]) && ( +
    + +
    + )} +
    {!!onCancel && (
    - {(createBookingMutation.isError || - createRecurringBookingMutation.isError || - bookingForm.formState.errors["globalError"]) && ( -
    - -
    - )}
    ); }; diff --git a/packages/features/bookings/Booker/components/BookEventForm/BookFormAsModal.tsx b/packages/features/bookings/Booker/components/BookEventForm/BookFormAsModal.tsx index 0165803cb0..f220be657b 100644 --- a/packages/features/bookings/Booker/components/BookEventForm/BookFormAsModal.tsx +++ b/packages/features/bookings/Booker/components/BookEventForm/BookFormAsModal.tsx @@ -20,9 +20,9 @@ export function BookFormAsModal({ visible, onCancel }: { visible: boolean; onCan -

    {t("confirm_your_details")}

    -
    + className="[&_.modalsticky]:border-t-subtle [&_.modalsticky]:bg-default max-h-[80vh] pt-6 pb-0 [&_.modalsticky]:sticky [&_.modalsticky]:bottom-0 [&_.modalsticky]:left-0 [&_.modalsticky]:right-0 [&_.modalsticky]:-mx-8 [&_.modalsticky]:border-t [&_.modalsticky]:px-6 [&_.modalsticky]:py-4"> +

    {t("confirm_your_details")}

    +
    {parsedSelectedTimeslot.format("LLL")} diff --git a/packages/features/bookings/Booker/components/EventMeta.tsx b/packages/features/bookings/Booker/components/EventMeta.tsx index 4b896df51e..71315cc028 100644 --- a/packages/features/bookings/Booker/components/EventMeta.tsx +++ b/packages/features/bookings/Booker/components/EventMeta.tsx @@ -66,18 +66,20 @@ export const EventMeta = () => { {bookerState === "booking" ? ( <>{timezone} ) : ( - + "!min-h-0 p-0 border-0 bg-transparent focus-within:ring-0", + control: () => "!min-h-0 p-0 w-full border-0 bg-transparent focus-within:ring-0", menu: () => "!w-64 max-w-[90vw]", singleValue: () => "text-text py-1", + indicatorsContainer: () => "ml-auto", + container: () => "max-w-full", }} value={timezone} onChange={(tz) => setTimezone(tz.value)} diff --git a/packages/features/bookings/Booker/config.ts b/packages/features/bookings/Booker/config.ts index 36ba429ffd..171d5bddbe 100644 --- a/packages/features/bookings/Booker/config.ts +++ b/packages/features/bookings/Booker/config.ts @@ -90,6 +90,28 @@ export const resizeAnimationConfig: ResizeAnimationConfig = { }, }; +export const getBookerSizeClassNames = (layout: BookerLayout, bookerState: BookerState) => { + return [ + // Size settings are abstracted on their own lines purely for readbility. + // General sizes, used always + "[--booker-timeslots-width:240px] lg:[--booker-timeslots-width:280px]", + // Small calendar defaults + layout === "small_calendar" && "[--booker-meta-width:240px]", + // Meta column get's wider in booking view to fit the full date on a single row in case + // of a multi occurance event. Also makes form less wide, which also looks better. + layout === "small_calendar" && + bookerState === "booking" && + "[--booker-main-width:420px] lg:[--booker-meta-width:340px]", + // Smaller meta when not in booking view. + layout === "small_calendar" && + bookerState !== "booking" && + "[--booker-main-width:480px] lg:[--booker-meta-width:280px]", + // Fullscreen view settings. + layout !== "small_calendar" && + "[--booker-main-width:480px] [--booker-meta-width:340px] lg:[--booker-meta-width:424px]", + ]; +}; + /** * This hook returns a ref that should be set on the booker element. * Based on that ref this hook animates the size of the booker element with framer motion. diff --git a/packages/features/bookings/components/AvailableTimes.tsx b/packages/features/bookings/components/AvailableTimes.tsx index 6532493c48..6d8a106638 100644 --- a/packages/features/bookings/components/AvailableTimes.tsx +++ b/packages/features/bookings/components/AvailableTimes.tsx @@ -39,7 +39,7 @@ export const AvailableTimes = ({ return (
    -
    +
    {nameOfDay(i18n.language, Number(date.format("d")), "short")} @@ -54,7 +54,7 @@ export const AvailableTimes = ({ {showTimeformatToggle && ( -
    +
    )} diff --git a/packages/features/bookings/components/BookerSeo.tsx b/packages/features/bookings/components/BookerSeo.tsx new file mode 100644 index 0000000000..ac2bcd1a11 --- /dev/null +++ b/packages/features/bookings/components/BookerSeo.tsx @@ -0,0 +1,44 @@ +import { useLocale } from "@calcom/lib/hooks/useLocale"; +import { trpc } from "@calcom/trpc/react"; +import { HeadSeo } from "@calcom/ui"; + +interface BookerSeoProps { + username: string; + eventSlug: string; + rescheduleUid: string | undefined; + hideBranding?: boolean; +} + +export const BookerSeo = (props: BookerSeoProps) => { + const { eventSlug, username, rescheduleUid, hideBranding } = props; + const { t } = useLocale(); + const { data: event } = trpc.viewer.public.event.useQuery( + { username, eventSlug }, + { refetchOnWindowFocus: false } + ); + + const profileName = event?.profile?.name ?? ""; + const profileImage = event?.profile?.image; + const title = event?.title ?? ""; + return ( + ({ + name: `${user.name}`, + username: `${user.username}`, + })), + ], + }} + nextSeoProps={{ + nofollow: event?.hidden, + noindex: event?.hidden, + }} + isBrandingHidden={hideBranding} + /> + ); +}; diff --git a/packages/features/bookings/components/event-meta/Occurences.tsx b/packages/features/bookings/components/event-meta/Occurences.tsx index f7b9b72c18..2dbf758e8b 100644 --- a/packages/features/bookings/components/event-meta/Occurences.tsx +++ b/packages/features/bookings/components/event-meta/Occurences.tsx @@ -2,17 +2,23 @@ import { useEffect } from "react"; import { useBookerStore } from "@calcom/features/bookings/Booker/store"; import { useLocale } from "@calcom/lib/hooks/useLocale"; +import { parseRecurringDates } from "@calcom/lib/parse-dates"; import { getRecurringFreq } from "@calcom/lib/recurringStrings"; +import { Tooltip } from "@calcom/ui"; import { Input } from "@calcom/ui"; +import { useTimePreferences } from "../../lib"; import type { PublicEvent } from "../../types"; export const EventOccurences = ({ event }: { event: PublicEvent }) => { - const { t } = useLocale(); + const { t, i18n } = useLocale(); const [setRecurringEventCount, recurringEventCount] = useBookerStore((state) => [ state.setRecurringEventCount, state.recurringEventCount, ]); + const selectedTimeslot = useBookerStore((state) => state.selectedTimeslot); + const bookerState = useBookerStore((state) => state.state); + const { timezone, timeFormat } = useTimePreferences(); // Set initial value in booker store. useEffect(() => { @@ -22,6 +28,32 @@ export const EventOccurences = ({ event }: { event: PublicEvent }) => { if (!event.recurringEvent) return null; + if (bookerState === "booking" && recurringEventCount && selectedTimeslot) { + const [recurringStrings] = parseRecurringDates( + { + startDate: selectedTimeslot, + timeZone: timezone, + recurringEvent: event.recurringEvent, + recurringCount: recurringEventCount, + selectedTimeFormat: timeFormat, + }, + i18n.language + ); + return ( + <> + {recurringStrings.slice(0, 5).map((timeFormatted, key) => ( +

    {timeFormatted}

    + ))} + ( +

    {timeFormatted}

    + ))}> +

    + {t("plus_more", { count: recurringStrings.length - 5 })}

    +
    + + ); + } + return ( <> {getRecurringFreq({ t, recurringEvent: event.recurringEvent })} diff --git a/packages/features/bookings/lib/handleCancelBooking.ts b/packages/features/bookings/lib/handleCancelBooking.ts index cc345d433f..5e631cc20f 100644 --- a/packages/features/bookings/lib/handleCancelBooking.ts +++ b/packages/features/bookings/lib/handleCancelBooking.ts @@ -18,6 +18,7 @@ import type { EventTypeInfo } from "@calcom/features/webhooks/lib/sendPayload"; import sendPayload from "@calcom/features/webhooks/lib/sendPayload"; import { isPrismaObjOrUndefined, parseRecurringEvent } from "@calcom/lib"; import { HttpError } from "@calcom/lib/http-error"; +import logger from "@calcom/lib/logger"; import { handleRefundError } from "@calcom/lib/payment/handleRefundError"; import { getTranslation } from "@calcom/lib/server/i18n"; import prisma, { bookingMinimalSelect } from "@calcom/prisma"; @@ -503,6 +504,7 @@ async function handler(req: CustomRequest) { ); if (videoCredential) { + logger.debug("videoCredential inside cancel booking handler", videoCredential); apiDeletes.push(deleteMeeting(videoCredential, uid)); } } diff --git a/packages/features/bookings/lib/handleNewBooking.ts b/packages/features/bookings/lib/handleNewBooking.ts index 2a55ab7c3a..733fe573f3 100644 --- a/packages/features/bookings/lib/handleNewBooking.ts +++ b/packages/features/bookings/lib/handleNewBooking.ts @@ -1940,7 +1940,11 @@ async function handler( originalEvent: results[0].originalEvent, }; - const googleCalResult = results.find((result) => result.type === "google_calendar"); + // Find index of google_calendar inside createManager.referencesToCreate + const googleCalIndex = createManager.referencesToCreate.findIndex( + (ref) => ref.type === "google_calendar" + ); + const googleCalResult = results[googleCalIndex]; if (!googleCalResult) { results.push({ @@ -1955,6 +1959,20 @@ async function handler( ...googleMeetResult, success: true, }); + + // Add google_meet to referencesToCreate in the same index as google_calendar + createManager.referencesToCreate[googleCalIndex] = { + ...createManager.referencesToCreate[googleCalIndex], + meetingUrl: googleCalResult.createdEvent.hangoutLink, + }; + + // Also create a new referenceToCreate with type video for google_meet + createManager.referencesToCreate.push({ + type: "google_meet_video", + meetingUrl: googleCalResult.createdEvent.hangoutLink, + uid: googleCalResult.uid, + credentialId: createManager.referencesToCreate[googleCalIndex].credentialId, + }); } else if (googleCalResult && !googleCalResult.createdEvent?.hangoutLink) { results.push({ ...googleMeetResult, diff --git a/packages/features/calendars/CalendarSwitch.tsx b/packages/features/calendars/CalendarSwitch.tsx index 75ec99fa5e..bce2cd7b24 100644 --- a/packages/features/calendars/CalendarSwitch.tsx +++ b/packages/features/calendars/CalendarSwitch.tsx @@ -72,7 +72,7 @@ const CalendarSwitch = (props: ICalendarSwitchProps) => { } ); return ( -
    +
    !previousUserIds?.includes(id)); const oldUserIds = currentUserIds?.filter((id) => previousUserIds?.includes(id)); + // Calculate if there are new workflows for which assigned members will get too + const currentWorkflowIds = eventType.workflows?.map((wf) => wf.workflowId); + // Define hashedLink query input const hashedLinkQuery = (userId: number) => { return hashedLink @@ -190,13 +194,11 @@ export default async function handleChildrenEventTypes({ }, parentId, hidden: children?.find((ch) => ch.owner.id === userId)?.hidden ?? false, - // Reserved for v2 - /* - workflows: eventType.workflows && { - createMany: { - data: eventType.workflows?.map((wf) => ({ ...wf, eventTypeId: undefined })), - }, + workflows: currentWorkflowIds && { + create: currentWorkflowIds.map((wfId) => ({ workflowId: wfId })), }, + // Reserved for future releases + /* webhooks: eventType.webhooks && { createMany: { data: eventType.webhooks?.map((wh) => ({ ...wh, eventTypeId: undefined })), @@ -221,7 +223,7 @@ export default async function handleChildrenEventTypes({ }); // Update event types for old users - await prisma.$transaction( + const oldEventTypes = await prisma.$transaction( oldUserIds.map((userId) => { return prisma.eventType.update({ where: { @@ -246,21 +248,30 @@ export default async function handleChildrenEventTypes({ }) ); - // Reserved for v2 - /*const updatedOldWorkflows = await prisma.workflow.updateMany({ - where: { - userId: { - in: oldUserIds, - }, - }, - data: { - ...eventType.workflows, - }, - }); - console.log( - "handleChildrenEventTypes:updatedOldWorkflows", - JSON.stringify({ updatedOldWorkflows }, null, 2) - ); + if (currentWorkflowIds?.length) { + await prisma.$transaction( + currentWorkflowIds.flatMap((wfId) => { + return oldEventTypes.map((oEvTy) => { + return prisma.workflowsOnEventTypes.upsert({ + create: { + eventTypeId: oEvTy.id, + workflowId: wfId, + }, + update: {}, + where: { + workflowId_eventTypeId: { + eventTypeId: oEvTy.id, + workflowId: wfId, + }, + }, + }); + }); + }) + ); + } + + // Reserved for future releases + /** const updatedOldWebhooks = await prisma.webhook.updateMany({ where: { userId: { diff --git a/packages/features/ee/teams/pages/team-profile-view.tsx b/packages/features/ee/teams/pages/team-profile-view.tsx index 2314fe1001..e9cdeecee7 100644 --- a/packages/features/ee/teams/pages/team-profile-view.tsx +++ b/packages/features/ee/teams/pages/team-profile-view.tsx @@ -264,7 +264,7 @@ const ProfileView = () => { <>
    diff --git a/packages/features/ee/workflows/components/EventWorkflowsTab.tsx b/packages/features/ee/workflows/components/EventWorkflowsTab.tsx index 7c742a43cf..d8b018e71f 100644 --- a/packages/features/ee/workflows/components/EventWorkflowsTab.tsx +++ b/packages/features/ee/workflows/components/EventWorkflowsTab.tsx @@ -23,6 +23,7 @@ type ItemProps = { id: number; title: string; }; + isChildrenManagedEventType: boolean; }; const WorkflowListItem = (props: ItemProps) => { @@ -131,18 +132,33 @@ const WorkflowListItem = (props: ItemProps) => { })}
    -
    - - - -
    - -
    + {!workflow.readOnly && ( +
    + + + +
    + )} + +
    + {workflow.readOnly && props.isChildrenManagedEventType && ( + + )} { activateEventTypeMutation.mutate({ workflowId: workflow.id, eventTypeId: eventType.id }); }} @@ -163,9 +179,14 @@ type Props = { function EventWorkflowsTab(props: Props) { const { workflows, eventType } = props; const { t } = useLocale(); + const { isManagedEventType, isChildrenManagedEventType } = useLockedFieldsManager( + eventType, + t("locked_fields_admin_description"), + t("locked_fields_member_description") + ); const { data, isLoading } = trpc.viewer.workflows.list.useQuery({ teamId: eventType.team?.id, - userId: eventType.userId || undefined, + userId: !isChildrenManagedEventType ? eventType.userId || undefined : undefined, }); const router = useRouter(); const [sortedWorkflows, setSortedWorkflows] = useState>([]); @@ -173,7 +194,11 @@ function EventWorkflowsTab(props: Props) { useEffect(() => { if (data?.workflows) { const activeWorkflows = workflows.map((workflowOnEventType) => { - return workflowOnEventType; + const dataWf = data.workflows.find((wf) => wf.id === workflowOnEventType.id); + return { + ...workflowOnEventType, + readOnly: isChildrenManagedEventType && dataWf?.teamId ? true : dataWf?.readOnly ?? false, + } as WorkflowType; }); const disabledWorkflows = data.workflows.filter( (workflow) => @@ -204,12 +229,6 @@ function EventWorkflowsTab(props: Props) { }, }); - const { isManagedEventType, isChildrenManagedEventType } = useLockedFieldsManager( - eventType, - t("locked_fields_admin_description"), - t("locked_fields_member_description") - ); - return ( {!isLoading ? ( @@ -217,6 +236,7 @@ function EventWorkflowsTab(props: Props) { {isManagedEventType && ( @@ -226,7 +246,12 @@ function EventWorkflowsTab(props: Props) {
    {sortedWorkflows.map((workflow) => { return ( - + ); })}
    @@ -238,19 +263,13 @@ function EventWorkflowsTab(props: Props) { headline={t("workflows")} description={t("no_workflows_description")} buttonRaw={ - isChildrenManagedEventType && !isManagedEventType ? ( - - ) : ( - - ) + } />
    diff --git a/packages/features/ee/workflows/components/TimeTimeUnitInput.tsx b/packages/features/ee/workflows/components/TimeTimeUnitInput.tsx index d5e8e35611..baa797e90a 100644 --- a/packages/features/ee/workflows/components/TimeTimeUnitInput.tsx +++ b/packages/features/ee/workflows/components/TimeTimeUnitInput.tsx @@ -17,6 +17,7 @@ import type { FormValues } from "../pages/workflow"; type Props = { form: UseFormReturn; + disabled: boolean; }; export const TimeTimeUnitInput = (props: Props) => { @@ -33,6 +34,7 @@ export const TimeTimeUnitInput = (props: Props) => { type="number" min="1" label="" + disabled={props.disabled} defaultValue={form.getValues("time") || 24} className="-mt-2 rounded-r-none text-sm focus:ring-0" {...form.register("time", { valueAsNumber: true })} diff --git a/packages/features/ee/workflows/components/WorkflowDetailsPage.tsx b/packages/features/ee/workflows/components/WorkflowDetailsPage.tsx index 6a8bb886eb..85da5b44ce 100644 --- a/packages/features/ee/workflows/components/WorkflowDetailsPage.tsx +++ b/packages/features/ee/workflows/components/WorkflowDetailsPage.tsx @@ -6,7 +6,7 @@ import { Controller } from "react-hook-form"; import { SENDER_ID, SENDER_NAME } from "@calcom/lib/constants"; import { useLocale } from "@calcom/lib/hooks/useLocale"; -import { WorkflowTemplates, SchedulingType } from "@calcom/prisma/enums"; +import { WorkflowTemplates } from "@calcom/prisma/enums"; import type { WorkflowActions } from "@calcom/prisma/enums"; import { trpc } from "@calcom/trpc/react"; import type { MultiSelectCheckboxesOptionType as Option } from "@calcom/ui"; @@ -26,6 +26,7 @@ interface Props { setSelectedEventTypes: Dispatch>; teamId?: number; isMixedEventType: boolean; + readOnly: boolean; } export default function WorkflowDetailsPage(props: Props) { @@ -48,15 +49,10 @@ export default function WorkflowDetailsPage(props: Props) { if (teamId && teamId !== group.teamId) return options; return [ ...options, - ...group.eventTypes - .filter( - (evType) => - !evType.metadata?.managedEventConfig && evType.schedulingType !== SchedulingType.MANAGED - ) - .map((eventType) => ({ - value: String(eventType.id), - label: eventType.title, - })), + ...group.eventTypes.map((eventType) => ({ + value: String(eventType.id), + label: `${eventType.title} ${eventType.children.length ? `(+${eventType.children.length})` : ``}`, + })), ]; }, [] as Option[]) || [], [data] @@ -117,7 +113,12 @@ export default function WorkflowDetailsPage(props: Props) {
    - +
    - + {!props.readOnly && ( + + )}
    @@ -154,7 +158,7 @@ export default function WorkflowDetailsPage(props: Props) {
    {form.getValues("trigger") && (
    - +
    )} {form.getValues("steps") && ( @@ -168,23 +172,28 @@ export default function WorkflowDetailsPage(props: Props) { reload={reload} setReload={setReload} teamId={teamId} + readOnly={props.readOnly} /> ); })} )} -
    - -
    -
    - -
    + {!props.readOnly && ( + <> +
    + +
    +
    + +
    + + )}
    {workflow.activeOn && workflow.activeOn.length > 0 ? ( ( -

    {activeOn.eventType.title}

    - ))}> + content={workflow.activeOn + .filter((wf) => (workflow.teamId ? wf.eventType.parentId === null : true)) + .map((activeOn, key) => ( +

    + {activeOn.eventType.title} + {activeOn.eventType._count.children > 0 + ? ` (+${activeOn.eventType._count.children})` + : ""} +

    + ))}>
    ) : ( diff --git a/packages/features/ee/workflows/components/WorkflowStepContainer.tsx b/packages/features/ee/workflows/components/WorkflowStepContainer.tsx index 4b5e9dce11..7135b6891c 100644 --- a/packages/features/ee/workflows/components/WorkflowStepContainer.tsx +++ b/packages/features/ee/workflows/components/WorkflowStepContainer.tsx @@ -52,6 +52,7 @@ type WorkflowStepProps = { reload?: boolean; setReload?: Dispatch>; teamId?: number; + readOnly: boolean; }; export default function WorkflowStepContainer(props: WorkflowStepProps) { @@ -249,6 +250,7 @@ export default function WorkflowStepContainer(props: WorkflowStepProps) { { if (val) { const oldValue = form.getValues(`steps.${step.stepNumber - 1}.action`); @@ -468,8 +475,9 @@ export default function WorkflowStepContainer(props: WorkflowStepProps) { { const isAlreadyVerified = !!verifiedNumbers @@ -483,9 +491,9 @@ export default function WorkflowStepContainer(props: WorkflowStepProps) { />
    ) : ( - <> -
    - { - setVerificationCode(e.target.value); - }} - required - /> - -
    - {form.formState.errors.steps && - form.formState?.errors?.steps[step.stepNumber - 1]?.sendTo && ( -

    - {form.formState?.errors?.steps[step.stepNumber - 1]?.sendTo?.message || ""} -

    - )} - + !props.readOnly && ( + <> +
    + { + setVerificationCode(e.target.value); + }} + required + /> + +
    + {form.formState.errors.steps && + form.formState?.errors?.steps[step.stepNumber - 1]?.sendTo && ( +

    + {form.formState?.errors?.steps[step.stepNumber - 1]?.sendTo?.message || ""} +

    + )} + + ) )}
    )} @@ -551,6 +562,7 @@ export default function WorkflowStepContainer(props: WorkflowStepProps) { @@ -566,6 +578,7 @@ export default function WorkflowStepContainer(props: WorkflowStepProps) { @@ -580,6 +593,7 @@ export default function WorkflowStepContainer(props: WorkflowStepProps) { control={form.control} render={() => ( @@ -611,6 +626,7 @@ export default function WorkflowStepContainer(props: WorkflowStepProps) {