name beim kopieren

This commit is contained in:
Bastian Wagner
2026-06-09 16:03:20 +02:00
parent 06c5b1768e
commit 16605c961f
18 changed files with 693 additions and 44 deletions

View File

@@ -0,0 +1,30 @@
<h2 mat-dialog-title>Liste aus Template erstellen</h2>
<form [formGroup]="form" (ngSubmit)="submit()">
<mat-dialog-content>
<p class="dialog-copy">
Vergib einen individuellen Namen für die neue Liste.
</p>
<mat-form-field appearance="outline">
<mat-label>Listenname</mat-label>
<input matInput formControlName="name" autocomplete="off" />
@if (form.controls.name.hasError('required')) {
<mat-error>Listenname ist erforderlich.</mat-error>
}
</mat-form-field>
<mat-form-field appearance="outline">
<mat-label>Beschreibung</mat-label>
<textarea matInput formControlName="description" rows="3"></textarea>
</mat-form-field>
</mat-dialog-content>
<mat-dialog-actions align="end">
<button mat-button type="button" mat-dialog-close>Abbrechen</button>
<button mat-flat-button type="submit">
<mat-icon aria-hidden="true">content_copy</mat-icon>
Als Liste erstellen
</button>
</mat-dialog-actions>
</form>

View File

@@ -0,0 +1,14 @@
.dialog-copy {
margin: 0 0 1rem;
color: var(--mat-sys-on-surface-variant);
}
mat-form-field {
width: 100%;
}
mat-dialog-content {
display: grid;
gap: 0.75rem;
min-width: min(100vw - 64px, 360px);
}

View File

@@ -0,0 +1,60 @@
import { Component, inject } from '@angular/core';
import { NonNullableFormBuilder, ReactiveFormsModule, Validators } from '@angular/forms';
import { MatButtonModule } from '@angular/material/button';
import {
MAT_DIALOG_DATA,
MatDialogModule,
MatDialogRef,
} from '@angular/material/dialog';
import { MatFormFieldModule } from '@angular/material/form-field';
import { MatIconModule } from '@angular/material/icon';
import { MatInputModule } from '@angular/material/input';
export interface CopyTemplateDialogData {
templateName: string;
templateDescription?: string;
}
export interface CopyTemplateDialogResult {
name: string;
description?: string;
}
@Component({
selector: 'app-copy-template-dialog',
imports: [
ReactiveFormsModule,
MatButtonModule,
MatDialogModule,
MatFormFieldModule,
MatIconModule,
MatInputModule,
],
templateUrl: './copy-template-dialog.component.html',
styleUrl: './copy-template-dialog.component.scss',
})
export class CopyTemplateDialogComponent {
protected readonly data = inject<CopyTemplateDialogData>(MAT_DIALOG_DATA);
private readonly dialogRef = inject(
MatDialogRef<CopyTemplateDialogComponent, CopyTemplateDialogResult>,
);
private readonly formBuilder = inject(NonNullableFormBuilder);
protected readonly form = this.formBuilder.group({
name: [this.data.templateName, [Validators.required]],
description: [this.data.templateDescription ?? ''],
});
protected submit(): void {
if (this.form.invalid) {
this.form.markAllAsTouched();
return;
}
const value = this.form.getRawValue();
this.dialogRef.close({
name: value.name.trim(),
description: value.description.trim() || undefined,
});
}
}

View File

