From 6c20a732e7087d44280f0483f9f975ab76523c3b Mon Sep 17 00:00:00 2001 From: Tobias Messner Date: Thu, 12 Sep 2024 11:33:28 +0200 Subject: [PATCH] feat: Animate load-in of notices Animate the load-in of notices to make the layout shift less jarring. Closes #1789 It also updates the code to the latest code styles: - Stories for notice and alerts were added. - NgFor / NgIf were replaced. - The service was moved from the general services directory to a context base directory. - Notices in the NoticeWrapperService is now a BehaviourSubject. - Custom Types and Functions were replaced with the OpenAPI schema. Co-authored-by: MoritzWeber0 --- .../app/general/notice/notice.component.css | 14 ---- .../app/general/notice/notice.component.html | 18 +++-- .../app/general/notice/notice.component.ts | 8 +- .../src/app/general/notice/notice.service.ts | 28 +++++++ .../src/app/general/notice/notice.stories.ts | 38 +++++++++ .../src/app/services/notice/notice.service.ts | 65 --------------- .../alert-settings.component.html | 81 ++++++++++++------- .../alert-settings.component.ts | 66 +++++---------- .../alert-settings/alert-settings.stories.ts | 52 ++++++++++++ frontend/src/storybook/notices.ts | 27 +++++++ 10 files changed, 234 insertions(+), 163 deletions(-) create mode 100644 frontend/src/app/general/notice/notice.service.ts create mode 100644 frontend/src/app/general/notice/notice.stories.ts delete mode 100644 frontend/src/app/services/notice/notice.service.ts create mode 100644 frontend/src/app/settings/core/alert-settings/alert-settings.stories.ts create mode 100644 frontend/src/storybook/notices.ts diff --git a/frontend/src/app/general/notice/notice.component.css b/frontend/src/app/general/notice/notice.component.css index 7047d8786..bde016226 100644 --- a/frontend/src/app/general/notice/notice.component.css +++ b/frontend/src/app/general/notice/notice.component.css @@ -2,20 +2,6 @@ * SPDX-FileCopyrightText: Copyright DB InfraGO AG and contributors * SPDX-License-Identifier: Apache-2.0 */ - -p { - margin: 0px; - padding: 0px; -} - -h3 { - margin: 0px !important; -} - -.url { - color: white; -} - .primary { background-color: #004085; color: white; diff --git a/frontend/src/app/general/notice/notice.component.html b/frontend/src/app/general/notice/notice.component.html index 0e86497de..5f29a6ad8 100644 --- a/frontend/src/app/general/notice/notice.component.html +++ b/frontend/src/app/general/notice/notice.component.html @@ -3,9 +3,17 @@ ~ SPDX-License-Identifier: Apache-2.0 --> -
-
-

-

