feat(environment): introduce isDefault flag for environments

- Added `isDefault` boolean column to the environment schema, defaulting to false.
- Updated environment creation and deletion logic to handle default environments, allowing the production environment to be created and renamed.
- Enhanced error handling for environment updates and deletions to prevent modifications to default environments.
- Updated UI components to reflect changes in environment selection based on the new default logic.
This commit is contained in:
Mauricio Siu
2025-12-10 00:10:05 -06:00
parent 48be8544cf
commit 99aa34f27e
8 changed files with 7027 additions and 50 deletions

View File

@@ -102,7 +102,9 @@ export const AdvancedEnvironmentSelector = ({
setName("");
setDescription("");
} catch (error) {
toast.error("Failed to create environment");
toast.error(
`Failed to create environment: ${error instanceof Error ? error.message : error}`,
);
}
};
@@ -123,7 +125,9 @@ export const AdvancedEnvironmentSelector = ({
setName("");
setDescription("");
} catch (error) {
toast.error("Failed to update environment");
toast.error(
`Failed to update environment: ${error instanceof Error ? error.message : error}`,
);
}
};
@@ -140,15 +144,18 @@ export const AdvancedEnvironmentSelector = ({
setIsDeleteDialogOpen(false);
setSelectedEnvironment(null);
// Redirect to production if we deleted the current environment
// Redirect to first available environment if we deleted the current environment
if (selectedEnvironment.environmentId === currentEnvironmentId) {
const productionEnv = environments?.find(
(env) => env.name === "production",
const firstEnv = environments?.find(
(env) => env.environmentId !== selectedEnvironment.environmentId,
);
if (productionEnv) {
if (firstEnv) {
router.push(
`/dashboard/project/${projectId}/environment/${productionEnv.environmentId}`,
`/dashboard/project/${projectId}/environment/${firstEnv.environmentId}`,
);
} else {
// No other environments, redirect to project page
router.push(`/dashboard/project/${projectId}`);
}
}
} catch (error) {
@@ -239,8 +246,8 @@ export const AdvancedEnvironmentSelector = ({
)}
</div>
</DropdownMenuItem>
{environment.name !== "production" && (
<div className="flex items-center gap-1 px-2">
<div className="flex items-center gap-1 px-2">
{!environment.isDefault && (
<Button
variant="ghost"
size="sm"
@@ -252,22 +259,21 @@ export const AdvancedEnvironmentSelector = ({
>
<PencilIcon className="h-3 w-3" />
</Button>
{canDeleteEnvironments && (
<Button
variant="ghost"
size="sm"
className="h-6 w-6 p-0 text-red-600 hover:text-red-700"
onClick={(e) => {
e.stopPropagation();
openDeleteDialog(environment);
}}
>
<TrashIcon className="h-3 w-3" />
</Button>
)}
</div>
)}
)}
{canDeleteEnvironments && !environment.isDefault && (
<Button
variant="ghost"
size="sm"
className="h-6 w-6 p-0 text-red-600 hover:text-red-700"
onClick={(e) => {
e.stopPropagation();
openDeleteDialog(environment);
}}
>
<TrashIcon className="h-3 w-3" />
</Button>
)}
</div>
</div>
);
})}

View File

@@ -89,24 +89,26 @@ export const SearchCommand = () => {
<CommandGroup heading={"Projects"}>
<CommandList>
{data?.map((project) => {
const productionEnvironment = project.environments.find(
(environment) => environment.name === "production",
);
// Find default environment, or fall back to first environment
const defaultEnvironment =
project.environments.find(
(environment) => environment.isDefault,
) || project?.environments?.[0];
if (!productionEnvironment) return null;
if (!defaultEnvironment) return null;
return (
<CommandItem
key={project.projectId}
onSelect={() => {
router.push(
`/dashboard/project/${project.projectId}/environment/${productionEnvironment!.environmentId}`,
`/dashboard/project/${project.projectId}/environment/${defaultEnvironment.environmentId}`,
);
setOpen(false);
}}
>
<BookIcon className="size-4 text-muted-foreground mr-2" />
{project.name} / {productionEnvironment!.name}
{project.name} / {defaultEnvironment.name}
</CommandItem>
);
})}

View File

@@ -0,0 +1,4 @@
ALTER TABLE "environment" ADD COLUMN "isDefault" boolean DEFAULT false NOT NULL;
-- Set isDefault to true for existing production environments
UPDATE "environment" SET "isDefault" = true WHERE "name" = 'production';

File diff suppressed because it is too large Load Diff

View File

@@ -925,6 +925,13 @@
"when": 1765342621312,
"tag": "0131_volatile_beast",
"breakpoints": true
},
{
"idx": 132,
"version": "7",
"when": 1765346573500,
"tag": "0132_clean_layla_miller",
"breakpoints": true
}
]
}

View File

@@ -66,10 +66,12 @@ export const environmentRouter = createTRPCRouter({
if (input.name === "production") {
throw new TRPCError({
code: "BAD_REQUEST",
message: "Environment name cannot be production",
message:
"You cannot create a environment with the name 'production'",
});
}
// Allow users to create environments with any name, including "production"
const environment = await createEnvironment(input);
if (ctx.user.role === "member") {
@@ -243,13 +245,7 @@ export const environmentRouter = createTRPCRouter({
try {
const { environmentId, ...updateData } = input;
if (updateData.name === "production") {
throw new TRPCError({
code: "BAD_REQUEST",
message: "Environment name cannot be production",
});
}
// Allow users to rename environments to any name, including "production"
if (ctx.user.role === "member") {
await checkEnvironmentAccess(
ctx.user.id,
@@ -259,6 +255,13 @@ export const environmentRouter = createTRPCRouter({
);
}
const currentEnvironment = await findEnvironmentById(environmentId);
if (currentEnvironment.isDefault) {
throw new TRPCError({
code: "BAD_REQUEST",
message: "You cannot update the default environment",
});
}
if (
currentEnvironment.project.organizationId !==
ctx.session.activeOrganizationId