Skip to content
Open
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
11 changes: 11 additions & 0 deletions gateway/sds_gateway/static/css/file-list.css
Original file line number Diff line number Diff line change
Expand Up @@ -574,3 +574,14 @@ body {
.progress-bar-width-0 {
width: 0%;
}

/* Selection column: hidden by default, shown when selection mode is active */
#captures-table .capture-select-column {
display: none;
width: 2.5rem;
vertical-align: middle;
}

#captures-table.selection-mode-active .capture-select-column {
display: table-cell;
}
288 changes: 288 additions & 0 deletions gateway/sds_gateway/static/js/actions/QuickAddToDatasetManager.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,288 @@
/**
* Quick Add to Dataset Manager
* Handles opening the quick-add modal, loading datasets, and adding a capture to a dataset.
*/
class QuickAddToDatasetManager {
constructor() {
this.modalEl = document.getElementById("quickAddToDatasetModal");
this.currentCaptureUuid = null;
this.currentCaptureName = null;
/** @type {string[]|null} When set, call quick-add API once per UUID (e.g. from file list "Add" button) */
this.currentCaptureUuids = null;
if (!this.modalEl) return;
this.quickAddUrl = this.modalEl.getAttribute("data-quick-add-url");
this.datasetsUrl = this.modalEl.getAttribute("data-datasets-url");
this.selectEl = document.getElementById("quick-add-dataset-select");
this.confirmBtn = document.getElementById("quick-add-confirm-btn");
this.messageEl = document.getElementById("quick-add-message");
this.captureNameEl = document.getElementById("quick-add-capture-name");
this.initializeEventListeners();
}

initializeEventListeners() {
// Delegate click on "Add to dataset" buttons (e.g. in table dropdown)
document.addEventListener("click", (e) => {
const btn = e.target.closest(".add-to-dataset-btn");
if (!btn) return;
e.preventDefault();
e.stopPropagation();
this.currentCaptureUuid = btn.getAttribute("data-capture-uuid");
this.currentCaptureName =
btn.getAttribute("data-capture-name") || "This capture";
this.openModal();
});

Comment on lines +23 to +34
Copy link

Copilot AI Feb 23, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The event listener for .add-to-dataset-btn (lines 24-33) appears to be unused code, as no elements with this class exist in the templates included in this PR. This suggests either incomplete implementation or planned future functionality. If this is intended for single-capture quick-add from dropdown menus, the corresponding HTML buttons should be added. Otherwise, remove this code to avoid confusion.

Suggested change
// Delegate click on "Add to dataset" buttons (e.g. in table dropdown)
document.addEventListener("click", (e) => {
const btn = e.target.closest(".add-to-dataset-btn");
if (!btn) return;
e.preventDefault();
e.stopPropagation();
this.currentCaptureUuid = btn.getAttribute("data-capture-uuid");
this.currentCaptureName =
btn.getAttribute("data-capture-name") || "This capture";
this.openModal();
});

Copilot uses AI. Check for mistakes.
if (!this.modalEl) return;

// When modal is shown, load datasets and apply state (single vs multi from file list)
this.modalEl.addEventListener("show.bs.modal", () => {
this.resetMessage();
const rawIds = this.modalEl.dataset.captureUuids;
if (rawIds) {
try {
this.currentCaptureUuids = JSON.parse(rawIds);
this.currentCaptureUuid = null;
this.currentCaptureName = null;
const n = this.currentCaptureUuids.length;
if (this.captureNameEl) {
this.captureNameEl.textContent =
n === 1 ? "1 capture" : `${n} captures`;
}
delete this.modalEl.dataset.captureUuids;
} catch (_) {
this.currentCaptureUuids = null;
}
} else {
this.currentCaptureUuids = null;
if (this.captureNameEl) {
this.captureNameEl.textContent =
this.currentCaptureName || "This capture";
}
}
this.loadDatasets();
});

// When dataset select changes, enable/disable Add button
if (this.selectEl) {
this.selectEl.addEventListener("change", () => {
if (this.confirmBtn) {
this.confirmBtn.disabled = !this.selectEl.value;
}
});
}

// Add button click
if (this.confirmBtn) {
this.confirmBtn.addEventListener("click", () => this.handleAdd());
}
}

openModal() {
if (!this.modalEl) return;
const Modal = window.bootstrap?.Modal;
if (Modal) {
const modal = Modal.getOrCreateInstance(this.modalEl);
modal.show();
}
}

resetMessage() {
if (this.messageEl) {
this.messageEl.classList.add("d-none");
this.messageEl.classList.remove(
"alert-success",
"alert-danger",
"alert-warning",
);
this.messageEl.textContent = "";
}
if (this.confirmBtn) {
this.confirmBtn.disabled = true;
}
if (this.selectEl) {
this.selectEl.innerHTML = '<option value="">Loading...</option>';
}
}

showMessage(text, type) {
if (!this.messageEl) return;
this.messageEl.textContent = text;
this.messageEl.classList.remove(
"d-none",
"alert-success",
"alert-danger",
"alert-warning",
);
this.messageEl.classList.add(`alert-${type}`);
}

async loadDatasets() {
if (!this.selectEl || !this.datasetsUrl) return;
this.selectEl.innerHTML = '<option value="">Loading...</option>';
if (this.confirmBtn) this.confirmBtn.disabled = true;
try {
const response = await window.APIClient.get(this.datasetsUrl);
const datasets = response.datasets || [];
this.selectEl.innerHTML = '<option value="">Select dataset...</option>';
for (const d of datasets) {
const opt = document.createElement("option");
opt.value = d.uuid;
opt.textContent = d.name;
this.selectEl.appendChild(opt);
}
if (datasets.length === 0) {
this.showMessage(
"You have no datasets you can add captures to.",
"warning",
);
}
} catch (err) {
this.selectEl.innerHTML = '<option value="">Failed to load</option>';
const reason = err?.data?.error || err?.message || "Try again.";
this.showMessage(`Failed to load datasets. ${reason}`, "danger");
}
}

async handleAdd() {
const datasetUuid = this.selectEl?.value;
if (!datasetUuid) return;
const isMulti =
Array.isArray(this.currentCaptureUuids) &&
this.currentCaptureUuids.length > 0;
const isSingle = this.currentCaptureUuid && this.quickAddUrl;
if (!isMulti && !isSingle) return;
if (this.confirmBtn) this.confirmBtn.disabled = true;
this.resetMessage();
if (isMulti) {
await this.handleMultiAdd(datasetUuid);
} else {
await this.handleSingleAdd(datasetUuid);
}
Comment on lines +146 to +160
Copy link

Copilot AI Feb 23, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When multi-add mode is triggered with an empty capture list (currentCaptureUuids is an empty array), the code proceeds silently without providing feedback. The handleAdd function should validate that currentCaptureUuids contains at least one capture and show an appropriate error message if empty, similar to how isSingle and isMulti are checked.

Copilot uses AI. Check for mistakes.
}

/**
* Build a concise summary from quick-add counts (added, skipped, failed count).
* API returns detailed JSON; we show one short line.
* Failed = request threw (non-2xx HTTP or network) or response.success false or per-capture errors in 200 body.
*/
formatQuickAddSummary(added, skipped, failedCount, firstErrorMessage) {
const parts = [];
if (added > 0) parts.push(`${added} added`);
if (skipped > 0) parts.push(`${skipped} already in dataset`);
if (failedCount > 0) {
parts.push(`${failedCount} failed`);
if (firstErrorMessage != null) {
const text =
typeof firstErrorMessage === "object"
? (firstErrorMessage.message ??
firstErrorMessage.detail ??
String(firstErrorMessage))
: String(firstErrorMessage);
Comment on lines +175 to +180
Copy link

Copilot AI Feb 23, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The formatQuickAddSummary function treats the first error message as either a string or an object, but the errors array from the API contains strings (per line 1827: errors.append(f"{c.uuid}: {e}")). The complex object handling (firstErrorMessage.message, firstErrorMessage.detail) is unnecessary and may never execute. Either simplify the code to expect strings only, or document why object error messages might occur.

Suggested change
const text =
typeof firstErrorMessage === "object"
? (firstErrorMessage.message ??
firstErrorMessage.detail ??
String(firstErrorMessage))
: String(firstErrorMessage);
const text = String(firstErrorMessage);

Copilot uses AI. Check for mistakes.
if (text) parts.push(`: ${text}`);
}
}
return parts.length ? `${parts.join(", ")}.` : "Done.";
}

/**
* Call quick-add API once per selected capture UUID (loop). Backend handles
* multi-channel grouping per UUID. We aggregate counts and show one concise message.
*/
async handleMultiAdd(datasetUuid) {
if (!this.quickAddUrl) {
this.showMessage("Quick-add URL not configured.", "danger");
if (this.confirmBtn) this.confirmBtn.disabled = false;
return;
}
let totalAdded = 0;
let totalSkipped = 0;
const errorMessages = [];
for (const captureUuid of this.currentCaptureUuids) {
try {
const response = await window.APIClient.post(
this.quickAddUrl,
{
dataset_uuid: datasetUuid,
capture_uuid: captureUuid,
},
null,
true,
);
if (response.success) {
totalAdded += response.added?.length ?? 0;
totalSkipped += response.skipped?.length ?? 0;
if (response.errors?.length) {
errorMessages.push(...(response.errors || []));
}
} else {
errorMessages.push(response.error || "Request failed");
}
} catch (err) {
// APIClient throws on non-2xx (and on network errors), so failed = exception or 4xx/5xx
errorMessages.push(err?.data?.error || err?.message || String(err));
}
}
// failed count = requests that threw (non-200 or network) + response.success false + per-capture errors in 200 response
const errorCount = errorMessages.length;
const hasErrors = errorCount > 0;
const hasSuccess = totalAdded > 0 || totalSkipped > 0;
const msg = this.formatQuickAddSummary(
totalAdded,
totalSkipped,
errorCount,
errorMessages[0],
);
this.showMessage(msg, hasErrors ? "warning" : "success");
if (window.showAlert)
window.showAlert(msg, hasErrors ? "warning" : "success");
if (hasSuccess || !hasErrors) {
setTimeout(() => {
window.bootstrap?.Modal?.getInstance(this.modalEl)?.hide();
}, 1500);
} else if (this.confirmBtn) {
this.confirmBtn.disabled = false;
}
Comment on lines +235 to +244
Copy link

Copilot AI Feb 23, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

After successfully adding captures to a dataset, the modal closes but selection mode remains active with checkboxes still checked. The modal should exit selection mode after a successful add operation by calling exitSelectionMode (or exposing it globally so the manager can access it) to provide a clean user experience.

Copilot uses AI. Check for mistakes.
}

async handleSingleAdd(datasetUuid) {
try {
const response = await window.APIClient.post(
this.quickAddUrl,
{
dataset_uuid: datasetUuid,
capture_uuid: this.currentCaptureUuid,
},
null,
true,
);
if (response.success) {
const added = response.added?.length ?? 0;
const skipped = response.skipped?.length ?? 0;
const errorCount = response.errors?.length ?? 0;
const firstError = response.errors?.[0];
const msg = this.formatQuickAddSummary(
added,
skipped,
errorCount,
firstError,
);
this.showMessage(msg, errorCount > 0 ? "warning" : "success");
if (window.showAlert)
window.showAlert(msg, errorCount > 0 ? "warning" : "success");
setTimeout(() => {
window.bootstrap?.Modal?.getInstance(this.modalEl)?.hide();
}, 1500);
} else {
this.showMessage(response.error || "Request failed.", "danger");
if (this.confirmBtn) this.confirmBtn.disabled = false;
}
} catch (err) {
const msg =
err?.data?.error || err?.message || "Failed to add capture to dataset.";
this.showMessage(msg, "danger");
if (this.confirmBtn) this.confirmBtn.disabled = false;
}
}
}

window.QuickAddToDatasetManager = QuickAddToDatasetManager;
7 changes: 6 additions & 1 deletion gateway/sds_gateway/static/js/components.js
Original file line number Diff line number Diff line change
Expand Up @@ -270,7 +270,12 @@ class TableManager {
const rows = this.tbody?.querySelectorAll('tr[data-clickable="true"]');
for (const row of rows || []) {
row.addEventListener("click", (e) => {
if (e.target.closest("button, a")) return; // Don't trigger on buttons/links
if (
e.target.closest(
"button, a, .capture-select-checkbox, .capture-select-column",
)
)
return; // Don't trigger on buttons/links/selection
this.onRowClick(row);
});
}
Expand Down
Loading