/** * @ngdoc interface * @name stateDefinitions * @description An API for generating a standard set of state definitions * generateTree - builds a full list/form tree * generateListNode - builds a single list node e.g. {name: 'projects', ...} * generateFormNode - builds a form node definition e.g. {name: 'projects.add', ...} * generateFormListDefinitions - builds form list definitions attached to a form node e.g. {name: 'projects.edit.permissions', ...} * generateLookupNodes - Attaches to a form node. Builds an abstract '*.lookup' node with field-specific 'lookup.*' children e.g. {name: 'projects.add.lookup.organizations', ...} */ export default ['$injector', '$stateExtender', '$log', function($injector, $stateExtender, $log) { return { /** * @ngdoc method * @name stateDefinitions.generateTree * @description intended for consumption by $stateProvider.state.lazyLoad in a placeholder node * @param {object} params { parent: 'stateName', // the name of the top-most node of this tree modes: ['add', 'edit'], // form modes to include in this state tree list: 'InjectableListDefinition', form: 'InjectableFormDefinition', controllers: { list: 'Injectable' || Object, add: 'Injectable' || Object, edit: 'Injectable' || Object, } * @returns {object} Promise which resolves to an object.state containing array of all state definitions in this tree * e.g. {state: [{...}, {...}, ...]} */ generateTree: function(params) { let form, list, formStates, listState, states = []; //return defer.promise; return new Promise((resolve) => { // returns array of the following states: // resource.add, resource.edit // resource.add.lookup, resource.add.lookup.* => [field in form.fields if field.type == 'lookup'] // resource.edit.lookup, resource.edit.lookup.* => [field in form.fields if field.type == 'lookup'] // resource.edit.* => [relationship in form.related] if (params.list) { list = $injector.get(params.list); listState = this.generateListNode(list, params); states.push(listState); } if (params.form) { // handle inconsistent typing of form definitions // can be either an object or fn form = $injector.get(params.form); form = typeof(form) === 'function' ? form() : form; formStates = _.map(params.modes, (mode) => this.generateFormNode(mode, form, params)); states = states.concat(_.flatten(formStates)); } $log.debug('*** Generated State Tree', states); resolve({ states: states }); }); }, /** * @ngdoc method * @name stateDefinitions.generateListNode * @description builds single list node * @params {object} list - list definition/configuration object * @params {object} params * @returns {object} a list state definition */ generateListNode: function(list, params) { let state, url = params.urls && params.urls.list ? params.urls.list : (params.url ? params.url : `/${list.name}`); // allows passed-in params to specify a custom templateUrl // otherwise, use html returned by generateList.build() to fulfill templateProvider fn function generateTemplateBlock() { if (params.templates && params.templates.list) { return params.templates.list; } else { return function(ListDefinition, generateList) { let html = generateList.build({ list: ListDefinition, mode: 'edit' }); html = generateList.wrapPanel(html); // generateList.formView() inserts a ui-view="form" inside the list view's hierarchy return generateList.insertFormView() + html; }; } } state = $stateExtender.buildDefinition({ searchPrefix: list.iterator, name: params.parent, url: url, data: params.data, ncyBreadcrumb: { label: list.title }, resolve: { Dataset: [params.list, 'QuerySet', '$stateParams', 'GetBasePath', function(list, qs, $stateParams, GetBasePath) { let path = GetBasePath(list.basePath) || GetBasePath(list.name); return qs.search(path, $stateParams[`${list.iterator}_search`]); } ], ListDefinition: () => list }, views: { '@': { // resolves to a variable property name: // 'templateUrl' OR 'templateProvider' [params.templates && params.templates.list ? 'templateUrl' : 'templateProvider']: generateTemplateBlock(), controller: params.controllers.list, } } }); // allow passed-in params to override default resolve block if (params.resolve && params.resolve.list) { state.resolve = _.merge(state.resolve, params.resolve.list); } // allow passed-in params to override default ncyBreadcrumb property if (params.ncyBreadcrumb) { state.ncyBreadcrumb = params.ncyBreadcrumb; } if (list.search) { state.params[`${list.iterator}_search`].value = _.merge(state.params[`${list.iterator}_search`].value, list.search); } return state; }, /** * @ngdoc method * @name stateDefinitions.generateFormNode * @description builds a node of form states, e.g. resource.edit.** or resource.add.** * @param {string} mode - 'add' || 'edit' - the form mode of this node * @param {object} form - form definition/configuration object * @returns {array} Array of state definitions required by form mode [{...}, {...}, ...] */ generateFormNode: function(mode, form, params) { let formNode, states = [], url; switch (mode) { case 'add': url = params.urls && params.urls.add ? params.urls.add : (params.url ? params.url : '/add'); // breadcrumbName necessary for resources that are more than one word like // job templates. form.name can't have spaces in it or it busts form gen formNode = $stateExtender.buildDefinition({ name: params.name || `${params.parent}.add`, url: url, ncyBreadcrumb: { [params.parent ? 'parent' : null]: `${params.parent}`, label: `CREATE ${form.breadcrumbName || form.name}` }, views: { 'form': { templateProvider: function(FormDefinition, GenerateForm) { let form = typeof(FormDefinition) === 'function' ? FormDefinition() : FormDefinition; return GenerateForm.buildHTML(form, { mode: 'add', related: false }); }, controller: params.controllers.add } }, resolve: { 'FormDefinition': [params.form, function(definition) { return definition; }] } }); if (params.resolve && params.resolve.add) { formNode.resolve = _.merge(formNode.resolve, params.resolve.add); } break; case 'edit': url = params.urls && params.urls.edit ? params.urls.edit : (params.url ? params.url : `/:${form.name}_id`); formNode = $stateExtender.buildDefinition({ name: params.name || `${params.parent}.edit`, url: url, ncyBreadcrumb: { [params.parent ? 'parent' : null]: `${params.parent}`, label: '{{parentObject.name || name}}' }, views: { 'form': { templateProvider: function(FormDefinition, GenerateForm) { let form = typeof(FormDefinition) === 'function' ? FormDefinition() : FormDefinition; return GenerateForm.buildHTML(form, { mode: 'edit' }); }, controller: params.controllers.edit } }, resolve: { FormDefinition: [params.form, function(definition) { return definition; }], resourceData: ['FormDefinition', 'Rest', '$stateParams', 'GetBasePath', function(FormDefinition, Rest, $stateParams, GetBasePath) { let form, path; form = typeof(FormDefinition) === 'function' ? FormDefinition() : FormDefinition; if (GetBasePath(form.basePath) === undefined && GetBasePath(form.stateTree) === undefined ){ throw { name: 'NotImplementedError', message: `${form.name} form definition is missing basePath or stateTree property.` }; } else{ path = (GetBasePath(form.basePath) || GetBasePath(form.stateTree) || form.basePath) + $stateParams[`${form.name}_id`]; } Rest.setUrl(path); return Rest.get(); } ] } }); if (params.resolve && params.resolve.edit) { formNode.resolve = _.merge(formNode.resolve, params.resolve.edit); } break; } states.push(formNode); states = states.concat(this.generateLookupNodes(form, formNode)).concat(this.generateFormListDefinitions(form, formNode)); return states; }, /** * @ngdoc method * @name stateDefinitions.generateFormListDefinitions * @description builds state definitions for a form's related lists, like notifications/permissions * @param {object} form - form definition/configuration object * @params {object} formStateDefinition - the parent form node * @returns {array} Array of state definitions [{...}, {...}, ...] */ generateFormListDefinitions: function(form, formStateDefinition) { function buildRbacUserTeamDirective(){ let states = []; states.push($stateExtender.buildDefinition({ name: `${formStateDefinition.name}.permissions.add`, squashSearchUrl: true, url: '/add-permissions', params: { project_search: { value: {order_by: 'name', page_size: '5', role_level: 'admin_role'}, dynamic: true }, job_template_search: { value: {order_by: 'name', page_size: '5', role_level: 'admin_role'}, dynamic: true }, workflow_template_search: { value: {order_by: 'name', page_size: '5', role_level: 'admin_role'}, dynamic: true }, inventory_search: { value: {order_by: 'name', page_size: '5', role_level: 'admin_role'}, dynamic: true }, credential_search: { value: {order_by: 'name', page_size: '5', role_level: 'admin_role'}, dynamic: true } }, views: { [`modal@${formStateDefinition.name}`]: { template: `` } }, resolve: { jobTemplatesDataset: ['QuerySet', '$stateParams', 'GetBasePath', function(qs, $stateParams, GetBasePath) { let path = GetBasePath('job_templates'); return qs.search(path, $stateParams.job_template_search); } ], workflowTemplatesDataset: ['QuerySet', '$stateParams', 'GetBasePath', function(qs, $stateParams, GetBasePath) { let path = GetBasePath('workflow_job_templates'); return qs.search(path, $stateParams.workflow_template_search); } ], projectsDataset: ['ProjectList', 'QuerySet', '$stateParams', 'GetBasePath', function(list, qs, $stateParams, GetBasePath) { let path = GetBasePath(list.basePath) || GetBasePath(list.name); return qs.search(path, $stateParams[`${list.iterator}_search`]); } ], inventoriesDataset: ['InventoryList', 'QuerySet', '$stateParams', 'GetBasePath', function(list, qs, $stateParams, GetBasePath) { let path = GetBasePath(list.basePath) || GetBasePath(list.name); return qs.search(path, $stateParams[`${list.iterator}_search`]); } ], credentialsDataset: ['CredentialList', 'QuerySet', '$stateParams', 'GetBasePath', function(list, qs, $stateParams, GetBasePath) { let path = GetBasePath(list.basePath) || GetBasePath(list.name); return qs.search(path, $stateParams[`${list.iterator}_search`]); } ], }, onExit: function($state) { if ($state.transition) { $('#add-permissions-modal').modal('hide'); $('.modal-backdrop').remove(); $('body').removeClass('modal-open'); } }, })); return states; } function buildRbacResourceDirective() { let states = []; states.push($stateExtender.buildDefinition({ name: `${formStateDefinition.name}.permissions.add`, squashSearchUrl: true, url: '/add-permissions', params: { user_search: { value: { order_by: 'username', page_size: '5' }, dynamic: true, }, team_search: { value: { order_by: 'name', page_size: '5' }, dynamic: true } }, views: { [`modal@${formStateDefinition.name}`]: { template: `` } }, resolve: { usersDataset: ['addPermissionsUsersList', 'QuerySet', '$stateParams', 'GetBasePath', function(list, qs, $stateParams, GetBasePath) { let path = GetBasePath(list.basePath) || GetBasePath(list.name); return qs.search(path, $stateParams.user_search); } ], teamsDataset: ['addPermissionsTeamsList', 'QuerySet', '$stateParams', 'GetBasePath', function(list, qs, $stateParams, GetBasePath) { let path = GetBasePath(list.basePath) || GetBasePath(list.name); return qs.search(path, $stateParams.team_search); } ] }, onExit: function($state) { if ($state.transition) { $('#add-permissions-modal').modal('hide'); $('.modal-backdrop').remove(); $('body').removeClass('modal-open'); } }, })); return states; } function buildListNodes(field) { let states = []; states.push(buildListDefinition(field)); if (field.iterator === 'permission' && field.actions && field.actions.add) { if (form.name === 'user' || form.name === 'team'){ states.push(buildRbacUserTeamDirective()); } else { states.push(buildRbacResourceDirective()); } states = _.flatten(states); } return states; } function buildListDefinition(field) { let state, list = field.include ? $injector.get(field.include) : field; state = $stateExtender.buildDefinition({ searchPrefix: `${list.iterator}`, name: `${formStateDefinition.name}.${list.iterator}s`, url: `/${list.iterator}s`, ncyBreadcrumb: { parent: `${formStateDefinition.name}`, label: `${field.iterator}s` }, params: { [list.iterator + '_search']: { value: { order_by: field.order_by ? field.order_by : 'name' } }, }, views: { 'related': { templateProvider: function(FormDefinition, GenerateForm) { let html = GenerateForm.buildCollection({ mode: 'edit', related: `${list.iterator}s`, form: typeof(FormDefinition) === 'function' ? FormDefinition() : FormDefinition }); return html; }, controller: ['$scope', 'ListDefinition', 'Dataset', function($scope, list, Dataset) { $scope.list = list; $scope[`${list.iterator}_dataset`] = Dataset.data; $scope[`${list.iterator}s`] = $scope[`${list.iterator}_dataset`].results; } ] } }, resolve: { ListDefinition: () => { return list; }, Dataset: ['ListDefinition', 'QuerySet', '$stateParams', 'GetBasePath', '$interpolate', '$rootScope', (list, qs, $stateParams, GetBasePath, $interpolate, $rootScope) => { // allow related list definitions to use interpolated $rootScope / $stateParams in basePath field let path, interpolator; if (GetBasePath(list.basePath)) { path = GetBasePath(list.basePath); } else { interpolator = $interpolate(list.basePath); path = interpolator({ $rootScope: $rootScope, $stateParams: $stateParams }); } return qs.search(path, $stateParams[`${list.iterator}_search`]); } ] } }); // appy any default search parameters in form definition if (field.search) { state.params[`${field.iterator}_search`].value = _.merge(state.params[`${field.iterator}_search`].value, field.search); } return state; } return _(form.related).map(buildListNodes).flatten().value(); }, /** * @ngdoc method * @name stateDefinitions.generateLookupNode * @description builds a node of child states for each lookup field in a form * @param {object} form - form definition/configuration object * @params {object} formStateDefinition - the parent form node * @returns {array} Array of state definitions [{...}, {...}, ...] */ generateLookupNodes: function(form, formStateDefinition) { function buildFieldDefinition(field) { let state = $stateExtender.buildDefinition({ searchPrefix: field.sourceModel, //squashSearchUrl: true, @issue enable name: `${formStateDefinition.name}.${field.sourceModel}`, url: `/${field.sourceModel}?selected`, // a lookup field's basePath takes precedence over generic list definition's basePath, if supplied data: { basePath: field.basePath || null, formChildState: true }, params: { [field.sourceModel + '_search']: { value: { page_size: '5' } } }, ncyBreadcrumb: { skip: true }, views: { 'modal': { templateProvider: function(ListDefinition, generateList) { let list_html = generateList.build({ mode: 'lookup', list: ListDefinition, input_type: 'radio' }); return `${list_html}`; } } }, resolve: { ListDefinition: [field.list, function(list) { list.iterator = field.sourceModel; return list; }], Dataset: ['ListDefinition', 'QuerySet', '$stateParams', 'GetBasePath', '$interpolate', '$rootScope', '$state', (list, qs, $stateParams, GetBasePath, $interpolate, $rootScope, $state) => { // allow lookup field definitions to use interpolated $stateParams / $rootScope in basePath field // the basePath on a form's lookup field will take precedence over the general model list's basepath let path, interpolator; if ($state.transition._targetState._definition.data && GetBasePath($state.transition._targetState._definition.data.basePath)) { path = GetBasePath($state.transition._targetState._definition.data.basePath); } else if ($state.transition._targetState._definition.data && $state.transition._targetState._definition.data.basePath) { interpolator = $interpolate($state.transition._targetState._definition.data.basePath); path = interpolator({ $rootScope: $rootScope, $stateParams: $stateParams }); } else if (GetBasePath(list.basePath)) { path = GetBasePath(list.basePath); } else { interpolator = $interpolate(list.basePath); path = interpolator({ $rootScope: $rootScope, $stateParams: $stateParams }); } return qs.search(path, $stateParams[`${list.iterator}_search`]); } ] }, onExit: function($state) { if ($state.transition) { $('#form-modal').modal('hide'); $('.modal-backdrop').remove(); $('body').removeClass('modal-open'); } }, }); if (field.search) { state.params[`${field.sourceModel}_search`].value = _.merge(state.params[`${field.sourceModel}_search`].value, field.search); } return state; } return _(form.fields).filter({ type: 'lookup' }).map(buildFieldDefinition).value(); } }; }];