-
+
+ @for (notice of noticesWrapperService.notices$ | async; track notice.id) { +
+

+

+
+ }
diff --git a/frontend/src/app/general/notice/notice.component.ts b/frontend/src/app/general/notice/notice.component.ts index cccd1e54d..6c9e55e18 100644 --- a/frontend/src/app/general/notice/notice.component.ts +++ b/frontend/src/app/general/notice/notice.component.ts @@ -2,9 +2,9 @@ * SPDX-FileCopyrightText: Copyright DB InfraGO AG and contributors * SPDX-License-Identifier: Apache-2.0 */ -import { NgFor } from '@angular/common'; +import { AsyncPipe, NgClass } from '@angular/common'; import { Component, ViewEncapsulation } from '@angular/core'; -import { NoticeService } from '../../services/notice/notice.service'; +import { NoticeWrapperService } from 'src/app/general/notice/notice.service'; @Component({ selector: 'app-notice', @@ -12,8 +12,8 @@ import { NoticeService } from '../../services/notice/notice.service'; styleUrls: ['./notice.component.css'], encapsulation: ViewEncapsulation.None, standalone: true, - imports: [NgFor], + imports: [NgClass, AsyncPipe], }) export class NoticeComponent { - constructor(public noticeService: NoticeService) {} + constructor(public noticesWrapperService: NoticeWrapperService) {} } diff --git a/frontend/src/app/general/notice/notice.service.ts b/frontend/src/app/general/notice/notice.service.ts new file mode 100644 index 000000000..9c0012a93 --- /dev/null +++ b/frontend/src/app/general/notice/notice.service.ts @@ -0,0 +1,28 @@ +/* + * SPDX-FileCopyrightText: Copyright DB InfraGO AG and contributors + * SPDX-License-Identifier: Apache-2.0 + */ +import { Injectable } from '@angular/core'; +import { BehaviorSubject } from 'rxjs'; +import { NoticeResponse, NoticesService } from 'src/app/openapi'; + +@Injectable({ + providedIn: 'root', +}) +export class NoticeWrapperService { + private _notices = new BehaviorSubject( + undefined, + ); + public readonly notices$ = this._notices.asObservable(); + + constructor(private noticesService: NoticesService) { + this.refreshNotices(); + } + + refreshNotices(): void { + this._notices.next(undefined); + this.noticesService.getNotices().subscribe((res) => { + this._notices.next(res); + }); + } +} diff --git a/frontend/src/app/general/notice/notice.stories.ts b/frontend/src/app/general/notice/notice.stories.ts new file mode 100644 index 000000000..896439ea9 --- /dev/null +++ b/frontend/src/app/general/notice/notice.stories.ts @@ -0,0 +1,38 @@ +/* + * SPDX-FileCopyrightText: Copyright DB InfraGO AG and contributors + * SPDX-License-Identifier: Apache-2.0 + */ +import { Meta, moduleMetadata, StoryObj } from '@storybook/angular'; +import { NoticeWrapperService } from 'src/app/general/notice/notice.service'; +import { NoticeLevel } from 'src/app/openapi'; +import { mockNotice, MockNoticeWrapperService } from 'src/storybook/notices'; +import { NoticeComponent } from './notice.component'; + +const meta: Meta = { + title: 'General Components / Notices', + component: NoticeComponent, +}; + +export default meta; +type Story = StoryObj; + +export const AllLevels: Story = { + args: {}, + decorators: [ + moduleMetadata({ + providers: [ + { + provide: NoticeWrapperService, + useFactory: () => + new MockNoticeWrapperService( + Object.values(NoticeLevel).map((level) => ({ + ...mockNotice, + title: 'This is an example notice with level ' + level, + level, + })), + ), + }, + ], + }), + ], +}; diff --git a/frontend/src/app/services/notice/notice.service.ts b/frontend/src/app/services/notice/notice.service.ts deleted file mode 100644 index 81eca4975..000000000 --- a/frontend/src/app/services/notice/notice.service.ts +++ /dev/null @@ -1,65 +0,0 @@ -/* - * SPDX-FileCopyrightText: Copyright DB InfraGO AG and contributors - * SPDX-License-Identifier: Apache-2.0 - */ -import { HttpClient } from '@angular/common/http'; -import { Injectable } from '@angular/core'; -import { Observable } from 'rxjs'; -import { environment } from 'src/environments/environment'; - -@Injectable({ - providedIn: 'root', -}) -export class NoticeService { - notices: Notice[] = []; - noticeLevels = [ - 'primary', - 'secondary', - 'success', - 'danger', - 'warning', - 'info', - 'alert', - ]; - - constructor(private http: HttpClient) { - this.refreshNotices(); - } - - refreshNotices(): void { - this.getNotices().subscribe((res) => { - this.notices = res; - }); - } - - getNotices(): Observable { - return this.http.get(environment.backend_url + '/notices'); - } - - deleteNotice(id: number): Observable { - return this.http.delete(environment.backend_url + '/notices/' + id); - } - - createNotice(body: CreateNotice): Observable { - return this.http.post(environment.backend_url + '/notices', body); - } -} - -export interface Notice extends CreateNotice { - id: number; -} - -export interface CreateNotice { - level: NoticeLevel; - title: string; - message: string; -} - -export type NoticeLevel = - | 'primary' - | 'secondary' - | 'success' - | 'danger' - | 'warning' - | 'info' - | 'alert'; diff --git a/frontend/src/app/settings/core/alert-settings/alert-settings.component.html b/frontend/src/app/settings/core/alert-settings/alert-settings.component.html index 84045901a..5f90dbbea 100644 --- a/frontend/src/app/settings/core/alert-settings/alert-settings.component.html +++ b/frontend/src/app/settings/core/alert-settings/alert-settings.component.html @@ -6,7 +6,6 @@

