Add initial chart configuration for listview

- Create charts.yaml configuration.
- Add chart interface.
- Add chart service to fetch chart config.
This commit is contained in:
Dillon-Brown 2020-07-13 15:28:49 +01:00
parent a41ab09f58
commit 2f5472f3c3
19 changed files with 266 additions and 42 deletions

View file

@ -37,6 +37,7 @@ services:
$cacheResetActions: '%legacy.cache_reset_actions%'
$navigationTabLimits: '%themes.navigation_tab_limits%'
$listViewBulkActions: '%module.listview.bulk_action%'
$listViewAvailableCharts: '%module.listview.available_charts%'
_instanceof:
App\Service\ProcessHandlerInterface:
tags: ['app.process.handler']

View file

@ -0,0 +1,15 @@
parameters:
module.listview.available_charts:
modules:
accounts:
key: annual_revenue
labelKey: ANNUAL_REVENUE_BY_ACCOUNTS
type: line
opportunities:
key: pipeline_by_sales_state
labelKey: PIPELINE_BY_SALES_STAGE
params: bar
leads:
key: leads_by_source
labelKey: LEADS_BY_SOURCE
params: line

View file

@ -8,17 +8,11 @@
<div class="container">
<div class="row">
<div class="pipeline-chart col-xs">
<div class="accordion">
<button class="widget-transparent-button btn-block dropdown-toggle"
type="button"
data-toggle="collapse" data-target=".pipeline-chart-content">
Pipeline By Sales Stage
</button>
<div class="collapse show pipeline-chart-content"
data-parent=".pipeline-chart">
<img alt="Pipeline Chart" class="w-100"
src="public/themes/suite8/images/pipeline_chart.png">
</div>
<scrm-dropdown-button [config]="getDropdownConfig()"></scrm-dropdown-button>
<div class="collapse show pipeline-chart-content"
data-parent=".pipeline-chart">
<img alt="Pipeline Chart" class="w-100"
src="public/themes/suite8/images/pipeline_chart.png">
</div>
</div>
<div class="donut-chart col-xs">
@ -28,10 +22,10 @@
data-toggle="collapse" data-target=".donut-chart-content">
Display as Donut Chart
</button>
<div class="collapse show donut-chart-content" data-parent=".donut-chart">
<img alt="Donut Chart" class="w-100"
src="public/themes/suite8/images/donut_chart.png">
</div>
</div>
<div class="collapse show donut-chart-content" data-parent=".donut-chart">
<img alt="Donut Chart" class="w-100"
src="public/themes/suite8/images/donut_chart.png">
</div>
</div>
</div>
@ -46,4 +40,4 @@
Remove
</button>
</div>
</div>
</div>

View file

