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
@@ -0,0 +1,9 @@
{
"type": "bug",
"message": "Fix Generate all AI values doesn't work when the output field is Single Select and Generate only values for empty cells is checked",
"issue_origin": "github",
"issue_number": 4596,
"domain": "database",
"bullet_points": [],
"created_at": "2026-01-28"
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
{
"type": "feature",
"message": "List all application types in left sidebar, even if there are no items in there.",
"domain": "core",
"issue_number": null,
"bullet_points": [],
"created_at": "2026-01-26"
}
11 changes: 8 additions & 3 deletions premium/backend/src/baserow_premium/fields/job_types.py
Original file line number Diff line number Diff line change
Expand Up @@ -188,9 +188,14 @@ def _filter_empty_values(
:return: The filtered queryset.
"""

return queryset.filter(
**{f"{ai_field.db_column}__isnull": True}
) | queryset.filter(**{ai_field.db_column: ""})
baserow_field_type = ai_field.get_type().get_baserow_field_type(ai_field)
model_field = baserow_field_type.get_model_field(ai_field)
q = ai_field.get_type().empty_query(
ai_field.db_column,
model_field,
ai_field,
)
return queryset.filter(q)

def _get_field(self, field_id: int) -> AIField:
"""
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@

from baserow.contrib.database.fields.exceptions import FieldDoesNotExist
from baserow.contrib.database.fields.handler import FieldHandler
from baserow.contrib.database.fields.models import SelectOption
from baserow.contrib.database.rows.exceptions import RowDoesNotExist
from baserow.contrib.database.rows.handler import RowHandler
from baserow.contrib.database.views.exceptions import ViewDoesNotExist
Expand All @@ -19,6 +20,7 @@
from baserow.core.jobs.handler import JobHandler
from baserow.core.storage import get_default_storage
from baserow.core.user_files.handler import UserFileHandler
from baserow_premium.fields.ai_field_output_types import ChoiceAIFieldOutputType
from baserow_premium.fields.models import GenerateAIValuesJob


Expand Down Expand Up @@ -182,6 +184,48 @@ def test_create_job_with_only_empty_flag_table_mode(premium_data_fixture):
assert job.mode == GenerateAIValuesJob.MODES.TABLE


@pytest.mark.django_db
@pytest.mark.field_ai
def test_create_job_with_only_empty_choice_output_type(premium_data_fixture):
"""Test job creation with only_empty=True and single select output."""

premium_data_fixture.register_fake_generate_ai_type()
user = premium_data_fixture.create_user()
database = premium_data_fixture.create_database_application(user=user)
table = premium_data_fixture.create_database_table(database=database)
field = premium_data_fixture.create_ai_field(
table=table, ai_prompt="'test'", ai_output_type=ChoiceAIFieldOutputType.type
)
option_1 = SelectOption.objects.create(field=field, value="A", order=1)
SelectOption.objects.create(field=field, value="B", order=2)
model = table.get_model()

rows = (
RowHandler()
.create_rows(
user, table, rows_values=[{f"field_{field.id}": option_1.value}, {}, {}]
)
.created_rows
)
row_ids = [row.id for row in rows]

job = JobHandler().create_and_start_job(
user,
"generate_ai_values",
field_id=field.id,
row_ids=row_ids,
only_empty=True,
sync=True,
)

assert job.only_empty is True
assert job.mode == GenerateAIValuesJob.MODES.ROWS

choice_values = model.objects.all().values_list(f"field_{field.id}", flat=True)
assert choice_values[0] == option_1.id
assert all(x is not None for x in choice_values), choice_values


@pytest.mark.django_db
@pytest.mark.field_ai
def test_create_job_with_nonexistent_field(premium_data_fixture):
Expand Down
36 changes: 34 additions & 2 deletions web-frontend/modules/core/assets/scss/components/tree.scss
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
margin: 0;

&:not(:last-child) {
margin-bottom: 12px;
margin-bottom: 8px;
}

.tree__item & {
Expand Down Expand Up @@ -312,8 +312,40 @@
}

.tree__heading {
display: flex;
margin: 8px;
justify-content: space-between;
align-items: center;
}

.tree__heading-name {
font-size: 11px;
font-weight: 500;
color: $palette-neutral-900;
margin: 8px;
line-height: 16px;
}

.tree__heading-add {
color: $palette-neutral-600;
width: 16px;
height: 16px;
display: flex;
align-items: center;
justify-content: center;
font-size: 16px;
line-height: 16px;

@include rounded($rounded);

&:hover {
text-decoration: none;
background-color: $palette-neutral-200;
outline: solid 2px $palette-neutral-200;
color: $palette-neutral-800;
}
}

.tree__separator {
border-bottom: solid 1px $palette-neutral-200;
margin: 8px 8px 16px;
}
125 changes: 72 additions & 53 deletions web-frontend/modules/core/components/sidebar/SidebarWithWorkspace.vue
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
<template>
<div class="sidebar__section sidebar__section--scrollable">
<div v-if="hasItems" class="sidebar__section-scrollable">
<div class="sidebar__section-scrollable">
<div
class="sidebar__section-scrollable-inner"
data-highlight="applications"
Expand All @@ -14,52 +14,66 @@
>
</component>
</ul>
<ul v-if="applicationsCount" class="tree">
<ul class="tree">
<li
v-for="applicationGroup in groupedApplicationsForSelectedWorkspace"
v-for="(applicationGroup, index) in groupedApplicationsForSelectedWorkspace"
:key="applicationGroup.type"
>
<template v-if="applicationGroup.applications.length > 0">
<div class="tree__heading">
<div class="tree__heading" :class="{'margin-bottom-2': applicationGroup.applications.length === 0 && index < groupedApplicationsForSelectedWorkspace.length - 1}">
<div class="tree__heading-name">
{{ applicationGroup.name }}
</div>
<ul
class="tree"
:class="{
'margin-bottom-0': pendingJobs[applicationGroup.type].length,
}"
data-highlight="applications"
<a
v-if="canCreateApplication"
:ref="'createApplicationModalToggle' + applicationGroup.type"
class="tree__heading-add"
@click="openCreateApplicationModal(applicationGroup.type)"
>
<component
:is="getApplicationComponent(application)"
v-for="application in applicationGroup.applications"
:key="application.id"
v-sortable="{
id: application.id,
update: orderApplications,
handle: '[data-sortable-handle]',
marginTop: -1.5,
enabled: $hasPermission(
'workspace.order_applications',
selectedWorkspace,
selectedWorkspace.id
),
}"
:application="application"
:pending-jobs="pendingJobs[application.type]"
<i class="iconoir-plus"></i>
<CreateApplicationModal
:ref="'createApplicationModal' + applicationGroup.type"
:application-type="applicationGroup.applicationType"
:workspace="selectedWorkspace"
></component>
</ul>
<ul v-if="pendingJobs[applicationGroup.type].length" class="tree">
<component
:is="getPendingJobComponent(job)"
v-for="job in pendingJobs[applicationGroup.type]"
:key="job.id"
:job="job"
>
</component>
</ul>
</template>
></CreateApplicationModal>
</a>
</div>
<ul
class="tree"
:class="{
'margin-bottom-0': pendingJobs[applicationGroup.type].length,
}"
data-highlight="applications"
>
<component
:is="getApplicationComponent(application)"
v-for="application in applicationGroup.applications"
:key="application.id"
v-sortable="{
id: application.id,
update: orderApplications,
handle: '[data-sortable-handle]',
marginTop: -1.5,
enabled: $hasPermission(
'workspace.order_applications',
selectedWorkspace,
selectedWorkspace.id
),
}"
:application="application"
:pending-jobs="pendingJobs[application.type]"
:workspace="selectedWorkspace"
></component>
</ul>
<ul v-if="pendingJobs[applicationGroup.type].length" class="tree">
<component
:is="getPendingJobComponent(job)"
v-for="job in pendingJobs[applicationGroup.type]"
:key="job.id"
:job="job"
>
</component>
</ul>
<div v-if="index < groupedApplicationsForSelectedWorkspace.length - 1" class="tree__separator"></div>
</li>
</ul>
</div>
Expand All @@ -72,10 +86,7 @@
selectedWorkspace.id
)
"
class="sidebar__new-wrapper"
:class="{
'sidebar__new-wrapper--separator': hasItems,
}"
class="sidebar__new-wrapper sidebar__new-wrapper--separator"
data-highlight="create-new"
>
<a
Expand All @@ -100,11 +111,12 @@

