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
88 changes: 28 additions & 60 deletions package-lock.json

Large diffs are not rendered by default.

38 changes: 18 additions & 20 deletions webapp/js/composables/useDialogManager.js
Original file line number Diff line number Diff line change
Expand Up @@ -226,39 +226,37 @@ export default {
this.selectedUsingFileBrowserList = true;
},

async handleRenameClose(id, newPath, entry, measurementsInfo) {
async handleRenameClose(id, newPath, entry, sidecarsInfo) {
if (id == "rename") {
if (newPath == null || newPath.length == 0) return;

// Handle measurements rename if requested
if (measurementsInfo && measurementsInfo.renameMeasurements) {
// Handle sidecar files rename if requested
if (sidecarsInfo && sidecarsInfo.sidecars && sidecarsInfo.sidecars.length > 0) {
this.isBusy = true;
try {
// First rename the point cloud
// First rename the main file
await this.renameSelectedFile(newPath);

// Then rename the measurements file
console.log('Renaming measurements file...');
await this.dataset.moveObj(
measurementsInfo.oldMeasurementsPath,
measurementsInfo.newMeasurementsPath
);
// Then rename each sidecar file, reusing renameFile() so the
// file browser list and treeview (via emitter) are refreshed too
for (const sc of sidecarsInfo.sidecars) {
const sidecarItem = this.fileBrowserFiles.find(item => item.entry.path === sc.oldPath);
if (sidecarItem) {
await this.renameFile(sidecarItem, sc.newPath, { action: 'rename', silent: true });
} else {
await this.dataset.moveObj(sc.oldPath, sc.newPath);
}
}

console.log('Both files renamed successfully');
this.$toast.add({ severity: 'success', summary: 'Renamed', detail: 'Point cloud and measurements file renamed successfully', life: 3000 });
const count = sidecarsInfo.sidecars.length;
this.$toast.add({ severity: 'success', summary: 'Renamed', detail: `File and ${count} sidecar${count > 1 ? 's' : ''} renamed successfully`, life: 3000 });
} catch (e) {
// If measurements rename fails, show warning but not critical error
if (e.message && e.message.includes('measurements')) {
this.$toast.add({ severity: 'warn', summary: 'Partial Rename', detail: 'Point cloud renamed, but measurements file could not be renamed', life: 5000 });
console.warn('Measurements rename failed:', e);
} else {
this.showError(e, "Rename");
}
this.showError(e, "Rename");
} finally {
this.isBusy = false;
}
} else {
// Normal rename without measurements
// Normal rename without sidecars
await this.renameSelectedFile(newPath);
}
} else if (id == "renameddb") {
Expand Down
18 changes: 13 additions & 5 deletions webapp/js/composables/useFileOperations.js
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,8 @@ export default {
* result toast and whether the build pipeline is re-triggered.
* DroneDB indexes builds by file hash, so a pure move never
* changes buildability and must NOT trigger build callbacks.
* @param {boolean} [options.silent=false] - skip the result toast (used
* when the caller shows its own aggregate toast, e.g. sidecar renames).
*/
async renameFile(file, newPath, options = {}) {
const action = options.action === 'move' ? 'move' : 'rename';
Expand Down Expand Up @@ -141,14 +143,20 @@ export default {

this.sortFiles();

// Only re-trigger build pipeline on rename (extension may have changed,
// making the file buildable). On move the hash is unchanged, so any
// existing build artifacts still apply and no notification is needed.
if (action === 'rename' && BuildManager.isBuildableType(newItem.entry.type)) {
// Only notify the build pipeline when the rename's extension change made a
// previously non-buildable file buildable. Hash-based build artifacts already
// apply to files that were buildable before the rename, so no new build is
// ever queued in that case - showing the "buildable files detected" toast then
// would be misleading since nothing is actually being built.
const wasBuildable = BuildManager.isBuildableType(file.entry.type);
const isBuildable = BuildManager.isBuildableType(newItem.entry.type);
if (action === 'rename' && !wasBuildable && isBuildable) {
BuildManager.onFilesAdded(this.dataset, [newItem.entry]);
}

if (action === 'move') {
if (options.silent) {
// Caller shows its own (aggregate) toast
} else if (action === 'move') {
this.$toast.add({ severity: 'success', summary: 'Moved', detail: `File moved successfully`, life: 3000 });
} else {
this.$toast.add({ severity: 'success', summary: 'Renamed', detail: `File renamed successfully`, life: 3000 });
Expand Down
11 changes: 2 additions & 9 deletions webapp/js/composables/useTaskFormatting.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
* Pure formatting (severity/icon/date/duration) stays client-side.
*/
import reg from '@/libs/api/sharedRegistry';
import { formatTimeAgo } from '@/libs/utils';

export default {
data() {
Expand Down Expand Up @@ -109,15 +110,7 @@ export default {

getRelativeTime(dateString) {
if (!dateString) return '';
const diffMs = Date.now() - new Date(dateString).getTime();
const minutes = Math.floor(diffMs / 60000);
const hours = Math.floor(diffMs / 3600000);
const days = Math.floor(diffMs / 86400000);
if (minutes < 1) return 'Just now';
if (minutes < 60) return `${minutes} minutes ago`;
if (hours < 24) return `${hours} hours ago`;
if (days < 30) return `${days} days ago`;
return 'Over a month ago';
return formatTimeAgo(new Date(dateString).getTime());
},

// Elapsed time for a task still in flight.
Expand Down
37 changes: 36 additions & 1 deletion webapp/js/features/admin/Tasks.vue
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@
style="flex: 1; min-height: 0;"
empty-message="No tasks match the current filters."
@page="onPage" @view-log="openLog" @download-result="downloadResult"
@cancel="cancelTask" @retry="retryTask" />
@cancel="cancelTask" @retry="retryTask" @delete="deleteTask" />

<TaskLogDialog v-model:visible="logDialogOpen" :title="logTaskTitle" :log-text="logText"
:is-active="logTask && isActive(logTask.state)" @refresh="refreshLog" />
Expand All @@ -39,6 +39,16 @@
confirmText="Cancel Task" cancelText="Don't Cancel" confirmButtonClass="danger"
@onClose="handleCancelDialogClose">
</ConfirmDialog>

<!-- Delete task confirmation -->
<ConfirmDialog v-if="deleteDialogOpen"
title="Delete Task"
:message="`Permanently delete this concluded task from the history?<br/><strong>${toolTitle(deletingTask.toolId, taskTools)}</strong> will be removed together with any downloadable results it produced.`"
confirmText="Delete" cancelText="Cancel" confirmButtonClass="danger"
warningTitle="Warning"
warningMessage="This action cannot be undone. Any task results still available for download will be deleted."
@onClose="handleDeleteDialogClose">
</ConfirmDialog>
</div>
</template>

Expand Down Expand Up @@ -84,6 +94,9 @@ export default {
cancelDialogOpen: false,
cancellingTask: null,

deleteDialogOpen: false,
deletingTask: null,

_refreshTimer: null
};
},
Expand Down Expand Up @@ -221,6 +234,28 @@ export default {
}
},

deleteTask(task) {
this.deletingTask = task;
this.deleteDialogOpen = true;
},

async handleDeleteDialogClose(buttonId) {
this.deleteDialogOpen = false;
if (buttonId !== 'confirm' || !this.deletingTask) {
this.deletingTask = null;
return;
}
const task = this.deletingTask;
this.deletingTask = null;
try {
await reg.adminDeleteTask(task.taskId);
await this.loadTasks();
this._toast('success', 'Task deleted', 'The task has been removed from history.');
} catch (e) {
this._toast('error', 'Delete failed', e.message);
}
},

async downloadResult(task) {
// Navigate to the authenticated result URL (cookie auth); the server
// sends Content-Disposition: attachment so the browser downloads it.
Expand Down
37 changes: 36 additions & 1 deletion webapp/js/features/dataset/TaskHistory.vue
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@
:downloading-task-id="downloadingTaskId"
empty-message="No processing tasks have been run for this dataset yet."
@view-log="openLog" @download-result="downloadResult"
@cancel="cancelTask" @retry="retryTask"
@cancel="cancelTask" @retry="retryTask" @delete="deleteTask"
@page-reset="onPageReset" />
</div>

Expand Down Expand Up @@ -200,6 +200,16 @@
@onClose="handleCancelDialogClose">
</ConfirmDialog>

<!-- Delete task confirmation -->
<ConfirmDialog v-if="deleteDialogOpen"
title="Delete Task"
:message="`Permanently delete this concluded task from the history?<br/><strong>${toolTitle(deletingTask.toolId, tools)}</strong> will be removed together with any downloadable results it produced.`"
confirmText="Delete" cancelText="Cancel" confirmButtonClass="danger"
warningTitle="Warning"
warningMessage="This action cannot be undone. Any task results still available for download will be deleted."
@onClose="handleDeleteDialogClose">
</ConfirmDialog>

<!-- Folder picker for output folder -->
<FolderPicker v-if="outputFolderPickerOpen" :dataset="dataset" mode="folder"
:initialPath="photogrammetryForm.destPath || ''" title="Select output folder"
Expand Down Expand Up @@ -345,6 +355,9 @@ export default {
logText: '',

clearDialogOpen: false,
deleteDialogOpen: false,
deletingTask: null,
deletingTask: null,

cancelDialogOpen: false,
cancellingTask: null,
Expand Down Expand Up @@ -824,6 +837,28 @@ export default {
}
},

deleteTask(task) {
this.deletingTask = task;
this.deleteDialogOpen = true;
},

async handleDeleteDialogClose(buttonId) {
this.deleteDialogOpen = false;
if (buttonId !== 'confirm' || !this.deletingTask) {
this.deletingTask = null;
return;
}
const task = this.deletingTask;
this.deletingTask = null;
try {
await this.dataset.deleteTask(task.taskId);
await this.loadTasks();
this._toast('success', 'Task deleted', 'The task has been removed from history.');
} catch (e) {
this._toast('error', 'Delete failed', e.message);
}
},

// ---- output folder picker ----

openOutputFolderPicker() {
Expand Down
27 changes: 25 additions & 2 deletions webapp/js/features/dataset/ViewDataset.vue
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,7 @@
<div v-else class="detail-layout">
<div class="detail-main" :class="{ 'with-detail': selectedDetailFile && !isMobile }">
<TableView ref="tableview" :files="fileBrowserFiles" :tools="explorerTools" :currentPath="currentPath"
:dataset="dataset" :viewMode="viewMode" :canWrite="canWrite" :isLoadingFiles="isLoadingFiles" @openItem="handleOpenItem" @createFolder="handleCreateFolder"
:dataset="dataset" :viewMode="viewMode" :expandedGrid="expandedGrid" :canWrite="canWrite" :isLoadingFiles="isLoadingFiles" @openItem="handleOpenItem" @createFolder="handleCreateFolder"
@deleteSelecteditems="openDeleteItemsDialog" @moveSelectedItems="openRenameItemsDialog"
@transferSelectedItems="openTransferItemsDialog"
@setAsCover="setAsCover"
Expand Down Expand Up @@ -132,7 +132,7 @@
confirmText="Replace"
confirmButtonClass="danger"
@onClose="handleSetCoverClose" />
<RenameDialog v-if="renameDialogOpen" :busy="isBusy" @onClose="handleRenameClose" :file="fileToRename"></RenameDialog>
<RenameDialog v-if="renameDialogOpen" :busy="isBusy" @onClose="handleRenameClose" :file="fileToRename" :all-entries="fileBrowserFiles"></RenameDialog>
<MergeMultispectralDialog v-if="mergeMultispectralDialogOpen" @onClose="handleMergeMultispectralClose" :files="mergeMultispectralFiles" :dataset="dataset" />
<AlignDialog v-if="alignDialogOpen" @onClose="handleAlignClose" :source-entry="alignSourceEntry" :dataset="dataset" :all-entries="fileBrowserFiles" />
<ExtractDialog v-if="extractDialogOpen" @onClose="handleExtractClose" :file="extractFile" :dataset="dataset" />
Expand Down Expand Up @@ -388,6 +388,9 @@ export default {
// Load view mode preference from localStorage
const savedViewMode = localStorage.getItem('fileViewMode') || 'grid';

// Load expanded grid preference (table view: thumbnails + relative time vs icons + date only)
const savedExpandedGrid = localStorage.getItem('tableExpandedGrid') !== 'false';

return {
startTab: mainTabs[0].key,
error: "",
Expand All @@ -401,6 +404,7 @@ export default {
.Dataset(this.$route.params.ds),
currentPath: null,
viewMode: savedViewMode, // 'grid' or 'table'
expandedGrid: savedExpandedGrid, // Table view: thumbnails + relative time vs icons + date only
selectedDetailFile: null, // For DetailPanel in table view
rootDatasetEntry: null, // Root dataset entry with permissions

Expand Down Expand Up @@ -705,6 +709,12 @@ export default {
this.switchViewMode(this.viewMode === 'grid' ? 'table' : 'grid');
},

toggleExpandedGrid() {
this.expandedGrid = !this.expandedGrid;
localStorage.setItem('tableExpandedGrid', this.expandedGrid);
this.updateExplorerTools();
},

handleRefresh() {
if (this.$refs.fileBrowser) {
this.$refs.fileBrowser.refreshAndNavigate(this.currentPath);
Expand Down Expand Up @@ -1389,6 +1399,19 @@ export default {
}
});

// Expanded grid toggle (thumbnails + relative time) only applies to table view
if (this.viewMode === 'table') {
this.explorerTools.push({
id: 'expanded-grid',
title: "Expanded Table View",
icon: "fa-solid fa-table-cells-large",
selected: this.expandedGrid,
onClick: () => {
this.toggleExpandedGrid();
}
});
}

// Show only the button to switch to the opposite view
if (this.viewMode === 'grid') {
this.explorerTools.push({
Expand Down
Loading