diff --git a/label_studio/core/settings/base.py b/label_studio/core/settings/base.py
index 98666a20988d..effefd68a400 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/components/Filters/types/Number.jsx b/web/libs/datamanager/src/components/Filters/types/Number.jsx
index 8b6fe66592d0..f54b6f543269 100644
--- a/web/libs/datamanager/src/components/Filters/types/Number.jsx
+++ b/web/libs/datamanager/src/components/Filters/types/Number.jsx
@@ -8,7 +8,8 @@ const valueFilter = (value) => {
return value;
}
if (typeof value === "string") {
- return value.replace(/([^\d.,]+)/, "");
+ const cleaned = value.replace(/([^\d.,]+)/, "");
+ return cleaned ? Number(cleaned) : null;
}
return value || null;
}
@@ -25,7 +26,6 @@ const RangeInput = observer(({ schema, value, onChange }) => {
const max = value?.max ?? null;
const onValueChange = (newValue) => {
- console.log({ newValue });
onChange(newValue);
};
diff --git a/web/libs/datamanager/src/stores/AppStore.js b/web/libs/datamanager/src/stores/AppStore.js
index 918e92c90435..5f6c92ba2dcb 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,
+ },
};
},
diff --git a/web/libs/ui/src/lib/toast/toast.module.scss b/web/libs/ui/src/lib/toast/toast.module.scss
index 5dda3d6a3e2e..594dc80a8448 100644
--- a/web/libs/ui/src/lib/toast/toast.module.scss
+++ b/web/libs/ui/src/lib/toast/toast.module.scss
@@ -89,9 +89,9 @@
.toast {
--text-color: var(--color-neutral-inverted-content);
--background-color: var(--color-neutral-inverted-surface);
- --border-color: var(--color-neutral-inverted-border);
+ --border-color: var(--color-surface-border);
--hover-color: var(--color-neutral-inverted-surface-hover);
- --padding: var(--toast-spacing) calc(var(--toast-spacing) * 2);
+ --padding: var(--toast-spacing);
display: flex;
align-items: center;
@@ -160,7 +160,7 @@
// Always maintain a dark background for info toasts
--text-color: var(--color-sand-100);
--background-color: var(--color-sand-900);
- --border-color: var(--color-neutral-border);
+ --border-color: var(--color-sand-700);
--hover-color: var(--color-sand-800);
}
diff --git a/web/libs/ui/src/lib/toast/toast.tsx b/web/libs/ui/src/lib/toast/toast.tsx
index cf7bcef6fa90..9758728bd37e 100644
--- a/web/libs/ui/src/lib/toast/toast.tsx
+++ b/web/libs/ui/src/lib/toast/toast.tsx
@@ -3,7 +3,6 @@ import * as ToastPrimitive from "@radix-ui/react-toast";
import styles from "./toast.module.scss";
import clsx from "clsx";
import { IconCross } from "../../assets/icons";
-import { Button } from "../button/button";
import { cn } from "@humansignal/shad/utils";
export type ToastViewportProps = ToastPrimitive.ToastViewportProps & any;
@@ -87,15 +86,9 @@ export interface ToastActionProps extends ToastPrimitive.ToastActionProps {
}
export const ToastAction: FC = ({ children, onClose, altText, ...props }) => (
-
);
export type ToastShowArgs = {