This commit is contained in:
Bastian Wagner
2026-06-15 10:45:39 +02:00
parent 8caf207c7b
commit fd7245f1fb
7 changed files with 175 additions and 0 deletions

View File

@@ -0,0 +1,20 @@
<div class="delete-dialog-icon">
<mat-icon aria-hidden="true">delete</mat-icon>
</div>
<h2 mat-dialog-title>Liste loeschen?</h2>
<mat-dialog-content>
<p>
<strong>{{ data.listName }}</strong> wird geloescht und ist danach nicht
mehr in deinen Listen sichtbar.
</p>
</mat-dialog-content>
<mat-dialog-actions align="end">
<button mat-button type="button" mat-dialog-close>Abbrechen</button>
<button mat-flat-button type="button" color="warn" [mat-dialog-close]="true">
<mat-icon aria-hidden="true">delete</mat-icon>
Loeschen
</button>
</mat-dialog-actions>

View File

@@ -0,0 +1,17 @@
import { Component, inject } from '@angular/core';
import { MAT_DIALOG_DATA, MatDialogModule } from '@angular/material/dialog';
import { MatButtonModule } from '@angular/material/button';
import { MatIconModule } from '@angular/material/icon';
export interface ConfirmDeleteListDialogData {
listName: string;
}
@Component({
selector: 'app-confirm-delete-list-dialog',
imports: [MatButtonModule, MatDialogModule, MatIconModule],
templateUrl: './confirm-delete-list-dialog.component.html',
})
export class ConfirmDeleteListDialogComponent {
protected readonly data = inject<ConfirmDeleteListDialogData>(MAT_DIALOG_DATA);
}

View File

@@ -61,6 +61,23 @@
<mat-icon aria-hidden="true">{{ showEditor() ? 'close' : 'edit' }}</mat-icon>
{{ showEditor() ? 'Abbrechen' : 'Bearbeiten' }}
</button>
@if (canDeleteList()) {
<button
mat-stroked-button
type="button"
color="warn"
[disabled]="deletingList()"
(click)="deleteList()"
>
@if (deletingList()) {
<mat-progress-spinner mode="indeterminate" diameter="18" />
} @else {
<mat-icon aria-hidden="true">delete</mat-icon>
}
Loeschen
</button>
}
</div>
}
</mat-card-header>

View File

