Add standard action handling

- Make all actions consistent
- Add base classes for action adapters and managers
- Remove store dependency on table component

- Make all actions consistent
- Add base classes for action adapters and managers
- Re-adjust existing and affected actions
- Remove store dependency on table component
- Add subpanel line actions adapter
- Remove unlink action. Now all backend backend driven

- Re-adjust line actions component
-- Use action interface
-- add line action limits configuration

- Refactor subpanel line actions
- Make table line actions optional
This commit is contained in:
Clemente Raposo 2021-05-07 02:25:13 +01:00 committed by Dillon-Brown
parent a68785147e
commit 453a0d3282
59 changed files with 1344 additions and 579 deletions

View file

@ -27,6 +27,7 @@ services:
$navigationTabLimits: '%themes.navigation_tab_limits%'
$listViewBulkActions: '%module.listview.bulk_action%'
$listViewLineActions: '%module.listview.line_action%'
$listViewLineActionsLimits: '%module.listview.line_actions_limits%'
$listViewSidebarWidgets: '%module.listview.sidebar_widgets%'
$listViewColumnLimits: '%module.listview.column_limits%'
$listViewSettingsLimits: '%module.listview.settings_limits%'

View file

@ -22,6 +22,7 @@ parameters:
listview_column_limits: true
listview_settings_limits: true
listview_actions_limits: true
listview_line_actions_limits: true
module_routing: true
recordview_actions_limits: true
ui: true

View file

@ -6,29 +6,41 @@ parameters:
key: create
related_modules:
calls:
name : create-calls
module : calls
name: create-calls
module: calls
icon: phone
labelKey: LBL_SCHEDULE_CALL
returnAction: DetailView
params:
create:
module: calls
returnAction: DetailView
meetings:
name : create-meetings
name: create-meetings
module: meetings
icon: calendar
labelKey: LBL_SCHEDULE_MEETING
returnAction: DetailView
params:
create:
module: meetings
returnAction: DetailView
tasks:
name : create-tasks
name: create-tasks
module: tasks
icon: list
labelKey: LBL_CREATE_TASK
returnAction: DetailView
params:
create:
module: tasks
returnAction: DetailView
emails:
name : create-emails
name: create-emails
module: emails
icon: email
action: compose
labelKey: LBL_COMPOSE_EMAIL_BUTTON_LABEL
returnAction: index
params:
create:
module: emails
returnAction: index
acl:
- edit

View file

@ -0,0 +1,14 @@
parameters:
module.listview.line_actions_limits:
without_sidebar:
XSmall: 5
Small: 5
Medium: 5
Large: 7
XLarge: 8
with_sidebar:
XSmall: 5
Small: 5
Medium: 5
Large: 6
XLarge: 7

View file

@ -25,19 +25,30 @@
*/
import {Observable} from 'rxjs';
import {ViewMode} from '../views/view.model';
import {Record} from '../record/record.model';
import {SearchCriteria} from '../views/list/search-criteria.model';
export interface ActionData {
[key: string]: any;
}
export interface ActionHandlerMap {
[key: string]: ActionHandler;
export interface ActionHandlerMap<D extends ActionData> {
[key: string]: ActionHandler<D>;
}
export abstract class ActionHandler {
export abstract class ActionHandler<D extends ActionData> {
abstract key: string;
abstract run(data: ActionData): void;
abstract modes: ViewMode[];
abstract run(data: D, action?: Action): void;
getStatus(data: D): string {
return '';
}
abstract shouldDisplay(data: D): boolean;
}
export interface ModeActions {
@ -58,7 +69,23 @@ export interface Action {
}
export interface ActionDataSource {
getActions(): Observable<Action[]>;
getActions(context?: ActionContext): Observable<Action[]>;
runAction(action: Action): void;
runAction(action: Action, context?: ActionContext): void;
}
export interface ActionManager<D extends ActionData> {
run(action: Action, mode: ViewMode, data: D): void;
getHandler(action: Action, mode: ViewMode): ActionHandler<D>;
}
export interface ActionContext {
[key: string]: any;
module?: string;
record?: Record;
ids?: string[];
criteria?: SearchCriteria;
}

View file

@ -1,6 +1,5 @@
export * from './actions/action.model';
export * from './actions/bulk-action.model';
export * from './actions/line-action.model';
export * from './components/button/button-group.model';
export * from './components/button/button.model';
export * from './components/button/dropdown-button.model';

View file

@ -26,15 +26,15 @@
import {ViewFieldDefinition} from './metadata.model';
import {WidgetMetadata} from './widget.metadata';
import {LineAction} from '../actions/line-action.model';
import {FieldDefinition} from '../record/field.model';
import {BulkActionsMap} from '../actions/bulk-action.model';
import {ChartTypesMap} from '../containers/chart/chart.model';
import {Action} from '../actions/action.model';
export interface RecordListMeta {
fields: ColumnDefinition[];
bulkActions: BulkActionsMap;
lineActions: LineAction[];
lineActions: Action[];
filters: Filter[];
}

View file

@ -26,7 +26,7 @@
import {ColumnDefinition} from './list.metadata.model';
import {WidgetOptionMap} from './widget.metadata';
import {LineAction} from "../actions/line-action.model";
import {Action} from '../actions/action.model';
export interface SubPanelTopButton {
key: string;
@ -69,7 +69,7 @@ export interface SubPanelDefinition {
collection_list: SubPanelCollectionList;
columns: ColumnDefinition[];
icon?: string;
lineActions?: LineAction[];
lineActions?: Action[];
get_subpanel_data?: string;
}

View file

@ -55,8 +55,7 @@ import {AppStateStore} from '../../store/app-state/app-state.store';
import {recordActionsMock} from '../../views/record/adapters/actions.adapter.spec.mock';
import {RecordActionsAdapter} from '../../views/record/adapters/actions.adapter';
import {ImageModule} from '../image/image.module';
import {ActionDataSource} from 'common';
import {Action} from '../../../../../common/src/lib/actions/action.model';
import {ActionDataSource, Action} from 'common';
@Component({
selector: 'action-group-menu-test-host-component',

View file

@ -27,7 +27,7 @@
import {Component, Input, OnInit} from '@angular/core';
import {BehaviorSubject, combineLatest, Observable, Subscription} from 'rxjs';
import {map} from 'rxjs/operators';
import {Action, ActionDataSource, Button, ButtonGroupInterface, ButtonInterface} from 'common';
import {Action, ActionContext, ActionDataSource, Button, ButtonGroupInterface, ButtonInterface} from 'common';
import {SystemConfigStore} from '../../store/system-config/system-config.store';
import {
ScreenSize,
@ -49,6 +49,7 @@ export class ActionGroupMenuComponent implements OnInit {
@Input() klass = '';
@Input() buttonClass = 'btn btn-sm';
@Input() actionContext: ActionContext;
@Input() config: ActionDataSource;
configState = new BehaviorSubject<ButtonGroupInterface>({buttons: []});
config$ = this.configState.asObservable();
@ -146,7 +147,7 @@ export class ActionGroupMenuComponent implements OnInit {
label: action.label || '',
klass: this.buttonClass,
onClick: (): void => {
this.config.runAction(action);
this.config.runAction(action, this.actionContext);
}
} as ButtonInterface;

View file

@ -25,25 +25,11 @@
* the words "Supercharged by SuiteCRM".
*/
-->
<div id="line-action-div" class="line-action float-right"
>
<div
*ngFor="let item of items"
class="line-action-item line-action float-right"
placement="left" ngbTooltip="{{ item.label }}"
>
<a *ngIf="item.routing === false"
[ngClass]="item.key"
(click)="runAction(item.action)">
<scrm-image image="{{ item.icon }}"></scrm-image>
</a>
<a *ngIf="item.routing !== false"
[ngClass]="item.key"
[routerLink]="item.link.route"
[queryParams]="item.link.params">
<scrm-image image="{{ item.icon }}"></scrm-image>
</a>
<ng-container *ngIf="(vm$ | async) as vm">
<div id="line-action-div" class="line-action float-right">
<div class="{{klass}}">
<scrm-button-group *ngIf="config$" [config$]="config$"></scrm-button-group>
</div>
</div>
</div>
</ng-container>

View file

@ -25,82 +25,144 @@
*/
import {Component, Input, OnInit} from '@angular/core';
import {LineAction, Record} from 'common';
import {LanguageStore} from '../../store/language/language.store';
import {Action, ActionContext, ActionDataSource, Button, ButtonGroupInterface, ButtonInterface, Record} from 'common';
import {LanguageStore, LanguageStrings} from '../../store/language/language.store';
import {SubpanelActionManager} from "../../containers/subpanel/components/subpanel/action-manager.service";
import {SubpanelActionData} from "../../containers/subpanel/actions/subpanel.action";
import {SubpanelStore} from "../../containers/subpanel/store/subpanel/subpanel.store";
import {BehaviorSubject, combineLatest, Observable, Subscription} from 'rxjs';
import {
ScreenSize,
ScreenSizeObserverService
} from '../../services/ui/screen-size-observer/screen-size-observer.service';
import {SystemConfigStore} from '../../store/system-config/system-config.store';
import {map} from 'rxjs/operators';
export interface LineActionMenuViewModel {
actions: Action[];
screenSize: ScreenSize;
languages: LanguageStrings;
}
@Component({
selector: 'scrm-line-action-menu',
templateUrl: 'line-action-menu.component.html'
})
export class LineActionMenuComponent implements OnInit {
@Input() lineActions: LineAction[];
@Input() klass = '';
@Input() record: Record;
@Input() store: SubpanelStore;
@Input() config: ActionDataSource;
@Input() limitConfigKey = 'listview_line_actions_limits';
configState = new BehaviorSubject<ButtonGroupInterface>({buttons: []});
config$ = this.configState.asObservable();
items: LineAction[];
vm$: Observable<LineActionMenuViewModel>;
constructor(protected languageStore: LanguageStore,
protected actionManager: SubpanelActionManager
protected buttonClass = 'line-action-item line-action';
protected buttonGroupClass = 'float-right';
protected subs: Subscription[];
protected screen: ScreenSize = ScreenSize.Medium;
protected defaultBreakpoint = 3;
protected breakpoint: number;
constructor(
protected languageStore: LanguageStore,
protected actionManager: SubpanelActionManager,
protected languages: LanguageStore,
protected screenSize: ScreenSizeObserverService,
protected systemConfigStore: SystemConfigStore,
) {
}
ngOnInit(): void {
this.setLineActions();
this.vm$ = combineLatest([
this.config.getActions(this.record),
this.screenSize.screenSize$,
this.languages.vm$
]).pipe(
map(([actions, screenSize, languages]) => {
if (screenSize) {
this.screen = screenSize;
}
this.configState.next(this.getButtonGroupConfig(actions));
return {actions, screenSize, languages};
})
);
}
setLineActions(): void {
const actions = [];
getButtonGroupConfig(actions: Action[]): ButtonGroupInterface {
this.lineActions.forEach(action => {
const recordAction = {...action};
const expanded = [];
const collapsed = [];
const routing = action.routing ?? '';
actions.forEach((action: Action) => {
const button = this.buildButton(action);
recordAction.label = this.languageStore.getAppString(recordAction.labelKey);
if (routing !== false) {
const params: { [key: string]: any } = {};
/* eslint-disable camelcase,@typescript-eslint/camelcase*/
params.return_module = action.legacyModuleName;
params.return_action = action.returnAction;
params.return_id = this.record.id;
/* eslint-enable camelcase,@typescript-eslint/camelcase */
params[action.mapping.moduleName] = action.legacyModuleName;
params[action.mapping.name] = this.record.attributes.name;
params[action.mapping.id] = this.record.id;
recordAction.link = {
label: recordAction.label,
url: null,
route: '/' + action.module + '/' + action.action,
params
};
if (action.params && action.params.expanded) {
expanded.push(button);
return;
}
actions.push(recordAction);
collapsed.push(button);
});
this.items = actions.reverse();
let breakpoint = this.getBreakpoint();
if (expanded.length < breakpoint) {
breakpoint = expanded.length;
}
const buttons = expanded.concat(collapsed);
return {
buttonKlass: [this.buttonClass],
dropdownLabel: this.languages.getAppString('LBL_ACTIONS') || '',
breakpoint,
dropdownOptions: {
placement: ['bottom-right'],
wrapperKlass: [(this.buttonGroupClass)]
},
buttons
} as ButtonGroupInterface;
}
runAction(actionKey: string) {
getBreakpoint(): number {
const breakpointMap = this.systemConfigStore.getConfigValue(this.limitConfigKey);
const subpanelActionData = {
subpanelMeta: this.store.metadata,
module: this.record.module || this.store.metadata.module,
id: this.record.id,
parentModule: this.store.parentModule,
parentId: this.store.parentId,
store: this.store
} as SubpanelActionData;
if (this.screen && breakpointMap && breakpointMap[this.screen]) {
this.breakpoint = breakpointMap[this.screen];
return this.breakpoint;
}
this.actionManager.run(actionKey, subpanelActionData);
if (this.breakpoint) {
return this.breakpoint;
}
return this.defaultBreakpoint;
}
protected buildButton(action: Action): ButtonInterface {
const button = {
titleKey: action.labelKey || '',
klass: this.buttonClass,
icon: action.icon || '',
onClick: (): void => {
this.config.runAction(action, {
module: (this.record && this.record.module) || '',
record: this.record
} as ActionContext);
}
} as ButtonInterface;
if (action.icon) {
button.icon = action.icon;
}
if (action.status) {
Button.appendClasses(button, [action.status]);
}
return button;
}
}

View file

@ -31,6 +31,7 @@ import {LineActionMenuComponent} from './line-action-menu.component';
import {NgbModule} from '@ng-bootstrap/ng-bootstrap';
import {RouterModule} from '@angular/router';
import {ImageModule} from '../image/image.module';
import {ButtonGroupModule} from '../button-group/button-group.module';
@NgModule({
declarations: [LineActionMenuComponent],
@ -40,6 +41,7 @@ import {ImageModule} from '../image/image.module';
NgbModule,
ImageModule,
RouterModule,
ButtonGroupModule,
]
})

View file

@ -0,0 +1,55 @@
/**
* 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".
*/
import {Injectable} from '@angular/core';
import {Action, ActionContext} from 'common';
import {AsyncActionService} from '../../../services/process/processes/async-action/async-action';
import {MessageService} from '../../../services/message/message.service';
import {LineActionActionManager} from '../line-actions/line-action-manager.service';
import {LineActionData} from '../line-actions/line.action';
import {ConfirmationModalService} from '../../../services/modals/confirmation-modal.service';
import {BaseRecordActionsAdapter} from '../../../services/actions/base-record-action.adapter';
import {LanguageStore} from '../../../store/language/language.store';
@Injectable()
export abstract class BaseLineActionsAdapter extends BaseRecordActionsAdapter<LineActionData> {
protected constructor(
protected actionManager: LineActionActionManager,
protected asyncActionService: AsyncActionService,
protected message: MessageService,
protected confirmation: ConfirmationModalService,
protected language: LanguageStore
) {
super(actionManager, asyncActionService, message, confirmation, language)
}
protected buildActionData(action: Action, context?: ActionContext): LineActionData {
return {
record: (context && context.record) || null
} as LineActionData;
}
}

View file

@ -0,0 +1,69 @@
/**
* 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".
*/
import {Injectable} from '@angular/core';
import {Router} from '@angular/router';
import {Action, ActionHandler, ViewMode} from 'common';
import {ModuleNameMapper} from '../../../../services/navigation/module-name-mapper/module-name-mapper.service';
import {LineActionData} from '../line.action';
@Injectable({
providedIn: 'root'
})
export class CreateRelatedLineAction extends ActionHandler<LineActionData> {
key = 'create';
modes = ['list' as ViewMode];
constructor(protected moduleNameMapper: ModuleNameMapper, protected router: Router) {
super();
}
run(data: LineActionData, action: Action = null): void {
const configs = action.params['create'] || {} as any;
const params: { [key: string]: any } = {};
/* eslint-disable camelcase,@typescript-eslint/camelcase*/
params.return_module = configs.legacyModuleName;
params.return_action = configs.returnAction;
params.return_id = data.record.id;
/* eslint-enable camelcase,@typescript-eslint/camelcase */
params[configs.mapping.moduleName] = configs.legacyModuleName;
params[configs.mapping.name] = data.record.attributes.name;
params[configs.mapping.id] = data.record.id;
const route = '/' + configs.module + '/' + configs.action;
this.router.navigate([route], {
queryParams: params
}).then();
}
shouldDisplay(data: LineActionData): boolean {
return true;
}
}

View file

@ -0,0 +1,43 @@
/**
* 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".
*/
import {Injectable} from '@angular/core';
import {LineActionData} from './line.action';
import {CreateRelatedLineAction} from './create-related/create-related.action';
import {BaseActionManager} from '../../../services/actions/base-action-manager.service';
@Injectable({
providedIn: 'root',
})
export class LineActionActionManager extends BaseActionManager<LineActionData> {
constructor(
protected createRelated: CreateRelatedLineAction,
) {
super();
createRelated.modes.forEach(mode => this.actions[mode][createRelated.key] = createRelated);
}
}

View file

@ -24,20 +24,9 @@
* the words "Supercharged by SuiteCRM".
*/
import {MenuItemLink} from '../menu/menu.model';
import {ActionData, Record} from 'common';
export interface LineAction {
key: string;
labelKey: string;
label: string;
module: string;
legacyModuleName: string;
icon: string;
action: string;
returnAction: string;
params: { [key: string]: any };
mapping: { [key: string]: any };
link: MenuItemLink;
acl: string[];
routing?: boolean;
export interface LineActionData extends ActionData {
record: Record;
}

View file

@ -77,7 +77,10 @@
<th cdk-header-cell scope="col" *cdkHeaderCellDef class="primary-table-header"></th>
<td cdk-cell *cdkCellDef="let record">
<scrm-line-action-menu [lineActions]="vm.lineActions" [record]="record" [store]="store"></scrm-line-action-menu>
<scrm-line-action-menu *ngIf="record && config.lineActions && config.lineActions.getActions()"
[config]="config.lineActions"
[record]="record">
</scrm-line-action-menu>
</td>
</ng-container>

View file

@ -28,9 +28,9 @@ import {Component, Input, OnInit} from '@angular/core';
import {combineLatest, Observable, of} from 'rxjs';
import {map, shareReplay} from 'rxjs/operators';
import {
Action,
ColumnDefinition,
Field,
LineAction,
Record,
RecordSelection,
SelectionStatus,
@ -40,11 +40,10 @@ import {
import {FieldManager} from '../../../services/record/field/field.manager';
import {TableConfig} from '../table.model';
import {SortDirectionDataSource} from '../../sort-button/sort-button.model';
import {SubpanelStore} from "../../../containers/subpanel/store/subpanel/subpanel.store";
interface TableViewModel {
columns: ColumnDefinition[];
lineActions: LineAction[];
lineActions: Action[];
selection: RecordSelection;
selected: { [key: string]: string };
selectionStatus: SelectionStatus;
@ -59,7 +58,6 @@ interface TableViewModel {
})
export class TableBodyComponent implements OnInit {
@Input() config: TableConfig;
@Input() store: SubpanelStore;
maxColumns = 4;
vm$: Observable<TableViewModel>;
@ -69,13 +67,13 @@ export class TableBodyComponent implements OnInit {
}
ngOnInit(): void {
const lineAction$ = this.config.lineActions$ || of([]).pipe(shareReplay(1));
const selection$ = this.config.selection$ || of(null).pipe(shareReplay(1));
const loading$ = this.config.loading$ || of(false).pipe(shareReplay(1));
const lineActions$ = (this.config.lineActions && this.config.lineActions.getActions()) || of([]).pipe(shareReplay(1));
this.vm$ = combineLatest([
this.config.columns,
lineAction$,
lineActions$,
selection$,
this.config.maxColumns$,
this.config.dataSource.connect(null),
@ -136,15 +134,15 @@ export class TableBodyComponent implements OnInit {
let hasLinkField = false;
const returnArray = [];
const fields = metaFields.filter(function(field){
const fields = metaFields.filter(function (field) {
return !field.hasOwnProperty('default')
|| (field.hasOwnProperty('default') && field.default === true);
});
while (i < this.maxColumns && i < fields.length) {
returnArray.push(fields[i].name);
hasLinkField = hasLinkField || fields[i].link;
i++;
returnArray.push(fields[i].name);
hasLinkField = hasLinkField || fields[i].link;
i++;
}
if (!hasLinkField && (this.maxColumns < fields.length)) {
for (i = this.maxColumns; i < fields.length; i++) {

View file

@ -32,7 +32,7 @@
[pagination]="config.pagination">
</scrm-table-header>
<scrm-table-body [config]="config" [store]="store"></scrm-table-body>
<scrm-table-body [config]="config"></scrm-table-body>
<scrm-table-footer *ngIf="showFooter()"
[selection]="config.selection"

View file

@ -26,7 +26,6 @@
import {Component, Input} from '@angular/core';
import {TableConfig} from './table.model';
import {SubpanelStore} from "../../containers/subpanel/store/subpanel/subpanel.store";
@Component({
selector: 'scrm-table',
@ -35,7 +34,6 @@ import {SubpanelStore} from "../../containers/subpanel/store/subpanel/subpanel.s
})
export class TableComponent {
@Input() config: TableConfig;
@Input() store: SubpanelStore;
showHeader(): boolean {
return this.config.showHeader;

View file

@ -27,8 +27,8 @@
import {Observable} from 'rxjs';
import {DataSource} from '@angular/cdk/collections';
import {
ActionDataSource,
ColumnDefinition,
LineAction,
PaginationDataSource,
Record,
RecordSelection,
@ -46,7 +46,7 @@ export interface TableConfig {
columns: Observable<ColumnDefinition[]>;
maxColumns$: Observable<number>;
lineActions$?: Observable<LineAction[]>;
lineActions?: ActionDataSource;
selection$?: Observable<RecordSelection>;
sort$?: Observable<SortingSelection>;
loading$?: Observable<boolean>;

View file

@ -30,7 +30,6 @@ import {take} from 'rxjs/operators';
import {MessageService} from '../../../../services/message/message.service';
import {SavedFilterActionData, SavedFilterActionHandler} from '../saved-filter.action';
import {AsyncActionInput, AsyncActionService} from '../../../../services/process/processes/async-action/async-action';
import {SavedFilterStore} from '../../store/saved-filter/saved-filter.store';
import {SavedFilter} from '../../../../store/saved-filters/saved-filter.model';
@Injectable({
@ -74,8 +73,8 @@ export class SavedFilterDeleteAction extends SavedFilterActionHandler {
});
}
shouldDisplay(store: SavedFilterStore): boolean {
shouldDisplay(data: SavedFilterActionData): boolean {
const store = data && data.store;
const filter = (store && store.recordStore.getBaseRecord()) || {} as SavedFilter;
return !!filter.id;

View file

@ -25,42 +25,22 @@
*/
import {Injectable} from '@angular/core';
import {ViewMode} from 'common';
import {SavedFilterActionData, SavedFilterActionHandler, SavedFilterActionHandlerMap} from './saved-filter.action';
import {SavedFilterActionData} from './saved-filter.action';
import {SavedFilterSaveAction} from './save/saved-filter-save.action';
import {SavedFilterDeleteAction} from './delete/saved-filter-delete.action';
import {BaseActionManager} from '../../../services/actions/base-action-manager.service';
@Injectable({
providedIn: 'root',
})
export class SavedFilterActionManager {
actions: { [key: string]: SavedFilterActionHandlerMap } = {
edit: {} as SavedFilterActionHandlerMap,
detail: {} as SavedFilterActionHandlerMap,
};
export class SavedFilterActionManager extends BaseActionManager<SavedFilterActionData> {
constructor(
save: SavedFilterSaveAction,
deleteAction: SavedFilterDeleteAction
) {
super();
save.modes.forEach(mode => this.actions[mode][save.key] = save);
deleteAction.modes.forEach(mode => this.actions[mode][deleteAction.key] = deleteAction);
}
run(actionKey: string, mode: ViewMode, data: SavedFilterActionData): void {
if (!this.actions || !this.actions[mode] || !this.actions[mode][actionKey]) {
return;
}
this.actions[mode][actionKey].run(data);
}
getHandler(action: string, mode: ViewMode): SavedFilterActionHandler {
if (!this.actions || !this.actions[mode] || !this.actions[mode][action]) {
return null;
}
return this.actions[mode][action];
}
}

View file

@ -24,7 +24,7 @@
* the words "Supercharged by SuiteCRM".
*/
import {ActionData, ActionHandler, ViewMode} from 'common';
import {ActionData, ActionHandler} from 'common';
import {SavedFilterStore} from '../store/saved-filter/saved-filter.store';
import {ListFilterStore} from '../store/list-filter/list-filter.store';
@ -33,19 +33,9 @@ export interface SavedFilterActionData extends ActionData {
listFilterStore: ListFilterStore;
}
export interface SavedFilterActionHandlerMap {
[key: string]: SavedFilterActionHandler;
}
export abstract class SavedFilterActionHandler extends ActionHandler {
abstract modes: ViewMode[];
getStatus(store: SavedFilterStore): string {
return '';
}
export abstract class SavedFilterActionHandler extends ActionHandler<SavedFilterActionData> {
abstract run(data: SavedFilterActionData): void;
abstract shouldDisplay(store: SavedFilterStore): boolean;
abstract shouldDisplay(data: SavedFilterActionData): boolean;
}

View file

@ -25,10 +25,10 @@
*/
import {Injectable} from '@angular/core';
import {Action, ActionDataSource, ModeActions} from 'common';
import {Action, ActionContext, ViewMode} from 'common';
import {combineLatest, Observable} from 'rxjs';
import {map, take} from 'rxjs/operators';
import {AsyncActionInput, AsyncActionService} from '../../../services/process/processes/async-action/async-action';
import {AsyncActionService} from '../../../services/process/processes/async-action/async-action';
import {LanguageStore} from '../../../store/language/language.store';
import {MessageService} from '../../../services/message/message.service';
import {Process} from '../../../services/process/process.service';
@ -37,15 +37,10 @@ import {SavedFilterActionManager} from '../actions/saved-filter-action-manager.s
import {SavedFilterActionData} from '../actions/saved-filter.action';
import {ListFilterStore} from '../store/list-filter/list-filter.store';
import {ConfirmationModalService} from '../../../services/modals/confirmation-modal.service';
import {BaseRecordActionsAdapter} from '../../../services/actions/base-record-action.adapter';
@Injectable()
export class SavedFilterActionsAdapter implements ActionDataSource {
defaultActions: ModeActions = {
detail: [],
edit: [],
create: [],
};
export class SavedFilterActionsAdapter extends BaseRecordActionsAdapter<SavedFilterActionData> {
constructor(
protected store: SavedFilterStore,
@ -54,11 +49,18 @@ export class SavedFilterActionsAdapter implements ActionDataSource {
protected actionManager: SavedFilterActionManager,
protected asyncActionService: AsyncActionService,
protected message: MessageService,
protected confimation: ConfirmationModalService
protected confirmation: ConfirmationModalService,
) {
super(
actionManager,
asyncActionService,
message,
confirmation,
language
)
}
getActions(): Observable<Action[]> {
getActions(context?: ActionContext): Observable<Action[]> {
return combineLatest(
[
this.store.meta$,
@ -70,113 +72,35 @@ export class SavedFilterActionsAdapter implements ActionDataSource {
map((
[
meta,
mode,
record,
languages,
mode
]
) => {
if (!mode || !meta) {
return [];
}
const availableActions = {
detail: [],
edit: [],
} as ModeActions;
if (meta.actions && meta.actions.length) {
meta.actions.forEach(action => {
if (!action.modes || !action.modes.length) {
return;
}
action.modes.forEach(actionMode => {
if (!availableActions[actionMode]) {
return;
}
availableActions[actionMode].push(action);
});
});
}
availableActions.detail = availableActions.detail.concat(this.defaultActions.detail);
availableActions.edit = availableActions.edit.concat(this.defaultActions.edit);
const actions = [];
availableActions[mode].forEach(action => {
if (!action.asyncProcess) {
const actionHandler = this.actionManager.getHandler(action.key, mode);
if (!actionHandler || !actionHandler.shouldDisplay(this.store)) {
return;
}
action.status = actionHandler.getStatus(this.store) || '';
}
const label = this.language.getFieldLabel(action.labelKey, record.module, languages);
actions.push({
...action,
label
});
});
return actions;
return this.parseModeActions(meta.actions, mode);
})
);
}
runAction(action: Action): void {
const params = (action && action.params) || {} as { [key: string]: any };
const displayConfirmation = params.displayConfirmation || false;
const confirmationLabel = params.confirmationLabel || '';
if (displayConfirmation) {
this.confimation.showModal(confirmationLabel, () => {
this.callAction(action);
});
return;
}
this.callAction(action);
}
protected callAction(action: Action) {
if (action.asyncProcess) {
this.runAsyncAction(action);
return;
}
this.runFrontEndAction(action);
}
protected runAsyncAction(action: Action): void {
const actionName = `record-${action.key}`;
const baseRecord = this.store.getBaseRecord();
this.message.removeMessages();
const asyncData = {
action: actionName,
module: baseRecord.module,
id: baseRecord.id,
} as AsyncActionInput;
this.asyncActionService.run(actionName, asyncData).pipe(take(1)).subscribe((process: Process) => {
if (process.data && process.data.reload) {
this.store.load(false).pipe(take(1)).subscribe();
}
});
}
protected runFrontEndAction(action: Action): void {
const data: SavedFilterActionData = {
protected buildActionData(action: Action, context?: ActionContext): SavedFilterActionData {
return {
store: this.store,
listFilterStore: this.listFilterStore,
action
};
} as SavedFilterActionData;
}
this.actionManager.run(action.key, this.store.getMode(), data);
protected getMode(): ViewMode {
return this.store.getMode();
}
protected getModuleName(context?: ActionContext): string {
return this.store.getModuleName();
}
protected reload(action: Action, process: Process, context?: ActionContext): void {
this.store.load(false).pipe(take(1)).subscribe();
}
}

View file

@ -35,25 +35,31 @@
<div modal-body>
<scrm-label *ngIf="!tableConfig" labelKey="LBL_CONFIG_NO_CONFIG"></scrm-label>
<ng-container *ngIf="!tableConfig">
<scrm-label labelKey="LBL_CONFIG_NO_CONFIG"></scrm-label>
</ng-container>
<div *ngIf="tableConfig">
<div class="container-fluid">
<div class="row pb-3">
<div class="col">
<scrm-list-filter *ngIf="filterConfig" [config]="filterConfig"></scrm-list-filter>
</div>
</div>
<div class="row">
<div class="col">
<scrm-table [config]="tableConfig"></scrm-table>
<ng-container *ngIf="tableConfig">
<div>
<div class="container-fluid">
<div class="row pb-3">
<div class="col">
<scrm-list-filter *ngIf="filterConfig" [config]="filterConfig"></scrm-list-filter>
</div>
</div>
<div class="row">
<div class="col">
<scrm-table [config]="tableConfig"></scrm-table>
</div>
</div>
</div>
<ng-container *ngIf="(loading$ | async) as loading">
<scrm-loading-spinner *ngIf="loading" [overlay]="true"></scrm-loading-spinner>
</ng-container>
</div>
<ng-container *ngIf="(loading$ | async) as loading">
<scrm-loading-spinner *ngIf="loading" [overlay]="true"></scrm-loading-spinner>
</ng-container>
</div>
</ng-container>
</div>
</scrm-modal>

View file

@ -27,7 +27,7 @@
import {Injectable} from '@angular/core';
import {Params, Router} from '@angular/router';
import {ModuleNameMapper,} from '../../../../services/navigation/module-name-mapper/module-name-mapper.service';
import {AttributeMap, isVoid} from 'common';
import {AttributeMap, isVoid, ViewMode} from 'common';
import get from 'lodash-es/get';
import {SubpanelActionData, SubpanelActionHandler} from '../subpanel.action';
@ -37,6 +37,7 @@ import {SubpanelActionData, SubpanelActionHandler} from '../subpanel.action';
})
export class SubpanelCreateAction extends SubpanelActionHandler {
key = 'create';
modes = ['list' as ViewMode];
constructor(
protected moduleNameMapper: ModuleNameMapper,
@ -67,6 +68,10 @@ export class SubpanelCreateAction extends SubpanelActionHandler {
}).then();
}
shouldDisplay(): boolean {
return true;
}
/**
* Add additional record fields
*

View file

@ -39,7 +39,7 @@ export interface SubpanelActionHandlerMap {
[key: string]: SubpanelActionHandler;
}
export abstract class SubpanelActionHandler extends ActionHandler {
export abstract class SubpanelActionHandler extends ActionHandler<SubpanelActionData> {
abstract run(data: SubpanelActionData): void;
}

View file

@ -1,89 +0,0 @@
/**
* 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".
*/
import {Injectable} from '@angular/core';
import {SubpanelActionData, SubpanelActionHandler} from '../subpanel.action';
import {MessageModalComponent} from "../../../../components/modal/components/message-modal/message-modal.component";
import {Action, ModalButtonInterface} from "common";
import {SubpanelActionsAdapter} from "../../adapters/subpanel-actions.adapter";
import {NgbModal} from "@ng-bootstrap/ng-bootstrap";
@Injectable({
providedIn: 'root'
})
export class SubpanelUnlinkAction extends SubpanelActionHandler {
key = 'unlink';
constructor(
protected subpanelAdaptor: SubpanelActionsAdapter,
private modalService: NgbModal
) {
super();
}
run(data: SubpanelActionData): void {
this.showConfirmationModal(data);
}
protected showConfirmationModal(data: SubpanelActionData): void {
const modal = this.modalService.open(MessageModalComponent);
modal.componentInstance.textKey = 'LBL_UNLINK_RELATIONSHIP_CONFIRM';
modal.componentInstance.buttons = [
{
labelKey: 'LBL_CANCEL',
klass: ['btn-secondary'],
onClick: activeModal => activeModal.dismiss()
} as ModalButtonInterface,
{
labelKey: 'LBL_PROCEED',
klass: ['btn-main'],
onClick: activeModal => {
const action: Action =
{
key: this.key,
asyncProcess: true,
params: {
store: data.store,
payload: {
baseModule: data.parentModule,
baseRecordId: data.parentId,
relateModule: data.store.metadata.get_subpanel_data?? data.module,
relateRecordId: data.id
}
}
}
this.subpanelAdaptor.runAction(action);
activeModal.close();
}
} as ModalButtonInterface,
];
}
}

View file

@ -25,52 +25,37 @@
*/
import {Injectable} from '@angular/core';
import {Action, ActionDataSource} from 'common';
import {Observable, of} from 'rxjs';
import {take} from 'rxjs/operators';
import {AsyncActionInput, AsyncActionService} from '../../../services/process/processes/async-action/async-action';
import {AsyncActionService} from '../../../services/process/processes/async-action/async-action';
import {MessageService} from '../../../services/message/message.service';
import {Process} from '../../../services/process/process.service';
import {ConfirmationModalService} from '../../../services/modals/confirmation-modal.service';
import {LanguageStore} from '../../../store/language/language.store';
import {SubpanelStore} from '../store/subpanel/subpanel.store';
import {SubpanelLineActionsAdapter} from './line-actions.adapter';
import {SubpanelLineActionManager} from '../line-actions/line-action-manager.service';
@Injectable({
providedIn: 'root',
})
export class SubpanelActionsAdapter implements ActionDataSource {
export class SubpanelLineActionsAdapterFactory {
constructor(
protected actionManager: SubpanelLineActionManager,
protected asyncActionService: AsyncActionService,
protected message: MessageService
protected message: MessageService,
protected confirmation: ConfirmationModalService,
protected language: LanguageStore
) {
}
getActions(): Observable<Action[]> {
// not yet implemented
return of([]);
create(store: SubpanelStore): SubpanelLineActionsAdapter {
return new SubpanelLineActionsAdapter(
store,
this.actionManager,
this.asyncActionService,
this.message,
this.confirmation,
this.language
);
}
runAction(action: Action): void {
if (action.asyncProcess) {
this.runAsyncAction(action);
return;
}
}
protected runAsyncAction(action: Action): void {
const actionName = `record-${action.key}`;
this.message.removeMessages();
const asyncData = {
action: actionName,
module: action.params.payload.relateModule,
payload: {...action.params.payload}
} as AsyncActionInput;
this.asyncActionService.run(actionName, asyncData).pipe(take(1)).subscribe((process: Process) => {
if (process.data.status === 'success' && process.data && process.data.reload) {
action.params.store.load(false).pipe(take(1)).subscribe();
action.params.store.loadAllStatistics(false).pipe(take(1)).subscribe();
}
});
}
}

View file

@ -0,0 +1,136 @@
/**
* 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".
*/
import {Injectable} from '@angular/core';
import {Action, ActionContext, ViewMode} from 'common';
import {combineLatest, Observable, of} from 'rxjs';
import {map, shareReplay, take} from 'rxjs/operators';
import {AsyncActionInput, AsyncActionService} from '../../../services/process/processes/async-action/async-action';
import {MessageService} from '../../../services/message/message.service';
import {Process} from '../../../services/process/process.service';
import {ConfirmationModalService} from '../../../services/modals/confirmation-modal.service';
import {LanguageStore} from '../../../store/language/language.store';
import {BaseRecordActionsAdapter} from '../../../services/actions/base-record-action.adapter';
import {SubpanelLineActionData} from '../line-actions/line.action';
import {SubpanelStore} from '../store/subpanel/subpanel.store';
import {SubpanelLineActionManager} from '../line-actions/line-action-manager.service';
@Injectable({
providedIn: 'root',
})
export class SubpanelLineActionsAdapter extends BaseRecordActionsAdapter<SubpanelLineActionData> {
constructor(
protected store: SubpanelStore,
protected actionManager: SubpanelLineActionManager,
protected asyncActionService: AsyncActionService,
protected message: MessageService,
protected confirmation: ConfirmationModalService,
protected language: LanguageStore
) {
super(actionManager, asyncActionService, message, confirmation, language)
}
getActions(context: ActionContext = null): Observable<Action[]> {
return combineLatest(
[
this.store.metadata$.pipe(map(metadata => metadata.lineActions)),
of('list' as ViewMode).pipe(shareReplay()),
]
).pipe(
map(([actions, mode]) => {
return this.parseModeActions(actions, mode, context);
})
);
}
protected buildActionData(action: Action, context?: ActionContext): SubpanelLineActionData {
return {
record: (context && context.record) || null,
store: this.store
} as SubpanelLineActionData;
}
protected getMode(): ViewMode {
return 'list' as ViewMode;
}
protected getModuleName(context?: ActionContext): string {
return this.store.metadata.module;
}
protected reload(action: Action, process: Process, context?: ActionContext): void {
this.store.load(false).pipe(take(1)).subscribe();
this.store.loadAllStatistics(false).pipe(take(1)).subscribe();
}
/**
* Build backend process input
*
* @param action
* @param actionName
* @param moduleName
* @param context
*/
protected buildActionInput(action: Action, actionName: string, moduleName: string, context: ActionContext = null): AsyncActionInput {
const metadata = this.store.metadata;
const collectionList = metadata.collection_list || null;
const module = (context && context.module) || moduleName;
let linkField: string = metadata.get_subpanel_data;
if(collectionList && collectionList[module] && collectionList[module].get_subpanel_data){
linkField = collectionList[module].get_subpanel_data;
}
if(linkField && action && action.params && action.params.linkFieldMapping){
Object.keys(action.params.linkFieldMapping).some(key => {
if (linkField.includes(key)){
linkField = action.params.linkFieldMapping[key];
return true;
}
})
}
return {
action: actionName,
module: moduleName,
id: (context && context.record && context.record.id) || '',
payload: {
baseModule: this.store.parentModule,
baseRecordId: this.store.parentId,
linkField,
recordModule: module,
relateModule: this.store.metadata.module,
relateRecordId: (context && context.record && context.record.id) || '',
}
} as AsyncActionInput;
}
}

View file

@ -0,0 +1,47 @@
/**
* 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".
*/
import {Injectable} from '@angular/core';
import {SubpanelStore} from '../store/subpanel/subpanel.store';
import {SubpanelTableAdapter} from './table.adapter';
import {SubpanelLineActionsAdapterFactory} from './line-actions.adapter.factory';
@Injectable({
providedIn: 'root',
})
export class SubpanelTableAdapterFactory {
constructor(protected lineActionsAdapterFactory: SubpanelLineActionsAdapterFactory) {
}
create(store: SubpanelStore): SubpanelTableAdapter {
return new SubpanelTableAdapter(
store,
this.lineActionsAdapterFactory
);
}
}

View file

@ -26,15 +26,19 @@
import {Observable, of} from 'rxjs';
import {Injectable} from '@angular/core';
import {ColumnDefinition, LineAction, SortDirection} from 'common';
import {ActionDataSource, ColumnDefinition, SortDirection} from 'common';
import {map} from 'rxjs/operators';
import {TableConfig} from '../../../components/table/table.model';
import {SubpanelStore} from '../store/subpanel/subpanel.store';
import {SubpanelLineActionsAdapterFactory} from './line-actions.adapter.factory';
@Injectable()
export class SubpanelTableAdapter {
constructor(protected store: SubpanelStore) {
constructor(
protected store: SubpanelStore,
protected lineActionsAdapterFactory: SubpanelLineActionsAdapterFactory
) {
}
getTable(): TableConfig {
@ -45,7 +49,7 @@ export class SubpanelTableAdapter {
module: this.store.metadata.headerModule,
columns: this.getColumns(),
lineActions$: this.getLineActions(),
lineActions: this.getLineActions(),
sort$: this.store.recordList.sort$,
maxColumns$: of(5),
loading$: this.store.recordList.loading$,
@ -67,7 +71,7 @@ export class SubpanelTableAdapter {
return this.store.metadata$.pipe(map(metadata => metadata.columns));
}
protected getLineActions(): Observable<LineAction[]> {
return this.store.metadata$.pipe(map(metadata => metadata.lineActions));
protected getLineActions(): ActionDataSource {
return this.lineActionsAdapterFactory.create(this.store);
}
}

View file

@ -27,7 +27,6 @@
import {Injectable} from '@angular/core';
import {SubpanelCreateAction} from '../../actions/create/create.action';
import {SubpanelActionData, SubpanelActionHandlerMap} from '../../actions/subpanel.action';
import {SubpanelUnlinkAction} from "../../actions/unlink/unlink.action";
@Injectable({
providedIn: 'root',
@ -38,10 +37,8 @@ export class SubpanelActionManager {
constructor(
protected create: SubpanelCreateAction,
protected unlink: SubpanelUnlinkAction,
) {
this.actions[create.key] = create;
this.actions[unlink.key] = unlink;
}
run(action: string, data: SubpanelActionData): void {

View file

@ -34,7 +34,7 @@
<scrm-button-group *ngIf="config$" [config$]="config$"></scrm-button-group>
</span>
<div panel-body>
<scrm-table [config]="tableConfig" [store]="store"></scrm-table>
<scrm-table [config]="tableConfig"></scrm-table>
</div>
</scrm-panel>
</ng-container>

View file

@ -33,6 +33,7 @@ import {SubpanelTableAdapter} from '../../adapters/table.adapter';
import {LanguageStore} from '../../../../store/language/language.store';
import {SubpanelStore} from '../../store/subpanel/subpanel.store';
import {SubpanelActionManager} from './action-manager.service';
import {SubpanelTableAdapterFactory} from '../../adapters/table.adapter.factory';
@Component({
selector: 'scrm-subpanel',
@ -53,11 +54,12 @@ export class SubpanelComponent implements OnInit {
constructor(
protected actionManager: SubpanelActionManager,
protected languages: LanguageStore,
protected tableAdapterFactory: SubpanelTableAdapterFactory
) {
}
ngOnInit(): void {
this.adapter = new SubpanelTableAdapter(this.store);
this.adapter = this.tableAdapterFactory.create(this.store);
this.tableConfig = this.adapter.getTable();
if (this.maxColumns$) {
this.tableConfig.maxColumns$ = this.maxColumns$;

View file

@ -0,0 +1,39 @@
/**
* 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".
*/
import {Injectable} from '@angular/core';
import {BaseActionManager} from '../../../services/actions/base-action-manager.service';
import {SubpanelLineActionData} from './line.action';
@Injectable({
providedIn: 'root',
})
export class SubpanelLineActionManager extends BaseActionManager<SubpanelLineActionData> {
constructor() {
super();
}
}

View file

@ -0,0 +1,41 @@
/**
* 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".
*/
import {Action, ActionHandler, Record} from 'common';
import {LineActionData} from '../../../components/table/line-actions/line.action';
import {SubpanelStore} from '../store/subpanel/subpanel.store';
export interface SubpanelLineActionData extends LineActionData {
record: Record;
store: SubpanelStore;
}
export abstract class SubpanelLineActionHandler extends ActionHandler<SubpanelLineActionData> {
abstract run(data: SubpanelLineActionData, action?: Action): void;
abstract shouldDisplay(data: SubpanelLineActionData): boolean;
}

View file

@ -115,6 +115,10 @@ export * from './components/status-bar/status-bar.module';
export * from './components/table/table.component';
export * from './components/table/table.model';
export * from './components/table/table.module';
export * from './components/table/adapters/base-line-actions.adapter';
export * from './components/table/line-actions/line-action-manager.service';
export * from './components/table/line-actions/line.action';
export * from './components/table/line-actions/create-related/create-related.action';
export * from './components/table/table-body/table-body.component';
export * from './components/table/table-body/table-body.module';
export * from './components/table/table-footer/table-footer.component';
@ -164,8 +168,9 @@ export * from './containers/sidebar-widget/components/statistics-sidebar-widget/
export * from './containers/sidebar-widget/components/statistics-sidebar-widget/statistics-sidebar-widget.module';
export * from './containers/subpanel/actions/subpanel.action';
export * from './containers/subpanel/actions/create/create.action';
export * from './containers/subpanel/actions/unlink/unlink.action';
export * from './containers/subpanel/adapters/subpanel-actions.adapter';
export * from './containers/subpanel/adapters/line-actions.adapter.factory';
export * from './containers/subpanel/adapters/line-actions.adapter';
export * from './containers/subpanel/adapters/table.adapter.factory';
export * from './containers/subpanel/adapters/table.adapter';
export * from './containers/subpanel/components/subpanel/action-manager.service';
export * from './containers/subpanel/components/subpanel/subpanel.component';
@ -173,6 +178,8 @@ export * from './containers/subpanel/components/subpanel/subpanel.module';
export * from './containers/subpanel/components/subpanel-container/subpanel-container.component';
export * from './containers/subpanel/components/subpanel-container/subpanel-container.model';
export * from './containers/subpanel/components/subpanel-container/subpanel-container.module';
export * from './containers/subpanel/line-actions/line-action-manager.service';
export * from './containers/subpanel/line-actions/line.action';
export * from './containers/subpanel/store/subpanel/subpanel.store.factory';
export * from './containers/subpanel/store/subpanel/subpanel.store';
export * from './containers/top-widget/components/statistics-top-widget/statistics-top-widget.component';
@ -286,6 +293,9 @@ export * from './pipes/format-number/format-number.module';
export * from './pipes/format-number/format-number.pipe';
export * from './pipes/html-sanitize/html-sanitize.module';
export * from './pipes/html-sanitize/html-sanitize.pipe';
export * from './services/actions/base-action-manager.service';
export * from './services/actions/base-action.adapter';
export * from './services/actions/base-record-action.adapter';
export * from './services/api/graphql-api/api.collection.get';
export * from './services/api/graphql-api/api.entity.get';
export * from './services/api/graphql-api/api.record.create';
@ -381,6 +391,7 @@ export * from './views/create/components/create-view/create-record.component';
export * from './views/create/components/create-view/create-record.module';
export * from './views/create/store/create-view/create-view.store';
export * from './views/list/adapters/filter.adapter';
export * from './views/list/adapters/line-actions.adapter';
export * from './views/list/adapters/sidebar-widget.adapter';
export * from './views/list/adapters/table.adapter';
export * from './views/list/components/action-menu/action-menu.component';

View file

@ -0,0 +1,57 @@
/**
* 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".
*/
import {Injectable} from '@angular/core';
import {Action, ActionData, ActionHandler, ActionHandlerMap, ActionManager, ViewMode} from 'common';
@Injectable({
providedIn: 'root',
})
export class BaseActionManager<D extends ActionData> implements ActionManager<D> {
actions: { [key: string]: ActionHandlerMap<D> } = {
edit: {} as ActionHandlerMap<D>,
create: {} as ActionHandlerMap<D>,
list: {} as ActionHandlerMap<D>,
detail: {} as ActionHandlerMap<D>
};
run(action: Action, mode: ViewMode, data: D): void {
if (!this.actions || !this.actions[mode] || !this.actions[mode][action.key]) {
return;
}
this.actions[mode][action.key].run(data, action);
}
getHandler(action: Action, mode: ViewMode): ActionHandler<D> {
if (!this.actions || !this.actions[mode] || !this.actions[mode][action.key]) {
return null;
}
return this.actions[mode][action.key];
}
}

View file

@ -0,0 +1,230 @@
/**
* 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".
*/
import {
Action,
ActionContext,
ActionData,
ActionDataSource,
ActionHandler,
ActionManager,
ModeActions,
ViewMode
} from 'common';
import {Observable} from 'rxjs';
import {take} from 'rxjs/operators';
import {AsyncActionInput, AsyncActionService} from '../process/processes/async-action/async-action';
import {MessageService} from '../message/message.service';
import {Process} from '../process/process.service';
import {ConfirmationModalService} from '../modals/confirmation-modal.service';
import {LanguageStore} from '../../store/language/language.store';
export abstract class BaseActionsAdapter<D extends ActionData> implements ActionDataSource {
defaultActions: ModeActions = {
detail: [],
list: [],
edit: [],
create: []
};
protected constructor(
protected actionManager: ActionManager<D>,
protected asyncActionService: AsyncActionService,
protected message: MessageService,
protected confirmation: ConfirmationModalService,
protected language: LanguageStore
) {
}
abstract getActions(context?: ActionContext): Observable<Action[]>;
protected abstract getModuleName(context?: ActionContext): string;
protected abstract reload(action: Action, process: Process, context?: ActionContext): void;
protected abstract getMode(): ViewMode;
protected abstract buildActionData(action: Action, context?: ActionContext): D;
/**
* Run the action using given context
* @param action
* @param context
*/
runAction(action: Action, context: ActionContext = null): void {
const params = (action && action.params) || {} as { [key: string]: any };
const displayConfirmation = params.displayConfirmation || false;
const confirmationLabel = params.confirmationLabel || '';
if (displayConfirmation) {
this.confirmation.showModal(confirmationLabel, () => {
this.callAction(action, context);
});
return;
}
this.callAction(action, context);
}
/**
* Build async process input
* @param action
* @param actionName
* @param moduleName
* @param context
*/
protected abstract buildActionInput(action: Action, actionName: string, moduleName: string, context?: ActionContext): AsyncActionInput;
/**
* Get action name
* @param action
*/
protected getActionName(action: Action) {
return `${action.key}`;
}
/**
* Parse mode actions
* @param declaredActions
* @param mode
* @param context
*/
protected parseModeActions(declaredActions: Action[], mode: ViewMode, context: ActionContext = null) {
if (!declaredActions) {
return [];
}
const availableActions = {
list: [],
detail: [],
edit: [],
create: [],
} as ModeActions;
if (declaredActions && declaredActions.length) {
declaredActions.forEach(action => {
if (!action.modes || !action.modes.length) {
return;
}
action.modes.forEach(actionMode => {
if (!availableActions[actionMode] && !action.asyncProcess) {
return;
}
availableActions[actionMode].push(action);
});
});
}
availableActions.detail = availableActions.detail.concat(this.defaultActions.detail);
availableActions.list = availableActions.list.concat(this.defaultActions.list);
availableActions.edit = availableActions.edit.concat(this.defaultActions.edit);
availableActions.create = availableActions.create.concat(this.defaultActions.create);
const actions = [];
availableActions[mode].forEach(action => {
if (!action.asyncProcess) {
const actionHandler = this.actionManager.getHandler(action, mode);
const data: D = this.buildActionData(action, context);
if (!this.shouldDisplay(actionHandler, data)) {
return;
}
action.status = actionHandler.getStatus(data) || '';
}
const module = (context && context.module) || '';
const label = this.language.getFieldLabel(action.labelKey, module);
actions.push({
...action,
label
});
});
return actions;
}
protected shouldDisplay(actionHandler: ActionHandler<D>, data: D): boolean {
return actionHandler && actionHandler.shouldDisplay(data);
}
/**
* Call actions
* @param action
* @param context
*/
protected callAction(action: Action, context: ActionContext = null) {
if (action.asyncProcess) {
this.runAsyncAction(action, context);
return;
}
this.runFrontEndAction(action, context);
}
/**
* Run async actions
* @param action
* @param context
*/
protected runAsyncAction(action: Action, context: ActionContext = null): void {
const actionName = this.getActionName(action);
const moduleName = this.getModuleName(context);
this.message.removeMessages();
const asyncData = this.buildActionInput(action, actionName, moduleName, context);
this.asyncActionService.run(actionName, asyncData).pipe(take(1)).subscribe((process: Process) => {
if (this.shouldReload(process)) {
this.reload(action, process, context);
}
});
}
/**
* Should reload page
* @param process
*/
protected shouldReload(process: Process): boolean {
return !!(process.data && process.data.reload);
}
/**
* Run front end action
* @param action
* @param context
*/
protected runFrontEndAction(action: Action, context: ActionContext = null): void {
const data: D = this.buildActionData(action, context);
this.actionManager.run(action, this.getMode(), data);
}
}

View file

@ -0,0 +1,78 @@
/**
* 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".
*/
import {Injectable} from '@angular/core';
import {Action, ActionContext, ActionManager} from 'common';
import {AsyncActionInput, AsyncActionService} from '../process/processes/async-action/async-action';
import {MessageService} from '../message/message.service';
import {ConfirmationModalService} from '../modals/confirmation-modal.service';
import {BaseActionsAdapter} from './base-action.adapter';
import {LanguageStore} from '../../store/language/language.store';
@Injectable()
export abstract class BaseRecordActionsAdapter<D> extends BaseActionsAdapter<D> {
protected constructor(
protected actionManager: ActionManager<D>,
protected asyncActionService: AsyncActionService,
protected message: MessageService,
protected confirmation: ConfirmationModalService,
protected language: LanguageStore
) {
super(
actionManager,
asyncActionService,
message,
confirmation,
language
)
}
/**
* Get action name
* @param action
*/
protected getActionName(action: Action) {
return `record-${action.key}`;
}
/**
* Build backend process input
*
* @param action
* @param actionName
* @param moduleName
* @param context
*/
protected buildActionInput(action: Action, actionName: string, moduleName: string, context: ActionContext = null): AsyncActionInput {
return {
action: actionName,
module: moduleName,
id: (context && context.record && context.record.id) || '',
} as AsyncActionInput;
}
}

View file

@ -42,6 +42,7 @@ export interface AsyncActionInput {
criteria?: SearchCriteria;
sort?: SortingSelection;
ids?: string[];
id?: string;
payload?: { [key: string]: any };
}

View file

@ -33,7 +33,6 @@ import {
ColumnDefinition,
deepClone,
FieldDefinitionMap,
LineAction,
ListViewMeta,
Panel,
SearchMeta,
@ -112,7 +111,7 @@ export class MetadataStore implements StateStore {
* Public long-lived observable streams
*/
listViewColumns$: Observable<ColumnDefinition[]>;
listViewLineActions$: Observable<LineAction[]>;
listViewLineActions$: Observable<Action[]>;
listMetadata$: Observable<ListViewMeta>;
searchMetadata$: Observable<SearchMeta>;
recordViewMetadata$: Observable<RecordViewMetadata>;

View file

@ -0,0 +1,82 @@
/**
* 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".
*/
import {Injectable} from '@angular/core';
import {Action, ActionContext, Record, ViewMode} from 'common';
import {combineLatest, Observable, of} from 'rxjs';
import {map, shareReplay} from 'rxjs/operators';
import {AsyncActionService} from '../../../services/process/processes/async-action/async-action';
import {MessageService} from '../../../services/message/message.service';
import {Process} from '../../../services/process/process.service';
import {ListViewStore} from '../store/list-view/list-view.store';
import {LineActionActionManager} from '../../../components/table/line-actions/line-action-manager.service';
import {BaseLineActionsAdapter} from '../../../components/table/adapters/base-line-actions.adapter';
import {ConfirmationModalService} from '../../../services/modals/confirmation-modal.service';
import {LanguageStore} from '../../../store/language/language.store';
@Injectable()
export class LineActionsAdapter extends BaseLineActionsAdapter {
constructor(
protected store: ListViewStore,
protected actionManager: LineActionActionManager,
protected asyncActionService: AsyncActionService,
protected message: MessageService,
protected confirmation: ConfirmationModalService,
protected language: LanguageStore
) {
super(
actionManager,
asyncActionService,
message,
confirmation,
language
)
}
getActions(context: ActionContext = null): Observable<Action[]> {
return combineLatest(
[this.store.lineActions$, of('list' as ViewMode).pipe(shareReplay())]
).pipe(
map(([lineActions, mode]) => {
return this.parseModeActions(lineActions, mode, context);
})
);
}
protected getModuleName(record?: Record): string {
return this.store.getModuleName();
}
protected reload(action: Action, process: Process, record?: Record): void {
this.store.recordList.clearSelection();
this.store.recordList.resetPagination();
}
protected getMode(): ViewMode {
return 'list' as ViewMode;
}
}

View file

@ -26,10 +26,16 @@
import {of} from 'rxjs';
import {Injectable} from '@angular/core';
import {SortDirection} from 'common';
import {ActionDataSource, SortDirection} from 'common';
import {ListViewStore} from '../store/list-view/list-view.store';
import {MetadataStore} from '../../../store/metadata/metadata.store.service';
import {TableConfig} from '../../../components/table/table.model';
import {LineActionsAdapter} from './line-actions.adapter';
import {LineActionActionManager} from '../../../components/table/line-actions/line-action-manager.service';
import {AsyncActionService} from '../../../services/process/processes/async-action/async-action';
import {MessageService} from '../../../services/message/message.service';
import {ConfirmationModalService} from '../../../services/modals/confirmation-modal.service';
import {LanguageStore} from '../../../store/language/language.store';
@Injectable()
export class TableAdapter {
@ -37,6 +43,11 @@ export class TableAdapter {
constructor(
protected store: ListViewStore,
protected metadata: MetadataStore,
protected actionManager: LineActionActionManager,
protected asyncActionService: AsyncActionService,
protected message: MessageService,
protected confirmation: ConfirmationModalService,
protected language: LanguageStore
) {
}
@ -48,7 +59,7 @@ export class TableAdapter {
module: this.store.getModuleName(),
columns: this.store.columns$,
lineActions$: this.store.lineActions$,
lineActions: this.getLineActionsDataSource(),
selection$: this.store.selection$,
sort$: this.store.sort$,
maxColumns$: of(4),
@ -68,4 +79,16 @@ export class TableAdapter {
},
} as TableConfig;
}
getLineActionsDataSource(): ActionDataSource {
return new LineActionsAdapter(
this.store,
this.actionManager,
this.asyncActionService,
this.message,
this.confirmation,
this.language
);
}
}

View file

@ -24,13 +24,12 @@
* the words "Supercharged by SuiteCRM".
*/
import {PageSelection, SelectionStatus} from 'common';
import {PageSelection, SelectionStatus, SearchCriteriaFieldFilter} from 'common';
import {take} from 'rxjs/operators';
import {ListViewStore} from './list-view.store';
import {listviewMockData, listviewStoreMock} from './list-view.store.spec.mock';
import {localStorageServiceMock} from '../../../../services/local-storage/local-storage.service.spec.mock';
import {SavedFilter} from '../../../../store/saved-filters/saved-filter.model';
import {SearchCriteriaFieldFilter} from '../../../../../../../common/src/lib/views/list/search-criteria.model';
describe('Listview Store', () => {
const service: ListViewStore = listviewStoreMock;

View file

@ -25,10 +25,10 @@
*/
import {
Action,
BulkActionsMap,
ColumnDefinition,
deepClone,
LineAction,
ListViewMeta,
Pagination,
Record,
@ -120,7 +120,7 @@ export class ListViewStore extends ViewStore implements StateStore,
moduleName$: Observable<string>;
columns: BehaviorSubject<ColumnDefinition[]>;
columns$: Observable<ColumnDefinition[]>;
lineActions$: Observable<LineAction[]>;
lineActions$: Observable<Action[]>;
records$: Observable<Record[]>;
criteria$: Observable<SearchCriteria>;
context$: Observable<ViewContext>;

View file

@ -25,8 +25,7 @@
*/
import {Injectable} from '@angular/core';
import {ViewMode} from 'common';
import {RecordActionData, RecordActionHandler, RecordActionHandlerMap} from './record.action';
import {RecordActionData} from './record.action';
import {RecordCancelAction} from './cancel/record-cancel.action';
import {RecordSaveAction} from './save/record-save.action';
import {RecordToggleWidgetsAction} from './toggle-widgets/record-widget-action.service';
@ -34,17 +33,12 @@ import {RecordEditAction} from './edit/record-edit.action';
import {RecordCreateAction} from './create/record-create.action';
import {RecordSaveNewAction} from './save-new/record-save-new.action';
import {CancelCreateAction} from './cancel-create/cancel-create.action';
import {BaseActionManager} from '../../../services/actions/base-action-manager.service';
@Injectable({
providedIn: 'root',
})
export class RecordActionManager {
actions: { [key: string]: RecordActionHandlerMap } = {
edit: {} as RecordActionHandlerMap,
detail: {} as RecordActionHandlerMap,
create: {} as RecordActionHandlerMap
};
export class RecordActionManager extends BaseActionManager<RecordActionData> {
constructor(
protected edit: RecordEditAction,
@ -55,6 +49,7 @@ export class RecordActionManager {
protected save: RecordSaveAction,
protected saveNew: RecordSaveNewAction,
) {
super();
edit.modes.forEach(mode => this.actions[mode][edit.key] = edit);
create.modes.forEach(mode => this.actions[mode][create.key] = create);
toggleWidgets.modes.forEach(mode => this.actions[mode][toggleWidgets.key] = toggleWidgets);
@ -63,20 +58,4 @@ export class RecordActionManager {
saveNew.modes.forEach(mode => this.actions[mode][saveNew.key] = saveNew);
cancelCreate.modes.forEach(mode => this.actions[mode][cancelCreate.key] = cancelCreate);
}
run(action: string, mode: ViewMode, data: RecordActionData): void {
if (!this.actions || !this.actions[mode] || !this.actions[mode][action]) {
return;
}
this.actions[mode][action].run(data);
}
getHandler(action: string, mode: ViewMode): RecordActionHandler {
if (!this.actions || !this.actions[mode] || !this.actions[mode][action]) {
return null;
}
return this.actions[mode][action];
}
}

View file

@ -24,26 +24,16 @@
* the words "Supercharged by SuiteCRM".
*/
import {ActionData, ActionHandler, ViewMode} from 'common';
import {ActionData, ActionHandler} from 'common';
import {RecordViewStore} from '../store/record-view/record-view.store';
export interface RecordActionData extends ActionData {
store: RecordViewStore;
}
export interface RecordActionHandlerMap {
[key: string]: RecordActionHandler;
}
export abstract class RecordActionHandler extends ActionHandler {
abstract modes: ViewMode[];
getStatus(store: RecordViewStore): string {
return '';
}
export abstract class RecordActionHandler extends ActionHandler<RecordActionData> {
abstract run(data: RecordActionData): void;
abstract shouldDisplay(store: RecordViewStore): boolean;
abstract shouldDisplay(data: RecordActionData): boolean;
}

View file

@ -27,7 +27,6 @@
import {Injectable} from '@angular/core';
import {ViewMode} from 'common';
import {RecordActionData, RecordActionHandler} from '../record.action';
import {RecordViewStore} from '../../store/record-view/record-view.store';
@Injectable({
providedIn: 'root'
@ -45,11 +44,11 @@ export class RecordToggleWidgetsAction extends RecordActionHandler {
data.store.showSidebarWidgets = !data.store.showSidebarWidgets;
}
shouldDisplay(store: RecordViewStore): boolean {
return store.widgets;
shouldDisplay(data: RecordActionData): boolean {
return data.store.widgets;
}
getStatus(store: RecordViewStore): string {
return store.showSidebarWidgets ? 'active': '';
getStatus(data: RecordActionData): string {
return data.store.showSidebarWidgets ? 'active' : '';
}
}

View file

@ -25,7 +25,7 @@
*/
import {Injectable} from '@angular/core';
import {Action, ActionDataSource, ModeActions} from 'common';
import {Action, ActionContext, ModeActions, ViewMode} from 'common';
import {combineLatest, Observable} from 'rxjs';
import {map, take} from 'rxjs/operators';
import {MetadataStore} from '../../../store/metadata/metadata.store.service';
@ -37,9 +37,10 @@ import {LanguageStore} from '../../../store/language/language.store';
import {MessageService} from '../../../services/message/message.service';
import {Process} from '../../../services/process/process.service';
import {ConfirmationModalService} from '../../../services/modals/confirmation-modal.service';
import {BaseRecordActionsAdapter} from '../../../services/actions/base-record-action.adapter';
@Injectable()
export class RecordActionsAdapter implements ActionDataSource {
export class RecordActionsAdapter extends BaseRecordActionsAdapter<RecordActionData> {
defaultActions: ModeActions = {
detail: [
@ -89,131 +90,77 @@ export class RecordActionsAdapter implements ActionDataSource {
protected actionManager: RecordActionManager,
protected asyncActionService: AsyncActionService,
protected message: MessageService,
protected confimation: ConfirmationModalService
protected confirmation: ConfirmationModalService
) {
super(
actionManager,
asyncActionService,
message,
confirmation,
language
)
}
getActions(): Observable<Action[]> {
getActions(context?: ActionContext): Observable<Action[]> {
return combineLatest(
[
this.metadata.recordViewMetadata$,
this.store.mode$,
this.store.record$,
this.language.vm$,
this.store.widgets$
this.store.language$,
this.store.widgets$,
]
).pipe(
map((
[
meta,
mode,
record,
languages,
widget
mode
]
) => {
if (!mode || !meta) {
return [];
}
const availableActions = {
detail: [],
edit: [],
create: [],
} as ModeActions;
if (meta.actions && meta.actions.length) {
meta.actions.forEach(action => {
if (!action.modes || !action.modes.length) {
return;
}
action.modes.forEach(actionMode => {
if (!availableActions[actionMode]) {
return;
}
availableActions[actionMode].push(action);
});
});
}
availableActions.detail = availableActions.detail.concat(this.defaultActions.detail);
availableActions.edit = availableActions.edit.concat(this.defaultActions.edit);
availableActions.create = availableActions.create.concat(this.defaultActions.create);
const actions = [];
availableActions[mode].forEach(action => {
if (!action.asyncProcess) {
const actionHandler = this.actionManager.getHandler(action.key, mode);
if (!actionHandler || !actionHandler.shouldDisplay(this.store)) {
return;
}
action.status = actionHandler.getStatus(this.store) || '';
}
const label = this.language.getFieldLabel(action.labelKey, record.module, languages);
actions.push({
...action,
label
});
});
return actions;
return this.parseModeActions(meta.actions, mode);
})
);
}
runAction(action: Action): void {
const params = (action && action.params) || {} as { [key: string]: any };
const displayConfirmation = params.displayConfirmation || false;
const confirmationLabel = params.confirmationLabel || '';
if (displayConfirmation) {
this.confimation.showModal(confirmationLabel, () => {
this.callAction(action);
});
return;
}
this.callAction(action);
protected buildActionData(action: Action, context?: ActionContext): RecordActionData {
return {
store: this.store
} as RecordActionData;
}
protected callAction(action: Action) {
if (action.asyncProcess) {
this.runAsyncAction(action);
return;
}
this.runFrontEndAction(action);
}
protected runAsyncAction(action: Action): void {
const actionName = `record-${action.key}`;
/**
* Build backend process input
*
* @param action
* @param actionName
* @param moduleName
* @param context
*/
protected buildActionInput(action: Action, actionName: string, moduleName: string, context: ActionContext = null): AsyncActionInput {
const baseRecord = this.store.getBaseRecord();
this.message.removeMessages();
const asyncData = {
return {
action: actionName,
module: baseRecord.module,
id: baseRecord.id,
} as AsyncActionInput;
this.asyncActionService.run(actionName, asyncData).pipe(take(1)).subscribe((process: Process) => {
if (process.data && process.data.reload) {
this.store.load(false).pipe(take(1)).subscribe();
}
});
}
protected runFrontEndAction(action: Action): void {
const data: RecordActionData = {
store: this.store
};
protected getMode(): ViewMode {
return this.store.getMode();
}
this.actionManager.run(action.key, this.store.getMode(), data);
protected getModuleName(context?: ActionContext): string {
return this.store.getModuleName();
}
protected reload(action: Action, process: Process, context?: ActionContext): void {
this.store.load(false).pipe(take(1)).subscribe();
}
}

View file

@ -27,7 +27,7 @@
import {combineLatest, Observable} from 'rxjs';
import {Injectable} from '@angular/core';
import {map} from 'rxjs/operators';
import {Panel, PanelRow, Record} from 'common';
import {Action, Panel, PanelRow, Record} from 'common';
import {MetadataStore, RecordViewMetadata} from '../../../store/metadata/metadata.store.service';
import {RecordContentConfig, RecordContentDataSource} from '../../../components/record-content/record-content.model';
import {RecordActionManager} from '../actions/record-action-manager.service';
@ -52,7 +52,11 @@ export class RecordContentAdapter implements RecordContentDataSource {
store: this.store
};
this.actions.run('edit', this.store.getMode(), data);
const action = {
key: 'edit'
} as Action;
this.actions.run(action, this.store.getMode(), data);
}
getDisplayConfig(): Observable<RecordContentConfig> {

View file

@ -76,6 +76,7 @@
<div class="w-100">
<scrm-action-group-menu
[config]="actionsAdapter"
[actionContext]="getActionContext(vm.record)"
klass="record-view-actions float-right"
buttonClass="settings-button"
>

View file

@ -82,6 +82,7 @@ import {map} from 'rxjs/operators';
import {RecordViewStore} from '../../store/record-view/record-view.store';
import {ModuleNavigation} from '../../../../services/navigation/module-navigation/module-navigation.service';
import {RecordActionsAdapter} from '../../adapters/actions.adapter';
import {ActionContext, Record} from 'common';
@Component({
selector: 'scrm-record-header',
@ -125,4 +126,19 @@ export class RecordHeaderComponent {
getSummaryTemplate(): string {
return this.recordViewStore.getSummaryTemplate();
}
/**
* Build action context
* @param record
*/
getActionContext(record: Record): ActionContext {
if (!record) {
return {} as ActionContext
}
return {
module: record.module || '',
record
} as ActionContext
}
}

View file

@ -127,7 +127,17 @@ class LineActionDefinitionProvider implements LineActionDefinitionProviderInterf
if ($this->checkAccess($relatedModuleName, $actionDefinition['acl']) === false) {
continue;
}
$createActions[] = array_merge($actionTemplate, $relatedModuleDef);
$action = array_merge($actionTemplate, $relatedModuleDef);
$action['modes'] = $action['modes'] ?? ['list'];
$action['params'] = $action['params'] ?? [];
$action['params']['create'] = $action['params']['create'] ?? [];
$action['params']['create']['module'] = $action['module'];
$action['params']['create']['mapping'] = $action['mapping'] ?? [];
$action['params']['create']['legacyModuleName'] = $action['legacyModuleName'] ?? '';
$action['params']['create']['action'] = $action['action'] ?? 'edit';
$createActions[] = $action;
}
return $createActions;

View file

@ -105,6 +105,7 @@ class SystemConfigHandler extends LegacyHandler implements SystemConfigProviderI
array $listViewSettingsLimits,
array $listViewActionsLimits,
array $recordViewActionLimits,
array $listViewLineActionsLimits,
array $uiConfigs,
array $extensions,
SessionInterface $session
@ -123,6 +124,7 @@ class SystemConfigHandler extends LegacyHandler implements SystemConfigProviderI
$this->injectedSystemConfigs['listview_settings_limits'] = $listViewSettingsLimits;
$this->injectedSystemConfigs['listview_actions_limits'] = $listViewActionsLimits;
$this->injectedSystemConfigs['recordview_actions_limits'] = $recordViewActionLimits;
$this->injectedSystemConfigs['listview_line_actions_limits'] = $listViewLineActionsLimits;
$this->injectedSystemConfigs['ui'] = $uiConfigs;
$this->injectedSystemConfigs['extensions'] = $extensions;
$this->mappers = $mappers;

View file

@ -34,7 +34,6 @@ use App\FieldDefinitions\Service\FieldDefinitionsProviderInterface;
use App\Module\Service\ModuleNameMapperInterface;
use App\ViewDefinitions\Service\SubPanelDefinitionProviderInterface;
use aSubPanel;
use Codeception\Step\Action;
use SubPanelDefinitions;
use Symfony\Component\HttpFoundation\Session\SessionInterface;
@ -81,9 +80,9 @@ class SubPanelDefinitionHandler extends LegacyHandler implements SubPanelDefinit
ModuleNameMapperInterface $moduleNameMapper,
FieldDefinitionsProviderInterface $fieldDefinitionProvider,
SessionInterface $session
)
{
parent::__construct($projectDir, $legacyDir, $legacySessionName, $defaultSessionName, $legacyScopeState, $session);
) {
parent::__construct($projectDir, $legacyDir, $legacySessionName, $defaultSessionName, $legacyScopeState,
$session);
$this->moduleNameMapper = $moduleNameMapper;
$this->fieldDefinitionProvider = $fieldDefinitionProvider;
}
@ -133,6 +132,7 @@ class SubPanelDefinitionHandler extends LegacyHandler implements SubPanelDefinit
foreach ($tabs as $key => $tab) {
/** @var aSubPanel $subpanel */
$subpanel = $spd->load_subpanel($key);
if ($subpanel === false) {
@ -479,18 +479,19 @@ class SubPanelDefinitionHandler extends LegacyHandler implements SubPanelDefinit
}
/**
* @param array $subpanel_def
* @param $subpanel_module
* @param aSubPanel $subpanelDef
* @param string $subpanelModule
* @description this function fetches all the line actions defined for a subpanel
* for now, only the remove action is filtered from all available line actions
* @return array
*/
public function getSubpanelLineActions($subpanel_def, $subpanel_module): array
public function getSubpanelLineActions(aSubPanel $subpanelDef, $subpanelModule): array
{
$lineActions = [];
$unlinkLineAction = [];
$subpanelLineActions = ['edit_button', 'close_button', 'remove_button'];
$thepanel = $subpanel_def->isCollection() ? $subpanel_def->get_header_panel_def() : $subpanel_def;
$thepanel = $subpanelDef->isCollection() ? $subpanelDef->get_header_panel_def() : $subpanelDef;
foreach ($thepanel->get_list_fields() as $field_name => $list_field) {
@ -514,17 +515,28 @@ class SubPanelDefinitionHandler extends LegacyHandler implements SubPanelDefinit
}
if (in_array('remove_button', $lineActions, true)) {
$unlinkLineAction = [
[
'key' => 'unlink',
'action' => 'unlink',
'icon' => 'unlink',
'asyncProcess' => true,
'labelKey' => 'LBL_UNLINK_RECORD',
'module' => $subpanel_module,
'module' => $subpanelModule,
'routing' => false,
'params' => [
'linkFieldMapping' => [
'get_emails_by_assign_or_link' => 'emails'
],
'displayConfirmation' => true,
'confirmationLabel' => 'LBL_UNLINK_RELATIONSHIP_CONFIRM'
],
'modes' => ['list']
],
];
}
return $unlinkLineAction;
}