Compare commits
30
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e6959f3c7c | ||
|
|
67af8b4973 | ||
|
|
7794f2596e | ||
|
|
167cf6e352 | ||
|
|
3d778ba481 | ||
|
|
b8f0000b59 | ||
|
|
618ef63017 | ||
|
|
ae8e633716 | ||
|
|
4a23832388 | ||
|
|
02e486d757 | ||
|
|
af6e24f433 | ||
|
|
f9e24f51a7 | ||
|
|
9e9f815809 | ||
|
|
a91561f3c2 | ||
|
|
ff79064ca7 | ||
|
|
1a573e1619 | ||
|
|
511be7386c | ||
|
|
2a0991c259 | ||
|
|
2e454c9723 | ||
|
|
7e6d94c8e0 | ||
|
|
e18b790ccd | ||
|
|
0274417ad1 | ||
|
|
a8a3a93ace | ||
|
|
88bbab216e | ||
|
|
f74434a282 | ||
|
|
ed6f9d8252 | ||
|
|
a19ccab16a | ||
|
|
77369a13f1 | ||
|
|
8231356d2a | ||
|
|
469a942b5a |
No files matched your search
Vendored
+2
-2
@@ -724,7 +724,7 @@ ${we.map(pe=>`Not found: ${lo(pe)}
|
||||
To understand the dependency tree requiring vulnerable packages, check the raw report with the \`--json\` flag or use \`yarn why <package>\` to get more information as to who depends on them.
|
||||
`,examples:[["Checks for known security issues with the installed packages. The output is a list of known issues.","yarn npm audit"],["Audit dependencies in all workspaces","yarn npm audit --all"],["Limit auditing to `dependencies` (excludes `devDependencies`)","yarn npm audit --environment production"],["Show audit report as valid JSON","yarn npm audit --json"],["Audit all direct and transitive dependencies","yarn npm audit --recursive"],["Output moderate (or more severe) vulnerabilities","yarn npm audit --severity moderate"],["Exclude certain packages","yarn npm audit --exclude package1 --exclude package2"],["Ignore specific advisories","yarn npm audit --ignore 1234567 --ignore 7654321"]]});var TM=Pe(Xr()),OM=J("util"),Yu=class extends De{constructor(){super(...arguments);this.fields=z.String("-f,--fields",{description:"A comma-separated list of manifest fields that should be displayed"});this.json=z.Boolean("--json",!1,{description:"Format the output as an NDJSON stream"});this.packages=z.Rest()}async execute(){let t=await ye.find(this.context.cwd,this.context.plugins),{project:i}=await je.find(t,this.context.cwd),n=typeof this.fields<"u"?new Set(["name",...this.fields.split(/\s*,\s*/)]):null,s=[],o=!1,a=await Ge.start({configuration:t,includeFooter:!1,json:this.json,stdout:this.context.stdout},async l=>{for(let c of this.packages){let u;if(c==="."){let L=i.topLevelWorkspace;if(!L.manifest.name)throw new be(`Missing ${ee.pretty(t,"name",ee.Type.CODE)} field in ${K.fromPortablePath(x.join(L.cwd,xt.manifest))}`);u=P.makeDescriptor(L.manifest.name,"unknown")}else u=P.parseDescriptor(c);let g=Mt.getIdentUrl(u),f=MM(await Mt.get(g,{configuration:t,ident:u,jsonResponse:!0,customErrorMessage:Mt.customPackageError})),h=Object.keys(f.versions).sort(TM.default.compareLoose),C=f["dist-tags"].latest||h[h.length-1],y=vt.validRange(u.range);if(y){let L=TM.default.maxSatisfying(h,y);L!==null?C=L:(l.reportWarning(0,`Unmet range ${P.prettyRange(t,u.range)}; falling back to the latest version`),o=!0)}else Object.prototype.hasOwnProperty.call(f["dist-tags"],u.range)?C=f["dist-tags"][u.range]:u.range!=="unknown"&&(l.reportWarning(0,`Unknown tag ${P.prettyRange(t,u.range)}; falling back to the latest version`),o=!0);let B=f.versions[C],v={...f,...B,version:C,versions:h},D;if(n!==null){D={};for(let L of n){let H=v[L];if(typeof H<"u")D[L]=H;else{l.reportWarning(1,`The ${ee.pretty(t,L,ee.Type.CODE)} field doesn't exist inside ${P.prettyIdent(t,u)}'s information`),o=!0;continue}}}else this.json||(delete v.dist,delete v.readme,delete v.users),D=v;l.reportJson(D),this.json||s.push(D)}});OM.inspect.styles.name="cyan";for(let l of s)(l!==s[0]||o)&&this.context.stdout.write(`
|
||||
`),this.context.stdout.write(`${(0,OM.inspect)(l,{depth:1/0,colors:!0,compact:!1})}
|
||||
`);return a.exitCode()}};Yu.paths=[["npm","info"]],Yu.usage=ve.Usage({category:"Npm-related commands",description:"show information about a package",details:"\n This command fetches information about a package from the npm registry and prints it in a tree format.\n\n The package does not have to be installed locally, but needs to have been published (in particular, local changes will be ignored even for workspaces).\n\n Append `@<range>` to the package argument to provide information specific to the latest version that satisfies the range or to the corresponding tagged version. If the range is invalid or if there is no version satisfying the range, the command will print a warning and fall back to the latest version.\n\n If the `-f,--fields` option is set, it's a comma-separated list of fields which will be used to only display part of the package information.\n\n By default, this command won't return the `dist`, `readme`, and `users` fields, since they are often very long. To explicitly request those fields, explicitly list them with the `--fields` flag or request the output in JSON mode.\n ",examples:[["Show all available information about react (except the `dist`, `readme`, and `users` fields)","yarn npm info react"],["Show all available information about react as valid JSON (including the `dist`, `readme`, and `users` fields)","yarn npm info react --json"],["Show all available information about react@16.12.0","yarn npm info react@16.12.0"],["Show all available information about react@next","yarn npm info react@next"],["Show the description of react","yarn npm info react --fields description"],["Show all available versions of react","yarn npm info react --fields versions"],["Show the readme of react","yarn npm info react --fields readme"],["Show a few fields of react","yarn npm info react --fields homepage,repository"]]});function MM(r){if(Array.isArray(r)){let e=[];for(let t of r)t=MM(t),t&&e.push(t);return e}else if(typeof r=="object"&&r!==null){let e={};for(let t of Object.keys(r)){if(t.startsWith("_"))continue;let i=MM(r[t]);i&&(e[t]=i)}return e}else return r||null}var sue=Pe(Pm()),ju=class extends De{constructor(){super(...arguments);this.scope=z.String("-s,--scope",{description:"Login to the registry configured for a given scope"});this.publish=z.Boolean("--publish",!1,{description:"Login to the publish registry"})}async execute(){let t=await ye.find(this.context.cwd,this.context.plugins),i=await Qb({configuration:t,cwd:this.context.cwd,publish:this.publish,scope:this.scope});return(await Ge.start({configuration:t,stdout:this.context.stdout,includeFooter:!1},async s=>{let o=await zVe({configuration:t,registry:i,report:s,stdin:this.context.stdin,stdout:this.context.stdout}),a=`/-/user/org.couchdb.user:${encodeURIComponent(o.name)}`,l=await Mt.put(a,o,{attemptedAs:o.name,configuration:t,registry:i,jsonResponse:!0,authType:Mt.AuthType.NO_AUTH});return await WVe(i,l.token,{configuration:t,scope:this.scope}),s.reportInfo(0,"Successfully logged in")})).exitCode()}};ju.paths=[["npm","login"]],ju.usage=ve.Usage({category:"Npm-related commands",description:"store new login info to access the npm registry",details:"\n This command will ask you for your username, password, and 2FA One-Time-Password (when it applies). It will then modify your local configuration (in your home folder, never in the project itself) to reference the new tokens thus generated.\n\n Adding the `-s,--scope` flag will cause the authentication to be done against whatever registry is configured for the associated scope (see also `npmScopes`).\n\n Adding the `--publish` flag will cause the authentication to be done against the registry used when publishing the package (see also `publishConfig.registry` and `npmPublishRegistry`).\n ",examples:[["Login to the default registry","yarn npm login"],["Login to the registry linked to the @my-scope registry","yarn npm login --scope my-scope"],["Login to the publish registry for the current package","yarn npm login --publish"]]});async function Qb({scope:r,publish:e,configuration:t,cwd:i}){return r&&e?or.getScopeRegistry(r,{configuration:t,type:or.RegistryType.PUBLISH_REGISTRY}):r?or.getScopeRegistry(r,{configuration:t}):e?or.getPublishRegistry((await Ph(t,i)).manifest,{configuration:t}):or.getDefaultRegistry({configuration:t})}async function WVe(r,e,{configuration:t,scope:i}){let n=o=>a=>{let l=Ie.isIndexableObject(a)?a:{},c=l[o],u=Ie.isIndexableObject(c)?c:{};return{...l,[o]:{...u,npmAuthToken:e}}},s=i?{npmScopes:n(i)}:{npmRegistries:n(r)};return await ye.updateHomeConfiguration(s)}async function zVe({configuration:r,registry:e,report:t,stdin:i,stdout:n}){t.reportInfo(0,`Logging in to ${ee.pretty(r,e,ee.Type.URL)}`);let s=!1;if(e.match(/^https:\/\/npm\.pkg\.github\.com(\/|$)/)&&(t.reportInfo(0,"You seem to be using the GitHub Package Registry. Tokens must be generated with the 'repo', 'write:packages', and 'read:packages' permissions."),s=!0),t.reporLine truncated
|
||||
`);return a.exitCode()}};Yu.paths=[["npm","info"]],Yu.usage=ve.Usage({category:"Npm-related commands",description:"show information about a package",details:"\n This command fetches information about a package from the npm registry and prints it in a tree format.\n\n The package does not have to be installed locally, but needs to have been published (in particular, local changes will be ignored even for workspaces).\n\n Append `@<range>` to the package argument to provide information specific to the latest version that satisfies the range or to the corresponding tagged version. If the range is invalid or if there is no version satisfying the range, the command will print a warning and fall back to the latest version.\n\n If the `-f,--fields` option is set, it's a comma-separated list of fields which will be used to only display part of the package information.\n\n By default, this command won't return the `dist`, `readme`, and `users` fields, since they are often very long. To explicitly request those fields, explicitly list them with the `--fields` flag or request the output in JSON mode.\n ",examples:[["Show all available information about react (except the `dist`, `readme`, and `users` fields)","yarn npm info react"],["Show all available information about react as valid JSON (including the `dist`, `readme`, and `users` fields)","yarn npm info react --json"],["Show all available information about react@16.12.0","yarn npm info react@16.12.0"],["Show all available information about react@next","yarn npm info react@next"],["Show the description of react","yarn npm info react --fields description"],["Show all available versions of react","yarn npm info react --fields versions"],["Show the readme of react","yarn npm info react --fields readme"],["Show a few fields of react","yarn npm info react --fields homepage,repository"]]});function MM(r){if(Array.isArray(r)){let e=[];for(let t of r)t=MM(t),t&&e.push(t);return e}else if(typeof r=="object"&&r!==null){let e={};for(let t of Object.keys(r)){if(t.startsWith("_"))continue;let i=MM(r[t]);i&&(e[t]=i)}return e}else return r||null}var sue=Pe(Pm()),ju=class extends De{constructor(){super(...arguments);this.scope=z.String("-s,--filter",{description:"Login to the registry configured for a given scope"});this.publish=z.Boolean("--publish",!1,{description:"Login to the publish registry"})}async execute(){let t=await ye.find(this.context.cwd,this.context.plugins),i=await Qb({configuration:t,cwd:this.context.cwd,publish:this.publish,scope:this.scope});return(await Ge.start({configuration:t,stdout:this.context.stdout,includeFooter:!1},async s=>{let o=await zVe({configuration:t,registry:i,report:s,stdin:this.context.stdin,stdout:this.context.stdout}),a=`/-/user/org.couchdb.user:${encodeURIComponent(o.name)}`,l=await Mt.put(a,o,{attemptedAs:o.name,configuration:t,registry:i,jsonResponse:!0,authType:Mt.AuthType.NO_AUTH});return await WVe(i,l.token,{configuration:t,scope:this.scope}),s.reportInfo(0,"Successfully logged in")})).exitCode()}};ju.paths=[["npm","login"]],ju.usage=ve.Usage({category:"Npm-related commands",description:"store new login info to access the npm registry",details:"\n This command will ask you for your username, password, and 2FA One-Time-Password (when it applies). It will then modify your local configuration (in your home folder, never in the project itself) to reference the new tokens thus generated.\n\n Adding the `-s,--filter` flag will cause the authentication to be done against whatever registry is configured for the associated scope (see also `npmScopes`).\n\n Adding the `--publish` flag will cause the authentication to be done against the registry used when publishing the package (see also `publishConfig.registry` and `npmPublishRegistry`).\n ",examples:[["Login to the default registry","yarn npm login"],["Login to the registry linked to the @my-scope registry","yarn npm login --filter my-scope"],["Login to the publish registry for the current package","yarn npm login --publish"]]});async function Qb({scope:r,publish:e,configuration:t,cwd:i}){return r&&e?or.getScopeRegistry(r,{configuration:t,type:or.RegistryType.PUBLISH_REGISTRY}):r?or.getScopeRegistry(r,{configuration:t}):e?or.getPublishRegistry((await Ph(t,i)).manifest,{configuration:t}):or.getDefaultRegistry({configuration:t})}async function WVe(r,e,{configuration:t,scope:i}){let n=o=>a=>{let l=Ie.isIndexableObject(a)?a:{},c=l[o],u=Ie.isIndexableObject(c)?c:{};return{...l,[o]:{...u,npmAuthToken:e}}},s=i?{npmScopes:n(i)}:{npmRegistries:n(r)};return await ye.updateHomeConfiguration(s)}async function zVe({configuration:r,registry:e,report:t,stdin:i,stdout:n}){t.reportInfo(0,`Logging in to ${ee.pretty(r,e,ee.Type.URL)}`);let s=!1;if(e.match(/^https:\/\/npm\.pkg\.github\.com(\/|$)/)&&(t.reportInfo(0,"You seem to be using the GitHub Package Registry. Tokens must be generated with the 'repo', 'write:packages', and 'read:packages' permissions."),s=!0),t.reLine truncated
|
||||
This command will list all tags of a package from the npm registry.
|
||||
|
||||
If the package is not specified, Yarn will default to the current workspace.
|
||||
@@ -732,7 +732,7 @@ ${we.map(pe=>`Not found: ${lo(pe)}
|
||||
This command will add a tag to the npm registry for a specific version of a package. If the tag already exists, it will be overwritten.
|
||||
`,examples:[["Add a `beta` tag for version `2.3.4-beta.4` of package `my-pkg`","yarn npm tag add my-pkg@2.3.4-beta.4 beta"]]});var Vu=class extends De{constructor(){super(...arguments);this.package=z.String();this.tag=z.String()}async execute(){if(this.tag==="latest")throw new be("The 'latest' tag cannot be removed.");let t=await ye.find(this.context.cwd,this.context.plugins),{project:i,workspace:n}=await je.find(t,this.context.cwd);if(!n)throw new ct(i.cwd,this.context.cwd);let s=P.parseIdent(this.package),o=or.getPublishRegistry(n.manifest,{configuration:t}),a=ee.pretty(t,this.tag,ee.Type.CODE),l=ee.pretty(t,s,ee.Type.IDENT),c=await IE(s,t);if(!Object.prototype.hasOwnProperty.call(c,this.tag))throw new be(`${a} is not a tag of package ${l}`);return(await Ge.start({configuration:t,stdout:this.context.stdout},async g=>{let f=`/-/package${Mt.getIdentUrl(s)}/dist-tags/${encodeURIComponent(this.tag)}`;await Mt.del(f,{configuration:t,registry:o,ident:s,jsonResponse:!0}),g.reportInfo(0,`Tag ${a} removed from package ${l}`)})).exitCode()}};Vu.paths=[["npm","tag","remove"]],Vu.usage=ve.Usage({category:"Npm-related commands",description:"remove a tag from a package",details:`
|
||||
This command will remove a tag from a package from the npm registry.
|
||||
`,examples:[["Remove the `beta` tag from package `my-pkg`","yarn npm tag remove my-pkg beta"]]});var Xu=class extends De{constructor(){super(...arguments);this.scope=z.String("-s,--scope",{description:"Print username for the registry configured for a given scope"});this.publish=z.Boolean("--publish",!1,{description:"Print username for the publish registry"})}async execute(){let t=await ye.find(this.context.cwd,this.context.plugins),i;return this.scope&&this.publish?i=or.getScopeRegistry(this.scope,{configuration:t,type:or.RegistryType.PUBLISH_REGISTRY}):this.scope?i=or.getScopeRegistry(this.scope,{configuration:t}):this.publish?i=or.getPublishRegistry((await Ph(t,this.context.cwd)).manifest,{configuration:t}):i=or.getDefaultRegistry({configuration:t}),(await Ge.start({configuration:t,stdout:this.context.stdout},async s=>{var a,l;let o;try{o=await Mt.get("/-/whoami",{configuration:t,registry:i,authType:Mt.AuthType.ALWAYS_AUTH,jsonResponse:!0,ident:this.scope?P.makeIdent(this.scope,""):void 0})}catch(c){if(((a=c.response)==null?void 0:a.statusCode)===401||((l=c.response)==null?void 0:l.statusCode)===403){s.reportError(41,"Authentication failed - your credentials may have expired");return}else throw c}s.reportInfo(0,o.username)})).exitCode()}};Xu.paths=[["npm","whoami"]],Xu.usage=ve.Usage({category:"Npm-related commands",description:"display the name of the authenticated user",details:"\n Print the username associated with the current authentication settings to the standard output.\n\n When using `-s,--scope`, the username printed will be the one that matches the authentication settings of the registry associated with the given scope (those settings can be overriden using the `npmRegistries` map, and the registry associated with the scope is configured via the `npmScopes` map).\n\n When using `--publish`, the registry we'll select will by default be the one used when publishing packages (`publishConfig.registry` or `npmPublishRegistry` if available, otherwise we'll fallback to the regular `npmRegistryServer`).\n ",examples:[["Print username for the default registry","yarn npm whoami"],["Print username for the registry on a given scope","yarn npm whoami --scope company"]]});var _Ve={configuration:{npmPublishAccess:{description:"Default access of the published packages",type:"STRING",default:null},npmAuditExcludePackages:{description:"Array of glob patterns of packages to exclude from npm audit",type:"STRING",default:[],isArray:!0},npmAuditIgnoreAdvisories:{description:"Array of glob patterns of advisory IDs to exclude from npm audit",type:"STRING",default:[],isArray:!0}},commands:[Gu,Yu,ju,qu,Ju,zu,Wu,Vu,Xu]},ZVe=_Ve;var zM={};ut(zM,{default:()=>h6e,patchUtils:()=>_u});var _u={};ut(_u,{applyPatchFile:()=>Sb,diffFolders:()=>JM,ensureUnpatchedDescriptor:()=>HM,extractPackageToDisk:()=>qM,extractPatchFlags:()=>hue,isParentRequired:()=>jM,loadPatchFiles:()=>QE,makeDescriptor:()=>GM,makeLocator:()=>YM,makePatchHash:()=>WM,parseDescriptor:()=>wE,parseLocator:()=>BE,parsePatchFile:()=>yE});var $Ve=/^@@ -(\d+)(,(\d+))? \+(\d+)(,(\d+))? @@.*/;function Ap(r){return x.relative(Oe.root,x.resolve(Oe.root,K.toPortablePath(r)))}function e6e(r){let e=r.trim().match($Ve);if(!e)throw new Error(`Bad header line: '${r}'`);return{original:{start:Math.max(Number(e[1]),1),length:Number(e[3]||1)},patched:{start:Math.max(Number(e[4]),1),length:Number(e[6]||1)}}}var t6e=420,r6e=493;var Aue=()=>({semverExclusivity:null,diffLineFromPath:null,diffLineToPath:null,oldMode:null,newMode:null,deletedFileMode:null,newFileMode:null,renameFrom:null,renameTo:null,beforeHash:null,afterHash:null,fromPath:null,toPath:null,hunks:null}),i6e=r=>({header:e6e(r),parts:[]}),n6e={["@"]:"header",["-"]:"deletion",["+"]:"insertion",[" "]:"context",["\\"]:"pragma",undefined:"context"};function s6e(r){let e=[],t=Aue(),i="parsing header",n=null,s=null;function o(){n&&(s&&(n.parts.push(s),s=null),t.hunks.push(n),n=null)}function a(){o(),e.push(t),t=Aue()}for(let l=0;l<r.length;l++){let c=r[l];if(i==="parsing header")if(c.startsWith("@@"))i="parsing hunks",t.hunks=[],l-=1;else if(c.startsWith("diff --git ")){t&&t.diffLineFromPath&&a();let u=c.match(/^diff --git a\/(.*?) b\/(.*?)\s*$/);if(!u)throw new Error(`Bad diff line: ${c}`);t.diffLineFromPath=u[1],t.diffLineToPath=u[2]}else if(c.startsWith("old mode "))t.oldMode=c.slice(9).trim();else if(c.startsWith("new mode "))t.newMode=c.slice(9).trim();else if(c.startsWith("deleted file mode "))t.deletedFileMode=c.slice(18).trim();else if(c.startsWith("new file mode "))t.newFileMode=c.slice(14).trim();else if(c.startsWith("rename from "))t.renameFrom=c.slice(12).trim();else if(c.startsWith("rename to "))t.renameTo=c.slice(10).trim();else if(c.startsWith("index ")){let u=c.match(/(\w+)\.\.(\w+)/);if(!u)continue;t.beforeHash=u[1],t.afterHash=u[2]}else c.startsWith("semver exclusivity ")?t.semverExclusivity=c.slice(19).trim():c.startsWith("--- ")?t.fromPath=c.slice(6).trim():c.startsWith("+++ ")&&(t.toP
Line truncated
|
||||
`,examples:[["Remove the `beta` tag from package `my-pkg`","yarn npm tag remove my-pkg beta"]]});var Xu=class extends De{constructor(){super(...arguments);this.scope=z.String("-s,--filter",{description:"Print username for the registry configured for a given scope"});this.publish=z.Boolean("--publish",!1,{description:"Print username for the publish registry"})}async execute(){let t=await ye.find(this.context.cwd,this.context.plugins),i;return this.scope&&this.publish?i=or.getScopeRegistry(this.scope,{configuration:t,type:or.RegistryType.PUBLISH_REGISTRY}):this.scope?i=or.getScopeRegistry(this.scope,{configuration:t}):this.publish?i=or.getPublishRegistry((await Ph(t,this.context.cwd)).manifest,{configuration:t}):i=or.getDefaultRegistry({configuration:t}),(await Ge.start({configuration:t,stdout:this.context.stdout},async s=>{var a,l;let o;try{o=await Mt.get("/-/whoami",{configuration:t,registry:i,authType:Mt.AuthType.ALWAYS_AUTH,jsonResponse:!0,ident:this.scope?P.makeIdent(this.scope,""):void 0})}catch(c){if(((a=c.response)==null?void 0:a.statusCode)===401||((l=c.response)==null?void 0:l.statusCode)===403){s.reportError(41,"Authentication failed - your credentials may have expired");return}else throw c}s.reportInfo(0,o.username)})).exitCode()}};Xu.paths=[["npm","whoami"]],Xu.usage=ve.Usage({category:"Npm-related commands",description:"display the name of the authenticated user",details:"\n Print the username associated with the current authentication settings to the standard output.\n\n When using `-s,--filter`, the username printed will be the one that matches the authentication settings of the registry associated with the given scope (those settings can be overriden using the `npmRegistries` map, and the registry associated with the scope is configured via the `npmScopes` map).\n\n When using `--publish`, the registry we'll select will by default be the one used when publishing packages (`publishConfig.registry` or `npmPublishRegistry` if available, otherwise we'll fallback to the regular `npmRegistryServer`).\n ",examples:[["Print username for the default registry","yarn npm whoami"],["Print username for the registry on a given scope","yarn npm whoami --filter company"]]});var _Ve={configuration:{npmPublishAccess:{description:"Default access of the published packages",type:"STRING",default:null},npmAuditExcludePackages:{description:"Array of glob patterns of packages to exclude from npm audit",type:"STRING",default:[],isArray:!0},npmAuditIgnoreAdvisories:{description:"Array of glob patterns of advisory IDs to exclude from npm audit",type:"STRING",default:[],isArray:!0}},commands:[Gu,Yu,ju,qu,Ju,zu,Wu,Vu,Xu]},ZVe=_Ve;var zM={};ut(zM,{default:()=>h6e,patchUtils:()=>_u});var _u={};ut(_u,{applyPatchFile:()=>Sb,diffFolders:()=>JM,ensureUnpatchedDescriptor:()=>HM,extractPackageToDisk:()=>qM,extractPatchFlags:()=>hue,isParentRequired:()=>jM,loadPatchFiles:()=>QE,makeDescriptor:()=>GM,makeLocator:()=>YM,makePatchHash:()=>WM,parseDescriptor:()=>wE,parseLocator:()=>BE,parsePatchFile:()=>yE});var $Ve=/^@@ -(\d+)(,(\d+))? \+(\d+)(,(\d+))? @@.*/;function Ap(r){return x.relative(Oe.root,x.resolve(Oe.root,K.toPortablePath(r)))}function e6e(r){let e=r.trim().match($Ve);if(!e)throw new Error(`Bad header line: '${r}'`);return{original:{start:Math.max(Number(e[1]),1),length:Number(e[3]||1)},patched:{start:Math.max(Number(e[4]),1),length:Number(e[6]||1)}}}var t6e=420,r6e=493;var Aue=()=>({semverExclusivity:null,diffLineFromPath:null,diffLineToPath:null,oldMode:null,newMode:null,deletedFileMode:null,newFileMode:null,renameFrom:null,renameTo:null,beforeHash:null,afterHash:null,fromPath:null,toPath:null,hunks:null}),i6e=r=>({header:e6e(r),parts:[]}),n6e={["@"]:"header",["-"]:"deletion",["+"]:"insertion",[" "]:"context",["\\"]:"pragma",undefined:"context"};function s6e(r){let e=[],t=Aue(),i="parsing header",n=null,s=null;function o(){n&&(s&&(n.parts.push(s),s=null),t.hunks.push(n),n=null)}function a(){o(),e.push(t),t=Aue()}for(let l=0;l<r.length;l++){let c=r[l];if(i==="parsing header")if(c.startsWith("@@"))i="parsing hunks",t.hunks=[],l-=1;else if(c.startsWith("diff --git ")){t&&t.diffLineFromPath&&a();let u=c.match(/^diff --git a\/(.*?) b\/(.*?)\s*$/);if(!u)throw new Error(`Bad diff line: ${c}`);t.diffLineFromPath=u[1],t.diffLineToPath=u[2]}else if(c.startsWith("old mode "))t.oldMode=c.slice(9).trim();else if(c.startsWith("new mode "))t.newMode=c.slice(9).trim();else if(c.startsWith("deleted file mode "))t.deletedFileMode=c.slice(18).trim();else if(c.startsWith("new file mode "))t.newFileMode=c.slice(14).trim();else if(c.startsWith("rename from "))t.renameFrom=c.slice(12).trim();else if(c.startsWith("rename to "))t.renameTo=c.slice(10).trim();else if(c.startsWith("index ")){let u=c.match(/(\w+)\.\.(\w+)/);if(!u)continue;t.beforeHash=u[1],t.afterHash=u[2]}else c.startsWith("semver exclusivity ")?t.semverExclusivity=c.slice(19).trim():c.startsWith("--- ")?t.fromPath=c.slice(6).trim():c.startsWith("+++ ")&&(t.
Line truncated
|
||||
`)+(n.hunk.parts[0].noNewlineAtEndOfFile?"":`
|
||||
`):"";await e.mkdirpPromise(x.dirname(n.path),{chmod:493,utimes:[xr.SAFE_TIME,xr.SAFE_TIME]}),await e.writeFilePromise(n.path,s,{mode:n.mode}),await e.utimesPromise(n.path,xr.SAFE_TIME,xr.SAFE_TIME)}break;case"patch":await cp(e,n.path,async()=>{await c6e(n,{baseFs:e,dryRun:t})});break;case"mode change":{let o=(await e.statPromise(n.path)).mode;if(lue(n.newMode)!==lue(o))continue;await cp(e,n.path,async()=>{await e.chmodPromise(n.path,n.newMode)})}break;default:Ie.assertNever(n);break}}function lue(r){return(r&64)>0}function cue(r){return r.replace(/\s+$/,"")}function l6e(r,e){return cue(r)===cue(e)}async function c6e({hunks:r,path:e},{baseFs:t,dryRun:i=!1}){let n=await t.statSync(e).mode,o=(await t.readFileSync(e,"utf8")).split(/\n/),a=[],l=0,c=0;for(let g of r){let f=Math.max(c,g.header.patched.start+l),h=Math.max(0,f-c),p=Math.max(0,o.length-f-g.header.original.length),C=Math.max(h,p),y=0,B=0,v=null;for(;y<=C;){if(y<=h&&(B=f-y,v=uue(g,o,B),v!==null)){y=-y;break}if(y<=p&&(B=f+y,v=uue(g,o,B),v!==null))break;y+=1}if(v===null)throw new lp(r.indexOf(g),g);a.push(v),l+=y,c=B+g.header.original.length}if(i)return;let u=0;for(let g of a)for(let f of g)switch(f.type){case"splice":{let h=f.index+u;o.splice(h,f.numToDelete,...f.linesToInsert),u+=f.linesToInsert.length-f.numToDelete}break;case"pop":o.pop();break;case"push":o.push(f.line);break;default:Ie.assertNever(f);break}await t.writeFilePromise(e,o.join(`
|
||||
`),{mode:n})}function uue(r,e,t){let i=[];for(let n of r.parts)switch(n.type){case"context":case"deletion":{for(let s of n.lines){let o=e[t];if(o==null||!l6e(o,s))return null;t+=1}n.type==="deletion"&&(i.push({type:"splice",index:t-n.lines.length,numToDelete:n.lines.length,linesToInsert:[]}),n.noNewlineAtEndOfFile&&i.push({type:"push",line:""}))}break;case"insertion":i.push({type:"splice",index:t,numToDelete:0,linesToInsert:n.lines}),n.noNewlineAtEndOfFile&&i.push({type:"pop"});break;default:Ie.assertNever(n.type);break}return i}var g6e=/^builtin<([^>]+)>$/;function UM(r,e){let{source:t,selector:i,params:n}=P.parseRange(r);if(t===null)throw new Error("Patch locators must explicitly define their source");let s=i?i.split(/&/).map(c=>K.toPortablePath(c)):[],o=n&&typeof n.locator=="string"?P.parseLocator(n.locator):null,a=n&&typeof n.version=="string"?n.version:null,l=e(t);return{parentLocator:o,sourceItem:l,patchPaths:s,sourceVersion:a}}function wE(r){let{sourceItem:e,...t}=UM(r.range,P.parseDescriptor);return{...t,sourceDescriptor:e}}function BE(r){let{sourceItem:e,...t}=UM(r.reference,P.parseLocator);return{...t,sourceLocator:e}}function HM(r){if(!r.range.startsWith("patch:"))return r;let{sourceItem:e}=UM(r.range,P.parseDescriptor);return e}function gue({parentLocator:r,sourceItem:e,patchPaths:t,sourceVersion:i,patchHash:n},s){let o=r!==null?{locator:P.stringifyLocator(r)}:{},a=typeof i<"u"?{version:i}:{},l=typeof n<"u"?{hash:n}:{};return P.makeRange({protocol:"patch:",source:s(e),selector:t.join("&"),params:{...a,...l,...o}})}function GM(r,{parentLocator:e,sourceDescriptor:t,patchPaths:i}){return P.makeDescriptor(r,gue({parentLocator:e,sourceItem:t,patchPaths:i},P.stringifyDescriptor))}function YM(r,{parentLocator:e,sourcePackage:t,patchPaths:i,patchHash:n}){return P.makeLocator(r,gue({parentLocator:e,sourceItem:t,sourceVersion:t.version,patchPaths:i,patchHash:n},P.stringifyLocator))}function fue({onAbsolute:r,onRelative:e,onBuiltin:t},i){i.startsWith("~")&&(i=i.slice(1));let s=i.match(g6e);return s!==null?t(s[1]):x.isAbsolute(i)?r(i):e(i)}function hue(r){let e=r.startsWith("~");return e&&(r=r.slice(1)),{optional:e}}function jM(r){return fue({onAbsolute:()=>!1,onRelative:()=>!0,onBuiltin:()=>!1},r)}async function QE(r,e,t){let i=r!==null?await t.fetcher.fetch(r,t):null,n=i&&i.localPath?{packageFs:new jt(Oe.root),prefixPath:x.relative(Oe.root,i.localPath)}:i;i&&i!==n&&i.releaseFs&&i.releaseFs();let s=await Ie.releaseAfterUseAsync(async()=>await Promise.all(e.map(async o=>{let a=hue(o),l=await fue({onAbsolute:async()=>await M.readFilePromise(o,"utf8"),onRelative:async()=>{if(n===null)throw new Error("Assertion failed: The parent locator should have been fetched");return await n.packageFs.readFilePromise(x.join(n.prefixPath,o),"utf8")},onBuiltin:async c=>await t.project.configuration.firstHook(u=>u.getBuiltinPatch,t.project,c)},o);return{...a,source:l}})));for(let o of s)typeof o.source=="string"&&(o.source=o.source.replace(/\r\n?/g,`
|
||||
|
||||
Vendored
+3
@@ -0,0 +1,3 @@
|
||||
undecided:
|
||||
- calcom-monorepo
|
||||
- "@calcom/prisma"
|
||||
Vendored
+2
@@ -0,0 +1,2 @@
|
||||
undecided:
|
||||
- "@calcom/prisma"
|
||||
@@ -199,7 +199,7 @@ There's some settings that you'll need to setup.
|
||||
Under Vercel > Your API Project > Settings
|
||||
|
||||
In General > Build & Development Settings
|
||||
BUILD COMMAND: `yarn turbo run build --scope=@calcom/api --include-dependencies --no-deps`
|
||||
BUILD COMMAND: `yarn turbo run build --filter=@calcom/api --no-deps`
|
||||
OUTPUT DIRECTORY: `apps/api/.next`
|
||||
|
||||
In Git > Ignored Build Step
|
||||
|
||||
@@ -38,7 +38,7 @@
|
||||
"@axiomhq/winston": "^1.2.0",
|
||||
"@calcom/platform-constants": "*",
|
||||
"@calcom/platform-enums": "*",
|
||||
"@calcom/platform-libraries": "npm:@calcom/platform-libraries@0.0.312",
|
||||
"@calcom/platform-libraries": "npm:@calcom/platform-libraries@0.0.318",
|
||||
"@calcom/platform-types": "*",
|
||||
"@calcom/platform-utils": "*",
|
||||
"@calcom/prisma": "*",
|
||||
|
||||
+233
-3
@@ -15,11 +15,11 @@ import { NestExpressApplication } from "@nestjs/platform-express";
|
||||
import { Test } from "@nestjs/testing";
|
||||
import { PlatformOAuthClient, Team, User } from "@prisma/client";
|
||||
import * as request from "supertest";
|
||||
import { BookingsRepositoryFixture } from "test/fixtures/repository/bookings.repository.fixture";
|
||||
import { EventTypesRepositoryFixture } from "test/fixtures/repository/event-types.repository.fixture";
|
||||
import { MembershipRepositoryFixture } from "test/fixtures/repository/membership.repository.fixture";
|
||||
import { OAuthClientRepositoryFixture } from "test/fixtures/repository/oauth-client.repository.fixture";
|
||||
import { ProfileRepositoryFixture } from "test/fixtures/repository/profiles.repository.fixture";
|
||||
import { SchedulesRepositoryFixture } from "test/fixtures/repository/schedules.repository.fixture";
|
||||
import { TeamRepositoryFixture } from "test/fixtures/repository/team.repository.fixture";
|
||||
import { UserRepositoryFixture } from "test/fixtures/repository/users.repository.fixture";
|
||||
import { randomString } from "test/utils/randomString";
|
||||
@@ -36,6 +36,7 @@ import {
|
||||
CreateBookingInput_2024_08_13,
|
||||
CreateEventTypeInput_2024_06_14,
|
||||
EventTypeOutput_2024_06_14,
|
||||
GetBookingOutput_2024_08_13,
|
||||
GetBookingsOutput_2024_08_13,
|
||||
} from "@calcom/platform-types";
|
||||
|
||||
@@ -50,9 +51,9 @@ describe("Managed user bookings 2024-08-13", () => {
|
||||
let oauthClientRepositoryFixture: OAuthClientRepositoryFixture;
|
||||
let teamRepositoryFixture: TeamRepositoryFixture;
|
||||
let eventTypesRepositoryFixture: EventTypesRepositoryFixture;
|
||||
let schedulesRepositoryFixture: SchedulesRepositoryFixture;
|
||||
let profilesRepositoryFixture: ProfileRepositoryFixture;
|
||||
let membershipsRepositoryFixture: MembershipRepositoryFixture;
|
||||
let bookingsRepositoryFixture: BookingsRepositoryFixture;
|
||||
|
||||
const platformAdminEmail = `managed-users-bookings-2024-08-13-admin-${randomString()}@api.com`;
|
||||
let platformAdmin: User;
|
||||
@@ -67,6 +68,10 @@ describe("Managed user bookings 2024-08-13", () => {
|
||||
let thirdManagedUser: CreateManagedUserData;
|
||||
|
||||
let firstManagedUserEventTypeId: number;
|
||||
let eventTypeRequiresConfirmationId: number;
|
||||
|
||||
const orgAdminManagedUserEmail = `managed-user-bookings-2024-08-13-org-admin-${randomString()}@api.com`;
|
||||
let orgAdminManagedUser: CreateManagedUserData;
|
||||
|
||||
let firstManagedUserBookingsCount = 0;
|
||||
let secondManagedUserBookingsCount = 0;
|
||||
@@ -85,9 +90,9 @@ describe("Managed user bookings 2024-08-13", () => {
|
||||
userRepositoryFixture = new UserRepositoryFixture(moduleRef);
|
||||
teamRepositoryFixture = new TeamRepositoryFixture(moduleRef);
|
||||
eventTypesRepositoryFixture = new EventTypesRepositoryFixture(moduleRef);
|
||||
schedulesRepositoryFixture = new SchedulesRepositoryFixture(moduleRef);
|
||||
profilesRepositoryFixture = new ProfileRepositoryFixture(moduleRef);
|
||||
membershipsRepositoryFixture = new MembershipRepositoryFixture(moduleRef);
|
||||
bookingsRepositoryFixture = new BookingsRepositoryFixture(moduleRef);
|
||||
|
||||
platformAdmin = await userRepositoryFixture.create({ email: platformAdminEmail });
|
||||
|
||||
@@ -95,6 +100,13 @@ describe("Managed user bookings 2024-08-13", () => {
|
||||
name: `oauth-client-users-organization-${randomString()}`,
|
||||
isPlatform: true,
|
||||
isOrganization: true,
|
||||
platformBilling: {
|
||||
create: {
|
||||
customerId: "cus_999",
|
||||
plan: "SCALE",
|
||||
subscriptionId: "sub_999",
|
||||
},
|
||||
},
|
||||
});
|
||||
oAuthClient = await createOAuthClient(organization.id);
|
||||
|
||||
@@ -243,6 +255,59 @@ describe("Managed user bookings 2024-08-13", () => {
|
||||
thirdManagedUser = responseBody.data;
|
||||
});
|
||||
|
||||
it(`should create org admin managed user`, async () => {
|
||||
const requestBody: CreateManagedUserInput = {
|
||||
email: orgAdminManagedUserEmail,
|
||||
timeZone: managedUsersTimeZone,
|
||||
weekStart: "Monday",
|
||||
timeFormat: 24,
|
||||
locale: Locales.FR,
|
||||
name: "Org Admin Smith",
|
||||
avatarUrl: "https://cal.com/api/avatar/2b735186-b01b-46d3-87da-019b8f61776b.png",
|
||||
};
|
||||
|
||||
const response = await request(app.getHttpServer())
|
||||
.post(`/api/v2/oauth-clients/${oAuthClient.id}/users`)
|
||||
.set("x-cal-secret-key", oAuthClient.secret)
|
||||
.send(requestBody)
|
||||
.expect(201);
|
||||
|
||||
const responseBody: CreateManagedUserOutput = response.body;
|
||||
expect(responseBody.status).toEqual(SUCCESS_STATUS);
|
||||
expect(responseBody.data).toBeDefined();
|
||||
expect(responseBody.data.user.email).toEqual(
|
||||
OAuthClientUsersService.getOAuthUserEmail(oAuthClient.id, requestBody.email)
|
||||
);
|
||||
expect(responseBody.data.accessToken).toBeDefined();
|
||||
expect(responseBody.data.refreshToken).toBeDefined();
|
||||
|
||||
orgAdminManagedUser = responseBody.data;
|
||||
|
||||
await request(app.getHttpServer())
|
||||
.post(`/v2/organizations/${organization.id}/memberships`)
|
||||
.set("x-cal-client-id", oAuthClient.id)
|
||||
.set("x-cal-secret-key", oAuthClient.secret)
|
||||
.send({
|
||||
userId: orgAdminManagedUser.user.id,
|
||||
role: "ADMIN",
|
||||
accepted: true,
|
||||
})
|
||||
.expect(201);
|
||||
});
|
||||
|
||||
it("should create an event type requiring confirmation for first managed user", async () => {
|
||||
const eventTypeRequiresConfirmation = await eventTypesRepositoryFixture.create(
|
||||
{
|
||||
title: `managed-user-bookings-event-type-requires-confirmation-${randomString()}`,
|
||||
slug: `managed-user-bookings-event-type-requires-confirmation-${randomString()}`,
|
||||
length: 60,
|
||||
requiresConfirmation: true,
|
||||
},
|
||||
firstManagedUser.user.id
|
||||
);
|
||||
eventTypeRequiresConfirmationId = eventTypeRequiresConfirmation.id;
|
||||
});
|
||||
|
||||
describe("bookings using original emails", () => {
|
||||
it("managed user should be booked by managed user attendee and booking shows up in both users' bookings", async () => {
|
||||
const body: CreateBookingInput_2024_08_13 = {
|
||||
@@ -543,10 +608,175 @@ describe("Managed user bookings 2024-08-13", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("booking confirmation by org admin", () => {
|
||||
it("should allow org admin managed user to confirm booking using access token", async () => {
|
||||
const bookingRequiringConfirmation = await bookingsRepositoryFixture.create({
|
||||
user: {
|
||||
connect: {
|
||||
id: firstManagedUser.user.id,
|
||||
},
|
||||
},
|
||||
startTime: new Date(Date.UTC(2030, 0, 9, 13, 0, 0)),
|
||||
endTime: new Date(Date.UTC(2030, 0, 9, 14, 0, 0)),
|
||||
title: "Booking requiring confirmation",
|
||||
uid: `booking-requiring-confirmation-${randomString()}`,
|
||||
eventType: {
|
||||
connect: {
|
||||
id: eventTypeRequiresConfirmationId,
|
||||
},
|
||||
},
|
||||
location: "https://meet.google.com/abc-def-ghi",
|
||||
customInputs: {},
|
||||
metadata: {},
|
||||
status: "PENDING",
|
||||
responses: {
|
||||
name: secondManagedUser.user.name,
|
||||
email: secondManagedUserEmail,
|
||||
},
|
||||
attendees: {
|
||||
create: {
|
||||
email: secondManagedUserEmail,
|
||||
name: secondManagedUser.user.name!,
|
||||
locale: secondManagedUser.user.locale,
|
||||
timeZone: secondManagedUser.user.timeZone,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const response = await request(app.getHttpServer())
|
||||
.post(`/v2/bookings/${bookingRequiringConfirmation.uid}/confirm`)
|
||||
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13)
|
||||
.set("Authorization", `Bearer ${orgAdminManagedUser.accessToken}`)
|
||||
.expect(200);
|
||||
|
||||
const responseBody: GetBookingOutput_2024_08_13 = response.body;
|
||||
expect(responseBody.status).toEqual(SUCCESS_STATUS);
|
||||
expect(responseBody.data).toBeDefined();
|
||||
|
||||
const bookingData = responseBody.data as BookingOutput_2024_08_13;
|
||||
expect(bookingData.status).toEqual("accepted");
|
||||
expect(bookingData.uid).toEqual(bookingRequiringConfirmation.uid);
|
||||
|
||||
const confirmedBooking = await bookingsRepositoryFixture.getByUid(bookingRequiringConfirmation.uid);
|
||||
expect(confirmedBooking?.status).toEqual("ACCEPTED");
|
||||
});
|
||||
|
||||
it("should allow org admin managed user to reject booking using access token", async () => {
|
||||
const bookingRequiringConfirmation = await bookingsRepositoryFixture.create({
|
||||
user: {
|
||||
connect: {
|
||||
id: firstManagedUser.user.id,
|
||||
},
|
||||
},
|
||||
startTime: new Date(Date.UTC(2030, 0, 9, 15, 0, 0)),
|
||||
endTime: new Date(Date.UTC(2030, 0, 9, 16, 0, 0)),
|
||||
title: "Booking requiring rejection",
|
||||
uid: `booking-requiring-rejection-${randomString()}`,
|
||||
eventType: {
|
||||
connect: {
|
||||
id: eventTypeRequiresConfirmationId,
|
||||
},
|
||||
},
|
||||
location: "https://meet.google.com/abc-def-ghi",
|
||||
customInputs: {},
|
||||
metadata: {},
|
||||
status: "PENDING",
|
||||
responses: {
|
||||
name: secondManagedUser.user.name,
|
||||
email: secondManagedUserEmail,
|
||||
},
|
||||
attendees: {
|
||||
create: {
|
||||
email: secondManagedUserEmail,
|
||||
name: secondManagedUser.user.name!,
|
||||
locale: secondManagedUser.user.locale,
|
||||
timeZone: secondManagedUser.user.timeZone,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const response = await request(app.getHttpServer())
|
||||
.post(`/v2/bookings/${bookingRequiringConfirmation.uid}/decline`)
|
||||
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13)
|
||||
.set("Authorization", `Bearer ${orgAdminManagedUser.accessToken}`)
|
||||
.expect(200);
|
||||
|
||||
const responseBody: GetBookingOutput_2024_08_13 = response.body;
|
||||
expect(responseBody.status).toEqual(SUCCESS_STATUS);
|
||||
expect(responseBody.data).toBeDefined();
|
||||
|
||||
const bookingData = responseBody.data as BookingOutput_2024_08_13;
|
||||
expect(bookingData.status).toEqual("rejected");
|
||||
expect(bookingData.uid).toEqual(bookingRequiringConfirmation.uid);
|
||||
|
||||
const rejectedBooking = await bookingsRepositoryFixture.getByUid(bookingRequiringConfirmation.uid);
|
||||
expect(rejectedBooking?.status).toEqual("REJECTED");
|
||||
});
|
||||
|
||||
it("should return unauthorized when org admin tries to confirm regular user's booking", async () => {
|
||||
const regularUser = await userRepositoryFixture.create({
|
||||
email: `managed-user-bookings-2024-08-13-regular-user-${randomString()}@api.com`,
|
||||
name: "Regular User",
|
||||
});
|
||||
|
||||
const regularUserEventType = await eventTypesRepositoryFixture.create(
|
||||
{
|
||||
title: `regular-user-event-type-requires-confirmation-${randomString()}`,
|
||||
slug: `regular-user-event-type-requires-confirmation-${randomString()}`,
|
||||
length: 60,
|
||||
requiresConfirmation: true,
|
||||
},
|
||||
regularUser.id
|
||||
);
|
||||
|
||||
const regularUserBooking = await bookingsRepositoryFixture.create({
|
||||
user: {
|
||||
connect: {
|
||||
id: regularUser.id,
|
||||
},
|
||||
},
|
||||
startTime: new Date(Date.UTC(2030, 0, 9, 17, 0, 0)),
|
||||
endTime: new Date(Date.UTC(2030, 0, 9, 18, 0, 0)),
|
||||
title: "Regular user booking requiring confirmation",
|
||||
uid: `regular-user-booking-${randomString()}`,
|
||||
eventType: {
|
||||
connect: {
|
||||
id: regularUserEventType.id,
|
||||
},
|
||||
},
|
||||
location: "https://meet.google.com/abc-def-ghi",
|
||||
customInputs: {},
|
||||
metadata: {},
|
||||
status: "PENDING",
|
||||
responses: {
|
||||
name: "External Attendee",
|
||||
email: "external@example.com",
|
||||
},
|
||||
attendees: {
|
||||
create: {
|
||||
email: "external@example.com",
|
||||
name: "External Attendee",
|
||||
locale: "en",
|
||||
timeZone: "Europe/Rome",
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
await request(app.getHttpServer())
|
||||
.post(`/v2/bookings/${regularUserBooking.uid}/confirm`)
|
||||
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13)
|
||||
.set("Authorization", `Bearer ${orgAdminManagedUser.accessToken}`)
|
||||
.expect(401);
|
||||
|
||||
await userRepositoryFixture.delete(regularUser.id);
|
||||
});
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
await userRepositoryFixture.delete(firstManagedUser.user.id);
|
||||
await userRepositoryFixture.delete(secondManagedUser.user.id);
|
||||
await userRepositoryFixture.delete(thirdManagedUser.user.id);
|
||||
await userRepositoryFixture.delete(orgAdminManagedUser.user.id);
|
||||
await userRepositoryFixture.delete(platformAdmin.id);
|
||||
await oauthClientRepositoryFixture.delete(oAuthClient.id);
|
||||
await teamRepositoryFixture.delete(organization.id);
|
||||
|
||||
@@ -702,6 +702,7 @@ export class InputBookingsService_2024_08_13 {
|
||||
beforeUpdatedDate: queryParams.beforeUpdatedAt,
|
||||
afterCreatedDate: queryParams.afterCreatedAt,
|
||||
beforeCreatedDate: queryParams.beforeCreatedAt,
|
||||
bookingUid: queryParams.bookingUid,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
+45
@@ -2147,6 +2147,51 @@ describe("Event types Endpoints", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("EventType Hidden Property", () => {
|
||||
let createdEventTypeId: number;
|
||||
|
||||
it("should create an event type with hidden=true", async () => {
|
||||
const createPayload = {
|
||||
title: "Hidden Event",
|
||||
slug: "hidden-event",
|
||||
lengthInMinutes: 30,
|
||||
hidden: true,
|
||||
};
|
||||
|
||||
const response = await request(app.getHttpServer())
|
||||
.post("/api/v2/event-types")
|
||||
.set(CAL_API_VERSION_HEADER, VERSION_2024_06_14)
|
||||
.send(createPayload)
|
||||
.expect(201);
|
||||
|
||||
const responseBody: ApiSuccessResponse<EventTypeOutput_2024_06_14> = response.body;
|
||||
const createdEventType = responseBody.data;
|
||||
expect(createdEventType).toHaveProperty("id");
|
||||
expect(createdEventType.title).toEqual(createPayload.title);
|
||||
expect(createdEventType.slug).toEqual(createPayload.slug);
|
||||
expect(createdEventType.lengthInMinutes).toEqual(createPayload.lengthInMinutes);
|
||||
expect(createdEventType.hidden).toBe(true);
|
||||
|
||||
createdEventTypeId = createdEventType.id;
|
||||
});
|
||||
|
||||
it("should update the hidden property to false", async () => {
|
||||
const updatePayload = {
|
||||
hidden: false,
|
||||
};
|
||||
|
||||
const response = await request(app.getHttpServer())
|
||||
.patch(`/api/v2/event-types/${createdEventTypeId}`)
|
||||
.set(CAL_API_VERSION_HEADER, VERSION_2024_06_14)
|
||||
.send(updatePayload)
|
||||
.expect(200);
|
||||
|
||||
const responseBody: ApiSuccessResponse<EventTypeOutput_2024_06_14> = response.body;
|
||||
const updatedEventType = responseBody.data;
|
||||
+expect(updatedEventType.hidden).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
await oauthClientRepositoryFixture.delete(oAuthClient.id);
|
||||
await teamRepositoryFixture.delete(organization.id);
|
||||
|
||||
+3
@@ -89,6 +89,7 @@ type Input = Pick<
|
||||
| "hideCalendarEventDetails"
|
||||
| "hideOrganizerEmail"
|
||||
| "calVideoSettings"
|
||||
| "hidden"
|
||||
>;
|
||||
|
||||
@Injectable()
|
||||
@@ -127,6 +128,7 @@ export class OutputEventTypesService_2024_06_14 {
|
||||
hideCalendarEventDetails,
|
||||
hideOrganizerEmail,
|
||||
calVideoSettings,
|
||||
hidden,
|
||||
} = databaseEventType;
|
||||
|
||||
const locations = this.transformLocations(databaseEventType.locations);
|
||||
@@ -203,6 +205,7 @@ export class OutputEventTypesService_2024_06_14 {
|
||||
hideCalendarEventDetails,
|
||||
hideOrganizerEmail,
|
||||
calVideoSettings,
|
||||
hidden,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -229,4 +229,19 @@ export class SchedulesRepository_2024_06_11 {
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
async getSchedulesByUserIds(userIds: number[], skip: number, take: number) {
|
||||
return this.dbRead.prisma.schedule.findMany({
|
||||
where: {
|
||||
userId: {
|
||||
in: userIds,
|
||||
},
|
||||
},
|
||||
include: {
|
||||
availability: true,
|
||||
},
|
||||
skip,
|
||||
take,
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -11,6 +11,7 @@ import { OrganizationsTeamsBookingsModule } from "@/modules/organizations/teams/
|
||||
import { OrganizationsUsersBookingsModule } from "@/modules/organizations/users/bookings/organizations-users-bookings.module";
|
||||
import { RouterModule } from "@/modules/router/router.module";
|
||||
import { StripeModule } from "@/modules/stripe/stripe.module";
|
||||
import { TeamsSchedulesModule } from "@/modules/teams/schedules/teams-schedules.module";
|
||||
import { TimezoneModule } from "@/modules/timezones/timezones.module";
|
||||
import { VerifiedResourcesModule } from "@/modules/verified-resources/verified-resources.module";
|
||||
import type { MiddlewareConsumer, NestModule } from "@nestjs/common";
|
||||
@@ -38,6 +39,7 @@ import { WebhooksModule } from "./webhooks/webhooks.module";
|
||||
OrganizationsRoutingFormsModule,
|
||||
VerifiedResourcesModule,
|
||||
RouterModule,
|
||||
TeamsSchedulesModule,
|
||||
],
|
||||
})
|
||||
export class EndpointsModule implements NestModule {
|
||||
|
||||
+128
-1
@@ -66,6 +66,8 @@ describe("Organizations Bookings Endpoints 2024-08-13", () => {
|
||||
let orgEventTypeId2: number;
|
||||
let nonOrgEventTypeId: number;
|
||||
|
||||
let collectiveOrgBookingUid: string;
|
||||
|
||||
beforeAll(async () => {
|
||||
const moduleRef = await withApiAuth(
|
||||
orgUserEmail2,
|
||||
@@ -91,12 +93,17 @@ describe("Organizations Bookings Endpoints 2024-08-13", () => {
|
||||
schedulesService = moduleRef.get<SchedulesService_2024_04_15>(SchedulesService_2024_04_15);
|
||||
|
||||
organization = await organizationsRepositoryFixture.create({ name: "organization bookings" });
|
||||
oAuthClient = await createOAuthClient(organization.id);
|
||||
team1 = await teamRepositoryFixture.create({
|
||||
name: "team orgs booking 1",
|
||||
isOrganization: false,
|
||||
parent: { connect: { id: organization.id } },
|
||||
createdByOAuthClient: {
|
||||
connect: {
|
||||
id: oAuthClient.id,
|
||||
},
|
||||
},
|
||||
});
|
||||
oAuthClient = await createOAuthClient(organization.id);
|
||||
|
||||
nonOrgUser1 = await userRepositoryFixture.create({
|
||||
email: nonOrgUserEmail1,
|
||||
@@ -315,6 +322,7 @@ describe("Organizations Bookings Endpoints 2024-08-13", () => {
|
||||
|
||||
if (responseDataIsBooking(responseBody.data)) {
|
||||
const data: BookingOutput_2024_08_13 = responseBody.data;
|
||||
collectiveOrgBookingUid = data.uid;
|
||||
expect(data.id).toBeDefined();
|
||||
expect(data.uid).toBeDefined();
|
||||
expect(data.hosts.length).toEqual(1);
|
||||
@@ -669,6 +677,125 @@ describe("Organizations Bookings Endpoints 2024-08-13", () => {
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("get by bookingUid param", () => {
|
||||
it("should get a specific booking by bookingUid query param", async () => {
|
||||
return request(app.getHttpServer())
|
||||
.get(`/v2/organizations/${organization.id}/bookings?bookingUid=${collectiveOrgBookingUid}`)
|
||||
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13)
|
||||
.set(X_CAL_CLIENT_ID, oAuthClient.id)
|
||||
.set(X_CAL_SECRET_KEY, oAuthClient.secret)
|
||||
.expect(200)
|
||||
.then(async (response) => {
|
||||
const responseBody: GetBookingsOutput_2024_08_13 = response.body;
|
||||
expect(responseBody.status).toEqual(SUCCESS_STATUS);
|
||||
expect(responseBody.data).toBeDefined();
|
||||
|
||||
const data: (
|
||||
| BookingOutput_2024_08_13
|
||||
| RecurringBookingOutput_2024_08_13
|
||||
| GetSeatedBookingOutput_2024_08_13
|
||||
)[] = responseBody.data;
|
||||
expect(data.length).toEqual(1);
|
||||
expect(responseDataIsBooking(data[0])).toBe(true);
|
||||
|
||||
if (responseDataIsBooking(data[0])) {
|
||||
const booking: BookingOutput_2024_08_13 = data[0];
|
||||
expect(booking.uid).toEqual(collectiveOrgBookingUid);
|
||||
expect(booking.eventTypeId).toEqual(orgEventTypeId);
|
||||
expect(booking.status).toEqual("accepted");
|
||||
expect(booking.attendees.length).toEqual(2);
|
||||
expect(booking.attendees[0].name).toEqual("alice");
|
||||
expect(booking.attendees[0].email).toEqual("alice@gmail.com");
|
||||
} else {
|
||||
throw new Error("Expected single booking but received different response type");
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
it("should return empty array for non-existent booking UID query param", async () => {
|
||||
const nonExistentUid = "non-existent-booking-uid";
|
||||
|
||||
return request(app.getHttpServer())
|
||||
.get(`/v2/organizations/${organization.id}/bookings?bookingUid=${nonExistentUid}`)
|
||||
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13)
|
||||
.set(X_CAL_CLIENT_ID, oAuthClient.id)
|
||||
.set(X_CAL_SECRET_KEY, oAuthClient.secret)
|
||||
.expect(200)
|
||||
.then(async (response) => {
|
||||
const responseBody: GetBookingsOutput_2024_08_13 = response.body;
|
||||
expect(responseBody.status).toEqual(SUCCESS_STATUS);
|
||||
expect(responseBody.data).toBeDefined();
|
||||
expect(responseBody.data.length).toEqual(0);
|
||||
});
|
||||
});
|
||||
|
||||
it("should return empty array for booking UID not belonging to the organization", async () => {
|
||||
const regularUser = await userRepositoryFixture.create({
|
||||
email: `org-bookings-regular-user-${Math.floor(Math.random() * 10000)}@api.com`,
|
||||
name: "Regular User",
|
||||
});
|
||||
|
||||
const regularUserEventType = await eventTypesRepositoryFixture.create(
|
||||
{
|
||||
title: `regular-user-event-type-${Math.floor(Math.random() * 10000)}`,
|
||||
slug: `regular-user-event-type-${Math.floor(Math.random() * 10000)}`,
|
||||
length: 60,
|
||||
bookingFields: [],
|
||||
locations: [],
|
||||
},
|
||||
regularUser.id
|
||||
);
|
||||
|
||||
const regularUserBooking = await bookingsRepositoryFixture.create({
|
||||
user: {
|
||||
connect: {
|
||||
id: regularUser.id,
|
||||
},
|
||||
},
|
||||
startTime: new Date(Date.UTC(2030, 0, 15, 13, 0, 0)),
|
||||
endTime: new Date(Date.UTC(2030, 0, 15, 14, 0, 0)),
|
||||
title: "Regular user booking",
|
||||
uid: `regular-user-booking-${Math.floor(Math.random() * 10000)}`,
|
||||
eventType: {
|
||||
connect: {
|
||||
id: regularUserEventType.id,
|
||||
},
|
||||
},
|
||||
location: "https://meet.google.com/regular-user",
|
||||
customInputs: {},
|
||||
metadata: {},
|
||||
status: "ACCEPTED",
|
||||
responses: {
|
||||
name: "Regular Attendee",
|
||||
email: "regular@example.com",
|
||||
},
|
||||
attendees: {
|
||||
create: {
|
||||
email: "regular@example.com",
|
||||
name: "Regular Attendee",
|
||||
locale: "en",
|
||||
timeZone: "Europe/Rome",
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
return request(app.getHttpServer())
|
||||
.get(`/v2/organizations/${organization.id}/bookings?bookingUid=${regularUserBooking.uid}`)
|
||||
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13)
|
||||
.set(X_CAL_CLIENT_ID, oAuthClient.id)
|
||||
.set(X_CAL_SECRET_KEY, oAuthClient.secret)
|
||||
.expect(200)
|
||||
.then(async (response) => {
|
||||
const responseBody: GetBookingsOutput_2024_08_13 = response.body;
|
||||
expect(responseBody.status).toEqual(SUCCESS_STATUS);
|
||||
expect(responseBody.data).toBeDefined();
|
||||
expect(responseBody.data.length).toEqual(0);
|
||||
|
||||
await userRepositoryFixture.delete(regularUser.id);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
async function createOAuthClient(organizationId: number) {
|
||||
|
||||
@@ -74,6 +74,7 @@ type Input = Pick<
|
||||
| "hideOrganizerEmail"
|
||||
| "team"
|
||||
| "calVideoSettings"
|
||||
| "hidden"
|
||||
>;
|
||||
|
||||
@Injectable()
|
||||
|
||||
+17
-1
@@ -2,6 +2,8 @@ import { CreateOrgMembershipDto } from "@/modules/organizations/memberships/inpu
|
||||
import { OrganizationsMembershipRepository } from "@/modules/organizations/memberships/organizations-membership.repository";
|
||||
import { Injectable, NotFoundException } from "@nestjs/common";
|
||||
|
||||
import { TeamService } from "@calcom/platform-libraries";
|
||||
|
||||
import { UpdateOrgMembershipDto } from "../inputs/update-organization-membership.input";
|
||||
import { OrganizationsMembershipOutputService } from "./organizations-membership-output.service";
|
||||
|
||||
@@ -62,10 +64,24 @@ export class OrganizationsMembershipService {
|
||||
}
|
||||
|
||||
async deleteOrgMembership(organizationId: number, membershipId: number) {
|
||||
const membership = await this.organizationsMembershipRepository.deleteOrgMembership(
|
||||
// Get the membership first to get the userId
|
||||
const membership = await this.organizationsMembershipRepository.findOrgMembership(
|
||||
organizationId,
|
||||
membershipId
|
||||
);
|
||||
|
||||
if (!membership) {
|
||||
throw new NotFoundException(
|
||||
`Membership with id ${membershipId} within organization id ${organizationId} not found`
|
||||
);
|
||||
}
|
||||
|
||||
await TeamService.removeMembers({
|
||||
teamIds: [organizationId],
|
||||
userIds: [membership.userId],
|
||||
isOrg: true,
|
||||
});
|
||||
|
||||
return this.organizationsMembershipOutputService.getOrgMembershipOutput(membership);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,9 @@
|
||||
import { OrganizationsEventTypesPrivateLinksController } from "@/ee/event-types-private-links/controllers/organizations-event-types-private-links.controller";
|
||||
import { EventTypesPrivateLinksModule } from "@/ee/event-types-private-links/event-types-private-links.module";
|
||||
import { EventTypesModule_2024_06_14 } from "@/ee/event-types/event-types_2024_06_14/event-types.module";
|
||||
import { SchedulesModule_2024_06_11 } from "@/ee/schedules/schedules_2024_06_11/schedules.module";
|
||||
import { InputSchedulesService_2024_06_11 } from "@/ee/schedules/schedules_2024_06_11/services/input-schedules.service";
|
||||
import { SchedulesService_2024_06_11 } from "@/ee/schedules/schedules_2024_06_11/services/schedules.service";
|
||||
import { AppsRepository } from "@/modules/apps/apps.repository";
|
||||
import { ConferencingRepository } from "@/modules/conferencing/repositories/conferencing.repository";
|
||||
import { ConferencingService } from "@/modules/conferencing/services/conferencing.service";
|
||||
@@ -18,13 +22,10 @@ import { OrganizationAttributesService } from "@/modules/organizations/attribute
|
||||
import { OrganizationAttributeOptionRepository } from "@/modules/organizations/attributes/options/organization-attribute-options.repository";
|
||||
import { OrganizationsAttributesOptionsController } from "@/modules/organizations/attributes/options/organizations-attributes-options.controller";
|
||||
import { OrganizationAttributeOptionService } from "@/modules/organizations/attributes/options/services/organization-attributes-option.service";
|
||||
import { OrganizationsConferencingController } from "@/modules/organizations/conferencing/organizations-conferencing.controller";
|
||||
import { OrganizationsConferencingModule } from "@/modules/organizations/conferencing/organizations-conferencing.module";
|
||||
import { OrganizationsConferencingService } from "@/modules/organizations/conferencing/services/organizations-conferencing.service";
|
||||
import { OrganizationsDelegationCredentialModule } from "@/modules/organizations/delegation-credentials/organizations-delegation-credential.module";
|
||||
import { OrganizationsEventTypesController } from "@/modules/organizations/event-types/organizations-event-types.controller";
|
||||
import { EventTypesPrivateLinksModule } from "@/ee/event-types-private-links/event-types-private-links.module";
|
||||
import { OrganizationsEventTypesPrivateLinksController } from "@/ee/event-types-private-links/controllers/organizations-event-types-private-links.controller";
|
||||
import { OrganizationsEventTypesRepository } from "@/modules/organizations/event-types/organizations-event-types.repository";
|
||||
import { OutputTeamEventTypesResponsePipe } from "@/modules/organizations/event-types/pipes/team-event-types-response.transformer";
|
||||
import { InputOrganizationsEventTypesService } from "@/modules/organizations/event-types/services/input.service";
|
||||
@@ -38,7 +39,6 @@ import { OrganizationsMembershipOutputService } from "@/modules/organizations/me
|
||||
import { OrganizationsMembershipService } from "@/modules/organizations/memberships/services/organizations-membership.service";
|
||||
import { OrganizationsOrganizationsModule } from "@/modules/organizations/organizations/organizations-organizations.module";
|
||||
import { OrganizationsSchedulesController } from "@/modules/organizations/schedules/organizations-schedules.controller";
|
||||
import { OrganizationSchedulesRepository } from "@/modules/organizations/schedules/organizations-schedules.repository";
|
||||
import { OrganizationsSchedulesService } from "@/modules/organizations/schedules/services/organizations-schedules.service";
|
||||
import { OrganizationsStripeModule } from "@/modules/organizations/stripe/organizations-stripe.module";
|
||||
import { OrganizationsStripeService } from "@/modules/organizations/stripe/services/organizations-stripe.service";
|
||||
@@ -65,6 +65,7 @@ import { RedisModule } from "@/modules/redis/redis.module";
|
||||
import { RedisService } from "@/modules/redis/redis.service";
|
||||
import { StripeModule } from "@/modules/stripe/stripe.module";
|
||||
import { TeamsEventTypesModule } from "@/modules/teams/event-types/teams-event-types.module";
|
||||
import { TeamsSchedulesService } from "@/modules/teams/schedules/services/teams-schedules.service";
|
||||
import { TeamsModule } from "@/modules/teams/teams/teams.module";
|
||||
import { TokensRepository } from "@/modules/tokens/tokens.repository";
|
||||
import { UsersModule } from "@/modules/users/users.module";
|
||||
@@ -102,7 +103,6 @@ import { Module } from "@nestjs/common";
|
||||
OrganizationsTeamsService,
|
||||
MembershipsRepository,
|
||||
OrganizationsSchedulesService,
|
||||
OrganizationSchedulesRepository,
|
||||
OrganizationsUsersRepository,
|
||||
OrganizationsUsersService,
|
||||
EmailService,
|
||||
@@ -144,6 +144,9 @@ import { Module } from "@nestjs/common";
|
||||
TeamWorkflowsService,
|
||||
WorkflowsInputService,
|
||||
WorkflowsOutputService,
|
||||
TeamsSchedulesService,
|
||||
SchedulesService_2024_06_11,
|
||||
InputSchedulesService_2024_06_11,
|
||||
],
|
||||
exports: [
|
||||
OrganizationsService,
|
||||
|
||||
+3
-3
@@ -1,5 +1,5 @@
|
||||
import { SchedulesRepository_2024_06_11 } from "@/ee/schedules/schedules_2024_06_11/schedules.repository";
|
||||
import { OutputSchedulesService_2024_06_11 } from "@/ee/schedules/schedules_2024_06_11/services/output-schedules.service";
|
||||
import { OrganizationSchedulesRepository } from "@/modules/organizations/schedules/organizations-schedules.repository";
|
||||
import { UsersRepository } from "@/modules/users/users.repository";
|
||||
import { Injectable } from "@nestjs/common";
|
||||
|
||||
@@ -8,8 +8,8 @@ import { ScheduleOutput_2024_06_11 } from "@calcom/platform-types";
|
||||
@Injectable()
|
||||
export class OrganizationsSchedulesService {
|
||||
constructor(
|
||||
private readonly organizationSchedulesService: OrganizationSchedulesRepository,
|
||||
private readonly outputSchedulesService: OutputSchedulesService_2024_06_11,
|
||||
private readonly schedulesRepository: SchedulesRepository_2024_06_11,
|
||||
private readonly usersRepository: UsersRepository
|
||||
) {}
|
||||
|
||||
@@ -17,7 +17,7 @@ export class OrganizationsSchedulesService {
|
||||
const users = await this.usersRepository.getOrganizationUsers(organizationId);
|
||||
const usersIds = users.map((user) => user.id);
|
||||
|
||||
const schedules = await this.organizationSchedulesService.getSchedulesByUserIds(usersIds, skip, take);
|
||||
const schedules = await this.schedulesRepository.getSchedulesByUserIds(usersIds, skip, take);
|
||||
|
||||
const responseSchedules: ScheduleOutput_2024_06_11[] = [];
|
||||
|
||||
|
||||
-4
@@ -20,7 +20,6 @@ import {
|
||||
OrgTeamMembershipOutputResponseDto,
|
||||
} from "@/modules/organizations/teams/memberships/outputs/organization-teams-memberships.output";
|
||||
import { OrganizationsTeamsMembershipsService } from "@/modules/organizations/teams/memberships/services/organizations-teams-memberships.service";
|
||||
import { TeamsEventTypesService } from "@/modules/teams/event-types/services/teams-event-types.service";
|
||||
import { TeamMembershipOutput } from "@/modules/teams/memberships/outputs/team-membership.output";
|
||||
import {
|
||||
Controller,
|
||||
@@ -59,7 +58,6 @@ export class OrganizationsTeamsMembershipsController {
|
||||
|
||||
constructor(
|
||||
private organizationsTeamsMembershipsService: OrganizationsTeamsMembershipsService,
|
||||
private teamsEventTypesService: TeamsEventTypesService,
|
||||
private readonly organizationsRepository: OrganizationsRepository
|
||||
) {}
|
||||
|
||||
@@ -127,8 +125,6 @@ export class OrganizationsTeamsMembershipsController {
|
||||
membershipId
|
||||
);
|
||||
|
||||
await this.teamsEventTypesService.deleteUserTeamEventTypesAndHosts(membership.userId, teamId);
|
||||
|
||||
return {
|
||||
status: SUCCESS_STATUS,
|
||||
data: plainToClass(TeamMembershipOutput, membership, { strategy: "excludeAll" }),
|
||||
|
||||
+13
-1
@@ -3,6 +3,8 @@ import { UpdateOrgTeamMembershipDto } from "@/modules/organizations/teams/member
|
||||
import { OrganizationsTeamsMembershipsRepository } from "@/modules/organizations/teams/memberships/organizations-teams-memberships.repository";
|
||||
import { Injectable, NotFoundException } from "@nestjs/common";
|
||||
|
||||
import { TeamService } from "@calcom/platform-libraries";
|
||||
|
||||
@Injectable()
|
||||
export class OrganizationsTeamsMembershipsService {
|
||||
constructor(
|
||||
@@ -58,11 +60,21 @@ export class OrganizationsTeamsMembershipsService {
|
||||
}
|
||||
|
||||
async deleteOrgTeamMembership(organizationId: number, teamId: number, membershipId: number) {
|
||||
const teamMembership = await this.organizationsTeamsMembershipsRepository.deleteOrgTeamMembershipById(
|
||||
// First get the membership to get the userId
|
||||
const teamMembership = await this.organizationsTeamsMembershipsRepository.findOrgTeamMembership(
|
||||
organizationId,
|
||||
teamId,
|
||||
membershipId
|
||||
);
|
||||
|
||||
if (!teamMembership) {
|
||||
throw new NotFoundException(
|
||||
`Membership with id ${membershipId} not found in team ${teamId} of organization ${organizationId}`
|
||||
);
|
||||
}
|
||||
|
||||
await TeamService.removeMembers({ teamIds: [teamId], userIds: [teamMembership.userId], isOrg: false });
|
||||
|
||||
return teamMembership;
|
||||
}
|
||||
}
|
||||
+28
-3
@@ -14,11 +14,12 @@ import { IsOrgGuard } from "@/modules/auth/guards/organizations/is-org.guard";
|
||||
import { RolesGuard } from "@/modules/auth/guards/roles/roles.guard";
|
||||
import { IsTeamInOrg } from "@/modules/auth/guards/teams/is-team-in-org.guard";
|
||||
import { IsUserInOrgTeam } from "@/modules/auth/guards/users/is-user-in-org-team.guard";
|
||||
import { Controller, UseGuards, Get, Param, ParseIntPipe } from "@nestjs/common";
|
||||
import { TeamsSchedulesService } from "@/modules/teams/schedules/services/teams-schedules.service";
|
||||
import { Controller, UseGuards, Get, Param, ParseIntPipe, Query } from "@nestjs/common";
|
||||
import { ApiHeader, ApiOperation, ApiTags as DocsTags } from "@nestjs/swagger";
|
||||
|
||||
import { SUCCESS_STATUS } from "@calcom/platform-constants";
|
||||
import { GetSchedulesOutput_2024_06_11 } from "@calcom/platform-types";
|
||||
import { GetSchedulesOutput_2024_06_11, SkipTakePagination } from "@calcom/platform-types";
|
||||
|
||||
@Controller({
|
||||
path: "/v2/organizations/:orgId/teams/:teamId",
|
||||
@@ -29,7 +30,31 @@ import { GetSchedulesOutput_2024_06_11 } from "@calcom/platform-types";
|
||||
@ApiHeader(OPTIONAL_X_CAL_SECRET_KEY_HEADER)
|
||||
@ApiHeader(OPTIONAL_API_KEY_HEADER)
|
||||
export class OrganizationsTeamsSchedulesController {
|
||||
constructor(private schedulesService: SchedulesService_2024_06_11) {}
|
||||
constructor(
|
||||
private schedulesService: SchedulesService_2024_06_11,
|
||||
|
||||
private teamsSchedulesService: TeamsSchedulesService
|
||||
) {}
|
||||
|
||||
@UseGuards(IsTeamInOrg)
|
||||
@Roles("TEAM_ADMIN")
|
||||
@PlatformPlan("ESSENTIALS")
|
||||
@Get("/schedules")
|
||||
@DocsTags("Orgs / Teams / Schedules")
|
||||
@ApiOperation({ summary: "Get all team member schedules" })
|
||||
async getTeamSchedules(
|
||||
@Param("teamId", ParseIntPipe) teamId: number,
|
||||
@Query() queryParams: SkipTakePagination
|
||||
): Promise<GetSchedulesOutput_2024_06_11> {
|
||||
const { skip, take } = queryParams;
|
||||
|
||||
const schedules = await this.teamsSchedulesService.getTeamSchedules(teamId, skip, take);
|
||||
|
||||
return {
|
||||
status: SUCCESS_STATUS,
|
||||
data: schedules,
|
||||
};
|
||||
}
|
||||
|
||||
@Roles("TEAM_ADMIN")
|
||||
@PlatformPlan("ESSENTIALS")
|
||||
|
||||
+39
@@ -18,6 +18,7 @@ import { randomString } from "test/utils/randomString";
|
||||
import { withApiAuth } from "test/utils/withApiAuth";
|
||||
|
||||
import { SUCCESS_STATUS } from "@calcom/platform-constants";
|
||||
import { GetSchedulesOutput_2024_06_11 } from "@calcom/platform-types";
|
||||
import { ApiSuccessResponse, ScheduleOutput_2024_06_11 } from "@calcom/platform-types";
|
||||
import { Team, Schedule } from "@calcom/prisma/client";
|
||||
|
||||
@@ -36,6 +37,8 @@ describe("Organizations Teams Schedules Endpoints", () => {
|
||||
let org: Team;
|
||||
let orgTeam: Team;
|
||||
let nonOrgTeam: Team;
|
||||
|
||||
let userSchedule: Schedule;
|
||||
let user2Schedule: Schedule;
|
||||
|
||||
const userEmail = `organizations-teams-schedules-admin-${randomString()}@api.com`;
|
||||
@@ -73,6 +76,16 @@ describe("Organizations Teams Schedules Endpoints", () => {
|
||||
username: userEmail2,
|
||||
});
|
||||
|
||||
userSchedule = await scheduleRepositoryFixture.create({
|
||||
user: {
|
||||
connect: {
|
||||
id: user.id,
|
||||
},
|
||||
},
|
||||
name: `organizations-teams-schedules-user-schedule-${randomString()}`,
|
||||
timeZone: "America/New_York",
|
||||
});
|
||||
|
||||
user2Schedule = await scheduleRepositoryFixture.create({
|
||||
user: {
|
||||
connect: {
|
||||
@@ -214,6 +227,32 @@ describe("Organizations Teams Schedules Endpoints", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("should get all the schedules of members in a team", async () => {
|
||||
return request(app.getHttpServer())
|
||||
.get(`/v2/organizations/${org.id}/teams/${orgTeam.id}/schedules`)
|
||||
.expect(200)
|
||||
.then((response) => {
|
||||
const responseBody: GetSchedulesOutput_2024_06_11 = response.body;
|
||||
expect(responseBody.status).toEqual(SUCCESS_STATUS);
|
||||
expect(Array.isArray(responseBody.data)).toBe(true);
|
||||
expect(responseBody.data.length).toEqual(2);
|
||||
|
||||
const userOneSchedule = responseBody.data.find((schedule) => schedule.id === userSchedule.id);
|
||||
const userTwoSchedule = responseBody.data.find((schedule) => schedule.id === user2Schedule.id);
|
||||
|
||||
expect(userOneSchedule).toBeDefined();
|
||||
expect(userTwoSchedule).toBeDefined();
|
||||
|
||||
expect(userOneSchedule?.id).toEqual(userSchedule.id);
|
||||
expect(userOneSchedule?.name).toEqual(userSchedule.name);
|
||||
expect(userOneSchedule?.timeZone).toEqual(userSchedule.timeZone);
|
||||
|
||||
expect(userTwoSchedule?.id).toEqual(user2Schedule.id);
|
||||
expect(userTwoSchedule?.name).toEqual(user2Schedule.name);
|
||||
expect(userOneSchedule?.timeZone).toEqual(user2Schedule.timeZone);
|
||||
});
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
await userRepositoryFixture.deleteByEmail(user.email);
|
||||
await userRepositoryFixture.deleteByEmail(userToInviteViaApi.email);
|
||||
|
||||
+21
-1
@@ -12,7 +12,12 @@ import {
|
||||
EMAIL,
|
||||
WorkflowEmailAttendeeStepDto,
|
||||
} from "@/modules/workflows/inputs/workflow-step.input";
|
||||
import { BEFORE_EVENT, DAY } from "@/modules/workflows/inputs/workflow-trigger.input";
|
||||
import {
|
||||
BEFORE_EVENT,
|
||||
DAY,
|
||||
OnAfterEventTriggerDto,
|
||||
OnBeforeEventTriggerDto,
|
||||
} from "@/modules/workflows/inputs/workflow-trigger.input";
|
||||
// Adjust path if needed
|
||||
import { GetWorkflowOutput, GetWorkflowsOutput } from "@/modules/workflows/outputs/workflow.output";
|
||||
import { INestApplication } from "@nestjs/common";
|
||||
@@ -255,6 +260,10 @@ describe("OrganizationsTeamsWorkflowsController (E2E)", () => {
|
||||
"+37255555555"
|
||||
);
|
||||
expect(responseBody.data.steps.find((step) => step.stepNumber === 3)?.email).toEqual(authEmail);
|
||||
const trigger = sampleCreateWorkflowDto.trigger as OnBeforeEventTriggerDto;
|
||||
expect(responseBody.data.trigger?.offset?.value).toEqual(trigger.offset.value);
|
||||
expect(responseBody.data.trigger?.offset?.unit).toEqual(trigger.offset.unit);
|
||||
|
||||
createdWorkflowId = responseBody.data.id;
|
||||
createdWorkflow = responseBody.data;
|
||||
});
|
||||
@@ -330,6 +339,13 @@ describe("OrganizationsTeamsWorkflowsController (E2E)", () => {
|
||||
expect(step1).toBeDefined();
|
||||
const partialUpdateDto: Partial<CreateWorkflowDto> = {
|
||||
name: updatedName,
|
||||
trigger: {
|
||||
type: "afterEvent",
|
||||
offset: {
|
||||
unit: "minute",
|
||||
value: 10,
|
||||
},
|
||||
},
|
||||
steps: step1 ? [{ ...step1, sender: "updatedSender" } as WorkflowEmailAttendeeStepDto] : [],
|
||||
};
|
||||
expect(createdWorkflowId).toBeDefined();
|
||||
@@ -348,6 +364,10 @@ describe("OrganizationsTeamsWorkflowsController (E2E)", () => {
|
||||
step1 && expect(responseBody.data.steps[0].id).toEqual(step1.id);
|
||||
expect(responseBody.data.steps[0].sender).toEqual("updatedSender");
|
||||
expect(responseBody.data.steps[1]?.id).toBeUndefined();
|
||||
const trigger = partialUpdateDto.trigger as OnAfterEventTriggerDto;
|
||||
expect(responseBody.data.trigger?.type).toEqual(trigger.type);
|
||||
expect(responseBody.data.trigger?.offset?.value).toEqual(trigger.offset.value);
|
||||
expect(responseBody.data.trigger?.offset?.unit).toEqual(trigger.offset.unit);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -156,17 +156,4 @@ export class TeamsEventTypesService {
|
||||
|
||||
return this.eventTypesRepository.deleteEventType(eventTypeId);
|
||||
}
|
||||
|
||||
async deleteUserTeamEventTypesAndHosts(userId: number, teamId: number) {
|
||||
try {
|
||||
await this.teamsEventTypesRepository.deleteUserManagedTeamEventTypes(userId, teamId);
|
||||
await this.teamsEventTypesRepository.removeUserFromTeamEventTypesHosts(userId, teamId);
|
||||
} catch (err) {
|
||||
this.logger.error("Could not remove user from all team event-types.", {
|
||||
error: err,
|
||||
userId,
|
||||
teamId,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
+1
-8
@@ -3,7 +3,6 @@ import { API_KEY_HEADER } from "@/lib/docs/headers";
|
||||
import { Roles } from "@/modules/auth/decorators/roles/roles.decorator";
|
||||
import { ApiAuthGuard } from "@/modules/auth/guards/api-auth/api-auth.guard";
|
||||
import { RolesGuard } from "@/modules/auth/guards/roles/roles.guard";
|
||||
import { TeamsEventTypesService } from "@/modules/teams/event-types/services/teams-event-types.service";
|
||||
import { CreateTeamMembershipInput } from "@/modules/teams/memberships/inputs/create-team-membership.input";
|
||||
import { UpdateTeamMembershipInput } from "@/modules/teams/memberships/inputs/update-team-membership.input";
|
||||
import { CreateTeamMembershipOutput } from "@/modules/teams/memberships/outputs/create-team-membership.output";
|
||||
@@ -45,10 +44,7 @@ import { SkipTakePagination } from "@calcom/platform-types";
|
||||
export class TeamsMembershipsController {
|
||||
private logger = new Logger("TeamsMembershipsController");
|
||||
|
||||
constructor(
|
||||
private teamsMembershipsService: TeamsMembershipsService,
|
||||
private teamsEventTypesService: TeamsEventTypesService
|
||||
) {}
|
||||
constructor(private teamsMembershipsService: TeamsMembershipsService) {}
|
||||
|
||||
@Roles("TEAM_ADMIN")
|
||||
@Post("/")
|
||||
@@ -127,7 +123,6 @@ export class TeamsMembershipsController {
|
||||
membershipId,
|
||||
body
|
||||
);
|
||||
|
||||
if (!currentMembership.accepted && updatedMembership.accepted) {
|
||||
try {
|
||||
await updateNewTeamMemberEventTypes(updatedMembership.userId, teamId);
|
||||
@@ -151,8 +146,6 @@ export class TeamsMembershipsController {
|
||||
): Promise<DeleteTeamMembershipOutput> {
|
||||
const membership = await this.teamsMembershipsService.deleteTeamMembership(teamId, membershipId);
|
||||
|
||||
await this.teamsEventTypesService.deleteUserTeamEventTypesAndHosts(membership.userId, teamId);
|
||||
|
||||
return {
|
||||
status: SUCCESS_STATUS,
|
||||
data: plainToClass(TeamMembershipOutput, membership, { strategy: "excludeAll" }),
|
||||
|
||||
@@ -3,6 +3,8 @@ import { UpdateTeamMembershipInput } from "@/modules/teams/memberships/inputs/up
|
||||
import { TeamsMembershipsRepository } from "@/modules/teams/memberships/teams-memberships.repository";
|
||||
import { Injectable, NotFoundException } from "@nestjs/common";
|
||||
|
||||
import { TeamService } from "@calcom/platform-libraries";
|
||||
|
||||
@Injectable()
|
||||
export class TeamsMembershipsService {
|
||||
constructor(private readonly teamsMembershipsRepository: TeamsMembershipsRepository) {}
|
||||
@@ -41,10 +43,15 @@ export class TeamsMembershipsService {
|
||||
}
|
||||
|
||||
async deleteTeamMembership(teamId: number, membershipId: number) {
|
||||
const teamMembership = await this.teamsMembershipsRepository.deleteTeamMembershipById(
|
||||
teamId,
|
||||
membershipId
|
||||
);
|
||||
// First get the membership to get the userId
|
||||
const teamMembership = await this.teamsMembershipsRepository.findTeamMembership(teamId, membershipId);
|
||||
|
||||
if (!teamMembership) {
|
||||
throw new NotFoundException(`Membership with id ${membershipId} not found in team ${teamId}`);
|
||||
}
|
||||
|
||||
await TeamService.removeMembers({ teamIds: [teamId], userIds: [teamMembership.userId], isOrg: false });
|
||||
|
||||
return teamMembership;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
import { API_VERSIONS_VALUES } from "@/lib/api-versions";
|
||||
import { API_KEY_OR_ACCESS_TOKEN_HEADER } from "@/lib/docs/headers";
|
||||
import { Roles } from "@/modules/auth/decorators/roles/roles.decorator";
|
||||
import { ApiAuthGuard } from "@/modules/auth/guards/api-auth/api-auth.guard";
|
||||
import { RolesGuard } from "@/modules/auth/guards/roles/roles.guard";
|
||||
import { TeamsSchedulesService } from "@/modules/teams/schedules/services/teams-schedules.service";
|
||||
import { Controller, UseGuards, Get, Param, ParseIntPipe, Query } from "@nestjs/common";
|
||||
import { ApiHeader, ApiOperation, ApiTags as DocsTags } from "@nestjs/swagger";
|
||||
|
||||
import { SUCCESS_STATUS } from "@calcom/platform-constants";
|
||||
import { GetSchedulesOutput_2024_06_11, SkipTakePagination } from "@calcom/platform-types";
|
||||
|
||||
@Controller({
|
||||
path: "/v2/teams/:teamId/schedules",
|
||||
version: API_VERSIONS_VALUES,
|
||||
})
|
||||
@UseGuards(ApiAuthGuard, RolesGuard)
|
||||
@DocsTags("Teams / Schedules")
|
||||
@ApiHeader(API_KEY_OR_ACCESS_TOKEN_HEADER)
|
||||
export class TeamsSchedulesController {
|
||||
constructor(private teamsSchedulesService: TeamsSchedulesService) {}
|
||||
|
||||
@Roles("TEAM_ADMIN")
|
||||
@Get("/")
|
||||
@ApiOperation({
|
||||
summary: "Get all team member schedules",
|
||||
})
|
||||
async getTeamSchedules(
|
||||
@Param("teamId", ParseIntPipe) teamId: number,
|
||||
@Query() queryParams: SkipTakePagination
|
||||
): Promise<GetSchedulesOutput_2024_06_11> {
|
||||
const { skip, take } = queryParams;
|
||||
|
||||
const schedules = await this.teamsSchedulesService.getTeamSchedules(teamId, skip, take);
|
||||
|
||||
return {
|
||||
status: SUCCESS_STATUS,
|
||||
data: schedules,
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,143 @@
|
||||
import { bootstrap } from "@/app";
|
||||
import { AppModule } from "@/app.module";
|
||||
import { PrismaModule } from "@/modules/prisma/prisma.module";
|
||||
import { TokensModule } from "@/modules/tokens/tokens.module";
|
||||
import { UsersModule } from "@/modules/users/users.module";
|
||||
import { INestApplication } from "@nestjs/common";
|
||||
import { NestExpressApplication } from "@nestjs/platform-express";
|
||||
import { Test } from "@nestjs/testing";
|
||||
import { User } from "@prisma/client";
|
||||
import * as request from "supertest";
|
||||
import { MembershipRepositoryFixture } from "test/fixtures/repository/membership.repository.fixture";
|
||||
import { SchedulesRepositoryFixture } from "test/fixtures/repository/schedules.repository.fixture";
|
||||
import { TeamRepositoryFixture } from "test/fixtures/repository/team.repository.fixture";
|
||||
import { UserRepositoryFixture } from "test/fixtures/repository/users.repository.fixture";
|
||||
import { randomString } from "test/utils/randomString";
|
||||
import { withApiAuth } from "test/utils/withApiAuth";
|
||||
|
||||
import { SUCCESS_STATUS } from "@calcom/platform-constants";
|
||||
import { GetSchedulesOutput_2024_06_11 } from "@calcom/platform-types";
|
||||
import { Team, Schedule } from "@calcom/prisma/client";
|
||||
|
||||
describe("Teams Schedules Endpoints", () => {
|
||||
describe("User Authentication - User is Team Admin", () => {
|
||||
let app: INestApplication;
|
||||
|
||||
let userRepositoryFixture: UserRepositoryFixture;
|
||||
|
||||
let teamsRepositoryFixture: TeamRepositoryFixture;
|
||||
let scheduleRepositoryFixture: SchedulesRepositoryFixture;
|
||||
|
||||
let membershipsRepositoryFixture: MembershipRepositoryFixture;
|
||||
|
||||
let nonOrgTeam: Team;
|
||||
|
||||
let userSchedule: Schedule;
|
||||
let user2Schedule: Schedule;
|
||||
|
||||
const userEmail = `teams-schedules-admin-${randomString()}@api.com`;
|
||||
const userEmail2 = `teams-schedules-member-${randomString()}@api.com`;
|
||||
|
||||
let user: User;
|
||||
let user2: User;
|
||||
|
||||
beforeAll(async () => {
|
||||
const moduleRef = await withApiAuth(
|
||||
userEmail,
|
||||
Test.createTestingModule({
|
||||
imports: [AppModule, PrismaModule, UsersModule, TokensModule],
|
||||
})
|
||||
).compile();
|
||||
|
||||
userRepositoryFixture = new UserRepositoryFixture(moduleRef);
|
||||
teamsRepositoryFixture = new TeamRepositoryFixture(moduleRef);
|
||||
|
||||
scheduleRepositoryFixture = new SchedulesRepositoryFixture(moduleRef);
|
||||
membershipsRepositoryFixture = new MembershipRepositoryFixture(moduleRef);
|
||||
|
||||
user = await userRepositoryFixture.create({
|
||||
email: userEmail,
|
||||
username: userEmail,
|
||||
});
|
||||
user2 = await userRepositoryFixture.create({
|
||||
email: userEmail2,
|
||||
username: userEmail2,
|
||||
});
|
||||
|
||||
userSchedule = await scheduleRepositoryFixture.create({
|
||||
user: {
|
||||
connect: {
|
||||
id: user.id,
|
||||
},
|
||||
},
|
||||
name: `teams-schedules-user-schedule-${randomString()}`,
|
||||
timeZone: "America/New_York",
|
||||
});
|
||||
|
||||
user2Schedule = await scheduleRepositoryFixture.create({
|
||||
user: {
|
||||
connect: {
|
||||
id: user2.id,
|
||||
},
|
||||
},
|
||||
name: `teams-schedules-user2-schedule-${randomString()}`,
|
||||
timeZone: "America/New_York",
|
||||
});
|
||||
|
||||
nonOrgTeam = await teamsRepositoryFixture.create({
|
||||
name: `teams-schedules-non-org-team-${randomString()}`,
|
||||
isOrganization: false,
|
||||
});
|
||||
|
||||
await membershipsRepositoryFixture.create({
|
||||
role: "ADMIN",
|
||||
user: { connect: { id: user.id } },
|
||||
team: { connect: { id: nonOrgTeam.id } },
|
||||
});
|
||||
|
||||
await membershipsRepositoryFixture.create({
|
||||
role: "MEMBER",
|
||||
user: { connect: { id: user2.id } },
|
||||
team: { connect: { id: nonOrgTeam.id } },
|
||||
});
|
||||
|
||||
app = moduleRef.createNestApplication();
|
||||
bootstrap(app as NestExpressApplication);
|
||||
|
||||
await app.init();
|
||||
});
|
||||
|
||||
it("should get all the schedules of members in a team", async () => {
|
||||
return request(app.getHttpServer())
|
||||
.get(`/v2/teams/${nonOrgTeam.id}/schedules`)
|
||||
.expect(200)
|
||||
.then((response) => {
|
||||
const responseBody: GetSchedulesOutput_2024_06_11 = response.body;
|
||||
expect(responseBody.status).toEqual(SUCCESS_STATUS);
|
||||
expect(Array.isArray(responseBody.data)).toBe(true);
|
||||
expect(responseBody.data.length).toEqual(2);
|
||||
|
||||
const userOneSchedule = responseBody.data.find((schedule) => schedule.id === userSchedule.id);
|
||||
const userTwoSchedule = responseBody.data.find((schedule) => schedule.id === user2Schedule.id);
|
||||
|
||||
expect(userOneSchedule).toBeDefined();
|
||||
expect(userTwoSchedule).toBeDefined();
|
||||
|
||||
expect(userOneSchedule?.id).toEqual(userSchedule.id);
|
||||
expect(userOneSchedule?.name).toEqual(userSchedule.name);
|
||||
expect(userOneSchedule?.timeZone).toEqual(userSchedule.timeZone);
|
||||
|
||||
expect(userTwoSchedule?.id).toEqual(user2Schedule.id);
|
||||
expect(userTwoSchedule?.name).toEqual(user2Schedule.name);
|
||||
expect(userOneSchedule?.timeZone).toEqual(user2Schedule.timeZone);
|
||||
});
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
await userRepositoryFixture.deleteByEmail(user.email);
|
||||
await userRepositoryFixture.deleteByEmail(user2.email);
|
||||
await teamsRepositoryFixture.delete(nonOrgTeam.id);
|
||||
await app.close();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,28 @@
|
||||
import { SchedulesRepository_2024_06_11 } from "@/ee/schedules/schedules_2024_06_11/schedules.repository";
|
||||
import { OutputSchedulesService_2024_06_11 } from "@/ee/schedules/schedules_2024_06_11/services/output-schedules.service";
|
||||
import { TeamsRepository } from "@/modules/teams/teams/teams.repository";
|
||||
import { Injectable } from "@nestjs/common";
|
||||
|
||||
import { ScheduleOutput_2024_06_11 } from "@calcom/platform-types";
|
||||
|
||||
@Injectable()
|
||||
export class TeamsSchedulesService {
|
||||
constructor(
|
||||
private readonly teamsRepository: TeamsRepository,
|
||||
private readonly schedulesRepository: SchedulesRepository_2024_06_11,
|
||||
private readonly outputSchedulesService: OutputSchedulesService_2024_06_11
|
||||
) {}
|
||||
|
||||
async getTeamSchedules(teamId: number, skip = 0, take = 250) {
|
||||
const userIds = await this.teamsRepository.getTeamUsersIds(teamId);
|
||||
const schedules = await this.schedulesRepository.getSchedulesByUserIds(userIds, skip, take);
|
||||
|
||||
const responseSchedules: ScheduleOutput_2024_06_11[] = [];
|
||||
|
||||
for (const schedule of schedules) {
|
||||
responseSchedules.push(await this.outputSchedulesService.getResponseSchedule(schedule));
|
||||
}
|
||||
|
||||
return responseSchedules;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
import { SchedulesRepository_2024_06_11 } from "@/ee/schedules/schedules_2024_06_11/schedules.repository";
|
||||
import { OutputSchedulesService_2024_06_11 } from "@/ee/schedules/schedules_2024_06_11/services/output-schedules.service";
|
||||
import { AppsRepository } from "@/modules/apps/apps.repository";
|
||||
import { CredentialsRepository } from "@/modules/credentials/credentials.repository";
|
||||
import { MembershipsModule } from "@/modules/memberships/memberships.module";
|
||||
import { OrganizationsRepository } from "@/modules/organizations/index/organizations.repository";
|
||||
import { OrganizationSchedulesRepository } from "@/modules/organizations/schedules/organizations-schedules.repository";
|
||||
import { OrganizationsTeamsRepository } from "@/modules/organizations/teams/index/organizations-teams.repository";
|
||||
import { PrismaModule } from "@/modules/prisma/prisma.module";
|
||||
import { RedisModule } from "@/modules/redis/redis.module";
|
||||
import { StripeService } from "@/modules/stripe/stripe.service";
|
||||
import { TeamsMembershipsService } from "@/modules/teams/memberships/services/teams-memberships.service";
|
||||
import { TeamsMembershipsRepository } from "@/modules/teams/memberships/teams-memberships.repository";
|
||||
import { TeamsSchedulesController } from "@/modules/teams/schedules/controllers/teams-schedules.controller";
|
||||
import { TeamsSchedulesService } from "@/modules/teams/schedules/services/teams-schedules.service";
|
||||
import { TeamsController } from "@/modules/teams/teams/controllers/teams.controller";
|
||||
import { TeamsService } from "@/modules/teams/teams/services/teams.service";
|
||||
import { TeamsRepository } from "@/modules/teams/teams/teams.repository";
|
||||
import { UsersRepository } from "@/modules/users/users.repository";
|
||||
import { Module } from "@nestjs/common";
|
||||
|
||||
@Module({
|
||||
imports: [PrismaModule, MembershipsModule, RedisModule],
|
||||
providers: [
|
||||
TeamsRepository,
|
||||
TeamsService,
|
||||
TeamsMembershipsRepository,
|
||||
TeamsMembershipsService,
|
||||
OutputSchedulesService_2024_06_11,
|
||||
OrganizationSchedulesRepository,
|
||||
StripeService,
|
||||
UsersRepository,
|
||||
AppsRepository,
|
||||
CredentialsRepository,
|
||||
OrganizationsRepository,
|
||||
TeamsSchedulesService,
|
||||
OrganizationsTeamsRepository,
|
||||
SchedulesRepository_2024_06_11,
|
||||
],
|
||||
controllers: [TeamsController, TeamsSchedulesController],
|
||||
exports: [TeamsRepository],
|
||||
})
|
||||
export class TeamsSchedulesModule {}
|
||||
@@ -1,7 +1,7 @@
|
||||
import { ApiProperty } from "@nestjs/swagger";
|
||||
import { TimeUnit, WorkflowTriggerEvents } from "@prisma/client";
|
||||
import { Type } from "class-transformer";
|
||||
import { IsNumber, ValidateNested } from "class-validator";
|
||||
import { IsIn, IsNumber, IsString, ValidateNested } from "class-validator";
|
||||
|
||||
export const BEFORE_EVENT = "beforeEvent";
|
||||
export const EVENT_CANCELLED = "eventCancelled";
|
||||
@@ -68,6 +68,8 @@ export class WorkflowTriggerOffsetDto {
|
||||
value!: number;
|
||||
|
||||
@ApiProperty({ description: "Unit for the offset time", example: HOUR })
|
||||
@IsString()
|
||||
@IsIn(TIME_UNITS)
|
||||
unit!: TimeUnitType;
|
||||
}
|
||||
|
||||
@@ -75,6 +77,8 @@ export class BaseWorkflowTriggerDto {
|
||||
@ApiProperty({
|
||||
description: "Trigger type for the workflow",
|
||||
})
|
||||
@IsString()
|
||||
@IsIn([WORKFLOW_TRIGGER_TYPES])
|
||||
type!: WorkflowTriggerType;
|
||||
}
|
||||
|
||||
@@ -82,6 +86,8 @@ export class OnCreationTriggerDto {
|
||||
@ApiProperty({
|
||||
description: "Trigger type for the workflow",
|
||||
})
|
||||
@IsString()
|
||||
@IsIn([NEW_EVENT])
|
||||
type: typeof NEW_EVENT = NEW_EVENT;
|
||||
}
|
||||
|
||||
@@ -89,12 +95,16 @@ export class OnRescheduleTriggerDto {
|
||||
@ApiProperty({
|
||||
description: "Trigger type for the workflow",
|
||||
})
|
||||
@IsString()
|
||||
@IsIn([RESCHEDULE_EVENT])
|
||||
type: typeof RESCHEDULE_EVENT = RESCHEDULE_EVENT;
|
||||
}
|
||||
export class OnCancelTriggerDto {
|
||||
@ApiProperty({
|
||||
description: "Trigger type for the workflow",
|
||||
})
|
||||
@IsString()
|
||||
@IsIn([EVENT_CANCELLED])
|
||||
type: typeof EVENT_CANCELLED = EVENT_CANCELLED;
|
||||
}
|
||||
|
||||
@@ -113,6 +123,8 @@ export class OnBeforeEventTriggerDto extends TriggerOffsetDTO {
|
||||
description: "Trigger type for the workflow",
|
||||
example: BEFORE_EVENT,
|
||||
})
|
||||
@IsString()
|
||||
@IsIn([BEFORE_EVENT])
|
||||
type: typeof BEFORE_EVENT = BEFORE_EVENT;
|
||||
}
|
||||
|
||||
@@ -121,6 +133,8 @@ export class OnAfterEventTriggerDto extends TriggerOffsetDTO {
|
||||
description: "Trigger type for the workflow",
|
||||
example: AFTER_EVENT,
|
||||
})
|
||||
@IsString()
|
||||
@IsIn([AFTER_EVENT])
|
||||
type: typeof AFTER_EVENT = AFTER_EVENT;
|
||||
}
|
||||
|
||||
@@ -129,6 +143,8 @@ export class OnAfterCalVideoGuestsNoShowTriggerDto extends TriggerOffsetDTO {
|
||||
description: "Trigger type for the workflow",
|
||||
example: AFTER_GUESTS_CAL_VIDEO_NO_SHOW,
|
||||
})
|
||||
@IsString()
|
||||
@IsIn([AFTER_GUESTS_CAL_VIDEO_NO_SHOW])
|
||||
type: typeof AFTER_GUESTS_CAL_VIDEO_NO_SHOW = AFTER_GUESTS_CAL_VIDEO_NO_SHOW;
|
||||
}
|
||||
|
||||
@@ -137,5 +153,7 @@ export class OnAfterCalVideoHostsNoShowTriggerDto extends TriggerOffsetDTO {
|
||||
description: "Trigger type for the workflow",
|
||||
example: AFTER_HOSTS_CAL_VIDEO_NO_SHOW,
|
||||
})
|
||||
@IsString()
|
||||
@IsIn([AFTER_HOSTS_CAL_VIDEO_NO_SHOW])
|
||||
type: typeof AFTER_HOSTS_CAL_VIDEO_NO_SHOW = AFTER_HOSTS_CAL_VIDEO_NO_SHOW;
|
||||
}
|
||||
@@ -59,4 +59,8 @@ export class EventTypesRepositoryFixture {
|
||||
async delete(eventTypeId: EventType["id"]) {
|
||||
return this.prismaWriteClient.eventType.delete({ where: { id: eventTypeId } });
|
||||
}
|
||||
|
||||
async findById(eventTypeId: EventType["id"]) {
|
||||
return this.prismaReadClient.eventType.findUnique({ where: { id: eventTypeId } });
|
||||
}
|
||||
}
|
||||
@@ -35,4 +35,8 @@ export class MembershipRepositoryFixture {
|
||||
await this.prismaWriteClient.user.update({ where: { id: user.id }, data: { organizationId: org.id } });
|
||||
return membership;
|
||||
}
|
||||
|
||||
async findById(membershipId: Membership["id"]) {
|
||||
return this.prismaReadClient.membership.findUnique({ where: { id: membershipId } });
|
||||
}
|
||||
}
|
||||
@@ -45,11 +45,13 @@ const InstalledAppsWrapper = async ({ params }: PageProps) => {
|
||||
createRouterCaller(appsRouter),
|
||||
]);
|
||||
|
||||
const connectedCalendars = await calendarsCaller.connectedCalendars();
|
||||
const installedCalendars = await appsCaller.integrations({
|
||||
variant: "calendar",
|
||||
onlyInstalled: true,
|
||||
});
|
||||
const [connectedCalendars, installedCalendars] = await Promise.all([
|
||||
calendarsCaller.connectedCalendars(),
|
||||
appsCaller.integrations({
|
||||
variant: "calendar",
|
||||
onlyInstalled: true,
|
||||
}),
|
||||
]);
|
||||
|
||||
return (
|
||||
<InstalledApps
|
||||
|
||||
+4
-1
@@ -5,6 +5,7 @@ import { unstable_cache } from "next/cache";
|
||||
import { RoleManagementFactory } from "@calcom/features/pbac/services/role-management.factory";
|
||||
import SettingsHeader from "@calcom/features/settings/appDir/SettingsHeader";
|
||||
import { PrismaAttributeRepository } from "@calcom/lib/server/repository/PrismaAttributeRepository";
|
||||
import prisma from "@calcom/prisma";
|
||||
import { viewerTeamsRouter } from "@calcom/trpc/server/routers/viewer/teams/_router";
|
||||
|
||||
import { TeamMembersView } from "~/teams/team-members-view";
|
||||
@@ -36,8 +37,10 @@ const getCachedTeamRoles = unstable_cache(
|
||||
const getCachedTeamAttributes = unstable_cache(
|
||||
async (organizationId?: number) => {
|
||||
if (!organizationId) return [];
|
||||
const attributeRepo = new PrismaAttributeRepository(prisma);
|
||||
|
||||
try {
|
||||
return await PrismaAttributeRepository.findAllByOrgIdWithOptions({ orgId: organizationId });
|
||||
return await attributeRepo.findAllByOrgIdWithOptions({ orgId: organizationId });
|
||||
} catch (error) {
|
||||
return [];
|
||||
}
|
||||
|
||||
+4
-1
@@ -4,6 +4,7 @@ import { unstable_cache } from "next/cache";
|
||||
|
||||
import { RoleManagementFactory } from "@calcom/features/pbac/services/role-management.factory";
|
||||
import { PrismaAttributeRepository } from "@calcom/lib/server/repository/PrismaAttributeRepository";
|
||||
import prisma from "@calcom/prisma";
|
||||
import { viewerOrganizationsRouter } from "@calcom/trpc/server/routers/viewer/organizations/_router";
|
||||
|
||||
import { MembersView } from "~/members/members-view";
|
||||
@@ -19,7 +20,9 @@ export const generateMetadata = async () =>
|
||||
|
||||
const getCachedAttributes = unstable_cache(
|
||||
async (orgId: number) => {
|
||||
return await PrismaAttributeRepository.findAllByOrgIdWithOptions({ orgId });
|
||||
const attributeRepo = new PrismaAttributeRepository(prisma);
|
||||
|
||||
return await attributeRepo.findAllByOrgIdWithOptions({ orgId });
|
||||
},
|
||||
undefined,
|
||||
{ revalidate: 3600, tags: ["viewer.attributes.list"] } // Cache for 1 hour
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import { _generateMetadata } from "app/_utils";
|
||||
import { notFound } from "next/navigation";
|
||||
|
||||
import { FeaturesRepository } from "@calcom/features/flags/features.repository";
|
||||
|
||||
@@ -5,6 +5,7 @@ import { getLocale } from "@calcom/features/auth/lib/getLocale";
|
||||
import type { AppImageProps, MeetingImageProps } from "@calcom/lib/OgImages";
|
||||
import { constructAppImage, constructGenericImage, constructMeetingImage } from "@calcom/lib/OgImages";
|
||||
import { IS_CALCOM, WEBAPP_URL, APP_NAME, SEO_IMG_OGIMG, CAL_URL } from "@calcom/lib/constants";
|
||||
import { getCalcomUrl } from "@calcom/lib/getCalcomUrl";
|
||||
import { buildCanonical } from "@calcom/lib/next-seo.config";
|
||||
import { getTranslation } from "@calcom/lib/server/i18n";
|
||||
import { truncateOnWord } from "@calcom/lib/text";
|
||||
@@ -32,7 +33,7 @@ const _generateMetadataWithoutImage = async (
|
||||
const description = getDescription(t);
|
||||
const titleSuffix = `| ${APP_NAME}`;
|
||||
const displayedTitle = title.includes(titleSuffix) || hideBranding ? title : `${title} ${titleSuffix}`;
|
||||
const metadataBase = new URL(IS_CALCOM ? "https://cal.com" : WEBAPP_URL);
|
||||
const metadataBase = new URL(IS_CALCOM ? getCalcomUrl() : WEBAPP_URL);
|
||||
|
||||
return {
|
||||
title: title.length === 0 ? APP_NAME : displayedTitle,
|
||||
|
||||
@@ -87,7 +87,10 @@ async function handler(req: NextRequest) {
|
||||
|
||||
return new Response(img.body, {
|
||||
status: 200,
|
||||
headers: { "Content-Type": "image/png", "cache-control": "max-age=0" },
|
||||
headers: {
|
||||
"Content-Type": "image/png",
|
||||
"Cache-Control": "public, max-age=3600, stale-while-revalidate=86400",
|
||||
},
|
||||
});
|
||||
} catch (error) {
|
||||
if (error instanceof ZodError) {
|
||||
@@ -116,7 +119,13 @@ async function handler(req: NextRequest) {
|
||||
});
|
||||
const img = new ImageResponse(<App name={name} description={description} slug={slug} />, ogConfig);
|
||||
|
||||
return new Response(img.body, { status: 200, headers: { "Content-Type": "image/png" } });
|
||||
return new Response(img.body, {
|
||||
status: 200,
|
||||
headers: {
|
||||
"Content-Type": "image/png",
|
||||
"Cache-Control": "public, max-age=3600, stale-while-revalidate=86400",
|
||||
},
|
||||
});
|
||||
} catch (error) {
|
||||
if (error instanceof ZodError) {
|
||||
return new Response(
|
||||
@@ -144,7 +153,13 @@ async function handler(req: NextRequest) {
|
||||
|
||||
const img = new ImageResponse(<Generic title={title} description={description} />, ogConfig);
|
||||
|
||||
return new Response(img.body, { status: 200, headers: { "Content-Type": "image/png" } });
|
||||
return new Response(img.body, {
|
||||
status: 200,
|
||||
headers: {
|
||||
"Content-Type": "image/png",
|
||||
"Cache-Control": "public, max-age=3600, stale-while-revalidate=86400",
|
||||
},
|
||||
});
|
||||
} catch (error) {
|
||||
if (error instanceof ZodError) {
|
||||
return new Response(
|
||||
|
||||
@@ -9,6 +9,7 @@ import Script from "next/script";
|
||||
import "@calcom/embed-core/src/embed-iframe";
|
||||
import LicenseRequired from "@calcom/features/ee/common/components/LicenseRequired";
|
||||
import { IS_CALCOM, WEBAPP_URL } from "@calcom/lib/constants";
|
||||
import { getCalcomUrl } from "@calcom/lib/getCalcomUrl";
|
||||
import { buildCanonical } from "@calcom/lib/next-seo.config";
|
||||
import { IconSprites } from "@calcom/ui/components/icon";
|
||||
|
||||
@@ -64,7 +65,7 @@ function PageWrapper(props: AppProps) {
|
||||
// Set canonical to https://cal.com or self-hosted URL
|
||||
canonical={
|
||||
IS_CALCOM
|
||||
? buildCanonical({ path, origin: "https://cal.com" }) // cal.com & .dev
|
||||
? buildCanonical({ path, origin: getCalcomUrl() }) // cal.com & .dev
|
||||
: buildCanonical({ path, origin: WEBAPP_URL }) // self-hosted
|
||||
}
|
||||
{...seoConfig.defaultNextSeo}
|
||||
|
||||
@@ -2,9 +2,11 @@ import { useMemo } from "react";
|
||||
import { useFormContext } from "react-hook-form";
|
||||
import type { UseFormGetValues, UseFormSetValue, Control, FormState } from "react-hook-form";
|
||||
|
||||
import type { TLocationOptions } from "@calcom/features/eventtypes/components/Locations";
|
||||
import type { TEventTypeLocation } from "@calcom/features/eventtypes/components/Locations";
|
||||
import Locations from "@calcom/features/eventtypes/components/Locations";
|
||||
import type {
|
||||
TLocationOptions,
|
||||
TEventTypeLocation,
|
||||
} from "@calcom/features/eventtypes/components/locations/Locations";
|
||||
import Locations from "@calcom/features/eventtypes/components/locations/Locations";
|
||||
import type { LocationFormValues } from "@calcom/features/eventtypes/lib/types";
|
||||
import type { SingleValueLocationOption } from "@calcom/features/form/components/LocationSelect";
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
|
||||
@@ -1,34 +0,0 @@
|
||||
import type { GetServerSidePropsContext } from "next";
|
||||
import { getSession } from "next-auth/react";
|
||||
|
||||
import { UserPermissionRole } from "@calcom/prisma/enums";
|
||||
|
||||
export async function getServerSideProps(ctx: GetServerSidePropsContext) {
|
||||
const session = await getSession(ctx);
|
||||
if (!session || !session.user) {
|
||||
return {
|
||||
redirect: {
|
||||
destination: "/login",
|
||||
permanent: false,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
const isAdmin = session.user.role === UserPermissionRole.ADMIN;
|
||||
if (!isAdmin) {
|
||||
return {
|
||||
redirect: {
|
||||
destination: "/",
|
||||
permanent: false,
|
||||
},
|
||||
};
|
||||
}
|
||||
return {
|
||||
props: {
|
||||
error: null,
|
||||
migrated: null,
|
||||
userId: session.user.id,
|
||||
username: session.user.username,
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -1,29 +0,0 @@
|
||||
import type { GetServerSidePropsContext } from "next";
|
||||
import { getSession } from "next-auth/react";
|
||||
|
||||
import { UserPermissionRole } from "@calcom/prisma/enums";
|
||||
|
||||
export async function getServerSideProps(ctx: GetServerSidePropsContext) {
|
||||
const session = await getSession(ctx);
|
||||
if (!session || !session.user) {
|
||||
return {
|
||||
redirect: {
|
||||
destination: "/login",
|
||||
permanent: false,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
const isAdmin = session.user.role === UserPermissionRole.ADMIN;
|
||||
if (!isAdmin) {
|
||||
return {
|
||||
redirect: {
|
||||
destination: "/",
|
||||
permanent: false,
|
||||
},
|
||||
};
|
||||
}
|
||||
return {
|
||||
props: {},
|
||||
};
|
||||
}
|
||||
@@ -1,29 +0,0 @@
|
||||
import type { GetServerSidePropsContext } from "next";
|
||||
import { getSession } from "next-auth/react";
|
||||
|
||||
import { UserPermissionRole } from "@calcom/prisma/enums";
|
||||
|
||||
export async function getServerSideProps(ctx: GetServerSidePropsContext) {
|
||||
const session = await getSession(ctx);
|
||||
if (!session || !session.user) {
|
||||
return {
|
||||
redirect: {
|
||||
destination: "/login",
|
||||
permanent: false,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
const isAdmin = session.user.role === UserPermissionRole.ADMIN;
|
||||
if (!isAdmin) {
|
||||
return {
|
||||
redirect: {
|
||||
destination: "/",
|
||||
permanent: false,
|
||||
},
|
||||
};
|
||||
}
|
||||
return {
|
||||
props: {},
|
||||
};
|
||||
}
|
||||
@@ -1,29 +0,0 @@
|
||||
import type { GetServerSidePropsContext } from "next";
|
||||
import { getSession } from "next-auth/react";
|
||||
|
||||
import { UserPermissionRole } from "@calcom/prisma/enums";
|
||||
|
||||
export async function getServerSideProps(ctx: GetServerSidePropsContext) {
|
||||
const session = await getSession(ctx);
|
||||
if (!session || !session.user) {
|
||||
return {
|
||||
redirect: {
|
||||
destination: "/login",
|
||||
permanent: false,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
const isAdmin = session.user.role === UserPermissionRole.ADMIN;
|
||||
if (!isAdmin) {
|
||||
return {
|
||||
redirect: {
|
||||
destination: "/",
|
||||
permanent: false,
|
||||
},
|
||||
};
|
||||
}
|
||||
return {
|
||||
props: {},
|
||||
};
|
||||
}
|
||||
@@ -156,7 +156,7 @@ export function AvailabilityList({ availabilities }: AvailabilityListProps) {
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
<div className="text-default mb-16 mt-4 hidden text-center text-sm md:block">
|
||||
<div className="text-default mb-16 mt-4 block text-center text-sm">
|
||||
{t("temporarily_out_of_office")}{" "}
|
||||
<Link href="settings/my-account/out-of-office" className="underline">
|
||||
{t("add_a_redirect")}
|
||||
|
||||
@@ -180,7 +180,7 @@ function BookingsContent({ status }: BookingsProps) {
|
||||
columnHelper.accessor((row) => row.type === "data" && row.booking.user?.id, {
|
||||
id: "userId",
|
||||
header: t("member"),
|
||||
enableColumnFilter: true,
|
||||
enableColumnFilter: user?.isTeamAdminOrOwner ?? false,
|
||||
enableSorting: false,
|
||||
cell: () => null,
|
||||
meta: {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@calcom/web",
|
||||
"version": "5.6.0",
|
||||
"version": "5.6.2",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"analyze": "ANALYZE=true next build",
|
||||
|
||||
@@ -479,7 +479,6 @@ test.describe("Bookings", () => {
|
||||
const filterItemsConfig = [
|
||||
{ key: "eventTypeId", name: "Event Type", testId: "add-filter-item-eventTypeId" },
|
||||
{ key: "teamId", name: "Team", testId: "add-filter-item-teamId" },
|
||||
{ key: "userId", name: "Member", testId: "add-filter-item-userId" },
|
||||
{ key: "attendeeName", name: "Attendees Name", testId: "add-filter-item-attendeeName" },
|
||||
{ key: "attendeeEmail", name: "Attendee Email", testId: "add-filter-item-attendeeEmail" },
|
||||
{ key: "dateRange", name: "Date Range", testId: "add-filter-item-dateRange" },
|
||||
@@ -488,8 +487,16 @@ test.describe("Bookings", () => {
|
||||
|
||||
const getFilterItemLocator = (page: Page, testId: string) => page.locator(`[data-testid="${testId}"]`);
|
||||
|
||||
test.beforeEach(async ({ page, users }) => {
|
||||
const user = await users.create();
|
||||
const setup = async ({
|
||||
isAdmin,
|
||||
page,
|
||||
users,
|
||||
}: {
|
||||
isAdmin: boolean;
|
||||
page: Page;
|
||||
users: Fixtures["users"];
|
||||
}) => {
|
||||
const user = isAdmin ? await users.create(undefined, { hasTeam: true }) : await users.create();
|
||||
await user.apiLogin();
|
||||
const bookingsGetResponse = page.waitForResponse((response) =>
|
||||
/\/api\/trpc\/bookings\/get.*/.test(response.url())
|
||||
@@ -498,9 +505,10 @@ test.describe("Bookings", () => {
|
||||
await bookingsGetResponse;
|
||||
await page.locator('[data-testid="add-filter-button"]').click();
|
||||
await expect(page.locator(searchInputSelector)).toBeVisible();
|
||||
});
|
||||
};
|
||||
|
||||
test("should show all filter items initially and after clearing search", async ({ page }) => {
|
||||
test("should show all filter items initially and after clearing search", async ({ page, users }) => {
|
||||
await setup({ isAdmin: false, page, users });
|
||||
const searchInput = page.locator(searchInputSelector);
|
||||
|
||||
// Initial check: all defined filter items should be visible
|
||||
@@ -524,7 +532,18 @@ test.describe("Bookings", () => {
|
||||
}
|
||||
});
|
||||
|
||||
test("search should be case-insensitive", async ({ page }) => {
|
||||
test("should show admin-only filter", async ({ page, users }) => {
|
||||
await setup({ isAdmin: true, page, users });
|
||||
|
||||
await expect(
|
||||
getFilterItemLocator(page, "add-filter-item-userId"),
|
||||
`Item "Member" should be visible initially`
|
||||
).toBeVisible();
|
||||
});
|
||||
|
||||
test("search should be case-insensitive", async ({ page, users }) => {
|
||||
await setup({ isAdmin: true, page, users });
|
||||
|
||||
const searchInput = page.locator(searchInputSelector);
|
||||
|
||||
// Search for "member" (lowercase)
|
||||
@@ -534,7 +553,8 @@ test.describe("Bookings", () => {
|
||||
await expect(getFilterItemLocator(page, "add-filter-item-teamId")).toBeHidden();
|
||||
});
|
||||
|
||||
test("should individually find each filter item by its full name", async ({ page }) => {
|
||||
test("should individually find each filter item by its full name", async ({ page, users }) => {
|
||||
await setup({ isAdmin: false, page, users });
|
||||
const searchInput = page.locator(searchInputSelector);
|
||||
|
||||
for (const targetItem of filterItemsConfig) {
|
||||
@@ -550,7 +570,8 @@ test.describe("Bookings", () => {
|
||||
}
|
||||
});
|
||||
|
||||
test("should show no items for a non-matching search term", async ({ page }) => {
|
||||
test("should show no items for a non-matching search term", async ({ page, users }) => {
|
||||
await setup({ isAdmin: false, page, users });
|
||||
const searchInput = page.locator(searchInputSelector);
|
||||
const nonExistentTerm = "NonExistentFilterXYZ123";
|
||||
await searchInput.fill(nonExistentTerm);
|
||||
|
||||
@@ -6,6 +6,7 @@ import { test } from "./lib/fixtures";
|
||||
import {
|
||||
bookTimeSlot,
|
||||
confirmReschedule,
|
||||
confirmBooking,
|
||||
doOnOrgDomain,
|
||||
selectFirstAvailableTimeSlotNextMonth,
|
||||
selectSecondAvailableTimeSlotNextMonth,
|
||||
@@ -98,6 +99,58 @@ test("dynamic booking info prefilled by query params", async ({ page, users }) =
|
||||
activeState = await listItemLocator.getAttribute("data-active");
|
||||
expect(activeState).toEqual("false");
|
||||
});
|
||||
|
||||
test("multiple duration selection updates event length correctly", async ({ page, users }) => {
|
||||
const user = await users.create();
|
||||
await user.apiLogin();
|
||||
|
||||
await test.step("update event title to include duration placeholder", async () => {
|
||||
await page.goto("/event-types");
|
||||
await page.waitForSelector('[data-testid="event-types"]');
|
||||
await page.click(`text=Multiple duration`);
|
||||
await page.waitForSelector('[data-testid="event-title"]');
|
||||
await expect(page.getByTestId("vertical-tab-event_setup_tab_title")).toHaveAttribute(
|
||||
"aria-current",
|
||||
"page"
|
||||
);
|
||||
await page.getByTestId("vertical-tab-event_advanced_tab_title").click();
|
||||
await page.fill('[name="eventName"]', "{Event duration} event btwn {Organiser} {Scheduler}");
|
||||
await page.locator('[data-testid="update-eventtype"]').click();
|
||||
await page.waitForResponse("/api/trpc/eventTypes/update?batch=1");
|
||||
});
|
||||
|
||||
await page.goto(`/${user.username}/multiple-duration`);
|
||||
|
||||
await page.locator('[data-testid="multiple-choice-30mins"]').waitFor({ state: "visible" });
|
||||
|
||||
await test.step("verify default 30min duration is selected", async () => {
|
||||
const duration30 = page.getByTestId("multiple-choice-30mins");
|
||||
const activeState = await duration30.getAttribute("data-active");
|
||||
expect(activeState).toEqual("true");
|
||||
});
|
||||
|
||||
await test.step("book with 90min duration and verify title", async () => {
|
||||
await page.getByTestId("multiple-choice-90mins").click();
|
||||
|
||||
const duration90 = page.getByTestId("multiple-choice-90mins");
|
||||
const activeState = await duration90.getAttribute("data-active");
|
||||
expect(activeState).toEqual("true");
|
||||
|
||||
await selectFirstAvailableTimeSlotNextMonth(page);
|
||||
|
||||
await page.fill('[name="name"]', "Test User");
|
||||
await page.fill('[name="email"]', "test@example.com");
|
||||
await confirmBooking(page);
|
||||
|
||||
await expect(page.locator("[data-testid=success-page]")).toBeVisible();
|
||||
|
||||
const bookingTitle = page.locator('[data-testid="booking-title"]');
|
||||
await expect(bookingTitle).toBeVisible();
|
||||
const titleText = await bookingTitle.textContent();
|
||||
expect(titleText).toContain("90");
|
||||
});
|
||||
});
|
||||
|
||||
// eslint-disable-next-line playwright/no-skipped-test
|
||||
test.skip("it contains the right event details", async ({ page }) => {
|
||||
const response = await page.goto(`http://acme.cal.local:3000/owner1+member1`);
|
||||
|
||||
@@ -76,7 +76,7 @@ Sometimes, yarn install might fail during deployment on Vercel, in which case, y
|
||||
<Step title="Override the build command">
|
||||
Override to:
|
||||
```
|
||||
cd ../.. && yarn build --include-dependencies --no-deps
|
||||
cd ../.. && yarn build --no-deps
|
||||
```
|
||||
</Step>
|
||||
<Step title="Hit Deploy">
|
||||
|
||||
@@ -33,8 +33,8 @@ RUN set -eux; \
|
||||
apt-get clean && \
|
||||
rm -rf /var/lib/apt/lists /var/cache/apt/archives && \
|
||||
yarn config set httpTimeout 1200000 && \
|
||||
npx turbo prune --scope=@calcom/web --docker && \
|
||||
npx turbo prune --scope=@calcom/api --docker && \
|
||||
npx turbo prune --filter=@calcom/web --docker && \
|
||||
npx turbo prune --filter=@calcom/api --docker && \
|
||||
yarn install && \
|
||||
yarn turbo run build --filter=@calcom/api
|
||||
|
||||
|
||||
+13
-13
@@ -28,23 +28,23 @@
|
||||
"edit-app-template": "yarn app-store edit-template",
|
||||
"delete-app-template": "yarn app-store delete-template",
|
||||
"build": "turbo run build --filter=@calcom/web...",
|
||||
"build:ai": "turbo run build --scope=\"@calcom/ai\"",
|
||||
"build:ai": "turbo run build --filter=\"@calcom/ai\"",
|
||||
"clean": "find . -name node_modules -o -name .next -o -name .turbo -o -name dist -type d -prune | xargs rm -rf",
|
||||
"db-deploy": "turbo run db-deploy",
|
||||
"db-seed": "turbo run db-seed",
|
||||
"db-studio": "yarn prisma studio",
|
||||
"deploy": "turbo run deploy",
|
||||
"dev:all": "turbo run dev --scope=\"@calcom/web\" --scope=\"@calcom/website\" --scope=\"@calcom/console\"",
|
||||
"dev:ai": "turbo run dev --scope=\"@calcom/web\" --scope=\"@calcom/api-proxy\" --scope=\"@calcom/api\" --scope=\"@calcom/ai\"",
|
||||
"dev:api": "turbo run dev --scope=\"@calcom/web\" --scope=\"@calcom/api-proxy\" --scope=\"@calcom/api\"",
|
||||
"dev:api:console": "turbo run dev --scope=\"@calcom/web\" --scope=\"@calcom/api-proxy\" --scope=\"@calcom/api\" --scope=\"@calcom/console\"",
|
||||
"dev:console": "turbo run dev --scope=\"@calcom/web\" --scope=\"@calcom/console\"",
|
||||
"dev:swagger": "turbo run dev --scope=\"@calcom/api-proxy\" --scope=\"@calcom/api\" --scope=\"@calcom/swagger\"",
|
||||
"dev:website": "turbo run dev --scope=\"@calcom/web\" --scope=\"@calcom/website\"",
|
||||
"dev": "turbo run dev --scope=\"@calcom/web\"",
|
||||
"build-storybook": "turbo run build --scope=\"@calcom/storybook\"",
|
||||
"dev:all": "turbo run dev --filter=\"@calcom/web\" --filter=\"@calcom/website\" --filter=\"@calcom/console\"",
|
||||
"dev:ai": "turbo run dev --filter=\"@calcom/web\" --filter=\"@calcom/api-proxy\" --filter=\"@calcom/api\" --filter=\"@calcom/ai\"",
|
||||
"dev:api": "turbo run dev --filter=\"@calcom/web\" --filter=\"@calcom/api-proxy\" --filter=\"@calcom/api\"",
|
||||
"dev:api:console": "turbo run dev --filter=\"@calcom/web\" --filter=\"@calcom/api-proxy\" --filter=\"@calcom/api\" --filter=\"@calcom/console\"",
|
||||
"dev:console": "turbo run dev --filter=\"@calcom/web\" --filter=\"@calcom/console\"",
|
||||
"dev:swagger": "turbo run dev --filter=\"@calcom/api-proxy\" --filter=\"@calcom/api\" --filter=\"@calcom/swagger\"",
|
||||
"dev:website": "turbo run dev --filter=\"@calcom/web\" --filter=\"@calcom/website\"",
|
||||
"dev": "turbo run dev --filter=\"@calcom/web\"",
|
||||
"build-storybook": "turbo run build --filter=\"@calcom/storybook\"",
|
||||
"dx": "turbo run dx",
|
||||
"i-dev": "infisical run -- turbo run dev --scope=\"@calcom/web\"",
|
||||
"i-dev": "infisical run -- turbo run dev --filter=\"@calcom/web\"",
|
||||
"i-dx": "infisical run -- turbo run dx",
|
||||
"i-gen-web-example-env": "infisical secrets generate-example-env --tags=web > .env.example",
|
||||
"i-gen-app-store-example-env": "infisical secrets generate-example-env --tags=appstore > .env.appStore.example",
|
||||
@@ -61,7 +61,7 @@
|
||||
"pre-commit": "lint-staged",
|
||||
"predev": "echo 'Checking env files'",
|
||||
"prisma": "yarn workspace @calcom/prisma prisma",
|
||||
"start": "turbo run start --scope=\"@calcom/web\"",
|
||||
"start": "turbo run start --filter=\"@calcom/web\"",
|
||||
"tdd": "vitest watch",
|
||||
"e2e": "NEXT_PUBLIC_IS_E2E=1 yarn playwright test --project=@calcom/web",
|
||||
"e2e:app-store": "QUICK=true yarn playwright test --project=@calcom/app-store",
|
||||
@@ -124,7 +124,7 @@
|
||||
"date-fns-tz": "^3.2.0",
|
||||
"eslint": "^8.34.0",
|
||||
"p-limit": "^6.2.0",
|
||||
"turbo": "^1.10.1"
|
||||
"turbo": "^2.5.5"
|
||||
},
|
||||
"resolutions": {
|
||||
"types-ramda": "0.29.4",
|
||||
|
||||
@@ -661,22 +661,30 @@ export default class Office365CalendarService implements Calendar {
|
||||
);
|
||||
};
|
||||
|
||||
private handleErrorJsonOffice365Calendar = <Type>(response: Response): Promise<Type | string> => {
|
||||
private async handleErrorJsonOffice365Calendar<Type>(response: Response): Promise<Type | string> {
|
||||
if (response.headers.get("content-encoding") === "gzip") {
|
||||
return response.text();
|
||||
}
|
||||
|
||||
if (response.status === 204) {
|
||||
return new Promise((resolve) => resolve({} as Type));
|
||||
return {} as Type;
|
||||
}
|
||||
|
||||
if (!response.ok && response.status < 200 && response.status >= 300) {
|
||||
response.json().then(console.log);
|
||||
throw Error(response.statusText);
|
||||
if (!response.ok) {
|
||||
let errorBody: string | object;
|
||||
try {
|
||||
errorBody = await response.json();
|
||||
} catch (e) {
|
||||
errorBody = await response.text();
|
||||
}
|
||||
this.log.error(
|
||||
`handleErrorJsonOffice365Calendar: Office365 API request failed with status ${response.status}`,
|
||||
errorBody
|
||||
);
|
||||
}
|
||||
|
||||
return response.json();
|
||||
};
|
||||
}
|
||||
|
||||
async getMainTimeZone(): Promise<string> {
|
||||
try {
|
||||
|
||||
@@ -55,7 +55,7 @@ export const EventMeta = ({
|
||||
timeZones,
|
||||
children,
|
||||
selectedTimeslot,
|
||||
roundRobinHideOrgAndTeam
|
||||
roundRobinHideOrgAndTeam,
|
||||
}: {
|
||||
event?: Pick<
|
||||
BookerEvent,
|
||||
|
||||
@@ -4,7 +4,7 @@ import { useEffect, useRef } from "react";
|
||||
import { useIsPlatform } from "@calcom/atoms/hooks/useIsPlatform";
|
||||
import { useIsEmbed } from "@calcom/embed-core/embed-iframe";
|
||||
import { useShouldShowArrows } from "@calcom/features/apps/components/AllApps";
|
||||
import { useBookerStore } from "@calcom/features/bookings/Booker/store";
|
||||
import { useBookerStoreContext } from "@calcom/features/bookings/Booker/BookerStoreProvider";
|
||||
import type { BookerEvent } from "@calcom/features/bookings/types";
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
import classNames from "@calcom/ui/classNames";
|
||||
@@ -45,7 +45,7 @@ export const EventDuration = ({
|
||||
const { t } = useLocale();
|
||||
const itemRefs = useRef<(HTMLLIElement | null)[]>([]);
|
||||
const isPlatform = useIsPlatform();
|
||||
const [selectedDuration, setSelectedDuration, state] = useBookerStore((state) => [
|
||||
const [selectedDuration, setSelectedDuration, state] = useBookerStoreContext((state) => [
|
||||
state.selectedDuration,
|
||||
state.setSelectedDuration,
|
||||
state.state,
|
||||
|
||||
@@ -550,6 +550,9 @@ async function handler(
|
||||
eventType.schedulingType === SchedulingType.ROUND_ROBIN
|
||||
) {
|
||||
const bookingRepo = new BookingRepository(prisma);
|
||||
|
||||
const requiresPayment = !Number.isNaN(paymentAppData.price) && paymentAppData.price > 0;
|
||||
|
||||
const existingBooking = await bookingRepo.getValidBookingFromEventTypeForAttendee({
|
||||
eventTypeId,
|
||||
bookerEmail,
|
||||
@@ -559,13 +562,20 @@ async function handler(
|
||||
});
|
||||
|
||||
if (existingBooking) {
|
||||
const hasPayments = existingBooking.payment.length > 0;
|
||||
const isPaidBooking = existingBooking.paid || !hasPayments;
|
||||
|
||||
const shouldShowPaymentForm = requiresPayment && !isPaidBooking;
|
||||
|
||||
const firstPayment = shouldShowPaymentForm ? existingBooking.payment[0] : undefined;
|
||||
|
||||
const bookingResponse = {
|
||||
...existingBooking,
|
||||
user: {
|
||||
...existingBooking.user,
|
||||
email: null,
|
||||
},
|
||||
paymentRequired: false,
|
||||
paymentRequired: shouldShowPaymentForm,
|
||||
seatReferenceUid: "",
|
||||
};
|
||||
|
||||
@@ -574,8 +584,8 @@ async function handler(
|
||||
luckyUsers: bookingResponse.userId ? [bookingResponse.userId] : [],
|
||||
isDryRun,
|
||||
...(isDryRun ? { troubleshooterData } : {}),
|
||||
paymentUid: undefined,
|
||||
paymentId: undefined,
|
||||
paymentUid: firstPayment?.uid,
|
||||
paymentId: firstPayment?.id,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,6 +7,8 @@
|
||||
*
|
||||
* They don't intend to test what the apps logic should do, but rather test if the apps are called with the correct data. For testing that, once should write tests within each app.
|
||||
*/
|
||||
import prismaMock from "../../../../../../tests/libs/__mocks__/prisma";
|
||||
|
||||
import {
|
||||
createBookingScenario,
|
||||
getDate,
|
||||
@@ -3277,6 +3279,122 @@ describe("handleNewBooking", () => {
|
||||
},
|
||||
timeout
|
||||
);
|
||||
|
||||
test(
|
||||
`Payment retry scenario - should return existing payment UID and prevent duplicate bookings when retrying canceled payment`,
|
||||
async ({ emails }) => {
|
||||
const handleNewBooking = (await import("@calcom/features/bookings/lib/handleNewBooking")).default;
|
||||
const booker = getBooker({
|
||||
email: "booker@example.com",
|
||||
name: "Booker",
|
||||
});
|
||||
|
||||
const organizer = getOrganizer({
|
||||
name: "Organizer",
|
||||
email: "organizer@example.com",
|
||||
id: 101,
|
||||
schedules: [TestData.schedules.IstWorkHours],
|
||||
credentials: [getGoogleCalendarCredential(), getStripeAppCredential()],
|
||||
selectedCalendars: [TestData.selectedCalendars.google],
|
||||
});
|
||||
|
||||
const scenarioData = getScenarioData({
|
||||
eventTypes: [
|
||||
{
|
||||
id: 1,
|
||||
title: "Paid Event",
|
||||
description: "It's a test Paid Event",
|
||||
slotInterval: 30,
|
||||
requiresConfirmation: false,
|
||||
metadata: {
|
||||
apps: {
|
||||
stripe: {
|
||||
price: 100,
|
||||
enabled: true,
|
||||
currency: "usd",
|
||||
},
|
||||
},
|
||||
},
|
||||
length: 30,
|
||||
users: [
|
||||
{
|
||||
id: 101,
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
organizer,
|
||||
apps: [TestData.apps["stripe-payment"]],
|
||||
});
|
||||
|
||||
await createBookingScenario(scenarioData);
|
||||
mockSuccessfulVideoMeetingCreation({
|
||||
metadataLookupKey: "dailyvideo",
|
||||
});
|
||||
const { paymentUid } = mockPaymentApp({
|
||||
metadataLookupKey: "stripe",
|
||||
appStoreLookupKey: "stripepayment",
|
||||
});
|
||||
mockCalendarToHaveNoBusySlots("googlecalendar");
|
||||
|
||||
const mockBookingData = getMockRequestDataForBooking({
|
||||
data: {
|
||||
eventTypeId: 1,
|
||||
responses: {
|
||||
email: booker.email,
|
||||
name: booker.name,
|
||||
location: { optionValue: "", value: "New York" },
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const firstBooking = await handleNewBooking({
|
||||
bookingData: mockBookingData,
|
||||
});
|
||||
|
||||
expect(firstBooking).toEqual(
|
||||
expect.objectContaining({
|
||||
paymentUid: expect.any(String),
|
||||
paymentRequired: true,
|
||||
uid: expect.any(String),
|
||||
})
|
||||
);
|
||||
|
||||
const firstBookingInDb = await prismaMock.booking.findUnique({
|
||||
where: { uid: firstBooking.uid },
|
||||
include: { payment: true },
|
||||
});
|
||||
|
||||
expect(firstBookingInDb).toBeTruthy();
|
||||
expect(firstBookingInDb?.payment).toHaveLength(1);
|
||||
expect(firstBookingInDb?.paid).toBe(false);
|
||||
|
||||
const secondBooking = await handleNewBooking({
|
||||
bookingData: mockBookingData,
|
||||
});
|
||||
|
||||
expect(secondBooking.uid).toBe(firstBooking.uid);
|
||||
expect(secondBooking.paymentUid).toBe(firstBooking.paymentUid);
|
||||
expect(secondBooking.paymentRequired).toBe(true);
|
||||
|
||||
const bookingsInDb = await prismaMock.booking.findMany({
|
||||
where: {
|
||||
eventTypeId: 1,
|
||||
attendees: {
|
||||
some: {
|
||||
email: booker.email,
|
||||
},
|
||||
},
|
||||
},
|
||||
include: { payment: true },
|
||||
});
|
||||
|
||||
expect(bookingsInDb).toHaveLength(1);
|
||||
expect(bookingsInDb[0].payment).toHaveLength(1);
|
||||
expect(bookingsInDb[0].uid).toBe(firstBooking.uid);
|
||||
},
|
||||
timeout
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -1,11 +1,7 @@
|
||||
import removeMember from "@calcom/features/ee/teams/lib/removeMember";
|
||||
import { TeamService } from "@calcom/lib/server/service/teamService";
|
||||
|
||||
const removeUserFromOrg = async ({ userId, orgId }: { userId: number; orgId: number }) => {
|
||||
return removeMember({
|
||||
memberId: userId,
|
||||
teamId: orgId,
|
||||
isOrg: true,
|
||||
});
|
||||
return TeamService.removeMembers({ teamIds: [orgId], userIds: [userId], isOrg: true });
|
||||
};
|
||||
|
||||
export default removeUserFromOrg;
|
||||
@@ -1,141 +0,0 @@
|
||||
import logger from "@calcom/lib/logger";
|
||||
import { ProfileRepository } from "@calcom/lib/server/repository/profile";
|
||||
import prisma from "@calcom/prisma";
|
||||
|
||||
import { TRPCError } from "@trpc/server";
|
||||
|
||||
import { deleteWorkfowRemindersOfRemovedMember } from "./deleteWorkflowRemindersOfRemovedMember";
|
||||
|
||||
const log = logger.getSubLogger({ prefix: ["removeMember"] });
|
||||
|
||||
const removeMember = async ({
|
||||
memberId,
|
||||
teamId,
|
||||
isOrg,
|
||||
}: {
|
||||
memberId: number;
|
||||
teamId: number;
|
||||
isOrg: boolean;
|
||||
}) => {
|
||||
const [membership] = await prisma.$transaction([
|
||||
prisma.membership.delete({
|
||||
where: {
|
||||
userId_teamId: { userId: memberId, teamId: teamId },
|
||||
},
|
||||
include: {
|
||||
user: true,
|
||||
team: true,
|
||||
},
|
||||
}),
|
||||
// remove user as host from team events associated with this membership
|
||||
prisma.host.deleteMany({
|
||||
where: {
|
||||
userId: memberId,
|
||||
eventType: {
|
||||
teamId: teamId,
|
||||
},
|
||||
},
|
||||
}),
|
||||
]);
|
||||
|
||||
const team = await prisma.team.findUnique({
|
||||
where: { id: teamId },
|
||||
select: {
|
||||
isOrganization: true,
|
||||
organizationSettings: true,
|
||||
id: true,
|
||||
metadata: true,
|
||||
activeOrgWorkflows: true,
|
||||
parentId: true,
|
||||
},
|
||||
});
|
||||
|
||||
const foundUser = await prisma.user.findUnique({
|
||||
where: { id: memberId },
|
||||
select: {
|
||||
id: true,
|
||||
movedToProfileId: true,
|
||||
email: true,
|
||||
username: true,
|
||||
completedOnboarding: true,
|
||||
teams: {
|
||||
select: {
|
||||
team: {
|
||||
select: {
|
||||
id: true,
|
||||
parentId: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
if (!team || !foundUser) throw new TRPCError({ code: "NOT_FOUND" });
|
||||
|
||||
if (isOrg) {
|
||||
log.debug("Removing a member from the organization");
|
||||
// Deleting membership from all child teams
|
||||
// Delete all sub-team memberships where this team is the organization
|
||||
await prisma.membership.deleteMany({
|
||||
where: {
|
||||
team: {
|
||||
parentId: teamId,
|
||||
},
|
||||
userId: membership.userId,
|
||||
},
|
||||
});
|
||||
|
||||
const userToDeleteMembershipOf = foundUser;
|
||||
|
||||
const profileToDelete = await ProfileRepository.findByUserIdAndOrgId({
|
||||
userId: userToDeleteMembershipOf.id,
|
||||
organizationId: team.id,
|
||||
});
|
||||
|
||||
if (
|
||||
userToDeleteMembershipOf.username &&
|
||||
userToDeleteMembershipOf.movedToProfileId === profileToDelete?.id
|
||||
) {
|
||||
log.debug("Cleaning up tempOrgRedirect for user", userToDeleteMembershipOf.username);
|
||||
|
||||
await prisma.tempOrgRedirect.deleteMany({
|
||||
where: {
|
||||
from: userToDeleteMembershipOf.username,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
await prisma.$transaction([
|
||||
prisma.user.update({
|
||||
where: { id: membership.userId },
|
||||
data: { organizationId: null },
|
||||
}),
|
||||
ProfileRepository.delete({
|
||||
userId: membership.userId,
|
||||
organizationId: team.id,
|
||||
}),
|
||||
prisma.host.deleteMany({
|
||||
where: {
|
||||
userId: memberId,
|
||||
eventType: {
|
||||
team: {
|
||||
parentId: teamId,
|
||||
},
|
||||
},
|
||||
},
|
||||
}),
|
||||
]);
|
||||
}
|
||||
|
||||
// Deleted managed event types from this team from this member
|
||||
await prisma.eventType.deleteMany({
|
||||
where: { parent: { teamId: teamId }, userId: membership.userId },
|
||||
});
|
||||
|
||||
await deleteWorkfowRemindersOfRemovedMember(team, memberId, isOrg);
|
||||
|
||||
return { membership };
|
||||
};
|
||||
|
||||
export default removeMember;
|
||||
@@ -109,6 +109,15 @@ function WorkflowPage({ workflow: workflowId }: PageProps) {
|
||||
|
||||
const isPending = isPendingWorkflow || isPendingEventTypes;
|
||||
|
||||
useEffect(() => {
|
||||
requestAnimationFrame(() => {
|
||||
window.scrollTo({
|
||||
top: 0,
|
||||
behavior: "smooth",
|
||||
});
|
||||
});
|
||||
}, [isPending]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!isPending) {
|
||||
setFormData(workflow);
|
||||
|
||||
@@ -0,0 +1,143 @@
|
||||
import { ErrorMessage } from "@hookform/error-message";
|
||||
import { useFormContext, Controller } from "react-hook-form";
|
||||
|
||||
import { useIsPlatform } from "@calcom/atoms/hooks/useIsPlatform";
|
||||
import type { FormValues } from "@calcom/features/eventtypes/lib/types";
|
||||
import type { CalVideoSettings as CalVideoSettingsType } from "@calcom/features/eventtypes/lib/types";
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
import classNames from "@calcom/ui/classNames";
|
||||
import { UpgradeTeamsBadge } from "@calcom/ui/components/badge";
|
||||
import { TextField } from "@calcom/ui/components/form";
|
||||
import { SettingsToggle } from "@calcom/ui/components/form";
|
||||
|
||||
import LocationSettingsContainer from "./LocationSettingsContainer";
|
||||
|
||||
const CalVideoSettings = ({ calVideoSettings }: { calVideoSettings?: CalVideoSettingsType }) => {
|
||||
const { t } = useLocale();
|
||||
const formMethods = useFormContext<FormValues>();
|
||||
const isPlatform = useIsPlatform();
|
||||
return (
|
||||
<LocationSettingsContainer>
|
||||
<Controller
|
||||
name="calVideoSettings.disableRecordingForGuests"
|
||||
defaultValue={!!calVideoSettings?.disableRecordingForGuests}
|
||||
render={({ field: { onChange, value } }) => {
|
||||
return (
|
||||
<SettingsToggle
|
||||
title={t("disable_recording_for_guests")}
|
||||
labelClassName="text-sm leading-6 whitespace-normal break-words"
|
||||
checked={value}
|
||||
onCheckedChange={onChange}
|
||||
Badge={<UpgradeTeamsBadge checkForActiveStatus />}
|
||||
/>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
|
||||
<Controller
|
||||
name="calVideoSettings.disableRecordingForOrganizer"
|
||||
defaultValue={!!calVideoSettings?.disableRecordingForOrganizer}
|
||||
render={({ field: { onChange, value } }) => {
|
||||
return (
|
||||
<SettingsToggle
|
||||
title={t("disable_recording_for_organizer")}
|
||||
labelClassName="text-sm leading-6 whitespace-normal break-words"
|
||||
checked={value}
|
||||
onCheckedChange={onChange}
|
||||
Badge={<UpgradeTeamsBadge checkForActiveStatus />}
|
||||
/>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
|
||||
{!isPlatform && (
|
||||
<Controller
|
||||
name="calVideoSettings.enableAutomaticRecordingForOrganizer"
|
||||
defaultValue={!!calVideoSettings?.enableAutomaticRecordingForOrganizer}
|
||||
render={({ field: { onChange, value } }) => {
|
||||
return (
|
||||
<SettingsToggle
|
||||
title={t("enable_automatic_recording")}
|
||||
labelClassName="text-sm"
|
||||
checked={value}
|
||||
onCheckedChange={onChange}
|
||||
Badge={<UpgradeTeamsBadge checkForActiveStatus />}
|
||||
/>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
|
||||
<Controller
|
||||
name="calVideoSettings.enableAutomaticTranscription"
|
||||
defaultValue={!!calVideoSettings?.enableAutomaticTranscription}
|
||||
render={({ field: { onChange, value } }) => {
|
||||
return (
|
||||
<SettingsToggle
|
||||
title={t("enable_automatic_transcription")}
|
||||
labelClassName="text-sm leading-6 whitespace-normal break-words"
|
||||
checked={value}
|
||||
onCheckedChange={onChange}
|
||||
Badge={<UpgradeTeamsBadge checkForActiveStatus />}
|
||||
/>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
|
||||
{!isPlatform && (
|
||||
<Controller
|
||||
name="calVideoSettings.disableTranscriptionForGuests"
|
||||
defaultValue={!!calVideoSettings?.disableTranscriptionForGuests}
|
||||
render={({ field: { onChange, value } }) => {
|
||||
return (
|
||||
<SettingsToggle
|
||||
title={t("disable_transcription_for_guests")}
|
||||
labelClassName="text-sm leading-6 whitespace-normal break-words"
|
||||
checked={value}
|
||||
onCheckedChange={onChange}
|
||||
Badge={<UpgradeTeamsBadge checkForActiveStatus />}
|
||||
/>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
{!isPlatform && (
|
||||
<Controller
|
||||
name="calVideoSettings.disableTranscriptionForOrganizer"
|
||||
defaultValue={!!calVideoSettings?.disableTranscriptionForOrganizer}
|
||||
render={({ field: { onChange, value } }) => {
|
||||
return (
|
||||
<SettingsToggle
|
||||
title={t("disable_transcription_for_organizer")}
|
||||
labelClassName="text-sm leading-6 whitespace-normal break-words"
|
||||
checked={value}
|
||||
onCheckedChange={onChange}
|
||||
Badge={<UpgradeTeamsBadge checkForActiveStatus />}
|
||||
/>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
|
||||
<TextField
|
||||
label={t("enter_redirect_url_on_exit_description")}
|
||||
defaultValue={calVideoSettings?.redirectUrlOnExit || ""}
|
||||
data-testid="calVideoSettings.redirectUrlOnExit"
|
||||
containerClassName="mt-4"
|
||||
className="leading-6"
|
||||
{...formMethods.register("calVideoSettings.redirectUrlOnExit", {
|
||||
setValueAs: (v) => (!v || v.trim() === "" ? null : v),
|
||||
})}
|
||||
/>
|
||||
<ErrorMessage
|
||||
errors={formMethods.formState.errors?.calVideoSettings}
|
||||
name="redirectUrlOnExit"
|
||||
className={classNames("text-error text-sm")}
|
||||
as="div"
|
||||
id="calVideoSettings.redirectUrlOnExit-error"
|
||||
/>
|
||||
</LocationSettingsContainer>
|
||||
);
|
||||
};
|
||||
|
||||
export default CalVideoSettings;
|
||||
@@ -0,0 +1,11 @@
|
||||
const LocationSettingsContainer = ({ children }: { children: React.ReactNode }) => {
|
||||
return (
|
||||
<div className="bg-muted mt-2 space-y-2 rounded-lg p-4">
|
||||
<div className="w-full">
|
||||
<div className="flex flex-col gap-4">{children}</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default LocationSettingsContainer;
|
||||
+3
-129
@@ -25,14 +25,13 @@ import ServerTrans from "@calcom/lib/components/ServerTrans";
|
||||
import { WEBAPP_URL } from "@calcom/lib/constants";
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
import classNames from "@calcom/ui/classNames";
|
||||
import { UpgradeTeamsBadge } from "@calcom/ui/components/badge";
|
||||
import { Button } from "@calcom/ui/components/button";
|
||||
import { TextField } from "@calcom/ui/components/form";
|
||||
import { SettingsToggle } from "@calcom/ui/components/form";
|
||||
import { Input } from "@calcom/ui/components/form";
|
||||
import { Icon } from "@calcom/ui/components/icon";
|
||||
import { showToast } from "@calcom/ui/components/toast";
|
||||
|
||||
import CalVideoSettings from "./CalVideoSettings";
|
||||
|
||||
export type TEventTypeLocation = Pick<EventTypeSetupProps["eventType"], "locations" | "calVideoSettings">;
|
||||
export type TLocationOptions = Pick<EventTypeSetupProps, "locationOptions">["locationOptions"];
|
||||
export type TDestinationCalendar = { integration: string } | null;
|
||||
@@ -342,132 +341,7 @@ const Locations: React.FC<LocationsProps> = ({
|
||||
)}
|
||||
</div>
|
||||
|
||||
{isCalVideo && !isPlatform && (
|
||||
<div className="bg-muted mt-2 space-y-2 rounded-lg p-4">
|
||||
<div className="w-full">
|
||||
<div className="flex flex-col gap-4">
|
||||
<Controller
|
||||
name="calVideoSettings.disableRecordingForGuests"
|
||||
defaultValue={!!eventType.calVideoSettings?.disableRecordingForGuests}
|
||||
render={({ field: { onChange, value } }) => {
|
||||
return (
|
||||
<SettingsToggle
|
||||
title={t("disable_recording_for_guests")}
|
||||
labelClassName="text-sm leading-6 whitespace-normal break-words"
|
||||
checked={value}
|
||||
onCheckedChange={onChange}
|
||||
Badge={<UpgradeTeamsBadge checkForActiveStatus />}
|
||||
/>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
|
||||
<Controller
|
||||
name="calVideoSettings.disableRecordingForOrganizer"
|
||||
defaultValue={!!eventType.calVideoSettings?.disableRecordingForOrganizer}
|
||||
render={({ field: { onChange, value } }) => {
|
||||
return (
|
||||
<SettingsToggle
|
||||
title={t("disable_recording_for_organizer")}
|
||||
labelClassName="text-sm leading-6 whitespace-normal break-words"
|
||||
checked={value}
|
||||
onCheckedChange={onChange}
|
||||
Badge={<UpgradeTeamsBadge checkForActiveStatus />}
|
||||
/>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
|
||||
{!isPlatform && (
|
||||
<Controller
|
||||
name="calVideoSettings.enableAutomaticRecordingForOrganizer"
|
||||
defaultValue={!!eventType.calVideoSettings?.enableAutomaticRecordingForOrganizer}
|
||||
render={({ field: { onChange, value } }) => {
|
||||
return (
|
||||
<SettingsToggle
|
||||
title={t("enable_automatic_recording")}
|
||||
labelClassName="text-sm"
|
||||
checked={value}
|
||||
onCheckedChange={onChange}
|
||||
Badge={<UpgradeTeamsBadge checkForActiveStatus />}
|
||||
/>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
|
||||
<Controller
|
||||
name="calVideoSettings.enableAutomaticTranscription"
|
||||
defaultValue={!!eventType.calVideoSettings?.enableAutomaticTranscription}
|
||||
render={({ field: { onChange, value } }) => {
|
||||
return (
|
||||
<SettingsToggle
|
||||
title={t("enable_automatic_transcription")}
|
||||
labelClassName="text-sm leading-6 whitespace-normal break-words"
|
||||
checked={value}
|
||||
onCheckedChange={onChange}
|
||||
Badge={<UpgradeTeamsBadge checkForActiveStatus />}
|
||||
/>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
|
||||
{!isPlatform && (
|
||||
<Controller
|
||||
name="calVideoSettings.disableTranscriptionForGuests"
|
||||
defaultValue={!!eventType.calVideoSettings?.disableTranscriptionForGuests}
|
||||
render={({ field: { onChange, value } }) => {
|
||||
return (
|
||||
<SettingsToggle
|
||||
title={t("disable_transcription_for_guests")}
|
||||
labelClassName="text-sm leading-6 whitespace-normal break-words"
|
||||
checked={value}
|
||||
onCheckedChange={onChange}
|
||||
Badge={<UpgradeTeamsBadge checkForActiveStatus />}
|
||||
/>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
{!isPlatform && (
|
||||
<Controller
|
||||
name="calVideoSettings.disableTranscriptionForOrganizer"
|
||||
defaultValue={!!eventType.calVideoSettings?.disableTranscriptionForOrganizer}
|
||||
render={({ field: { onChange, value } }) => {
|
||||
return (
|
||||
<SettingsToggle
|
||||
title={t("disable_transcription_for_organizer")}
|
||||
labelClassName="text-sm leading-6 whitespace-normal break-words"
|
||||
checked={value}
|
||||
onCheckedChange={onChange}
|
||||
Badge={<UpgradeTeamsBadge checkForActiveStatus />}
|
||||
/>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
|
||||
<TextField
|
||||
label={t("enter_redirect_url_on_exit_description")}
|
||||
defaultValue={eventType.calVideoSettings?.redirectUrlOnExit || ""}
|
||||
data-testid="calVideoSettings.redirectUrlOnExit"
|
||||
containerClassName="mt-4"
|
||||
className="leading-6"
|
||||
{...formMethods.register("calVideoSettings.redirectUrlOnExit", {
|
||||
setValueAs: (v) => (!v || v.trim() === "" ? null : v),
|
||||
})}
|
||||
/>
|
||||
<ErrorMessage
|
||||
errors={formMethods.formState.errors?.calVideoSettings}
|
||||
name="redirectUrlOnExit"
|
||||
className={classNames("text-error text-sm")}
|
||||
as="div"
|
||||
id="calVideoSettings.redirectUrlOnExit-error"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{isCalVideo && !isPlatform && <CalVideoSettings />}
|
||||
|
||||
{eventLocationType?.organizerInputType && (
|
||||
<div className="mt-2 space-y-2">
|
||||
@@ -5,8 +5,6 @@ import type { MultiValue } from "react-select";
|
||||
|
||||
import { useIsPlatform } from "@calcom/atoms/hooks/useIsPlatform";
|
||||
import useLockedFieldsManager from "@calcom/features/ee/managed-event-types/hooks/useLockedFieldsManager";
|
||||
import type { LocationCustomClassNames } from "@calcom/features/eventtypes/components/Locations";
|
||||
import Locations from "@calcom/features/eventtypes/components/Locations";
|
||||
import type {
|
||||
EventTypeSetupProps,
|
||||
InputClassNames,
|
||||
@@ -28,6 +26,9 @@ import { Select } from "@calcom/ui/components/form";
|
||||
import { SettingsToggle } from "@calcom/ui/components/form";
|
||||
import { Skeleton } from "@calcom/ui/components/skeleton";
|
||||
|
||||
import type { LocationCustomClassNames } from "../../locations/Locations";
|
||||
import Locations from "../../locations/Locations";
|
||||
|
||||
export type EventSetupTabCustomClassNames = {
|
||||
wrapper?: string;
|
||||
titleSection?: {
|
||||
|
||||
@@ -153,6 +153,17 @@ export const getPublicEventSelect = (fetchAllUsers: boolean) => {
|
||||
hidden: true,
|
||||
assignAllTeamMembers: true,
|
||||
rescheduleWithSameRoundRobinHost: true,
|
||||
parent: {
|
||||
select: {
|
||||
team: {
|
||||
select: {
|
||||
theme: true,
|
||||
brandColor: true,
|
||||
darkBrandColor: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
} satisfies Prisma.EventTypeSelect;
|
||||
};
|
||||
|
||||
@@ -866,10 +877,11 @@ type GetProfileFromEventInput = Omit<Event, "hosts"> & {
|
||||
|
||||
export function getProfileFromEvent(event: GetProfileFromEventInput) {
|
||||
const { team, subsetOfHosts: hosts, owner } = event;
|
||||
const nonTeamprofile = hosts?.[0]?.user || owner;
|
||||
const profile = team || nonTeamprofile;
|
||||
const nonTeamProfile = hosts?.[0]?.user || owner;
|
||||
const profile = team || nonTeamProfile;
|
||||
if (!profile) throw new Error("Event has no owner");
|
||||
|
||||
const styleProfile = team || event.parent?.team || nonTeamProfile;
|
||||
const username = "username" in profile ? profile.username : team?.slug;
|
||||
const weekStart = hosts?.[0]?.user?.weekStart || owner?.weekStart || "Monday";
|
||||
const eventMetaData = eventTypeMetaDataSchemaWithTypedApps.parse(event.metadata || {});
|
||||
@@ -882,11 +894,11 @@ export function getProfileFromEvent(event: GetProfileFromEventInput) {
|
||||
image: team
|
||||
? getOrgOrTeamAvatar(team)
|
||||
: getUserAvatarUrl({
|
||||
avatarUrl: nonTeamprofile?.avatarUrl,
|
||||
avatarUrl: nonTeamProfile?.avatarUrl,
|
||||
}),
|
||||
brandColor: profile.brandColor,
|
||||
darkBrandColor: profile.darkBrandColor,
|
||||
theme: profile.theme,
|
||||
brandColor: styleProfile.brandColor,
|
||||
darkBrandColor: styleProfile.darkBrandColor,
|
||||
theme: styleProfile.theme,
|
||||
bookerLayouts: bookerLayoutsSchema.parse(
|
||||
eventMetaData?.bookerLayouts ||
|
||||
(userMetaData && "defaultBookerLayouts" in userMetaData ? userMetaData.defaultBookerLayouts : null)
|
||||
|
||||
@@ -168,15 +168,7 @@ export type FormValues = {
|
||||
restrictionScheduleId: number | null;
|
||||
useBookerTimezone: boolean;
|
||||
restrictionScheduleName: string | null;
|
||||
calVideoSettings?: {
|
||||
disableRecordingForOrganizer?: boolean;
|
||||
disableRecordingForGuests?: boolean;
|
||||
enableAutomaticTranscription?: boolean;
|
||||
enableAutomaticRecordingForOrganizer?: boolean;
|
||||
disableTranscriptionForGuests?: boolean;
|
||||
disableTranscriptionForOrganizer?: boolean;
|
||||
redirectUrlOnExit?: string;
|
||||
};
|
||||
calVideoSettings?: CalVideoSettings;
|
||||
maxActiveBookingPerBookerOfferReschedule: boolean;
|
||||
};
|
||||
|
||||
@@ -243,3 +235,13 @@ export interface EventTypePlatformWrapperRef {
|
||||
validateForm: () => Promise<FormValidationResult>;
|
||||
handleFormSubmit: (callbacks?: { onSuccess?: () => void; onError?: (error: Error) => void }) => void;
|
||||
}
|
||||
|
||||
export interface CalVideoSettings {
|
||||
disableRecordingForOrganizer?: boolean;
|
||||
disableRecordingForGuests?: boolean;
|
||||
enableAutomaticTranscription?: boolean;
|
||||
enableAutomaticRecordingForOrganizer?: boolean;
|
||||
disableTranscriptionForGuests?: boolean;
|
||||
disableTranscriptionForOrganizer?: boolean;
|
||||
redirectUrlOnExit?: string;
|
||||
}
|
||||
@@ -110,13 +110,15 @@ describe("RoleManagementFactory", () => {
|
||||
it("should allow role change when user has permission", async () => {
|
||||
mockPermissionCheckService.checkPermission.mockResolvedValue(true);
|
||||
const manager = await factory.createRoleManager(organizationId);
|
||||
await expect(manager.checkPermissionToChangeRole(userId, organizationId)).resolves.not.toThrow();
|
||||
await expect(
|
||||
manager.checkPermissionToChangeRole(userId, organizationId, "org")
|
||||
).resolves.not.toThrow();
|
||||
});
|
||||
|
||||
it("should throw UNAUTHORIZED when user lacks permission", async () => {
|
||||
mockPermissionCheckService.checkPermission.mockResolvedValue(false);
|
||||
const manager = await factory.createRoleManager(organizationId);
|
||||
await expect(manager.checkPermissionToChangeRole(userId, organizationId)).rejects.toThrow(
|
||||
await expect(manager.checkPermissionToChangeRole(userId, organizationId, "org")).rejects.toThrow(
|
||||
new RoleManagementError(
|
||||
"You do not have permission to change roles",
|
||||
RoleManagementErrorCode.UNAUTHORIZED
|
||||
@@ -193,13 +195,15 @@ describe("RoleManagementFactory", () => {
|
||||
customRoleId: null,
|
||||
});
|
||||
const manager = await factory.createRoleManager(organizationId);
|
||||
await expect(manager.checkPermissionToChangeRole(userId, organizationId)).resolves.not.toThrow();
|
||||
await expect(
|
||||
manager.checkPermissionToChangeRole(userId, organizationId, "org")
|
||||
).resolves.not.toThrow();
|
||||
});
|
||||
|
||||
it("should throw UNAUTHORIZED when user is not owner", async () => {
|
||||
vi.mocked(isOrganisationAdmin).mockResolvedValue(false);
|
||||
const manager = await factory.createRoleManager(organizationId);
|
||||
await expect(manager.checkPermissionToChangeRole(userId, organizationId)).rejects.toThrow(
|
||||
await expect(manager.checkPermissionToChangeRole(userId, organizationId, "org")).rejects.toThrow(
|
||||
new RoleManagementError(
|
||||
"Only owners or admin can update roles",
|
||||
RoleManagementErrorCode.UNAUTHORIZED
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { FeaturesRepository } from "@calcom/features/flags/features.repository";
|
||||
import { isOrganisationAdmin } from "@calcom/lib/server/queries/organisations";
|
||||
import { isTeamAdmin } from "@calcom/lib/server/queries/teams";
|
||||
import { prisma } from "@calcom/prisma";
|
||||
import { MembershipRole } from "@calcom/prisma/enums";
|
||||
|
||||
@@ -10,7 +11,7 @@ import { RoleService } from "./role.service";
|
||||
|
||||
interface IRoleManager {
|
||||
isPBACEnabled: boolean;
|
||||
checkPermissionToChangeRole(userId: number, organizationId: number): Promise<void>;
|
||||
checkPermissionToChangeRole(userId: number, targetId: number, scope: "org" | "team"): Promise<void>;
|
||||
assignRole(
|
||||
userId: number,
|
||||
organizationId: number,
|
||||
@@ -29,11 +30,11 @@ class PBACRoleManager implements IRoleManager {
|
||||
private readonly permissionCheckService: PermissionCheckService
|
||||
) {}
|
||||
|
||||
async checkPermissionToChangeRole(userId: number, organizationId: number): Promise<void> {
|
||||
async checkPermissionToChangeRole(userId: number, targetId: number, scope: "org" | "team"): Promise<void> {
|
||||
const hasPermission = await this.permissionCheckService.checkPermission({
|
||||
userId,
|
||||
teamId: organizationId,
|
||||
permission: "organization.changeMemberRole",
|
||||
teamId: targetId,
|
||||
permission: scope === "team" ? "team.changeMemberRole" : "organization.changeMemberRole",
|
||||
fallbackRoles: [MembershipRole.OWNER, MembershipRole.ADMIN],
|
||||
});
|
||||
|
||||
@@ -95,9 +96,11 @@ class PBACRoleManager implements IRoleManager {
|
||||
|
||||
class LegacyRoleManager implements IRoleManager {
|
||||
public isPBACEnabled = false;
|
||||
async checkPermissionToChangeRole(userId: number, organizationId: number): Promise<void> {
|
||||
const membership = await isOrganisationAdmin(userId, organizationId);
|
||||
|
||||
async checkPermissionToChangeRole(userId: number, targetId: number, scope: "org" | "team"): Promise<void> {
|
||||
const membership =
|
||||
scope === "team"
|
||||
? !!(await isTeamAdmin(userId, targetId))
|
||||
: !!(await isOrganisationAdmin(userId, targetId));
|
||||
// Only OWNER/ADMIN can update role
|
||||
if (!membership) {
|
||||
throw new RoleManagementError(
|
||||
|
||||
@@ -3,6 +3,7 @@ import { useForm } from "react-hook-form";
|
||||
|
||||
import type { Dayjs } from "@calcom/dayjs";
|
||||
import dayjs from "@calcom/dayjs";
|
||||
import { BookerStoreProvider } from "@calcom/features/bookings/Booker/BookerStoreProvider";
|
||||
import { Dialog } from "@calcom/features/components/controlled-dialog";
|
||||
import { yyyymmdd } from "@calcom/lib/dayjs";
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
@@ -136,19 +137,21 @@ const DateOverrideForm = ({
|
||||
className="p-6 sm:flex sm:p-0 xl:flex-row">
|
||||
<div className="sm:border-subtle w-full sm:border-r sm:p-4 sm:pr-6 md:p-8">
|
||||
<DialogHeader title={t("date_overrides_dialog_title")} />
|
||||
<DatePicker
|
||||
excludedDates={excludedDates}
|
||||
weekStart={weekStart}
|
||||
selected={selectedDates}
|
||||
onChange={(day) => {
|
||||
if (day) onDateChange(day);
|
||||
}}
|
||||
onMonthChange={(newMonth) => {
|
||||
setBrowsingDate(newMonth);
|
||||
}}
|
||||
browsingDate={browsingDate}
|
||||
locale={isLocaleReady ? i18n.language : "en"}
|
||||
/>
|
||||
<BookerStoreProvider>
|
||||
<DatePicker
|
||||
excludedDates={excludedDates}
|
||||
weekStart={weekStart}
|
||||
selected={selectedDates}
|
||||
onChange={(day) => {
|
||||
if (day) onDateChange(day);
|
||||
}}
|
||||
onMonthChange={(newMonth) => {
|
||||
setBrowsingDate(newMonth);
|
||||
}}
|
||||
browsingDate={browsingDate}
|
||||
locale={isLocaleReady ? i18n.language : "en"}
|
||||
/>
|
||||
</BookerStoreProvider>
|
||||
</div>
|
||||
<div className="relative mt-8 flex w-full flex-col sm:mt-0 sm:p-4 md:p-8">
|
||||
{selectedDates[0] ? (
|
||||
|
||||
@@ -45,7 +45,8 @@ export const IS_CALCOM =
|
||||
(new URL(WEBAPP_URL).hostname.endsWith("cal.com") ||
|
||||
new URL(WEBAPP_URL).hostname.endsWith("cal.dev") ||
|
||||
new URL(WEBAPP_URL).hostname.endsWith("cal.qa") ||
|
||||
new URL(WEBAPP_URL).hostname.endsWith("cal-staging.com"));
|
||||
new URL(WEBAPP_URL).hostname.endsWith("cal-staging.com") ||
|
||||
new URL(WEBAPP_URL).hostname.endsWith("cal.eu"));
|
||||
|
||||
export const CONSOLE_URL =
|
||||
new URL(WEBAPP_URL).hostname.endsWith(".cal.dev") ||
|
||||
|
||||
@@ -213,21 +213,6 @@ describe("getAllDelegationCredentialsForUserIncludeServiceAccountKey", () => {
|
||||
buildDelegationCredentialGoogleMeetCredential(),
|
||||
]);
|
||||
});
|
||||
|
||||
it("should return empty array for non-Google platforms(as they are not supported yet)", async () => {
|
||||
vi.mocked(
|
||||
DelegationCredentialRepository.findUniqueByOrgMemberEmailIncludeSensitiveServiceAccountKey
|
||||
).mockResolvedValue(
|
||||
buildMockDelegationCredential({
|
||||
workspacePlatform: {
|
||||
name: "Microsoft",
|
||||
slug: "microsoft",
|
||||
},
|
||||
})
|
||||
);
|
||||
const result = await getAllDelegationCredentialsForUserIncludeServiceAccountKey({ user: mockUser });
|
||||
expect(result).toEqual([]);
|
||||
});
|
||||
});
|
||||
|
||||
describe("buildAllCredentials", () => {
|
||||
|
||||
@@ -5,8 +5,8 @@ import type {
|
||||
InsightsRoutingServicePublicOptions,
|
||||
InsightsRoutingServiceFilterOptions,
|
||||
InsightsRoutingBaseService,
|
||||
} from "@calcom/lib/server/service/insightsRoutingBase";
|
||||
import type { InsightsRoutingService } from "@calcom/lib/server/service/insightsRoutingDI";
|
||||
} from "@calcom/lib/server/service/InsightsRoutingBaseService";
|
||||
import type { InsightsRoutingService } from "@calcom/lib/server/service/InsightsRoutingDIService";
|
||||
import { prismaModule } from "@calcom/prisma/prisma.module";
|
||||
|
||||
import { insightsRoutingModule } from "../modules/InsightsRouting";
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { createModule } from "@evyweb/ioctopus";
|
||||
|
||||
import type { IInsightsRoutingService } from "@calcom/lib/server/service/insightsRoutingDI";
|
||||
import { InsightsRoutingService } from "@calcom/lib/server/service/insightsRoutingDI";
|
||||
import type { IInsightsRoutingService } from "@calcom/lib/server/service/InsightsRoutingDIService";
|
||||
import { InsightsRoutingService } from "@calcom/lib/server/service/InsightsRoutingDIService";
|
||||
|
||||
import { DI_TOKENS } from "../tokens";
|
||||
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
import { WEBAPP_URL, IS_CALCOM } from "./constants";
|
||||
|
||||
export const getCalcomUrl = () => {
|
||||
if (IS_CALCOM) {
|
||||
return new URL(WEBAPP_URL).hostname.endsWith("cal.eu") ? "https://cal.eu" : "https://cal.com";
|
||||
}
|
||||
return WEBAPP_URL;
|
||||
};
|
||||
@@ -1,14 +1,16 @@
|
||||
import prisma from "@calcom/prisma";
|
||||
import type { PrismaClient } from "@calcom/prisma";
|
||||
|
||||
export class PrismaAttributeRepository {
|
||||
static async findManyByNamesAndOrgIdIncludeOptions({
|
||||
constructor(private prismaClient: PrismaClient) {}
|
||||
|
||||
async findManyByNamesAndOrgIdIncludeOptions({
|
||||
attributeNames,
|
||||
orgId,
|
||||
}: {
|
||||
attributeNames: string[];
|
||||
orgId: number;
|
||||
}) {
|
||||
return prisma.attribute.findMany({
|
||||
return this.prismaClient.attribute.findMany({
|
||||
where: {
|
||||
name: { in: attributeNames, mode: "insensitive" },
|
||||
teamId: orgId,
|
||||
@@ -25,9 +27,9 @@ export class PrismaAttributeRepository {
|
||||
});
|
||||
}
|
||||
|
||||
static async findManyByOrgId({ orgId }: { orgId: number }) {
|
||||
async findManyByOrgId({ orgId }: { orgId: number }) {
|
||||
// It should be a faster query because of lesser number of attributes record and index on teamId
|
||||
const result = await prisma.attribute.findMany({
|
||||
const result = await this.prismaClient.attribute.findMany({
|
||||
where: {
|
||||
teamId: orgId,
|
||||
},
|
||||
@@ -43,8 +45,8 @@ export class PrismaAttributeRepository {
|
||||
return result;
|
||||
}
|
||||
|
||||
static async findAllByOrgIdWithOptions({ orgId }: { orgId: number }) {
|
||||
return await prisma.attribute.findMany({
|
||||
async findAllByOrgIdWithOptions({ orgId }: { orgId: number }) {
|
||||
return await this.prismaClient.attribute.findMany({
|
||||
where: {
|
||||
teamId: orgId,
|
||||
},
|
||||
|
||||
@@ -828,6 +828,7 @@ export class BookingRepository {
|
||||
attendees: true,
|
||||
references: true,
|
||||
user: true,
|
||||
payment: true,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
import prisma from "@calcom/prisma";
|
||||
import type { PrismaClient } from "@calcom/prisma";
|
||||
|
||||
export class HostRepository {
|
||||
static async updateHostsSchedule(userId: number, oldScheduleId: number, newScheduleId: number) {
|
||||
return await prisma.host.updateMany({
|
||||
constructor(private prismaClient: PrismaClient) {}
|
||||
|
||||
async updateHostsSchedule(userId: number, oldScheduleId: number, newScheduleId: number) {
|
||||
return await this.prismaClient.host.updateMany({
|
||||
where: {
|
||||
userId,
|
||||
scheduleId: oldScheduleId,
|
||||
|
||||
File renamed without changes.
+1
-1
@@ -4,7 +4,7 @@ import {
|
||||
InsightsRoutingBaseService,
|
||||
type InsightsRoutingServicePublicOptions,
|
||||
type InsightsRoutingServiceFilterOptions,
|
||||
} from "./insightsRoutingBase";
|
||||
} from "./InsightsRoutingBaseService";
|
||||
|
||||
export interface IInsightsRoutingService {
|
||||
prisma: typeof readonlyPrisma;
|
||||
@@ -7,7 +7,7 @@ import { ColumnFilterType } from "@calcom/features/data-table/lib/types";
|
||||
import prisma from "@calcom/prisma";
|
||||
import { BookingStatus, MembershipRole } from "@calcom/prisma/enums";
|
||||
|
||||
import { InsightsRoutingBaseService as InsightsRoutingService } from "../../service/insightsRoutingBase";
|
||||
import { InsightsRoutingBaseService as InsightsRoutingService } from "../../service/InsightsRoutingBaseService";
|
||||
|
||||
// SQL condition constants for testing
|
||||
const NOTHING_CONDITION = Prisma.sql`1=0`;
|
||||
|
||||
File diff suppressed because it is too large.
Load diff
@@ -1,16 +1,52 @@
|
||||
import { Prisma } from "@prisma/client";
|
||||
|
||||
import { TeamBilling } from "@calcom/features/ee/billing/teams";
|
||||
import removeMember from "@calcom/features/ee/teams/lib/removeMember";
|
||||
import { deleteWorkfowRemindersOfRemovedMember } from "@calcom/features/ee/teams/lib/deleteWorkflowRemindersOfRemovedMember";
|
||||
import { deleteDomain } from "@calcom/lib/domainManager/organization";
|
||||
import logger from "@calcom/lib/logger";
|
||||
import { ProfileRepository } from "@calcom/lib/server/repository/profile";
|
||||
import { TeamRepository } from "@calcom/lib/server/repository/team";
|
||||
import { WorkflowService } from "@calcom/lib/server/service/workflows";
|
||||
import prisma from "@calcom/prisma";
|
||||
import type { Membership } from "@calcom/prisma/client";
|
||||
import { MembershipRole } from "@calcom/prisma/enums";
|
||||
|
||||
import { TRPCError } from "@trpc/server";
|
||||
|
||||
const log = logger.getSubLogger({ prefix: ["TeamService"] });
|
||||
|
||||
type MembershipWithRelations = Pick<
|
||||
Membership,
|
||||
"id" | "userId" | "teamId" | "role" | "accepted" | "disableImpersonation"
|
||||
>;
|
||||
|
||||
type TeamWithSettings = {
|
||||
id: number;
|
||||
isOrganization: boolean | null;
|
||||
organizationSettings: unknown;
|
||||
metadata: unknown;
|
||||
activeOrgWorkflows: unknown;
|
||||
parentId: number | null;
|
||||
};
|
||||
|
||||
type UserWithTeams = {
|
||||
id: number;
|
||||
movedToProfileId: number | null;
|
||||
email: string;
|
||||
username: string | null;
|
||||
completedOnboarding: boolean;
|
||||
teams: {
|
||||
team: {
|
||||
id: number;
|
||||
parentId: number | null;
|
||||
};
|
||||
}[];
|
||||
};
|
||||
|
||||
export type RemoveMemberResult = {
|
||||
membership: MembershipWithRelations;
|
||||
};
|
||||
|
||||
export class TeamService {
|
||||
/**
|
||||
* Deletes a team and all its associated data in a safe, transactional order.
|
||||
@@ -45,15 +81,23 @@ export class TeamService {
|
||||
return deletedTeam;
|
||||
}
|
||||
|
||||
static async removeMembers(teamIds: number[], memberIds: number[], isOrg = false) {
|
||||
const deleteMembershipPromises = [];
|
||||
static async removeMembers({
|
||||
teamIds,
|
||||
userIds,
|
||||
isOrg = false,
|
||||
}: {
|
||||
teamIds: number[];
|
||||
userIds: number[];
|
||||
isOrg?: boolean;
|
||||
}) {
|
||||
const deleteMembershipPromises: Promise<RemoveMemberResult>[] = [];
|
||||
|
||||
for (const memberId of memberIds) {
|
||||
for (const userId of userIds) {
|
||||
for (const teamId of teamIds) {
|
||||
deleteMembershipPromises.push(
|
||||
removeMember({
|
||||
TeamService.removeMember({
|
||||
teamId,
|
||||
memberId,
|
||||
userId,
|
||||
isOrg,
|
||||
})
|
||||
);
|
||||
@@ -121,4 +165,217 @@ export class TeamService {
|
||||
const teamBilling = await TeamBilling.findAndInit(teamId);
|
||||
return teamBilling.publish();
|
||||
}
|
||||
|
||||
private static async removeMember({
|
||||
userId,
|
||||
teamId,
|
||||
isOrg,
|
||||
}: {
|
||||
userId: number;
|
||||
teamId: number;
|
||||
isOrg: boolean;
|
||||
}) {
|
||||
const membership = await TeamService.fetchMembershipOrThrow(userId, teamId);
|
||||
const team = await TeamService.fetchTeamOrThrow(teamId);
|
||||
const user = await TeamService.fetchUserOrThrow(userId);
|
||||
|
||||
if (isOrg) {
|
||||
log.debug("Removing a member from the organization");
|
||||
await TeamService.removeFromOrganization(membership, team, user);
|
||||
} else {
|
||||
log.debug("Removing a member from a team");
|
||||
await TeamService.removeFromTeam(membership, teamId);
|
||||
}
|
||||
|
||||
await deleteWorkfowRemindersOfRemovedMember(team, userId, isOrg);
|
||||
|
||||
return { membership };
|
||||
}
|
||||
|
||||
// TODO: Needs to be moved to repository
|
||||
private static async fetchMembershipOrThrow(
|
||||
userId: number,
|
||||
teamId: number
|
||||
): Promise<MembershipWithRelations> {
|
||||
const membership = await prisma.membership.findUnique({
|
||||
where: {
|
||||
userId_teamId: { userId: userId, teamId: teamId },
|
||||
},
|
||||
select: {
|
||||
id: true,
|
||||
userId: true,
|
||||
teamId: true,
|
||||
role: true,
|
||||
accepted: true,
|
||||
disableImpersonation: true,
|
||||
},
|
||||
});
|
||||
|
||||
if (!membership) {
|
||||
throw new TRPCError({ code: "NOT_FOUND", message: "Membership not found" });
|
||||
}
|
||||
|
||||
return membership;
|
||||
}
|
||||
|
||||
// TODO: Needs to be moved to repository
|
||||
private static async fetchTeamOrThrow(teamId: number): Promise<TeamWithSettings> {
|
||||
const team = await prisma.team.findUnique({
|
||||
where: { id: teamId },
|
||||
select: {
|
||||
isOrganization: true,
|
||||
organizationSettings: true,
|
||||
id: true,
|
||||
metadata: true,
|
||||
activeOrgWorkflows: true,
|
||||
parentId: true,
|
||||
},
|
||||
});
|
||||
|
||||
if (!team) {
|
||||
throw new TRPCError({ code: "NOT_FOUND", message: "Team not found" });
|
||||
}
|
||||
|
||||
return team;
|
||||
}
|
||||
|
||||
// TODO: Needs to be moved to repository
|
||||
private static async fetchUserOrThrow(userId: number): Promise<UserWithTeams> {
|
||||
const user = await prisma.user.findUnique({
|
||||
where: { id: userId },
|
||||
select: {
|
||||
id: true,
|
||||
movedToProfileId: true,
|
||||
email: true,
|
||||
username: true,
|
||||
completedOnboarding: true,
|
||||
teams: {
|
||||
select: {
|
||||
team: {
|
||||
select: {
|
||||
id: true,
|
||||
parentId: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
if (!user) {
|
||||
throw new TRPCError({ code: "NOT_FOUND", message: "User not found" });
|
||||
}
|
||||
|
||||
return user;
|
||||
}
|
||||
|
||||
// TODO: Needs to be moved to repository
|
||||
private static async cleanupTempOrgRedirect(user: UserWithTeams, team: TeamWithSettings) {
|
||||
const profileToDelete = await ProfileRepository.findByUserIdAndOrgId({
|
||||
userId: user.id,
|
||||
organizationId: team.id,
|
||||
});
|
||||
|
||||
if (user.username && user.movedToProfileId === profileToDelete?.id) {
|
||||
log.debug("Cleaning up tempOrgRedirect for user", user.username);
|
||||
await prisma.tempOrgRedirect.deleteMany({
|
||||
where: {
|
||||
from: user.username,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private static async removeFromOrganization(
|
||||
membership: MembershipWithRelations,
|
||||
team: TeamWithSettings,
|
||||
user: UserWithTeams
|
||||
) {
|
||||
await TeamService.cleanupTempOrgRedirect(user, team);
|
||||
const newUsername = generateNewUsername(user);
|
||||
|
||||
await prisma.$transaction([
|
||||
// Remove user from all sub-teams event type hosts
|
||||
prisma.host.deleteMany({
|
||||
where: {
|
||||
userId: membership.userId,
|
||||
eventType: {
|
||||
team: {
|
||||
parentId: team.id,
|
||||
},
|
||||
},
|
||||
},
|
||||
}),
|
||||
// Delete managed child events in sub-teams
|
||||
prisma.eventType.deleteMany({
|
||||
where: {
|
||||
userId: membership.userId,
|
||||
parent: {
|
||||
team: {
|
||||
parentId: team.id,
|
||||
},
|
||||
},
|
||||
},
|
||||
}),
|
||||
// Remove organizationId from the user
|
||||
prisma.user.update({
|
||||
where: { id: membership.userId },
|
||||
data: {
|
||||
organizationId: null,
|
||||
username: newUsername,
|
||||
},
|
||||
}),
|
||||
// Delete the profile of the user from the organization
|
||||
ProfileRepository.delete({
|
||||
userId: membership.userId,
|
||||
organizationId: team.id,
|
||||
}),
|
||||
// Delete all sub-team memberships where this team is the organization
|
||||
prisma.membership.deleteMany({
|
||||
where: {
|
||||
team: {
|
||||
parentId: team.id,
|
||||
},
|
||||
userId: membership.userId,
|
||||
},
|
||||
}),
|
||||
// Delete the membership of the user from the organization
|
||||
prisma.membership.delete({
|
||||
where: {
|
||||
userId_teamId: { userId: membership.userId, teamId: team.id },
|
||||
},
|
||||
}),
|
||||
]);
|
||||
|
||||
// Generate new username for user leaving organization
|
||||
function generateNewUsername(user: UserWithTeams): string | null {
|
||||
// We ensure that new username would be unique across all users in the global namespace outside any organization
|
||||
return user.username != null ? `${user.username}-${user.id}` : null;
|
||||
}
|
||||
}
|
||||
|
||||
// Remove member from regular team
|
||||
private static async removeFromTeam(membership: MembershipWithRelations, teamId: number) {
|
||||
await prisma.$transaction([
|
||||
// Remove user from all team event types' hosts
|
||||
prisma.host.deleteMany({
|
||||
where: {
|
||||
userId: membership.userId,
|
||||
eventType: {
|
||||
teamId: teamId,
|
||||
},
|
||||
},
|
||||
}),
|
||||
// Deleted managed event types from this team for this member
|
||||
prisma.eventType.deleteMany({
|
||||
where: { parent: { teamId: teamId }, userId: membership.userId },
|
||||
}),
|
||||
// Delete the membership of the user from the team
|
||||
prisma.membership.delete({
|
||||
where: {
|
||||
userId_teamId: { userId: membership.userId, teamId: teamId },
|
||||
},
|
||||
}),
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -37,7 +37,8 @@ const findAttributesByName = async ({
|
||||
orgId: number;
|
||||
attributeNames: AttributeName[];
|
||||
}) => {
|
||||
const attributesFromDb = await PrismaAttributeRepository.findManyByNamesAndOrgIdIncludeOptions({
|
||||
const attributeRepo = new PrismaAttributeRepository(prisma);
|
||||
const attributesFromDb = await attributeRepo.findManyByNamesAndOrgIdIncludeOptions({
|
||||
attributeNames,
|
||||
orgId,
|
||||
});
|
||||
|
||||
@@ -216,9 +216,11 @@ async function _getOrgMembershipToUserIdForTeam({ orgId, teamId }: { orgId: numb
|
||||
}
|
||||
|
||||
async function _queryAllData({ orgId, teamId }: { orgId: number; teamId: number }) {
|
||||
const attributeRepo = new PrismaAttributeRepository(prisma);
|
||||
|
||||
const [orgMembershipToUserIdForTeamMembers, attributesOfTheOrg] = await Promise.all([
|
||||
_getOrgMembershipToUserIdForTeam({ orgId, teamId }),
|
||||
PrismaAttributeRepository.findManyByOrgId({ orgId }),
|
||||
attributeRepo.findManyByOrgId({ orgId }),
|
||||
]);
|
||||
|
||||
const orgMembershipIds = Array.from(orgMembershipToUserIdForTeamMembers.keys());
|
||||
|
||||
@@ -1,5 +1,11 @@
|
||||
## 1.1.2
|
||||
|
||||
## 1.5.0
|
||||
|
||||
### Minor Changes
|
||||
|
||||
- [#23174](https://github.com/calcom/cal.com/pull/23174) [`618ef63`](https://github.com/calcom/cal.com/commit/618ef630170eb1c05b4a131a01d0c2316b3d1880) Thanks [@SomayChauhan](https://github.com/SomayChauhan)! - fix: Availability atom handleFormSubmit callbacks not triggering
|
||||
|
||||
## 1.4.0
|
||||
|
||||
### Minor Changes
|
||||
|
||||
@@ -124,6 +124,7 @@ type AvailabilitySettingsProps = {
|
||||
isEventTypesFetching?: boolean;
|
||||
handleBulkEditDialogToggle: () => void;
|
||||
};
|
||||
callbacksRef?: React.MutableRefObject<{ onSuccess?: () => void; onError?: (error: Error) => void }>;
|
||||
};
|
||||
|
||||
const DeleteDialogButton = ({
|
||||
@@ -306,6 +307,7 @@ export const AvailabilitySettings = forwardRef<AvailabilitySettingsFormRef, Avai
|
||||
bulkUpdateModalProps,
|
||||
allowSetToDefault = true,
|
||||
allowDelete = true,
|
||||
callbacksRef,
|
||||
} = props;
|
||||
const [openSidebar, setOpenSidebar] = useState(false);
|
||||
const { t, i18n } = useLocale();
|
||||
@@ -336,11 +338,9 @@ export const AvailabilitySettings = forwardRef<AvailabilitySettingsFormRef, Avai
|
||||
|
||||
const saveButtonRef = useRef<HTMLButtonElement>(null);
|
||||
|
||||
const callbacksRef = useRef<{ onSuccess?: () => void; onError?: (error: Error) => void }>({});
|
||||
|
||||
const handleFormSubmit = useCallback(
|
||||
(customCallbacks?: { onSuccess?: () => void; onError?: (error: Error) => void }) => {
|
||||
if (customCallbacks) {
|
||||
if (callbacksRef && customCallbacks) {
|
||||
callbacksRef.current = customCallbacks;
|
||||
}
|
||||
|
||||
@@ -350,14 +350,14 @@ export const AvailabilitySettings = forwardRef<AvailabilitySettingsFormRef, Avai
|
||||
form.handleSubmit(async (data) => {
|
||||
try {
|
||||
await handleSubmit(data);
|
||||
callbacksRef.current?.onSuccess?.();
|
||||
callbacksRef?.current?.onSuccess?.();
|
||||
} catch (error) {
|
||||
callbacksRef.current?.onError?.(error as Error);
|
||||
callbacksRef?.current?.onError?.(error as Error);
|
||||
}
|
||||
})();
|
||||
}
|
||||
},
|
||||
[form, handleSubmit]
|
||||
[form, handleSubmit, callbacksRef]
|
||||
);
|
||||
|
||||
const validateForm = useCallback(async () => {
|
||||
|
||||
+2
-1
@@ -39,7 +39,7 @@ export type AvailabilitySettingsPlatformWrapperProps = {
|
||||
};
|
||||
|
||||
export const AvailabilitySettingsPlatformWrapper = forwardRef<
|
||||
AvailabilitySettingsFormRef,
|
||||
AvailabilitySettingsFormRef,
|
||||
AvailabilitySettingsPlatformWrapperProps
|
||||
>(function AvailabilitySettingsPlatformWrapper(props, ref) {
|
||||
const {
|
||||
@@ -202,6 +202,7 @@ AvailabilitySettingsFormRef,
|
||||
allowDelete={allowDelete}
|
||||
allowSetToDefault={allowSetToDefault}
|
||||
onFormStateChange={onFormStateChange}
|
||||
callbacksRef={callbacksRef}
|
||||
/>
|
||||
</AtomsWrapper>
|
||||
);
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
"type": "module",
|
||||
"description": "Customizable UI components to integrate scheduling into your product.",
|
||||
"authors": "Cal.com, Inc.",
|
||||
"version": "1.4.0",
|
||||
"version": "1.5.0",
|
||||
"scripts": {
|
||||
"dev": "yarn vite build --watch & npx tailwindcss -i ./globals.css -o ./globals.min.css --postcss --minify --watch",
|
||||
"build": "NODE_OPTIONS='--max_old_space_size=12288' rm -rf dist && yarn vite build && npx tailwindcss -i ./globals.css -o ./globals.min.css --postcss --minify && mkdir ./dist/packages/prisma-client && cp -rf ../../../node_modules/.prisma/client/*.d.ts ./dist/packages/prisma-client",
|
||||
|
||||
@@ -120,4 +120,5 @@ export { checkAdminOrOwner } from "@calcom/features/auth/lib/checkAdminOrOwner";
|
||||
|
||||
export { verifyPhoneNumber, sendVerificationCode };
|
||||
|
||||
export { TeamService } from "@calcom/lib/server/service/teamService";
|
||||
export { CacheService } from "@calcom/features/calendar-cache/lib/getShouldServeCache";
|
||||
+5
@@ -446,6 +446,11 @@ class BaseCreateEventTypeInput {
|
||||
type: CalVideoSettings,
|
||||
})
|
||||
calVideoSettings?: CalVideoSettings;
|
||||
|
||||
@IsOptional()
|
||||
@IsBoolean()
|
||||
@DocsPropertyOptional()
|
||||
hidden?: boolean;
|
||||
}
|
||||
export class CreateEventTypeInput_2024_06_14 extends BaseCreateEventTypeInput {
|
||||
@IsOptional()
|
||||
|
||||
+5
@@ -414,6 +414,11 @@ class BaseUpdateEventTypeInput {
|
||||
type: CalVideoSettings,
|
||||
})
|
||||
calVideoSettings?: CalVideoSettings;
|
||||
|
||||
@IsOptional()
|
||||
@IsBoolean()
|
||||
@DocsPropertyOptional()
|
||||
hidden?: boolean;
|
||||
}
|
||||
export class UpdateEventTypeInput_2024_06_14 extends BaseUpdateEventTypeInput {
|
||||
@IsOptional()
|
||||
|
||||
+5
@@ -439,6 +439,11 @@ class BaseEventTypeOutput_2024_06_14 {
|
||||
type: CalVideoSettings,
|
||||
})
|
||||
calVideoSettings?: CalVideoSettings | null;
|
||||
|
||||
@IsOptional()
|
||||
@IsBoolean()
|
||||
@DocsProperty()
|
||||
hidden?: boolean;
|
||||
}
|
||||
|
||||
export class TeamEventTypeResponseHost extends TeamEventTypeHostInput {
|
||||
|
||||
Binary file not shown.
@@ -90,6 +90,9 @@ type OmitPrismaClient = Omit<
|
||||
// we cant pass tx to functions as types miss match since we have a custom prisma client https://github.com/prisma/prisma/discussions/20924#discussioncomment-10077649
|
||||
export type PrismaTransaction = OmitPrismaClient;
|
||||
|
||||
/**
|
||||
* @deprecated Use named export `prisma` instead
|
||||
*/
|
||||
export default prisma;
|
||||
|
||||
export * from "./selects";
|
||||
@@ -1,4 +1,5 @@
|
||||
import { PrismaAttributeRepository } from "@calcom/lib/server/repository/PrismaAttributeRepository";
|
||||
import prisma from "@calcom/prisma";
|
||||
|
||||
import { TRPCError } from "@trpc/server";
|
||||
|
||||
@@ -19,8 +20,9 @@ const listHandler = async (opts: GetOptions) => {
|
||||
message: "You need to be apart of an organization to use this feature",
|
||||
});
|
||||
}
|
||||
const attributeRepo = new PrismaAttributeRepository(prisma);
|
||||
|
||||
return await PrismaAttributeRepository.findAllByOrgIdWithOptions({ orgId: org.id });
|
||||
return await attributeRepo.findAllByOrgIdWithOptions({ orgId: org.id });
|
||||
};
|
||||
|
||||
export default listHandler;
|
||||
@@ -1,9 +1,9 @@
|
||||
import { HostRepository } from "@calcom/lib/server/repository/host";
|
||||
import { prisma } from "@calcom/prisma";
|
||||
|
||||
import { TRPCError } from "@trpc/server";
|
||||
|
||||
import type { TrpcSessionUser } from "../../../../types";
|
||||
import { updateHostsWithNewDefaultSchedule } from "../util";
|
||||
import type { TDeleteInputSchema } from "./delete.schema";
|
||||
|
||||
type DeleteOptions = {
|
||||
@@ -15,6 +15,7 @@ type DeleteOptions = {
|
||||
|
||||
export const deleteHandler = async ({ input, ctx }: DeleteOptions) => {
|
||||
const { user } = ctx;
|
||||
const hostRepo = new HostRepository(prisma);
|
||||
|
||||
const scheduleToDelete = await prisma.schedule.findUnique({
|
||||
where: {
|
||||
@@ -46,7 +47,7 @@ export const deleteHandler = async ({ input, ctx }: DeleteOptions) => {
|
||||
// to throw the error if there arent any other schedules
|
||||
if (!scheduleToSetAsDefault) throw new TRPCError({ code: "BAD_REQUEST" });
|
||||
|
||||
await updateHostsWithNewDefaultSchedule(user.id, input.scheduleId, scheduleToSetAsDefault.id);
|
||||
await hostRepo.updateHostsSchedule(user.id, input.scheduleId, scheduleToSetAsDefault.id);
|
||||
|
||||
await prisma.user.update({
|
||||
where: {
|
||||
@@ -57,7 +58,7 @@ export const deleteHandler = async ({ input, ctx }: DeleteOptions) => {
|
||||
},
|
||||
});
|
||||
} else if (user.defaultScheduleId) {
|
||||
await updateHostsWithNewDefaultSchedule(user.id, input.scheduleId, user.defaultScheduleId);
|
||||
await hostRepo.updateHostsSchedule(user.id, input.scheduleId, user.defaultScheduleId);
|
||||
}
|
||||
|
||||
await prisma.schedule.delete({
|
||||
|
||||
@@ -1,13 +1,9 @@
|
||||
import { describe, expect, it, vi, beforeEach } from "vitest";
|
||||
|
||||
import { HostRepository } from "@calcom/lib/server/repository/host";
|
||||
import { PrismaClient } from "@calcom/prisma";
|
||||
|
||||
import {
|
||||
getDefaultScheduleId,
|
||||
hasDefaultSchedule,
|
||||
setupDefaultSchedule,
|
||||
updateHostsWithNewDefaultSchedule,
|
||||
} from "./util";
|
||||
import { getDefaultScheduleId, hasDefaultSchedule, setupDefaultSchedule } from "./util";
|
||||
|
||||
vi.mock("@calcom/prisma", () => {
|
||||
const mockPrisma = {
|
||||
@@ -31,9 +27,11 @@ vi.mock("@calcom/prisma", () => {
|
||||
|
||||
describe("Availability Utils", () => {
|
||||
let prisma: PrismaClient;
|
||||
let hostRepo: HostRepository;
|
||||
|
||||
beforeEach(() => {
|
||||
prisma = new PrismaClient();
|
||||
hostRepo = new HostRepository(prisma);
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
@@ -158,7 +156,7 @@ describe("Availability Utils", () => {
|
||||
|
||||
(prisma.host.updateMany as any).mockResolvedValue(updateResult);
|
||||
|
||||
const result = await updateHostsWithNewDefaultSchedule(userId, oldScheduleId, newScheduleId, prisma);
|
||||
const result = await hostRepo.updateHostsSchedule(userId, oldScheduleId, newScheduleId);
|
||||
|
||||
expect(prisma.host.updateMany).toHaveBeenCalledWith({
|
||||
where: {
|
||||
@@ -180,7 +178,7 @@ describe("Availability Utils", () => {
|
||||
|
||||
(prisma.host.updateMany as any).mockResolvedValue(updateResult);
|
||||
|
||||
const result = await updateHostsWithNewDefaultSchedule(userId, oldScheduleId, newScheduleId, prisma);
|
||||
const result = await hostRepo.updateHostsSchedule(userId, oldScheduleId, newScheduleId);
|
||||
|
||||
expect(prisma.host.updateMany).toHaveBeenCalledWith({
|
||||
where: {
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import type { User } from "@prisma/client";
|
||||
|
||||
import { HostRepository } from "@calcom/lib/server/repository/host";
|
||||
import type { PrismaClient } from "@calcom/prisma";
|
||||
|
||||
export const getDefaultScheduleId = async (userId: number, prisma: PrismaClient) => {
|
||||
@@ -54,11 +53,3 @@ export const setupDefaultSchedule = async (userId: number, scheduleId: number, p
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
export const updateHostsWithNewDefaultSchedule = async (
|
||||
userId: number,
|
||||
defaultScheduleId: number,
|
||||
scheduleId: number
|
||||
) => {
|
||||
return await HostRepository.updateHostsSchedule(userId, defaultScheduleId, scheduleId);
|
||||
};
|
||||
@@ -440,5 +440,63 @@ const checkIfUserIsAuthorizedToConfirmBooking = async ({
|
||||
if (membership) return;
|
||||
}
|
||||
|
||||
if (bookingUserId && (await isLoggedInUserOrgAdminOfBookingUser(loggedInUserId, bookingUserId))) {
|
||||
return;
|
||||
}
|
||||
|
||||
throw new TRPCError({ code: "UNAUTHORIZED", message: "User is not authorized to confirm this booking" });
|
||||
};
|
||||
|
||||
async function isLoggedInUserOrgAdminOfBookingUser(loggedInUserId: number, bookingUserId: number) {
|
||||
const orgIdsWhereLoggedInUserAdmin = await getOrgIdsWhereAdmin(loggedInUserId);
|
||||
|
||||
if (orgIdsWhereLoggedInUserAdmin.length === 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const bookingUserOrgMembership = await prisma.membership.findFirst({
|
||||
where: {
|
||||
userId: bookingUserId,
|
||||
teamId: {
|
||||
in: orgIdsWhereLoggedInUserAdmin,
|
||||
},
|
||||
team: {
|
||||
parentId: null,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
if (bookingUserOrgMembership) return true;
|
||||
|
||||
const bookingUserOrgTeamMembership = await prisma.membership.findFirst({
|
||||
where: {
|
||||
userId: bookingUserId,
|
||||
team: {
|
||||
parentId: {
|
||||
in: orgIdsWhereLoggedInUserAdmin,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
return !!bookingUserOrgTeamMembership;
|
||||
}
|
||||
|
||||
async function getOrgIdsWhereAdmin(loggedInUserId: number) {
|
||||
const loggedInUserOrgMemberships = await prisma.membership.findMany({
|
||||
where: {
|
||||
userId: loggedInUserId,
|
||||
role: {
|
||||
in: [MembershipRole.OWNER, MembershipRole.ADMIN],
|
||||
},
|
||||
team: {
|
||||
parentId: null,
|
||||
},
|
||||
},
|
||||
select: {
|
||||
teamId: true,
|
||||
},
|
||||
});
|
||||
|
||||
return loggedInUserOrgMemberships.map((m) => m.teamId);
|
||||
}
|
||||
@@ -80,7 +80,7 @@ export async function toggleDelegationCredentialEnabled(
|
||||
if (slug === "google") {
|
||||
calendarAppName = "Google Calendar";
|
||||
conferencingAppName = "Google Meet";
|
||||
} else if (slug === "microsoft") {
|
||||
} else if (slug === "office365") {
|
||||
calendarAppName = "Microsoft 365";
|
||||
conferencingAppName = "Microsoft Teams";
|
||||
} else {
|
||||
|
||||
@@ -47,7 +47,7 @@ export const updateUserHandler = async ({ ctx, input }: UpdateUserOptions) => {
|
||||
const roleManager = await RoleManagementFactory.getInstance().createRoleManager(organizationId);
|
||||
|
||||
try {
|
||||
await roleManager.checkPermissionToChangeRole(userId, organizationId);
|
||||
await roleManager.checkPermissionToChangeRole(userId, organizationId, "org");
|
||||
} catch (error) {
|
||||
if (error instanceof RoleManagementError) {
|
||||
throw new TRPCError({ code: "UNAUTHORIZED", message: error.message });
|
||||
|
||||
Loaded 100 of 106 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user