diff --git a/label_studio/core/settings/base.py b/label_studio/core/settings/base.py
index 358eedd26493..a30074e6553f 100644
--- a/label_studio/core/settings/base.py
+++ b/label_studio/core/settings/base.py
@@ -899,3 +899,7 @@ def collect_versions_dummy(**kwargs):
# Base FSM (Finite State Machine) Configuration for Label Studio
FSM_CACHE_TTL = 300 # Cache TTL in seconds (5 minutes)
+
+# Used for async migrations. In LSE this is set to a real queue name, including here so we
+# can use settings.SERVICE_QUEUE_NAME in async migrations in LSO
+SERVICE_QUEUE_NAME = ''
diff --git a/label_studio/data_manager/migrations/0017_update_agreement_selected_to_nested_structure.py b/label_studio/data_manager/migrations/0017_update_agreement_selected_to_nested_structure.py
new file mode 100644
index 000000000000..3a6aa5eff53e
--- /dev/null
+++ b/label_studio/data_manager/migrations/0017_update_agreement_selected_to_nested_structure.py
@@ -0,0 +1,103 @@
+from django.db import migrations, connection
+from copy import deepcopy
+from django.apps import apps as django_apps
+from django.conf import settings
+from core.models import AsyncMigrationStatus
+from core.redis import start_job_async_or_sync
+from core.utils.iterators import iterate_queryset
+import logging
+
+migration_name = '0017_update_agreement_selected_to_nested_structure'
+
+logger = logging.getLogger(__name__)
+
+
+def forward_migration():
+ """
+ Migrates views that have agreement_selected populated to the new structure
+
+ Old structure:
+ 'agreement_selected': {
+ 'annotators': List[int]
+ 'models': List[str]
+ 'ground_truth': bool
+ }
+
+ New structure:
+ 'agreement_selected': {
+ 'annotators': {
+ 'all': bool
+ 'ids': List[int]
+ },
+ 'models': {
+ 'all': bool
+ 'ids': List[str]
+ },
+ 'ground_truth': bool
+ }
+ """
+ migration, created = AsyncMigrationStatus.objects.get_or_create(
+ name=migration_name,
+ defaults={'status': AsyncMigrationStatus.STATUS_STARTED}
+ )
+ if not created:
+ return # already in progress or done
+
+ # Look up models at runtime inside the worker process
+ View = django_apps.get_model('data_manager', 'View')
+
+ # Iterate using values() to avoid loading full model instances
+ # Fetch only the fields we need, filtering to views that have 'agreement_selected' in data
+ qs = (
+ View.objects
+ .filter(data__has_key='agreement_selected')
+ .filter(data__agreement_selected__isnull=False)
+ .values('id', 'data')
+ )
+
+ updated = 0
+ for row in qs:
+ view_id = row['id']
+ data = row.get('data') or {}
+
+ new_data = deepcopy(data)
+ # Always use the new nested structure
+ new_data['agreement_selected'] = {
+ 'annotators': {'all': True, 'ids': []},
+ 'models': {'all': True, 'ids': []},
+ 'ground_truth': False
+ }
+
+ # Update only the JSON field via update(); do not load model instance or call save()
+ View.objects.filter(id=view_id).update(data=new_data)
+ logger.info(f'Updated View {view_id} agreement selected to default all annotators + all models')
+ updated += 1
+
+ if updated:
+ logger.info(f'{migration_name} Updated {updated} View rows')
+
+ migration.status = AsyncMigrationStatus.STATUS_FINISHED
+ migration.save(update_fields=['status'])
+
+def forwards(apps, schema_editor):
+ start_job_async_or_sync(forward_migration, queue_name=settings.SERVICE_QUEUE_NAME)
+
+
+def backwards(apps, schema_editor):
+ # Irreversible: we cannot reconstruct the previous annotator lists safely
+ pass
+
+
+class Migration(migrations.Migration):
+ atomic = False
+
+ dependencies = [
+ ('data_manager', '0016_migrate_agreement_selected_annotators_to_unique')
+ ]
+
+ operations = [
+ migrations.RunPython(forwards, backwards),
+ ]
+
+
+
diff --git a/web/libs/datamanager/src/components/CellViews/AgreementSelected.jsx b/web/libs/datamanager/src/components/CellViews/AgreementSelected.jsx
index 46cc8f2fdd89..3beb08426cf4 100644
--- a/web/libs/datamanager/src/components/CellViews/AgreementSelected.jsx
+++ b/web/libs/datamanager/src/components/CellViews/AgreementSelected.jsx
@@ -34,13 +34,17 @@ export const AgreementSelected = (cell) => {
AgreementSelected.userSelectable = false;
-AgreementSelected.HeaderCell = ({ agreementFilters, onSave }) => {
+AgreementSelected.HeaderCell = ({ agreementFilters, onSave, onClose }) => {
const sdk = useSDK();
const [content, setContent] = useState(null);
useEffect(() => {
- sdk.invoke("AgreementSelectedHeaderClick", { agreementFilters, onSave }, (jsx) => setContent(jsx));
+ sdk.invoke("AgreementSelectedHeaderClick", { agreementFilters, onSave, onClose }, (jsx) => setContent(jsx));
}, []);
return content;
};
+
+AgreementSelected.style = {
+ minWidth: 210,
+};
diff --git a/web/libs/datamanager/src/components/Common/Table/TableHead/TableHead.jsx b/web/libs/datamanager/src/components/Common/Table/TableHead/TableHead.jsx
index f1d2cc71d330..da01b2b92e95 100644
--- a/web/libs/datamanager/src/components/Common/Table/TableHead/TableHead.jsx
+++ b/web/libs/datamanager/src/components/Common/Table/TableHead/TableHead.jsx
@@ -73,8 +73,14 @@ const AgreementSelectedWrapper = observer(({ column, children }) => {
const selectedView = root.viewsStore.selected;
const agreementFilters = selectedView.agreement_selected;
const [isOpen, setIsOpen] = useState(false);
+ const ref = useRef(null);
+ const closeHandler = () => {
+ ref.current?.close();
+ setIsOpen(false);
+ };
const onSave = (agreementFilters) => {
selectedView.setAgreementFilters(agreementFilters);
+ closeHandler();
return selectedView.save();
};
const onToggle = (isOpen) => {
@@ -82,9 +88,15 @@ const AgreementSelectedWrapper = observer(({ column, children }) => {
};
return (
+
) : (
<>>
)
diff --git a/web/libs/datamanager/src/stores/AppStore.js b/web/libs/datamanager/src/stores/AppStore.js
index dc73fc478224..86e390ed7f0a 100644
--- a/web/libs/datamanager/src/stores/AppStore.js
+++ b/web/libs/datamanager/src/stores/AppStore.js
@@ -711,6 +711,8 @@ export const AppStore = types
invokeAction: flow(function* (actionId, options = {}) {
const view = self.currentView ?? {};
+ const viewReloaded = view;
+ let projectFetched = self.project;
const needsLock = self.availableActions.findIndex((a) => a.id === actionId) >= 0;
@@ -749,7 +751,13 @@ export const AppStore = types
}
if (actionCallback instanceof Function) {
- return actionCallback(actionParams, view);
+ const result = yield actionCallback(actionParams, view);
+ self.SDK.invoke("actionDialogOkComplete", actionId, {
+ result,
+ view: viewReloaded,
+ project: projectFetched,
+ });
+ return result;
}
const requestParams = {
@@ -774,17 +782,28 @@ export const AppStore = types
if (result.reload) {
self.SDK.reload();
+ self.SDK.invoke("actionDialogOkComplete", actionId, {
+ result,
+ view: viewReloaded,
+ project: projectFetched,
+ });
return;
}
if (options.reload !== false) {
yield view.reload();
- self.fetchProject();
+ yield self.fetchProject();
+ projectFetched = self.project;
view.clearSelection();
}
view?.unlock?.();
+ self.SDK.invoke("actionDialogOkComplete", actionId, {
+ result,
+ view: viewReloaded,
+ project: projectFetched,
+ });
return result;
}),
diff --git a/web/libs/datamanager/src/stores/Tabs/tab.js b/web/libs/datamanager/src/stores/Tabs/tab.js
index fc3a23d0bd3c..036a1aacb05f 100644
--- a/web/libs/datamanager/src/stores/Tabs/tab.js
+++ b/web/libs/datamanager/src/stores/Tabs/tab.js
@@ -422,11 +422,21 @@ export const Tab = types
self.save();
},
- setAgreementFilters({ ground_truth = false, annotators = [], models = [] }) {
+ setAgreementFilters({
+ ground_truth = false,
+ annotators = { all: true, ids: [] },
+ models = { all: true, ids: [] },
+ }) {
self.agreement_selected = {
ground_truth,
- annotators,
- models,
+ annotators: {
+ all: annotators.all,
+ ids: annotators.ids,
+ },
+ models: {
+ all: models.all,
+ ids: models.ids,
+ },
};
},