diff --git a/apps/api/v2/src/modules/organizations/organizations/inputs/create-managed-organization.input.ts b/apps/api/v2/src/modules/organizations/organizations/inputs/create-managed-organization.input.ts index 4a2bb1328c..2708d1ccb1 100644 --- a/apps/api/v2/src/modules/organizations/organizations/inputs/create-managed-organization.input.ts +++ b/apps/api/v2/src/modules/organizations/organizations/inputs/create-managed-organization.input.ts @@ -10,6 +10,16 @@ export class CreateOrganizationInput extends RefreshApiKeyInput { @ApiProperty({ description: "Name of the organization", example: "CalTeam" }) readonly name!: string; + @IsOptional() + @IsString() + @ApiPropertyOptional({ + type: String, + description: + "Organization slug in kebab-case - if not provided will be generated automatically based on name.", + example: "cal-tel", + }) + readonly slug?: string; + @ApiPropertyOptional({ type: Object, description: METADATA_DOCS, diff --git a/apps/api/v2/src/modules/organizations/organizations/organizations-organizations.controller.e2e-spec.ts b/apps/api/v2/src/modules/organizations/organizations/organizations-organizations.controller.e2e-spec.ts index f29e41341f..0e3a43f1a2 100644 --- a/apps/api/v2/src/modules/organizations/organizations/organizations-organizations.controller.e2e-spec.ts +++ b/apps/api/v2/src/modules/organizations/organizations/organizations-organizations.controller.e2e-spec.ts @@ -46,6 +46,7 @@ import { X_CAL_SECRET_KEY, } from "@calcom/platform-constants"; import { SUCCESS_STATUS } from "@calcom/platform-constants"; +import { slugify } from "@calcom/platform-libraries"; import { ApiSuccessResponse, CreateOAuthClientInput } from "@calcom/platform-types"; import { Team } from "@calcom/prisma/client"; @@ -176,6 +177,7 @@ describe("Organizations Organizations Endpoints", () => { managedOrg = responseBody.data; expect(managedOrg?.id).toBeDefined(); expect(managedOrg?.name).toEqual(body.name); + expect(managedOrg?.slug).toEqual(slugify(body.name)); expect(managedOrg?.metadata).toEqual(body.metadata); expect(managedOrg?.apiKey).toBeDefined(); diff --git a/apps/api/v2/src/modules/organizations/organizations/outputs/managed-organization.output.ts b/apps/api/v2/src/modules/organizations/organizations/outputs/managed-organization.output.ts index 6a38643feb..4dd313e2c8 100644 --- a/apps/api/v2/src/modules/organizations/organizations/outputs/managed-organization.output.ts +++ b/apps/api/v2/src/modules/organizations/organizations/outputs/managed-organization.output.ts @@ -16,6 +16,12 @@ export class ManagedOrganizationOutput { @ApiProperty() readonly name!: string; + @IsString() + @IsOptional() + @Expose() + @ApiPropertyOptional() + readonly slug?: string; + @ApiPropertyOptional({ type: Object, example: { key: "value" }, diff --git a/apps/api/v2/src/modules/organizations/organizations/services/managed-organizations.service.ts b/apps/api/v2/src/modules/organizations/organizations/services/managed-organizations.service.ts index 752183783d..f4a917c3b0 100644 --- a/apps/api/v2/src/modules/organizations/organizations/services/managed-organizations.service.ts +++ b/apps/api/v2/src/modules/organizations/organizations/services/managed-organizations.service.ts @@ -10,6 +10,8 @@ import { ManagedOrganizationsOutputService } from "@/modules/organizations/organ import { ProfilesRepository } from "@/modules/profiles/profiles.repository"; import { ForbiddenException, Injectable, NotFoundException } from "@nestjs/common"; +import { slugify } from "@calcom/platform-libraries"; + @Injectable() export class ManagedOrganizationsService { constructor( @@ -36,6 +38,10 @@ export class ManagedOrganizationsService { const { apiKeyDaysValid, apiKeyNeverExpires, ...organizationData } = organizationInput; + if (!organizationData.slug) { + organizationData.slug = slugify(organizationData.name); + } + const organization = await this.managedOrganizationsRepository.createManagedOrganization( managerOrganizationId, { diff --git a/apps/api/v2/src/modules/organizations/teams/index/organizations-teams.controller.e2e-spec.ts b/apps/api/v2/src/modules/organizations/teams/index/organizations-teams.controller.e2e-spec.ts index af61e236da..21072dae37 100644 --- a/apps/api/v2/src/modules/organizations/teams/index/organizations-teams.controller.e2e-spec.ts +++ b/apps/api/v2/src/modules/organizations/teams/index/organizations-teams.controller.e2e-spec.ts @@ -36,6 +36,7 @@ describe("Organizations Team Endpoints", () => { let team2: Team; let teamCreatedViaApi: Team; let teamCreatedViaApi2: Team; + let teamCreatedViaApi3: Team; const userEmail = `organizations-teams-admin-${randomString()}@api.com`; let user: User; @@ -245,11 +246,38 @@ describe("Organizations Team Endpoints", () => { }); }); + it("should create the team of the org with automatically set slug", async () => { + const teamName = `Organizations Teams Automatic Slug`; + return request(app.getHttpServer()) + .post(`/v2/organizations/${org.id}/teams`) + .send({ + name: teamName, + bio: "This is our test team created via API", + } satisfies CreateOrgTeamDto) + .expect(201) + .then(async (response) => { + const responseBody: ApiSuccessResponse = response.body; + expect(responseBody.status).toEqual(SUCCESS_STATUS); + teamCreatedViaApi3 = responseBody.data; + expect(teamCreatedViaApi3.name).toEqual(teamName); + expect(teamCreatedViaApi3.slug).toEqual("organizations-teams-automatic-slug"); + expect(teamCreatedViaApi3.bio).toEqual("This is our test team created via API"); + expect(teamCreatedViaApi3.parentId).toEqual(org.id); + const membership = await membershipsRepositoryFixture.getUserMembershipByTeamId( + user.id, + teamCreatedViaApi3.id + ); + expect(membership?.role ?? "").toEqual("OWNER"); + expect(membership?.accepted).toEqual(true); + }); + }); + afterAll(async () => { await userRepositoryFixture.deleteByEmail(user.email); await teamsRepositoryFixture.delete(team.id); await teamsRepositoryFixture.delete(team2.id); await teamsRepositoryFixture.delete(teamCreatedViaApi2.id); + await teamsRepositoryFixture.delete(teamCreatedViaApi3.id); await teamsRepositoryFixture.delete(org.id); await app.close(); }); diff --git a/apps/api/v2/src/modules/organizations/teams/index/services/organizations-teams.service.ts b/apps/api/v2/src/modules/organizations/teams/index/services/organizations-teams.service.ts index f4e3c6ed1f..a9f305cb45 100644 --- a/apps/api/v2/src/modules/organizations/teams/index/services/organizations-teams.service.ts +++ b/apps/api/v2/src/modules/organizations/teams/index/services/organizations-teams.service.ts @@ -5,6 +5,8 @@ import { OrganizationsTeamsRepository } from "@/modules/organizations/teams/inde import { UserWithProfile } from "@/modules/users/users.repository"; import { Injectable } from "@nestjs/common"; +import { slugify } from "@calcom/platform-libraries"; + @Injectable() export class OrganizationsTeamsService { constructor( @@ -40,6 +42,10 @@ export class OrganizationsTeamsService { async createOrgTeam(organizationId: number, data: CreateOrgTeamDto, user: UserWithProfile) { const { autoAcceptCreator, ...rest } = data; + if (!rest.slug) { + rest.slug = slugify(rest.name); + } + const team = await this.organizationsTeamRepository.createOrgTeam(organizationId, rest); if (user.role !== "ADMIN") { @@ -56,6 +62,10 @@ export class OrganizationsTeamsService { ) { const { autoAcceptCreator, ...rest } = data; + if (!rest.slug) { + rest.slug = slugify(rest.name); + } + const team = await this.organizationsTeamRepository.createPlatformOrgTeam( organizationId, oAuthClientId, diff --git a/apps/api/v2/src/modules/teams/teams/controllers/teams.controller.e2e-spec.ts b/apps/api/v2/src/modules/teams/teams/controllers/teams.controller.e2e-spec.ts index f6ec86da58..c43d225691 100644 --- a/apps/api/v2/src/modules/teams/teams/controllers/teams.controller.e2e-spec.ts +++ b/apps/api/v2/src/modules/teams/teams/controllers/teams.controller.e2e-spec.ts @@ -20,6 +20,7 @@ import { UserRepositoryFixture } from "test/fixtures/repository/users.repository import { randomString } from "test/utils/randomString"; import { SUCCESS_STATUS } from "@calcom/platform-constants"; +import { slugify } from "@calcom/platform-libraries"; import { TeamOutputDto } from "@calcom/platform-types"; describe("Teams endpoint", () => { @@ -75,7 +76,7 @@ describe("Teams endpoint", () => { describe("User has membership in created team", () => { it("should create first team", async () => { const body: CreateTeamInput = { - name: `teams-dog-${randomString()}`, + name: `teams dog ${randomString()}`, metadata: { teamKey: "teamValue", }, @@ -93,6 +94,7 @@ describe("Teams endpoint", () => { expect(responseData).toBeDefined(); expect(responseData.id).toBeDefined(); expect(responseData.name).toEqual(body.name); + expect(responseData.slug).toEqual(slugify(body.name)); expect(responseData.metadata).toEqual(body.metadata); team1 = responseData; }); diff --git a/apps/api/v2/src/modules/teams/teams/inputs/create-team.input.ts b/apps/api/v2/src/modules/teams/teams/inputs/create-team.input.ts index cc4a74430d..d82602ece3 100644 --- a/apps/api/v2/src/modules/teams/teams/inputs/create-team.input.ts +++ b/apps/api/v2/src/modules/teams/teams/inputs/create-team.input.ts @@ -11,7 +11,11 @@ export class CreateTeamInput { @IsOptional() @IsString() - @ApiPropertyOptional({ type: String, description: "Team slug", example: "caltel" }) + @ApiPropertyOptional({ + type: String, + description: "Team slug in kebab-case - if not provided will be generated automatically based on name.", + example: "caltel", + }) readonly slug?: string; @IsOptional() diff --git a/apps/api/v2/src/modules/teams/teams/services/teams.service.ts b/apps/api/v2/src/modules/teams/teams/services/teams.service.ts index 191296d96f..383e89a0f9 100644 --- a/apps/api/v2/src/modules/teams/teams/services/teams.service.ts +++ b/apps/api/v2/src/modules/teams/teams/services/teams.service.ts @@ -6,6 +6,8 @@ import { TeamsRepository } from "@/modules/teams/teams/teams.repository"; import { BadRequestException, Injectable, InternalServerErrorException } from "@nestjs/common"; import { ConfigService } from "@nestjs/config"; +import { slugify } from "@calcom/platform-libraries"; + @Injectable() export class TeamsService { private isTeamBillingEnabled = this.configService.get("stripe.isTeamBillingEnabled"); @@ -19,6 +21,9 @@ export class TeamsService { async createTeam(input: CreateTeamInput, ownerId: number) { const { autoAcceptCreator, ...teamData } = input; + if (!teamData.slug) { + teamData.slug = slugify(teamData.name); + } const existingTeam = await this.teamsMembershipsRepository.findTeamMembershipsByNameAndUser( input.name, diff --git a/apps/api/v2/swagger/documentation.json b/apps/api/v2/swagger/documentation.json index 92ed44c6b4..39b519a70f 100644 --- a/apps/api/v2/swagger/documentation.json +++ b/apps/api/v2/swagger/documentation.json @@ -16155,7 +16155,7 @@ }, "slug": { "type": "string", - "description": "Team slug", + "description": "Team slug in kebab-case - if not provided will be generated automatically based on name.", "example": "caltel" }, "logoUrl": { @@ -19354,7 +19354,7 @@ }, "slug": { "type": "string", - "description": "Team slug", + "description": "Team slug in kebab-case - if not provided will be generated automatically based on name.", "example": "caltel" }, "logoUrl": { @@ -20267,6 +20267,11 @@ "description": "Name of the organization", "example": "CalTeam" }, + "slug": { + "type": "string", + "description": "Organization slug in kebab-case - if not provided will be generated automatically based on name.", + "example": "cal-tel" + }, "metadata": { "type": "object", "description": "You can store any additional data you want here.\nMetadata must have at most 50 keys, each key up to 40 characters.\nValues can be strings (up to 500 characters), numbers, or booleans.", @@ -20289,6 +20294,9 @@ "type": "string", "minLength": 1 }, + "slug": { + "type": "string" + }, "metadata": { "type": "object", "example": { @@ -20335,6 +20343,9 @@ "type": "string", "minLength": 1 }, + "slug": { + "type": "string" + }, "metadata": { "type": "object", "example": { diff --git a/docs/api-reference/v2/openapi.json b/docs/api-reference/v2/openapi.json index 2e752c40dc..1bc2fbb830 100644 --- a/docs/api-reference/v2/openapi.json +++ b/docs/api-reference/v2/openapi.json @@ -15024,7 +15024,7 @@ }, "slug": { "type": "string", - "description": "Team slug", + "description": "Team slug in kebab-case - if not provided will be generated automatically based on name.", "example": "caltel" }, "logoUrl": { @@ -17751,7 +17751,7 @@ }, "slug": { "type": "string", - "description": "Team slug", + "description": "Team slug in kebab-case - if not provided will be generated automatically based on name.", "example": "caltel" }, "logoUrl": { @@ -18546,6 +18546,11 @@ "description": "Name of the organization", "example": "CalTeam" }, + "slug": { + "type": "string", + "description": "Organization slug in kebab-case - if not provided will be generated automatically based on name.", + "example": "cal-tel" + }, "metadata": { "type": "object", "description": "You can store any additional data you want here.\nMetadata must have at most 50 keys, each key up to 40 characters.\nValues can be strings (up to 500 characters), numbers, or booleans.", @@ -18566,6 +18571,9 @@ "type": "string", "minLength": 1 }, + "slug": { + "type": "string" + }, "metadata": { "type": "object", "example": { @@ -18602,6 +18610,9 @@ "type": "string", "minLength": 1 }, + "slug": { + "type": "string" + }, "metadata": { "type": "object", "example": {