Revamp history-sidebar-widget

This commit is contained in:
Clemente Raposo 2024-12-10 23:21:43 +00:00 committed by Jack Anderson
parent 8834ba63ec
commit bbe3b175b1
9 changed files with 471 additions and 51 deletions

View file

@ -0,0 +1,283 @@
<div class="history-sidebar-skeleton-loading history-timeline-viewport">
<div class="" style="">
<div class="d-flex entry-grey flex-row history-timeline-entry m-2 ">
<div class="history-timeline-entry-icon">
<div class="rounded-square icon-square box-loading">
<div class="d-flex justify-content-center align-items-center h-100 history-timeline-image">
</div>
</div>
</div>
<div class="flex-grow-1">
<div class="card">
<div class="card-body p-1 pr-2 pl-2">
<p class="card-title text-break history-timeline-entry-title pb-2">
<a class="">
<div mode="list" class="field field-mode-list field-type-varchar">
<div class="dynamic-field dynamic-field-mode-list dynamic-field-type-varchar ">
<div class="box-loading medium-size-text-skeleton large-length-text-skeleton rounded skeleton-field-content">
</div>
</div>
</div>
</a>
</p>
<div class="card-text history-timeline-entry-user pb-2 text-uppercase">
<small class="text-break">
<div mode="list" class="field field-mode-list field-type-varchar">
<div class="dynamic-field dynamic-field-mode-list dynamic-field-type-varchar ">
<div class="box-loading small-size-text-skeleton small-length-text-skeleton rounded skeleton-field-content">
</div>
</div>
</div>
</small>
</div>
<div class="card-text text-break history-timeline-entry-date pb-1">
<small class="font-italic">
<div mode="list" class="field field-mode-list field-type-datetime">
<div class="dynamic-field dynamic-field-mode-list dynamic-field-type-datetime ">
<div class="box-loading small-size-text-skeleton medium-length-text-skeleton rounded skeleton-field-content">
<span class=""></span>
</div>
</div>
</div>
</small>
</div>
</div>
</div>
</div>
</div>
</div>
<div class="" style="">
<div class="d-flex entry-grey flex-row history-timeline-entry m-2 ">
<div class="history-timeline-entry-icon">
<div class="rounded-square icon-square box-loading">
<div class="d-flex justify-content-center align-items-center h-100 history-timeline-image">
</div>
</div>
</div>
<div class="flex-grow-1">
<div class="card">
<div class="card-body p-1 pr-2 pl-2">
<p class="card-title text-break history-timeline-entry-title pb-2 box-loading">
<a class="">
<span mode="list" class="field field-mode-list field-type-varchar">
<div class="dynamic-field dynamic-field-mode-list dynamic-field-type-varchar ">
<div class="box-loading medium-size-text-skeleton large-length-text-skeleton rounded skeleton-field-content">
</div>
</div>
</span>
</a></p>
<div class="card-text history-timeline-entry-user pb-2 text-uppercase">
<small class="text-break">
<div mode="list" class="field field-mode-list field-type-varchar">
<div class="dynamic-field dynamic-field-mode-list dynamic-field-type-varchar ">
<div class="box-loading small-size-text-skeleton small-length-text-skeleton rounded skeleton-field-content">
</div>
</div>
</div>
</small>
</div>
<div class="card-text text-break history-timeline-entry-date pb-1 ">
<small class="font-italic">
<span mode="list" class="field field-mode-list field-type-datetime">
<div class="dynamic-field dynamic-field-mode-list dynamic-field-type-datetime ">
<div class="box-loading small-size-text-skeleton medium-length-text-skeleton rounded skeleton-field-content">
<span class=""> </span>
</div>
</div>
</span>
</small>
</div>
</div>
</div>
</div>
</div>
</div>
<div class="" style="">
<div class="d-flex entry-grey flex-row history-timeline-entry m-2 ">
<div class="history-timeline-entry-icon">
<div class="rounded-square icon-square box-loading">
<div class="d-flex justify-content-center align-items-center h-100 history-timeline-image">
</div>
</div>
</div>
<div class="flex-grow-1">
<div class="card">
<div class="card-body p-1 pr-2 pl-2">
<p class="card-title text-break history-timeline-entry-title pb-2"><a
class="">
<div mode="list" class="field field-mode-list field-type-varchar">
<div class="dynamic-field dynamic-field-mode-list dynamic-field-type-varchar ">
<div class="box-loading medium-size-text-skeleton large-length-text-skeleton rounded skeleton-field-content">
</div>
</div>
</div>
</a></p>
<div class="card-text history-timeline-entry-user pb-2 text-uppercase">
<small class="text-break">
<div mode="list" class="field field-mode-list field-type-varchar">
<div class="dynamic-field dynamic-field-mode-list dynamic-field-type-varchar ">
<div class="box-loading small-size-text-skeleton small-length-text-skeleton rounded skeleton-field-content">
</div>
</div>
</div>
</small>
</div>
<div class="card-text text-break history-timeline-entry-date pb-1">
<small class="font-italic">
<div mode="list" class="field field-mode-list field-type-datetime">
<div class="dynamic-field dynamic-field-mode-list dynamic-field-type-datetime ">
<div class="box-loading small-size-text-skeleton medium-length-text-skeleton rounded skeleton-field-content">
<span class=""> </span>
</div>
</div>
</div>
</small>
</div>
</div>
</div>
</div>
</div>
</div>
<div class="" style="">
<div class="d-flex entry-grey flex-row history-timeline-entry m-2 ">
<div class="history-timeline-entry-icon">
<div class="rounded-square icon-square box-loading">
<div class="d-flex justify-content-center align-items-center h-100 history-timeline-image">
</div>
</div>
</div>
<div class="flex-grow-1">
<div class="card">
<div class="card-body p-1 pr-2 pl-2">
<p class="card-title text-break history-timeline-entry-title pb-2"><a
class="">
<div mode="list" class="field field-mode-list field-type-varchar">
<div class="dynamic-field dynamic-field-mode-list dynamic-field-type-varchar ">
<div class="box-loading medium-size-text-skeleton large-length-text-skeleton rounded skeleton-field-content">
</div>
</div>
</div>
</a></p>
<div class="card-text history-timeline-entry-user pb-2 text-uppercase">
<small
class="text-break">
<div mode="list" class="field field-mode-list field-type-varchar">
<div class="dynamic-field dynamic-field-mode-list dynamic-field-type-varchar ">
<div class="box-loading small-size-text-skeleton small-length-text-skeleton rounded skeleton-field-content">
</div>
</div>
</div>
</small>
</div>
<div class="card-text text-break history-timeline-entry-date pb-1">
<small
class="font-italic">
<div mode="list" class="field field-mode-list field-type-datetime">
<div class="dynamic-field dynamic-field-mode-list dynamic-field-type-datetime ">
<div class="box-loading small-size-text-skeleton medium-length-text-skeleton rounded skeleton-field-content">
<span class=""> </span>
</div>
</div>
</div>
</small>
</div>
</div>
</div>
</div>
</div>
</div>
<div class="" style="">
<div class="d-flex entry-grey flex-row history-timeline-entry m-2 ">
<div class="history-timeline-entry-icon">
<div class="rounded-square icon-square box-loading">
<div class="d-flex justify-content-center align-items-center h-100 history-timeline-image">
</div>
</div>
</div>
<div class="flex-grow-1">
<div class="card">
<div class="card-body p-1 pr-2 pl-2">
<p class="card-title text-break history-timeline-entry-title pb-2"><a
class="">
<div mode="list" class="field field-mode-list field-type-varchar">
<div class="dynamic-field dynamic-field-mode-list dynamic-field-type-varchar ">
<div class="box-loading medium-size-text-skeleton large-length-text-skeleton rounded skeleton-field-content">
</div>
</div>
</div>
</a></p>
<div class="card-text history-timeline-entry-user pb-2 text-uppercase">
<small
class="text-break">
<div mode="list" class="field field-mode-list field-type-varchar">
<div class="dynamic-field dynamic-field-mode-list dynamic-field-type-varchar ">
<div class="box-loading small-size-text-skeleton small-length-text-skeleton rounded skeleton-field-content">
</div>
</div>
</div>
</small>
</div>
<div class="card-text text-break history-timeline-entry-date pb-1">
<small class="font-italic">
<div mode="list" class="field field-mode-list field-type-datetime">
<div class="dynamic-field dynamic-field-mode-list dynamic-field-type-datetime ">
<div class="box-loading small-size-text-skeleton medium-length-text-skeleton rounded skeleton-field-content">
<span class=""> </span>
</div>
</div>
</div>
</small>
</div>
</div>
</div>
</div>
</div>
</div>
<div class="" style="">
<div class="d-flex entry-grey flex-row history-timeline-entry m-2 ">
<div class="history-timeline-entry-icon">
<div class="rounded-square icon-square box-loading">
<div class="d-flex justify-content-center align-items-center h-100 history-timeline-image">
</div>
</div>
</div>
<div class="flex-grow-1">
<div class="card">
<div class="card-body p-1 pr-2 pl-2">
<p class="card-title text-break history-timeline-entry-title pb-2">
<a class="">
<div mode="list" class="field field-mode-list field-type-varchar">
<div class="dynamic-field dynamic-field-mode-list dynamic-field-type-varchar ">
<div class="box-loading medium-size-text-skeleton large-length-text-skeleton rounded skeleton-field-content">
</div>
</div>
</div>
</a>
</p>
<div class="card-text history-timeline-entry-user pb-2 text-uppercase">
<small class="text-break">
<div mode="list" class="field field-mode-list field-type-varchar">
<div class="dynamic-field dynamic-field-mode-list dynamic-field-type-varchar ">
<div class="box-loading small-size-text-skeleton small-length-text-skeleton rounded skeleton-field-content">
</div>
</div>
</div>
</small>
</div>
<div class="card-text text-break history-timeline-entry-date pb-1">
<small class="font-italic">
<div mode="list" class="field field-mode-list field-type-datetime">
<div class="dynamic-field dynamic-field-mode-list dynamic-field-type-datetime ">
<div class="box-loading small-size-text-skeleton medium-length-text-skeleton rounded skeleton-field-content">
<span class=""></span>
</div>
</div>
</div>
</small>
</div>
</div>
</div>
</div>
</div>
</div>
</div>