@@ -15,6 +15,10 @@ import { MatSnackBar, MatSnackBarModule } from '@angular/material/snack-bar';
import { getAuthErrorMessage } from '../../auth/error-message';
import { OnboardingService } from '../../onboarding/onboarding.service';
import { ConfirmDeleteDialogComponent } from '../confirm-delete-dialog/confirm-delete-dialog.component';
import {
CopyTemplateDialogComponent,
CopyTemplateDialogResult,
} from '../copy-template-dialog/copy-template-dialog.component';
import { ListTemplate } from '../templates.models';
import { TemplatesService } from '../templates.service';
@@ -239,27 +243,48 @@ export class TemplateDetailComponent implements OnInit {
protected copyTemplateToList(): void {
const templateId = this.templateId();
const template = this.template();
if (!templateId || !this.canEditItems() || this.copyingTemplate()) {
if (!templateId || !template || !this.canEditItems() || this.copyingTemplate()) {
return;
}
this.copyingTemplate.set(true);
this.dialog
.open<
CopyTemplateDialogComponent,
{ templateName: string; templateDescription?: string },
CopyTemplateDialogResult
>(CopyTemplateDialogComponent, {
data: {
templateName: template.name,
templateDescription: template.description,
},
maxWidth: '480px',
width: 'calc(100vw - 32px)',
})
.afterClosed()
.subscribe((result) => {
if (!result) {
return;
}
this.templatesService
.createListFromTemplate(templateId)
.pipe(finalize(() => this.copyingTemplate.set(false)))
.subscribe({
next: (list) => {
this.snackBar.open('Liste aus Template erstellt.', 'OK', {
duration: 3000,
this.copyingTemplate.set(true);
this.templatesService
.createListFromTemplate(templateId, result)
.pipe(finalize(() => this.copyingTemplate.set(false)))
.subscribe({
next: (list) => {
this.snackBar.open('Liste aus Template erstellt.', 'OK', {
duration: 3000,
});
this.onboarding.templateCopiedToList(list.id);
void this.router.navigateByUrl('/lists');
},
error: (error: unknown) => {
this.snackBar.open(getAuthErrorMessage(error), 'OK', { duration: 5000 });
},
});
this.onboarding.templateCopiedToList(list.id);
void this.router.navigateByUrl('/lists');
},
error: (error: unknown) => {
this.snackBar.open(getAuthErrorMessage(error), 'OK', { duration: 5000 });
},
});
}

View File

@@ -11,6 +11,10 @@ import { MatSnackBar, MatSnackBarModule } from '@angular/material/snack-bar';
import { getAuthErrorMessage } from '../auth/error-message';
import { OnboardingService } from '../onboarding/onboarding.service';
import { ConfirmDeleteDialogComponent } from './confirm-delete-dialog/confirm-delete-dialog.component';
import {
CopyTemplateDialogComponent,
CopyTemplateDialogResult,
} from './copy-template-dialog/copy-template-dialog.component';
import { ListTemplate, ListTemplateKind } from './templates.models';
import { TemplatesService } from './templates.service';
@@ -79,23 +83,43 @@ export class TemplatesComponent implements OnInit {
return;
}
this.copyingTemplateId.set(template.id);
this.dialog
.open<
CopyTemplateDialogComponent,
{ templateName: string; templateDescription?: string },
CopyTemplateDialogResult
>(CopyTemplateDialogComponent, {
data: {
templateName: template.name,
templateDescription: template.description,
},
maxWidth: '480px',
width: 'calc(100vw - 32px)',
})
.afterClosed()
.subscribe((result) => {
if (!result) {
return;
}
this.templatesService
.createListFromTemplate(template.id)
.pipe(finalize(() => this.copyingTemplateId.set(null)))
.subscribe({
next: () => {
this.snackBar.open('Liste aus Template erstellt.', 'OK', {
duration: 3000,
this.copyingTemplateId.set(template.id);
this.templatesService
.createListFromTemplate(template.id, result)
.pipe(finalize(() => this.copyingTemplateId.set(null)))
.subscribe({
next: () => {
this.snackBar.open('Liste aus Template erstellt.', 'OK', {
duration: 3000,
});
void this.router.navigateByUrl('/lists');
},
error: (error: unknown) => {
this.snackBar.open(getAuthErrorMessage(error), 'OK', {
duration: 5000,
});
},
});
void this.router.navigateByUrl('/lists');
},
error: (error: unknown) => {
this.snackBar.open(getAuthErrorMessage(error), 'OK', {
duration: 5000,
});
},
});
}