Add support for extra query parameters on classic view

- Update ClassicView Graphql API
-- add custom query for retrieving the classic view
-- add custom resolver

- Add ClassicView legacy handler

- Update ClassicView angular rendering
-- add classic view facade
-- add classic-view graphql query
-- remove rest classic-view request

- Update ClassicView angular resolver
-- send information about extra query params

- Add custom router re-use strategy
-- Allow configuring if a given route should be re-used
-- Disable re-usage in classic view routes
This commit is contained in:
Clemente Raposo 2020-03-25 12:42:33 +00:00 committed by Dillon-Brown
parent 3bce817795
commit ec5688c2e0
17 changed files with 447 additions and 131 deletions

View file

@ -57,7 +57,8 @@
"App\\DataFixtures\\": "core/src/DataFixtures/",
"App\\DataProvider\\": "core/src/DataProvider/",
"App\\Services\\": "core/src/Service/",
"App\\EventListener\\": "core/src/EventListener/"
"App\\EventListener\\": "core/src/EventListener/",
"App\\Resolver\\": "core/src/Resolver/"
}
},
"scripts": {

View file

@ -11,3 +11,11 @@ parameters:
modulelistmenu: modulelistmenu
favorites: favorites
noaccess: noaccess
step1: step1
composeview: compose
wizardhome: wizard-home
campaigndiagnostic: diagnostic
webtoleadcreation: web-to-lead
resourcelist: resource-list
quick_radius: quick-radius

View file

@ -22,77 +22,101 @@ parameters:
frontend: notes
core: Notes
Leads:
frontend: leads
core: Leads
frontend: leads
core: Leads
Contacts:
frontend: contacts
core: Contacts
frontend: contacts
core: Contacts
Accounts:
frontend: accounts
core: Accounts
frontend: accounts
core: Accounts
Opportunities:
frontend: opportunities
core: Opportunities
frontend: opportunities
core: Opportunities
Import:
frontend: import
core: Import
Emails:
frontend: emails
core: Emails
frontend: emails
core: Emails
EmailTemplates:
frontend: email-templates
core: EmailTemplates
frontend: email-templates
core: EmailTemplates
InboundEmail:
frontend: inbound-email
core: InboundEmail
MailMerge:
frontend: mail-merge
core: MailMerge
Schedulers:
frontend: schedulers
core: Schedulers
Campaigns:
frontend: campaigns
core: Campaigns
frontend: campaigns
core: Campaigns
Targets:
frontend: targets
core: Targets
Prospects:
frontend: prospects
core: Prospects
frontend: prospects
core: Prospects
ProspectLists:
frontend: prospect-lists
core: ProspectLists
frontend: prospect-lists
core: ProspectLists
Documents:
frontend: documents
core: Documents
frontend: documents
core: Documents
Cases:
frontend: cases
core: Cases
frontend: cases
core: Cases
Project:
frontend: project
core: Project
frontend: project
core: Project
ProjectTask:
frontend: project-task
core: ProjectTask
Bugs:
frontend: bugs
core: Bugs
frontend: bugs
core: Bugs
ResourceCalendar:
frontend: resource-calendar
core: ResourceCalendar
frontend: resource-calendar
core: ResourceCalendar
AOBH_BusinessHours:
frontend: business-hours
core: BusinessHours
frontend: business-hours
core: BusinessHours
Spots:
frontend: spots
core: Spots
frontend: spots
core: Spots
SecurityGroups:
frontend: security-groups
core: SecurityGroups
frontend: security-groups
core: SecurityGroups
ACL:
frontend: acl
core: ACL
frontend: acl
core: ACL
ACLRoles:
frontend: acl-roles
core: ACLRoles
frontend: acl-roles
core: ACLRoles
Roles:
frontend: roles
core: Roles
Configurator:
frontend: configurator
core: Configurator
frontend: configurator
core: Configurator
UserPreferences:
frontend: user-preferences
core: UserPreferences
frontend: user-preferences
core: UserPreferences
Users:
frontend: users
core: Users
SavedSearch:
frontend: saved-search
core: SavedSearch
frontend: saved-search
core: SavedSearch
Studio:
frontend: studio
core: Studio
frontend: studio
core: Studio
Connectors:
frontend: connectors
core: Connectors
frontend: connectors
core: Connectors
SugarFeed:
frontend: sugar-feed
core: SugarFeed
@ -154,8 +178,8 @@ parameters:
frontend: events
core: Events
FP_Event_Locations:
frontend: event-Locations
core: EventLocations
frontend: event-locations
core: EventLocations
AOS_Contracts:
frontend: contracts
core: Contracts
@ -220,14 +244,17 @@ parameters:
frontend: report-conditions
core: ReportConditions
AOW_WorkFlow:
frontend: workFlow
core: WorkFlow
frontend: workflow
core: Workflow
AOW_Actions:
frontend: workflow-actions
core: WorkflowActions
frontend: workflow-actions
core: WorkflowActions
AOW_Processed:
frontend: workflow-processed
core: WorflowProcessed
frontend: workflow-processed
core: WorflowProcessed
AOW_Conditions:
frontend: workflow-conditions
core: WorkflowConditions
frontend: workflow-conditions
core: WorkflowConditions
Help:
frontend: help
core: Help

View file

@ -0,0 +1,26 @@
import {ActivatedRouteSnapshot, DetachedRouteHandle, RouteReuseStrategy} from '@angular/router';
export class AppRouteReuseStrategy implements RouteReuseStrategy {
shouldDetach(route: ActivatedRouteSnapshot): boolean {
return false;
}
store(route: ActivatedRouteSnapshot, detachedTree: DetachedRouteHandle): void {
}
shouldAttach(route: ActivatedRouteSnapshot): boolean {
return false;
}
retrieve(route: ActivatedRouteSnapshot): DetachedRouteHandle | null {
return null;
}
shouldReuseRoute(future: ActivatedRouteSnapshot, curr: ActivatedRouteSnapshot): boolean {
if (future.data && future.data.reuseRoute === false) {
return false;
}
return future.routeConfig === curr.routeConfig;
}
}

View file

@ -1,7 +1,7 @@
import {NgModule} from '@angular/core';
import {Routes, RouterModule} from '@angular/router';
import {ClassicViewUiComponent} from '@components/classic-view/classic-view.component';
import {ClassicViewResolver} from '@services/api/resolvers/classic-view.resolver';
import {ClassicViewResolver} from '@services/classic-view/classic-view.resolver';
import {BaseMetadataResolver} from '@services/metadata/base-metadata.resolver';
import {AuthGuard} from '../services/auth/auth-guard.service';
import {ListComponent} from '@views/list/list.component';
@ -25,19 +25,31 @@ const routes: Routes = [
path: ':module',
component: ClassicViewUiComponent,
canActivate: [AuthGuard],
resolve: {view: ClassicViewResolver}
runGuardsAndResolvers: 'always',
resolve: {view: ClassicViewResolver},
data: {
reuseRoute: false
}
},
{
path: ':module/:action',
component: ClassicViewUiComponent,
canActivate: [AuthGuard],
resolve: {view: ClassicViewResolver}
runGuardsAndResolvers: 'always',
resolve: {view: ClassicViewResolver},
data: {
reuseRoute: false
}
},
{
path: ':module/:action/:record',
component: ClassicViewUiComponent,
canActivate: [AuthGuard],
resolve: {view: ClassicViewResolver}
runGuardsAndResolvers: 'always',
resolve: {view: ClassicViewResolver},
data: {
reuseRoute: false
}
},
{path: '**', redirectTo: 'Login'},
];