View file

@ -0,0 +1,14 @@
import {Component} from '@angular/core';
import {ImageModule} from "../../../../../components/image/image.module";
@Component({
selector: 'scrm-history-sidebar-skeleton-loading',
standalone: true,
imports: [
ImageModule
],
templateUrl: './history-sidebar-skeleton-loading.component.html',
})
export class HistorySidebarSkeletonLoadingComponent {
}

View file

@ -30,14 +30,18 @@
<div class="widget-background history-timeline p-2 pt-0">
<div *ngIf="adapter?.initializing() || adapter?.loading()" class="d-flex record-thread-loading justify-content-center">
<div *ngIf="adapter?.initializing() || (adapter?.firstLoad() && adapter?.loading())" class="record-thread-loading">
<scrm-history-sidebar-skeleton-loading></scrm-history-sidebar-skeleton-loading>
</div>
<div *ngIf="!adapter?.firstLoad() && adapter?.loading()" class="d-flex record-thread-loading justify-content-center">
<scrm-loading-spinner [overlay]="true"></scrm-loading-spinner>
</div>
<cdk-virtual-scroll-viewport itemSize="100"
class="history-timeline-viewport"
[ngClass]="[vm.length <= 0 ? 'history-timeline-viewport-no-data' : 'history-timeline-viewport']"
(scroll)="onScroll()">
<div class="history-timeline-viewport"
*ngIf="!(adapter?.firstLoad() && adapter?.loading())"
[ngClass]="[vm.length <= 0 ? 'history-timeline-viewport-no-data' : 'history-timeline-viewport']"
>
<scrm-chart-message-area *ngIf="!adapter?.initializing() && !adapter?.loading() && vm.length <= 0"
labelKey="LBL_NO_DATA"></scrm-chart-message-area>
@ -45,10 +49,10 @@
<div *ngFor="let entry of vm;">
<div *ngIf="vm.length > 0"
class="d-flex flex-row m-2 history-timeline-entry entry-{{entry.color}}">
<div class="">
<div class="history-timeline-entry-icon">
<div class="rounded-square icon-square">
<div
class="d-flex justify-content-center align-items-center h-100 history-timeline-image">
class="d-flex justify-content-center align-items-center h-100 history-timeline-image">
<scrm-image [image]="entry.icon"></scrm-image>
</div>
</div>
@ -61,11 +65,11 @@
<a *ngIf="entry.record.module !== 'audit'"
[routerLink]="redirectLink(entry.record.module, entry.record.id)"
>
<scrm-field [type]="entry.title.type"
mode="list"
[field]="entry.title"
[record]="entry.record">
</scrm-field>
<scrm-field [type]="entry.title.type"
mode="list"
[field]="entry.title"
[record]="entry.record">
</scrm-field>
</a>
<a *ngIf="entry.record.module === 'audit'">
<scrm-field [type]="entry.title.type"
@ -109,7 +113,12 @@
</div>
</div>
</cdk-virtual-scroll-viewport>
<div *ngIf="!adapter.initializing() && !adapter.allLoaded()"
class="record-thread-load-more d-flex justify-content-center flex-grow-1">
<scrm-button [config]="getLoadMoreButton()"></scrm-button>
</div>
</div>
</div>
</div>
</scrm-widget-panel>

