mirror of
https://github.com/SuiteCRM/SuiteCRM-Core.git
synced 2025-08-29 04:21:06 +08:00
Update list view api data retrieval
- Update ListViewHandler to use new method to retrieve data - Update View definitions api to send fieldDefinitions as sub-object - Update table and list view components to use new data format - Move common front end interfaces to a common module - UnEscape and sanitize html characters in varchar field -- Add htmlSanitize pipe - Update unit tests
This commit is contained in:
parent
b1c2f9efb1
commit
994b2c69bf
49 changed files with 307 additions and 158 deletions
|
@ -11,6 +11,7 @@ module.exports = {
|
|||
"map": [
|
||||
["@app", "./core/app/src/app"],
|
||||
["@base", "./core/app/src"],
|
||||
["@app-common", "./core/app/src/app-common"],
|
||||
["@views", "./core/app/views"],
|
||||
["@fields", "./core/app/fields"],
|
||||
["@services", "./core/app/src/services"],
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import {Input} from '@angular/core';
|
||||
import {Field} from '../field.model';
|
||||
import {FieldComponentInterface} from './field.interface';
|
||||
import {Field} from '@app-common/record/field.model';
|
||||
|
||||
export class BaseFieldComponent implements FieldComponentInterface {
|
||||
@Input() field: Field;
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import {Field} from '../field.model';
|
||||
import {Field} from '@app-common/record/field.model';
|
||||
|
||||
export interface FieldComponentInterface {
|
||||
field?: Field;
|
||||
|
|
|
@ -6,7 +6,7 @@ import {UserPreferenceStore} from '@store/user-preference/user-preference.store'
|
|||
import {SystemConfigStore} from '@store/system-config/system-config.store';
|
||||
import {distinctUntilChanged} from 'rxjs/operators';
|
||||
import {FormatCurrencyPipe} from '@base/pipes/format-currency/format-currency.pipe';
|
||||
import {Field} from '@fields/field.model';
|
||||
import {Field} from '@app-common/record/field.model';
|
||||
|
||||
@Component({
|
||||
selector: 'currency-detail-field-test-host-component',
|
||||
|
|
|
@ -6,7 +6,7 @@ import {BehaviorSubject, of} from 'rxjs';
|
|||
import {CommonModule} from '@angular/common';
|
||||
import {UserPreferenceStore} from '@store/user-preference/user-preference.store';
|
||||
import {SystemConfigStore} from '@store/system-config/system-config.store';
|
||||
import {Field} from '@fields/field.model';
|
||||
import {Field} from '@app-common/record/field.model';
|
||||
|
||||
@Component({
|
||||
selector: 'date-detail-field-test-host-component',
|
||||
|
|
|
@ -6,7 +6,7 @@ import {distinctUntilChanged} from 'rxjs/operators';
|
|||
import {BehaviorSubject, of} from 'rxjs';
|
||||
import {UserPreferenceStore} from '@store/user-preference/user-preference.store';
|
||||
import {SystemConfigStore} from '@store/system-config/system-config.store';
|
||||
import {Field} from '@fields/field.model';
|
||||
import {Field} from '@app-common/record/field.model';
|
||||
|
||||
@Component({
|
||||
selector: 'datetime-detail-field-test-host-component',
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import {async, ComponentFixture, TestBed} from '@angular/core/testing';
|
||||
import {EmailDetailFieldsComponent} from './email.component';
|
||||
import {Field} from '@fields/field.model';
|
||||
import {Component} from '@angular/core';
|
||||
import {Field} from '@app-common/record/field.model';
|
||||
|
||||
@Component({
|
||||
selector: 'email-detail-field-test-host-component',
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import {async, ComponentFixture, TestBed} from '@angular/core/testing';
|
||||
import {EmailListFieldsComponent} from './email.component';
|
||||
import {Field} from '@fields/field.model';
|
||||
import {Component} from '@angular/core';
|
||||
import {Field} from '@app-common/record/field.model';
|
||||
|
||||
@Component({
|
||||
selector: 'email-list-field-test-host-component',
|
||||
|
|
|
@ -1,9 +1,9 @@
|
|||
import {Component, Input} from '@angular/core';
|
||||
|
||||
import {Field} from './field.model';
|
||||
import {viewFieldsMap} from './field.manifest';
|
||||
import {Record} from '@store/list-view/list-view.store';
|
||||
import {ModuleNavigation} from '@services/navigation/module-navigation/module-navigation.service';
|
||||
import {Record} from '@app-common/record/record.model';
|
||||
import {Field} from '@app-common/record/field.model';
|
||||
|
||||
@Component({
|
||||
selector: 'scrm-field',
|
||||
|
|
|
@ -1,19 +0,0 @@
|
|||
import {SearchCriteriaFieldFilter} from '@store/list-view/list-view.store';
|
||||
|
||||
export interface FieldMetadata {
|
||||
format?: boolean;
|
||||
target?: string;
|
||||
link?: boolean;
|
||||
rows?: number;
|
||||
cols?: number;
|
||||
}
|
||||
|
||||
export interface Field {
|
||||
type: string;
|
||||
value?: string;
|
||||
name?: string;
|
||||
label?: string;
|
||||
labelKey?: string;
|
||||
metadata?: FieldMetadata;
|
||||
criteria?: SearchCriteriaFieldFilter;
|
||||
}
|
|
@ -6,7 +6,7 @@ import {UserPreferenceStore} from '@store/user-preference/user-preference.store'
|
|||
import {SystemConfigStore} from '@store/system-config/system-config.store';
|
||||
import {FormatNumberPipe} from '@base/pipes/format-number/format-number.pipe';
|
||||
import {distinctUntilChanged} from 'rxjs/operators';
|
||||
import {Field} from '@fields/field.model';
|
||||
import {Field} from '@app-common/record/field.model';
|
||||
|
||||
@Component({
|
||||
selector: 'float-detail-field-test-host-component',
|
||||
|
|
|
@ -5,7 +5,7 @@ import {UserPreferenceStore} from '@store/user-preference/user-preference.store'
|
|||
import {BehaviorSubject, of} from 'rxjs';
|
||||
import {SystemConfigStore} from '@store/system-config/system-config.store';
|
||||
import {FormatNumberPipe} from '@base/pipes/format-number/format-number.pipe';
|
||||
import {Field} from '@fields/field.model';
|
||||
import {Field} from '@app-common/record/field.model';
|
||||
|
||||
|
||||
@Component({
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import {async, ComponentFixture, TestBed} from '@angular/core/testing';
|
||||
import {Component} from '@angular/core';
|
||||
import {PhoneDetailFieldComponent} from './phone.component';
|
||||
import {Field} from '@fields/field.model';
|
||||
import {Field} from '@app-common/record/field.model';
|
||||
|
||||
@Component({
|
||||
selector: 'phone-detail-field-test-host-component',
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import {async, ComponentFixture, TestBed} from '@angular/core/testing';
|
||||
import {Component} from '@angular/core';
|
||||
import {TextDetailFieldComponent} from './text.component';
|
||||
import {Field} from '@fields/field.model';
|
||||
import {Field} from '@app-common/record/field.model';
|
||||
|
||||
@Component({
|
||||
selector: 'text-detail-field-test-host-component',
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import {async, ComponentFixture, TestBed} from '@angular/core/testing';
|
||||
import {Component} from '@angular/core';
|
||||
import {UrlDetailFieldComponent} from './url.component';
|
||||
import {Field, FieldMetadata} from '@fields/field.model';
|
||||
import {Field, FieldMetadata} from '@app-common/record/field.model';
|
||||
|
||||
@Component({
|
||||
selector: 'url-detail-field-test-host-component',
|
||||
|
|
|
@ -1 +1 @@
|
|||
{{field.value}}
|
||||
<span [innerHTML]="field.value | htmlSanitize"></span>
|
||||
|
|
|
@ -1,7 +1,8 @@
|
|||
import {async, ComponentFixture, TestBed} from '@angular/core/testing';
|
||||
import {Component} from '@angular/core';
|
||||
import {VarcharDetailFieldComponent} from './varchar.component';
|
||||
import {Field} from '@fields/field.model';
|
||||
import {Field} from '@app-common/record/field.model';
|
||||
import {HtmlSanitizeModule} from '@base/pipes/html-sanitize/html-sanitize.module';
|
||||
|
||||
@Component({
|
||||
selector: 'varchar-detail-field-test-host-component',
|
||||
|
@ -24,7 +25,9 @@ describe('VarcharDetailFieldComponent', () => {
|
|||
VarcharDetailFieldTestHostComponent,
|
||||
VarcharDetailFieldComponent,
|
||||
],
|
||||
imports: [],
|
||||
imports: [
|
||||
HtmlSanitizeModule
|
||||
],
|
||||
providers: [],
|
||||
}).compileComponents();
|
||||
|
||||
|
|
|
@ -3,13 +3,15 @@ import {CommonModule} from '@angular/common';
|
|||
|
||||
import {AppManagerModule} from '@base/app-manager/app-manager.module';
|
||||
import {VarcharDetailFieldComponent} from './varchar.component';
|
||||
import {HtmlSanitizeModule} from '@base/pipes/html-sanitize/html-sanitize.module';
|
||||
|
||||
@NgModule({
|
||||
declarations: [VarcharDetailFieldComponent],
|
||||
exports: [VarcharDetailFieldComponent],
|
||||
imports: [
|
||||
CommonModule,
|
||||
AppManagerModule.forChild(VarcharDetailFieldComponent)
|
||||
AppManagerModule.forChild(VarcharDetailFieldComponent),
|
||||
HtmlSanitizeModule
|
||||
]
|
||||
})
|
||||
export class VarcharDetailFieldModule {
|
||||
|
|
|
@ -1,8 +1,8 @@
|
|||
import {async, ComponentFixture, TestBed} from '@angular/core/testing';
|
||||
import {Component} from '@angular/core';
|
||||
import {VarcharEditFieldComponent} from './varchar.component';
|
||||
import {Field} from '@fields/field.model';
|
||||
import {FormsModule} from '@angular/forms';
|
||||
import {Field} from '@app-common/record/field.model';
|
||||
|
||||
@Component({
|
||||
selector: 'varchar-edit-field-test-host-component',
|
||||
|
|
|
@ -1,8 +1,8 @@
|
|||
import {async, ComponentFixture, TestBed} from '@angular/core/testing';
|
||||
import {Component} from '@angular/core';
|
||||
import {VarcharFilterFieldComponent} from './filter.component';
|
||||
import {Field} from '@fields/field.model';
|
||||
import {FormsModule} from '@angular/forms';
|
||||
import {Field} from '@app-common/record/field.model';
|
||||
|
||||
@Component({
|
||||
selector: 'varchar-filter-field-test-host-component',
|
||||
|
|
12
core/app/src/app-common/app-common.module.ts
Normal file
12
core/app/src/app-common/app-common.module.ts
Normal file
|
@ -0,0 +1,12 @@
|
|||
import {NgModule} from '@angular/core';
|
||||
import {CommonModule} from '@angular/common';
|
||||
|
||||
|
||||
@NgModule({
|
||||
declarations: [],
|
||||
imports: [
|
||||
CommonModule
|
||||
]
|
||||
})
|
||||
export class AppCommonModule {
|
||||
}
|
9
core/app/src/app-common/metadata/metadata.model.ts
Normal file
9
core/app/src/app-common/metadata/metadata.model.ts
Normal file
|
@ -0,0 +1,9 @@
|
|||
import {FieldDefinition} from '@app-common/record/field.model';
|
||||
|
||||
export interface ViewFieldDefinition {
|
||||
name?: string;
|
||||
label?: string;
|
||||
link?: boolean;
|
||||
type?: string;
|
||||
fieldDefinition?: FieldDefinition;
|
||||
}
|
68
core/app/src/app-common/record/field.model.ts
Normal file
68
core/app/src/app-common/record/field.model.ts
Normal file
|
@ -0,0 +1,68 @@
|
|||
import {Record} from './record.model';
|
||||
import {ViewFieldDefinition} from '@app-common/metadata/metadata.model';
|
||||
import {SearchCriteriaFieldFilter} from '@app-common/views/list/search-criteria.model';
|
||||
|
||||
export interface FieldDefinition {
|
||||
name?: string;
|
||||
type?: string; // label key to use
|
||||
vname?: string; // original label
|
||||
reportable?: boolean;
|
||||
required?: boolean;
|
||||
// eslint-disable-next-line camelcase
|
||||
duplicate_merge?: string;
|
||||
source?: string;
|
||||
id?: string;
|
||||
// eslint-disable-next-line camelcase
|
||||
id_name?: string;
|
||||
link?: string;
|
||||
module?: string;
|
||||
rname?: string;
|
||||
table?: string;
|
||||
}
|
||||
|
||||
export interface FieldMetadata {
|
||||
format?: boolean;
|
||||
target?: string;
|
||||
link?: boolean;
|
||||
rows?: number;
|
||||
cols?: number;
|
||||
}
|
||||
|
||||
export interface Field {
|
||||
type: string;
|
||||
value?: string;
|
||||
name?: string;
|
||||
label?: string;
|
||||
labelKey?: string;
|
||||
metadata?: FieldMetadata;
|
||||
definition?: FieldDefinition;
|
||||
criteria?: SearchCriteriaFieldFilter;
|
||||
}
|
||||
|
||||
export class FieldManager {
|
||||
|
||||
public static buildField(record: Record, viewField: ViewFieldDefinition): Field {
|
||||
|
||||
const definition = (viewField && viewField.fieldDefinition) || {} as FieldDefinition;
|
||||
const type = (viewField && viewField.type) || '';
|
||||
const source = (definition && definition.source) || '';
|
||||
const rname = (definition && definition.rname) || '';
|
||||
let value;
|
||||
|
||||
if (type === 'relate' && source === 'non-db' && rname !== '') {
|
||||
value = record.attributes[viewField.name][rname];
|
||||
} else {
|
||||
value = record.attributes[viewField.name];
|
||||
}
|
||||
|
||||
return {
|
||||
type: viewField.type,
|
||||
value,
|
||||
metadata: {
|
||||
link: viewField.link,
|
||||
},
|
||||
definition,
|
||||
labelKey: viewField.label
|
||||
} as Field;
|
||||
}
|
||||
}
|
10
core/app/src/app-common/record/record.model.ts
Normal file
10
core/app/src/app-common/record/record.model.ts
Normal file
|
@ -0,0 +1,10 @@
|
|||
export interface FieldMap {
|
||||
[key: string]: any;
|
||||
}
|
||||
|
||||
export interface Record {
|
||||
type: string;
|
||||
module: string;
|
||||
attributes: FieldMap;
|
||||
id?: string;
|
||||
}
|
17
core/app/src/app-common/views/list/search-criteria.model.ts
Normal file
17
core/app/src/app-common/views/list/search-criteria.model.ts
Normal file
|
@ -0,0 +1,17 @@
|
|||
export interface SearchCriteriaFieldFilter {
|
||||
field?: string;
|
||||
operator: string;
|
||||
values?: string[];
|
||||
start?: string;
|
||||
end?: string;
|
||||
}
|
||||
|
||||
export interface SearchCriteriaFilter {
|
||||
[key: string]: SearchCriteriaFieldFilter;
|
||||
}
|
||||
|
||||
export interface SearchCriteria {
|
||||
name?: string;
|
||||
type?: string;
|
||||
filters: SearchCriteriaFilter;
|
||||
}
|
|
@ -1,11 +1,12 @@
|
|||
import {Injectable} from '@angular/core';
|
||||
import {Observable} from 'rxjs';
|
||||
import {ListViewStore, Record} from '@store/list-view/list-view.store';
|
||||
import {ListViewStore} from '@store/list-view/list-view.store';
|
||||
import {map} from 'rxjs/operators';
|
||||
import {LineChartDataSource, LineChartResult} from '@components/chart/charts/line-chart/line-chart.model';
|
||||
import {SystemConfigStore} from '@store/system-config/system-config.store';
|
||||
import {DateTime} from 'luxon';
|
||||
import {LanguageStore} from '@store/language/language.store';
|
||||
import {Record} from '@app-common/record/record.model';
|
||||
|
||||
@Injectable()
|
||||
export class AccountTypesPerMonthLineChart implements LineChartDataSource {
|
||||
|
|
|
@ -1,12 +1,13 @@
|
|||
import {Injectable} from '@angular/core';
|
||||
import {Observable} from 'rxjs';
|
||||
import {Record, ListViewStore} from '@store/list-view/list-view.store';
|
||||
import {ListViewStore} from '@store/list-view/list-view.store';
|
||||
import {map} from 'rxjs/operators';
|
||||
import {
|
||||
PieGridChartChartDataSource,
|
||||
PieGridChartResult
|
||||
} from '@components/chart/charts/pie-grid-chart/pie-grid-chart.model';
|
||||
import {LanguageStore} from '@store/language/language.store';
|
||||
import {Record} from '@app-common/record/record.model';
|
||||
|
||||
@Injectable()
|
||||
export class LeadsByStatus implements PieGridChartChartDataSource {
|
||||
|
|
|
@ -4,11 +4,12 @@ import {
|
|||
VerticalBarChartResult
|
||||
} from '@components/chart/charts/vertical-bar-chart/vertical-bar-chart.model';
|
||||
import {Observable} from 'rxjs';
|
||||
import {Record, ListViewStore} from '@store/list-view/list-view.store';
|
||||
import {ListViewStore} from '@store/list-view/list-view.store';
|
||||
import {map} from 'rxjs/operators';
|
||||
import {LanguageStore} from '@store/language/language.store';
|
||||
import {UserPreferenceStore} from '@store/user-preference/user-preference.store';
|
||||
import {SystemConfigStore} from '@store/system-config/system-config.store';
|
||||
import {Record} from '@app-common/record/record.model';
|
||||
|
||||
@Injectable()
|
||||
export class PipelineBySalesStage implements VerticalBarChartDataSource {
|
||||
|
|
|
@ -7,10 +7,10 @@ import {DropdownButtonModule} from '@components/dropdown-button/dropdown-button.
|
|||
import {BrowserDynamicTestingModule} from '@angular/platform-browser-dynamic/testing';
|
||||
import {LayoutModule} from '@angular/cdk/layout';
|
||||
import {FieldModule} from '@fields/field.module';
|
||||
import {Field} from '@fields/field.model';
|
||||
import {By} from '@angular/platform-browser';
|
||||
import {CommonModule} from '@angular/common';
|
||||
import {RouterTestingModule} from '@angular/router/testing';
|
||||
import {Field} from '@app-common/record/field.model';
|
||||
|
||||
@Component({
|
||||
selector: 'field-grid-host-component',
|
||||
|
|
|
@ -1,8 +1,8 @@
|
|||
import {Component, Input, OnChanges, OnDestroy, OnInit} from '@angular/core';
|
||||
import {Field} from '@fields/field.model';
|
||||
import {FieldGridColumn, FieldGridRow} from '@components/field-grid/field-grid.model';
|
||||
import {BreakpointObserver, Breakpoints, BreakpointState} from '@angular/cdk/layout';
|
||||
import {Subscription} from 'rxjs';
|
||||
import {Field} from '@app-common/record/field.model';
|
||||
|
||||
@Component({
|
||||
selector: 'scrm-field-grid',
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import {Field} from '@fields/field.model';
|
||||
import {ButtonInterface} from '@components/button/button.model';
|
||||
import {Field} from '@app-common/record/field.model';
|
||||
|
||||
export interface FieldGridColumn {
|
||||
field?: Field;
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import {Component, Input, OnInit} from '@angular/core';
|
||||
import {LanguageStore} from '@store/language/language.store';
|
||||
import {LineAction, ListViewMeta} from '@store/metadata/metadata.store.service';
|
||||
import {Record} from '@store/list-view/list-view.store';
|
||||
import {Record} from '@app-common/record/record.model';
|
||||
|
||||
@Component({
|
||||
selector: 'scrm-line-action-menu',
|
||||
|
|
|
@ -1,13 +1,14 @@
|
|||
import {Component, OnInit} from '@angular/core';
|
||||
import {BulkActionsMap, Filter, SearchMetaField} from '@store/metadata/metadata.store.service';
|
||||
import {Filter, SearchMetaField} from '@store/metadata/metadata.store.service';
|
||||
import {LanguageStore, LanguageStringMap, LanguageStrings} from '@store/language/language.store';
|
||||
import {Field} from '@fields/field.model';
|
||||
import {ListViewStore, SearchCriteria, SearchCriteriaFieldFilter} from '@store/list-view/list-view.store';
|
||||
import {ListViewStore} from '@store/list-view/list-view.store';
|
||||
import {DropdownButtonInterface} from '@components/dropdown-button/dropdown-button.model';
|
||||
import {ButtonInterface} from '@components/button/button.model';
|
||||
import {deepClone} from '@base/utils/object-utils';
|
||||
import {combineLatest, Observable} from 'rxjs';
|
||||
import {map} from 'rxjs/operators';
|
||||
import {Field} from '@app-common/record/field.model';
|
||||
import {SearchCriteria, SearchCriteriaFieldFilter} from '@app-common/views/list/search-criteria.model';
|
||||
|
||||
export interface FilterDataSource {
|
||||
getFilter(): Observable<Filter>;
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import {Component, OnInit} from '@angular/core';
|
||||
import {ListViewStore, SearchCriteriaFilter} from '@store/list-view/list-view.store';
|
||||
import {ListViewStore} from '@store/list-view/list-view.store';
|
||||
import {ButtonInterface} from '@components/button/button.model';
|
||||
import {DropdownButtonInterface} from '@components/dropdown-button/dropdown-button.model';
|
||||
import {ButtonGroupInterface} from '@components/button-group/button-group.model';
|
||||
|
@ -9,6 +9,7 @@ import {BehaviorSubject, combineLatest} from 'rxjs';
|
|||
import {map} from 'rxjs/operators';
|
||||
import {ScreenSize, ScreenSizeObserverService} from '@services/ui/screen-size-observer/screen-size-observer.service';
|
||||
import {SystemConfigStore} from '@store/system-config/system-config.store';
|
||||
import {SearchCriteriaFilter} from '@app-common/views/list/search-criteria.model';
|
||||
|
||||
@Component({
|
||||
selector: 'scrm-settings-menu',
|
||||
|
|
|
@ -15,7 +15,7 @@
|
|||
</td>
|
||||
</ng-container>
|
||||
|
||||
<ng-container *ngFor="let column of vm.listMetadata.fields" [cdkColumnDef]="column.fieldName">
|
||||
<ng-container *ngFor="let column of vm.listMetadata.fields" [cdkColumnDef]="column.name">
|
||||
<th cdk-header-cell
|
||||
*cdkHeaderCellDef
|
||||
scope="col"
|
||||
|
|
|
@ -1,14 +1,15 @@
|
|||
import {Component, Input} from '@angular/core';
|
||||
import {combineLatest, Observable} from 'rxjs';
|
||||
import {LanguageStore, LanguageStrings} from '@store/language/language.store';
|
||||
import {ListField, ListViewMeta, MetadataStore} from '@store/metadata/metadata.store.service';
|
||||
import {ColumnDefinition, ListViewMeta, MetadataStore} from '@store/metadata/metadata.store.service';
|
||||
import {map} from 'rxjs/operators';
|
||||
import {ListViewStore, Record, RecordSelection, SortingSelection} from '@store/list-view/list-view.store';
|
||||
import {ListViewStore, RecordSelection, SortingSelection} from '@store/list-view/list-view.store';
|
||||
import {SelectionStatus} from '@components/bulk-action-menu/bulk-action-menu.component';
|
||||
import {SortDirection, SortDirectionDataSource} from '@components/sort-button/sort-button.model';
|
||||
import {ScreenSize, ScreenSizeObserverService} from '@services/ui/screen-size-observer/screen-size-observer.service';
|
||||
import {SystemConfigStore} from '@store/system-config/system-config.store';
|
||||
import {Field} from '@fields/field.model';
|
||||
import {Record} from '@app-common/record/record.model';
|
||||
import {Field, FieldManager} from '@app-common/record/field.model';
|
||||
|
||||
@Component({
|
||||
selector: 'scrm-table-body',
|
||||
|
@ -91,15 +92,15 @@ export class TableBodyComponent {
|
|||
let hasLinkField = false;
|
||||
const returnArray = [];
|
||||
while (i < this.maxColumns && i < listMetadata.fields.length) {
|
||||
returnArray.push(listMetadata.fields[i].fieldName);
|
||||
returnArray.push(listMetadata.fields[i].name);
|
||||
hasLinkField = hasLinkField || listMetadata.fields[i].link;
|
||||
i++;
|
||||
}
|
||||
if (!hasLinkField && (this.maxColumns < listMetadata.fields.length)) {
|
||||
for (i = this.maxColumns; i < listMetadata.fields.length; i++) {
|
||||
if (listMetadata.fields[i].link) {
|
||||
returnArray.splice(-1,1);
|
||||
returnArray.push(listMetadata.fields[i].fieldName);
|
||||
returnArray.splice(-1, 1);
|
||||
returnArray.push(listMetadata.fields[i].name);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
@ -131,13 +132,13 @@ export class TableBodyComponent {
|
|||
return this.language.getFieldLabel(label, module, languages);
|
||||
}
|
||||
|
||||
getFieldSort(field: ListField): SortDirectionDataSource {
|
||||
getFieldSort(field: ColumnDefinition): SortDirectionDataSource {
|
||||
return {
|
||||
getSortDirection: (): Observable<SortDirection> => this.sort$.pipe(
|
||||
map((sort: SortingSelection) => {
|
||||
let direction = SortDirection.NONE;
|
||||
|
||||
if (sort.orderBy === field.fieldName) {
|
||||
if (sort.orderBy === field.name) {
|
||||
direction = sort.sortOrder;
|
||||
}
|
||||
|
||||
|
@ -145,20 +146,14 @@ export class TableBodyComponent {
|
|||
})
|
||||
),
|
||||
changeSortDirection: (direction: SortDirection): void => {
|
||||
this.changeSort(field.fieldName, direction);
|
||||
this.changeSort(field.name, direction);
|
||||
}
|
||||
} as SortDirectionDataSource;
|
||||
}
|
||||
|
||||
getField(column: ListField, record: Record): Field {
|
||||
return {
|
||||
type: column.type,
|
||||
value: record.attributes[column.fieldName],
|
||||
metadata: {
|
||||
link: column.link,
|
||||
},
|
||||
labelKey: column.label
|
||||
} as Field;
|
||||
getField(column: ColumnDefinition, record: Record): Field {
|
||||
|
||||
return FieldManager.buildField(record, column);
|
||||
}
|
||||
|
||||
protected changeSort(orderBy: string, sortOrder: SortDirection): void {
|
||||
|
|
18
core/app/src/pipes/html-sanitize/html-sanitize.module.ts
Normal file
18
core/app/src/pipes/html-sanitize/html-sanitize.module.ts
Normal file
|
@ -0,0 +1,18 @@
|
|||
import {NgModule} from '@angular/core';
|
||||
import {CommonModule} from '@angular/common';
|
||||
import {HtmlSanitizePipe} from '@base/pipes/html-sanitize/html-sanitize.pipe';
|
||||
|
||||
|
||||
@NgModule({
|
||||
declarations: [
|
||||
HtmlSanitizePipe
|
||||
],
|
||||
exports: [
|
||||
HtmlSanitizePipe
|
||||
],
|
||||
imports: [
|
||||
CommonModule
|
||||
]
|
||||
})
|
||||
export class HtmlSanitizeModule {
|
||||
}
|
37
core/app/src/pipes/html-sanitize/html-sanitize.pipe.spec.ts
Normal file
37
core/app/src/pipes/html-sanitize/html-sanitize.pipe.spec.ts
Normal file
|
@ -0,0 +1,37 @@
|
|||
import {HtmlSanitizePipe} from './html-sanitize.pipe';
|
||||
import {async, ComponentFixture, TestBed} from '@angular/core/testing';
|
||||
import {Component} from '@angular/core';
|
||||
import {BrowserDynamicTestingModule} from '@angular/platform-browser-dynamic/testing';
|
||||
|
||||
@Component({
|
||||
selector: 'html-sanitize-test-host-component',
|
||||
template: '<span [innerHTML]="value | htmlSanitize"></span>'
|
||||
})
|
||||
class HtmlSanitizePipeTestHostComponent {
|
||||
value = 'V8 Api test Account &ç!"#$%&/`à';
|
||||
}
|
||||
|
||||
describe('HtmlSanitizePipe', () => {
|
||||
let testHostComponent: HtmlSanitizePipeTestHostComponent;
|
||||
let testHostFixture: ComponentFixture<HtmlSanitizePipeTestHostComponent>;
|
||||
|
||||
beforeEach(async(() => {
|
||||
TestBed.configureTestingModule({
|
||||
declarations: [
|
||||
HtmlSanitizePipeTestHostComponent,
|
||||
HtmlSanitizePipe,
|
||||
],
|
||||
imports: [BrowserDynamicTestingModule],
|
||||
providers: [],
|
||||
}).compileComponents();
|
||||
|
||||
testHostFixture = TestBed.createComponent(HtmlSanitizePipeTestHostComponent);
|
||||
testHostComponent = testHostFixture.componentInstance;
|
||||
testHostFixture.detectChanges();
|
||||
}));
|
||||
|
||||
it('create an instance', () => {
|
||||
expect(testHostComponent).toBeTruthy();
|
||||
expect(testHostFixture.nativeElement.textContent).toContain('V8 Api test Account &ç!"#$%&/`à');
|
||||
});
|
||||
});
|
15
core/app/src/pipes/html-sanitize/html-sanitize.pipe.ts
Normal file
15
core/app/src/pipes/html-sanitize/html-sanitize.pipe.ts
Normal file
|
@ -0,0 +1,15 @@
|
|||
import {Pipe, PipeTransform, SecurityContext} from '@angular/core';
|
||||
import {DomSanitizer, SafeHtml} from '@angular/platform-browser';
|
||||
|
||||
@Pipe({
|
||||
name: 'htmlSanitize'
|
||||
})
|
||||
export class HtmlSanitizePipe implements PipeTransform {
|
||||
|
||||
constructor(private sanitizer: DomSanitizer) {
|
||||
}
|
||||
|
||||
transform(data): SafeHtml {
|
||||
return this.sanitizer.sanitize(SecurityContext.HTML, data);
|
||||
}
|
||||
}
|
|
@ -3,11 +3,12 @@ import {Observable} from 'rxjs';
|
|||
import {catchError, take, tap} from 'rxjs/operators';
|
||||
import {Process, ProcessService} from '@services/process/process.service';
|
||||
import {AppStateStore} from '@store/app-state/app-state.store';
|
||||
import {SearchCriteria, SortingSelection} from '@store/list-view/list-view.store';
|
||||
import {SortingSelection} from '@store/list-view/list-view.store';
|
||||
import {MessageService} from '@services/message/message.service';
|
||||
import {BulkActionHandler} from '@services/process/processes/bulk-action/bulk-action.model';
|
||||
import {RedirectBulkAction} from '@services/process/processes/bulk-action/actions/redirect/redirect.bulk-action';
|
||||
import {ExportBulkAction} from '@services/process/processes/bulk-action/actions/export/export.bulk-action';
|
||||
import {SearchCriteria} from '@app-common/views/list/search-criteria.model';
|
||||
|
||||
export interface BulkActionProcessInput {
|
||||
action: string;
|
||||
|
|
|
@ -1,9 +1,10 @@
|
|||
import {ListViewStore, SearchCriteria} from '@store/list-view/list-view.store';
|
||||
import {ListViewStore} from '@store/list-view/list-view.store';
|
||||
import {listviewMockData, listviewStoreMock} from '@store/list-view/list-view.store.spec.mock';
|
||||
import {SelectionStatus} from '@components/bulk-action-menu/bulk-action-menu.component';
|
||||
import {take} from 'rxjs/operators';
|
||||
import {PageSelection} from '@components/pagination/pagination.model';
|
||||
import {localStorageServiceMock} from '@services/local-storage/local-storage.service.spec.mock';
|
||||
import {SearchCriteria} from '@app-common/views/list/search-criteria.model';
|
||||
|
||||
describe('Listview Store', () => {
|
||||
const service: ListViewStore = listviewStoreMock;
|
||||
|
|
|
@ -26,35 +26,8 @@ import {SortDirection} from '@components/sort-button/sort-button.model';
|
|||
import {BulkActionProcess, BulkActionProcessInput} from '@services/process/processes/bulk-action/bulk-action';
|
||||
import {MessageService} from '@services/message/message.service';
|
||||
import {Process} from '@services/process/process.service';
|
||||
|
||||
export interface FieldMap {
|
||||
[key: string]: any;
|
||||
}
|
||||
|
||||
export interface Record {
|
||||
type: string;
|
||||
module: string;
|
||||
attributes: FieldMap;
|
||||
id?: string;
|
||||
}
|
||||
|
||||
export interface SearchCriteriaFieldFilter {
|
||||
field?: string;
|
||||
operator: string;
|
||||
values?: string[];
|
||||
start?: string;
|
||||
end?: string;
|
||||
}
|
||||
|
||||
export interface SearchCriteriaFilter {
|
||||
[key: string]: SearchCriteriaFieldFilter;
|
||||
}
|
||||
|
||||
export interface SearchCriteria {
|
||||
name?: string;
|
||||
type?: string;
|
||||
filters: SearchCriteriaFilter;
|
||||
}
|
||||
import {Record} from '@app-common/record/record.model';
|
||||
import {SearchCriteria} from '@app-common/views/list/search-criteria.model';
|
||||
|
||||
const initialSearchCriteria = {
|
||||
filters: {}
|
||||
|
@ -536,7 +509,7 @@ export class ListViewStore extends ViewStore implements StateStore,
|
|||
const displayedFields = [];
|
||||
|
||||
this.metadata.listView.fields.forEach(value => {
|
||||
displayedFields.push(value.fieldName);
|
||||
displayedFields.push(value.name);
|
||||
});
|
||||
|
||||
const data = {
|
||||
|
@ -761,7 +734,7 @@ export class ListViewStore extends ViewStore implements StateStore,
|
|||
.pipe(map(({data}) => {
|
||||
const recordsList: ListData = {
|
||||
records: [],
|
||||
pagination: {} as Pagination
|
||||
pagination: {...pagination} as Pagination
|
||||
};
|
||||
|
||||
if (!data || !data.getListView) {
|
||||
|
|
|
@ -6,6 +6,7 @@ import {deepClone} from '@base/utils/object-utils';
|
|||
import {StateStore} from '@base/store/state';
|
||||
import {AppStateStore} from '@store/app-state/app-state.store';
|
||||
import {MenuItemLink} from '@components/navbar/navbar.abstract';
|
||||
import {ViewFieldDefinition} from '@app-common/metadata/metadata.model';
|
||||
|
||||
export interface ChartType {
|
||||
key: string;
|
||||
|
@ -44,7 +45,7 @@ export interface LineAction {
|
|||
}
|
||||
|
||||
export interface ListViewMeta {
|
||||
fields: ListField[];
|
||||
fields: ColumnDefinition[];
|
||||
bulkActions: BulkActionsMap;
|
||||
lineActions: LineAction[];
|
||||
chartTypes: ChartTypesMap;
|
||||
|
@ -57,16 +58,12 @@ export interface Filter {
|
|||
contents: { [key: string]: any };
|
||||
}
|
||||
|
||||
export interface ListField {
|
||||
fieldName: string;
|
||||
export interface ColumnDefinition extends ViewFieldDefinition {
|
||||
width: string;
|
||||
label: string;
|
||||
link: boolean;
|
||||
default: boolean;
|
||||
module: string;
|
||||
id: string;
|
||||
sortable: boolean;
|
||||
type: string;
|
||||
}
|
||||
|
||||
export interface SearchMetaField {
|
||||
|
@ -117,7 +114,7 @@ export class MetadataStore implements StateStore {
|
|||
/**
|
||||
* Public long-lived observable streams
|
||||
*/
|
||||
fields$: Observable<ListField[]>;
|
||||
fields$: Observable<ColumnDefinition[]>;
|
||||
listMetadata$: Observable<ListViewMeta>;
|
||||
searchMetadata$: Observable<SearchMeta>;
|
||||
metadata$: Observable<Metadata>;
|
||||
|
@ -291,7 +288,7 @@ export class MetadataStore implements StateStore {
|
|||
};
|
||||
|
||||
if (data.viewDefinition.listView.columns) {
|
||||
data.viewDefinition.listView.columns.forEach((field: ListField) => {
|
||||
data.viewDefinition.listView.columns.forEach((field: ColumnDefinition) => {
|
||||
listViewMeta.fields.push(
|
||||
field
|
||||
);
|
||||
|
|
|
@ -184,7 +184,7 @@ export const metadataMockData = {
|
|||
},
|
||||
columns: [
|
||||
{
|
||||
fieldName: 'name',
|
||||
name: 'name',
|
||||
width: '20%',
|
||||
label: 'LBL_LIST_ACCOUNT_NAME',
|
||||
link: true,
|
||||
|
@ -194,7 +194,7 @@ export const metadataMockData = {
|
|||
sortable: false
|
||||
},
|
||||
{
|
||||
fieldName: 'billing_address_city',
|
||||
name: 'billing_address_city',
|
||||
width: '10%',
|
||||
label: 'LBL_LIST_CITY',
|
||||
link: false,
|
||||
|
@ -204,7 +204,7 @@ export const metadataMockData = {
|
|||
sortable: false
|
||||
},
|
||||
{
|
||||
fieldName: 'billing_address_country',
|
||||
name: 'billing_address_country',
|
||||
width: '10%',
|
||||
label: 'LBL_BILLING_ADDRESS_COUNTRY',
|
||||
link: false,
|
||||
|
@ -214,7 +214,7 @@ export const metadataMockData = {
|
|||
sortable: false
|
||||
},
|
||||
{
|
||||
fieldName: 'phone_office',
|
||||
name: 'phone_office',
|
||||
width: '10%',
|
||||
label: 'LBL_LIST_PHONE',
|
||||
link: false,
|
||||
|
@ -224,7 +224,7 @@ export const metadataMockData = {
|
|||
sortable: false
|
||||
},
|
||||
{
|
||||
fieldName: 'assigned_user_name',
|
||||
name: 'assigned_user_name',
|
||||
width: '10%',
|
||||
label: 'LBL_LIST_ASSIGNED_USER',
|
||||
link: false,
|
||||
|
@ -234,7 +234,7 @@ export const metadataMockData = {
|
|||
sortable: false
|
||||
},
|
||||
{
|
||||
fieldName: 'email1',
|
||||
name: 'email1',
|
||||
width: '15%',
|
||||
label: 'LBL_EMAIL_ADDRESS',
|
||||
link: true,
|
||||
|
@ -245,7 +245,7 @@ export const metadataMockData = {
|
|||
customCode: '{$EMAIL1_LINK}'
|
||||
},
|
||||
{
|
||||
fieldName: 'date_entered',
|
||||
name: 'date_entered',
|
||||
width: '5%',
|
||||
label: 'LBL_DATE_ENTERED',
|
||||
link: false,
|
||||
|
|
|
@ -12,8 +12,8 @@ import {ModuleNavigation} from '@services/navigation/module-navigation/module-na
|
|||
import {LocalStorageService} from '@services/local-storage/local-storage.service';
|
||||
import {MessageService} from '@services/message/message.service';
|
||||
import {distinctUntilChanged, map, shareReplay, tap, catchError} from 'rxjs/operators';
|
||||
import {Record} from '@store/list-view/list-view.store';
|
||||
import {RecordViewGQL} from '@store/record-view/api.record.get';
|
||||
import {Record} from '@app-common/record/record.model';
|
||||
|
||||
export interface RecordViewModel {
|
||||
data: RecordViewData;
|
||||
|
|
|
@ -15,13 +15,30 @@
|
|||
"../../node_modules/@types"
|
||||
],
|
||||
"paths": {
|
||||
"@app/*": ["./src/app/*"],
|
||||
"@base/*": ["./src/*"],
|
||||
"@views/*": ["./views/*"],
|
||||
"@fields/*": ["./fields/*"],
|
||||
"@services/*": ["./src/services/*"],
|
||||
"@components/*": ["./src/components/*"],
|
||||
"@store/*": ["./src/store/*"]
|
||||
"@app/*": [
|
||||
"./src/app/*"
|
||||
],
|
||||
"@base/*": [
|
||||
"./src/*"
|
||||
],
|
||||
"@app-common/*": [
|
||||
"./src/app-common/*"
|
||||
],
|
||||
"@views/*": [
|
||||
"./views/*"
|
||||
],
|
||||
"@fields/*": [
|
||||
"./fields/*"
|
||||
],
|
||||
"@services/*": [
|
||||
"./src/services/*"
|
||||
],
|
||||
"@components/*": [
|
||||
"./src/components/*"
|
||||
],
|
||||
"@store/*": [
|
||||
"./src/store/*"
|
||||
]
|
||||
},
|
||||
"lib": [
|
||||
"es2018",
|
||||
|
|
|
@ -247,7 +247,7 @@ class ListViewHandler extends LegacyHandler implements ListViewProviderInterface
|
|||
require_once 'include/portability/ListView/ListViewDataPort.php';
|
||||
$listViewData = new ListViewDataPort();
|
||||
|
||||
return $listViewData->getListViewData($bean, $where, $offset, $limit, $filter_fields, $params);
|
||||
return $listViewData->get($bean, $where, $offset, $limit, $filter_fields, $params);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -25,13 +25,11 @@ class ViewDefinitionsHandler extends LegacyHandler implements ViewDefinitionsPro
|
|||
{
|
||||
public const HANDLER_KEY = 'view-definitions';
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* @var array
|
||||
*/
|
||||
protected static $listViewColumnInterface = [
|
||||
'fieldName' => '',
|
||||
'name' => '',
|
||||
'width' => '',
|
||||
'label' => '',
|
||||
'link' => false,
|
||||
|
@ -72,21 +70,19 @@ class ViewDefinitionsHandler extends LegacyHandler implements ViewDefinitionsPro
|
|||
*/
|
||||
private $filterDefinitionProvider;
|
||||
|
||||
/**
|
||||
* @var array
|
||||
*/
|
||||
private $defaultFields = [
|
||||
'type' => 'fieldType',
|
||||
'type' => 'type',
|
||||
'label' => 'vname',
|
||||
];
|
||||
|
||||
private $vardefsToRename = [
|
||||
'type' => 'fieldType',
|
||||
];
|
||||
|
||||
/**
|
||||
* @var ModuleNameMapperInterface
|
||||
*/
|
||||
protected $moduleNameMapper;
|
||||
|
||||
|
||||
/**
|
||||
* ViewDefinitionsHandler constructor.
|
||||
* @param string $projectDir
|
||||
|
@ -272,11 +268,11 @@ class ViewDefinitionsHandler extends LegacyHandler implements ViewDefinitionsPro
|
|||
protected function buildListViewColumn($column, $key, ?array $vardefs): array
|
||||
{
|
||||
$column = array_merge(self::$listViewColumnInterface, $column);
|
||||
$column['fieldName'] = strtolower($key);
|
||||
$column['name'] = strtolower($key);
|
||||
|
||||
$column = $this->addFieldDefinition($vardefs, strtolower($key), $column);
|
||||
|
||||
if ($column['fieldName'] === 'email1') {
|
||||
if ($column['name'] === 'email1') {
|
||||
$column['type'] = 'email';
|
||||
$column['link'] = false;
|
||||
}
|
||||
|
@ -360,17 +356,7 @@ class ViewDefinitionsHandler extends LegacyHandler implements ViewDefinitionsPro
|
|||
return $field;
|
||||
}
|
||||
|
||||
foreach( $vardefs[$key] as $attributeName => $attributeValue){
|
||||
$attribute = $attributeName;
|
||||
|
||||
if (!empty($this->vardefsToRename[$attributeName])){
|
||||
$attribute = $this->vardefsToRename[$attributeName];
|
||||
}
|
||||
|
||||
if (!isset($field[$attribute]) || empty($field[$attribute])) {
|
||||
$field[$attribute] = $attributeValue;
|
||||
}
|
||||
}
|
||||
$field['fieldDefinition'] = $vardefs[$key];
|
||||
|
||||
$field = $this->applyDefaults($field);
|
||||
|
||||
|
@ -390,10 +376,6 @@ class ViewDefinitionsHandler extends LegacyHandler implements ViewDefinitionsPro
|
|||
foreach ($definition['layout'][$type] as $key => $field) {
|
||||
$name = $field['name'] ?? '';
|
||||
|
||||
if (empty($name)) {
|
||||
$name = $field['fieldName'] ?? '';
|
||||
}
|
||||
|
||||
if ($this->useRangeSearch($module, $searchDefs, $name)) {
|
||||
$definition['layout'][$type][$key]['enable_range_search'] = true;
|
||||
}
|
||||
|
@ -478,7 +460,6 @@ class ViewDefinitionsHandler extends LegacyHandler implements ViewDefinitionsPro
|
|||
if (is_string($field)) {
|
||||
$baseField = [
|
||||
'name' => $field,
|
||||
'fieldName' => $field
|
||||
];
|
||||
}
|
||||
|
||||
|
@ -487,14 +468,14 @@ class ViewDefinitionsHandler extends LegacyHandler implements ViewDefinitionsPro
|
|||
|
||||
/**
|
||||
* Apply defaults
|
||||
* @param $field
|
||||
* @return mixed
|
||||
* @param array $field
|
||||
* @return array
|
||||
*/
|
||||
protected function applyDefaults($field)
|
||||
protected function applyDefaults(array $field): array
|
||||
{
|
||||
foreach ($this->defaultFields as $attribute => $default) {
|
||||
if (empty($field[$attribute])) {
|
||||
$defaultValue = $field[$default] ?? '';
|
||||
$defaultValue = $field['fieldDefinition'][$default] ?? '';
|
||||
$field[$attribute] = $defaultValue;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -219,12 +219,18 @@ final class ViewDefinitionsHandlerTest extends Unit
|
|||
static::assertIsArray($firstColumn);
|
||||
static::assertNotEmpty($firstColumn);
|
||||
|
||||
static::assertArrayHasKey('fieldName', $firstColumn);
|
||||
static::assertArrayHasKey('name', $firstColumn);
|
||||
static::assertArrayHasKey('label', $firstColumn);
|
||||
static::assertArrayHasKey('link', $firstColumn);
|
||||
static::assertIsBool($firstColumn['link']);
|
||||
static::assertArrayHasKey('sortable', $firstColumn);
|
||||
static::assertIsBool($firstColumn['sortable']);
|
||||
static::assertArrayHasKey('fieldDefinition', $firstColumn);
|
||||
static::assertIsArray($firstColumn['fieldDefinition']);
|
||||
|
||||
static::assertArrayHasKey('name', $firstColumn['fieldDefinition']);
|
||||
static::assertArrayHasKey('type', $firstColumn['fieldDefinition']);
|
||||
static::assertArrayHasKey('vname', $firstColumn['fieldDefinition']);
|
||||
|
||||
$actions = $listViewDefs->getListView()['bulkActions'];
|
||||
static::assertIsArray($actions);
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue