return crmApi4('SavedSearch', 'get', {
where: [['id', '=', params.id]],
chain: {
- groups: ['Group', 'get', {where: [['saved_search_id', '=', '$id']]}],
+ groups: ['Group', 'get', {select: ['id', 'title', 'description', 'visibility', 'group_type'], where: [['saved_search_id', '=', '$id']]}],
displays: ['SearchDisplay', 'get', {where: [['saved_search_id', '=', '$id']]}]
}
}, 0);
$scope.$watch('$ctrl.savedSearch', onChangeAnything, true);
- // Is this savedSearch record saved, unsaved or saving
- $scope.status = this.savedSearch && this.savedSearch.id ? 'saved' : 'unsaved';
+ // After watcher runs for the first time and messes up the status, set it correctly
+ $timeout(function() {
+ $scope.status = ctrl.savedSearch && ctrl.savedSearch.id ? 'saved' : 'unsaved';
+ });
loadFieldOptions();
};
}
this.save = function() {
+ if (!validate()) {
+ return;
+ }
$scope.status = 'saving';
var params = _.cloneDeep(ctrl.savedSearch),
apiCalls = {},
delete params.displays;
apiCalls.saved = ['SavedSearch', 'save', {records: [params], chain: chain}, 0];
crmApi4(apiCalls).then(function(results) {
+ // Set new status to saved unless the user changed something in the interim
+ var newStatus = $scope.status === 'unsaved' ? 'unsaved' : 'saved';
ctrl.savedSearch.id = results.saved.id;
- ctrl.savedSearch.groups = results.saved.groups || [];
- ctrl.savedSearch.displays = results.saved.displays || [];
- if ($scope.status === 'saving') {
- $scope.status = 'saved';
+ if (results.saved.groups && results.saved.groups.length) {
+ ctrl.savedSearch.groups[0].id = results.saved.groups[0].id;
}
+ ctrl.savedSearch.displays = results.saved.displays || [];
+ // Wait until after onChangeAnything to update status
+ $timeout(function() {
+ $scope.status = newStatus;
+ });
});
};
var display = ctrl.savedSearch.displays[index];
if (display.id) {
display.trashed = !display.trashed;
+ if ($scope.controls.tab === ('display_' + index) && display.trashed) {
+ $scope.selectTab('compose');
+ } else if (!display.trashed) {
+ $scope.selectTab('display_' + index);
+ }
} else {
$scope.selectTab('compose');
ctrl.savedSearch.displays.splice(index, 1);
if (!ctrl.groupExists && (!ctrl.savedSearch.groups.length || !ctrl.savedSearch.groups[0].id)) {
ctrl.savedSearch.groups.length = 0;
}
- $scope.selectTab('compose');
+ if ($scope.controls.tab === 'group') {
+ $scope.selectTab('compose');
+ }
};
$scope.getJoinEntities = function() {
}
};
+ function validate() {
+ var errors = [],
+ errorEl,
+ label,
+ tab;
+ if (!ctrl.savedSearch.label) {
+ errorEl = '#crm-saved-search-label';
+ label = ts('Search Label');
+ errors.push(ts('%1 is a required field.', {1: label}));
+ }
+ if (ctrl.groupExists && !ctrl.savedSearch.groups[0].title) {
+ errorEl = '#crm-search-admin-group-title';
+ label = ts('Group Title');
+ errors.push(ts('%1 is a required field.', {1: label}));
+ tab = 'group';
+ }
+ _.each(ctrl.savedSearch.displays, function(display, index) {
+ if (!display.trashed && !display.label) {
+ errorEl = '#crm-search-admin-display-label';
+ label = ts('Display Label');
+ errors.push(ts('%1 is a required field.', {1: label}));
+ tab = 'display_' + index;
+ }
+ });
+ if (errors.length) {
+ if (tab) {
+ $scope.selectTab(tab);
+ }
+ $(errorEl).crmError(errors.join('<br>'), ts('Error Saving'), {expires: 5000});
+ }
+ return !errors.length;
+ }
+
/**
* Called when clicking on a column header
* @param col
</div>
<form>
- <div class="navbar-form clearfix">
- <label for="crm-saved-search-label">{{:: ts('Label:') }}</label>
- <input id="crm-saved-search-label" class="form-control" ng-model="$ctrl.savedSearch.label" type="text" />
- <label for="crm-search-main-entity">{{:: ts('Search for:') }}</label>
- <input id="crm-search-main-entity" class="form-control" ng-model="$ctrl.savedSearch.api_entity" crm-ui-select="::{allowClear: false, data: entities}" ng-disabled="$ctrl.savedSearch.id" />
- <div class="btn-group btn-group-md pull-right">
- <button type="submit" class="btn" ng-class="{'btn-primary': status === 'unsaved', 'btn-warning': status === 'saving', 'btn-success': status === 'saved'}" ng-disabled="status !== 'unsaved'" ng-click="$ctrl.save()">
- <i class="crm-i" ng-class="{'fa-check': status !== 'saving', 'fa-spin fa-spinner': status === 'saving'}"></i>
- <span ng-if="status === 'saved'">{{ ts('Saved') }}</span>
- <span ng-if="status === 'unsaved'">{{ ts('Save') }}</span>
- <span ng-if="status === 'saving'">{{ ts('Saving...') }}</span>
- </button>
+ <div class="crm-flex-box">
+ <div class="nav-stacked">
+ <input id="crm-saved-search-label" class="form-control" ng-model="$ctrl.savedSearch.label" type="text" required placeholder="{{ ts('Untitled Search') }}" />
+ </div>
+ <div class="crm-flex-4 form-inline">
+ <label for="crm-search-main-entity">{{:: ts('Search for:') }}</label>
+ <input id="crm-search-main-entity" class="form-control" ng-model="$ctrl.savedSearch.api_entity" crm-ui-select="::{allowClear: false, data: entities}" ng-disabled="$ctrl.savedSearch.id" />
+ <div class="btn-group btn-group-md pull-right">
+ <button type="submit" class="btn" ng-class="{'btn-primary': status === 'unsaved', 'btn-warning': status === 'saving', 'btn-success': status === 'saved'}" ng-disabled="status !== 'unsaved'" ng-click="$ctrl.save()">
+ <i class="crm-i" ng-class="{'fa-check': status !== 'saving', 'fa-spin fa-spinner': status === 'saving'}"></i>
+ <span ng-if="status === 'saved'">{{ ts('Saved') }}</span>
+ <span ng-if="status === 'unsaved'">{{ ts('Save') }}</span>
+ <span ng-if="status === 'saving'">{{ ts('Saving...') }}</span>
+ </button>
+ </div>
</div>
</div>
<div class="crm-flex-box">
<fieldset>
<div class="form-inline">
- <label for="search_display_label">{{:: ts('Name:') }} <span class="crm-marker">*</span></label>
- <input id="search_display_label" type="text" class="form-control" ng-model="$ctrl.display.label" required />
+ <label for="crm-search-admin-display-label">{{:: ts('Name:') }} <span class="crm-marker">*</span></label>
+ <input id="crm-search-admin-display-label" type="text" class="form-control" ng-model="$ctrl.display.label" required placeholder="{{ ts('Untitled') }}"/>
<label class="pull-right">{{:: $ctrl.displayTypes[$ctrl.display.type].label }}</label>
</div>
</fieldset>
-<div ng-if="!$ctrl.groupExists">
- <div class="alert alert-warning">
- {{:: ts('Smart group "%1" will be deleted.', {1: $ctrl.savedSearch.groups[0].title}) }}
- </div>
+<div class="alert alert-warning" ng-show="!smartGroupColumns.length">
+ {{:: ts('Unable to create smart group because this search does not include any contacts.') }}
+</div>
+
+<div class="form-inline">
+ <label for="crm-search-admin-group-title">{{ ts('Group Title:') }} <span class="crm-marker">*</span></label>
+ <input id="crm-search-admin-group-title" class="form-control" placeholder="{{:: ts('Untitled') }}" ng-model="$ctrl.savedSearch.groups[0].title" ng-disabled="!smartGroupColumns.length" ng-required="smartGroupColumns.length">
+ <label for="api-save-search-select-column">{{:: ts('Contact Column:') }}</label>
+ <input id="api-save-search-select-column" ng-model="$ctrl.savedSearch.api_params.select[0]" class="form-control" crm-ui-select="{data: smartGroupColumns}"/>
</div>
-<div ng-if="$ctrl.groupExists">
- <div class="alert alert-warning" ng-show="!smartGroupColumns.length">
- {{:: ts('Unable to create smart group because this search does not include any contacts.') }}
+<fieldset ng-show="smartGroupColumns.length">
+ <label>{{:: ts('Description:') }}</label>
+ <textarea class="form-control" ng-model="$ctrl.savedSearch.groups[0].description"></textarea>
+ <div class="form-inline">
+ <label>{{:: ts('Group Type:') }} </label>
+ <div class="checkbox" ng-repeat="option in groupOptions.group_type track by option.id">
+ <label>
+ <input type="checkbox" checklist-model="$ctrl.savedSearch.groups[0].group_type" checklist-value="option.id">
+ {{ option.label }}
+ </label>
+ </div>
</div>
- <input class="form-control" placeholder="{{:: ts('Group Title') }}" ng-model="$ctrl.savedSearch.groups[0].title" ng-disabled="!smartGroupColumns.length">
- <hr />
<div class="form-inline">
- <label for="api-save-search-select-column">{{:: ts('Contact Column:') }} <span class="crm-marker">*</span></label>
- <input id="api-save-search-select-column" ng-model="$ctrl.savedSearch.api_params.select[0]" class="form-control" crm-ui-select="{data: smartGroupColumns}"/>
+ <label>{{:: ts('Visibility:') }}</label>
+ <select class="form-control" ng-model="$ctrl.savedSearch.groups[0].visibility" ng-options="item.id as item.label for item in groupOptions.visibility track by item.id" crm-ui-select></select>
</div>
- <fieldset ng-show="smartGroupColumns.length">
- <label>{{:: ts('Description:') }}</label>
- <textarea class="form-control" ng-model="$ctrl.savedSearch.groups[0].description"></textarea>
- <div class="form-inline">
- <label>{{:: ts('Group Type:') }} </label>
- <div class="checkbox" ng-repeat="option in groupOptions.group_type track by option.id">
- <label>
- <input type="checkbox" checklist-model="$ctrl.savedSearch.groups[0].group_type" checklist-value="option.id">
- {{ option.label }}
- </label>
- </div>
- </div>
- <div class="form-inline">
- <label>{{:: ts('Visibility:') }}</label>
- <select class="form-control" ng-model="$ctrl.savedSearch.groups[0].visibility" ng-options="item.id as item.label for item in groupOptions.visibility track by item.id" crm-ui-select></select>
- </div>
- </fieldset>
-</div>
+</fieldset>
{{ ts('Compose Search') }}
</a>
</li>
-<li role="presentation" ng-class="{active: controls.tab === 'group'}" ng-if="$ctrl.savedSearch.groups.length">
- <a href ng-click="selectTab('group')" ng-class="{strikethrough: !$ctrl.groupExists}">
+<li role="presentation" ng-class="{active: controls.tab === 'group'}" ng-if="$ctrl.savedSearch.groups.length" title="{{ !$ctrl.groupExists ? ts('Group will be deleted.') : '' }}">
+ <a href ng-click="selectTab('group')" ng-disabled="!$ctrl.groupExists">
<i class="crm-i fa-users"></i>
{{:: ts('Smart Group:') }} {{ $ctrl.savedSearch.groups[0].title }}
</a>
- <button class="btn-xs btn-danger-outline crm-search-delete-display" ng-click="$ctrl.removeGroup()">
- <i class="crm-i fa-trash"></i>
+ <button class="btn-xs btn-danger-outline crm-search-delete-display" ng-click="$ctrl.removeGroup()" title="{{ $ctrl.groupExists ? ts('Delete') : ts('Undelete') }}">
+ <i class="crm-i fa-{{ $ctrl.groupExists ? 'trash' : 'undo' }}"></i>
</button>
</li>
-<li role="presentation" ng-repeat="display in $ctrl.savedSearch.displays" ng-class="{active: controls.tab === ('display_' + $index)}">
- <a href ng-click="selectTab('display_' + $index)" ng-class="{strikethrough: display.trashed}">
+<li role="presentation" ng-repeat="display in $ctrl.savedSearch.displays" ng-class="{active: controls.tab === ('display_' + $index)}" title="{{ display.trashed ? ts('Display will be deleted.') : '' }}">
+ <a href ng-click="selectTab('display_' + $index)" ng-disabled="display.trashed">
<i class="crm-i {{ $ctrl.displayTypes[display.type].icon }}"></i>
{{ display.label || ts('Untitled') }}
</a>
- <button class="btn-xs btn-danger-outline crm-search-delete-display" ng-click="$ctrl.removeDisplay($index)">
- <i class="crm-i fa-trash"></i>
+ <button class="btn-xs btn-danger-outline crm-search-delete-display" ng-click="$ctrl.removeDisplay($index)" title="{{ display.trashed ? ts('Undelete') : ts('Delete') }}">
+ <i class="crm-i fa-{{ display.trashed ? 'undo' : 'trash' }}"></i>
</button>
</li>
<li role="presentation">
min-height: 200px;
}
-#bootstrap-theme.crm-search ul.nav.nav-stacked {
+#bootstrap-theme.crm-search .nav-stacked {
margin-left: 0;
margin-right: 20px;
}
+#bootstrap-theme.crm-search ul.nav-stacked {
+ margin-top: 20px;
+}
+
+#bootstrap-theme.crm-search input.ng-invalid {
+ border-color: #8A1F11;
+}
+#bootstrap-theme.crm-search input.ng-invalid::placeholder {
+ color: #8A1F11;
+}
+
+#bootstrap-theme.crm-search ul.nav-stacked li {
+ cursor: default;
+}
+
+#bootstrap-theme.crm-search ul.nav-stacked li a[disabled] {
+ text-decoration: line-through !important;
+ color: grey;
+ cursor: default;
+ pointer-events: none;
+}
+
#bootstrap-theme.crm-search fieldset {
padding: 6px;
border-top: 1px solid lightgrey;
var extra = {
expires: 0
- };
+ }, label;
if ($(this).length) {
if (title === '') {
- var label = $('label[for="' + $(this).attr('name') + '"], label[for="' + $(this).attr('id') + '"]').not('[generated=true]');
+ label = $('label[for="' + $(this).attr('name') + '"], label[for="' + $(this).attr('id') + '"]').not('[generated=true]');
if (label.length) {
label.addClass('crm-error');
var $label = label.clone();
ele.one('change', function () {
if (msg && msg.close) msg.close();
ele.removeClass('crm-error');
- label.removeClass('crm-error');
+ if (label) {
+ label.removeClass('crm-error');
+ }
});
}, 1000);
}