feat: prepare skeleton loading indicator
This commit is contained in:
parent
96414c60b9
commit
5f37743019
|
|
@ -0,0 +1,23 @@
|
||||||
|
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||||
|
|
||||||
|
import { SkeletonRectComponent } from './skeleton-rect.component';
|
||||||
|
|
||||||
|
describe('SkeletonRectComponentComponent', () => {
|
||||||
|
let component: SkeletonRectComponent;
|
||||||
|
let fixture: ComponentFixture<SkeletonRectComponent>;
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
await TestBed.configureTestingModule({
|
||||||
|
imports: [SkeletonRectComponent]
|
||||||
|
})
|
||||||
|
.compileComponents();
|
||||||
|
|
||||||
|
fixture = TestBed.createComponent(SkeletonRectComponent);
|
||||||
|
component = fixture.componentInstance;
|
||||||
|
fixture.detectChanges();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should create', () => {
|
||||||
|
expect(component).toBeTruthy();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
@ -0,0 +1,57 @@
|
||||||
|
import { Component, ElementRef } from '@angular/core';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'app-skeleton-rect',
|
||||||
|
imports: [],
|
||||||
|
host: {
|
||||||
|
class: 'pulse',
|
||||||
|
},
|
||||||
|
template: `<div
|
||||||
|
role="status"
|
||||||
|
class="flex items-center justify-center h-56 max-w-sm bg-gray-300 rounded-lg animate-pulse dark:bg-gray-700"
|
||||||
|
>
|
||||||
|
<!-- <svg
|
||||||
|
class="w-10 h-10 text-gray-200 dark:text-gray-600"
|
||||||
|
aria-hidden="true"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
fill="currentColor"
|
||||||
|
viewBox="0 0 16 20"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
d="M5 5V.13a2.96 2.96 0 0 0-1.293.749L.879 3.707A2.98 2.98 0 0 0 .13 5H5Z"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
d="M14.066 0H7v5a2 2 0 0 1-2 2H0v11a1.97 1.97 0 0 0 1.934 2h12.132A1.97 1.97 0 0 0 16 18V2a1.97 1.97 0 0 0-1.934-2ZM9 13a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-2a2 2 0 0 1 2-2h2a2 2 0 0 1 2 2v2Zm4 .382a1 1 0 0 1-1.447.894L10 13v-2l1.553-1.276a1 1 0 0 1 1.447.894v2.764Z"
|
||||||
|
/>
|
||||||
|
</svg> -->
|
||||||
|
<span class="sr-only">Loading...</span>
|
||||||
|
</div>`,
|
||||||
|
styles: [
|
||||||
|
`
|
||||||
|
:host {
|
||||||
|
/* display: block;
|
||||||
|
width: var(--skeleton-rect-width);
|
||||||
|
height: var(--skeleton-rect-height);
|
||||||
|
background: rgb(239, 241, 246) no-repeat; */
|
||||||
|
}
|
||||||
|
`,
|
||||||
|
],
|
||||||
|
})
|
||||||
|
export class SkeletonRectComponent {
|
||||||
|
width?: string;
|
||||||
|
height?: string;
|
||||||
|
className?: string;
|
||||||
|
|
||||||
|
constructor(private host: ElementRef<HTMLElement>) {}
|
||||||
|
|
||||||
|
ngOnInit() {
|
||||||
|
const host = this.host.nativeElement;
|
||||||
|
|
||||||
|
if (this.className) {
|
||||||
|
host.classList.add(this.className);
|
||||||
|
}
|
||||||
|
|
||||||
|
host.style.setProperty('--skeleton-rect-width', this.width ?? '100%');
|
||||||
|
host.style.setProperty('--skeleton-rect-height', this.height ?? '20px');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,43 @@
|
||||||
|
import {
|
||||||
|
Directive,
|
||||||
|
Input,
|
||||||
|
TemplateRef,
|
||||||
|
ViewContainerRef,
|
||||||
|
SimpleChanges,
|
||||||
|
} from '@angular/core';
|
||||||
|
import { SkeletonRectComponent } from './skeleton-rect.component';
|
||||||
|
|
||||||
|
function random(min: number, max: number): number {
|
||||||
|
return Math.floor(Math.random() * (max - min + 1)) + min;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Directive({ selector: '[skeleton]' })
|
||||||
|
export class SkeletonDirective {
|
||||||
|
@Input('skeleton') isLoading = false;
|
||||||
|
@Input('skeletonRepeat') size = 1;
|
||||||
|
@Input('skeletonWidth') width?: string;
|
||||||
|
@Input('skeletonHeight') height?: string;
|
||||||
|
@Input('skeletonClassName') className?: string;
|
||||||
|
|
||||||
|
constructor(private tpl: TemplateRef<any>, private vcr: ViewContainerRef) {}
|
||||||
|
|
||||||
|
ngOnChanges(changes: SimpleChanges) {
|
||||||
|
if (changes['isLoading']) {
|
||||||
|
this.vcr.clear();
|
||||||
|
|
||||||
|
if (changes['isLoading'].currentValue) {
|
||||||
|
Array.from({ length: this.size }).forEach(() => {
|
||||||
|
const ref = this.vcr.createComponent(SkeletonRectComponent);
|
||||||
|
|
||||||
|
Object.assign(ref.instance, {
|
||||||
|
width: this.width === 'rand' ? `${random(30, 90)}%` : this.width,
|
||||||
|
height: this.height,
|
||||||
|
className: this.className,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
this.vcr.createEmbeddedView(this.tpl);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,11 +1,10 @@
|
||||||
import { ErrorHandler, Injectable } from '@angular/core';
|
import { ErrorHandler, Injectable } from '@angular/core';
|
||||||
import { faro } from '@grafana/faro-web-sdk';
|
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class GlobalErrorHandler implements ErrorHandler {
|
export class GlobalErrorHandler implements ErrorHandler {
|
||||||
handleError(error: any) {
|
handleError(error: any) {
|
||||||
if (error instanceof Error) {
|
if (error instanceof Error) {
|
||||||
faro.api.pushError(error);
|
// faro.api.pushError(error);
|
||||||
}
|
}
|
||||||
console.error(error);
|
console.error(error);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -11,6 +11,7 @@ import {
|
||||||
import { ManagerInfoCardComponent } from '../manager/components/manager-info-card/manager-info-card.component';
|
import { ManagerInfoCardComponent } from '../manager/components/manager-info-card/manager-info-card.component';
|
||||||
import { TicketStateInfoCardComponent } from '../ticket-system/ticket-state-info-card/ticket-state-info-card.component';
|
import { TicketStateInfoCardComponent } from '../ticket-system/ticket-state-info-card/ticket-state-info-card.component';
|
||||||
import { LinkCardComponent } from './link-card/link-card.component';
|
import { LinkCardComponent } from './link-card/link-card.component';
|
||||||
|
import { SkeletonDirective } from '../../core/components/skeleton-rect/skeleton.directive';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-home',
|
selector: 'app-home',
|
||||||
|
|
@ -21,6 +22,7 @@ import { LinkCardComponent } from './link-card/link-card.component';
|
||||||
LinkCardComponent,
|
LinkCardComponent,
|
||||||
MatDividerModule,
|
MatDividerModule,
|
||||||
CurrencyPipe,
|
CurrencyPipe,
|
||||||
|
SkeletonDirective,
|
||||||
],
|
],
|
||||||
template: `
|
template: `
|
||||||
<div class="flex flex-col m-0 md:m-4">
|
<div class="flex flex-col m-0 md:m-4">
|
||||||
|
|
@ -196,12 +198,14 @@ export class HomeComponent implements OnInit {
|
||||||
};
|
};
|
||||||
}, {} as Record<string, number[]>);
|
}, {} as Record<string, number[]>);
|
||||||
|
|
||||||
return Object.entries(groupedByOrdinanceType).map(([ot, revenues]) => {
|
return Object.entries(groupedByOrdinanceType)
|
||||||
return {
|
.sort(([ot1], [ot2]) => ot1.localeCompare(ot2))
|
||||||
label: ot,
|
.map(([ot, revenues]) => {
|
||||||
value: revenues.at(-1), //.reduce((sum, cur) => (sum += cur), 0),
|
return {
|
||||||
};
|
label: ot,
|
||||||
});
|
value: revenues.at(-1), //.reduce((sum, cur) => (sum += cur), 0),
|
||||||
|
};
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
constructor(private readonly homeDashboardGql: HomeDashboardGQL) {
|
constructor(private readonly homeDashboardGql: HomeDashboardGQL) {
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,6 @@
|
||||||
|
<!-- <ng-container
|
||||||
|
*skeleton="isLoading(); repeat: 5; className: 'mr-2'; height: '50px'"
|
||||||
|
></ng-container> -->
|
||||||
@if (kpi()?.main?.label) {
|
@if (kpi()?.main?.label) {
|
||||||
<app-info-card
|
<app-info-card
|
||||||
[chartValue]="kpi()?.main?.value!"
|
[chartValue]="kpi()?.main?.value!"
|
||||||
|
|
|
||||||
|
|
@ -7,27 +7,31 @@ import {
|
||||||
Metric,
|
Metric,
|
||||||
} from '../../../../core/data-access/graphql/generated/generated';
|
} from '../../../../core/data-access/graphql/generated/generated';
|
||||||
import { ManagerDashboardService } from '../../manager-dashboard.service';
|
import { ManagerDashboardService } from '../../manager-dashboard.service';
|
||||||
|
import { SkeletonDirective } from '../../../../core/components/skeleton-rect/skeleton.directive';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-manager-info-card',
|
selector: 'app-manager-info-card',
|
||||||
imports: [InfoCardModule, DecimalPipe],
|
imports: [InfoCardModule, DecimalPipe, SkeletonDirective],
|
||||||
templateUrl: './manager-info-card.component.html',
|
templateUrl: './manager-info-card.component.html',
|
||||||
styleUrl: './manager-info-card.component.scss',
|
styleUrl: './manager-info-card.component.scss',
|
||||||
})
|
})
|
||||||
export class ManagerInfoCardComponent {
|
export class ManagerInfoCardComponent {
|
||||||
private service = inject(ManagerDashboardService);
|
private service = inject(ManagerDashboardService);
|
||||||
|
|
||||||
|
isLoading = signal<boolean>(false);
|
||||||
ordinanceType = input.required<string>();
|
ordinanceType = input.required<string>();
|
||||||
kpi = signal<KpiInfo | undefined>(undefined);
|
kpi = signal<KpiInfo | undefined>(undefined);
|
||||||
metrics = signal<Metric[]>([]);
|
metrics = signal<Metric[]>([]);
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
effect(() => {
|
effect(() => {
|
||||||
|
this.isLoading.set(true);
|
||||||
this.service
|
this.service
|
||||||
.fetchManagerKpi(this.ordinanceType())
|
.fetchManagerKpi(this.ordinanceType())
|
||||||
.pipe(
|
.pipe(
|
||||||
tap((res) => this.metrics.set(res.additional)),
|
tap((res) => this.metrics.set(res.additional)),
|
||||||
tap((res) => this.kpi.set(res))
|
tap((res) => this.kpi.set(res)),
|
||||||
|
tap(() => this.isLoading.set(false))
|
||||||
)
|
)
|
||||||
.subscribe();
|
.subscribe();
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -4,33 +4,16 @@
|
||||||
(filterChange)="filterChanged($event)"
|
(filterChange)="filterChanged($event)"
|
||||||
></dks-ticket-lane-header>
|
></dks-ticket-lane-header>
|
||||||
@if (tickets$ | async; as tickets) {
|
@if (tickets$ | async; as tickets) {
|
||||||
<cdk-virtual-scroll-viewport
|
<cdk-virtual-scroll-viewport
|
||||||
itemSize="104"
|
itemSize="104"
|
||||||
(scrolledIndexChange)="fetchNextPage()"
|
(scrolledIndexChange)="fetchNextPage()"
|
||||||
>
|
>
|
||||||
<dks-ticket-item
|
<dks-ticket-item
|
||||||
*cdkVirtualFor="let ticket of tickets; trackBy: trackByFn"
|
*cdkVirtualFor="let ticket of tickets; trackBy: trackByFn"
|
||||||
class="mat-elevation-z4 animate-fadeIn"
|
class="mat-elevation-z4 animate-fadeIn"
|
||||||
(click)="openTicketView(ticket)"
|
(click)="openTicketView(ticket)"
|
||||||
[ticket]="ticket"
|
[ticket]="ticket"
|
||||||
>
|
>
|
||||||
</dks-ticket-item>
|
</dks-ticket-item>
|
||||||
</cdk-virtual-scroll-viewport>
|
</cdk-virtual-scroll-viewport>
|
||||||
<!-- <ng-container *ngIf="ticketStateMeta$ | async as ticketStateMeta">
|
|
||||||
<dks-ticket-lane-footer
|
|
||||||
*dksRole="['manager', 'admin']"
|
|
||||||
[ticketStateMeta]="ticketStateMeta"
|
|
||||||
[withTicketCount]="true"
|
|
||||||
[withApprovalRequiredCount]="
|
|
||||||
ticketState === TicketState.UserInteractionRequired
|
|
||||||
"
|
|
||||||
[withApprovalUnknownCount]="
|
|
||||||
ticketState === TicketState.UserInteractionRequired
|
|
||||||
"
|
|
||||||
></dks-ticket-lane-footer>
|
|
||||||
<dks-ticket-lane-footer
|
|
||||||
*dksRole="['controlcenter']"
|
|
||||||
[ticketStateMeta]="ticketStateMeta"
|
|
||||||
></dks-ticket-lane-footer
|
|
||||||
></ng-container> -->
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -52,8 +52,8 @@ import { TicketLaneHeaderComponent } from './ticket-lane-header.component';
|
||||||
TicketLaneHeaderComponent,
|
TicketLaneHeaderComponent,
|
||||||
TicketItemComponent,
|
TicketItemComponent,
|
||||||
AsyncPipe,
|
AsyncPipe,
|
||||||
ScrollingModule
|
ScrollingModule,
|
||||||
],
|
],
|
||||||
})
|
})
|
||||||
export class TicketLaneComponent {
|
export class TicketLaneComponent {
|
||||||
private dialog = inject(DialogService);
|
private dialog = inject(DialogService);
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,6 @@
|
||||||
|
<!-- <ng-container
|
||||||
|
*skeleton="isLoading(); repeat: 5; className: 'mr-2'; height: '50px'"
|
||||||
|
></ng-container> -->
|
||||||
@if (kpi()?.main?.label) {
|
@if (kpi()?.main?.label) {
|
||||||
<app-info-card
|
<app-info-card
|
||||||
[chartValue]="kpi()?.main?.value!"
|
[chartValue]="kpi()?.main?.value!"
|
||||||
|
|
|
||||||
|
|
@ -8,27 +8,31 @@ import {
|
||||||
TicketValidationState,
|
TicketValidationState,
|
||||||
} from '../../../core/data-access/graphql/generated/generated';
|
} from '../../../core/data-access/graphql/generated/generated';
|
||||||
import { TicketsService } from '../tickets.service';
|
import { TicketsService } from '../tickets.service';
|
||||||
|
import { SkeletonDirective } from '../../../core/components/skeleton-rect/skeleton.directive';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-ticket-state-info-card',
|
selector: 'app-ticket-state-info-card',
|
||||||
imports: [InfoCardModule, DecimalPipe],
|
imports: [InfoCardModule, DecimalPipe, SkeletonDirective],
|
||||||
templateUrl: './ticket-state-info-card.component.html',
|
templateUrl: './ticket-state-info-card.component.html',
|
||||||
styleUrl: './ticket-state-info-card.component.scss',
|
styleUrl: './ticket-state-info-card.component.scss',
|
||||||
})
|
})
|
||||||
export class TicketStateInfoCardComponent {
|
export class TicketStateInfoCardComponent {
|
||||||
private service = inject(TicketsService);
|
private service = inject(TicketsService);
|
||||||
|
|
||||||
|
isLoading = signal<boolean>(false);
|
||||||
ticketState = input.required<TicketValidationState>();
|
ticketState = input.required<TicketValidationState>();
|
||||||
kpi = signal<KpiInfo | undefined>(undefined);
|
kpi = signal<KpiInfo | undefined>(undefined);
|
||||||
metrics = signal<Metric[]>([]);
|
metrics = signal<Metric[]>([]);
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
effect(() => {
|
effect(() => {
|
||||||
|
this.isLoading.set(true);
|
||||||
this.service
|
this.service
|
||||||
.fetchTicketStateKpi(this.ticketState())
|
.fetchTicketStateKpi(this.ticketState())
|
||||||
.pipe(
|
.pipe(
|
||||||
tap((res) => this.metrics.set(res.additional)),
|
tap((res) => this.metrics.set(res.additional)),
|
||||||
tap((res) => this.kpi.set(res))
|
tap((res) => this.kpi.set(res)),
|
||||||
|
tap(() => this.isLoading.set(false))
|
||||||
)
|
)
|
||||||
.subscribe();
|
.subscribe();
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -44,11 +44,6 @@ import { TicketsSearchDialogComponent } from './tickets-search-dialog/tickets-se
|
||||||
class="overflow-y-auto mat-elevation-z2"
|
class="overflow-y-auto mat-elevation-z2"
|
||||||
[ticketState]="TicketState.DocumentsMissing"
|
[ticketState]="TicketState.DocumentsMissing"
|
||||||
></dks-ticket-lane>
|
></dks-ticket-lane>
|
||||||
<!-- <dks-ticket-lane
|
|
||||||
*dksRole="['manager', 'admin']"
|
|
||||||
class="overflow-y-auto mat-elevation-z2"
|
|
||||||
[ticketState]="TicketState.Archived"
|
|
||||||
></dks-ticket-lane> -->
|
|
||||||
</section>`,
|
</section>`,
|
||||||
styles: [
|
styles: [
|
||||||
`
|
`
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
/* You can add global styles to this file, and also import other style files */
|
/* You can add global styles to this file, and also import other style files */
|
||||||
@use '@angular/material' as mat;
|
@use "@angular/material" as mat;
|
||||||
|
|
||||||
html,
|
html,
|
||||||
body {
|
body {
|
||||||
|
|
@ -11,3 +11,20 @@ body {
|
||||||
background: var(--mat-sys-surface-dim);
|
background: var(--mat-sys-surface-dim);
|
||||||
color: var(--mat-sys-on-surface);
|
color: var(--mat-sys-on-surface);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.pulse {
|
||||||
|
animation: pulse 1.5s cubic-bezier(0.4, 0, 0.2, 1) infinite;
|
||||||
|
animation-delay: 0.5s;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes pulse {
|
||||||
|
0% {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
50% {
|
||||||
|
opacity: 0.5;
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue