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
9 changes: 9 additions & 0 deletions frontend/src/app/models/dtos/SearchResultDto.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
export interface SearchResultDto {
name: string;
path: string;
type: 'file' | 'folder';
size?: number;
mimeType: string | null;
lastModifiedAt: number;
score: number;
}
47 changes: 42 additions & 5 deletions frontend/src/app/pages/cloud/cloud.component.html
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,15 @@
<div>
<h1 class="cloud-title text-base sm:text-lg">Cloud Page</h1>
<p class="cloud-subtitle text-xxs sm:text-xs">
{{ totalItemsInView }} items in this folder
<ng-container *ngIf="searchActive">
{{ totalItemsInView }} result{{
totalItemsInView === 1 ? "" : "s"
}}
for "{{ searchQuery }}"
</ng-container>
<ng-container *ngIf="!searchActive">
{{ totalItemsInView }} items in this folder
</ng-container>
</p>
</div>
<div class="cloud-headline-actions">
Expand Down Expand Up @@ -56,6 +64,26 @@ <h1 class="cloud-title text-base sm:text-lg">Cloud Page</h1>
[appendTo]="'body'"
[model]="sortMenuItems"
></p-menu>
<span class="cloud-search">
<i class="pi pi-search cloud-search-icon"></i>
<input
pInputText
type="text"
[(ngModel)]="searchQuery"
(keydown.enter)="onSearch()"
placeholder="Search this folder"
class="cloud-search-input text-xxs sm:text-xs"
/>
<button
*ngIf="searchActive || searchQuery"
type="button"
class="cloud-search-clear"
aria-label="Clear search"
(click)="clearSearch()"
>
<i class="pi pi-times"></i>
</button>
</span>
<input
#fileUploadInput
type="file"
Expand Down Expand Up @@ -87,8 +115,8 @@ <h1 class="cloud-title text-base sm:text-lg">Cloud Page</h1>
<section *ngIf="currentFolder && !loading" class="cloud-table-wrap">
<p-table
[value]="entries"
[lazy]="true"
[paginator]="totalElements > pageSize"
[lazy]="!searchActive"
[paginator]="!searchActive && totalElements > pageSize"
[rows]="pageSize"
[first]="contentFirst"
[totalRecords]="totalElements"
Expand All @@ -101,6 +129,7 @@ <h1 class="cloud-title text-base sm:text-lg">Cloud Page</h1>
<th>Name</th>
<th>Size</th>
<th>Type</th>
<th>Modified</th>
<th class="text-right">Actions</th>
</tr>
</ng-template>
Expand Down Expand Up @@ -139,6 +168,9 @@ <h1 class="cloud-title text-base sm:text-lg">Cloud Page</h1>
</td>
<td class="cloud-muted">{{ entry.sizeLabel }}</td>
<td class="cloud-muted">{{ entry.typeLabel }}</td>
<td class="cloud-muted">
{{ entry.lastModifiedAt | date: "short" }}
</td>
<td>
<div class="cloud-actions">
<p-button
Expand Down Expand Up @@ -174,8 +206,13 @@ <h1 class="cloud-title text-base sm:text-lg">Cloud Page</h1>
</ng-template>
<ng-template pTemplate="emptymessage">
<tr>
<td colspan="4" class="cloud-empty text-xxs sm:text-xs">
This folder is empty. Create a new folder or file.
<td colspan="5" class="cloud-empty text-xxs sm:text-xs">
<ng-container *ngIf="searchActive; else emptyFolderMsg">
No results for "{{ searchQuery }}".
</ng-container>
<ng-template #emptyFolderMsg>
This folder is empty. Create a new folder or file.
</ng-template>
</td>
</tr>
</ng-template>
Expand Down
38 changes: 38 additions & 0 deletions frontend/src/app/pages/cloud/cloud.component.scss
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
78 changes: 78 additions & 0 deletions frontend/src/app/pages/cloud/cloud.component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -30,6 +31,7 @@ interface CloudEntry {
path: string;
sizeLabel: string;
typeLabel: string;
lastModifiedAt: number;
}

type CloudSort =
Expand Down Expand Up @@ -89,6 +91,10 @@ export class CloudComponent implements OnInit {
entries: CloudEntry[] = [];
downloadingPaths = new Set<string>();

searchQuery = '';
searchActive = false;
searching = false;

pageSize = 50;
totalElements = 0;
contentFirst = 0;
Expand Down Expand Up @@ -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,
}));
}

Expand Down Expand Up @@ -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);
}
Comment thread
DenizAltunkapan marked this conversation as resolved.

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(
Expand All @@ -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;
Comment thread
DenizAltunkapan marked this conversation as resolved.
// 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;
Expand Down Expand Up @@ -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) => {
Expand Down
15 changes: 15 additions & 0 deletions frontend/src/app/services/cloud.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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({
Expand Down Expand Up @@ -59,6 +60,20 @@ export class CloudService {
);
}

searchInFolder(
folderPath: string,
query: string,
maxResults = 20,
): Observable<SearchResultDto[]> {
const params = new HttpParams()
.set('folderPath', this.normalizePath(folderPath))
.set('query', query)
.set('maxResults', String(maxResults));
return this.http.get<SearchResultDto[]>(`${this.apiUrl}/folders/search`, {
params,
});
}

getFileContent(relativePath: string): Observable<string> {
const path = this.normalizePath(relativePath);
return this.http.get(`${this.apiUrl}/files/content`, {
Expand Down