diff --git a/config/bootstrap.php b/config/bootstrap.php index 59fed08c3..03fcb0a64 100644 --- a/config/bootstrap.php +++ b/config/bootstrap.php @@ -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']; diff --git a/core/app/src/app/app-routing.module.ts b/core/app/src/app/app-routing.module.ts index dc5b9106b..e7a44deac 100644 --- a/core/app/src/app/app-routing.module.ts +++ b/core/app/src/app/app-routing.module.ts @@ -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, diff --git a/core/app/src/app/app.module.ts b/core/app/src/app/app.module.ts index dbc81c8e8..2136f8edf 100644 --- a/core/app/src/app/app.module.ts +++ b/core/app/src/app/app.module.ts @@ -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, diff --git a/core/app/src/services/metadata/base-record.resolver.ts b/core/app/src/services/metadata/base-record.resolver.ts new file mode 100644 index 000000000..940c2e4c4 --- /dev/null +++ b/core/app/src/services/metadata/base-record.resolver.ts @@ -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 { + return forkJoin({ + base: super.resolve(route), + metadata: this.metadataStore.load(route.params.module, this.metadataStore.getMetadataTypes()), + }); + } +} diff --git a/core/app/src/services/record-view/record-view-guard.service.ts b/core/app/src/services/record-view/record-view-guard.service.ts new file mode 100644 index 000000000..50eeedf06 --- /dev/null +++ b/core/app/src/services/record-view/record-view-guard.service.ts @@ -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 { + 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)), + ); + } +} diff --git a/core/app/src/store/record-view/api.record.get.ts b/core/app/src/store/record-view/api.record.get.ts new file mode 100644 index 000000000..54d802cff --- /dev/null +++ b/core/app/src/store/record-view/api.record.get.ts @@ -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> + */ + public fetch( + module: string, + record: string, + metadata: { fields: string[] } + ): Observable> { + 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); + } +} diff --git a/core/app/src/store/record-view/record-view.store.ts b/core/app/src/store/record-view/record-view.store.ts new file mode 100644 index 000000000..6f55d8055 --- /dev/null +++ b/core/app/src/store/record-view/record-view.store.ts @@ -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; + vm: RecordViewModel; + + clear(): void { + } + + /** + * Clean destroy + */ + public destroy(): void { + this.clear(); + } +} diff --git a/core/app/views/record/record.component.html b/core/app/views/record/record.component.html new file mode 100644 index 000000000..e2b26becd --- /dev/null +++ b/core/app/views/record/record.component.html @@ -0,0 +1,4 @@ + +
+
+ diff --git a/core/app/views/record/record.component.spec.ts b/core/app/views/record/record.component.spec.ts new file mode 100644 index 000000000..e22a6f0dd --- /dev/null +++ b/core/app/views/record/record.component.spec.ts @@ -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: '' +}) +class RecordTestHostComponent { +} + +describe('RecordComponent', () => { + let testHostComponent: RecordTestHostComponent; + let testHostFixture: ComponentFixture; + + 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(); + }); +}); diff --git a/core/app/views/record/record.component.ts b/core/app/views/record/record.component.ts new file mode 100644 index 000000000..cf2b59075 --- /dev/null +++ b/core/app/views/record/record.component.ts @@ -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 = 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(); + } +} diff --git a/core/app/views/record/record.module.ts b/core/app/views/record/record.module.ts new file mode 100644 index 000000000..fdc4479ca --- /dev/null +++ b/core/app/views/record/record.module.ts @@ -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 { +} diff --git a/core/app/views/record/record.php b/core/app/views/record/record.php deleted file mode 100644 index ceee4818a..000000000 --- a/core/app/views/record/record.php +++ /dev/null @@ -1,3 +0,0 @@ -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; + } +} diff --git a/core/src/DataProvider/RecordViewItemDataProvider.php b/core/src/DataProvider/RecordViewItemDataProvider.php new file mode 100644 index 000000000..14341581b --- /dev/null +++ b/core/src/DataProvider/RecordViewItemDataProvider.php @@ -0,0 +1,59 @@ +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); + } +} diff --git a/core/src/Entity/RecordView.php b/core/src/Entity/RecordView.php new file mode 100644 index 000000000..37cf995a9 --- /dev/null +++ b/core/src/Entity/RecordView.php @@ -0,0 +1,96 @@ +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; + } +} diff --git a/core/src/Resolver/RecordViewResolver.php b/core/src/Resolver/RecordViewResolver.php new file mode 100644 index 000000000..5a52742ce --- /dev/null +++ b/core/src/Resolver/RecordViewResolver.php @@ -0,0 +1,41 @@ +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); + } +} diff --git a/core/src/Service/RecordViewProviderInterface.php b/core/src/Service/RecordViewProviderInterface.php new file mode 100644 index 000000000..3eaf6af85 --- /dev/null +++ b/core/src/Service/RecordViewProviderInterface.php @@ -0,0 +1,18 @@ +