list suggestions
This commit is contained in:
@@ -40,6 +40,17 @@ export class ListsController {
|
|||||||
return this.listsService.createList(this.requireUserId(request), createDto);
|
return this.listsService.createList(this.requireUserId(request), createDto);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Post('with-item-suggestions')
|
||||||
|
createListWithItemSuggestions(
|
||||||
|
@Req() request: AuthenticatedRequest,
|
||||||
|
@Body() createDto: CreateListDto,
|
||||||
|
) {
|
||||||
|
return this.listsService.createListWithItemSuggestions(
|
||||||
|
this.requireUserId(request),
|
||||||
|
createDto,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
@Get()
|
@Get()
|
||||||
listLists(@Req() request: AuthenticatedRequest) {
|
listLists(@Req() request: AuthenticatedRequest) {
|
||||||
return this.listsService.listLists(this.requireUserId(request));
|
return this.listsService.listLists(this.requireUserId(request));
|
||||||
|
|||||||
@@ -465,6 +465,55 @@ describe('ListsService', () => {
|
|||||||
expect(requestPayload.tools).toBeUndefined();
|
expect(requestPayload.tools).toBeUndefined();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('creates a list and returns item suggestions for it', async () => {
|
||||||
|
process.env.MISTRAL_API_KEY = 'test-key';
|
||||||
|
process.env.MISTRAL_AGENT_ID = 'agent-listify';
|
||||||
|
mockMistralResponse({
|
||||||
|
choices: [
|
||||||
|
{
|
||||||
|
message: {
|
||||||
|
content: JSON.stringify({
|
||||||
|
suggestions: [
|
||||||
|
{
|
||||||
|
title: 'Location buchen',
|
||||||
|
notes: 'Mit Kapazitaet abgleichen',
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
const response = await service.createListWithItemSuggestions('user-1', {
|
||||||
|
name: 'Sommerfest',
|
||||||
|
description: 'Planung fuer Team-Event',
|
||||||
|
kind: 'todo',
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(response.list.name).toBe('Sommerfest');
|
||||||
|
expect(response.list.items).toHaveLength(0);
|
||||||
|
expect(response.suggestions).toEqual([
|
||||||
|
{
|
||||||
|
title: 'Location buchen',
|
||||||
|
notes: 'Mit Kapazitaet abgleichen',
|
||||||
|
quantity: undefined,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
await expect(service.listLists('user-1')).resolves.toHaveLength(1);
|
||||||
|
|
||||||
|
const requestPayload = getMistralRequestPayload();
|
||||||
|
expect(requestPayload.messages[1].content).toContain('Name: Sommerfest');
|
||||||
|
expect(requestPayload.messages[1].content).toContain(
|
||||||
|
'Beschreibung: Planung fuer Team-Event',
|
||||||
|
);
|
||||||
|
expect(requestPayload.messages[1].content).toContain(
|
||||||
|
'Vorhandene Items: keine',
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
it('returns an empty suggestion list for malformed provider content', async () => {
|
it('returns an empty suggestion list for malformed provider content', async () => {
|
||||||
process.env.MISTRAL_API_KEY = 'test-key';
|
process.env.MISTRAL_API_KEY = 'test-key';
|
||||||
process.env.MISTRAL_AGENT_ID = 'agent-listify';
|
process.env.MISTRAL_AGENT_ID = 'agent-listify';
|
||||||
|
|||||||
@@ -41,6 +41,11 @@ export interface ListItemSuggestionsResponse {
|
|||||||
suggestions: ListItemSuggestion[];
|
suggestions: ListItemSuggestion[];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface CreateListWithItemSuggestionsResponse {
|
||||||
|
list: UserList;
|
||||||
|
suggestions: ListItemSuggestion[];
|
||||||
|
}
|
||||||
|
|
||||||
interface MistralAgentCompletionResponse {
|
interface MistralAgentCompletionResponse {
|
||||||
choices?: Array<{
|
choices?: Array<{
|
||||||
message?: {
|
message?: {
|
||||||
@@ -109,6 +114,21 @@ export class ListsService {
|
|||||||
return userList;
|
return userList;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async createListWithItemSuggestions(
|
||||||
|
ownerId: string,
|
||||||
|
createDto: CreateListDto,
|
||||||
|
): Promise<CreateListWithItemSuggestionsResponse> {
|
||||||
|
const list = await this.createList(ownerId, createDto);
|
||||||
|
const listEntity = await this.findAccessibleList(ownerId, list.id);
|
||||||
|
const response = await this.callMistralForItemSuggestions(listEntity);
|
||||||
|
const suggestions = this.normalizeItemSuggestions(response, listEntity.items);
|
||||||
|
|
||||||
|
return {
|
||||||
|
list,
|
||||||
|
suggestions,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
async createListFromTemplate(
|
async createListFromTemplate(
|
||||||
ownerId: string,
|
ownerId: string,
|
||||||
template: ListTemplate,
|
template: ListTemplate,
|
||||||
|
|||||||
@@ -63,7 +63,9 @@
|
|||||||
</button>
|
</button>
|
||||||
|
|
||||||
@if (canDeleteList()) {
|
@if (canDeleteList()) {
|
||||||
<button mat-icon-button
|
<button
|
||||||
|
mat-stroked-button
|
||||||
|
class="delete-list-button"
|
||||||
type="button"
|
type="button"
|
||||||
color="warn"
|
color="warn"
|
||||||
[disabled]="deletingList()"
|
[disabled]="deletingList()"
|
||||||
@@ -71,8 +73,10 @@
|
|||||||
>
|
>
|
||||||
@if (deletingList()) {
|
@if (deletingList()) {
|
||||||
<mat-progress-spinner mode="indeterminate" diameter="18" />
|
<mat-progress-spinner mode="indeterminate" diameter="18" />
|
||||||
|
} @else {
|
||||||
|
<mat-icon aria-hidden="true">delete</mat-icon>
|
||||||
}
|
}
|
||||||
<mat-icon aria-hidden="true">delete</mat-icon>
|
Loeschen
|
||||||
</button>
|
</button>
|
||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
@@ -95,14 +99,32 @@
|
|||||||
<textarea matInput formControlName="description" rows="4"></textarea>
|
<textarea matInput formControlName="description" rows="4"></textarea>
|
||||||
</mat-form-field>
|
</mat-form-field>
|
||||||
|
|
||||||
<button mat-flat-button type="submit" [disabled]="saving()">
|
<div class="list-form-actions" [class.create-actions]="isCreateMode()">
|
||||||
@if (saving()) {
|
<button mat-flat-button type="submit" [disabled]="saving() || creatingWithAi()">
|
||||||
<mat-progress-spinner mode="indeterminate" diameter="18" />
|
@if (saving()) {
|
||||||
} @else {
|
<mat-progress-spinner mode="indeterminate" diameter="18" />
|
||||||
<mat-icon aria-hidden="true">save</mat-icon>
|
} @else {
|
||||||
|
<mat-icon aria-hidden="true">save</mat-icon>
|
||||||
|
}
|
||||||
|
{{ isCreateMode() ? 'Liste anlegen' : 'Speichern' }}
|
||||||
|
</button>
|
||||||
|
|
||||||
|
@if (isCreateMode()) {
|
||||||
|
<button
|
||||||
|
mat-stroked-button
|
||||||
|
type="button"
|
||||||
|
[disabled]="saving() || creatingWithAi() || !onlineStatus.online()"
|
||||||
|
(click)="createListWithAi()"
|
||||||
|
>
|
||||||
|
@if (creatingWithAi()) {
|
||||||
|
<mat-progress-spinner mode="indeterminate" diameter="18" />
|
||||||
|
} @else {
|
||||||
|
<mat-icon aria-hidden="true">auto_awesome</mat-icon>
|
||||||
|
}
|
||||||
|
Liste mit KI erstellen
|
||||||
|
</button>
|
||||||
}
|
}
|
||||||
{{ isCreateMode() ? 'Liste anlegen' : 'Speichern' }}
|
</div>
|
||||||
</button>
|
|
||||||
</form>
|
</form>
|
||||||
} @else {
|
} @else {
|
||||||
<div class="list-summary">
|
<div class="list-summary">
|
||||||
|
|||||||
@@ -2,11 +2,9 @@
|
|||||||
display: grid;
|
display: grid;
|
||||||
gap: 1rem;
|
gap: 1rem;
|
||||||
width: min(100%, 760px);
|
width: min(100%, 760px);
|
||||||
max-width: 100%;
|
|
||||||
min-height: calc(100dvh - 56px);
|
min-height: calc(100dvh - 56px);
|
||||||
margin: 0 auto;
|
margin: 0 auto;
|
||||||
padding: 1rem;
|
padding: 1rem;
|
||||||
overflow-x: hidden;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.detail-header {
|
.detail-header {
|
||||||
@@ -14,7 +12,6 @@
|
|||||||
grid-template-columns: auto minmax(0, 1fr);
|
grid-template-columns: auto minmax(0, 1fr);
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 0.75rem;
|
gap: 0.75rem;
|
||||||
min-width: 0;
|
|
||||||
padding: 0.75rem 0 0.25rem;
|
padding: 0.75rem 0 0.25rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -37,7 +34,6 @@
|
|||||||
.sharing-card,
|
.sharing-card,
|
||||||
.items-card {
|
.items-card {
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
max-width: 100%;
|
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
border: 1px solid color-mix(in srgb, var(--mat-sys-outline-variant) 72%, transparent);
|
border: 1px solid color-mix(in srgb, var(--mat-sys-outline-variant) 72%, transparent);
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
@@ -51,8 +47,10 @@
|
|||||||
gap: 0.75rem;
|
gap: 0.75rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.share-search-field {
|
.detail-actions {
|
||||||
width: 100%;
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 0.5rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.share-results,
|
.share-results,
|
||||||
@@ -108,10 +106,6 @@
|
|||||||
margin-right: 0.5rem;
|
margin-right: 0.5rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.editor-card mat-card-header button {
|
|
||||||
flex: 0 0 auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
.list-form,
|
.list-form,
|
||||||
.item-form {
|
.item-form {
|
||||||
display: grid;
|
display: grid;
|
||||||
@@ -126,7 +120,12 @@
|
|||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.list-form button[type='submit'],
|
.create-actions {
|
||||||
|
display: grid;
|
||||||
|
gap: 0.6rem;
|
||||||
|
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||||
|
}
|
||||||
|
|
||||||
.item-form button[type='submit'] {
|
.item-form button[type='submit'] {
|
||||||
min-height: 48px;
|
min-height: 48px;
|
||||||
}
|
}
|
||||||
@@ -236,10 +235,6 @@
|
|||||||
color: var(--mat-sys-on-surface-variant);
|
color: var(--mat-sys-on-surface-variant);
|
||||||
}
|
}
|
||||||
|
|
||||||
.secondary-back {
|
|
||||||
justify-self: start;
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (min-width: 701px) {
|
@media (min-width: 701px) {
|
||||||
.list-detail-page {
|
.list-detail-page {
|
||||||
gap: 1.25rem;
|
gap: 1.25rem;
|
||||||
@@ -256,3 +251,9 @@
|
|||||||
align-items: center;
|
align-items: center;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@media (max-width: 520px) {
|
||||||
|
.create-actions {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -64,6 +64,7 @@ export class ListDetailComponent implements OnInit {
|
|||||||
protected readonly isCreateMode = signal(false);
|
protected readonly isCreateMode = signal(false);
|
||||||
protected readonly loading = signal(true);
|
protected readonly loading = signal(true);
|
||||||
protected readonly saving = signal(false);
|
protected readonly saving = signal(false);
|
||||||
|
protected readonly creatingWithAi = signal(false);
|
||||||
protected readonly creatingTemplate = signal(false);
|
protected readonly creatingTemplate = signal(false);
|
||||||
protected readonly deletingList = signal(false);
|
protected readonly deletingList = signal(false);
|
||||||
protected readonly editing = signal(false);
|
protected readonly editing = signal(false);
|
||||||
@@ -163,11 +164,7 @@ export class ListDetailComponent implements OnInit {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const formValue = this.listForm.getRawValue();
|
const payload = this.listFormPayload();
|
||||||
const payload = {
|
|
||||||
name: formValue.name.trim(),
|
|
||||||
description: formValue.description.trim() || undefined,
|
|
||||||
};
|
|
||||||
const saveRequest =
|
const saveRequest =
|
||||||
this.isCreateMode() || !listId
|
this.isCreateMode() || !listId
|
||||||
? this.listsService.createList(payload)
|
? this.listsService.createList(payload)
|
||||||
@@ -193,8 +190,43 @@ export class ListDetailComponent implements OnInit {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected createListWithAi(): void {
|
||||||
|
if (!this.isCreateMode() || this.listForm.invalid || this.creatingWithAi()) {
|
||||||
|
this.listForm.markAllAsTouched();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.creatingWithAi.set(true);
|
||||||
|
this.suggestionsLoaded.set(false);
|
||||||
|
this.itemSuggestions.set([]);
|
||||||
|
|
||||||
|
this.listsService
|
||||||
|
.createListWithItemSuggestions(this.listFormPayload())
|
||||||
|
.pipe(finalize(() => this.creatingWithAi.set(false)))
|
||||||
|
.subscribe({
|
||||||
|
next: (response) => {
|
||||||
|
this.setList(response.list);
|
||||||
|
this.itemSuggestions.set(response.suggestions);
|
||||||
|
this.suggestionsLoaded.set(true);
|
||||||
|
this.isCreateMode.set(false);
|
||||||
|
this.editing.set(true);
|
||||||
|
this.snackBar.open('Liste mit KI erstellt.', 'OK', {
|
||||||
|
duration: 2500,
|
||||||
|
});
|
||||||
|
void this.router.navigate(['/lists', response.list.id], {
|
||||||
|
replaceUrl: true,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
error: (error: unknown) => {
|
||||||
|
this.snackBar.open(getAuthErrorMessage(error), 'OK', {
|
||||||
|
duration: 5000,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
protected addItem(): void {
|
protected addItem(): void {
|
||||||
const listId = this.listId();
|
const listId = this.currentListId();
|
||||||
|
|
||||||
if (!listId || !this.canEditItems() || this.itemForm.invalid) {
|
if (!listId || !this.canEditItems() || this.itemForm.invalid) {
|
||||||
this.itemForm.markAllAsTouched();
|
this.itemForm.markAllAsTouched();
|
||||||
@@ -286,7 +318,7 @@ export class ListDetailComponent implements OnInit {
|
|||||||
}
|
}
|
||||||
|
|
||||||
protected loadSuggestions(): void {
|
protected loadSuggestions(): void {
|
||||||
const listId = this.listId();
|
const listId = this.currentListId();
|
||||||
|
|
||||||
if (!listId || !this.canEditItems() || this.loadingSuggestions()) {
|
if (!listId || !this.canEditItems() || this.loadingSuggestions()) {
|
||||||
return;
|
return;
|
||||||
@@ -310,7 +342,7 @@ export class ListDetailComponent implements OnInit {
|
|||||||
}
|
}
|
||||||
|
|
||||||
protected addSuggestion(suggestion: ListItemSuggestion): void {
|
protected addSuggestion(suggestion: ListItemSuggestion): void {
|
||||||
const listId = this.listId();
|
const listId = this.currentListId();
|
||||||
|
|
||||||
if (!listId || this.addingSuggestionTitle()) {
|
if (!listId || this.addingSuggestionTitle()) {
|
||||||
return;
|
return;
|
||||||
@@ -343,7 +375,7 @@ export class ListDetailComponent implements OnInit {
|
|||||||
}
|
}
|
||||||
|
|
||||||
protected toggleItem(item: UserListItem, checked: boolean): void {
|
protected toggleItem(item: UserListItem, checked: boolean): void {
|
||||||
const listId = this.listId();
|
const listId = this.currentListId();
|
||||||
const currentList = this.list();
|
const currentList = this.list();
|
||||||
|
|
||||||
if (!listId || !currentList || this.updatingItemId()) {
|
if (!listId || !currentList || this.updatingItemId()) {
|
||||||
@@ -520,6 +552,19 @@ export class ListDetailComponent implements OnInit {
|
|||||||
return this.route.snapshot.paramMap.get('listId');
|
return this.route.snapshot.paramMap.get('listId');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private currentListId(): string | null {
|
||||||
|
return this.listId() ?? this.list()?.id ?? null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private listFormPayload() {
|
||||||
|
const formValue = this.listForm.getRawValue();
|
||||||
|
|
||||||
|
return {
|
||||||
|
name: formValue.name.trim(),
|
||||||
|
description: formValue.description.trim() || undefined,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
private uncheckedFirst(items: UserListItem[]): UserListItem[] {
|
private uncheckedFirst(items: UserListItem[]): UserListItem[] {
|
||||||
return items
|
return items
|
||||||
.map((item, index) => ({ item, index }))
|
.map((item, index) => ({ item, index }))
|
||||||
|
|||||||
@@ -167,23 +167,6 @@
|
|||||||
</mat-card-content>
|
</mat-card-content>
|
||||||
|
|
||||||
<mat-card-actions align="end">
|
<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
|
<a
|
||||||
mat-button
|
mat-button
|
||||||
[routerLink]="['/lists', list.id]"
|
[routerLink]="['/lists', list.id]"
|
||||||
|
|||||||
@@ -2,20 +2,16 @@ import { DatePipe } from '@angular/common';
|
|||||||
import { Component, DestroyRef, OnInit, computed, inject, signal } from '@angular/core';
|
import { Component, DestroyRef, OnInit, computed, inject, signal } from '@angular/core';
|
||||||
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
|
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
|
||||||
import { RouterLink } from '@angular/router';
|
import { RouterLink } from '@angular/router';
|
||||||
import { finalize } from 'rxjs';
|
|
||||||
import { MatButtonModule } from '@angular/material/button';
|
import { MatButtonModule } from '@angular/material/button';
|
||||||
import { MatButtonToggleModule } from '@angular/material/button-toggle';
|
import { MatButtonToggleModule } from '@angular/material/button-toggle';
|
||||||
import { MatCardModule } from '@angular/material/card';
|
import { MatCardModule } from '@angular/material/card';
|
||||||
import { MatDialog, MatDialogModule } from '@angular/material/dialog';
|
|
||||||
import { MatFormFieldModule } from '@angular/material/form-field';
|
import { MatFormFieldModule } from '@angular/material/form-field';
|
||||||
import { MatIconModule } from '@angular/material/icon';
|
import { MatIconModule } from '@angular/material/icon';
|
||||||
import { MatInputModule } from '@angular/material/input';
|
import { MatInputModule } from '@angular/material/input';
|
||||||
import { MatProgressSpinnerModule } from '@angular/material/progress-spinner';
|
import { MatProgressSpinnerModule } from '@angular/material/progress-spinner';
|
||||||
import { MatSelectModule } from '@angular/material/select';
|
import { MatSelectModule } from '@angular/material/select';
|
||||||
import { MatSnackBar, MatSnackBarModule } from '@angular/material/snack-bar';
|
|
||||||
import { getAuthErrorMessage } from '../auth/error-message';
|
import { getAuthErrorMessage } from '../auth/error-message';
|
||||||
import { OnboardingService } from '../onboarding/onboarding.service';
|
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 { ListTemplateKind } from '../templates/templates.models';
|
||||||
import { ListRealtimeEvent, UserList, UserListItem } from './lists.models';
|
import { ListRealtimeEvent, UserList, UserListItem } from './lists.models';
|
||||||
import { ListsRealtimeService } from './lists-realtime.service';
|
import { ListsRealtimeService } from './lists-realtime.service';
|
||||||
@@ -38,28 +34,23 @@ type ListKindFilter = ListTemplateKind | 'all';
|
|||||||
MatButtonModule,
|
MatButtonModule,
|
||||||
MatButtonToggleModule,
|
MatButtonToggleModule,
|
||||||
MatCardModule,
|
MatCardModule,
|
||||||
MatDialogModule,
|
|
||||||
MatFormFieldModule,
|
MatFormFieldModule,
|
||||||
MatIconModule,
|
MatIconModule,
|
||||||
MatInputModule,
|
MatInputModule,
|
||||||
MatProgressSpinnerModule,
|
MatProgressSpinnerModule,
|
||||||
MatSelectModule,
|
MatSelectModule,
|
||||||
MatSnackBarModule,
|
|
||||||
],
|
],
|
||||||
templateUrl: './lists.component.html',
|
templateUrl: './lists.component.html',
|
||||||
styleUrls: ['../workspace-page.scss', './lists.component.scss'],
|
styleUrls: ['../workspace-page.scss', './lists.component.scss'],
|
||||||
})
|
})
|
||||||
export class ListsComponent implements OnInit {
|
export class ListsComponent implements OnInit {
|
||||||
private readonly destroyRef = inject(DestroyRef);
|
private readonly destroyRef = inject(DestroyRef);
|
||||||
private readonly dialog = inject(MatDialog);
|
|
||||||
private readonly listsService = inject(ListsService);
|
private readonly listsService = inject(ListsService);
|
||||||
private readonly listsRealtimeService = inject(ListsRealtimeService);
|
private readonly listsRealtimeService = inject(ListsRealtimeService);
|
||||||
private readonly snackBar = inject(MatSnackBar);
|
|
||||||
protected readonly onboarding = inject(OnboardingService);
|
protected readonly onboarding = inject(OnboardingService);
|
||||||
|
|
||||||
protected readonly lists = signal<UserList[]>([]);
|
protected readonly lists = signal<UserList[]>([]);
|
||||||
protected readonly loading = signal(true);
|
protected readonly loading = signal(true);
|
||||||
protected readonly deletingListId = signal<string | null>(null);
|
|
||||||
protected readonly errorMessage = signal<string | null>(null);
|
protected readonly errorMessage = signal<string | null>(null);
|
||||||
protected readonly searchTerm = signal('');
|
protected readonly searchTerm = signal('');
|
||||||
protected readonly kindFilter = signal<ListKindFilter>('all');
|
protected readonly kindFilter = signal<ListKindFilter>('all');
|
||||||
@@ -180,47 +171,6 @@ export class ListsComponent implements OnInit {
|
|||||||
this.sortOption.set('updated-desc');
|
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 {
|
private subscribeToRealtime(): void {
|
||||||
this.listsRealtimeService
|
this.listsRealtimeService
|
||||||
.events()
|
.events()
|
||||||
|
|||||||
@@ -71,6 +71,11 @@ export interface ListItemSuggestionsResponse {
|
|||||||
suggestions: ListItemSuggestion[];
|
suggestions: ListItemSuggestion[];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface CreateListWithItemSuggestionsResponse {
|
||||||
|
list: UserList;
|
||||||
|
suggestions: ListItemSuggestion[];
|
||||||
|
}
|
||||||
|
|
||||||
export interface UpdateListItemRequest {
|
export interface UpdateListItemRequest {
|
||||||
title?: string;
|
title?: string;
|
||||||
notes?: string;
|
notes?: string;
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import { of, tap, throwError } from 'rxjs';
|
|||||||
import {
|
import {
|
||||||
AddListItemRequest,
|
AddListItemRequest,
|
||||||
CreateListRequest,
|
CreateListRequest,
|
||||||
|
CreateListWithItemSuggestionsResponse,
|
||||||
ListItemSuggestionsResponse,
|
ListItemSuggestionsResponse,
|
||||||
UpdateListItemRequest,
|
UpdateListItemRequest,
|
||||||
UpdateListRequest,
|
UpdateListRequest,
|
||||||
@@ -59,6 +60,23 @@ export class ListsService {
|
|||||||
return this.http.post<UserList>(this.apiUrl, data);
|
return this.http.post<UserList>(this.apiUrl, data);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
createListWithItemSuggestions(
|
||||||
|
data: CreateListRequest,
|
||||||
|
): Observable<CreateListWithItemSuggestionsResponse> {
|
||||||
|
if (!this.onlineStatus.online()) {
|
||||||
|
return throwError(
|
||||||
|
() => new Error('Listen mit KI koennen nur online erstellt werden.'),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.http
|
||||||
|
.post<CreateListWithItemSuggestionsResponse>(
|
||||||
|
`${this.apiUrl}/with-item-suggestions`,
|
||||||
|
data,
|
||||||
|
)
|
||||||
|
.pipe(tap((response) => this.upsertCachedList(response.list)));
|
||||||
|
}
|
||||||
|
|
||||||
updateList(listId: string, data: UpdateListRequest): Observable<UserList> {
|
updateList(listId: string, data: UpdateListRequest): Observable<UserList> {
|
||||||
if (!this.onlineStatus.online()) {
|
if (!this.onlineStatus.online()) {
|
||||||
const updatedList = this.updateCachedList(listId, (list) => ({
|
const updatedList = this.updateCachedList(listId, (list) => ({
|
||||||
|
|||||||
Reference in New Issue
Block a user