<script>
import { notifyIf } from '@baserow/modules/core/utils/error'
import CreateApplicationModal from '@baserow/modules/core/components/application/CreateApplicationModal'
import CreateApplicationContext from '@baserow/modules/core/components/application/CreateApplicationContext'

export default {
name: 'SidebarWithWorkspace',
components: { CreateApplicationContext },
components: { CreateApplicationContext, CreateApplicationModal },
props: {
applications: {
type: Array,
Expand All @@ -127,6 +139,7 @@ export default {
return {
name: applicationType.getNamePlural(),
type: applicationType.getType(),
applicationType: applicationType,
developmentStage: applicationType.developmentStage,
applications: this.applications
.filter((application) => {
Expand All @@ -141,12 +154,6 @@ export default {
})
return applicationTypes
},
applicationsCount() {
return this.groupedApplicationsForSelectedWorkspace.reduce(
(acc, group) => acc + group.applications.length,
0
)
},
pendingJobs() {
const grouped = { null: [] }
Object.values(this.$registry.getAll('application')).forEach(
Expand All @@ -162,8 +169,12 @@ export default {
})
return grouped
},
hasItems() {
return this.applicationsCount || this.pendingJobs.null.length
canCreateApplication() {
return this.$hasPermission(
'workspace.create_application',
this.selectedWorkspace,
this.selectedWorkspace.id
)
},
},
methods: {
Expand All @@ -186,6 +197,14 @@ export default {
notifyIf(error, 'application')
}
},
openCreateApplicationModal(type) {
if (!this.canCreateApplication) {
return
}

const target = this.$refs['createApplicationModalToggle' + type]
this.$refs['createApplicationModal' + type][0].toggle(target)
},
},
}
</script>
Loading