Initial release: DictIA v0.8.14-alpha (fork de Speakr, AGPL-3.0)
This commit is contained in:
475
static/js/modules/composables/bulk-operations.js
Normal file
475
static/js/modules/composables/bulk-operations.js
Normal file
@@ -0,0 +1,475 @@
|
||||
/**
|
||||
* Bulk Operations Composable
|
||||
* Handles bulk API operations for multiple recordings
|
||||
*/
|
||||
|
||||
const { ref, computed } = Vue;
|
||||
|
||||
export function useBulkOperations({
|
||||
selectedRecordingIds,
|
||||
selectedRecordings,
|
||||
recordings,
|
||||
selectedRecording,
|
||||
bulkActionInProgress,
|
||||
availableTags,
|
||||
availableFolders,
|
||||
showToast,
|
||||
setGlobalError,
|
||||
startReprocessingPoll
|
||||
}) {
|
||||
// Modal state
|
||||
const showBulkDeleteModal = ref(false);
|
||||
const showBulkTagModal = ref(false);
|
||||
const showBulkReprocessModal = ref(false);
|
||||
const showBulkFolderModal = ref(false);
|
||||
const bulkTagAction = ref('add'); // 'add' or 'remove'
|
||||
const bulkTagSelectedId = ref('');
|
||||
const bulkReprocessType = ref('summary'); // 'transcription' or 'summary'
|
||||
|
||||
// Get CSRF token
|
||||
const getCsrfToken = () => {
|
||||
return document.querySelector('meta[name="csrf-token"]')?.getAttribute('content');
|
||||
};
|
||||
|
||||
// Helper to get selected IDs as array
|
||||
const getSelectedIds = () => {
|
||||
return Array.from(selectedRecordingIds.value);
|
||||
};
|
||||
|
||||
// =========================================
|
||||
// Bulk Delete
|
||||
// =========================================
|
||||
|
||||
const openBulkDeleteModal = () => {
|
||||
showBulkDeleteModal.value = true;
|
||||
};
|
||||
|
||||
const closeBulkDeleteModal = () => {
|
||||
showBulkDeleteModal.value = false;
|
||||
};
|
||||
|
||||
const executeBulkDelete = async () => {
|
||||
const ids = getSelectedIds();
|
||||
if (ids.length === 0) return;
|
||||
|
||||
bulkActionInProgress.value = true;
|
||||
closeBulkDeleteModal();
|
||||
|
||||
try {
|
||||
const response = await fetch('/api/recordings/bulk', {
|
||||
method: 'DELETE',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'X-CSRFToken': getCsrfToken()
|
||||
},
|
||||
body: JSON.stringify({ recording_ids: ids })
|
||||
});
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(data.error || 'Failed to delete recordings');
|
||||
}
|
||||
|
||||
// Remove deleted recordings from local state
|
||||
const deletedIds = new Set(data.deleted_ids || ids);
|
||||
recordings.value = recordings.value.filter(r => !deletedIds.has(r.id));
|
||||
|
||||
// Clear selected recording if it was deleted
|
||||
if (selectedRecording.value && deletedIds.has(selectedRecording.value.id)) {
|
||||
selectedRecording.value = null;
|
||||
}
|
||||
|
||||
// Remove deleted IDs from selection
|
||||
deletedIds.forEach(id => selectedRecordingIds.value.delete(id));
|
||||
|
||||
const count = deletedIds.size;
|
||||
showToast(`${count} recording${count !== 1 ? 's' : ''} deleted`, 'fa-trash', 3000, 'success');
|
||||
} catch (error) {
|
||||
console.error('Bulk delete error:', error);
|
||||
setGlobalError(`Failed to delete recordings: ${error.message}`);
|
||||
} finally {
|
||||
bulkActionInProgress.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
// =========================================
|
||||
// Bulk Tag Operations
|
||||
// =========================================
|
||||
|
||||
const openBulkTagModal = (action = 'add') => {
|
||||
bulkTagAction.value = action;
|
||||
bulkTagSelectedId.value = '';
|
||||
showBulkTagModal.value = true;
|
||||
};
|
||||
|
||||
const closeBulkTagModal = () => {
|
||||
showBulkTagModal.value = false;
|
||||
bulkTagSelectedId.value = '';
|
||||
};
|
||||
|
||||
const executeBulkTag = async () => {
|
||||
const ids = getSelectedIds();
|
||||
const tagId = bulkTagSelectedId.value;
|
||||
const action = bulkTagAction.value;
|
||||
|
||||
// Validate before making API call
|
||||
if (ids.length === 0) {
|
||||
console.warn('No recordings selected for bulk tag operation');
|
||||
return;
|
||||
}
|
||||
if (!tagId && tagId !== 0) {
|
||||
console.warn('No tag selected for bulk tag operation');
|
||||
return;
|
||||
}
|
||||
|
||||
bulkActionInProgress.value = true;
|
||||
closeBulkTagModal();
|
||||
|
||||
try {
|
||||
const response = await fetch('/api/recordings/bulk-tags', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'X-CSRFToken': getCsrfToken()
|
||||
},
|
||||
body: JSON.stringify({
|
||||
recording_ids: ids,
|
||||
tag_id: parseInt(tagId),
|
||||
action: action
|
||||
})
|
||||
});
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(data.error || `Failed to ${action} tag`);
|
||||
}
|
||||
|
||||
// Update local state
|
||||
const tag = availableTags.value.find(t => t.id == tagId);
|
||||
if (tag) {
|
||||
const affectedIds = new Set(data.affected_ids || ids);
|
||||
recordings.value.forEach(recording => {
|
||||
if (affectedIds.has(recording.id)) {
|
||||
if (!recording.tags) recording.tags = [];
|
||||
|
||||
if (action === 'add') {
|
||||
// Add tag if not already present
|
||||
if (!recording.tags.find(t => t.id === tag.id)) {
|
||||
recording.tags.push(tag);
|
||||
}
|
||||
} else {
|
||||
// Remove tag
|
||||
recording.tags = recording.tags.filter(t => t.id !== tag.id);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Update selected recording if affected
|
||||
if (selectedRecording.value && affectedIds.has(selectedRecording.value.id)) {
|
||||
if (!selectedRecording.value.tags) selectedRecording.value.tags = [];
|
||||
|
||||
if (action === 'add') {
|
||||
if (!selectedRecording.value.tags.find(t => t.id === tag.id)) {
|
||||
selectedRecording.value.tags.push(tag);
|
||||
}
|
||||
} else {
|
||||
selectedRecording.value.tags = selectedRecording.value.tags.filter(t => t.id !== tag.id);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const count = data.affected_ids?.length || ids.length;
|
||||
const actionText = action === 'add' ? 'added to' : 'removed from';
|
||||
showToast(`Tag ${actionText} ${count} recording${count !== 1 ? 's' : ''}`, 'fa-tags', 3000, 'success');
|
||||
} catch (error) {
|
||||
console.error('Bulk tag error:', error);
|
||||
setGlobalError(`Failed to ${action} tag: ${error.message}`);
|
||||
} finally {
|
||||
bulkActionInProgress.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
// =========================================
|
||||
// Bulk Reprocess
|
||||
// =========================================
|
||||
|
||||
const openBulkReprocessModal = () => {
|
||||
bulkReprocessType.value = 'summary';
|
||||
showBulkReprocessModal.value = true;
|
||||
};
|
||||
|
||||
const closeBulkReprocessModal = () => {
|
||||
showBulkReprocessModal.value = false;
|
||||
};
|
||||
|
||||
const executeBulkReprocess = async () => {
|
||||
const ids = getSelectedIds();
|
||||
if (ids.length === 0) return;
|
||||
|
||||
bulkActionInProgress.value = true;
|
||||
closeBulkReprocessModal();
|
||||
|
||||
try {
|
||||
const response = await fetch('/api/recordings/bulk-reprocess', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'X-CSRFToken': getCsrfToken()
|
||||
},
|
||||
body: JSON.stringify({
|
||||
recording_ids: ids,
|
||||
type: bulkReprocessType.value
|
||||
})
|
||||
});
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(data.error || 'Failed to queue reprocessing');
|
||||
}
|
||||
|
||||
// Update status for queued recordings
|
||||
const queuedIds = new Set(data.queued_ids || ids);
|
||||
const newStatus = bulkReprocessType.value === 'transcription' ? 'PROCESSING' : 'SUMMARIZING';
|
||||
|
||||
recordings.value.forEach(recording => {
|
||||
if (queuedIds.has(recording.id)) {
|
||||
recording.status = newStatus;
|
||||
// Start polling for each
|
||||
if (startReprocessingPoll) {
|
||||
startReprocessingPoll(recording.id);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
if (selectedRecording.value && queuedIds.has(selectedRecording.value.id)) {
|
||||
selectedRecording.value.status = newStatus;
|
||||
}
|
||||
|
||||
const count = queuedIds.size;
|
||||
const typeText = bulkReprocessType.value === 'transcription' ? 'Transcription' : 'Summary';
|
||||
showToast(`${typeText} reprocessing queued for ${count} recording${count !== 1 ? 's' : ''}`, 'fa-sync-alt', 3000, 'success');
|
||||
} catch (error) {
|
||||
console.error('Bulk reprocess error:', error);
|
||||
setGlobalError(`Failed to queue reprocessing: ${error.message}`);
|
||||
} finally {
|
||||
bulkActionInProgress.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
// =========================================
|
||||
// Bulk Toggle (Inbox/Highlight)
|
||||
// =========================================
|
||||
|
||||
const bulkToggleInbox = async (value = null) => {
|
||||
const ids = getSelectedIds();
|
||||
if (ids.length === 0) return;
|
||||
|
||||
// If no value specified, toggle based on majority
|
||||
if (value === null) {
|
||||
const inboxCount = selectedRecordings.value.filter(r => r.is_inbox).length;
|
||||
value = inboxCount < ids.length / 2;
|
||||
}
|
||||
|
||||
bulkActionInProgress.value = true;
|
||||
|
||||
try {
|
||||
const response = await fetch('/api/recordings/bulk-toggle', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'X-CSRFToken': getCsrfToken()
|
||||
},
|
||||
body: JSON.stringify({
|
||||
recording_ids: ids,
|
||||
field: 'inbox',
|
||||
value: value
|
||||
})
|
||||
});
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(data.error || 'Failed to update inbox status');
|
||||
}
|
||||
|
||||
// Update local state
|
||||
const affectedIds = new Set(data.affected_ids || ids);
|
||||
recordings.value.forEach(recording => {
|
||||
if (affectedIds.has(recording.id)) {
|
||||
recording.is_inbox = value;
|
||||
}
|
||||
});
|
||||
|
||||
if (selectedRecording.value && affectedIds.has(selectedRecording.value.id)) {
|
||||
selectedRecording.value.is_inbox = value;
|
||||
}
|
||||
|
||||
const count = affectedIds.size;
|
||||
const actionText = value ? 'added to' : 'removed from';
|
||||
showToast(`${count} recording${count !== 1 ? 's' : ''} ${actionText} inbox`, 'fa-inbox', 3000, 'success');
|
||||
} catch (error) {
|
||||
console.error('Bulk toggle inbox error:', error);
|
||||
setGlobalError(`Failed to update inbox status: ${error.message}`);
|
||||
} finally {
|
||||
bulkActionInProgress.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
const bulkToggleHighlight = async (value = null) => {
|
||||
const ids = getSelectedIds();
|
||||
if (ids.length === 0) return;
|
||||
|
||||
// If no value specified, toggle based on majority
|
||||
if (value === null) {
|
||||
const highlightCount = selectedRecordings.value.filter(r => r.is_highlighted).length;
|
||||
value = highlightCount < ids.length / 2;
|
||||
}
|
||||
|
||||
bulkActionInProgress.value = true;
|
||||
|
||||
try {
|
||||
const response = await fetch('/api/recordings/bulk-toggle', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'X-CSRFToken': getCsrfToken()
|
||||
},
|
||||
body: JSON.stringify({
|
||||
recording_ids: ids,
|
||||
field: 'highlight',
|
||||
value: value
|
||||
})
|
||||
});
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(data.error || 'Failed to update highlight status');
|
||||
}
|
||||
|
||||
// Update local state
|
||||
const affectedIds = new Set(data.affected_ids || ids);
|
||||
recordings.value.forEach(recording => {
|
||||
if (affectedIds.has(recording.id)) {
|
||||
recording.is_highlighted = value;
|
||||
}
|
||||
});
|
||||
|
||||
if (selectedRecording.value && affectedIds.has(selectedRecording.value.id)) {
|
||||
selectedRecording.value.is_highlighted = value;
|
||||
}
|
||||
|
||||
const count = affectedIds.size;
|
||||
const actionText = value ? 'highlighted' : 'unhighlighted';
|
||||
showToast(`${count} recording${count !== 1 ? 's' : ''} ${actionText}`, 'fa-star', 3000, 'success');
|
||||
} catch (error) {
|
||||
console.error('Bulk toggle highlight error:', error);
|
||||
setGlobalError(`Failed to update highlight status: ${error.message}`);
|
||||
} finally {
|
||||
bulkActionInProgress.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
// =========================================
|
||||
// Bulk Folder Assignment
|
||||
// =========================================
|
||||
|
||||
const bulkAssignFolder = async (folderId) => {
|
||||
const ids = getSelectedIds();
|
||||
if (ids.length === 0) return;
|
||||
|
||||
bulkActionInProgress.value = true;
|
||||
showBulkFolderModal.value = false;
|
||||
|
||||
try {
|
||||
const response = await fetch('/api/recordings/bulk/folder', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'X-CSRFToken': getCsrfToken()
|
||||
},
|
||||
body: JSON.stringify({
|
||||
recording_ids: ids,
|
||||
folder_id: folderId
|
||||
})
|
||||
});
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(data.error || 'Failed to update folders');
|
||||
}
|
||||
|
||||
// Update local state
|
||||
const folder = folderId ? availableFolders.value.find(f => f.id === folderId) : null;
|
||||
recordings.value.forEach(recording => {
|
||||
if (ids.includes(recording.id)) {
|
||||
recording.folder_id = folderId;
|
||||
recording.folder = folder;
|
||||
}
|
||||
});
|
||||
|
||||
// Update selected recording if affected
|
||||
if (selectedRecording.value && ids.includes(selectedRecording.value.id)) {
|
||||
selectedRecording.value.folder_id = folderId;
|
||||
selectedRecording.value.folder = folder;
|
||||
}
|
||||
|
||||
// Update folder recording counts
|
||||
if (availableFolders.value) {
|
||||
availableFolders.value.forEach(f => {
|
||||
const count = recordings.value.filter(r => r.folder_id === f.id).length;
|
||||
f.recording_count = count;
|
||||
});
|
||||
}
|
||||
|
||||
const count = data.updated_count || ids.length;
|
||||
if (folderId) {
|
||||
showToast(`${count} recording${count !== 1 ? 's' : ''} moved to "${folder?.name || 'folder'}"`, 'fa-folder', 3000, 'success');
|
||||
} else {
|
||||
showToast(`${count} recording${count !== 1 ? 's' : ''} removed from folder`, 'fa-folder-minus', 3000, 'success');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Bulk folder assignment error:', error);
|
||||
setGlobalError(`Failed to update folders: ${error.message}`);
|
||||
} finally {
|
||||
bulkActionInProgress.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
return {
|
||||
// Modal state
|
||||
showBulkDeleteModal,
|
||||
showBulkTagModal,
|
||||
showBulkReprocessModal,
|
||||
showBulkFolderModal,
|
||||
bulkTagAction,
|
||||
bulkTagSelectedId,
|
||||
bulkReprocessType,
|
||||
|
||||
// Bulk Delete
|
||||
openBulkDeleteModal,
|
||||
closeBulkDeleteModal,
|
||||
executeBulkDelete,
|
||||
|
||||
// Bulk Tag
|
||||
openBulkTagModal,
|
||||
closeBulkTagModal,
|
||||
executeBulkTag,
|
||||
|
||||
// Bulk Reprocess
|
||||
openBulkReprocessModal,
|
||||
closeBulkReprocessModal,
|
||||
executeBulkReprocess,
|
||||
|
||||
// Bulk Toggle
|
||||
bulkToggleInbox,
|
||||
bulkToggleHighlight,
|
||||
|
||||
// Bulk Folder
|
||||
bulkAssignFolder
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user