diff --git a/backend/src/baserow/contrib/database/rows/webhook_event_types.py b/backend/src/baserow/contrib/database/rows/webhook_event_types.py index b7fdfa9a07..fd1359e6ae 100644 --- a/backend/src/baserow/contrib/database/rows/webhook_event_types.py +++ b/backend/src/baserow/contrib/database/rows/webhook_event_types.py @@ -156,6 +156,9 @@ def get_related_table_row_ids_with_changes( :return: A list of row IDs in the related table that have changed. """ + if not rows: + return [] + model = rows[0]._meta.model table_field_objs = model.get_field_objects() @@ -251,7 +254,7 @@ def get_filters_for_webhooks_to_call( q = self._get_filters_for_webhooks_to_call(model=model, table=table, **kwargs) - if kwargs.get("rows") is not None: + if kwargs.get("rows"): q |= self.get_filters_for_related_webhook_to_call( model=model, table=table, **kwargs ) diff --git a/backend/tests/baserow/contrib/database/rows/test_row_webhook_event_types.py b/backend/tests/baserow/contrib/database/rows/test_row_webhook_event_types.py index 7de9a1fcec..9e94dac789 100644 --- a/backend/tests/baserow/contrib/database/rows/test_row_webhook_event_types.py +++ b/backend/tests/baserow/contrib/database/rows/test_row_webhook_event_types.py @@ -8,7 +8,10 @@ from baserow.contrib.database.fields.handler import FieldHandler from baserow.contrib.database.rows.handler import RowHandler -from baserow.contrib.database.rows.webhook_event_types import RowsUpdatedEventType +from baserow.contrib.database.rows.webhook_event_types import ( + RowsDeletedEventType, + RowsUpdatedEventType, +) from baserow.contrib.database.webhooks.handler import WebhookHandler from baserow.contrib.database.webhooks.models import TableWebhook from baserow.contrib.database.webhooks.registries import webhook_event_type_registry @@ -974,3 +977,30 @@ def _without_version_checks(queries): assert len(_without_version_checks(captured.captured_queries)) >= len( _without_version_checks(captured2.captured_queries) ) + + +@pytest.mark.django_db() +def test_get_filters_for_webhooks_to_call_with_empty_rows(data_fixture): + user = data_fixture.create_user() + table_a, table_b, link_a_to_b = data_fixture.create_two_linked_tables(user=user) + + event_type: RowsDeletedEventType = webhook_event_type_registry.get("rows.deleted") + model_a = table_a.get_model() + + q = event_type.get_filters_for_webhooks_to_call( + model=model_a, table=table_a, rows=[] + ) + + assert list(TableWebhook.objects.filter(q).values_list("id", flat=True)) == [] + + +@pytest.mark.django_db() +def test_get_related_table_row_ids_with_changes_empty_rows(data_fixture): + user = data_fixture.create_user() + table_a, table_b, link_a_to_b = data_fixture.create_two_linked_tables(user=user) + + event_type: RowsDeletedEventType = webhook_event_type_registry.get("rows.deleted") + + result = event_type.get_related_table_row_ids_with_changes(table_b, rows=[]) + + assert result == [] diff --git a/changelog/entries/unreleased/bug/5838_guard_webhook_filter_computation_against_empty_rows_lists_to.json b/changelog/entries/unreleased/bug/5838_guard_webhook_filter_computation_against_empty_rows_lists_to.json new file mode 100644 index 0000000000..a8e37be397 --- /dev/null +++ b/changelog/entries/unreleased/bug/5838_guard_webhook_filter_computation_against_empty_rows_lists_to.json @@ -0,0 +1,9 @@ +{ + "type": "bug", + "message": "Guard webhook filter computation against empty rows lists to prevent IndexError on undo.", + "issue_origin": "github", + "issue_number": 5838, + "domain": "database", + "bullet_points": [], + "created_at": "2026-08-04" +} diff --git a/changelog/entries/unreleased/bug/5846_fix_field_store_crash_when_deleting_a_kanban_singleselect_st.json b/changelog/entries/unreleased/bug/5846_fix_field_store_crash_when_deleting_a_kanban_singleselect_st.json new file mode 100644 index 0000000000..7662b5aa2d --- /dev/null +++ b/changelog/entries/unreleased/bug/5846_fix_field_store_crash_when_deleting_a_kanban_singleselect_st.json @@ -0,0 +1,9 @@ +{ + "type": "bug", + "message": "Fix field store crash when deleting a Kanban single-select stack.", + "issue_origin": "github", + "issue_number": 5846, + "domain": "database", + "bullet_points": [], + "created_at": "2026-08-04" +} diff --git a/premium/web-frontend/modules/baserow_premium/store/view/kanban.js b/premium/web-frontend/modules/baserow_premium/store/view/kanban.js index 76d909f7c9..8547d37dd7 100644 --- a/premium/web-frontend/modules/baserow_premium/store/view/kanban.js +++ b/premium/web-frontend/modules/baserow_premium/store/view/kanban.js @@ -1120,7 +1120,7 @@ export const actions = { await dispatch( 'field/forceUpdate', { - singleSelectField, + field: singleSelectField, oldField: clone(singleSelectField), data, relatedFields: data.related_fields, diff --git a/premium/web-frontend/test/unit/premium/store/view/kanban.spec.js b/premium/web-frontend/test/unit/premium/store/view/kanban.spec.js index 855a3a887f..bd1f3ff485 100644 --- a/premium/web-frontend/test/unit/premium/store/view/kanban.spec.js +++ b/premium/web-frontend/test/unit/premium/store/view/kanban.spec.js @@ -1,4 +1,5 @@ import kanbanStore from '@baserow_premium/store/view/kanban' +import fieldStore from '@baserow/modules/database/store/field' import { TestApp } from '@baserow/test/helpers/testApp' import { UNDO_REDO_ACTION_GROUP_HEADER } from '@baserow/modules/database/utils/action' @@ -23,6 +24,7 @@ describe('Kanban view store', () => { store = testApp.createStore({ modules: { kanban: kanbanStore, + field: fieldStore, }, }) }) @@ -509,4 +511,54 @@ describe('Kanban view store', () => { before ) }) + + test('deleteStack updates the field in the store instead of throwing', async () => { + const singleSelectField = { + id: 1, + table_id: 99, + name: 'Status', + type: 'single_select', + select_options: [ + { id: 1, value: 'A', color: 'blue' }, + { id: 2, value: 'B', color: 'red' }, + ], + } + + const fieldState = Object.assign(fieldStore.state(), { + items: [singleSelectField], + }) + store.replaceState({ ...store.state, field: fieldState }) + + const state = Object.assign(kanbanStore.state(), { + singleSelectFieldId: 1, + stacks: {}, + }) + store.replaceState({ ...store.state, kanban: state }) + + const updatedField = { + id: 1, + table_id: 99, + name: 'Status', + type: 'single_select', + select_options: [{ id: 1, value: 'A', color: 'blue' }], + related_fields: [], + } + testApp.mock.onPatch('/database/fields/1/').reply(200, updatedField) + + const originalGetAll = store.$registry.getAll.bind(store.$registry) + vi.spyOn(store.$registry, 'getAll').mockImplementation((namespace) => + namespace === 'view' ? {} : originalGetAll(namespace) + ) + + const doFieldUpdate = await store.dispatch('kanban/deleteStack', { + singleSelectField, + optionId: 2, + deferredFieldUpdate: true, + }) + + await doFieldUpdate() + + expect(store.state.field.items[0].id).toBe(1) + expect(store.state.field.items[0].select_options).toHaveLength(1) + }) })