Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,11 +1,5 @@
from rest_framework.status import HTTP_400_BAD_REQUEST, HTTP_404_NOT_FOUND

ERROR_AUTOMATION_WORKFLOW_NAME_NOT_UNIQUE = (
"ERROR_AUTOMATION_WORKFLOW_NAME_NOT_UNIQUE",
HTTP_400_BAD_REQUEST,
"The workflow name {e.name} already exists for your automation instance.",
)

ERROR_AUTOMATION_WORKFLOW_DOES_NOT_EXIST = (
"ERROR_AUTOMATION_WORKFLOW_DOES_NOT_EXIST",
HTTP_404_NOT_FOUND,
Expand Down
6 changes: 0 additions & 6 deletions backend/src/baserow/contrib/automation/api/workflows/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,6 @@
from baserow.api.serializers import get_example_pagination_serializer_class
from baserow.contrib.automation.api.workflows.errors import (
ERROR_AUTOMATION_WORKFLOW_DOES_NOT_EXIST,
ERROR_AUTOMATION_WORKFLOW_NAME_NOT_UNIQUE,
ERROR_AUTOMATION_WORKFLOW_NOT_IN_AUTOMATION,
)
from baserow.contrib.automation.api.workflows.serializers import (
Expand All @@ -38,7 +37,6 @@
)
from baserow.contrib.automation.workflows.exceptions import (
AutomationWorkflowDoesNotExist,
AutomationWorkflowNameNotUnique,
AutomationWorkflowNotInAutomation,
)
from baserow.contrib.automation.workflows.job_types import (
Expand Down Expand Up @@ -75,7 +73,6 @@ class AutomationWorkflowsView(APIView):
400: get_error_schema(
[
"ERROR_REQUEST_BODY_VALIDATION",
"ERROR_AUTOMATION_WORKFLOW_NAME_NOT_UNIQUE",
]
),
404: get_error_schema(["ERROR_APPLICATION_DOES_NOT_EXIST"]),
Expand All @@ -85,7 +82,6 @@ class AutomationWorkflowsView(APIView):
@map_exceptions(
{
ApplicationDoesNotExist: ERROR_APPLICATION_DOES_NOT_EXIST,
AutomationWorkflowNameNotUnique: ERROR_AUTOMATION_WORKFLOW_NAME_NOT_UNIQUE,
}
)
@validate_body(CreateAutomationWorkflowSerializer, return_validated=True)
Expand Down Expand Up @@ -152,7 +148,6 @@ def get(self, request, workflow_id: int):
400: get_error_schema(
[
"ERROR_REQUEST_BODY_VALIDATION",
"ERROR_AUTOMATION_WORKFLOW_NAME_NOT_UNIQUE",
]
),
404: get_error_schema(
Expand All @@ -167,7 +162,6 @@ def get(self, request, workflow_id: int):
@map_exceptions(
{
ApplicationDoesNotExist: ERROR_APPLICATION_DOES_NOT_EXIST,
AutomationWorkflowNameNotUnique: ERROR_AUTOMATION_WORKFLOW_NAME_NOT_UNIQUE,
AutomationWorkflowDoesNotExist: ERROR_AUTOMATION_WORKFLOW_DOES_NOT_EXIST,
}
)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
# Generated by Django 5.2.12 on 2026-03-25 04:56

from django.db import migrations


class Migration(migrations.Migration):

dependencies = [
('automation', '0025_automationnoderesult_iteration_path'),
]

operations = [
migrations.AlterUniqueTogether(
name='automationworkflow',
unique_together=set(),
),
]
14 changes: 0 additions & 14 deletions backend/src/baserow/contrib/automation/workflows/exceptions.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,20 +17,6 @@ def __init__(self, workflow_id=None, *args, **kwargs):
)


class AutomationWorkflowNameNotUnique(AutomationWorkflowError):
"""When a new workflow's name conflicts an existing name."""

def __init__(self, name=None, automation_id=None, *args, **kwargs):
self.name = name
self.automation_id = automation_id
super().__init__(
f"A workflow with the name {name} already exists in the automation with id "
f"{automation_id}",
*args,
**kwargs,
)


class AutomationWorkflowDoesNotExist(AutomationWorkflowError):
"""When the workflow doesn't exist."""

Expand Down
35 changes: 7 additions & 28 deletions backend/src/baserow/contrib/automation/workflows/handler.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
from django.conf import settings
from django.contrib.auth.models import AbstractUser
from django.core.files.storage import Storage
from django.db import IntegrityError, transaction
from django.db import transaction
from django.db.models import QuerySet
from django.utils import timezone

Expand Down Expand Up @@ -37,7 +37,6 @@
from baserow.contrib.automation.workflows.exceptions import (
AutomationWorkflowBeforeRunError,
AutomationWorkflowDoesNotExist,
AutomationWorkflowNameNotUnique,
AutomationWorkflowNotInAutomation,
AutomationWorkflowRateLimited,
AutomationWorkflowTooManyErrors,
Expand All @@ -51,7 +50,6 @@
from baserow.contrib.automation.workflows.types import UpdatedAutomationWorkflow
from baserow.core.cache import global_cache, local_cache
from baserow.core.exceptions import IdDoesNotExist
from baserow.core.psycopg import is_unique_violation_error
from baserow.core.registries import ImportExportConfig
from baserow.core.storage import ExportZipFile, get_default_storage
from baserow.core.telemetry.utils import baserow_trace_methods
Expand Down Expand Up @@ -166,23 +164,11 @@ def create_workflow(self, automation: Automation, name: str) -> AutomationWorkfl

last_order = AutomationWorkflow.get_last_order(automation)

# Find a name unused in a trashed or existing workflow
unused_name = self.find_unused_workflow_name(automation, name)

try:
workflow = AutomationWorkflow.objects.create(
automation=automation,
name=unused_name,
order=last_order,
)
except IntegrityError as e:
if "unique constraint" in e.args[0] and "name" in e.args[0]:
raise AutomationWorkflowNameNotUnique(
name=name, automation_id=automation.id
) from e
raise

return workflow
return AutomationWorkflow.objects.create(
automation=automation,
name=name,
order=last_order,
)

def delete_workflow(self, user: AbstractUser, workflow: AutomationWorkflow) -> None:
"""
Expand Down Expand Up @@ -238,14 +224,7 @@ def update_workflow(
for key, value in allowed_values.items():
setattr(workflow, key, value)

try:
workflow.save()
except IntegrityError as e:
if is_unique_violation_error(e) and "name" in str(e):
raise AutomationWorkflowNameNotUnique(
name=workflow.name, automation_id=workflow.automation_id
) from e
raise
workflow.save()

new_workflow_values = self.export_prepared_values(workflow)

Expand Down
1 change: 0 additions & 1 deletion backend/src/baserow/contrib/automation/workflows/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,6 @@ class AutomationWorkflow(

class Meta:
ordering = ("order",)
unique_together = [["automation", "name"]]

def get_parent(self):
return self.automation
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -115,7 +115,7 @@ def test_create_workflow_duplicate_name(api_client, data_fixture):
)

assert response.status_code == HTTP_200_OK
assert response.json()["name"] != "test"
assert response.json()["name"] == "test"


@pytest.mark.django_db
Expand Down Expand Up @@ -181,7 +181,7 @@ def test_update_workflow_duplicate_name(api_client, data_fixture):
user, automation=automation, name="test"
)
workflow_2 = data_fixture.create_automation_workflow(
user, automation=automation, name="test2"
user, automation=automation, name="test"
)

url = reverse(API_URL_WORKFLOW_ITEM, kwargs={"workflow_id": workflow_2.id})
Expand All @@ -192,8 +192,11 @@ def test_update_workflow_duplicate_name(api_client, data_fixture):
HTTP_AUTHORIZATION=f"JWT {token}",
)

assert response.status_code == HTTP_400_BAD_REQUEST
assert response.json()["error"] == "ERROR_AUTOMATION_WORKFLOW_NAME_NOT_UNIQUE"
assert response.status_code == HTTP_200_OK
workflow.refresh_from_db()
assert workflow.name == "test"
workflow_2.refresh_from_db()
assert workflow_2.name == "test"


@pytest.mark.django_db
Expand Down Expand Up @@ -617,5 +620,8 @@ def test_rename_workflow_using_existing_workflow_name(api_client, data_fixture):
HTTP_AUTHORIZATION=f"JWT {token}",
)

assert response.status_code == HTTP_400_BAD_REQUEST
assert response.json()["error"] == "ERROR_AUTOMATION_WORKFLOW_NAME_NOT_UNIQUE"
assert response.status_code == HTTP_200_OK
workflow_1.refresh_from_db()
assert workflow_1.name == "test1"
workflow_2.refresh_from_db()
assert workflow_2.name == "test1"
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,6 @@
from baserow.contrib.automation.workflows.exceptions import (
AutomationWorkflowBeforeRunError,
AutomationWorkflowDoesNotExist,
AutomationWorkflowNameNotUnique,
AutomationWorkflowNotInAutomation,
AutomationWorkflowRateLimited,
AutomationWorkflowTooManyErrors,
Expand Down Expand Up @@ -107,22 +106,6 @@ def test_create_workflow(data_fixture):
assert workflow.name == "test"


@pytest.mark.django_db
def test_create_workflow_name_not_unique(data_fixture):
workflow = data_fixture.create_automation_workflow(name="test")

handler = AutomationWorkflowHandler()
# Simulate it returning the same name
handler.find_unused_workflow_name = MagicMock(return_value="test")

with pytest.raises(AutomationWorkflowNameNotUnique):
handler.create_workflow(workflow.automation, name="test")

handler.find_unused_workflow_name.assert_called_once_with(
workflow.automation, "test"
)


@pytest.mark.django_db
def test_create_workflow_integrity_error(data_fixture):
unexpected_error = IntegrityError("unexpected integrity error")
Expand Down Expand Up @@ -172,8 +155,12 @@ def test_update_workflow_name_not_unique(data_fixture):
automation=workflow_1.automation, name="test2"
)

with pytest.raises(AutomationWorkflowNameNotUnique):
AutomationWorkflowHandler().update_workflow(workflow_2, name=workflow_1.name)
AutomationWorkflowHandler().update_workflow(workflow_2, name=workflow_1.name)

workflow_1.refresh_from_db()
assert workflow_1.name == "test1"
workflow_2.refresh_from_db()
assert workflow_2.name == "test1"


@pytest.mark.django_db
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
{
"type": "bug",
"message": "Fixed a bug that prevented reordering workflows in an automation.",
"issue_origin": "github",
"issue_number": 4995,
"domain": "automation",
"bullet_points": [],
"created_at": "2026-03-18"
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
{
"type": "bug",
"message": "Removed a constraint that forced unique workflow names.",
"issue_origin": "github",
"issue_number": 4995,
"domain": "automation",
"bullet_points": [],
"created_at": "2026-03-18"
}
Original file line number Diff line number Diff line change
Expand Up @@ -100,9 +100,6 @@ export default {
getFormValues() {
return Object.assign({}, this.values, this.getChildFormsValues(), {})
},
isNameUnique(name) {
return !this.workflowNames.includes(name) || name === this.workflow?.name
},
},
validations() {
return {
Expand All @@ -112,10 +109,6 @@ export default {
this.$t('error.requiredField'),
required
),
isUnique: helpers.withMessage(
this.$t('automationWorkflowErrors.errorNameNotUnique'),
this.isNameUnique
),
maxLength: helpers.withMessage(
this.$t('error.maxLength', { max: 255 }),
maxLength(225)
Expand Down
4 changes: 0 additions & 4 deletions web-frontend/modules/automation/locales/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -48,10 +48,6 @@
"duplicating": "Duplicating",
"duplicatedTitle": "Workflow duplicated"
},
"automationWorkflowErrors": {
"errorNameNotUnique": "A workflow with this name already exists",
"errorNameNotUniqueDescription": "Please enter a unique name for the workflow"
},
"trashType": {
"workflow": "workflow",
"automation": "automation"
Expand Down
8 changes: 1 addition & 7 deletions web-frontend/modules/automation/plugin.js
Original file line number Diff line number Diff line change
Expand Up @@ -48,16 +48,10 @@ export default defineNuxtPlugin({
name: 'automation',
dependsOn: ['core', 'store'],
setup(nuxtApp) {
const { $registry, $store, $clientErrorMap, $i18n } = nuxtApp
const { $registry, $store } = nuxtApp

const context = { app: nuxtApp }

$clientErrorMap.setError(
'ERROR_AUTOMATION_WORKFLOW_NAME_NOT_UNIQUE',
$i18n.t('automationWorkflowErrors.errorNameNotUnique'),
$i18n.t('automationWorkflowErrors.errorNameNotUniqueDescription')
)

// Register stores
$store.registerModuleNuxtSafe(
'automationApplication',
Expand Down
6 changes: 3 additions & 3 deletions web-frontend/modules/automation/services/workflow.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,9 +17,9 @@ export default (client) => {
delete(workflowId) {
return client.delete(`automation/workflows/${workflowId}/`)
},
order(workflowId, nodeIds) {
return client.post(`/automation/workflows/${workflowId}/nodes/order/`, {
node_ids: nodeIds,
order(automationId, order) {
return client.post(`/automation/${automationId}/workflows/order/`, {
workflow_ids: order,
})
},
duplicate(workflowId) {
Expand Down
32 changes: 32 additions & 0 deletions web-frontend/modules/core/middleware/dashboardRedirect.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
/**
* Redirects from the dashboard to the appropriate workspace page if the user
* has workspaces. This must run as middleware (not in <script setup>) to avoid
* chained navigateTo during Suspense, which breaks in Nuxt 3.21+.
*/
export default defineNuxtRouteMiddleware(async (to) => {
const nuxtApp = useNuxtApp()
const store = nuxtApp.$store

const selectedWorkspace = store.getters['workspace/getSelected']
const allWorkspaces = store.getters['workspace/getAll']

if (selectedWorkspace?.id) {
return navigateTo(
{
name: 'workspace',
params: { workspaceId: selectedWorkspace.id },
query: to.query,
},
{ replace: true }
)
} else if (allWorkspaces?.length > 0) {
return navigateTo(
{
name: 'workspace',
params: { workspaceId: allWorkspaces[0].id },
query: to.query,
},
{ replace: true }
)
}
})
5 changes: 5 additions & 0 deletions web-frontend/modules/core/module.js
Original file line number Diff line number Diff line change
Expand Up @@ -177,6 +177,11 @@ export default defineNuxtModule({
path: resolve('./middleware/urlCheck'),
})

addRouteMiddleware({
name: 'dashboardRedirect',
path: resolve('./middleware/dashboardRedirect'),
})

// Changes the stroke-width of the iconoir svg files because this way, we don't
// have to fork the repository and change it there.
const iconoirCssPath = require.resolve('iconoir/css/iconoir.css')
Expand Down
Loading
Loading