View file

@ -24,16 +24,17 @@
* the words "Supercharged by SuiteCRM".
*/
import {AfterViewInit, Component, OnDestroy, OnInit, ViewChild} from '@angular/core';
import {AfterViewInit, Component, OnDestroy, OnInit, signal, ViewChild, WritableSignal} from '@angular/core';
import {HistoryTimelineAdapter} from './history-timeline.adapter.service';
import {BaseWidgetComponent} from '../../../widgets/base-widget.model';
import {LanguageStore} from '../../../../store/language/language.store';
import {HistoryTimelineAdapterFactory} from './history-timeline.adapter.factory';
import {combineLatestWith, Subscription, timer} from 'rxjs';
import {debounce, map, tap} from 'rxjs/operators';
import {debounce, map, take, tap} from 'rxjs/operators';
import {floor} from 'lodash-es';
import {CdkVirtualScrollViewport} from '@angular/cdk/scrolling';
import {ModuleNavigation} from "../../../../services/navigation/module-navigation/module-navigation.service";
import {ButtonInterface} from "../../../../common/components/button/button.model";
@Component({
selector: 'scrm-history-timeline-widget',
@ -42,8 +43,8 @@ import {ModuleNavigation} from "../../../../services/navigation/module-navigatio
providers: [HistoryTimelineAdapter]
})
export class HistorySidebarWidgetComponent extends BaseWidgetComponent implements OnInit, AfterViewInit, OnDestroy {
@ViewChild(CdkVirtualScrollViewport) virtualScroll: CdkVirtualScrollViewport;
public initialLoad: WritableSignal<boolean> = signal(false);
public adapter: HistoryTimelineAdapter;
private subscription = new Subscription();
@ -106,21 +107,14 @@ export class HistorySidebarWidgetComponent extends BaseWidgetComponent implement
return this.languageStore.getFieldLabel('LBL_QUICK_HISTORY');
}
/**
* @returns {void} Timeline Entry
* @description {checks if end of the scroll is reached to make a backend call for next set of timeline entries}
*/
onScroll(): void {
if (!this.adapter) {
return;
}
const scrollOffset = this.virtualScroll.measureScrollOffset('bottom');
if (floor(scrollOffset) === 0) {
this.adapter.fetchTimelineEntries(false);
}
getLoadMoreButton(): ButtonInterface {
return {
klass: 'load-more-button btn btn-link btn-sm',
labelKey: 'LBL_LOAD_MORE',
onClick: () => {
this.adapter.fetchTimelineEntries(false);
}
} as ButtonInterface;
}
redirectLink(module: string, id: string) {

View file

@ -35,6 +35,10 @@ import {LoadingSpinnerModule} from '../../../../components/loading-spinner/loadi
import {LabelModule} from '../../../../components/label/label.module';
import {ChartMessageAreaModule} from '../../../../components/chart/components/chart-message-area/chart-message-area.module';
import {RouterModule} from "@angular/router";
import {ButtonModule} from "../../../../components/button/button.module";
import {
HistorySidebarSkeletonLoadingComponent
} from "./history-sidebar-skeleton-loading/history-sidebar-skeleton-loading.component";
@NgModule({
declarations: [HistorySidebarWidgetComponent],
@ -51,6 +55,8 @@ import {RouterModule} from "@angular/router";
LabelModule,
ChartMessageAreaModule,
RouterModule,
ButtonModule,
HistorySidebarSkeletonLoadingComponent,
]
})
export class HistorySidebarWidgetModule {

View file

@ -33,6 +33,7 @@ import {Record} from '../../../../common/record/record.model';
import {ViewContext} from '../../../../common/views/view.model';
import {take} from 'rxjs/operators';
import {HistoryTimelineStoreFactory} from './history-timeline.store.factory';
import {sign} from "mathjs";
export type ActivityTypes = 'calls' | 'tasks' | 'meetings' | 'history' | 'audit' | 'notes' | string;
@ -40,6 +41,8 @@ export type ActivityTypes = 'calls' | 'tasks' | 'meetings' | 'history' | 'audit'
export class HistoryTimelineAdapter {
loading: WritableSignal<boolean> = signal(false);
initializing: WritableSignal<boolean> = signal(true);
firstLoad: WritableSignal<boolean> = signal(true);
allLoaded: WritableSignal<boolean> = signal(false);
cache: HistoryTimelineEntry[] = [];
dataStream = new BehaviorSubject<HistoryTimelineEntry[]>(this.cache);
@ -84,6 +87,7 @@ export class HistoryTimelineAdapter {
this.initializing.set(false)
this.store.load(false).pipe(take(1)).subscribe(value => {
this.loading.set(false);
this.firstLoad.set(false);
const records: Record [] = value.records;
if (!emptyObject(records)) {
@ -93,6 +97,9 @@ export class HistoryTimelineAdapter {
this.cache.push(this.buildTimelineEntry(records[key]));
});
}
this.allLoaded.set((value?.pagination?.pageLast ?? 0) < (value?.pagination?.pageSize ?? 0));
this.dataStream.next([...this.cache]);
});
return this.dataStream$;
@ -138,17 +145,17 @@ export class HistoryTimelineAdapter {
title: {
type: 'varchar',
value: record.attributes.name,
loading: this.loading
loading: signal(false)
},
user: {
type: 'varchar',
value: record.attributes.assigned_user_name.user_name,
loading: this.loading
loading: signal(false)
},
date: {
type: 'datetime',
value: record.attributes.date_end,
loading: this.loading
loading: signal(false)
},
record
} as HistoryTimelineEntry;
@ -158,7 +165,7 @@ export class HistoryTimelineAdapter {
timelineEntry.description = {
type: 'html',
value: record.attributes.description,
loading: this.loading
loading: signal(false)
};
}
return timelineEntry;

View file

@ -0,0 +1,84 @@
/**
* SuiteCRM is a customer relationship management program developed by SalesAgility Ltd.
* Copyright (C) 2021 SalesAgility Ltd.
*
* This program is free software; you can redistribute it and/or modify it under
* the terms of the GNU Affero General Public License version 3 as published by the
* Free Software Foundation with the addition of the following permission added
* to Section 15 as permitted in Section 7(a): FOR ANY PART OF THE COVERED WORK
* IN WHICH THE COPYRIGHT IS OWNED BY SALESAGILITY, SALESAGILITY DISCLAIMS THE
* WARRANTY OF NON INFRINGEMENT OF THIRD PARTY RIGHTS.
*
* 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 Affero General Public License for more
* details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
* In accordance with Section 7(b) of the GNU Affero General Public License
* version 3, these Appropriate Legal Notices must retain the display of the
* "Supercharged by SuiteCRM" logo. If the display of the logos is not reasonably
* feasible for technical reasons, the Appropriate Legal Notices must display
* the words "Supercharged by SuiteCRM".
*/
.history-timeline-viewport {
.skeleton-field-content {
height: inherit;
}
.history-timeline-entry-title {
skeleton-field-content {
opacity: 0.6;
}
}
&.history-sidebar-skeleton-loading {
}
.icon-square.box-loading {
opacity: 0.8;
}
.entry-purple {
.history-timeline-entry-title {
.skeleton-field-content {
background-color: $dark-purple;
}
}
}
.entry-yellow {
.history-timeline-entry-title {
.skeleton-field-content {
background-color: $dark-yellow;
}
}
}
.entry-blue {
.history-timeline-entry-title {
.skeleton-field-content {
background-color: $mild-blue;
}
}
}
.entry-orange {
.history-timeline-entry-title {
.skeleton-field-content {
background-color: $thick-orange;
}
}
}
.entry-green {
.history-timeline-entry-title {
.skeleton-field-content {
background-color: $thick-green;
}
}
}
}

View file

@ -26,6 +26,7 @@
.history-timeline-viewport {
height: 45em;
overflow-x: auto;
}
.history-timeline-viewport-no-data {
@ -43,17 +44,18 @@
padding-left: 18px;
}
.icon-square {
width: 2rem;
height: 2rem;
position: absolute;
left: 0;
float: left;
z-index: 2;
}
.history-timeline-entry{
.history-timeline-entry {
color: $dim-grey;
.history-timeline-entry-icon {
z-index: 2;
margin-right: -1.5rem;
.icon-square {
width: 2rem;
height: 2rem;
}
}
}
.history-timeline-image svg {
@ -92,7 +94,7 @@
}
.history-timeline .history-timeline-entry.entry-yellow .icon-square{
background: $dark-yellow;
background: $goldenrod;
}
.history-timeline .entry-yellow .card {
@ -105,7 +107,7 @@
}
.history-timeline .history-timeline-entry.entry-yellow a {
color: $dark-yellow;
color: $goldenrod;
}
.history-timeline .history-timeline-entry.entry-purple {
@ -152,7 +154,7 @@
}
.history-timeline .history-timeline-entry.entry-orange .icon-square{
background: $thick-orange;
background: $dark-yellow;
}
.history-timeline .history-timeline-entry.entry-orange svg {
@ -161,7 +163,7 @@
}
.history-timeline .history-timeline-entry.entry-orange a {
color: $thick-orange;
color: $dark-yellow;
}
.history-timeline .entry-orange .card {
@ -184,6 +186,26 @@
color: $thick-green;
}
.history-timeline .entry-green .card {
background: $lighter-green;
.history-timeline .entry-grey .card {
background: $off-white;
}
.history-timeline .history-timeline-entry.entry-grey {
}
.history-timeline .history-timeline-entry.entry-grey .icon-square{
background: $light-grey;
}
.history-timeline .history-timeline-entry.entry-grey svg {
fill: $white;
stroke: $white;
}
.history-timeline .history-timeline-entry.entry-grey a {
color: $light-grey;
}
.history-timeline .entry-green .card {
background: $off-white;
}

View file

@ -61,6 +61,7 @@
@import 'components/subpanel-activities';
@import 'components/shapes';
@import 'components/history-timeline-widget';
@import 'components/history-sidebar-skeleton-loading';
@import 'components/loading-spinner';
@import 'components/inline-loading-spinner';
@import 'components/widget';