Create new Alert

-
@@ -16,22 +15,21 @@

Create new Alert

Level - - {{ noticeLevel }} - + @for (noticeLevel of noticeLevels; track noticeLevel) { + + {{ noticeLevel }} + + } Please select a level!
Description - Please enter a description or title! + @if (message.getError("titleOrDescriptionAvailable")) { + Please enter a description or title! + }

Handle alerts

- - - - {{ notice.title }} - - {{ notice.level }} - - -

{{ notice.message }}

- -
-
- @if (!noticeService.notices.length) { - There are no existing alerts. + @if ((noticeWrapperService.notices$ | async) === undefined) { + @for (_ of [0, 1, 2]; track $index) { + + } + } @else { + + @for ( + notice of noticeWrapperService.notices$ | async; + track notice.id + ) { + + + {{ notice.title }} + + {{ notice.level }} + + +

{{ notice.message }}

+ +
+ } @empty { + There are no existing alerts. + } +
}
diff --git a/frontend/src/app/settings/core/alert-settings/alert-settings.component.ts b/frontend/src/app/settings/core/alert-settings/alert-settings.component.ts index e1dce50ee..09d4975b8 100644 --- a/frontend/src/app/settings/core/alert-settings/alert-settings.component.ts +++ b/frontend/src/app/settings/core/alert-settings/alert-settings.component.ts @@ -2,7 +2,7 @@ * SPDX-FileCopyrightText: Copyright DB InfraGO AG and contributors * SPDX-License-Identifier: Apache-2.0 */ -import { NgFor, NgIf } from '@angular/common'; +import { AsyncPipe } from '@angular/common'; import { Component } from '@angular/core'; import { AbstractControl, @@ -16,21 +16,17 @@ import { } from '@angular/forms'; import { MatButton } from '@angular/material/button'; import { MatOption } from '@angular/material/core'; -import { - MatAccordion, - MatExpansionPanel, - MatExpansionPanelHeader, - MatExpansionPanelTitle, - MatExpansionPanelDescription, -} from '@angular/material/expansion'; -import { MatFormField, MatLabel, MatError } from '@angular/material/form-field'; +import { MatExpansionModule } from '@angular/material/expansion'; +import { MatError, MatFormFieldModule } from '@angular/material/form-field'; import { MatInput } from '@angular/material/input'; import { MatSelect } from '@angular/material/select'; -import { ToastService } from 'src/app/helpers/toast/toast.service'; +import { NgxSkeletonLoaderModule } from 'ngx-skeleton-loader'; +import { NoticeWrapperService } from 'src/app/general/notice/notice.service'; import { - CreateNotice, - NoticeService, -} from 'src/app/services/notice/notice.service'; + CreateNoticeRequest, + NoticeLevel, + NoticesService, +} from 'src/app/openapi'; @Component({ selector: 'app-alert-settings', @@ -40,20 +36,15 @@ import { imports: [ FormsModule, ReactiveFormsModule, - MatFormField, - MatLabel, + MatFormFieldModule, MatInput, MatSelect, - NgFor, MatOption, MatError, - NgIf, MatButton, - MatAccordion, - MatExpansionPanel, - MatExpansionPanelHeader, - MatExpansionPanelTitle, - MatExpansionPanelDescription, + MatExpansionModule, + AsyncPipe, + NgxSkeletonLoaderModule, ], }) export class AlertSettingsComponent { @@ -70,9 +61,13 @@ export class AlertSettingsComponent { return this.createAlertForm.get('message') as FormControl; } + get noticeLevels(): string[] { + return Object.values(NoticeLevel); + } + constructor( - public noticeService: NoticeService, - private toastService: ToastService, + public noticeWrapperService: NoticeWrapperService, + private noticeService: NoticesService, ) {} titleOrDescriptionRequired(): ValidatorFn { @@ -89,20 +84,10 @@ export class AlertSettingsComponent { createNotice(): void { if (this.createAlertForm.valid) { this.noticeService - .createNotice(this.createAlertForm.value as CreateNotice) + .createNotice(this.createAlertForm.value as CreateNoticeRequest) .subscribe({ next: () => { - this.noticeService.refreshNotices(); - this.toastService.showSuccess( - 'Alert created', - this.createAlertForm.value.title as string, - ); - }, - error: () => { - this.toastService.showError( - 'Creation of alert failed', - 'Please try again', - ); + this.noticeWrapperService.refreshNotices(); }, }); } @@ -111,14 +96,7 @@ export class AlertSettingsComponent { deleteNotice(id: number): void { this.noticeService.deleteNotice(id).subscribe({ next: () => { - this.toastService.showSuccess('Alert deleted', 'ID: ' + id); - this.noticeService.refreshNotices(); - }, - error: () => { - this.toastService.showSuccess( - 'Deletion of alert failed', - 'Please try again', - ); + this.noticeWrapperService.refreshNotices(); }, }); } diff --git a/frontend/src/app/settings/core/alert-settings/alert-settings.stories.ts b/frontend/src/app/settings/core/alert-settings/alert-settings.stories.ts new file mode 100644 index 000000000..11560e458 --- /dev/null +++ b/frontend/src/app/settings/core/alert-settings/alert-settings.stories.ts @@ -0,0 +1,52 @@ +/* + * SPDX-FileCopyrightText: Copyright DB InfraGO AG and contributors + * SPDX-License-Identifier: Apache-2.0 + */ +import { Meta, moduleMetadata, StoryObj } from '@storybook/angular'; +import { NoticeWrapperService } from 'src/app/general/notice/notice.service'; +import { mockNotice, MockNoticeWrapperService } from 'src/storybook/notices'; +import { AlertSettingsComponent } from './alert-settings.component'; + +const meta: Meta = { + title: 'Settings Components / Alert Settings', + component: AlertSettingsComponent, +}; + +export default meta; +type Story = StoryObj; + +export const Loading: Story = { + args: {}, +}; + +export const NoAlerts: Story = { + args: {}, + decorators: [ + moduleMetadata({ + providers: [ + { + provide: NoticeWrapperService, + useFactory: () => new MockNoticeWrapperService([]), + }, + ], + }), + ], +}; + +export const SomeAlerts: Story = { + args: {}, + decorators: [ + moduleMetadata({ + providers: [ + { + provide: NoticeWrapperService, + useFactory: () => + new MockNoticeWrapperService([ + mockNotice, + { ...mockNotice, id: 2 }, + ]), + }, + ], + }), + ], +}; diff --git a/frontend/src/storybook/notices.ts b/frontend/src/storybook/notices.ts new file mode 100644 index 000000000..3dc37e094 --- /dev/null +++ b/frontend/src/storybook/notices.ts @@ -0,0 +1,27 @@ +/* + * SPDX-FileCopyrightText: Copyright DB InfraGO AG and contributors + * SPDX-License-Identifier: Apache-2.0 + */ +import { BehaviorSubject } from 'rxjs'; +import { NoticeWrapperService } from 'src/app/general/notice/notice.service'; +import { NoticeLevel, NoticeResponse } from 'src/app/openapi'; + +export const mockNotice: NoticeResponse = { + id: 1, + title: 'Title of the notice', + message: + 'This is the message / content of a notice. It can also contain simple HTML like links: ' + + "example.com", + level: NoticeLevel.Info, +}; + +export class MockNoticeWrapperService implements Partial { + private _notices = new BehaviorSubject( + undefined, + ); + public readonly notices$ = this._notices.asObservable(); + + constructor(notices: NoticeResponse[]) { + this._notices.next(notices); + } +}