View file

@ -31,6 +31,8 @@ import {
import {environment} from '../environments/environment';
import {FetchPolicy} from 'apollo-client/core/watchQueryOptions';
import {FullPageSpinnerComponent} from '@components/full-page-spinner/full-page-spinner.component';
import {RouteReuseStrategy} from '@angular/router';
import {AppRouteReuseStrategy} from './app-router-reuse-strategy';
@NgModule({
declarations: [
@ -60,6 +62,9 @@ import {FullPageSpinnerComponent} from '@components/full-page-spinner/full-page-
BrowserAnimationsModule,
NgbModule
],
providers: [
{provide: RouteReuseStrategy, useClass: AppRouteReuseStrategy}
],
bootstrap: [AppComponent],
entryComponents: []
})

View file

@ -6,6 +6,8 @@ import {HttpClientTestingModule, HttpTestingController} from '@angular/common/ht
import {LoginUiComponent} from './login.component';
import {ApiService} from '../../services/api/api.service';
import {ApolloTestingModule} from 'apollo-angular/testing';
import {BrowserAnimationsModule} from '@angular/platform-browser/animations';
describe('LoginComponent', () => {
let component: LoginUiComponent;
@ -14,7 +16,13 @@ describe('LoginComponent', () => {
beforeEach(async(() => {
TestBed.configureTestingModule({
schemas: [CUSTOM_ELEMENTS_SCHEMA],
imports: [RouterTestingModule, HttpClientTestingModule, FormsModule],
imports: [
RouterTestingModule,
HttpClientTestingModule,
FormsModule,
ApolloTestingModule,
BrowserAnimationsModule
],
declarations: [LoginUiComponent]
})
.compileComponents();

View file

@ -278,31 +278,4 @@ export class ApiService {
): boolean {
return this.request({type: 'GET', url}, onSuccess, onError);
}
getClassicView(routeParams: ParamMap): Observable<any> {
let url = API_URL
let module = routeParams.get('module') || '';
url += '/classic-views/' + module;
let params = new HttpParams();
routeParams.keys.forEach((name) => {
let value = routeParams.get(name);
if (name = 'module'){
return;
}
if (value == null || value == undefined) {
return;
}
params = params.set(name, value);
});
return this.http.get(url, {
params: params
});
}
}

View file

@ -1,21 +0,0 @@
import {Injectable} from '@angular/core';
import {
Resolve,
ActivatedRouteSnapshot,
RouterStateSnapshot
} from '@angular/router';
import {ApiService} from "../api.service";
@Injectable({providedIn: 'root'})
export class ClassicViewResolver implements Resolve<any> {
constructor(private apiService: ApiService) {
}
resolve(route: ActivatedRouteSnapshot, state: RouterStateSnapshot) {
return this.apiService.getClassicView(route.paramMap);
}
}

View file

@ -0,0 +1,45 @@
import {Injectable} from '@angular/core';
import {Apollo} from 'apollo-angular';
import gql from 'graphql-tag';
import {Observable} from 'rxjs';
import {ApolloQueryResult} from 'apollo-client';
@Injectable({
providedIn: 'root'
})
export class ClassicViewGQL {
constructor(private apollo: Apollo) {
}
/**
* Fetch data either from backend
*
* @param module to get from
* @param id of the record
* @param params
* @param metadata with the fields to ask for
*/
public fetch(module: string,
params: { [key: string]: string },
metadata: { fields: string[] }): Observable<ApolloQueryResult<any>> {
const fields = metadata.fields;
const queryOptions = {
query: gql`
query getClassicView($module: String!, $params: Iterable) {
getClassicView(module:$module, params:$params) {
${fields.join('\n')}
}
}
`,
variables: {
module,
params
},
};
return this.apollo.query(queryOptions);
}
}

View file

@ -0,0 +1,77 @@
import {Injectable} from '@angular/core';
import {Observable} from 'rxjs';
import {map} from 'rxjs/operators';
import {ClassicViewGQL} from './api.classic-view.get';
export interface ClassicView {
id: string;
_id: string;
action: string;
record: string;
html: string;
}
@Injectable({
providedIn: 'root',
})
export class ClassicViewFacade {
protected fieldsMetadata = {
fields: [
'id',
'_id',
'action',
'record',
'html'
]
};
constructor(private classicViewGQL: ClassicViewGQL) {
}
/**
* Public Api
*/
/**
* Load ClassicView from the backend.
* Returns observable to be used in resolver if needed
*
* @returns Observable<any>
*/
public load($module: string, $params: { [key: string]: string }): Observable<any> {
return this.fetch($module, $params);
}
/**
* Internal API
*/
/**
* Fetch the classic view from the backend
*
* @returns Observable<any>
*/
protected fetch($module: string, $params: { [key: string]: string }): Observable<ClassicView> {
return this.classicViewGQL.fetch($module, $params, this.fieldsMetadata)
.pipe(map(({data}) => {
const classicView: ClassicView = {
id: null,
_id: null,
action: null,
record: null,
html: null,
};
if (data.getClassicView) {
return data.getClassicView;
}
return classicView;
}));
}
}

View file

@ -0,0 +1,33 @@
import {Injectable} from '@angular/core';
import {
Resolve,
ActivatedRouteSnapshot,
RouterStateSnapshot
} from '@angular/router';
import {ClassicViewFacade} from '@services/classic-view/classic-view.facade';
@Injectable({providedIn: 'root'})
export class ClassicViewResolver implements Resolve<any> {
constructor(private classicViewFacade: ClassicViewFacade) {
}
resolve(route: ActivatedRouteSnapshot, state: RouterStateSnapshot) {
const params = {
...route.queryParams
}
if (route.params.action) {
params.action = route.params.action;
}
if (route.params.record) {
params.record = route.params.record;
}
return this.classicViewFacade.load(route.params.module, params);
}
}

View file

@ -0,0 +1,40 @@
<?php
namespace SuiteCRM\Core\Legacy;
use App\Entity\ClassicView;
class ClassicViewHandler extends LegacyHandler
{
/**
* Get app strings for given $language
* @param string $module
* @param array $params
* @return ClassicView|null
*/
public function getClassicView(string $module, array $params): ?ClassicView
{
$output = new ClassicView();
$output->setId('123');
$html = '<h1>HTML working</h1><script>alert(\'JS Working\');</script><button onClick="alert(\'JS Working\');">Click Me</button>';
$html .= '<br/><a href="index.php?module=Contacts&action=ListView">Legacy Link to Contacts List View</a>';
$html .= '<br/><strong>Legacy folder Image:</strong>';
$html .= '<img src="themes/default/images/company_logo.png" alt="SuiteCRM" style="margin: 5px 0;">';
$html .= '<br/><strong>Legacy Link with extra params: </strong>' . '<a href="index.php?module=Import&action=Step1&import_module=Accounts&return_module=Accounts&return_action=index">Legacy Link to Accounts Import</a>';
$html .= '<br/><strong>Received Params:</strong>';
$html .= '<br/><ul><li><strong>Module: </strong> "' . $module . '"</li>';
if (!empty($params)) {
foreach ($params as $key => $value) {
$html .= '<li><strong>' . $key . '</strong> "' . $value . '"</li>';
}
}
$html .= '</ul>';
$output->setHtml($html);
return $output;
}
}

View file

@ -5,6 +5,7 @@ namespace App\DataProvider;
use ApiPlatform\Core\DataProvider\ItemDataProviderInterface;
use ApiPlatform\Core\DataProvider\RestrictedDataProviderInterface;
use App\Entity\ClassicView;
use SuiteCRM\Core\Legacy\ClassicViewHandler;
/**
* Class ClassicViewItemDataProvider
@ -12,6 +13,21 @@ use App\Entity\ClassicView;
*/
final class ClassicViewItemDataProvider implements ItemDataProviderInterface, RestrictedDataProviderInterface
{
/**
* @var ClassicViewHandler
*/
private $classicViewHandler;
/**
* ClassicViewItemDataProvider constructor.
* @param ClassicViewHandler $classicViewHandler
*/
public function __construct(ClassicViewHandler $classicViewHandler)
{
$this->classicViewHandler = $classicViewHandler;
}
/**
* Defined supported resources
* @param string $resourceClass
@ -34,15 +50,11 @@ final class ClassicViewItemDataProvider implements ItemDataProviderInterface, Re
*/
public function getItem(string $resourceClass, $id, string $operationName = null, array $context = []): ?ClassicView
{
$output = new ClassicView();
$output->setId('123');
$params = [];
if (!empty($context['args']) && !empty($context['args']['params'])) {
$params = $context['args']['params'];
}
$html = '<h1>HTML working</h1><script>alert(\'JS Working\');</script><button onClick="alert(\'JS Working\');">Click Me</button>';
$html .= '<br/><a href="index.php?module=Contacts&action=ListView">Legacy Link to Contacts List View</a>';
$html .= '<br/><strong>Legacy folder Image:</strong>';
$html .= '<img src="themes/default/images/company_logo.png" alt="SuiteCRM" style="margin: 5px 0;">';
$output->setHtml($html);
return $output;
return $this->classicViewHandler->getClassicView($id, $params);
}
}

View file

@ -4,6 +4,7 @@ namespace App\Entity;
use ApiPlatform\Core\Annotation\ApiProperty;
use ApiPlatform\Core\Annotation\ApiResource;
use App\Resolver\ClassicViewResolver;
/**
* @ApiResource(
@ -39,6 +40,15 @@ use ApiPlatform\Core\Annotation\ApiResource;
* },
* collectionOperations={
* },
* graphql={
* "get"={
* "item_query"=ClassicViewResolver::class,
* "args"={
* "module"={"type"="String!"},
* "params"={"type"="Iterable" , "description"="legacy query params list"}
* }
* },
* },
* )
*/
class ClassicView
@ -66,7 +76,7 @@ class ClassicView
/**
* The action
*
* @var string
* @var string|null
*
* @ApiProperty(
* attributes={
@ -78,12 +88,12 @@ class ClassicView
* }
* )
*/
protected $action;
protected $action = null;
/**
* The record.
*
* @var string
* @var string|null
*
* @ApiProperty(
* attributes={
@ -94,7 +104,7 @@ class ClassicView
* }
* )
*/
protected $record;
protected $record = null;
/**
* The view html.
@ -132,9 +142,9 @@ class ClassicView
/**
* Set Action
* @param string $action
* @param string|null $action
*/
public function setAction(string $action): void
public function setAction(?string $action): void
{
$this->action = $action;
}
@ -150,9 +160,9 @@ class ClassicView
/**
* Set Record
* @param string $record
* @param string|null $record
*/
public function setRecord(string $record): void
public function setRecord(?string $record): void
{
$this->record = $record;
}

View file

@ -0,0 +1,36 @@
<?php
namespace App\Resolver;
use ApiPlatform\Core\GraphQl\Resolver\QueryItemResolverInterface;
use App\Entity\ClassicView;
use SuiteCRM\Core\Legacy\ClassicViewHandler;
class ClassicViewResolver implements QueryItemResolverInterface
{
/**
* @var ClassicViewHandler
*/
private $classicViewHandler;
/**
* ClassicViewResolver constructor.
* @param ClassicViewHandler $classicViewHandler
*/
public function __construct(ClassicViewHandler $classicViewHandler)
{
$this->classicViewHandler = $classicViewHandler;
}
/**
* @param ClassicView|null $item
*
* @param array $context
* @return ClassicView
*/
public function __invoke($item, array $context)
{
return $this->classicViewHandler->getClassicView($context['args']['module'], $context['args']['params']);
}
}

View file

@ -76,11 +76,20 @@ class RouteConverter
$action = $request->query->get('action');
$record = $request->query->get('record');
if (empty($module)){
if (empty($module)) {
throw new InvalidArgumentException('No module defined');
}
return $this->buildRoute($module, $action, $record);
$route = $this->buildRoute($module, $action, $record);
if (null !== $queryString = $request->getQueryString()) {
$queryString = $this->removeParameter($queryString, 'module', $module);
$queryString = $this->removeParameter($queryString, 'action', $action);
$queryString = $this->removeParameter($queryString, 'record', $record);
$queryString = '?' . Request::normalizeQueryString($queryString);
}
return $route . $queryString;
}
/**
@ -159,4 +168,19 @@ class RouteConverter
{
return $this->moduleNameMapper->toFrontEnd($module);
}
/**
* @param string|null $queryString
* @param string|null $param
* @param string|null $value
* @return string|string[]|null
*/
protected function removeParameter(?string $queryString, ?string $param, ?string $value)
{
if (empty($value) || empty($param) || empty($queryString)) {
return $queryString;
}
return str_replace("$param=$value", '', $queryString);;
}
}