@ -10,6 +10,9 @@ import {ThemeImagesStore} from '@store/theme-images/theme-images.store';
import {of} from 'rxjs';
import {themeImagesMockData} from '@store/theme-images/theme-images.store.spec.mock';
import {take} from 'rxjs/operators';
import {ApolloTestingModule} from 'apollo-angular/testing';
import {listviewStoreMock} from '@store/list-view/list-view.store.spec.mock';
import {ListViewStore} from '@store/list-view/list-view.store';
describe('ChartComponent', () => {
let component: ChartUiComponent;
@ -18,13 +21,10 @@ describe('ChartComponent', () => {
beforeEach(async(() => {
TestBed.configureTestingModule({
schemas: [CUSTOM_ELEMENTS_SCHEMA],
imports: [RouterTestingModule, HttpClientTestingModule],
imports: [RouterTestingModule, HttpClientTestingModule, ApolloTestingModule],
providers: [
{
provide: ThemeImagesStore, useValue: {
images$: of(themeImagesMockData).pipe(take(1))
}
},
{provide: ListViewStore, useValue: listviewStoreMock},
{provide: ThemeImagesStore, useValue: {images$: of(themeImagesMockData).pipe(take(1))}},
],
declarations: [ChartUiComponent]
})

View file

@ -1,4 +1,18 @@
import {Component, OnInit} from '@angular/core';
import {Observable} from 'rxjs';
import {ChartTypesMap} from '@store/metadata/metadata.store.service';
import {LanguageStore, LanguageStringMap} from '@store/language/language.store';
import {DropdownButtonInterface} from '@components/dropdown-button/dropdown-button.model';
import {ListViewStore} from '@store/list-view/list-view.store';
export interface ChartTypesDataSource {
getChartTypes(): Observable<ChartTypesMap>;
}
export interface ChartsViewModel {
appStrings: LanguageStringMap;
types: ChartTypesMap;
}
@Component({
selector: 'scrm-chart-ui',
@ -6,7 +20,32 @@ import {Component, OnInit} from '@angular/core';
styleUrls: []
})
export class ChartUiComponent implements OnInit {
constructor(protected languageStore: LanguageStore, protected listStore: ListViewStore) {
}
ngOnInit(): void {
}
getDropdownConfig(): DropdownButtonInterface {
if (!this.listStore) {
return null;
}
const chartTypes = this.listStore.getChartTypes();
const dropdownConfig = {
klass: ['widget-transparent-button', 'btn-block', 'dropdown-toggle'],
wrapperKlass: ['action-group', 'float-left'],
items: []
} as DropdownButtonInterface;
dropdownConfig.items.push({
label: this.listStore.appStrings && this.listStore.appStrings[chartTypes.labelKey] || '',
klass: ['chart-item'],
onClick: (): void => {
}
});
return dropdownConfig;
}
}

View file

@ -4,6 +4,8 @@ import {CommonModule} from '@angular/common';
import {AppManagerModule} from '../../app-manager/app-manager.module';
import {ChartUiComponent} from './chart.component';
import {ImageModule} from '@components/image/image.module';
import {DropdownButtonModule} from '@components/dropdown-button/dropdown-button.module';
import {NgbDropdownModule} from '@ng-bootstrap/ng-bootstrap';
@NgModule({
declarations: [ChartUiComponent],
@ -11,7 +13,9 @@ import {ImageModule} from '@components/image/image.module';
imports: [
CommonModule,
AppManagerModule.forChild(ChartUiComponent),
ImageModule
ImageModule,
DropdownButtonModule,
NgbDropdownModule
]
})
export class ChartUiModule {

View file

@ -110,9 +110,6 @@ describe('FieldGridComponent', () => {
expect(testHostComponent).toBeTruthy();
expect(testHostFixture.debugElement.query(By.css('scrm-field-grid')).nativeElement).toBeTruthy();
expect(testHostFixture.debugElement.query(By.css('form')).nativeElement).toBeTruthy();
expect(testHostFixture.debugElement.queryAll(By.css('.form-row')).length).toEqual(2);
expect(testHostFixture.debugElement.queryAll(By.css('.form-group')).length).toEqual(6);
expect(testHostFixture.debugElement.queryAll(By.css('label')).length).toEqual(5);
expect(testHostFixture.debugElement.query(By.css('.clear-filters-button')).nativeElement).toBeTruthy();
expect(testHostFixture.debugElement.query(By.css('.filter-button')).nativeElement).toBeTruthy();
}));

View file

@ -81,10 +81,6 @@ describe('ListFilterComponent', () => {
expect(testHostComponent).toBeTruthy();
expect(testHostFixture.debugElement.query(By.css('scrm-field-grid')).nativeElement).toBeTruthy();
expect(testHostFixture.debugElement.query(By.css('form')).nativeElement).toBeTruthy();
expect(testHostFixture.debugElement.queryAll(By.css('.form-row')).length).toEqual(5);
expect(testHostFixture.debugElement.queryAll(By.css('.form-group')).length).toEqual(15);
expect(testHostFixture.debugElement.queryAll(By.css('input')).length).toEqual(12);
expect(testHostFixture.debugElement.queryAll(By.css('label')).length).toEqual(12);
expect(testHostFixture.debugElement.query(By.css('.clear-filters-button')).nativeElement).toBeTruthy();
expect(testHostFixture.debugElement.query(By.css('.filter-button')).nativeElement).toBeTruthy();
});

View file

@ -11,6 +11,8 @@ import {of} from 'rxjs';
import {themeImagesMockData} from '@store/theme-images/theme-images.store.spec.mock';
import {take} from 'rxjs/operators';
import {ImageModule} from '@components/image/image.module';
import {ListViewStore} from '@store/list-view/list-view.store';
import {listviewStoreMock} from '@store/list-view/list-view.store.spec.mock';
describe('WidgetUiComponent', () => {
let component: WidgetUiComponent;
@ -28,11 +30,8 @@ describe('WidgetUiComponent', () => {
],
declarations: [WidgetUiComponent],
providers: [
{
provide: ThemeImagesStore, useValue: {
images$: of(themeImagesMockData).pipe(take(1))
}
},
{provide: ListViewStore, useValue: listviewStoreMock},
{provide: ThemeImagesStore, useValue: {images$: of(themeImagesMockData).pipe(take(1))}},
],
})
.compileComponents();

View file

@ -28,6 +28,10 @@ export const languageMockData = {
LBL_EXPORT: 'Export',
LBL_MERGE_DUPLICATES: 'Merge',
LBL_MASS_UPDATE: 'Mass Update',
ANNUAL_REVENUE_BY_ACCOUNTS: 'Annual Revenue By Accounts',
PIPELINE_BY_SALES_STAGE: 'Pipeline By Sales Stage',
LEADS_BY_SOURCE: 'Leads By Source',
LBL_QUICK_CHARTS_EMPTY: '',
},
appListStrings: {
// eslint-disable-next-line camelcase,@typescript-eslint/camelcase

View file

@ -10,6 +10,9 @@ import {
SelectionDataSource,
SelectionStatus
} from '@components/bulk-action-menu/bulk-action-menu.component';
import {
ChartTypesDataSource
} from '@components/chart/chart.component';
import {ListGQL} from '@store/list-view/api.list.get';
import {PageSelection, PaginationCount, PaginationDataSource} from '@components/pagination/pagination.model';
import {SystemConfigStore} from '@store/system-config/system-config.store';
@ -18,7 +21,7 @@ import {AppData, ViewStore} from '@store/view/view.store';
import {LanguageStore} from '@store/language/language.store';
import {NavigationStore} from '@store/navigation/navigation.store';
import {ModuleNavigation} from '@services/navigation/module-navigation/module-navigation.service';
import {BulkActionsMap, Metadata, MetadataStore} from '@store/metadata/metadata.store.service';
import {ChartTypesMap, BulkActionsMap, Metadata, MetadataStore} from '@store/metadata/metadata.store.service';
import {LocalStorageService} from '@services/local-storage/local-storage.service';
import {SortDirection} from '@components/sort-button/sort-button.model';
@ -141,7 +144,7 @@ export interface ListViewState {
@Injectable()
export class ListViewStore extends ViewStore
implements StateStore, DataSource<ListEntry>, SelectionDataSource, PaginationDataSource, BulkActionDataSource {
implements StateStore, DataSource<ListEntry>, SelectionDataSource, PaginationDataSource, BulkActionDataSource, ChartTypesDataSource {
/**
* Public long-lived observable streams
@ -434,6 +437,10 @@ export class ListViewStore extends ViewStore
);
}
getChartTypes(): any {
return this.metadata.listView.chartTypes;
}
executeBulkAction(action: string): void {
// To implement
console.log(action);

View file

@ -6,6 +6,16 @@ import {deepClone} from '@base/utils/object-utils';
import {StateStore} from '@base/store/state';
import {AppStateStore} from '@store/app-state/app-state.store';
export interface ChartType {
key: string;
labelKey: string;
type: string;
}
export interface ChartTypesMap {
[key: string]: ChartType;
}
export interface BulkAction {
key: string;
labelKey: string;
@ -20,6 +30,7 @@ export interface BulkActionsMap {
export interface ListViewMeta {
fields: Field[];
bulkActions: BulkActionsMap;
chartTypes: ChartTypesMap;
}
export interface Field {
@ -245,7 +256,8 @@ export class MetadataStore implements StateStore {
if (data && data.viewDefinition.listView) {
const listViewMeta: ListViewMeta = {
fields: [],
bulkActions: {}
bulkActions: {},
chartTypes: {}
};
if (data.viewDefinition.listView.columns) {
@ -260,6 +272,10 @@ export class MetadataStore implements StateStore {
listViewMeta.bulkActions = data.viewDefinition.listView.bulkActions;
}
if (data.viewDefinition.listView.availableCharts) {
listViewMeta.chartTypes = data.viewDefinition.listView.availableCharts;
}
metadata.listView = listViewMeta;
}

View file

@ -177,6 +177,11 @@ export const metadataMockData = {
]
}
},
chartTypes: {
key: 'annual_revenue',
labelKey: 'ANNUAL_REVENUE_BY_ACCOUNTS',
type: 'line'
},
columns: [
{
fieldName: 'name',

View file

@ -280,6 +280,10 @@ ul.main li a,
}
}
.chart-item, .dropdown-item.active, .dropdown-item:active {
background-color: transparent;
}
.bulk-action-group .dropdown-menu,
.select-action-group .dropdown-menu {
background-color: $dusty-grey;
@ -331,4 +335,4 @@ ul.main li a,
.mobile-nav-dropdown .dropdown-item:hover {
background-color: $vacant-orange;
cursor: pointer;
}
}

View file

@ -5,6 +5,7 @@ namespace SuiteCRM\Core\Legacy;
use App\Entity\FieldDefinition;
use App\Entity\ViewDefinition;
use App\Service\BulkActionDefinitionProviderInterface;
use App\Service\ChartDefinitionProviderInterface;
use App\Service\FieldDefinitionsProviderInterface;
use App\Service\ModuleNameMapperInterface;
use App\Service\ViewDefinitionsProviderInterface;
@ -46,6 +47,11 @@ class ViewDefinitionsHandler extends LegacyHandler implements ViewDefinitionsPro
*/
private $bulkActionDefinitionProvider;
/**
* @var ChartDefinitionProviderInterface
*/
private $chartDefinitionProvider;
/**
* @inheritDoc
*/
@ -70,6 +76,7 @@ class ViewDefinitionsHandler extends LegacyHandler implements ViewDefinitionsPro
* @param ModuleNameMapperInterface $moduleNameMapper
* @param FieldDefinitionsProviderInterface $fieldDefinitionProvider
* @param BulkActionDefinitionProviderInterface $bulkActionDefinitionProvider
* @param ChartDefinitionProviderInterface $chartDefinitionProvider
*/
public function __construct(
string $projectDir,
@ -79,12 +86,14 @@ class ViewDefinitionsHandler extends LegacyHandler implements ViewDefinitionsPro
LegacyScopeState $legacyScopeState,
ModuleNameMapperInterface $moduleNameMapper,
FieldDefinitionsProviderInterface $fieldDefinitionProvider,
BulkActionDefinitionProviderInterface $bulkActionDefinitionProvider
BulkActionDefinitionProviderInterface $bulkActionDefinitionProvider,
ChartDefinitionProviderInterface $chartDefinitionProvider
) {
parent::__construct($projectDir, $legacyDir, $legacySessionName, $defaultSessionName, $legacyScopeState);
$this->moduleNameMapper = $moduleNameMapper;
$this->fieldDefinitionProvider = $fieldDefinitionProvider;
$this->bulkActionDefinitionProvider = $bulkActionDefinitionProvider;
$this->chartDefinitionProvider = $chartDefinitionProvider;
}
/**
@ -182,7 +191,8 @@ class ViewDefinitionsHandler extends LegacyHandler implements ViewDefinitionsPro
): array {
$metadata = [
'columns' => [],
'bulkActions' => []
'bulkActions' => [],
'availableCharts' => []
];
/* @noinspection PhpIncludeInspection */
@ -198,6 +208,7 @@ class ViewDefinitionsHandler extends LegacyHandler implements ViewDefinitionsPro
$metadata['columns'] = $data;
$metadata['bulkActions'] = $this->bulkActionDefinitionProvider->getBulkActions($module);
$metadata['availableCharts'] = $this->chartDefinitionProvider->getCharts($module);
return $metadata;
}

View file

@ -0,0 +1,34 @@
<?php
namespace App\Service;
/**
* Class ChartDefinitionProvider
* @package App\Service
*/
class ChartDefinitionProvider implements ChartDefinitionProviderInterface
{
/**
* @var array
*/
private $listViewAvailableCharts;
/**
* ChartDefinitionProvider constructor.
* @param array $listViewAvailableCharts
*/
public function __construct(array $listViewAvailableCharts)
{
$this->listViewAvailableCharts = $listViewAvailableCharts;
}
/**
* @param string $module
* @return array
*/
public function getCharts(string $module): array
{
return $this->listViewAvailableCharts['modules'][$module] ?? [];
}
}

View file

@ -0,0 +1,18 @@
<?php
namespace App\Service;
/**
* Interface ChartDefinitionProviderInterface
* @package App\Service
*/
interface ChartDefinitionProviderInterface
{
/**
* Get list of charts for module
* @param string $module
* @return array
*/
public function getCharts(string $module): array;
}

View file

@ -4,6 +4,7 @@ namespace App\Tests;
use App\Service\AclManagerInterface;
use App\Service\BulkActionDefinitionProvider;
use App\Service\ChartDefinitionProvider;
use Codeception\Test\Unit;
use Exception;
use SuiteCRM\Core\Legacy\AclHandler;
@ -79,6 +80,26 @@ final class ViewDefinitionsHandlerTest extends Unit
]
];
$chartDefinitions = [
'modules' => [
'Accounts' => [
'key' => 'annual_revenue',
'labelKey' => 'ANNUAL_REVENUE_BY_ACCOUNTS',
'type' => 'line',
],
'Opportunities' => [
'key' => 'pipeline_by_sales_state',
'labelKey' => 'PIPELINE_BY_SALES_STAGE',
'params' => 'bar',
],
'Leads' => [
'key' => 'leads_by_source',
'labelKey' => 'LEADS_BY_SOURCE',
'params' => 'line',
],
],
];
/** @var AclManagerInterface $aclManager */
$aclManager = $this->make(
@ -101,6 +122,10 @@ final class ViewDefinitionsHandlerTest extends Unit
$aclManager
);
$chartDefinitionProvider = new ChartDefinitionProvider(
$chartDefinitions
);
$this->viewDefinitionHandler = new ViewDefinitionsHandler(
$projectDir,
$legacyDir,
@ -109,7 +134,8 @@ final class ViewDefinitionsHandlerTest extends Unit
$legacyScope,
$moduleNameMapper,
$fieldDefinitionsHandler,
$bulkActionProvider
$bulkActionProvider,
$chartDefinitionProvider
);
// Needed for aspect mock

View file

@ -0,0 +1,54 @@
<?php
namespace App\Tests;
use App\Service\ChartDefinitionProvider;
use Codeception\Test\Unit;
use Exception;
class ChartDefinitionProviderTest extends Unit
{
/**
* @var UnitTester
*/
protected $tester;
/**
* @var ChartDefinitionProvider
*/
protected $service;
/**
* @throws Exception
*/
protected function _before(): void
{
$listViewAvailableCharts = [
'modules' => [
'Accounts' => [
'key' => 'annual_revenue',
'labelKey' => 'ANNUAL_REVENUE_BY_ACCOUNTS',
'type' => 'line',
],
'Opportunities' => [
'key' => 'pipeline_by_sales_state',
'labelKey' => 'PIPELINE_BY_SALES_STAGE',
'type' => 'bar',
],
'Leads' => [
'key' => 'leads_by_source',
'labelKey' => 'LEADS_BY_SOURCE',
'type' => 'line',
]
]
];
$this->service = new ChartDefinitionProvider($listViewAvailableCharts);
}
public function testDefaultActionsRetrieval(): void
{
$actions = $this->service->getCharts('accounts');
static::assertNotNull($actions);
}
}