Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: use (nice) toast instead of alert #36

Merged
merged 1 commit into from
Dec 12, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions ui/src/app/app.component.html
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
~ If not, write to the Free Software Foundation Inc.,
~ 59 Temple Place - Suite 330, Boston, MA 02111-1307 USA
-->
<ui-toast aria-live="polite" aria-atomic="true"></ui-toast>
<header>
<nav class="navbar">
<ul class="navbar-nav">
Expand Down
1 change: 1 addition & 0 deletions ui/src/app/app.component.scss
Original file line number Diff line number Diff line change
Expand Up @@ -25,4 +25,5 @@
@import "navigation";
@import "form";
@import "spacing";
@import "toast";
@import "dropdown";
12 changes: 8 additions & 4 deletions ui/src/app/app.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,14 +26,16 @@ import { RouterModule } from "@angular/router";
import { DeviceInfoComponent, OTAComponent, SettingsComponent } from "./components";
import { NgbTypeaheadModule } from "@ng-bootstrap/ng-bootstrap";
import { OBDStatesComponent } from "./components/obdStates.component";
import { ToastsComponent } from "./components/toasts.component";
import { ToastService } from "./services/toast.service";

@NgModule({
bootstrap: [AppComponent],
declarations: [
AppComponent,
DeviceInfoComponent,
SettingsComponent,
OBDStatesComponent
OBDStatesComponent,
SettingsComponent
],
imports: [
BrowserModule,
Expand All @@ -47,11 +49,13 @@ import { OBDStatesComponent } from "./components/obdStates.component";
{path: "ota", component: OTAComponent},
],
{useHash: true}
)
),
ToastsComponent
],
providers: [
provideHttpClient(withInterceptorsFromDi()),
ApiService
ApiService,
ToastService
]
})
export class AppModule {
Expand Down
4 changes: 2 additions & 2 deletions ui/src/app/components/obdStates.component.html
Original file line number Diff line number Diff line change
Expand Up @@ -18,9 +18,9 @@
<div id="states">
<div class="d-flex my-2 justify-content-end">
<div class="btn-group">
<a class="btn btn-primary" [href]="downloadHref" download="states.json" (click)="generateDownload()">&#x2B07; Download</a>
<a class="btn btn-primary" [href]="downloadHref" download="states.json" (click)="generateDownload()">&#8675; Download</a>
<div class="btn btn-secondary p-0">
<label for="states-upload" class="d-inline-block py-1 px-2">&#x2B06; Upload</label>
<label for="states-upload" class="d-inline-block py-1 px-2">&#8673; Upload</label>
<input type="file" id="states-upload" name="states-upload" accept="application/json"
(change)="onFileChange($event)" [hidden]="true"
>
Expand Down
19 changes: 14 additions & 5 deletions ui/src/app/components/obdStates.component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ import {
ValueTypes
} from "../definitions";
import { DomSanitizer } from "@angular/platform-browser";
import { ToastService } from "../services/toast.service";

export function expressionValidator(checkStates: boolean = true, allowedVariables: Array<string> = [],
allowedFunctions: Array<string> = []): ValidatorFn {
Expand Down Expand Up @@ -165,7 +166,7 @@ export class OBDStatesComponent implements OnInit {

downloadHref: any;

constructor(private $api: ApiService, private sanitizer: DomSanitizer) {
constructor(private $api: ApiService, private sanitizer: DomSanitizer, private toast: ToastService) {
this.states = new FormArray([]);
this.form = new FormGroup({states: this.states});
}
Expand Down Expand Up @@ -306,8 +307,8 @@ export class OBDStatesComponent implements OnInit {

generateDownload() {
const theJSON = JSON.stringify(this.stripEmptyProps(this.states.value));
const uri = this.sanitizer.bypassSecurityTrustUrl("data:text/json;charset=UTF-8," + encodeURIComponent(theJSON));
this.downloadHref = uri;
this.downloadHref = this.sanitizer
.bypassSecurityTrustUrl("data:text/json;charset=UTF-8," + encodeURIComponent(theJSON));
}

onFileChange(event: Event) {
Expand All @@ -331,9 +332,17 @@ export class OBDStatesComponent implements OnInit {
const states: Array<OBDState> = this.stripEmptyProps(value.states);
this.$api.updateStates(states).subscribe({
next: () => {
window.alert("States updated successfully.");
this.toast.show({
text: "States updated successfully.",
classname: "bg-success text-light",
delay: 10000
});
}, error: (err) => {
window.alert(err.message);
this.toast.show({
text: err.message,
classname: "bg-danger text-light",
delay: 10000
});
}
});
}
Expand Down
17 changes: 12 additions & 5 deletions ui/src/app/components/settings.component.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import { SettingsComponent } from "./settings.component";
import { of } from "rxjs";
import { ReactiveFormsModule } from "@angular/forms";
import { NgbTypeaheadModule } from "@ng-bootstrap/ng-bootstrap";
import { ToastService } from "../services/toast.service";

const testSettings: Settings = {
wifi: {
Expand Down Expand Up @@ -52,7 +53,8 @@ export class MockApiService {
}

describe("SettingsComponent", () => {
let component: SettingsComponent, fixture: ComponentFixture<SettingsComponent>, service: ApiService;
let component: SettingsComponent, fixture: ComponentFixture<SettingsComponent>, service: ApiService,
toast: ToastService;
const getElement: (selector: string) => HTMLElement = (selector) =>
fixture.elementRef.nativeElement.querySelector(selector);

Expand All @@ -61,17 +63,18 @@ describe("SettingsComponent", () => {
TestBed.configureTestingModule({
declarations: [SettingsComponent],
imports: [NgbTypeaheadModule, ReactiveFormsModule],
providers: [{provide: ApiService, useClass: MockApiService}],
providers: [{provide: ApiService, useClass: MockApiService}, ToastService],
teardown: {destroyAfterEach: true},
}).compileComponents();
})
);

beforeEach(inject([ApiService], (apiService: ApiService) => {
beforeEach(inject([ApiService, ToastService], (apiService: ApiService, toastService: ToastService) => {
fixture = TestBed.createComponent(SettingsComponent);
component = fixture.componentInstance;

service = apiService;
toast = toastService;

fixture.detectChanges();
}));
Expand Down Expand Up @@ -101,7 +104,7 @@ describe("SettingsComponent", () => {
});

it("should update done", () => {
spyOn(window, "alert");
const spyToast = spyOn(toast, "show");

expect(getElement("#settings")).toBeTruthy();

Expand Down Expand Up @@ -129,7 +132,11 @@ describe("SettingsComponent", () => {
expect(submitBtn.disabled).toBeFalse();
submitBtn?.click();

expect(window.alert).toHaveBeenCalledWith("Settings updated successfully.");
expect(spyToast).toHaveBeenCalledWith({
text: "Settings updated successfully.",
classname: "bg-success text-light",
delay: 10000
});
});

});
15 changes: 12 additions & 3 deletions ui/src/app/components/settings.component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ import {
OperatorFunction,
Subject
} from "rxjs";
import { ToastService } from "../services/toast.service";

@Component({
selector: "ui-settings",
Expand Down Expand Up @@ -87,7 +88,7 @@ export class SettingsComponent implements OnInit {

protected readonly locationIntervals = locationIntervals;

constructor(private $api: ApiService) {
constructor(private $api: ApiService, private toast: ToastService) {
this.wifi = new FormGroup({
ssid: new FormControl("", Validators.maxLength(64)),
password: new FormControl("", [Validators.minLength(8), Validators.maxLength(32)])
Expand Down Expand Up @@ -201,9 +202,17 @@ export class SettingsComponent implements OnInit {
if (valid) {
this.$api.updateSettings(value).subscribe({
next: () => {
window.alert("Settings updated successfully.");
this.toast.show({
text: "Settings updated successfully.",
classname: "bg-success text-light",
delay: 10000
});
}, error: (err) => {
window.alert(err.message);
this.toast.show({
text: err.message,
classname: "bg-danger text-light",
delay: 10000
});
}
});
}
Expand Down
51 changes: 51 additions & 0 deletions ui/src/app/components/toasts.component.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
/*
* This program is free software; you can use it, redistribute it
* and / or modify it under the terms of the GNU General Public License
* (GPL) as published by the Free Software Foundation; either version 3
* of the License or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program, in a file called gpl.txt or license.txt.
* If not, write to the Free Software Foundation Inc.,
* 59 Temple Place - Suite 330, Boston, MA 02111-1307 USA
*/

import { Component, inject } from "@angular/core";

import { ToastService } from "../services/toast.service";
import { NgTemplateOutlet } from "@angular/common";
import { NgbToastModule } from "@ng-bootstrap/ng-bootstrap";

@Component({
selector: "ui-toast",
standalone: true,
imports: [NgbToastModule, NgTemplateOutlet],
template: `
@for (toast of toastService.toasts; track toast) {
<ngb-toast
[class]="toast.classname"
[autohide]="true"
[delay]="toast.delay || 5000"
(hidden)="toastService.remove(toast)"
>
@if (toast.template) {
<ng-template [ngTemplateOutlet]="toast.template"></ng-template>
} @else {
{{ toast.text }}
}
</ngb-toast>
}
`,
host: {
class: "toast-container p-3",
style: "position: fixed; top: 0; right: 0; z-index: 1200"
},
})
export class ToastsComponent {
toastService = inject(ToastService);
}
42 changes: 42 additions & 0 deletions ui/src/app/services/toast.service.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
/*
* This program is free software; you can use it, redistribute it
* and / or modify it under the terms of the GNU General Public License
* (GPL) as published by the Free Software Foundation; either version 3
* of the License or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program, in a file called gpl.txt or license.txt.
* If not, write to the Free Software Foundation Inc.,
* 59 Temple Place - Suite 330, Boston, MA 02111-1307 USA
*/

import { Injectable, TemplateRef } from "@angular/core";

export interface Toast {
template?: TemplateRef<any>;
text?: string;
classname?: string;
delay?: number;
}

@Injectable({providedIn: "root"})
export class ToastService {
toasts: Array<Toast> = [];

show(toast: Toast) {
this.toasts.push(toast);
}

remove(toast: Toast) {
this.toasts = this.toasts.filter((t) => t !== toast);
}

clear() {
this.toasts.splice(0, this.toasts.length);
}
}
49 changes: 49 additions & 0 deletions ui/src/scss/toast.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
/*!
* This program is free software; you can use it, redistribute it
* and / or modify it under the terms of the GNU General Public License
* (GPL) as published by the Free Software Foundation; either version 3
* of the License or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program, in a file called gpl.txt or license.txt.
* If not, write to the Free Software Foundation Inc.,
* 59 Temple Place - Suite 330, Boston, MA 02111-1307 USA
*/

.toast {
--ui-toast-zindex: 1090;
--ui-toast-padding-x: 0.75rem;
--ui-toast-padding-y: 0.5rem;
--ui-toast-spacing: 1.5rem;
--ui-toast-max-width: 350px;
--ui-toast-font-size: 0.875rem;
--ui-toast-color: ;
--ui-toast-bg: rgba(var(--ui-body-bg-rgb), 0.85);
--ui-toast-border-width: var(--ui-border-width);
--ui-toast-border-color: var(--ui-border-color-translucent);
--ui-toast-border-radius: var(--ui-border-radius);
--ui-toast-box-shadow: var(--ui-box-shadow);
--ui-toast-header-color: var(--ui-secondary-color);
--ui-toast-header-bg: rgba(var(--ui-body-bg-rgb), 0.85);
--ui-toast-header-border-color: var(--ui-border-color-translucent);
width: var(--ui-toast-max-width);
max-width: 100%;
font-size: var(--ui-toast-font-size);
color: var(--ui-toast-color);
pointer-events: auto;
background-color: var(--ui-toast-bg);
background-clip: padding-box;
border: var(--ui-toast-border-width) solid var(--ui-toast-border-color);
box-shadow: var(--ui-toast-box-shadow);
border-radius: var(--ui-toast-border-radius);
}

.toast-body {
padding: var(--ui-toast-padding-x);
word-wrap: break-word;
}
Loading