refactor: v2 automatic team slugs (#20897)
* refactor: automatic team slugs * refactor: automatic organizations team slugs * refactor: automatic managed organizations slugs * docs * refactor: test
This commit is contained in:
+10
@@ -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,
|
||||
|
||||
+2
@@ -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();
|
||||
|
||||
|
||||
+6
@@ -16,6 +16,12 @@ export class ManagedOrganizationOutput {
|
||||
@ApiProperty()
|
||||
readonly name!: string;
|
||||
|
||||
@IsString()
|
||||
@IsOptional()
|
||||
@Expose()
|
||||
@ApiPropertyOptional()
|
||||
readonly slug?: string;
|
||||
|
||||
@ApiPropertyOptional({
|
||||
type: Object,
|
||||
example: { key: "value" },
|
||||
|
||||
+6
@@ -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,
|
||||
{
|
||||
|
||||
+28
@@ -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<Team> = 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();
|
||||
});
|
||||
|
||||
+10
@@ -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,
|
||||
|
||||
@@ -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;
|
||||
});
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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": {
|
||||
|
||||
@@ -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": {
|
||||
|
||||
Reference in New Issue
Block a user