TB-71: Ability to activate users without sending email.

This commit is contained in:
Igor Kulikov 2017-07-21 17:31:59 +03:00
parent 3019bf7570
commit 86515896dd
15 changed files with 331 additions and 13 deletions

View File

@ -173,7 +173,12 @@ public class AuthController extends BaseController {
String baseUrl = constructBaseUrl(request); String baseUrl = constructBaseUrl(request);
String loginUrl = String.format("%s/login", baseUrl); String loginUrl = String.format("%s/login", baseUrl);
String email = user.getEmail(); String email = user.getEmail();
mailService.sendAccountActivatedEmail(loginUrl, email);
try {
mailService.sendAccountActivatedEmail(loginUrl, email);
} catch (Exception e) {
log.info("Unable to send account activation email [{}]", e.getMessage());
}
JwtToken accessToken = tokenFactory.createAccessJwtToken(securityUser); JwtToken accessToken = tokenFactory.createAccessJwtToken(securityUser);
JwtToken refreshToken = refreshTokenRepository.requestRefreshToken(securityUser); JwtToken refreshToken = refreshTokenRepository.requestRefreshToken(securityUser);

View File

@ -63,6 +63,7 @@ public class UserController extends BaseController {
@RequestMapping(value = "/user", method = RequestMethod.POST) @RequestMapping(value = "/user", method = RequestMethod.POST)
@ResponseBody @ResponseBody
public User saveUser(@RequestBody User user, public User saveUser(@RequestBody User user,
@RequestParam(required = false, defaultValue = "true") boolean sendActivationMail,
HttpServletRequest request) throws ThingsboardException { HttpServletRequest request) throws ThingsboardException {
try { try {
SecurityUser authUser = getCurrentUser(); SecurityUser authUser = getCurrentUser();
@ -70,7 +71,7 @@ public class UserController extends BaseController {
throw new ThingsboardException("You don't have permission to perform this operation!", throw new ThingsboardException("You don't have permission to perform this operation!",
ThingsboardErrorCode.PERMISSION_DENIED); ThingsboardErrorCode.PERMISSION_DENIED);
} }
boolean sendEmail = user.getId() == null; boolean sendEmail = user.getId() == null && sendActivationMail;
if (getCurrentUser().getAuthority() == Authority.TENANT_ADMIN) { if (getCurrentUser().getAuthority() == Authority.TENANT_ADMIN) {
user.setTenantId(getCurrentUser().getTenantId()); user.setTenantId(getCurrentUser().getTenantId());
} }
@ -116,6 +117,35 @@ public class UserController extends BaseController {
} }
} }
@PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN')")
@RequestMapping(value = "/user/{userId}/activationLink", method = RequestMethod.GET, produces = "text/plain")
@ResponseBody
public String getActivationLink(
@PathVariable("userId") String strUserId,
HttpServletRequest request) throws ThingsboardException {
checkParameter("userId", strUserId);
try {
UserId userId = new UserId(toUUID(strUserId));
SecurityUser authUser = getCurrentUser();
if (authUser.getAuthority() == Authority.CUSTOMER_USER && !authUser.getId().equals(userId)) {
throw new ThingsboardException("You don't have permission to perform this operation!",
ThingsboardErrorCode.PERMISSION_DENIED);
}
User user = checkUserId(userId);
UserCredentials userCredentials = userService.findUserCredentialsByUserId(user.getId());
if (!userCredentials.isEnabled()) {
String baseUrl = constructBaseUrl(request);
String activateUrl = String.format("%s/api/noauth/activate?activateToken=%s", baseUrl,
userCredentials.getActivateToken());
return activateUrl;
} else {
throw new ThingsboardException("User is already active!", ThingsboardErrorCode.BAD_REQUEST_PARAMS);
}
} catch (Exception e) {
throw handleException(e);
}
}
@PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN')") @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN')")
@RequestMapping(value = "/user/{userId}", method = RequestMethod.DELETE) @RequestMapping(value = "/user/{userId}", method = RequestMethod.DELETE)
@ResponseStatus(value = HttpStatus.OK) @ResponseStatus(value = HttpStatus.OK)

View File

@ -45,6 +45,7 @@ function UserService($http, $q, $rootScope, adminService, dashboardService, logi
isUserLoaded: isUserLoaded, isUserLoaded: isUserLoaded,
saveUser: saveUser, saveUser: saveUser,
sendActivationEmail: sendActivationEmail, sendActivationEmail: sendActivationEmail,
getActivationLink: getActivationLink,
setUserFromJwtToken: setUserFromJwtToken, setUserFromJwtToken: setUserFromJwtToken,
getJwtToken: getJwtToken, getJwtToken: getJwtToken,
clearJwtToken: clearJwtToken, clearJwtToken: clearJwtToken,
@ -397,9 +398,12 @@ function UserService($http, $q, $rootScope, adminService, dashboardService, logi
return deferred.promise; return deferred.promise;
} }
function saveUser(user) { function saveUser(user, sendActivationMail) {
var deferred = $q.defer(); var deferred = $q.defer();
var url = '/api/user'; var url = '/api/user';
if (angular.isDefined(sendActivationMail)) {
url += '?sendActivationMail=' + sendActivationMail;
}
$http.post(url, user).then(function success(response) { $http.post(url, user).then(function success(response) {
deferred.resolve(response.data); deferred.resolve(response.data);
}, function fail(response) { }, function fail(response) {
@ -441,6 +445,17 @@ function UserService($http, $q, $rootScope, adminService, dashboardService, logi
return deferred.promise; return deferred.promise;
} }
function getActivationLink(userId) {
var deferred = $q.defer();
var url = `/api/user/${userId}/activationLink`
$http.get(url).then(function success(response) {
deferred.resolve(response.data);
}, function fail() {
deferred.reject();
});
return deferred.promise;
}
function forceDefaultPlace(to, params) { function forceDefaultPlace(to, params) {
if (currentUser && isAuthenticated()) { if (currentUser && isAuthenticated()) {
if (currentUser.authority === 'TENANT_ADMIN' || currentUser.authority === 'CUSTOMER_USER') { if (currentUser.authority === 'TENANT_ADMIN' || currentUser.authority === 'CUSTOMER_USER') {

View File

@ -74,6 +74,11 @@ export default function AppRun($rootScope, $window, $injector, $location, $log,
var locationSearch = $location.search(); var locationSearch = $location.search();
var publicId = locationSearch.publicId; var publicId = locationSearch.publicId;
var activateToken = locationSearch.activateToken;
if (to.url === '/createPassword?activateToken' && activateToken && activateToken.length) {
userService.setUserFromJwtToken(null, null, false);
}
if (userService.isUserLoaded() === true) { if (userService.isUserLoaded() === true) {
if (userService.isAuthenticated()) { if (userService.isAuthenticated()) {
@ -124,7 +129,7 @@ export default function AppRun($rootScope, $window, $injector, $location, $log,
} }
}) })
$rootScope.pageTitle = 'Thingsboard'; $rootScope.pageTitle = 'ThingsBoard';
$rootScope.stateChangeSuccessHandle = $rootScope.$on('$stateChangeSuccess', function (evt, to, params) { $rootScope.stateChangeSuccessHandle = $rootScope.$on('$stateChangeSuccess', function (evt, to, params) {
if (userService.isPublic() && to.name === 'home.dashboards.dashboard') { if (userService.isPublic() && to.name === 'home.dashboards.dashboard') {
@ -133,9 +138,9 @@ export default function AppRun($rootScope, $window, $injector, $location, $log,
} }
if (angular.isDefined(to.data.pageTitle)) { if (angular.isDefined(to.data.pageTitle)) {
$translate(to.data.pageTitle).then(function (translation) { $translate(to.data.pageTitle).then(function (translation) {
$rootScope.pageTitle = 'Thingsboard | ' + translation; $rootScope.pageTitle = 'ThingsBoard | ' + translation;
}, function (translationId) { }, function (translationId) {
$rootScope.pageTitle = 'Thingsboard | ' + translationId; $rootScope.pageTitle = 'ThingsBoard | ' + translationId;
}); });
} }
}) })

View File

@ -26,6 +26,7 @@ import gridTemplate from './grid.tpl.html';
export default angular.module('thingsboard.directives.grid', [thingsboardScopeElement, thingsboardDetailsSidenav]) export default angular.module('thingsboard.directives.grid', [thingsboardScopeElement, thingsboardDetailsSidenav])
.directive('tbGrid', Grid) .directive('tbGrid', Grid)
.controller('AddItemController', AddItemController)
.controller('ItemCardController', ItemCardController) .controller('ItemCardController', ItemCardController)
.directive('tbGridCardContent', GridCardContent) .directive('tbGridCardContent', GridCardContent)
.filter('range', RangeFilter) .filter('range', RangeFilter)
@ -342,6 +343,11 @@ function GridController($scope, $state, $mdDialog, $document, $q, $timeout, $tra
} else { } else {
vm.itemCardController = 'ItemCardController'; vm.itemCardController = 'ItemCardController';
} }
if (vm.config.addItemController) {
vm.addItemController = vm.config.addItemController;
} else {
vm.addItemController = 'AddItemController';
}
vm.parentCtl = vm.config.parentCtl || vm; vm.parentCtl = vm.config.parentCtl || vm;
@ -468,7 +474,7 @@ function GridController($scope, $state, $mdDialog, $document, $q, $timeout, $tra
function addItem($event) { function addItem($event) {
$mdDialog.show({ $mdDialog.show({
controller: AddItemController, controller: vm.addItemController,
controllerAs: 'vm', controllerAs: 'vm',
templateUrl: vm.addItemTemplateUrl, templateUrl: vm.addItemTemplateUrl,
parent: angular.element($document[0].body), parent: angular.element($document[0].body),

View File

@ -1037,6 +1037,7 @@ export default angular.module('thingsboard.locale', [])
"resend-activation": "Resend activation", "resend-activation": "Resend activation",
"email": "Email", "email": "Email",
"email-required": "Email is required.", "email-required": "Email is required.",
"invalid-email-format": "Invalid email format.",
"first-name": "First Name", "first-name": "First Name",
"last-name": "Last Name", "last-name": "Last Name",
"description": "Description", "description": "Description",
@ -1044,7 +1045,14 @@ export default angular.module('thingsboard.locale', [])
"always-fullscreen": "Always fullscreen", "always-fullscreen": "Always fullscreen",
"select-user": "Select user", "select-user": "Select user",
"no-users-matching": "No users matching '{{entity}}' were found.", "no-users-matching": "No users matching '{{entity}}' were found.",
"user-required": "User is required" "user-required": "User is required",
"activation-method": "Activation method",
"display-activation-link": "Display activation link",
"send-activation-mail": "Send activation mail",
"activation-link": "User activation link",
"activation-link-text": "In order to activate user use the following <a href='{{activationLink}}' target='_blank'>activation link</a> :",
"copy-activation-link": "Copy activation link",
"activation-link-copied-message": "User activation link has been copied to clipboard"
}, },
"value": { "value": {
"type": "Value type", "type": "Value type",

View File

@ -0,0 +1,35 @@
/*
* Copyright © 2016-2017 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/*@ngInject*/
export default function ActivationLinkDialogController($mdDialog, $translate, toast, activationLink) {
var vm = this;
vm.activationLink = activationLink;
vm.onActivationLinkCopied = onActivationLinkCopied;
vm.close = close;
function onActivationLinkCopied(){
toast.showSuccess($translate.instant('user.activation-link-copied-message'), 750, angular.element('#activation-link-dialog-content'), 'bottom left');
}
function close() {
$mdDialog.hide();
}
}

View File

@ -0,0 +1,55 @@
<!--
Copyright © 2016-2017 The Thingsboard Authors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<md-dialog aria-label="{{ 'user.activation-link' | translate }}" style="min-width: 400px;">
<form>
<md-toolbar>
<div class="md-toolbar-tools">
<h2 translate="user.activation-link"></h2>
<span flex></span>
<md-button class="md-icon-button" ng-click="vm.close()">
<ng-md-icon icon="close" aria-label="{{ 'dialog.close' | translate }}"></ng-md-icon>
</md-button>
</div>
</md-toolbar>
<md-dialog-content>
<div id="activation-link-dialog-content" class="md-dialog-content">
<md-content class="md-padding" layout="column">
<span translate="user.activation-link-text" translate-values="{activationLink: vm.activationLink}"></span>
<div layout="row" layout-align="start center">
<pre class="tb-highlight" flex><code>{{ vm.activationLink }}</code></pre>
<md-button class="md-icon-button"
ngclipboard
data-clipboard-text="{{ vm.activationLink }}"
ngclipboard-success="vm.onActivationLinkCopied(e)">
<md-icon md-svg-icon="mdi:clipboard-arrow-left"></md-icon>
<md-tooltip md-direction="top">
{{ 'user.copy-activation-link' | translate }}
</md-tooltip>
</md-button>
</div>
</md-content>
</div>
</md-dialog-content>
<md-dialog-actions layout="row">
<span flex></span>
<md-button ng-click="vm.close()">{{ 'action.ok' |
translate }}
</md-button>
</md-dialog-actions>
</form>
</md-dialog>

View File

@ -0,0 +1,112 @@
/*
* Copyright © 2016-2017 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/* eslint-disable import/no-unresolved, import/default */
import activationLinkDialogTemplate from './activation-link.dialog.tpl.html';
/* eslint-enable import/no-unresolved, import/default */
/*@ngInject*/
export default function AddUserController($scope, $mdDialog, $state, $stateParams, $document, $q, types, userService, saveItemFunction, helpLinks) {
var vm = this;
var tenantId = $stateParams.tenantId;
var customerId = $stateParams.customerId;
var usersType = $state.$current.data.usersType;
vm.helpLinks = helpLinks;
vm.item = {};
vm.activationMethods = [
{
value: 'displayActivationLink',
name: 'user.display-activation-link'
},
{
value: 'sendActivationMail',
name: 'user.send-activation-mail'
}
];
vm.userActivationMethod = 'displayActivationLink';
vm.add = add;
vm.cancel = cancel;
function cancel() {
$mdDialog.cancel();
}
function add($event) {
var sendActivationMail = false;
if (vm.userActivationMethod == 'sendActivationMail') {
sendActivationMail = true;
}
if (usersType === 'tenant') {
vm.item.authority = "TENANT_ADMIN";
vm.item.tenantId = {
entityType: types.entityType.tenant,
id: tenantId
};
} else if (usersType === 'customer') {
vm.item.authority = "CUSTOMER_USER";
vm.item.customerId = {
entityType: types.entityType.customer,
id: customerId
};
}
userService.saveUser(vm.item, sendActivationMail).then(function success(item) {
vm.item = item;
$scope.theForm.$setPristine();
if (vm.userActivationMethod == 'displayActivationLink') {
userService.getActivationLink(vm.item.id.id).then(
function success(activationLink) {
displayActivationLink($event, activationLink).then(
function() {
$mdDialog.hide();
}
);
}
);
} else {
$mdDialog.hide();
}
});
}
function displayActivationLink($event, activationLink) {
var deferred = $q.defer();
$mdDialog.show({
controller: 'ActivationLinkDialogController',
controllerAs: 'vm',
templateUrl: activationLinkDialogTemplate,
locals: {
activationLink: activationLink
},
parent: angular.element($document[0].body),
fullscreen: true,
skipHide: true,
targetEvent: $event
}).then(function () {
deferred.resolve();
});
return deferred.promise;
}
}

View File

@ -15,8 +15,8 @@
limitations under the License. limitations under the License.
--> -->
<md-dialog aria-label="{{ 'user.add' | translate }}" tb-help="'users'" help-container-id="help-container"> <md-dialog style="width: 600px;" aria-label="{{ 'user.add' | translate }}" tb-help="'users'" help-container-id="help-container">
<form name="theForm" ng-submit="vm.add()"> <form name="theForm" ng-submit="vm.add($event)">
<md-toolbar> <md-toolbar>
<div class="md-toolbar-tools"> <div class="md-toolbar-tools">
<h2 translate>user.add</h2> <h2 translate>user.add</h2>
@ -32,6 +32,15 @@
<md-dialog-content> <md-dialog-content>
<div class="md-dialog-content"> <div class="md-dialog-content">
<tb-user user="vm.item" is-edit="true" the-form="theForm"></tb-user> <tb-user user="vm.item" is-edit="true" the-form="theForm"></tb-user>
<md-input-container class="md-block">
<label translate>user.activation-method</label>
<md-select aria-label="{{ 'user.activation-method' | translate }}"
ng-model="vm.userActivationMethod">
<md-option ng-repeat="activationMethod in vm.activationMethods" ng-value="activationMethod.value">
{{activationMethod.name | translate}}
</md-option>
</md-select>
</md-input-container>
</div> </div>
</md-dialog-content> </md-dialog-content>
<md-dialog-actions layout="row"> <md-dialog-actions layout="row">

View File

@ -20,6 +20,8 @@ import thingsboardToast from '../services/toast';
import UserRoutes from './user.routes'; import UserRoutes from './user.routes';
import UserController from './user.controller'; import UserController from './user.controller';
import AddUserController from './add-user.controller';
import ActivationLinkDialogController from './activation-link.controller';
import UserDirective from './user.directive'; import UserDirective from './user.directive';
export default angular.module('thingsboard.user', [ export default angular.module('thingsboard.user', [
@ -30,5 +32,7 @@ export default angular.module('thingsboard.user', [
]) ])
.config(UserRoutes) .config(UserRoutes)
.controller('UserController', UserController) .controller('UserController', UserController)
.controller('AddUserController', AddUserController)
.controller('ActivationLinkDialogController', ActivationLinkDialogController)
.directive('tbUser', UserDirective) .directive('tbUser', UserDirective)
.name; .name;

View File

@ -15,6 +15,9 @@
limitations under the License. limitations under the License.
--> -->
<md-button ng-click="onDisplayActivationLink({event: $event})" ng-show="!isEdit" class="md-raised md-primary">{{
'user.display-activation-link' | translate }}
</md-button>
<md-button ng-click="onResendActivation({event: $event})" ng-show="!isEdit" class="md-raised md-primary">{{ <md-button ng-click="onResendActivation({event: $event})" ng-show="!isEdit" class="md-raised md-primary">{{
'user.resend-activation' | translate }} 'user.resend-activation' | translate }}
</md-button> </md-button>
@ -26,9 +29,12 @@
<fieldset ng-disabled="loading || !isEdit"> <fieldset ng-disabled="loading || !isEdit">
<md-input-container class="md-block"> <md-input-container class="md-block">
<label translate>user.email</label> <label translate>user.email</label>
<input required name="email" type="email" ng-model="user.email"> <input required name="email"
ng-pattern="/^[_a-z0-9]+(\.[_a-z0-9]+)*@[a-z0-9-]+(\.[a-z0-9-]+)*(\.[a-z]{2,4})$/"
ng-model="user.email">
<div ng-messages="theForm.email.$error"> <div ng-messages="theForm.email.$error">
<div translate ng-message="required">user.email-required</div> <div translate ng-message="required">user.email-required</div>
<div translate ng-message="pattern">user.invalid-email-format</div>
</div> </div>
</md-input-container> </md-input-container>
<md-input-container class="md-block"> <md-input-container class="md-block">
@ -43,7 +49,7 @@
<label translate>user.description</label> <label translate>user.description</label>
<textarea ng-model="user.additionalInfo.description" rows="2"></textarea> <textarea ng-model="user.additionalInfo.description" rows="2"></textarea>
</md-input-container> </md-input-container>
<section class="tb-default-dashboard" flex layout="column"> <section class="tb-default-dashboard" flex layout="column" ng-if="user.id">
<span class="tb-default-dashboard-label" ng-class="{'tb-disabled-label': loading || !isEdit}" translate>user.default-dashboard</span> <span class="tb-default-dashboard-label" ng-class="{'tb-disabled-label': loading || !isEdit}" translate>user.default-dashboard</span>
<section flex layout="column" layout-gt-sm="row"> <section flex layout="column" layout-gt-sm="row">
<tb-dashboard-autocomplete ng-if="isTenantAdmin()" <tb-dashboard-autocomplete ng-if="isTenantAdmin()"

View File

@ -17,12 +17,13 @@
import addUserTemplate from './add-user.tpl.html'; import addUserTemplate from './add-user.tpl.html';
import userCard from './user-card.tpl.html'; import userCard from './user-card.tpl.html';
import activationLinkDialogTemplate from './activation-link.dialog.tpl.html';
/* eslint-enable import/no-unresolved, import/default */ /* eslint-enable import/no-unresolved, import/default */
/*@ngInject*/ /*@ngInject*/
export default function UserController(userService, toast, $scope, $controller, $state, $stateParams, $translate, types) { export default function UserController(userService, toast, $scope, $mdDialog, $document, $controller, $state, $stateParams, $translate, types) {
var tenantId = $stateParams.tenantId; var tenantId = $stateParams.tenantId;
var customerId = $stateParams.customerId; var customerId = $stateParams.customerId;
@ -58,6 +59,7 @@ export default function UserController(userService, toast, $scope, $controller,
onGridInited: gridInited, onGridInited: gridInited,
addItemTemplateUrl: addUserTemplate, addItemTemplateUrl: addUserTemplate,
addItemController: 'AddUserController',
addItemText: function() { return $translate.instant('user.add-user-text') }, addItemText: function() { return $translate.instant('user.add-user-text') },
noItemsText: function() { return $translate.instant('user.no-users-text') }, noItemsText: function() { return $translate.instant('user.no-users-text') },
@ -72,6 +74,7 @@ export default function UserController(userService, toast, $scope, $controller,
vm.userGridConfig.topIndex = $stateParams.topIndex; vm.userGridConfig.topIndex = $stateParams.topIndex;
} }
vm.displayActivationLink = displayActivationLink;
vm.resendActivation = resendActivation; vm.resendActivation = resendActivation;
initController(); initController();
@ -151,6 +154,29 @@ export default function UserController(userService, toast, $scope, $controller,
return userService.deleteUser(userId); return userService.deleteUser(userId);
} }
function displayActivationLink(event, user) {
userService.getActivationLink(user.id.id).then(
function success(activationLink) {
openActivationLinkDialog(event, activationLink);
}
);
}
function openActivationLinkDialog(event, activationLink) {
$mdDialog.show({
controller: 'ActivationLinkDialogController',
controllerAs: 'vm',
templateUrl: activationLinkDialogTemplate,
locals: {
activationLink: activationLink
},
parent: angular.element($document[0].body),
fullscreen: true,
skipHide: true,
targetEvent: event
});
}
function resendActivation(user) { function resendActivation(user) {
userService.sendActivationEmail(user.email).then(function success() { userService.sendActivationEmail(user.email).then(function success() {
toast.showSuccess($translate.instant('user.activation-email-sent-message')); toast.showSuccess($translate.instant('user.activation-email-sent-message'));

View File

@ -45,6 +45,7 @@ export default function UserDirective($compile, $templateCache/*, dashboardServi
user: '=', user: '=',
isEdit: '=', isEdit: '=',
theForm: '=', theForm: '=',
onDisplayActivationLink: '&',
onResendActivation: '&', onResendActivation: '&',
onDeleteUser: '&' onDeleteUser: '&'
} }

View File

@ -22,6 +22,7 @@
<tb-user user="vm.grid.operatingItem()" <tb-user user="vm.grid.operatingItem()"
is-edit="vm.grid.detailsConfig.isDetailsEditMode" is-edit="vm.grid.detailsConfig.isDetailsEditMode"
the-form="vm.grid.detailsForm" the-form="vm.grid.detailsForm"
on-display-activation-link="vm.displayActivationLink(event, vm.grid.detailsConfig.currentItem)"
on-resend-activation="vm.resendActivation(vm.grid.detailsConfig.currentItem)" on-resend-activation="vm.resendActivation(vm.grid.detailsConfig.currentItem)"
on-delete-user="vm.grid.deleteItem(event, vm.grid.detailsConfig.currentItem)"></tb-user> on-delete-user="vm.grid.deleteItem(event, vm.grid.detailsConfig.currentItem)"></tb-user>
</tb-grid> </tb-grid>