diff --git a/alembic/versions/2026_07_07_1535-e2d87251fee8_browser_profile_name_unique_partial_on_deleted_at.py b/alembic/versions/2026_07_07_1535-e2d87251fee8_browser_profile_name_unique_partial_on_deleted_at.py new file mode 100644 index 000000000..0ac0a82ca --- /dev/null +++ b/alembic/versions/2026_07_07_1535-e2d87251fee8_browser_profile_name_unique_partial_on_deleted_at.py @@ -0,0 +1,51 @@ +"""browser profile name unique partial on deleted_at + +Revision ID: e2d87251fee8 +Revises: aff1632dc377 +Create Date: 2026-07-07T15:35:51.042177+00:00 + +""" + +from typing import Sequence, Union + +import sqlalchemy as sa + +from alembic import op + +# revision identifiers, used by Alembic. +revision: str = "e2d87251fee8" +down_revision: Union[str, None] = "aff1632dc377" +branch_labels: Union[str, Sequence[str], None] = None +depends_on: Union[str, Sequence[str], None] = None + + +def upgrade() -> None: + op.drop_index( + "uq_browser_profiles_org_name_user", + table_name="browser_profiles", + postgresql_where=sa.text("is_managed = false"), + ) + op.create_index( + "uq_browser_profiles_org_name_user", + "browser_profiles", + ["organization_id", "name"], + unique=True, + postgresql_where=sa.text("is_managed = false AND deleted_at IS NULL"), + sqlite_where=sa.text("is_managed = false AND deleted_at IS NULL"), + ) + + +def downgrade() -> None: + op.drop_index( + "uq_browser_profiles_org_name_user", + table_name="browser_profiles", + postgresql_where=sa.text("is_managed = false AND deleted_at IS NULL"), + ) + op.create_index( + "uq_browser_profiles_org_name_user", + "browser_profiles", + ["organization_id", "name"], + unique=True, + postgresql_where=sa.text("is_managed = false"), + sqlite_where=sa.text("is_managed = false"), + ) diff --git a/skyvern-frontend/src/routes/credentials/CredentialsModal.tsx b/skyvern-frontend/src/routes/credentials/CredentialsModal.tsx index 39bf26cb5..ccf1d960b 100644 --- a/skyvern-frontend/src/routes/credentials/CredentialsModal.tsx +++ b/skyvern-frontend/src/routes/credentials/CredentialsModal.tsx @@ -999,8 +999,13 @@ function CredentialsModal({ } setAuthenticatorSaveError(null); - // If test passed, rename the temp credential instead of creating a new one - if (testAndSave && testStatus === "completed" && testCredentialId) { + const hasCompletedInlineTest = + testAndSave && testStatus === "completed" && !!testCredentialId; + + // Create mode: the temp credential created by the inline test already holds + // the entered secret and the saved browser profile, so rename it in place + // instead of creating a duplicate. + if (!isEditMode && hasCompletedInlineTest && testCredentialId) { const url = testUrl.trim(); const ctx = userContext.trim(); renameCredentialMutation.mutate({ @@ -1014,6 +1019,20 @@ function CredentialsModal({ return; } + // Edit mode: the inline test's profile is tied to the throwaway temp + // credential, so we must overwrite the real credential's secret (below) and + // re-run the test against it to attach a fresh profile. Delete the orphaned + // temp credential — onSuccess resets the refs before the dialog close + // handler would otherwise clean it up. + if (isEditMode && hasCompletedInlineTest && testCredentialId) { + const tempCredentialId = testCredentialId; + getClient(credentialGetter) + .then((client) => client.delete(`/credentials/${tempCredentialId}`)) + .catch(() => { + // Best-effort cleanup + }); + } + // Capture intent before mutation — state will be reset in onSuccess // In edit mode, only trigger a background test if the user actually changed // credentials, user_context, or URL — not just because the checkbox was pre-checked. @@ -1025,7 +1044,8 @@ function CredentialsModal({ saveIntentRef.current = { shouldTestAfterSave: testAndSave && - testStatus !== "completed" && + (testStatus !== "completed" || + (isEditMode && hasCompletedInlineTest)) && testUrl.trim() !== "" && hasEditModeChanges, saveBrowserSessionIntent: testAndSave, diff --git a/skyvern-frontend/src/routes/credentials/hooks/useCredentialFolderMutations.ts b/skyvern-frontend/src/routes/credentials/hooks/useCredentialFolderMutations.ts index 824a2d8e5..5251cb220 100644 --- a/skyvern-frontend/src/routes/credentials/hooks/useCredentialFolderMutations.ts +++ b/skyvern-frontend/src/routes/credentials/hooks/useCredentialFolderMutations.ts @@ -75,6 +75,11 @@ function useDeleteCredentialFolderMutation() { onSuccess: () => { queryClient.invalidateQueries({ queryKey: ["credential-folders"] }); queryClient.invalidateQueries({ queryKey: ["credentials"] }); + toast({ + variant: "success", + title: "Folder deleted", + description: "The folder has been deleted.", + }); }, onError: (error: Error) => { toast({ diff --git a/skyvern/forge/sdk/db/models.py b/skyvern/forge/sdk/db/models.py index 5fcdfdaa1..c51bf9bec 100644 --- a/skyvern/forge/sdk/db/models.py +++ b/skyvern/forge/sdk/db/models.py @@ -1243,8 +1243,8 @@ class BrowserProfileModel(Base): "organization_id", "name", unique=True, - postgresql_where=text("is_managed = false"), - sqlite_where=text("is_managed = false"), + postgresql_where=text("is_managed = false AND deleted_at IS NULL"), + sqlite_where=text("is_managed = false AND deleted_at IS NULL"), ), Index( "uq_browser_profiles_managed_segment",