Update subpanels UI

This commit is contained in:
Clemente Raposo 2024-12-05 01:01:44 +00:00 committed by Jack Anderson
parent 2058da049d
commit 0d1b835989
5 changed files with 380 additions and 155 deletions

View file

@ -46,7 +46,7 @@ export interface SubPanelMeta {
}
export interface SubPanelDefinition {
insightWidget?: WidgetOptionMap;
subpanelWidget?: WidgetOptionMap;
order?: 10;
sort_order?: string;
sort_by?: string;

View file

@ -25,45 +25,73 @@
* the words "Supercharged by SuiteCRM".
*/
-->
<ng-container *ngIf="(vm$ | async) as vm">
<div class="card border shadow-sm">
<div ngbAccordion class="sub-panel-banner" #accordion="ngbAccordion" activeIds="sub-panel-buttons">
<div ngbAccordionItem id="sub-panel-buttons" class="card" [collapsed]="isCollapsed()">
<div ngbAccordionHeader class="card-header">
<a (click)="toggleSubPanels()" class="clickable">
<div class="d-flex align-items-center justify-content-between">
<scrm-label labelKey="LBL_SELECT_SUBPANEL_BANNER"></scrm-label>
<ng-container *ngIf="isCollapsed()">
<scrm-image
[attr.aria-expanded]="false"
[image]="toggleIcon()"
aria-controls="collapseShowSubPanels"
class="float-right">
</scrm-image>
</ng-container>
<ng-container *ngIf="!isCollapsed()">
<scrm-image
[attr.aria-expanded]="true"
[image]="toggleIcon()"
aria-controls="collapseShowSubPanels"
class="float-right">
</scrm-image>
</ng-container>
<div class="d-flex justify-content-between">
<div class="d-flex align-items-start sub-panel-banner-header">
<a (click)="toggleSubPanels()" class="clickable">
<scrm-label labelKey="LBL_RELATIONSHIPS"></scrm-label>
</a>
</div>
</a>
<div class="d-flex align-items-center justify-content-end">
<div class="row insight-panel" *ngIf="isCollapsed()">
<div class="col-auto mr-3 insight-panel-card border-insight"
*ngFor="let subpanelKey of headerSubpanels()"
[ngClass]="{'sub-panel-banner-button-active': subpanels[subpanelKey].show === true}"
(click)="showSubpanel(subpanels[subpanelKey].metadata.name, subpanels[subpanelKey])">
<scrm-grid-widget
[config]="getGridConfig(subpanels[subpanelKey])"></scrm-grid-widget>
</div>
</div>
<div class="d-flex align-items-center sub-panel-header-toggle">
<a (click)="toggleSubPanels()" class="clickable position-relative">
<ng-container *ngIf="isCollapsed()">
<scrm-image
[attr.aria-expanded]="false"
[image]="'chevron-down'"
aria-controls="collapseShowSubPanels"
class="float-right">
</scrm-image>
<ng-container *ngIf="activeHiddenButtonsCount() > 0">
<span class="hidden-subpanel-buttons-count badge position-absolute">{{ activeHiddenButtonsCount() }}</span>
</ng-container>
</ng-container>
<ng-container *ngIf="!isCollapsed()">
<scrm-image
[attr.aria-expanded]="true"
[image]="'chevron-up'"
aria-controls="collapseShowSubPanels"
class="float-right">
</scrm-image>
</ng-container>
</a>
</div>
</div>
</div>
</div>
<div ngbAccordionCollapse>
<div ngbAccordionBody>
<ng-template>
<div id="collapseShowSubPanels">
<div class="row insight-panel">
<div class="col-xs-6 col-sm-3 col-md-2 insight-panel-card border-insight"
*ngFor="let item of vm.subpanels | keyvalue"
[ngClass]="{'sub-panel-banner-button-active': item.value.show === true}"
(click)="showSubpanel(item.key, item.value)">
<scrm-grid-widget [config]="getGridConfig(item.value)"></scrm-grid-widget>
</div>
</div>
<div id="collapseShowSubPanels" class="sub-panel-banner-body d-flex align-items-center justify-content-center border-bottom border-top pt-2 pb-3 ml-2 mr-2">
<table class="sub-panel-banner-body-table">
<tr class="insight-panel sub-panel-banner-body-table-row" *ngFor="let subpanelRow of bodySubpanels()">
<td *ngFor="let subpanelKey of subpanelRow" class="sub-panel-banner-body-table-col">
<div class="insight-panel-card border-insight pl-2 pr-2"
[ngClass]="{'sub-panel-banner-button-active': subpanels[subpanelKey].show === true}"
(click)="showSubpanel(subpanels[subpanelKey].metadata.name, subpanels[subpanelKey])">
<scrm-grid-widget
[config]="getGridConfig(subpanels[subpanelKey])"></scrm-grid-widget>
</div>
</td>
</tr>
</table>
</div>
</ng-template>
</div>
@ -72,19 +100,19 @@
</div>
</div>
<div id="sub-panels">
<ng-container *ngFor="let subpanelKey of this.openSubpanels">
<ng-container *ngIf="(vm.subpanels[subpanelKey]) as item">
<div class="sub-panels">
<ng-container *ngFor="let subpanelKey of this.openSubpanels()">
<ng-container *ngIf="(subpanels[subpanelKey]) as item">
<scrm-subpanel *ngIf="item.show"
[maxColumns$]="maxColumns$"
[store]="item"
[filterConfig]="filterConfig"
[onClose]="getCloseCallBack(subpanelKey, item)"
class="sub-panel">
class="sub-panel minimal-table">
</scrm-subpanel>
</ng-container>
</ng-container>
</div>
</ng-container>
</div>

View file

@ -24,11 +24,11 @@
* the words "Supercharged by SuiteCRM".
*/
import {Component, Input, OnInit, signal} from '@angular/core';
import {map, take, tap} from 'rxjs/operators';
import {Observable} from 'rxjs';
import {Component, computed, Input, OnDestroy, OnInit, Signal, signal, WritableSignal} from '@angular/core';
import {take} from 'rxjs/operators';
import {Observable, Subscription} from 'rxjs';
import {SubpanelContainerConfig} from './subpanel-container.model';
import {LanguageStore, LanguageStrings} from '../../../../store/language/language.store';
import {LanguageStore} from '../../../../store/language/language.store';
import {SubpanelStore, SubpanelStoreMap} from '../../store/subpanel/subpanel.store';
import {MaxColumnsCalculator} from '../../../../services/ui/max-columns-calculator/max-columns-calculator.service';
import {ViewContext} from '../../../../common/views/view.model';
@ -38,13 +38,18 @@ import {GridWidgetConfig, StatisticsQueryArgs} from '../../../../components/grid
import {LocalStorageService} from "../../../../services/local-storage/local-storage.service";
import {FilterConfig} from "../../../list-filter/components/list-filter/list-filter.model";
import {UserPreferenceStore} from '../../../../store/user-preference/user-preference.store';
import {SystemConfigStore} from "../../../../store/system-config/system-config.store";
import {
ScreenSize,
ScreenSizeObserverService
} from "../../../../services/ui/screen-size-observer/screen-size-observer.service";
@Component({
selector: 'scrm-subpanel-container',
templateUrl: 'subpanel-container.component.html',
providers: [MaxColumnsCalculator]
})
export class SubpanelContainerComponent implements OnInit {
export class SubpanelContainerComponent implements OnInit, OnDestroy {
@Input() config: SubpanelContainerConfig;
@ -52,18 +57,32 @@ export class SubpanelContainerComponent implements OnInit {
toggleIcon = signal('arrow_down_filled');
maxColumns$: Observable<number>;
languages$: Observable<LanguageStrings> = this.languageStore.vm$;
vm$: Observable<{ subpanels: SubpanelStoreMap }>;
openSubpanels: string[] = [];
subpanels: SubpanelStoreMap;
orderedSubpanels: WritableSignal<string[]> = signal([]);
headerSubpanels: Signal<string[]> = signal([]);
bodySubpanels: Signal<string[]> = signal([]);
openSubpanels: WritableSignal<string[]> = signal([]);
activeHiddenButtonsCount: Signal<number> = signal(0);
filterConfig: FilterConfig;
subs: Subscription[] = [];
protected subpanelButtonLimits: any = {
XSmall: 2,
Small: 3,
Medium: 3,
Large: 5,
XLarge: 5
};
protected subpanelButtonBreakpoint: WritableSignal<number> = signal(3);
constructor(
protected languageStore: LanguageStore,
protected maxColumnCalculator: MaxColumnsCalculator,
protected localStorage: LocalStorageService,
protected preferences: UserPreferenceStore
protected preferences: UserPreferenceStore,
protected systemConfigs: SystemConfigStore,
protected screenSize: ScreenSizeObserverService,
) {
}
@ -71,38 +90,99 @@ export class SubpanelContainerComponent implements OnInit {
const module = this?.config?.parentModule ?? 'default';
this.setCollapsed(isTrue(this.preferences.getUi(module, 'subpanel-container-collapse') ?? false));
this.openSubpanels = this.preferences.getUi(module, 'subpanel-container-open-subpanels') ?? [];
const subpanelButtonLimits = this.systemConfigs.getConfigValue('recordview_subpanel_button_limits') ?? {};
if (subpanelButtonLimits && Object.keys(subpanelButtonLimits).length) {
this.subpanelButtonLimits = subpanelButtonLimits;
}
this.vm$ = this.config.subpanels$.pipe(
map((subpanelsMap) => ({
subpanels: subpanelsMap
})),
tap((subpanelsMap) => {
if (!subpanelsMap || !Object.keys(subpanelsMap).length) {
return;
}
this.openSubpanels.set(this.preferences.getUi(module, 'subpanel-container-open-subpanels') ?? []);
if (!this.openSubpanels || this.openSubpanels.length < 1) {
return;
}
this.subs.push(this.config.subpanels$.subscribe({
next: (subpanelsMap) => {
this.subpanels = {...subpanelsMap};
this.openSubpanels.forEach(subpanelKey => {
const subpanel = subpanelsMap.subpanels[subpanelKey];
const orderedSubpanels = Object.values(this.subpanels)
.filter(item => item?.metadata?.order !== undefined)
.sort((a, b) => (a.metadata.order ?? 0) - (b.metadata.order ?? 0))
.map(item => item.metadata.name);
if (!subpanel || subpanel.show) {
if (orderedSubpanels) {
this.orderedSubpanels.set(orderedSubpanels);
}
if (!this.subpanels || !Object.keys(this.subpanels).length) {
return;
}
subpanel.show = true;
subpanel.load().pipe(take(1)).subscribe();
if (!this.openSubpanels() || this.openSubpanels().length < 1) {
return;
}
});
this.openSubpanels().forEach(subpanelKey => {
const subpanel = this.subpanels[subpanelKey];
}));
if (!subpanel || subpanel.show) {
return;
}
subpanel.show = true;
subpanel.load().pipe(take(1)).subscribe();
});
}
})
);
this.headerSubpanels = computed(() => this.orderedSubpanels().slice(0, this.subpanelButtonBreakpoint()));
this.bodySubpanels = computed(() => {
const sliced = [...this.orderedSubpanels()];
let count = 0;
const groups = [];
sliced.forEach(value => {
if (count === 0) {
groups.push([]);
}
groups[groups.length - 1].push(value);
count++;
if (count >= this.subpanelButtonBreakpoint()) {
count = 0;
}
});
return groups;
});
this.activeHiddenButtonsCount = computed(() => {
const openSubpanelsSet = new Set(this.openSubpanels());
const headerSubpanelsSet = new Set(this.headerSubpanels());
return this.bodySubpanels().flat().reduce(
(count, subpanelKey) => {
const isOpen = openSubpanelsSet.has(subpanelKey);
const inHeader = headerSubpanelsSet.has(subpanelKey);
return count + ((isOpen && !inHeader) ? 1 : 0);
},
0
);
});
this.subs.push(this.screenSize.screenSize$.subscribe({
next: (screenSize: ScreenSize) => {
if (screenSize && this.subpanelButtonLimits[screenSize]) {
this.subpanelButtonBreakpoint.set(this.subpanelButtonLimits[screenSize]);
}
}
}));
this.maxColumns$ = this.getMaxColumns();
}
ngOnDestroy() {
this.subs.forEach(sub => sub.unsubscribe());
}
getMaxColumns(): Observable<number> {
return this.maxColumnCalculator.getMaxColumns(this.config.sidebarActive$);
}
@ -117,17 +197,21 @@ export class SubpanelContainerComponent implements OnInit {
showSubpanel(key: string, item: SubpanelStore): void {
item.show = !item.show;
let openSubpanels = [...this.openSubpanels()];
if (item.show) {
if (!this.openSubpanels.includes(key)) {
this.openSubpanels.push(key);
if (!openSubpanels.includes(key)) {
openSubpanels.push(key);
}
item.load().pipe(take(1)).subscribe();
} else {
this.openSubpanels = this.openSubpanels.filter(subpanelKey => subpanelKey != key);
openSubpanels = openSubpanels.filter(subpanelKey => subpanelKey != key);
}
this.openSubpanels.set(openSubpanels);
const module = this?.config?.parentModule ?? 'default';
this.preferences.setUi(module, 'subpanel-container-open-subpanels', this.openSubpanels);
this.preferences.setUi(module, 'subpanel-container-open-subpanels', this.openSubpanels());
}
getCloseCallBack(key: string, item: SubpanelStore): Function {
@ -136,7 +220,7 @@ export class SubpanelContainerComponent implements OnInit {
getGridConfig(vm: SubpanelStore): GridWidgetConfig {
if (!vm.metadata || !vm.metadata.insightWidget) {
if (!vm.metadata || !vm.metadata.subpanelWidget) {
return {
layout: null,
} as GridWidgetConfig;

View file

@ -319,7 +319,7 @@ export class SubpanelStore implements StateStore {
*/
public shouldBatchStatistic(): boolean {
const metadata: SubPanelDefinition = this.metadata || {} as SubPanelDefinition;
return !(metadata.insightWidget && metadata.insightWidget.batch && metadata.insightWidget.batch === false);
return !(metadata.subpanelWidget && metadata.subpanelWidget.batch && metadata.subpanelWidget.batch === false);
}
/**
@ -411,11 +411,11 @@ export class SubpanelStore implements StateStore {
public getWidgetLayout(): StatisticWidgetOptions {
const meta = this.metadata;
if (!meta || !meta.insightWidget || !meta.insightWidget.options || !meta.insightWidget.options.insightWidget) {
if (!meta || !meta.subpanelWidget || !meta.subpanelWidget.options || !meta.subpanelWidget.options.subpanelWidget) {
return {rows: []} as StatisticWidgetOptions;
}
const layout = deepClone(meta.insightWidget.options.insightWidget);
const layout = deepClone(meta.subpanelWidget.options.subpanelWidget);
if (!layout.rows || !layout.rows.length) {
layout.rows = {};

View file

@ -36,15 +36,6 @@
background-color: $midnight-blue-transparent;
}
.sub-panel-banner-button-active {
background-color: $midnight-blue-transparent;
}
.widget-entry-icon svg {
fill: $salmon-pink;
stroke: $salmon-pink;
font-size: 1.6em;
}
.sub-panel .subpanel-icon svg {
font-size: x-large;
@ -66,6 +57,7 @@
background-color: $midnight-blue;
color: $white;
border-color: $midnight-blue;
font-weight: bold;
scrm-image {
display: inline;
@ -86,7 +78,7 @@
display: inline;
svg {
fill: $midnight-blue;
fill: $midnight-blue;
}
}
}
@ -99,100 +91,213 @@ div.widget-panel .panel-card .card-header {
padding: 0.5rem 0.6rem;
}
.insight-panel {
margin-left: 0;
margin-right: 0;
padding: 1em;
.sub-panel-banner {
.insight-panel {
margin-left: 0;
margin-right: 0;
.border-insight {
margin: 4px;
border: 0.15em solid $very-light-grey;
border-radius: 4px;
border-top: 3px solid $midnight-blue;
.insight-panel-card {
cursor: pointer;
padding: 0;
border: none;
border-bottom: 1px solid $shell-grey;
border-radius: 0;
margin: 0;
margin-right: 0.5em;
color: $shell-grey;
svg {
font-size: 1.5em;
fill: $midnight-blue;
stroke: $midnight-blue;
}
}
.grid-widget {
min-height: 2.1em;
padding-left: 0.5em;
padding-right: 0.5em;
height: 100%;
justify-content: flex-end;
flex-direction: row !important;
align-items: start;
.insight-panel-card {
padding-left: 0.5rem;
padding-right: 0.5rem;
cursor: pointer;
}
height: 100%;
justify-content: flex-end;
.insight-panel-card:hover {
box-shadow: 0 0.25rem 0.5rem rgb(0 0 0 / 15%);
}
.widget-entry-icon {
display: flex;
justify-content: flex-end;
margin-right: 0.4em;
@media (min-width: 768px) {
.col-md-2 {
flex: 0 0 15.666667%;
max-width: 15.666667%;
}
}
svg {
font-size: 1.5em;
fill: $shell-grey;
stroke: $shell-grey;
}
}
.widget-entry-icon {
display: flex;
justify-content: flex-end;
margin-top: 0.7em;
}
.sub-panel-banner-value {
margin-left: 0.4em;
.sub-panel-banner-value {
font-size: 1rem;
font-weight: bold;
display: flex;
justify-content: space-between;
align-items: center;
margin-top: -0.5em;
color: $burnt-red;
}
.widget-entry-value {
color: $shell-grey;
font-size: .67rem;
}
}
.sub-panel-banner-tooltip {
width: 100%;
.sub-panel-banner-button-title {
width: 100%;
.widget-entry-label {
border-bottom: 2px solid $very-light-grey;
font-size: 0.7rem;
line-height: 1rem;
.widget-entry-label {
text-transform: uppercase;
font-size: .62rem;
width: 100%;
padding: .5em 0;
color: $shell-grey;
}
}
label {
white-space: nowrap;
text-overflow: ellipsis;
overflow: hidden;
width: 100%;
margin: 0;
}
&:hover,
&.sub-panel-banner-button-active {
border-bottom: 1px solid $bright-purple;
.grid-widget {
background-color: inherit;
.widget-entry-icon {
svg {
fill: $bright-purple;
stroke: $bright-purple;
}
}
.sub-panel-banner-button-title {
.widget-entry-label {
color: $bright-purple;
font-weight: 900;
}
}
.sub-panel-banner-value {
.widget-entry-value {
color: $bright-purple;
}
}
}
}
}
}
.sub-panel-banner-button-title {
width: 100%;
.widget-entry-label {
text-transform: uppercase;
font-size: 0.62rem;
width: 100%;
padding: 0.5em 0;
color: $midnight-blue;
@media (min-width: 768px) {
.col-md-2 {
flex: 0 0 15.666667%;
max-width: 15.666667%;
}
}
.sub-panel-banner-tooltip {
width: 100%;
.widget-entry-label {
border-bottom: 2px solid $very-light-grey;
font-size: 0.7rem;
line-height: 1rem;
label {
white-space: nowrap;
text-overflow: ellipsis;
overflow: hidden;
width: 100%;
margin: 0;
}
}
}
}
.grid-widget {
height: 100%;
justify-content: flex-end;
.sub-panel-banner-body {
.sub-panel-banner-body-table {
width: 95%;
table-layout: fixed;
.sub-panel-banner-body-table-row:first-child {
.sub-panel-banner-body-table-col {
padding-bottom: 0.5em;
}
}
.sub-panel-banner-body-table-row {
height: 100%;
.sub-panel-banner-body-table-col {
height: 100%;
.insight-panel-card {
height: 100%;
display: flex;
}
.grid-widget {
justify-content: flex-end;
flex-direction: row !important;
align-items: start;
.statistics-sidebar-widget-row {
width: 100%;
height: 100%;
min-height: 2.7em;
}
.sub-panel-banner-button-title {
.widget-entry-label {
text-wrap: auto;
word-break: normal;
}
}
}
}
}
}
}
}
.accordion {
.sub-panels {
padding: 0 1.25rem 1.25rem;
}
.sub-panel-banner.accordion {
.card {
border: none;
.card-header {
min-height: 2.1em;
padding: .50rem 1.25rem;
background: $white;
border-top: 3px solid $midnight-blue;
.sub-panel-banner-header {
min-height: 2.2em;
}
.sub-panel-header-toggle {
border-radius: 50%;
.hidden-subpanel-buttons-count {
top: -7px;
right: -10px;
background: $sky-blue;
color: $white;
border-radius: 7px;
}
svg {
font-size: 1.1em;
}
&:hover {
}
}
a.clickable {
color: $midnight-blue;
@ -218,18 +323,26 @@ div.widget-panel .panel-card .card-header {
.sub-panel {
&:first-child {
.card.panel-card {
margin-top: 0.5em;
}
}
table {
tr {
td:first-child,
th:first-child{
th:first-child {
padding-left: 1.3em;
}
td:last-child,
th:last-child {
padding-right: 1.3em;
}
}
}
.table .show-more-column {
float: none !important;
width: 16px;