diff --git a/awx/api/generics.py b/awx/api/generics.py index 06f80887ae..be58b057d8 100644 --- a/awx/api/generics.py +++ b/awx/api/generics.py @@ -489,9 +489,12 @@ class SubListAPIView(ParentMixin, ListAPIView): parent = self.get_parent_object() self.check_parent_access(parent) qs = self.request.user.get_queryset(self.model).distinct() - sublist_qs = getattrd(parent, self.relationship).distinct() + sublist_qs = self.get_sublist_queryset(parent) return qs & sublist_qs + def get_sublist_queryset(self, parent): + return getattrd(parent, self.relationship).distinct() + class DestroyAPIView(generics.DestroyAPIView): diff --git a/awx/api/serializers.py b/awx/api/serializers.py index c7a322489b..8100a78114 100644 --- a/awx/api/serializers.py +++ b/awx/api/serializers.py @@ -1261,6 +1261,7 @@ class OrganizationSerializer(BaseSerializer): notification_templates_started = self.reverse('api:organization_notification_templates_started_list', kwargs={'pk': obj.pk}), notification_templates_success = self.reverse('api:organization_notification_templates_success_list', kwargs={'pk': obj.pk}), notification_templates_error = self.reverse('api:organization_notification_templates_error_list', kwargs={'pk': obj.pk}), + notification_templates_approvals = self.reverse('api:organization_notification_templates_approvals_list', kwargs={'pk': obj.pk}), object_roles = self.reverse('api:organization_object_roles_list', kwargs={'pk': obj.pk}), access_list = self.reverse('api:organization_access_list', kwargs={'pk': obj.pk}), instance_groups = self.reverse('api:organization_instance_groups_list', kwargs={'pk': obj.pk}), @@ -3335,6 +3336,7 @@ class WorkflowJobTemplateSerializer(JobTemplateMixin, LabelsListMixin, UnifiedJo notification_templates_started = self.reverse('api:workflow_job_template_notification_templates_started_list', kwargs={'pk': obj.pk}), notification_templates_success = self.reverse('api:workflow_job_template_notification_templates_success_list', kwargs={'pk': obj.pk}), notification_templates_error = self.reverse('api:workflow_job_template_notification_templates_error_list', kwargs={'pk': obj.pk}), + notification_templates_approvals = self.reverse('api:workflow_job_template_notification_templates_approvals_list', kwargs={'pk': obj.pk}), access_list = self.reverse('api:workflow_job_template_access_list', kwargs={'pk': obj.pk}), object_roles = self.reverse('api:workflow_job_template_object_roles_list', kwargs={'pk': obj.pk}), survey_spec = self.reverse('api:workflow_job_template_survey_spec', kwargs={'pk': obj.pk}), @@ -3490,7 +3492,7 @@ class WorkflowApprovalTemplateSerializer(UnifiedJobTemplateSerializer): if 'last_job' in res: del res['last_job'] - res.update(dict(jobs = self.reverse('api:workflow_approval_template_jobs_list', kwargs={'pk': obj.pk}),)) + res.update(jobs = self.reverse('api:workflow_approval_template_jobs_list', kwargs={'pk': obj.pk})) return res diff --git a/awx/api/urls/organization.py b/awx/api/urls/organization.py index 952209423e..1b0997b05c 100644 --- a/awx/api/urls/organization.py +++ b/awx/api/urls/organization.py @@ -18,6 +18,7 @@ from awx.api.views import ( OrganizationNotificationTemplatesErrorList, OrganizationNotificationTemplatesStartedList, OrganizationNotificationTemplatesSuccessList, + OrganizationNotificationTemplatesApprovalList, OrganizationInstanceGroupsList, OrganizationObjectRolesList, OrganizationAccessList, @@ -43,6 +44,8 @@ urls = [ name='organization_notification_templates_error_list'), url(r'^(?P[0-9]+)/notification_templates_success/$', OrganizationNotificationTemplatesSuccessList.as_view(), name='organization_notification_templates_success_list'), + url(r'^(?P[0-9]+)/notification_templates_approvals/$', OrganizationNotificationTemplatesApprovalList.as_view(), + name='organization_notification_templates_approvals_list'), url(r'^(?P[0-9]+)/instance_groups/$', OrganizationInstanceGroupsList.as_view(), name='organization_instance_groups_list'), url(r'^(?P[0-9]+)/object_roles/$', OrganizationObjectRolesList.as_view(), name='organization_object_roles_list'), url(r'^(?P[0-9]+)/access_list/$', OrganizationAccessList.as_view(), name='organization_access_list'), diff --git a/awx/api/urls/workflow_job_template.py b/awx/api/urls/workflow_job_template.py index 0a33a8eaaa..349dad1aa5 100644 --- a/awx/api/urls/workflow_job_template.py +++ b/awx/api/urls/workflow_job_template.py @@ -16,6 +16,7 @@ from awx.api.views import ( WorkflowJobTemplateNotificationTemplatesErrorList, WorkflowJobTemplateNotificationTemplatesStartedList, WorkflowJobTemplateNotificationTemplatesSuccessList, + WorkflowJobTemplateNotificationTemplatesApprovalList, WorkflowJobTemplateAccessList, WorkflowJobTemplateObjectRolesList, WorkflowJobTemplateLabelList, @@ -38,6 +39,8 @@ urls = [ name='workflow_job_template_notification_templates_error_list'), url(r'^(?P[0-9]+)/notification_templates_success/$', WorkflowJobTemplateNotificationTemplatesSuccessList.as_view(), name='workflow_job_template_notification_templates_success_list'), + url(r'^(?P[0-9]+)/notification_templates_approvals/$', WorkflowJobTemplateNotificationTemplatesApprovalList.as_view(), + name='workflow_job_template_notification_templates_approvals_list'), url(r'^(?P[0-9]+)/access_list/$', WorkflowJobTemplateAccessList.as_view(), name='workflow_job_template_access_list'), url(r'^(?P[0-9]+)/object_roles/$', WorkflowJobTemplateObjectRolesList.as_view(), name='workflow_job_template_object_roles_list'), url(r'^(?P[0-9]+)/labels/$', WorkflowJobTemplateLabelList.as_view(), name='workflow_job_template_label_list'), diff --git a/awx/api/views/__init__.py b/awx/api/views/__init__.py index d77ec92b91..7b831edfa5 100644 --- a/awx/api/views/__init__.py +++ b/awx/api/views/__init__.py @@ -119,6 +119,7 @@ from awx.api.views.organization import ( # noqa OrganizationNotificationTemplatesErrorList, OrganizationNotificationTemplatesStartedList, OrganizationNotificationTemplatesSuccessList, + OrganizationNotificationTemplatesApprovalList, OrganizationInstanceGroupsList, OrganizationAccessList, OrganizationObjectRolesList, @@ -3288,6 +3289,11 @@ class WorkflowJobTemplateNotificationTemplatesSuccessList(WorkflowJobTemplateNot relationship = 'notification_templates_success' +class WorkflowJobTemplateNotificationTemplatesApprovalList(WorkflowJobTemplateNotificationTemplatesAnyList): + + relationship = 'notification_templates_approvals' + + class WorkflowJobTemplateAccessList(ResourceAccessList): model = models.User # needs to be User for AccessLists's @@ -3373,6 +3379,11 @@ class WorkflowJobNotificationsList(SubListAPIView): relationship = 'notifications' search_fields = ('subject', 'notification_type', 'body',) + def get_sublist_queryset(self, parent): + return self.model.objects.filter(Q(unifiedjob_notifications=parent) | + Q(unifiedjob_notifications__unified_job_node__workflow_job=parent, + unifiedjob_notifications__workflowapproval__isnull=False)).distinct() + class WorkflowJobActivityStreamList(SubListAPIView): diff --git a/awx/api/views/organization.py b/awx/api/views/organization.py index 6213e14f63..e1af4c67b1 100644 --- a/awx/api/views/organization.py +++ b/awx/api/views/organization.py @@ -195,6 +195,11 @@ class OrganizationNotificationTemplatesSuccessList(OrganizationNotificationTempl relationship = 'notification_templates_success' +class OrganizationNotificationTemplatesApprovalList(OrganizationNotificationTemplatesAnyList): + + relationship = 'notification_templates_approvals' + + class OrganizationInstanceGroupsList(SubListAttachDetachAPIView): model = InstanceGroup diff --git a/awx/main/migrations/0091_v360_approval_node_notifications.py b/awx/main/migrations/0091_v360_approval_node_notifications.py new file mode 100644 index 0000000000..3963b776b8 --- /dev/null +++ b/awx/main/migrations/0091_v360_approval_node_notifications.py @@ -0,0 +1,28 @@ +# Generated by Django 2.2.4 on 2019-09-11 13:44 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('main', '0090_v360_WFJT_prompts'), + ] + + operations = [ + migrations.AddField( + model_name='organization', + name='notification_templates_approvals', + field=models.ManyToManyField(blank=True, related_name='organization_notification_templates_for_approvals', to='main.NotificationTemplate'), + ), + migrations.AddField( + model_name='workflowjobtemplate', + name='notification_templates_approvals', + field=models.ManyToManyField(blank=True, related_name='workflowjobtemplate_notification_templates_for_approvals', to='main.NotificationTemplate'), + ), + migrations.AlterField( + model_name='workflowjobnode', + name='do_not_run', + field=models.BooleanField(default=False, help_text='Indicates that a job will not be created when True. Workflow runtime semantics will mark this True if the node is in a path that will decidedly not be ran. A value of False means the node may not run.'), + ), + ] diff --git a/awx/main/models/organization.py b/awx/main/models/organization.py index 60505b6e0a..df5d491d20 100644 --- a/awx/main/models/organization.py +++ b/awx/main/models/organization.py @@ -51,6 +51,11 @@ class Organization(CommonModel, NotificationFieldsModel, ResourceMixin, CustomVi default=0, help_text=_('Maximum number of hosts allowed to be managed by this organization.'), ) + notification_templates_approvals = models.ManyToManyField( + "NotificationTemplate", + blank=True, + related_name='%(class)s_notification_templates_for_approvals' + ) admin_role = ImplicitRoleField( parent_role='singleton:' + ROLE_SINGLETON_SYSTEM_ADMINISTRATOR, diff --git a/awx/main/models/workflow.py b/awx/main/models/workflow.py index b2312ab63d..29715d4cc8 100644 --- a/awx/main/models/workflow.py +++ b/awx/main/models/workflow.py @@ -3,9 +3,11 @@ # Python import logging +from copy import copy +from urllib.parse import urljoin # Django -from django.db import models +from django.db import connection, models from django.conf import settings from django.utils.translation import ugettext_lazy as _ from django.core.exceptions import ObjectDoesNotExist @@ -38,9 +40,6 @@ from awx.main.fields import JSONField from awx.main.utils import schedule_task_manager -from copy import copy -from urllib.parse import urljoin - __all__ = ['WorkflowJobTemplate', 'WorkflowJob', 'WorkflowJobOptions', 'WorkflowJobNode', 'WorkflowJobTemplateNode', 'WorkflowApprovalTemplate', 'WorkflowApproval'] @@ -196,7 +195,7 @@ class WorkflowJobNode(WorkflowNodeBase): ) do_not_run = models.BooleanField( default=False, - help_text=_("Indidcates that a job will not be created when True. Workflow runtime " + help_text=_("Indicates that a job will not be created when True. Workflow runtime " "semantics will mark this True if the node is in a path that will " "decidedly not be ran. A value of False means the node may not run."), ) @@ -388,6 +387,12 @@ class WorkflowJobTemplate(UnifiedJobTemplate, WorkflowJobOptions, SurveyJobTempl blank=True, default=False, ) + notification_templates_approvals = models.ManyToManyField( + "NotificationTemplate", + blank=True, + related_name='%(class)s_notification_templates_for_approvals' + ) + admin_role = ImplicitRoleField(parent_role=[ 'singleton:' + ROLE_SINGLETON_SYSTEM_ADMINISTRATOR, 'organization.workflow_admin_role' @@ -441,9 +446,16 @@ class WorkflowJobTemplate(UnifiedJobTemplate, WorkflowJobOptions, SurveyJobTempl .filter(unifiedjobtemplate_notification_templates_for_started__in=[self])) success_notification_templates = list(base_notification_templates .filter(unifiedjobtemplate_notification_templates_for_success__in=[self])) + approval_notification_templates = list(base_notification_templates + .filter(workflowjobtemplate_notification_templates_for_approvals__in=[self])) + # Get Organization NotificationTemplates + if self.organization is not None: + approval_notification_templates = set(approval_notification_templates + list(base_notification_templates.filter( + organization_notification_templates_for_approvals=self.organization))) return dict(error=list(error_notification_templates), started=list(started_notification_templates), - success=list(success_notification_templates)) + success=list(success_notification_templates), + approvals=list(approval_notification_templates)) def create_unified_job(self, **kwargs): workflow_job = super(WorkflowJobTemplate, self).create_unified_job(**kwargs) @@ -649,7 +661,7 @@ class WorkflowApprovalTemplate(UnifiedJobTemplate): return self.workflowjobtemplatenodes.first().workflow_job_template -class WorkflowApproval(UnifiedJob): +class WorkflowApproval(UnifiedJob, JobNotificationMixin): class Meta: app_label = 'main' @@ -689,6 +701,7 @@ class WorkflowApproval(UnifiedJob): def approve(self, request=None): self.status = 'successful' self.save() + self.send_approval_notification('approved') self.websocket_emit_status(self.status) schedule_task_manager() return reverse('api:workflow_approval_approve', kwargs={'pk': self.pk}, request=request) @@ -696,10 +709,53 @@ class WorkflowApproval(UnifiedJob): def deny(self, request=None): self.status = 'failed' self.save() + self.send_approval_notification('denied') self.websocket_emit_status(self.status) schedule_task_manager() return reverse('api:workflow_approval_deny', kwargs={'pk': self.pk}, request=request) + def signal_start(self, **kwargs): + can_start = super(WorkflowApproval, self).signal_start(**kwargs) + self.send_approval_notification('running') + return can_start + + def send_approval_notification(self, approval_status): + from awx.main.tasks import send_notifications # avoid circular import + if self.workflow_job_template is None: + return + for nt in self.workflow_job_template.notification_templates["approvals"]: + try: + (notification_subject, notification_body) = self.build_approval_notification_message(nt, approval_status) + except Exception: + raise NotImplementedError("build_approval_notification_message() does not exist") + + # Use kwargs to force late-binding + # https://stackoverflow.com/a/3431699/10669572 + def send_it(local_nt=nt, local_subject=notification_subject, local_body=notification_body): + def _func(): + send_notifications.delay([local_nt.generate_notification(local_subject, local_body).id], + job_id=self.id) + return _func + connection.on_commit(send_it()) + + def build_approval_notification_message(self, nt, approval_status): + subject = [] + workflow_url = urljoin(settings.TOWER_URL_BASE, '/#/workflows/{}'.format(self.workflow_job.id)) + subject.append(('The approval node "{}"').format(self.workflow_approval_template.name)) + if approval_status == 'running': + subject.append(('needs review. This node can be viewed at: {}').format(workflow_url)) + if approval_status == 'approved': + subject.append(('was approved. {}').format(workflow_url)) + if approval_status == 'timed_out': + subject.append(('has timed out. {}').format(workflow_url)) + elif approval_status == 'denied': + subject.append(('was denied. {}').format(workflow_url)) + subject = " ".join(subject) + body = self.notification_data() + body['body'] = subject + + return subject, body + @property def workflow_job_template(self): try: diff --git a/awx/main/scheduler/task_manager.py b/awx/main/scheduler/task_manager.py index 3d89d6aecc..73218b6f81 100644 --- a/awx/main/scheduler/task_manager.py +++ b/awx/main/scheduler/task_manager.py @@ -533,6 +533,7 @@ class TaskManager(): logger.warn(timeout_message) task.timed_out = True task.status = 'failed' + task.send_approval_notification('timed_out') task.websocket_emit_status(task.status) task.job_explanation = timeout_message task.save(update_fields=['status', 'job_explanation', 'timed_out']) diff --git a/awx/main/tasks.py b/awx/main/tasks.py index 818714998e..03bdd924d6 100644 --- a/awx/main/tasks.py +++ b/awx/main/tasks.py @@ -323,7 +323,7 @@ def send_notifications(notification_list, job_id=None): notification.status = "successful" notification.notifications_sent = sent except Exception as e: - logger.error("Send Notification Failed {}".format(e)) + logger.exception("Send Notification Failed {}".format(e)) notification.status = "failed" notification.error = smart_str(e) update_fields.append('error') diff --git a/awx/main/tests/functional/api/test_notifications.py b/awx/main/tests/functional/api/test_notifications.py index 1a64220dfa..b7e9af9fcd 100644 --- a/awx/main/tests/functional/api/test_notifications.py +++ b/awx/main/tests/functional/api/test_notifications.py @@ -135,3 +135,45 @@ def test_search_on_notification_configuration_is_prevented(get, admin): response = get(url, {'notification_configuration__regex': 'ABCDEF'}, admin) assert response.status_code == 403 assert response.data == {"detail": "Filtering on notification_configuration is not allowed."} + + +@pytest.mark.django_db +def test_get_wfjt_approval_notification(get, admin, workflow_job_template): + url = reverse('api:workflow_job_template_notification_templates_approvals_list', kwargs={'pk': workflow_job_template.pk}) + response = get(url, admin) + assert response.status_code == 200 + assert len(response.data['results']) == 0 + + +@pytest.mark.django_db +def test_post_wfjt_approval_notification(get, post, admin, notification_template, workflow_job_template): + url = reverse('api:workflow_job_template_notification_templates_approvals_list', kwargs={'pk': workflow_job_template.pk}) + response = post(url, + dict(id=notification_template.id, + associate=True), + admin) + assert response.status_code == 204 + response = get(url, admin) + assert response.status_code == 200 + assert len(response.data['results']) == 1 + + +@pytest.mark.django_db +def test_get_org_approval_notification(get, admin, organization): + url = reverse('api:organization_notification_templates_approvals_list', kwargs={'pk': organization.pk}) + response = get(url, admin) + assert response.status_code == 200 + assert len(response.data['results']) == 0 + + +@pytest.mark.django_db +def test_post_org_approval_notification(get, post, admin, notification_template, organization): + url = reverse('api:organization_notification_templates_approvals_list', kwargs={'pk': organization.pk}) + response = post(url, + dict(id=notification_template.id, + associate=True), + admin) + assert response.status_code == 204 + response = get(url, admin) + assert response.status_code == 200 + assert len(response.data['results']) == 1 diff --git a/awx/ui/client/src/notifications/notifications.list.js b/awx/ui/client/src/notifications/notifications.list.js index d146465f87..756588d200 100644 --- a/awx/ui/client/src/notifications/notifications.list.js +++ b/awx/ui/client/src/notifications/notifications.list.js @@ -25,15 +25,29 @@ export default ['i18n', 'templateUrl', function(i18n, templateUrl){ name: { key: true, label: i18n._('Name'), - columnClass: 'col-md-4 col-sm-9 col-xs-9', - linkTo: '/#/notification_templates/{{notification.id}}' + columnClass: 'col-sm-9 col-xs-9', + linkTo: '/#/notification_templates/{{notification.id}}', + columnNgClass: "{'col-lg-4 col-md-2': showApprovalColumn, 'col-lg-5 col-md-3': !showApprovalColumn}" }, notification_type: { label: i18n._('Type'), searchType: 'select', searchOptions: [], excludeModal: true, - columnClass: 'd-none d-sm-flex col-md-4 col-sm-3' + columnClass: 'd-none d-sm-flex col-lg-4 col-md-2 col-sm-3', + }, + notification_templates_approvals: { + label: i18n._('Approval'), + columnClass: 'd-none d-md-flex justify-content-start col-lg-1 col-md-2', + flag: 'notification_templates_approvals', + type: "toggle", + ngClick: "toggleNotification($event, notification.id, 'notification_templates_approvals')", + ngDisabled: "!sufficientRoleForNotifToggle", + awToolTip: "{{ schedule.play_tip }}", + dataTipWatch: "schedule.play_tip", + dataPlacement: "right", + nosort: true, + ngIf: "showApprovalColumn" }, notification_templates_started: { label: i18n._("Start"), @@ -45,7 +59,7 @@ export default ['i18n', 'templateUrl', function(i18n, templateUrl){ dataTipWatch: "schedule.play_tip", dataPlacement: "right", nosort: true, - columnClass: 'd-none d-md-flex justify-content-start col-md-1' + columnClass: 'd-none d-md-flex justify-content-start col-lg-1 col-md-2' }, notification_templates_success: { label: i18n._('Success'), @@ -57,11 +71,11 @@ export default ['i18n', 'templateUrl', function(i18n, templateUrl){ dataTipWatch: "schedule.play_tip", dataPlacement: "right", nosort: true, - columnClass: 'd-none d-md-flex justify-content-start col-md-1' + columnClass: 'd-none d-md-flex justify-content-start col-lg-1 col-md-2' }, notification_templates_error: { label: i18n._('Failure'), - columnClass: 'd-none d-md-flex justify-content-start col-md-1 NotifierList-lastColumn', + columnClass: 'd-none d-md-flex justify-content-start col-lg-1 col-md-2 NotifierList-lastColumn', flag: 'notification_templates_error', type: "toggle", ngClick: "toggleNotification($event, notification.id, 'notification_templates_error')", diff --git a/awx/ui/client/src/notifications/shared/notification-list-init.factory.js b/awx/ui/client/src/notifications/shared/notification-list-init.factory.js index 03027feab9..7829f9e781 100644 --- a/awx/ui/client/src/notifications/shared/notification-list-init.factory.js +++ b/awx/ui/client/src/notifications/shared/notification-list-init.factory.js @@ -22,6 +22,10 @@ export default ['Wait', 'GetBasePath', 'ProcessErrors', 'Rest', 'GetChoices', url = params.url, id = params.id; + if ($state.includes('templates.editWorkflowJobTemplate') || $state.includes('organizations.edit')) { + scope.showApprovalColumn = true; + } + scope.addNotificationTemplate = function() { var org_id; if($stateParams.hasOwnProperty('project_id')){ @@ -51,6 +55,10 @@ export default ['Wait', 'GetBasePath', 'ProcessErrors', 'Rest', 'GetChoices', scope.relatednotificationsRemove = scope.$on('relatednotifications', function () { var columns = ['/notification_templates_started/', '/notification_templates_success/', '/notification_templates_error/']; + if ($state.includes('templates.editWorkflowJobTemplate') || $state.includes('organizations.edit')) { + columns.push('/notification_templates_approvals'); + } + GetChoices({ scope: scope, url: GetBasePath('notifications'), @@ -64,9 +72,17 @@ export default ['Wait', 'GetBasePath', 'ProcessErrors', 'Rest', 'GetChoices', Rest.setUrl(notifier_url); Rest.get() .then(function(response) { - let checkForSuccessOrError = response.config.url.indexOf('success') > 0 ? "notification_templates_success" : "notification_templates_error"; + let type; - let type = response.config.url.indexOf('started') > 0 ? "notification_templates_started" : checkForSuccessOrError; + if (response.config.url.indexOf('started') > 0) { + type = "notification_templates_started"; + } else if (response.config.url.indexOf('success') > 0) { + type = "notification_templates_success"; + } else if (response.config.url.indexOf('error') > 0) { + type = "notification_templates_error"; + } else if (response.config.url.indexOf('approvals') > 0) { + type = "notification_templates_approvals"; + } if (response.data.results) { _.forEach(response.data.results, function(result){ diff --git a/awx/ui/client/src/shared/column-sort/column-sort.partial.html b/awx/ui/client/src/shared/column-sort/column-sort.partial.html index c250ad4571..2efe6d8db5 100644 --- a/awx/ui/client/src/shared/column-sort/column-sort.partial.html +++ b/awx/ui/client/src/shared/column-sort/column-sort.partial.html @@ -1,4 +1,4 @@ -
+
{{columnLabel | translate}}
diff --git a/awx/ui/client/src/shared/generator-helpers.js b/awx/ui/client/src/shared/generator-helpers.js index 60af4a17aa..c0489bd941 100644 --- a/awx/ui/client/src/shared/generator-helpers.js +++ b/awx/ui/client/src/shared/generator-helpers.js @@ -527,8 +527,9 @@ angular.module('GeneratorHelpers', [systemStatus.name]) } else if (field.type === 'template') { html = Template(field); } else if (field.type === 'toggle') { + const ngIf = field.ngIf ? `ng-if="${field.ngIf}"` : ''; html += ` -
+
`; diff --git a/awx/ui/client/src/shared/list-generator/list-generator.factory.js b/awx/ui/client/src/shared/list-generator/list-generator.factory.js index 11e932cc09..3b0b15e32c 100644 --- a/awx/ui/client/src/shared/list-generator/list-generator.factory.js +++ b/awx/ui/client/src/shared/list-generator/list-generator.factory.js @@ -546,7 +546,9 @@ export default ['$compile', 'Attr', 'Icon', for (fld in list.fields) { if (options.mode !== 'lookup' || (options.mode === 'lookup' && (fld === 'name' || _.has(list.fields[fld], 'includeModal')))){ let customClass = list.fields[fld].columnClass || ''; + const ngIf = list.fields[fld].ngIf ? `ng-if="${list.fields[fld].ngIf}"` : ''; html += `
`; }