Skip to content

Commit c7c0d96

Browse files
johaven7185
authored andcommitted
feat(files): add overwrite confirmation dialog for file rename and update API to support overwrite behavior
1 parent 901fdf8 commit c7c0d96

File tree

20 files changed

+114
-58
lines changed

20 files changed

+114
-58
lines changed

backend/src/applications/files/dto/file-operations.dto.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,8 +18,12 @@ export class CopyMoveFileDto {
1818
@IsOptional()
1919
@IsString()
2020
@RejectIfMatch(regExpInvalidFileName, { message: 'Forbidden characters' })
21-
// renaming use case
21+
// Renaming scenario
2222
dstName?: string
23+
24+
@IsOptional()
25+
@IsBoolean()
26+
overwrite? = false
2327
}
2428

2529
export class DownloadFileDto {

backend/src/applications/files/services/files-methods.service.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -165,7 +165,7 @@ export class FilesMethods {
165165
let dstSpace: SpaceEnv
166166
try {
167167
dstSpace = await this.spacesManager.spaceEnv(user, dstUrl.split('/'))
168-
await this.filesManager.copyMove(user, space, dstSpace, isMove)
168+
await this.filesManager.copyMove(user, space, dstSpace, isMove, copyMoveFileDto.overwrite)
169169
} catch (e) {
170170
this.handleError(space, isMove ? FILE_OPERATION.MOVE : FILE_OPERATION.COPY, e, dstSpace)
171171
}

frontend/src/app/applications/files/components/dialogs/files-overwrite-dialog.component.html

Lines changed: 17 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -7,17 +7,27 @@
77
<div class="modal-header">
88
<h4 class="modal-title" l10nTranslate>
99
<fa-icon [icon]="icons.faFileShield" class="me-1"></fa-icon>
10-
<span l10nTranslate>Overwrite Existing File(s)</span>
10+
@if (renamedTo) {
11+
<span l10nTranslate>Replace Existing File</span>
12+
} @else {
13+
<span l10nTranslate>Overwrite Existing File(s)</span>
14+
}
1115
</h4>
1216
<button (click)="layout.closeDialog()" aria-label="Close" class="btn-close btn-close-white" type="button"></button>
1317
</div>
1418
<div class="modal-body">
15-
<div l10nTranslate>Do you want to replace the existing file(s)?</div>
16-
<ul class="my-2" style="padding-left: inherit">
17-
@for (file of files; track $index) {
18-
<li><b>{{ file.name }}</b></li>
19-
}
20-
</ul>
19+
@if (renamedTo) {
20+
<div
21+
[innerHTML]="'RenameFileToExisting' | translate:locale.language:{ old: files[0].name, new: renamedTo }">
22+
</div>
23+
} @else {
24+
<div l10nTranslate>Do you want to replace the existing file(s)?</div>
25+
<ul class="my-2" style="padding-left: inherit">
26+
@for (file of files; track $index) {
27+
<li><b>{{ file.name }}</b></li>
28+
}
29+
</ul>
30+
}
2131
</div>
2232
<div class="modal-footer">
2333
<button (click)="onAction(false)" class="btn btn-sm btn-secondary" data-dismiss="modal" type="button" l10nTranslate>Cancel</button>

frontend/src/app/applications/files/components/dialogs/files-overwrite-dialog.component.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,17 +7,19 @@
77
import { Component, HostListener, inject, Input, output } from '@angular/core'
88
import { FaIconComponent } from '@fortawesome/angular-fontawesome'
99
import { faFileShield } from '@fortawesome/free-solid-svg-icons'
10-
import { L10nTranslateDirective } from 'angular-l10n'
10+
import { L10N_LOCALE, L10nLocale, L10nTranslateDirective, L10nTranslatePipe } from 'angular-l10n'
1111
import { LayoutService } from '../../../../layout/layout.service'
1212

1313
@Component({
1414
selector: 'app-files-overwrite-dialog',
15-
imports: [FaIconComponent, L10nTranslateDirective],
15+
imports: [FaIconComponent, L10nTranslateDirective, L10nTranslatePipe],
1616
templateUrl: 'files-overwrite-dialog.component.html'
1717
})
1818
export class FilesOverwriteDialogComponent {
1919
@Input() files: File[] = []
20+
@Input() renamedTo: string
2021
public overwrite = output<boolean>()
22+
protected readonly locale = inject<L10nLocale>(L10N_LOCALE)
2123
protected layout = inject(LayoutService)
2224
protected readonly icons = { faFileShield }
2325
protected submitted = false

frontend/src/app/applications/files/services/files.service.ts

Lines changed: 8 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ import type { FileRecent } from '@sync-in-server/backend/src/applications/files/
3333
import { API_SPACES_TREE } from '@sync-in-server/backend/src/applications/spaces/constants/routes'
3434
import { forbiddenChars, isValidFileName } from '@sync-in-server/backend/src/common/shared'
3535
import { BsModalRef } from 'ngx-bootstrap/modal'
36-
import { firstValueFrom, map, Observable, Subject } from 'rxjs'
36+
import { EMPTY, firstValueFrom, map, Observable, Subject } from 'rxjs'
3737
import { downloadWithAnchor } from '../../../common/utils/functions'
3838
import { TAB_MENU } from '../../../layout/layout.interfaces'
3939
import { LayoutService } from '../../../layout/layout.service'
@@ -115,17 +115,11 @@ export class FilesService {
115115
}
116116
}
117117

118-
rename(file: FileModel, name: string) {
119-
if (!this.isValidName(name)) return
118+
rename(file: FileModel, name: string, overwrite = false): Observable<Pick<FileTask, 'name'>> {
119+
if (!this.isValidName(name)) return EMPTY
120120
const dstDirectory = file.path.split('/').slice(0, -1).join('/') || '.'
121-
const op: CopyMoveFileDto = { dstDirectory: dstDirectory, dstName: name }
122-
this.http.request<Pick<FileTask, 'name'>>(FILE_OPERATION.MOVE, file.dataUrl, { body: op }).subscribe({
123-
next: (dto: Pick<FileTask, 'name'>) => {
124-
file.rename(dto.name)
125-
file.isEditable = false
126-
},
127-
error: (e: HttpErrorResponse) => this.layout.sendNotification('error', 'Rename', file.name, e)
128-
})
121+
const op: CopyMoveFileDto = { dstDirectory: dstDirectory, dstName: name, overwrite: overwrite }
122+
return this.http.request<Pick<FileTask, 'name'>>(FILE_OPERATION.MOVE, file.dataUrl, { body: op })
129123
}
130124

131125
delete(files: FileModel[]) {
@@ -212,10 +206,11 @@ export class FilesService {
212206
)
213207
}
214208

215-
async openOverwriteDialog(files: File[]): Promise<boolean> {
209+
async openOverwriteDialog(files: File[] | FileModel[], renamedTo?: string): Promise<boolean> {
216210
const overwriteDialog: BsModalRef<FilesOverwriteDialogComponent> = this.layout.openDialog(FilesOverwriteDialogComponent, null, {
217211
initialState: {
218-
files: files
212+
files: files,
213+
renamedTo: renamedTo
219214
} as FilesOverwriteDialogComponent
220215
})
221216
return new Promise<boolean>((resolve) => {

frontend/src/app/applications/spaces/components/spaces-browser.component.ts

Lines changed: 38 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@ import { FILE_OPERATION } from '@sync-in-server/backend/src/applications/files/c
4040
import type { CompressFileDto } from '@sync-in-server/backend/src/applications/files/dto/file-operations.dto'
4141
import type { FileProps } from '@sync-in-server/backend/src/applications/files/interfaces/file-props.interface'
4242
import type { FileSpace } from '@sync-in-server/backend/src/applications/files/interfaces/file-space.interface'
43-
import { FileTaskStatus } from '@sync-in-server/backend/src/applications/files/models/file-task'
43+
import { type FileTask, FileTaskStatus } from '@sync-in-server/backend/src/applications/files/models/file-task'
4444
import { SHARE_TYPE } from '@sync-in-server/backend/src/applications/shares/constants/shares'
4545
import { SPACE_OPERATION, SPACE_REPOSITORY } from '@sync-in-server/backend/src/applications/spaces/constants/spaces'
4646
import type { SpaceFiles } from '@sync-in-server/backend/src/applications/spaces/interfaces/space-files.interface'
@@ -476,14 +476,32 @@ export class SpacesBrowserComponent implements OnInit, AfterViewInit, OnDestroy
476476
this.selection[0].isRenamed = !this.selection[0].isRenamed
477477
}
478478

479-
renameFile(ev: { object: FileModel; name: string }) {
479+
async renameFile(ev: { object: FileModel; name: string }) {
480480
const f: FileModel = ev.object
481-
const name = ev.name
482-
if (this.files.find((file) => file.name === name)) {
483-
this.layout.sendNotification('error', 'Rename', 'The destination already exists')
484-
return
485-
}
486-
this.filesService.rename(f, name)
481+
const renamedTo = ev.name
482+
let overwrite = false
483+
const fileExists = this.files.find((file) => file.name.toLowerCase() === renamedTo.toLowerCase() && file.id !== f.id)
484+
if (fileExists) {
485+
overwrite = await this.filesService.openOverwriteDialog([f], renamedTo)
486+
if (!overwrite) return
487+
}
488+
this.filesService
489+
.rename(f, renamedTo, overwrite)
490+
.pipe(take(1))
491+
.subscribe({
492+
next: (dto: Pick<FileTask, 'name'>) => {
493+
f.rename(dto.name)
494+
f.isEditable = false
495+
if (overwrite) {
496+
this.sortBy(
497+
this.sortTable.sortParam.column,
498+
false,
499+
this.files.filter((file) => file.id !== fileExists.id)
500+
)
501+
}
502+
},
503+
error: (e: HttpErrorResponse) => this.layout.sendNotification('error', 'Rename', f.name, e)
504+
})
487505
}
488506

489507
setRenamingInProgress(ev: boolean) {
@@ -557,32 +575,32 @@ export class SpacesBrowserComponent implements OnInit, AfterViewInit, OnDestroy
557575
}
558576

559577
async onUploadFiles(ev: { files: File[] }, isDirectory = false) {
560-
let doOverwrite = false
578+
let overwrite = false
561579
if (isDirectory) {
562580
const dirName = ev.files[0].webkitRelativePath.split('/')[0]
563-
const dirExist = this.files.find((f) => f.name.toLowerCase() === dirName.toLowerCase())
564-
if (dirExist) {
565-
doOverwrite = await this.filesService.openOverwriteDialog([{ name: dirName } as File])
566-
if (!doOverwrite) return
581+
const dirExists = this.files.find((f) => f.name.toLowerCase() === dirName.toLowerCase())
582+
if (dirExists) {
583+
overwrite = await this.filesService.openOverwriteDialog([dirExists])
584+
if (!overwrite) return
567585
}
568586
} else {
569587
const exist: File[] = [...ev.files].filter((f: File) => this.files.some((x) => x.name.toLowerCase() === f.name.toLowerCase()))
570588
if (exist.length > 0) {
571-
doOverwrite = await this.filesService.openOverwriteDialog(exist)
572-
if (!doOverwrite) return
589+
overwrite = await this.filesService.openOverwriteDialog(exist)
590+
if (!overwrite) return
573591
}
574592
}
575-
this.filesUpload.addFiles(ev.files, doOverwrite).catch(console.error)
593+
this.filesUpload.addFiles(ev.files, overwrite).catch(console.error)
576594
}
577595

578596
async onDropFiles(ev: { dataTransfer: { files: File[] } }) {
579-
let doOverwrite = false
597+
let overwrite = false
580598
const exist: File[] = [...ev.dataTransfer.files].filter((f: File) => this.files.some((x) => x.name.toLowerCase() === f.name.toLowerCase()))
581599
if (exist.length > 0) {
582-
doOverwrite = await this.filesService.openOverwriteDialog(exist)
583-
if (!doOverwrite) return
600+
overwrite = await this.filesService.openOverwriteDialog(exist)
601+
if (!overwrite) return
584602
}
585-
this.filesUpload.onDropFiles(ev, doOverwrite)
603+
this.filesUpload.onDropFiles(ev, overwrite)
586604
}
587605

588606
decompressFile() {

frontend/src/i18n/de.json

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -597,5 +597,7 @@
597597
"Keep these codes in a safe place. They will allow you to access your account if you lose access to your two-factor authentication.": "Bewahren Sie diese Codes an einem sicheren Ort auf. Sie ermöglichen Ihnen den Zugriff auf Ihr Konto, falls Sie den Zugriff auf Ihre Zwei‑Faktor‑Authentifizierung verlieren.",
598598
"These recovery codes are for one-time use only and will only be displayed here once.": "Diese Wiederherstellungscodes sind nur einmal verwendbar und werden hier nur einmal angezeigt.",
599599
"Overwrite Existing File(s)": "Vorhandene Datei(en) überschreiben",
600-
"Do you want to replace the existing file(s)?": "Möchten Sie die vorhandene(n) Datei(en) ersetzen?"
600+
"Replace Existing File": "Vorhandene Datei ersetzen",
601+
"Do you want to replace the existing file(s)?": "Möchten Sie die vorhandene(n) Datei(en) ersetzen?",
602+
"RenameFileToExisting": "Möchten Sie <b>{{ old }}</b> in <b>{{ new }}</b> umbenennen?"
601603
}

frontend/src/i18n/en.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,5 +20,6 @@
2020
"scheduler_unit_day": "scheduler_unit_day",
2121
"no_selection": "no item selected",
2222
"one_selection": "{{ nb }} item selected",
23-
"nb_selections": "{{ nb }} items selected"
23+
"nb_selections": "{{ nb }} items selected",
24+
"RenameFileToExisting": "Do you want to rename <b>{{ old }}</b> to <b>{{ new }}</b>?"
2425
}

frontend/src/i18n/es.json

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -597,5 +597,7 @@
597597
"Keep these codes in a safe place. They will allow you to access your account if you lose access to your two-factor authentication.": "Guarde estos códigos en un lugar seguro. Le permitirán acceder a su cuenta si pierde el acceso a su autenticación de dos factores.",
598598
"These recovery codes are for one-time use only and will only be displayed here once.": "Estos códigos de recuperación son de un solo uso y solo se mostrarán aquí una vez.",
599599
"Overwrite Existing File(s)": "Sobrescribir archivo(s) existente(s)",
600-
"Do you want to replace the existing file(s)?": "¿Desea reemplazar el/los archivo(s) existente(s)?"
600+
"Replace Existing File": "Reemplazar archivo existente",
601+
"Do you want to replace the existing file(s)?": "¿Desea reemplazar el/los archivo(s) existente(s)?",
602+
"RenameFileToExisting": "¿Desea renombrar <b>{{ old }}</b> a <b>{{ new }}</b>?"
601603
}

frontend/src/i18n/fr.json

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -597,5 +597,7 @@
597597
"Keep these codes in a safe place. They will allow you to access your account if you lose access to your two-factor authentication.": "Conservez ces codes en lieu sûr. Ils vous permettront d’accéder à votre compte si vous perdez l’accès à votre authentification à deux facteurs.",
598598
"These recovery codes are for one-time use only and will only be displayed here once.": "Ces codes de récupération sont à usage unique et ne seront affichés qu’une seule fois ici.",
599599
"Overwrite Existing File(s)": "Écraser le(s) fichier(s) existant(s)",
600-
"Do you want to replace the existing file(s)?": "Voulez-vous remplacer le(s) fichier(s) existant(s) ?"
600+
"Replace Existing File": "Remplacer le fichier existant",
601+
"Do you want to replace the existing file(s)?": "Voulez-vous remplacer le(s) fichier(s) existant(s) ?",
602+
"RenameFileToExisting": "Souhaitez-vous renommer <b>{{ old }}</b> en <b>{{ new }}</b> ?"
601603
}

0 commit comments

Comments
 (0)