[CORD-1338] Inline navigation for related models
Change-Id: I58ff4a4675d1ce1140fe162f1f8360f2dc9a6527
diff --git a/src/app/views/crud/crud.html b/src/app/views/crud/crud.html
index adb0500..b162cc5 100644
--- a/src/app/views/crud/crud.html
+++ b/src/app/views/crud/crud.html
@@ -19,29 +19,26 @@
<hr>
</div>
</div>
-<div class="row" ng-show="vm.related.length > 0 && vm.model.id">
- <div class="view-header">
- <div class="col-lg-4">
- <h4>Related Items: </h4>
- </div>
- <div class="col-lg-8 text-right">
- <div class="btn-group">
- <a ng-if="vm.list" ng-repeat="r in vm.related" href="#/core/{{r.model.toLowerCase()}}s/" class="btn btn-default">
- {{r.model}}
- </a>
- <a ng-if="!vm.list && vm.getRelatedItem(r, vm.model)" ng-repeat="r in vm.related" href="#/core/{{r.model.toLowerCase()}}s/{{vm.getRelatedItem(r, vm.model)}}" class="btn btn-default">
- {{r.model}}
- </a>
- </div>
- </div>
- <hr>
- </div>
-</div>
<div ng-if="vm.list">
<xos-table config="vm.tableCfg" data="vm.tableData"></xos-table>
</div>
<div ng-if="!vm.list">
- <!--<pre>{{vm.model | json}}</pre>-->
- <xos-form ng-model="vm.model" config="vm.formCfg"></xos-form>
+ <uib-tabset active="active">
+ <uib-tab heading="{{vm.data.model}}">
+ <div class="panel-body">
+ <xos-form ng-model="vm.model" config="vm.formCfg"></xos-form>
+ </div>
+ </uib-tab>
+ <uib-tab ng-if="vm.getRelatedItemId(r, vm.model)" ng-repeat="r in vm.related.manytoone" heading="{{r.model}} {{vm.getHumanReadableOnField(r)}}">
+ <div class="panel-body">
+ <xos-form ng-model="vm.relatedModels.manytoone[r.model][r.on_field].model" config="vm.relatedModels.manytoone[r.model][r.on_field].formConfig"></xos-form>
+ </div>
+ </uib-tab>
+ <uib-tab classes="{{vm.relatedModels.onetomany[r.model][r.on_field].class}}" ng-if="vm.relatedModels.onetomany[r.model]" ng-repeat="r in vm.related.onetomany" heading="{{r.model}} {{vm.getHumanReadableOnField(r)}}">
+ <div class="panel-body">
+ <xos-table config="vm.relatedModels.onetomany[r.model][r.on_field].tableConfig" data="vm.relatedModels.onetomany[r.model][r.on_field].model"></xos-table>
+ </div>
+ </uib-tab>
+ </uib-tabset>
</div>
diff --git a/src/app/views/crud/crud.relations.service.spec.ts b/src/app/views/crud/crud.relations.service.spec.ts
new file mode 100644
index 0000000..8c0f2f9
--- /dev/null
+++ b/src/app/views/crud/crud.relations.service.spec.ts
@@ -0,0 +1,196 @@
+import {
+ IXosCrudRelationService, XosCrudRelationService, IXosCrudRelationFormTabData,
+ IXosCrudRelationTableTabData
+} from './crud.relations.service';
+import {BehaviorSubject} from 'rxjs';
+import {ConfigHelpers} from '../../core/services/helpers/config.helpers';
+
+const XosModelStoreMock = {
+ get: null,
+ query: null
+};
+
+const XosModelDiscovererMock = {
+ get: null
+};
+
+let service, scope;
+
+describe('The XosCrudRelation service', () => {
+ beforeEach(() => {
+ angular
+ .module('test', ['ui.router', 'toastr'])
+ .service('XosCrudRelation', XosCrudRelationService)
+ .value('XosModelStore', XosModelStoreMock)
+ .value('XosModelDiscoverer', XosModelDiscovererMock)
+ .service('ConfigHelpers', ConfigHelpers);
+
+ angular.mock.module('test');
+ });
+
+ beforeEach(angular.mock.inject((XosCrudRelation: IXosCrudRelationService, $rootScope: ng.IScope) => {
+ service = XosCrudRelation;
+ scope = $rootScope;
+ }));
+
+ it('should have the correct methods', () => {
+ expect(service.getModel).toBeDefined();
+ expect(service.getModels).toBeDefined();
+ expect(service.existsRelatedItem).toBeDefined();
+ });
+
+ describe('the existsRelatedItem method', () => {
+ it('should return true if the we have a reference to the related model', () => {
+ const relation = {
+ model: 'Test',
+ type: 'manytoone',
+ on_field: 'test'
+ };
+ const item = {test_id: 5};
+
+ const res = service.existsRelatedItem(relation, item);
+ expect(res).toBeTruthy();
+ });
+ it('should return false if the we don\'t have a reference to the related model', () => {
+ const relation = {
+ model: 'Test',
+ type: 'manytoone',
+ on_field: 'test'
+ };
+ const item = {foo: 5};
+
+ const res = service.existsRelatedItem(relation, item);
+ expect(res).toBeFalsy();
+ });
+ });
+
+ describe('the getHumanReadableOnField method', () => {
+ it('should return a human readable version of the on_field param', () => {
+ const relation = {
+ model: 'Test',
+ type: 'onetomany',
+ on_field: 'relate_to_test'
+ };
+
+ const res = service.getHumanReadableOnField(relation, 'Instance');
+ expect(res).toEqual('[Relate to test]');
+ });
+
+ it('should return am empty string if the on_field param equal the model param', () => {
+ const relation = {
+ model: 'Test',
+ type: 'onetomany',
+ on_field: 'test'
+ };
+
+ const res = service.getHumanReadableOnField(relation, 'Instance');
+ expect(res).toEqual('');
+ });
+
+ it('should return am empty string if the type on_field equal the base model', () => {
+ const relation = {
+ model: 'Test',
+ type: 'manytoone',
+ on_field: 'instance'
+ };
+
+ const res = service.getHumanReadableOnField(relation, 'Instance');
+ expect(res).toEqual('');
+ });
+ });
+
+ describe('the getModel method', () => {
+ it('should return the tab config for a single object', (done) => {
+ const relation = {
+ model: 'Test',
+ type: 'manytoone',
+ on_field: 'test'
+ };
+
+ const resModel = {foo: 'bar'};
+ const resFormCfg = {form: 'config'};
+
+ spyOn(XosModelStoreMock, 'get').and.callFake(() => {
+ const subject = new BehaviorSubject({});
+ subject.next(resModel);
+ return subject.asObservable();
+ });
+ spyOn(XosModelDiscovererMock, 'get').and.returnValue({formCfg: resFormCfg});
+
+ service.getModel(relation, '5')
+ .then((res: IXosCrudRelationFormTabData) => {
+ expect(res.model).toEqual(resModel);
+ expect(res.class).toEqual('full');
+ expect(res.formConfig).toEqual(resFormCfg);
+ done();
+ });
+ scope.$apply();
+ });
+ });
+
+ describe('the getModels method', () => {
+ it('should return one related model', (done) => {
+ const relation = {
+ model: 'Test',
+ type: 'onetomany',
+ on_field: 'test'
+ };
+
+ const resModels = [
+ {test_id: 5},
+ {test_id: 25}
+ ];
+ const resTableCfg = {table: 'config'};
+
+ spyOn(XosModelStoreMock, 'query').and.callFake(() => {
+ const subject = new BehaviorSubject(resModels);
+ return subject.asObservable();
+ });
+ spyOn(XosModelDiscovererMock, 'get').and.returnValue({tableCfg: resTableCfg});
+
+ service.getModels(relation, 5)
+ .then((res: IXosCrudRelationTableTabData) => {
+ expect(res.model.length).toEqual(1);
+ expect(res.class).toEqual('full');
+ expect(res.tableConfig).toEqual({
+ table: 'config',
+ filter: null
+ });
+ done();
+ });
+ scope.$apply();
+ });
+
+ it('should not return related models', (done) => {
+ const relation = {
+ model: 'Test',
+ type: 'onetomany',
+ on_field: 'test'
+ };
+
+ const resModels = [
+ {test_id: 15},
+ {test_id: 25}
+ ];
+ const resTableCfg = {table: 'config'};
+
+ spyOn(XosModelStoreMock, 'query').and.callFake(() => {
+ const subject = new BehaviorSubject(resModels);
+ return subject.asObservable();
+ });
+ spyOn(XosModelDiscovererMock, 'get').and.returnValue({tableCfg: resTableCfg});
+
+ service.getModels(relation, 5)
+ .then((res: IXosCrudRelationTableTabData) => {
+ expect(res.model.length).toEqual(0);
+ expect(res.class).toEqual('empty');
+ expect(res.tableConfig).toEqual({
+ table: 'config',
+ filter: null
+ });
+ done();
+ });
+ scope.$apply();
+ });
+ });
+});
diff --git a/src/app/views/crud/crud.relations.service.ts b/src/app/views/crud/crud.relations.service.ts
new file mode 100644
index 0000000..292c6f5
--- /dev/null
+++ b/src/app/views/crud/crud.relations.service.ts
@@ -0,0 +1,109 @@
+import {IXosModelRelation} from './crud';
+import {IXosModelStoreService} from '../../datasources/stores/model.store';
+import {IXosModelDiscovererService} from '../../datasources/helpers/model-discoverer.service';
+import * as _ from 'lodash';
+import {IXosFormCfg} from '../../core/form/form';
+import {IXosTableCfg} from '../../core/table/table';
+import {IXosConfigHelpersService} from '../../core/services/helpers/config.helpers';
+
+interface IXosCrudRelationBaseTabData {
+ model: any;
+ class?: 'full' | 'empty';
+}
+
+export interface IXosCrudRelationFormTabData extends IXosCrudRelationBaseTabData {
+ formConfig: IXosFormCfg;
+}
+
+export interface IXosCrudRelationTableTabData extends IXosCrudRelationBaseTabData {
+ tableConfig: IXosTableCfg;
+}
+
+export interface IXosCrudRelationService {
+ getModel(r: IXosModelRelation, id: string | number): Promise<IXosCrudRelationFormTabData>;
+ getModels(r: IXosModelRelation, source_id: string | number): Promise<IXosCrudRelationTableTabData>;
+ existsRelatedItem(r: IXosModelRelation, item: any): boolean;
+ getHumanReadableOnField(r: IXosModelRelation, baseModel: string): string;
+}
+
+export class XosCrudRelationService implements IXosCrudRelationService {
+
+ static $inject = [
+ '$log',
+ '$q',
+ 'XosModelStore',
+ 'XosModelDiscoverer',
+ 'ConfigHelpers'
+ ];
+
+ constructor (
+ private $log: ng.ILogService,
+ private $q: ng.IQService,
+ private XosModelStore: IXosModelStoreService,
+ private XosModelDiscovererService: IXosModelDiscovererService,
+ private ConfigHelpers: IXosConfigHelpersService
+ ) {}
+
+ public getModel (r: IXosModelRelation, id: string | number): Promise<IXosCrudRelationFormTabData> {
+ const d = this.$q.defer();
+ this.XosModelStore.get(r.model, id)
+ .subscribe(
+ item => {
+ this.$log.debug(`[XosCrud] Loaded manytoone relation with ${r.model} on ${r.on_field}`, item);
+
+ const data: IXosCrudRelationFormTabData = {
+ model: item,
+ formConfig: this.XosModelDiscovererService.get(r.model).formCfg,
+ class: angular ? 'full' : 'empty'
+ };
+
+ d.resolve(data);
+ },
+ err => d.reject
+ );
+ return d.promise;
+ };
+
+ public getModels(r: IXosModelRelation, source_id: string | number): Promise<IXosCrudRelationTableTabData> {
+ const d = this.$q.defer();
+
+ this.XosModelStore.query(r.model)
+ .subscribe(
+ items => {
+ this.$log.debug(`[XosCrud] Loaded onetomany relation with ${r.model} on ${r.on_field}`, items);
+ // building the filter parameters
+ const match = {};
+ match[`${r.on_field.toLowerCase()}_id`] = source_id;
+ const filtered = _.filter(items, match);
+ // removing search bar from table
+ const tableCfg = this.XosModelDiscovererService.get(r.model).tableCfg;
+ tableCfg.filter = null;
+
+ const data: IXosCrudRelationTableTabData = {
+ model: filtered,
+ tableConfig: tableCfg,
+ class: filtered.length > 0 ? 'full' : 'empty'
+ };
+
+ d.resolve(data);
+ },
+ err => d.reject
+ );
+
+ return d.promise;
+ }
+
+ public existsRelatedItem(r: IXosModelRelation, item: any): boolean {
+ return item && angular.isDefined(item[`${r.on_field.toLowerCase()}_id`]);
+ }
+
+ public getHumanReadableOnField(r: IXosModelRelation, baseModel: string): string {
+ if (r.on_field.toLowerCase() === baseModel.toLowerCase()) {
+ return '';
+ }
+ if (r.model.toLowerCase() === r.on_field.toLowerCase()) {
+ return '';
+ }
+ return `[${this.ConfigHelpers.toLabel(r.on_field, false)}]`;
+ }
+}
diff --git a/src/app/views/crud/crud.scss b/src/app/views/crud/crud.scss
new file mode 100644
index 0000000..ac6fd81
--- /dev/null
+++ b/src/app/views/crud/crud.scss
@@ -0,0 +1,7 @@
+@import './../../style/vars.scss';
+li.uib-tab {
+ &.empty > a {
+ color: $color-placeholder;
+ font-style: italic;
+ }
+}
\ No newline at end of file
diff --git a/src/app/views/crud/crud.ts b/src/app/views/crud/crud.ts
index 90d3342..fc422f3 100644
--- a/src/app/views/crud/crud.ts
+++ b/src/app/views/crud/crud.ts
@@ -2,35 +2,34 @@
import {IXosModelStoreService} from '../../datasources/stores/model.store';
import {IXosConfigHelpersService} from '../../core/services/helpers/config.helpers';
import * as _ from 'lodash';
-import {IXosFormCfg} from '../../core/form/form';
import {IXosResourceService} from '../../datasources/rest/model.rest';
import {IStoreHelpersService} from '../../datasources/helpers/store.helpers';
import {IXosModelDiscovererService} from '../../datasources/helpers/model-discoverer.service';
-
-export interface IXosCrudData {
- model: string;
- related: IXosModelRelation[];
- xosTableCfg: IXosTableCfg;
- xosFormCfg: IXosFormCfg;
-}
+import './crud.scss';
+import {IXosCrudRelationService} from './crud.relations.service';
export interface IXosModelRelation {
model: string;
type: string;
+ on_field: string;
}
class CrudController {
static $inject = [
'$scope',
+ '$log',
'$state',
'$stateParams',
'XosModelStore',
'ConfigHelpers',
'ModelRest',
'StoreHelpers',
- 'XosModelDiscoverer'
+ 'XosModelDiscoverer',
+ 'XosCrudRelation'
];
+ // bindings
+
public data: {model: string};
public tableCfg: IXosTableCfg;
public formCfg: any;
@@ -39,18 +38,28 @@
public title: string;
public tableData: any[];
public model: any;
- public related: string[];
+ public related: {manytoone: IXosModelRelation[], onetomany: IXosModelRelation[]} = {
+ manytoone: [],
+ onetomany: []
+ };
+ public relatedModels: {manytoone: any, onetomany: any} = {
+ manytoone: {},
+ onetomany: {}
+ };
constructor(
private $scope: angular.IScope,
+ private $log: angular.ILogService,
private $state: angular.ui.IStateService,
private $stateParams: ng.ui.IStateParamsService,
private store: IXosModelStoreService,
private ConfigHelpers: IXosConfigHelpersService,
private ModelRest: IXosResourceService,
private StoreHelpers: IStoreHelpersService,
- private XosModelDiscovererService: IXosModelDiscovererService
+ private XosModelDiscovererService: IXosModelDiscovererService,
+ private XosCrudRelation: IXosCrudRelationService
) {
+
this.data = this.$state.current.data;
this.model = this.XosModelDiscovererService.get(this.data.model);
this.title = this.ConfigHelpers.pluralize(this.data.model);
@@ -60,8 +69,7 @@
// TODO get the proper URL from model discoverer
this.baseUrl = '#/' + this.model.clientUrl.replace(':id?', '');
-
- this.related = $state.current.data.related;
+ this.$log.debug('[XosCrud]', $state.current.data);
this.tableCfg = this.model.tableCfg;
this.formCfg = this.model.formCfg;
@@ -76,7 +84,10 @@
// if it is a detail page for an existing model
if ($stateParams['id'] && $stateParams['id'] !== 'add') {
+ this.related.onetomany = _.filter($state.current.data.relations, {type: 'onetomany'});
+ this.related.manytoone = _.filter($state.current.data.relations, {type: 'manytoone'});
this.model = _.find(this.tableData, {id: parseInt($stateParams['id'], 10)});
+ this.getRelatedModels(this.related, this.model);
}
});
}
@@ -96,11 +107,59 @@
}
}
- public getRelatedItem(relation: IXosModelRelation, item: any): number {
- if (item && angular.isDefined(item[`${relation.model.toLowerCase()}_id`])) {
- return item[`${relation.model.toLowerCase()}_id`];
- }
- return 0;
+
+ public getRelatedItemId(relation: IXosModelRelation, item: any): boolean {
+ return this.XosCrudRelation.existsRelatedItem(relation, item);
+ }
+
+ public getHumanReadableOnField(r: IXosModelRelation) {
+ return this.XosCrudRelation.getHumanReadableOnField(r, this.data.model);
+ }
+
+ public getRelatedModels(relations: {manytoone: IXosModelRelation[], onetomany: IXosModelRelation[]}, item: any) {
+ this.$log.info(`[XosCrud] Managing relation for ${this.data.model}:`, relations);
+
+ // loading many to one relations (you'll get a model)
+ _.forEach(relations.manytoone, (r: IXosModelRelation) => {
+ if (!item || !item[`${r.on_field.toLowerCase()}_id`]) {
+ return;
+ }
+
+ this.$log.debug(`[XosCrud] Loading manytoone relation with ${r.model} on ${r.on_field}`);
+
+ if (!angular.isDefined(this.relatedModels.manytoone[r.model])) {
+ this.relatedModels.manytoone[r.model] = {};
+ }
+
+ this.XosCrudRelation.getModel(r, item[`${r.on_field.toLowerCase()}_id`])
+ .then(res => {
+ this.relatedModels.manytoone[r.model][r.on_field] = res;
+ })
+ .catch(err => {
+ this.$log.error(`[XosCrud] Error loading manytoone relation with ${r.model} on ${r.on_field}`, err);
+ });
+ });
+
+ // loading onetomany relations (you'll get a list of models)
+ _.forEach(relations.onetomany, (r: IXosModelRelation) => {
+ if (!item) {
+ return;
+ }
+
+ this.$log.debug(`[XosCrud] Loading onetomany relation with ${r.model} on ${r.on_field}`);
+
+ if (!angular.isDefined(this.relatedModels.onetomany[r.model])) {
+ this.relatedModels.onetomany[r.model] = {};
+ }
+
+ this.XosCrudRelation.getModels(r, item.id)
+ .then(res => {
+ this.relatedModels.onetomany[r.model][r.on_field] = res;
+ })
+ .catch(err => {
+ this.$log.error(`[XosCrud] Error loading onetomany relation with ${r.model} on ${r.on_field}`, err);
+ });
+ });
}
}
diff --git a/src/app/views/index.ts b/src/app/views/index.ts
index ee87ef9..b296f9b 100644
--- a/src/app/views/index.ts
+++ b/src/app/views/index.ts
@@ -1,10 +1,12 @@
import {xosCore} from '../core/index';
import {xosCrud} from './crud/crud';
import {xosDashboard} from './dashboard/dashboard';
+import {XosCrudRelationService} from './crud/crud.relations.service';
export const xosViews = 'xosViews';
angular
.module('xosViews', [xosCore])
+ .service('XosCrudRelation', XosCrudRelationService)
.component('xosCrud', xosCrud)
.component('xosDashboard', xosDashboard);