pageSize"
+ [lazy]="!searchActive"
+ [paginator]="!searchActive && totalElements > pageSize"
[rows]="pageSize"
[first]="contentFirst"
[totalRecords]="totalElements"
@@ -101,6 +129,7 @@ Cloud Page
Name |
Size |
Type |
+ Modified |
Actions |
@@ -139,6 +168,9 @@ Cloud Page
{{ entry.sizeLabel }} |
{{ entry.typeLabel }} |
+
+ {{ entry.lastModifiedAt | date: "short" }}
+ |
Cloud Page
- |
- This folder is empty. Create a new folder or file.
+ |
+
+ No results for "{{ searchQuery }}".
+
+
+ This folder is empty. Create a new folder or file.
+
|
diff --git a/frontend/src/app/pages/cloud/cloud.component.scss b/frontend/src/app/pages/cloud/cloud.component.scss
index 1c715d584..2df52dd1f 100644
--- a/frontend/src/app/pages/cloud/cloud.component.scss
+++ b/frontend/src/app/pages/cloud/cloud.component.scss
@@ -44,9 +44,47 @@
.cloud-toolbar-start {
display: flex;
align-items: center;
+ flex-wrap: wrap;
gap: 0.4rem;
}
+.cloud-search {
+ display: inline-flex;
+ align-items: center;
+ gap: 0.3rem;
+ padding: 0.15rem 0.5rem;
+ border: 1px solid var(--color-input-border);
+ border-radius: 999px;
+ background: var(--color-input-bg);
+}
+
+.cloud-search-icon {
+ color: var(--color-text-secondary);
+ font-size: 0.7rem;
+}
+
+:host ::ng-deep .cloud-search .cloud-search-input.p-inputtext {
+ border: 0;
+ background: transparent;
+ box-shadow: none;
+ padding: 0.1rem 0;
+ width: clamp(6rem, 30vw, 9rem);
+}
+
+.cloud-search-clear {
+ border: 0;
+ background: transparent;
+ color: var(--color-text-secondary);
+ cursor: pointer;
+ display: inline-flex;
+ align-items: center;
+ padding: 0;
+}
+
+.cloud-search-clear:hover {
+ color: var(--color-text-primary);
+}
+
:host ::ng-deep .cloud-breadcrumb {
border: 0;
padding: 0;
diff --git a/frontend/src/app/pages/cloud/cloud.component.ts b/frontend/src/app/pages/cloud/cloud.component.ts
index f371733ec..cdafafb4f 100644
--- a/frontend/src/app/pages/cloud/cloud.component.ts
+++ b/frontend/src/app/pages/cloud/cloud.component.ts
@@ -15,6 +15,7 @@ import { ToolbarModule } from 'primeng/toolbar';
import { FileDto } from '../../models/dtos/FileDto';
import { FolderDto } from '../../models/dtos/FolderDto';
import { FolderContentItemDto } from '../../models/dtos/FolderContentItemDto';
+import { SearchResultDto } from '../../models/dtos/SearchResultDto';
import { CloudService } from '../../services/cloud.service';
import { finalize, firstValueFrom } from 'rxjs';
import { UiToastService } from '../../core/services/ui-toast.service';
@@ -30,6 +31,7 @@ interface CloudEntry {
path: string;
sizeLabel: string;
typeLabel: string;
+ lastModifiedAt: number;
}
type CloudSort =
@@ -89,6 +91,10 @@ export class CloudComponent implements OnInit {
entries: CloudEntry[] = [];
downloadingPaths = new Set();
+ searchQuery = '';
+ searchActive = false;
+ searching = false;
+
pageSize = 50;
totalElements = 0;
contentFirst = 0;
@@ -195,6 +201,19 @@ export class CloudComponent implements OnInit {
path: item.path,
sizeLabel: this.formatFileSize(item.size),
typeLabel: item.directory ? 'Folder' : item.mimeType || 'Unknown',
+ lastModifiedAt: item.lastModifiedAt,
+ }));
+ }
+
+ private buildSearchEntries(results: SearchResultDto[]): CloudEntry[] {
+ return results.map((result) => ({
+ kind: result.type === 'folder' ? 'folder' : 'file',
+ name: result.name,
+ path: result.path,
+ sizeLabel: this.formatFileSize(result.size ?? 0),
+ typeLabel:
+ result.type === 'folder' ? 'Folder' : result.mimeType || 'Unknown',
+ lastModifiedAt: result.lastModifiedAt,
}));
}
@@ -243,6 +262,53 @@ export class CloudComponent implements OnInit {
setSort(sort: CloudSort) {
if (this.sort === sort) return;
this.sort = sort;
+ this.searchActive = false;
+ this.searchQuery = '';
+ this.searching = false;
+ this.contentFirst = 0;
+ this.loading = true;
+ const relativePath = this.getRelativePath(
+ this.currentFolder?.path || this.rootPath,
+ );
+ this.loadFolderContent(relativePath, 0);
+ }
+
+ onSearch() {
+ const query = this.searchQuery.trim();
+ if (!query) {
+ this.clearSearch();
+ return;
+ }
+ const relativePath = this.getRelativePath(
+ this.currentFolder?.path || this.rootPath,
+ );
+ const wasSearchActive = this.searchActive;
+ this.searching = true;
+ this.searchActive = true;
+ const requestId = ++this.contentRequestId;
+ this.cloudService.searchInFolder(relativePath, query, 100).subscribe({
+ next: (results) => {
+ if (requestId !== this.contentRequestId) return;
+ this.entries = this.buildSearchEntries(results);
+ this.totalElements = this.entries.length;
+ this.searching = false;
+ },
+ error: (err) => {
+ if (requestId !== this.contentRequestId) return;
+ this.searching = false;
+ // Restore the prior mode so a failed search doesn't strand the UI in
+ // "search mode" (no pagination, wrong subtitle) over folder contents.
+ this.searchActive = wasSearchActive;
+ this.toast.error('Search failed', this.getErrorMessage(err));
+ },
+ });
+ }
+
+ clearSearch() {
+ if (!this.searchActive && this.searchQuery === '') return;
+ this.searchQuery = '';
+ this.searchActive = false;
+ this.searching = false;
this.contentFirst = 0;
this.loading = true;
const relativePath = this.getRelativePath(
@@ -252,9 +318,15 @@ export class CloudComponent implements OnInit {
}
loadRootFolder() {
+ this.searchActive = false;
+ this.searchQuery = '';
+ this.searching = false;
this.loading = true;
this.error = undefined;
this.contentFirst = 0;
+ // Invalidate any in-flight search/content request so a stale, fast
+ // response can't overwrite the root reload that's about to start.
+ this.contentRequestId++;
this.cloudService.getRootFolder(false).subscribe({
next: (folder) => {
this.currentFolder = folder;
@@ -282,8 +354,14 @@ export class CloudComponent implements OnInit {
}
navigateToFolder(folderPath?: string) {
+ this.searchActive = false;
+ this.searchQuery = '';
+ this.searching = false;
this.loading = true;
this.contentFirst = 0;
+ // Invalidate any in-flight search/content request so a stale response
+ // can't overwrite the entries while navigation is in progress.
+ this.contentRequestId++;
const relativePath = this.getRelativePath(folderPath || this.rootPath);
this.cloudService.getFolderByPath(relativePath, false).subscribe({
next: (folder) => {
diff --git a/frontend/src/app/services/cloud.service.ts b/frontend/src/app/services/cloud.service.ts
index 8cc51a2f1..c46344014 100644
--- a/frontend/src/app/services/cloud.service.ts
+++ b/frontend/src/app/services/cloud.service.ts
@@ -5,6 +5,7 @@ import { FolderDto } from '../models/dtos/FolderDto';
import { FolderContentItemDto } from '../models/dtos/FolderContentItemDto';
import { PageResponseDto } from '../models/dtos/PageResponseDto';
import { TrashEntryDto } from '../models/dtos/TrashEntryDto';
+import { SearchResultDto } from '../models/dtos/SearchResultDto';
import { environment } from '../../environments/environment';
@Injectable({
@@ -59,6 +60,20 @@ export class CloudService {
);
}
+ searchInFolder(
+ folderPath: string,
+ query: string,
+ maxResults = 20,
+ ): Observable {
+ const params = new HttpParams()
+ .set('folderPath', this.normalizePath(folderPath))
+ .set('query', query)
+ .set('maxResults', String(maxResults));
+ return this.http.get(`${this.apiUrl}/folders/search`, {
+ params,
+ });
+ }
+
getFileContent(relativePath: string): Observable {
const path = this.normalizePath(relativePath);
return this.http.get(`${this.apiUrl}/files/content`, {
|