@@ -7,6 +7,7 @@ import { finalize } from 'rxjs';
import { MatButtonModule } from '@angular/material/button';
import { MatCardModule } from '@angular/material/card';
import { MatCheckboxModule } from '@angular/material/checkbox';
import { MatDialog, MatDialogModule } from '@angular/material/dialog';
import { MatFormFieldModule } from '@angular/material/form-field';
import { MatIconModule } from '@angular/material/icon';
import { MatInputModule } from '@angular/material/input';
@@ -16,6 +17,7 @@ import { AuthService } from '../../auth/auth.service';
import { getAuthErrorMessage } from '../../auth/error-message';
import { PublicUserSearchResult } from '../../auth/auth.models';
import { OnboardingService } from '../../onboarding/onboarding.service';
import { ConfirmDeleteListDialogComponent } from '../confirm-delete-list-dialog/confirm-delete-list-dialog.component';
import {
ListItemSuggestion,
ListRealtimeEvent,
@@ -34,6 +36,7 @@ import { ListsService } from '../lists.service';
MatButtonModule,
MatCardModule,
MatCheckboxModule,
MatDialogModule,
MatFormFieldModule,
MatIconModule,
MatInputModule,
@@ -47,6 +50,7 @@ export class ListDetailComponent implements OnInit {
private readonly destroyRef = inject(DestroyRef);
private readonly formBuilder = inject(NonNullableFormBuilder);
private readonly authService = inject(AuthService);
private readonly dialog = inject(MatDialog);
private readonly listsService = inject(ListsService);
private readonly listsRealtimeService = inject(ListsRealtimeService);
private readonly route = inject(ActivatedRoute);
@@ -59,6 +63,7 @@ export class ListDetailComponent implements OnInit {
protected readonly loading = signal(true);
protected readonly saving = signal(false);
protected readonly creatingTemplate = signal(false);
protected readonly deletingList = signal(false);
protected readonly editing = signal(false);
protected readonly addingItem = signal(false);
protected readonly loadingSuggestions = signal(false);
@@ -76,6 +81,9 @@ export class ListDetailComponent implements OnInit {
protected readonly canManageShares = computed(
() => this.list()?.accessRole === 'owner' && !this.isCreateMode(),
);
protected readonly canDeleteList = computed(
() => this.list()?.accessRole === 'owner' && !this.isCreateMode(),
);
protected readonly showShareControls = computed(
() => this.canManageShares() && this.showEditor(),
);
@@ -233,6 +241,48 @@ export class ListDetailComponent implements OnInit {
});
}
protected deleteList(): void {
const listId = this.listId();
const list = this.list();
if (!listId || !list || !this.canDeleteList() || this.deletingList()) {
return;
}
this.dialog
.open<ConfirmDeleteListDialogComponent, { listName: string }, boolean>(
ConfirmDeleteListDialogComponent,
{
data: { listName: list.name },
maxWidth: '420px',
width: 'calc(100vw - 32px)',
},
)
.afterClosed()
.subscribe((confirmed) => {
if (!confirmed) {
return;
}
this.deletingList.set(true);
this.listsService
.deleteList(listId)
.pipe(finalize(() => this.deletingList.set(false)))
.subscribe({
next: () => {
this.snackBar.open('Liste geloescht.', 'OK', { duration: 3000 });
void this.router.navigateByUrl('/lists');
},
error: (error: unknown) => {
this.snackBar.open(getAuthErrorMessage(error), 'OK', {
duration: 5000,
});
},
});
});
}
protected loadSuggestions(): void {
const listId = this.listId();

View File

@@ -167,6 +167,23 @@
</mat-card-content>
<mat-card-actions align="end">
@if (list.accessRole === 'owner') {
<button
mat-button
type="button"
color="warn"
[disabled]="deletingListId() === list.id"
(click)="deleteList(list)"
>
@if (deletingListId() === list.id) {
<mat-progress-spinner mode="indeterminate" diameter="18" />
} @else {
<mat-icon aria-hidden="true">delete</mat-icon>
}
Loeschen
</button>
}
<a
mat-button
[routerLink]="['/lists', list.id]"

View File

@@ -2,16 +2,20 @@ import { DatePipe } from '@angular/common';
import { Component, DestroyRef, OnInit, computed, inject, signal } from '@angular/core';
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
import { RouterLink } from '@angular/router';
import { finalize } from 'rxjs';
import { MatButtonModule } from '@angular/material/button';
import { MatButtonToggleModule } from '@angular/material/button-toggle';
import { MatCardModule } from '@angular/material/card';
import { MatDialog, MatDialogModule } from '@angular/material/dialog';
import { MatFormFieldModule } from '@angular/material/form-field';
import { MatIconModule } from '@angular/material/icon';
import { MatInputModule } from '@angular/material/input';
import { MatProgressSpinnerModule } from '@angular/material/progress-spinner';
import { MatSelectModule } from '@angular/material/select';
import { MatSnackBar, MatSnackBarModule } from '@angular/material/snack-bar';
import { getAuthErrorMessage } from '../auth/error-message';
import { OnboardingService } from '../onboarding/onboarding.service';
import { ConfirmDeleteListDialogComponent } from './confirm-delete-list-dialog/confirm-delete-list-dialog.component';
import { ListTemplateKind } from '../templates/templates.models';
import { ListRealtimeEvent, UserList, UserListItem } from './lists.models';
import { ListsRealtimeService } from './lists-realtime.service';
@@ -34,23 +38,28 @@ type ListKindFilter = ListTemplateKind | 'all';
MatButtonModule,
MatButtonToggleModule,
MatCardModule,
MatDialogModule,
MatFormFieldModule,
MatIconModule,
MatInputModule,
MatProgressSpinnerModule,
MatSelectModule,
MatSnackBarModule,
],
templateUrl: './lists.component.html',
styleUrls: ['../workspace-page.scss', './lists.component.scss'],
})
export class ListsComponent implements OnInit {
private readonly destroyRef = inject(DestroyRef);
private readonly dialog = inject(MatDialog);
private readonly listsService = inject(ListsService);
private readonly listsRealtimeService = inject(ListsRealtimeService);
private readonly snackBar = inject(MatSnackBar);
protected readonly onboarding = inject(OnboardingService);
protected readonly lists = signal<UserList[]>([]);
protected readonly loading = signal(true);
protected readonly deletingListId = signal<string | null>(null);
protected readonly errorMessage = signal<string | null>(null);
protected readonly searchTerm = signal('');
protected readonly kindFilter = signal<ListKindFilter>('all');
@@ -171,6 +180,47 @@ export class ListsComponent implements OnInit {
this.sortOption.set('updated-desc');
}
protected deleteList(list: UserList): void {
if (list.accessRole !== 'owner' || this.deletingListId()) {
return;
}
this.dialog
.open<ConfirmDeleteListDialogComponent, { listName: string }, boolean>(
ConfirmDeleteListDialogComponent,
{
data: { listName: list.name },
maxWidth: '420px',
width: 'calc(100vw - 32px)',
},
)
.afterClosed()
.subscribe((confirmed) => {
if (!confirmed) {
return;
}
this.deletingListId.set(list.id);
this.listsService
.deleteList(list.id)
.pipe(finalize(() => this.deletingListId.set(null)))
.subscribe({
next: () => {
this.lists.update((lists) =>
lists.filter((existingList) => existingList.id !== list.id),
);
this.snackBar.open('Liste geloescht.', 'OK', { duration: 3000 });
},
error: (error: unknown) => {
this.snackBar.open(getAuthErrorMessage(error), 'OK', {
duration: 5000,
});
},
});
});
}
private subscribeToRealtime(): void {
this.listsRealtimeService
.events()

View File

@@ -32,6 +32,10 @@ export class ListsService {
return this.http.patch<UserList>(`${this.apiUrl}/${listId}`, data);
}
deleteList(listId: string): Observable<{ message: string }> {
return this.http.delete<{ message: string }>(`${this.apiUrl}/${listId}`);
}
shareList(listId: string, userId: string): Observable<UserList> {
return this.http.post<UserList>(`${this.apiUrl}/${listId}/shares`, { userId });
}