Implement record-view routing

Signed-off-by: Dillon-Brown <dillon.brown@salesagility.com>
This commit is contained in:
Dillon-Brown 2020-08-06 13:54:14 +01:00
parent 573fed6d2b
commit c5ee6b4f8e
17 changed files with 610 additions and 6 deletions

View file

@ -17,6 +17,11 @@ if (is_array($env = @include dirname(__DIR__) . '/.env.local.php') && (!isset($e
(new Dotenv(false))->loadEnv(dirname(__DIR__) . '/.env');
}
// Global annotations to ignore
Doctrine\Common\Annotations\AnnotationReader::addGlobalIgnoredName('query');
Doctrine\Common\Annotations\AnnotationReader::addGlobalIgnoredName('fields_array');
Doctrine\Common\Annotations\AnnotationReader::addGlobalIgnoredName('absrtact');
$_SERVER += $_ENV;
$_SERVER['APP_ENV'] = $_ENV['APP_ENV'] = ($_SERVER['APP_ENV'] ?? $_ENV['APP_ENV'] ?? null) ?: 'dev';
$_SERVER['APP_DEBUG'] = $_SERVER['APP_DEBUG'] ?? $_ENV['APP_DEBUG'] ?? 'prod' !== $_SERVER['APP_ENV'];

View file

@ -8,6 +8,9 @@ import {ListComponent} from '@views/list/list.component';
import {LoginAuthGuard} from '@services/auth/login-auth-guard.service';
import {BaseListResolver} from '@services/metadata/base-list.resolver';
import {BaseModuleResolver} from '@base/services/metadata/base-module.resolver';
import {BaseRecordResolver} from '@services/metadata/base-record.resolver';
import {RecordComponent} from '@views/record/record.component';
import {RecordViewGuard} from '@services/record-view/record-view-guard.service';
/**
* @param {[]} segments of url
@ -140,11 +143,12 @@ const routes: Routes = [
},
{
path: ':module/:action/:record',
component: ClassicViewUiComponent,
canActivate: [AuthGuard],
component: RecordComponent,
canActivate: [AuthGuard, RecordViewGuard],
runGuardsAndResolvers: 'always',
resolve: {
legacyUrl: ClassicViewResolver,
view: BaseModuleResolver,
metadata: BaseRecordResolver
},
data: {
reuseRoute: false,

View file

@ -22,6 +22,7 @@ import {ModuleTitleModule} from '@components/module-title/module-title.module';
import {ListHeaderModule} from '@components/list-header/list-header.module';
import {ListcontainerUiModule} from '@components/list-container/list-container.module';
import {ListModule} from '@views/list/list.module';
import {RecordModule} from '@views/record/record.module';
import {BrowserAnimationsModule} from '@angular/platform-browser/animations';
import {NgbModule} from '@ng-bootstrap/ng-bootstrap';
import {ErrorInterceptor} from '@services/auth/error.interceptor';
@ -56,6 +57,7 @@ import {BnNgIdleService} from 'bn-ng-idle';
ClassicViewUiModule,
FilterUiModule,
ListModule,
RecordModule,
WidgetUiModule,
TableUiModule,
ModuleTitleModule,

View file

@ -0,0 +1,47 @@
import {Injectable} from '@angular/core';
import {ActivatedRouteSnapshot} from '@angular/router';
import {ModuleNameMapper} from '@services/navigation/module-name-mapper/module-name-mapper.service';
import {ActionNameMapper} from '@services/navigation/action-name-mapper/action-name-mapper.service';
import {SystemConfigStore} from '@base/store/system-config/system-config.store';
import {LanguageStore} from '@base/store/language/language.store';
import {NavigationStore} from '@base/store/navigation/navigation.store';
import {UserPreferenceStore} from '@base/store/user-preference/user-preference.store';
import {ThemeImagesStore} from '@base/store/theme-images/theme-images.store';
import {AppStateStore} from '@base/store/app-state/app-state.store';
import {MetadataStore} from '@store/metadata/metadata.store.service';
import {BaseModuleResolver} from '@services/metadata/base-module.resolver';
import {forkJoin, Observable} from 'rxjs';
@Injectable({providedIn: 'root'})
export class BaseRecordResolver extends BaseModuleResolver {
constructor(
protected systemConfigStore: SystemConfigStore,
protected languageStore: LanguageStore,
protected navigationStore: NavigationStore,
protected metadataStore: MetadataStore,
protected userPreferenceStore: UserPreferenceStore,
protected themeImagesStore: ThemeImagesStore,
protected moduleNameMapper: ModuleNameMapper,
protected actionNameMapper: ActionNameMapper,
protected appStateStore: AppStateStore,
) {
super(
systemConfigStore,
languageStore,
navigationStore,
userPreferenceStore,
themeImagesStore,
moduleNameMapper,
actionNameMapper,
appStateStore
);
}
resolve(route: ActivatedRouteSnapshot): Observable<any> {
return forkJoin({
base: super.resolve(route),
metadata: this.metadataStore.load(route.params.module, this.metadataStore.getMetadataTypes()),
});
}
}

View file

@ -0,0 +1,43 @@
import {Injectable} from '@angular/core';
import {ActivatedRouteSnapshot, CanActivate, Router} from '@angular/router';
import {Observable, throwError} from 'rxjs';
import {catchError, map} from 'rxjs/operators';
import {MessageService} from '@services/message/message.service';
import {RecordViewGQL} from '@store/record-view/api.record.get';
@Injectable({
providedIn: 'root'
})
export class RecordViewGuard implements CanActivate {
protected fieldsMetadata = {
fields: [
'_id',
'id',
'record'
]
};
constructor(
protected message: MessageService,
protected recordViewGQL: RecordViewGQL,
protected router: Router
) {
}
canActivate(route: ActivatedRouteSnapshot): Observable<boolean> {
return this.recordViewGQL.fetch(route.params.module, route.params.record, this.fieldsMetadata)
.pipe(
map(({data}) => {
const id = data.getRecordView.record.id;
if (id) {
return true;
} else {
this.message.addDangerMessageByKey('LBL_RECORD_DOES_NOT_EXIST');
return false;
}
}),
catchError(err => throwError(err)),
);
}
}

View file

@ -0,0 +1,46 @@
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 RecordViewGQL {
constructor(private apollo: Apollo) {
}
/**
* Fetch data from backend
*
* @param {string} module name
* @param {string} record id
* @param {object} metadata with the fields to ask for
* @returns {object} Observable<ApolloQueryResult<any>>
*/
public fetch(
module: string,
record: string,
metadata: { fields: string[] }
): Observable<ApolloQueryResult<any>> {
const fields = metadata.fields;
const queryOptions = {
query: gql`
query recordView($module: String!, $record: String!) {
getRecordView(module: $module, record: $record) {
${fields.join('\n')}
}
}
`,
variables: {
module,
record,
},
};
return this.apollo.query(queryOptions);
}
}

View file

@ -0,0 +1,25 @@
import {Injectable} from '@angular/core';
import {AppData, ViewStore} from '@store/view/view.store';
import {Metadata} from '@store/metadata/metadata.store.service';
import {Observable} from 'rxjs';
export interface RecordViewModel {
appData: AppData;
metadata: Metadata;
}
@Injectable()
export class RecordViewStore extends ViewStore {
vm$: Observable<RecordViewModel>;
vm: RecordViewModel;
clear(): void {
}
/**
* Clean destroy
*/
public destroy(): void {
this.clear();
}
}

View file

@ -0,0 +1,4 @@
<!-- Start Record View Section -->
<div class="record-view" *ngIf="(vm$ | async) as vm">
</div>
<!-- End Record View Section -->

View file

@ -0,0 +1,61 @@
import {async, ComponentFixture, TestBed} from '@angular/core/testing';
import {Component} from '@angular/core';
import {HttpClientTestingModule} from '@angular/common/http/testing';
import {RouterTestingModule} from '@angular/router/testing';
import {BrowserAnimationsModule} from '@angular/platform-browser/animations';
import {ApolloTestingModule} from 'apollo-angular/testing';
import {ImageModule} from '@components/image/image.module';
import {DynamicModule} from 'ng-dynamic-component';
import {FieldModule} from '@fields/field.module';
import {DropdownButtonModule} from '@components/dropdown-button/dropdown-button.module';
import {SortButtonModule} from '@components/sort-button/sort-button.module';
import {RecordViewStore} from '@store/record-view/record-view.store';
import {RecordComponent} from '@views/record/record.component';
@Component({
selector: 'record-test-host-component',
template: '<scrm-record></scrm-record>'
})
class RecordTestHostComponent {
}
describe('RecordComponent', () => {
let testHostComponent: RecordTestHostComponent;
let testHostFixture: ComponentFixture<RecordTestHostComponent>;
beforeEach(async(() => {
/* eslint-disable camelcase, @typescript-eslint/camelcase */
TestBed.configureTestingModule({
imports: [
HttpClientTestingModule,
RouterTestingModule,
BrowserAnimationsModule,
ImageModule,
ApolloTestingModule,
DynamicModule,
FieldModule,
DropdownButtonModule,
DropdownButtonModule,
SortButtonModule
],
declarations: [RecordComponent, RecordTestHostComponent],
providers: [
{
provide: RecordViewStore
}
],
})
.compileComponents();
/* eslint-enable camelcase, @typescript-eslint/camelcase */
}));
beforeEach(() => {
testHostFixture = TestBed.createComponent(RecordTestHostComponent);
testHostComponent = testHostFixture.componentInstance;
testHostFixture.detectChanges();
});
it('should create', () => {
expect(testHostComponent).toBeTruthy();
});
});

View file

@ -0,0 +1,30 @@
import {Component, OnDestroy, OnInit} from '@angular/core';
import {AppStateStore} from '@store/app-state/app-state.store';
import {Observable, Subscription} from 'rxjs';
import {RecordViewModel, RecordViewStore} from '@store/record-view/record-view.store';
@Component({
selector: 'scrm-record',
templateUrl: './record.component.html',
styleUrls: [],
providers: [RecordViewStore]
})
export class RecordComponent implements OnInit, OnDestroy {
recordSub: Subscription;
vm$: Observable<RecordViewModel> = null;
constructor(protected appState: AppStateStore, protected recordStore: RecordViewStore) {
}
ngOnInit(): void {
this.vm$ = this.recordStore.vm$;
}
ngOnDestroy(): void {
if (this.recordSub) {
this.recordSub.unsubscribe();
}
this.recordStore.destroy();
}
}

View file

@ -0,0 +1,15 @@
import {NgModule} from '@angular/core';
import {CommonModule} from '@angular/common';
import {RecordComponent} from './record.component';
import {FieldModule} from '@fields/field.module';
@NgModule({
declarations: [RecordComponent],
exports: [RecordComponent],
imports: [
CommonModule,
FieldModule
],
})
export class RecordModule {
}

View file

@ -1,3 +0,0 @@
<?php
// stub for record view

View file

@ -0,0 +1,111 @@
<?php
namespace SuiteCRM\Core\Legacy;
use App\Entity\RecordView;
use App\Service\ModuleNameMapperInterface;
use App\Service\RecordViewProviderInterface;
use BeanFactory;
use InvalidArgumentException;
use SugarBean;
/**
* Class RecordViewHandler
* @package SuiteCRM\Core\Legacy
*/
class RecordViewHandler extends LegacyHandler implements RecordViewProviderInterface
{
public const HANDLER_KEY = 'record';
/**
* @var ModuleNameMapperInterface
*/
private $moduleNameMapper;
/**
* RecordViewHandler constructor.
* @param string $projectDir
* @param string $legacyDir
* @param string $legacySessionName
* @param string $defaultSessionName
* @param LegacyScopeState $legacyScopeState
* @param ModuleNameMapperInterface $moduleNameMapper
*/
public function __construct(
string $projectDir,
string $legacyDir,
string $legacySessionName,
string $defaultSessionName,
LegacyScopeState $legacyScopeState,
ModuleNameMapperInterface $moduleNameMapper
) {
parent::__construct($projectDir, $legacyDir, $legacySessionName, $defaultSessionName, $legacyScopeState);
$this->moduleNameMapper = $moduleNameMapper;
}
/**
* @inheritDoc
*/
public function getHandlerKey(): string
{
return self::HANDLER_KEY;
}
/**
* @param string $module
* @param string $id
* @return RecordView
*/
public function getRecord(string $module, string $id): RecordView
{
$this->init();
$recordView = new RecordView();
$moduleName = $this->validateModuleName($module);
$bean = BeanFactory::getBean($moduleName, $id);
if (!$bean) {
$bean = $this->newBeanSafe($moduleName);
}
$recordView->setId($id);
$recordView->setRecord((array)$bean);
$this->close();
return $recordView;
}
/**
* @param string $module
*
* @return SugarBean
* @throws InvalidArgumentException When the module is invalid.
*/
private function newBeanSafe($module): SugarBean
{
$bean = BeanFactory::newBean($module);
if (!$bean instanceof SugarBean) {
throw new InvalidArgumentException(sprintf('Module %s does not exist', $module));
}
return $bean;
}
/**
* @param $moduleName
* @return string
*/
private function validateModuleName($moduleName): string
{
$moduleName = $this->moduleNameMapper->toLegacy($moduleName);
if (!$this->moduleNameMapper->isValidModule($moduleName)) {
throw new InvalidArgumentException('Invalid module name: ' . $moduleName);
}
return $moduleName;
}
}

View file

@ -0,0 +1,59 @@
<?php
namespace App\DataProvider;
use ApiPlatform\Core\DataProvider\ItemDataProviderInterface;
use ApiPlatform\Core\DataProvider\RestrictedDataProviderInterface;
use App\Entity\RecordView;
use App\Service\RecordViewProviderInterface;
use Exception;
/**
* Class RecordViewItemDataProvider
*/
final class RecordViewItemDataProvider implements ItemDataProviderInterface, RestrictedDataProviderInterface
{
/**
* @var RecordViewProviderInterface
*/
private $recordViewHandler;
/**
* RecordViewItemDataProvider constructor.
* @param RecordViewProviderInterface $recordViewHandler
*/
public function __construct(RecordViewProviderInterface $recordViewHandler)
{
$this->recordViewHandler = $recordViewHandler;
}
/**
* Defined supported resources
* @param string $resourceClass
* @param string|null $operationName
* @param array $context
* @return bool
*/
public function supports(string $resourceClass, string $operationName = null, array $context = []): bool
{
return RecordView::class === $resourceClass;
}
/**
* Get get record by id
* @param string $resourceClass
* @param array|int|string $id
* @param string|null $operationName
* @param array $context
* @return RecordView|null
* @throws Exception
*/
public function getItem(
string $resourceClass,
$id,
string $operationName = null,
array $context = []
): ?RecordView {
return $this->recordViewHandler->getRecord($id);
}
}

View file

@ -0,0 +1,96 @@
<?php
namespace App\Entity;
use ApiPlatform\Core\Annotation\ApiResource;
use ApiPlatform\Core\Annotation\ApiProperty;
use App\Resolver\RecordViewResolver;
/**
* @ApiResource(
* attributes={"security"="is_granted('ROLE_USER')"},
* itemOperations={
* "get"={"path"="/record/{id}"}
* },
* collectionOperations={},
* graphql={
* "get"={
* "item_query"=RecordViewResolver::class,
* "args"={
* "module"={"type"="String!"},
* "record"={"type"="String!"},
* }
* },
* },
* )
*/
class RecordView
{
/**
* The record ID
*
* @var string
*
* @ApiProperty(
* identifier=true,
* attributes={
* "openapi_context"={
* "type"="string",
* "description"="The record ID.",
* }
* },
*
* )
*/
protected $id;
/**
* RecordView data
*
* @var array
*
* @ApiProperty(
* attributes={
* "openapi_context"={
* "type"="array",
* "description"="The record-view data",
* },
* }
* )
*/
public $record;
/**
* @return string
*/
public function getId(): string
{
return $this->id;
}
/**
* @param string $id
*/
public function setId($id): void
{
$this->id = $id;
}
/**
* Get RecordView record
* @return array
*/
public function getRecord(): ?array
{
return $this->record;
}
/**
* Set RecordView record
* @param array $record
*/
public function setRecord(array $record): void
{
$this->record = $record;
}
}

View file

@ -0,0 +1,41 @@
<?php
namespace App\Resolver;
use ApiPlatform\Core\GraphQl\Resolver\QueryItemResolverInterface;
use App\Entity\RecordView;
use Exception;
use SuiteCRM\Core\Legacy\RecordViewHandler;
class RecordViewResolver implements QueryItemResolverInterface
{
/**
* @var RecordViewHandler
*/
protected $recordViewHandler;
/**
* RecordViewResolver constructor.
* @param RecordViewHandler $recordViewHandler
*/
public function __construct(RecordViewHandler $recordViewHandler)
{
$this->recordViewHandler = $recordViewHandler;
}
/**
* @param RecordView|null $item
*
* @param array $context
* @return RecordView
* @throws Exception
*/
public function __invoke($item, array $context): RecordView
{
$module = $context['args']['module'] ?? '';
$record = $context['args']['record'] ?? '';
return $this->recordViewHandler->getRecord($module, $record);
}
}

View file

@ -0,0 +1,18 @@
<?php
namespace App\Service;
use App\Entity\RecordView;
use Exception;
interface RecordViewProviderInterface
{
/**
* Get record
* @param string $module
* @param string $id
* @return RecordView
* @throws Exception
*/
public function getRecord(string $module, string $id): RecordView;
}