diff --git a/backend/e2e-test/routes/v1/project-env.spec.ts b/backend/e2e-test/routes/v1/project-env.spec.ts index ec06d64748..0726f3a507 100644 --- a/backend/e2e-test/routes/v1/project-env.spec.ts +++ b/backend/e2e-test/routes/v1/project-env.spec.ts @@ -123,7 +123,7 @@ describe("Project Environment Router", async () => { id: deletedProjectEnvironment.id, name: mockProjectEnv.name, slug: mockProjectEnv.slug, - position: 4, + position: 5, createdAt: expect.any(String), updatedAt: expect.any(String) }) diff --git a/backend/src/lib/api-docs/constants.ts b/backend/src/lib/api-docs/constants.ts index 470bc4dee6..0d519ab8cf 100644 --- a/backend/src/lib/api-docs/constants.ts +++ b/backend/src/lib/api-docs/constants.ts @@ -533,7 +533,8 @@ export const ENVIRONMENTS = { CREATE: { workspaceId: "The ID of the project to create the environment in.", name: "The name of the environment to create.", - slug: "The slug of the environment to create." + slug: "The slug of the environment to create.", + position: "The position of the environment. The lowest number will be displayed as the first environment." }, UPDATE: { workspaceId: "The ID of the project to update the environment in.", diff --git a/backend/src/server/routes/v1/project-env-router.ts b/backend/src/server/routes/v1/project-env-router.ts index 1b0e074c28..c5ded83e46 100644 --- a/backend/src/server/routes/v1/project-env-router.ts +++ b/backend/src/server/routes/v1/project-env-router.ts @@ -123,6 +123,7 @@ export const registerProjectEnvRouter = async (server: FastifyZodProvider) => { }), body: z.object({ name: z.string().trim().describe(ENVIRONMENTS.CREATE.name), + position: z.number().min(1).optional().describe(ENVIRONMENTS.CREATE.position), slug: z .string() .trim() diff --git a/backend/src/services/project-env/project-env-dal.ts b/backend/src/services/project-env/project-env-dal.ts index 8d42aab868..15e37bfdc9 100644 --- a/backend/src/services/project-env/project-env-dal.ts +++ b/backend/src/services/project-env/project-env-dal.ts @@ -65,10 +65,16 @@ export const projectEnvDALFactory = (db: TDbClient) => { } }; + const shiftPositions = async (projectId: string, pos: number, tx?: Knex) => { + // Shift all positions >= the new position up by 1 + await (tx || db)(TableName.Environment).where({ projectId }).where("position", ">=", pos).increment("position", 1); + }; + return { ...projectEnvOrm, findBySlugs, findLastEnvPosition, - updateAllPosition + updateAllPosition, + shiftPositions }; }; diff --git a/backend/src/services/project-env/project-env-service.ts b/backend/src/services/project-env/project-env-service.ts index ea826b3a63..67bdd867a4 100644 --- a/backend/src/services/project-env/project-env-service.ts +++ b/backend/src/services/project-env/project-env-service.ts @@ -37,6 +37,7 @@ export const projectEnvServiceFactory = ({ actor, actorOrgId, actorAuthMethod, + position, name, slug }: TCreateEnvDTO) => { @@ -83,9 +84,25 @@ export const projectEnvServiceFactory = ({ } const env = await projectEnvDAL.transaction(async (tx) => { + if (position !== undefined) { + // Check if there's an environment at the specified position + const existingEnvWithPosition = await projectEnvDAL.findOne({ projectId, position }, tx); + + // If there is, then shift positions + if (existingEnvWithPosition) { + await projectEnvDAL.shiftPositions(projectId, position, tx); + } + + const doc = await projectEnvDAL.create({ slug, name, projectId, position }, tx); + await folderDAL.create({ name: "root", parentId: null, envId: doc.id, version: 1 }, tx); + + return doc; + } + // If no position is specified, add to the end const lastPos = await projectEnvDAL.findLastEnvPosition(projectId, tx); const doc = await projectEnvDAL.create({ slug, name, projectId, position: lastPos + 1 }, tx); await folderDAL.create({ name: "root", parentId: null, envId: doc.id, version: 1 }, tx); + return doc; }); @@ -150,7 +167,11 @@ export const projectEnvServiceFactory = ({ const env = await projectEnvDAL.transaction(async (tx) => { if (position) { - await projectEnvDAL.updateAllPosition(projectId, oldEnv.position, position, tx); + const existingEnvWithPosition = await projectEnvDAL.findOne({ projectId, position }, tx); + + if (existingEnvWithPosition && existingEnvWithPosition.id !== oldEnv.id) { + await projectEnvDAL.updateAllPosition(projectId, oldEnv.position, position, tx); + } } return projectEnvDAL.updateById(oldEnv.id, { name, slug, position }, tx); }); @@ -199,7 +220,6 @@ export const projectEnvServiceFactory = ({ name: "DeleteEnvironment" }); - await projectEnvDAL.updateAllPosition(projectId, doc.position, -1, tx); return doc; }); diff --git a/backend/src/services/project-env/project-env-types.ts b/backend/src/services/project-env/project-env-types.ts index 229e8b195a..a87c76d4db 100644 --- a/backend/src/services/project-env/project-env-types.ts +++ b/backend/src/services/project-env/project-env-types.ts @@ -3,6 +3,7 @@ import { TProjectPermission } from "@app/lib/types"; export type TCreateEnvDTO = { name: string; slug: string; + position?: number; } & TProjectPermission; export type TUpdateEnvDTO = {