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 { faro } from '@grafana/faro-web-sdk';
|
||||
|
||||
@Injectable()
|
||||
export class GlobalErrorHandler implements ErrorHandler {
|
||||
handleError(error: any) {
|
||||
if (error instanceof Error) {
|
||||
faro.api.pushError(error);
|
||||
// faro.api.pushError(error);
|
||||
}
|
||||
console.error(error);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@ import {
|
|||
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 { LinkCardComponent } from './link-card/link-card.component';
|
||||
import { SkeletonDirective } from '../../core/components/skeleton-rect/skeleton.directive';
|
||||
|
||||
@Component({
|
||||
selector: 'app-home',
|
||||
|
|
@ -21,6 +22,7 @@ import { LinkCardComponent } from './link-card/link-card.component';
|
|||
LinkCardComponent,
|
||||
MatDividerModule,
|
||||
CurrencyPipe,
|
||||
SkeletonDirective,
|
||||
],
|
||||
template: `
|
||||
<div class="flex flex-col m-0 md:m-4">
|
||||
|
|
@ -196,7 +198,9 @@ export class HomeComponent implements OnInit {
|
|||
};
|
||||
}, {} as Record<string, number[]>);
|
||||
|
||||
return Object.entries(groupedByOrdinanceType).map(([ot, revenues]) => {
|
||||
return Object.entries(groupedByOrdinanceType)
|
||||
.sort(([ot1], [ot2]) => ot1.localeCompare(ot2))
|
||||
.map(([ot, revenues]) => {
|
||||
return {
|
||||
label: ot,
|
||||
value: revenues.at(-1), //.reduce((sum, cur) => (sum += cur), 0),
|
||||
|
|
|
|||
|
|
@ -1,3 +1,6 @@
|
|||
<!-- <ng-container
|
||||
*skeleton="isLoading(); repeat: 5; className: 'mr-2'; height: '50px'"
|
||||
></ng-container> -->
|
||||
@if (kpi()?.main?.label) {
|
||||
<app-info-card
|
||||
[chartValue]="kpi()?.main?.value!"
|
||||
|
|
|
|||
|
|
@ -7,27 +7,31 @@ import {
|
|||
Metric,
|
||||
} from '../../../../core/data-access/graphql/generated/generated';
|
||||
import { ManagerDashboardService } from '../../manager-dashboard.service';
|
||||
import { SkeletonDirective } from '../../../../core/components/skeleton-rect/skeleton.directive';
|
||||
|
||||
@Component({
|
||||
selector: 'app-manager-info-card',
|
||||
imports: [InfoCardModule, DecimalPipe],
|
||||
imports: [InfoCardModule, DecimalPipe, SkeletonDirective],
|
||||
templateUrl: './manager-info-card.component.html',
|
||||
styleUrl: './manager-info-card.component.scss',
|
||||
})
|
||||
export class ManagerInfoCardComponent {
|
||||
private service = inject(ManagerDashboardService);
|
||||
|
||||
isLoading = signal<boolean>(false);
|
||||
ordinanceType = input.required<string>();
|
||||
kpi = signal<KpiInfo | undefined>(undefined);
|
||||
metrics = signal<Metric[]>([]);
|
||||
|
||||
constructor() {
|
||||
effect(() => {
|
||||
this.isLoading.set(true);
|
||||
this.service
|
||||
.fetchManagerKpi(this.ordinanceType())
|
||||
.pipe(
|
||||
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();
|
||||
});
|
||||
|
|
|
|||
|
|
@ -4,10 +4,10 @@
|
|||
(filterChange)="filterChanged($event)"
|
||||
></dks-ticket-lane-header>
|
||||
@if (tickets$ | async; as tickets) {
|
||||
<cdk-virtual-scroll-viewport
|
||||
<cdk-virtual-scroll-viewport
|
||||
itemSize="104"
|
||||
(scrolledIndexChange)="fetchNextPage()"
|
||||
>
|
||||
>
|
||||
<dks-ticket-item
|
||||
*cdkVirtualFor="let ticket of tickets; trackBy: trackByFn"
|
||||
class="mat-elevation-z4 animate-fadeIn"
|
||||
|
|
@ -15,22 +15,5 @@
|
|||
[ticket]="ticket"
|
||||
>
|
||||
</dks-ticket-item>
|
||||
</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> -->
|
||||
</cdk-virtual-scroll-viewport>
|
||||
}
|
||||
|
|
|
|||
|
|
@ -52,8 +52,8 @@ import { TicketLaneHeaderComponent } from './ticket-lane-header.component';
|
|||
TicketLaneHeaderComponent,
|
||||
TicketItemComponent,
|
||||
AsyncPipe,
|
||||
ScrollingModule
|
||||
],
|
||||
ScrollingModule,
|
||||
],
|
||||
})
|
||||
export class TicketLaneComponent {
|
||||
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) {
|
||||
<app-info-card
|
||||
[chartValue]="kpi()?.main?.value!"
|
||||
|
|
|
|||
|
|
@ -8,27 +8,31 @@ import {
|
|||
TicketValidationState,
|
||||
} from '../../../core/data-access/graphql/generated/generated';
|
||||
import { TicketsService } from '../tickets.service';
|
||||
import { SkeletonDirective } from '../../../core/components/skeleton-rect/skeleton.directive';
|
||||
|
||||
@Component({
|
||||
selector: 'app-ticket-state-info-card',
|
||||
imports: [InfoCardModule, DecimalPipe],
|
||||
imports: [InfoCardModule, DecimalPipe, SkeletonDirective],
|
||||
templateUrl: './ticket-state-info-card.component.html',
|
||||
styleUrl: './ticket-state-info-card.component.scss',
|
||||
})
|
||||
export class TicketStateInfoCardComponent {
|
||||
private service = inject(TicketsService);
|
||||
|
||||
isLoading = signal<boolean>(false);
|
||||
ticketState = input.required<TicketValidationState>();
|
||||
kpi = signal<KpiInfo | undefined>(undefined);
|
||||
metrics = signal<Metric[]>([]);
|
||||
|
||||
constructor() {
|
||||
effect(() => {
|
||||
this.isLoading.set(true);
|
||||
this.service
|
||||
.fetchTicketStateKpi(this.ticketState())
|
||||
.pipe(
|
||||
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();
|
||||
});
|
||||
|
|
|
|||
|
|
@ -44,11 +44,6 @@ import { TicketsSearchDialogComponent } from './tickets-search-dialog/tickets-se
|
|||
class="overflow-y-auto mat-elevation-z2"
|
||||
[ticketState]="TicketState.DocumentsMissing"
|
||||
></dks-ticket-lane>
|
||||
<!-- <dks-ticket-lane
|
||||
*dksRole="['manager', 'admin']"
|
||||
class="overflow-y-auto mat-elevation-z2"
|
||||
[ticketState]="TicketState.Archived"
|
||||
></dks-ticket-lane> -->
|
||||
</section>`,
|
||||
styles: [
|
||||
`
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
/* 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,
|
||||
body {
|
||||
|
|
@ -11,3 +11,20 @@ body {
|
|||
background: var(--mat-sys-surface-dim);
|
||||
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