#159: display and validate route steps

This commit is contained in:
Benjamin Gamard 2018-02-02 12:37:56 +01:00
parent 8a854bb37d
commit 5b8cd18128
20 changed files with 292 additions and 50 deletions

View File

@ -1,6 +1,10 @@
package com.sismics.docs.core.dao.jpa.dto; package com.sismics.docs.core.dao.jpa.dto;
import com.sismics.docs.core.constant.RouteStepType; import com.sismics.docs.core.constant.RouteStepType;
import com.sismics.util.JsonUtil;
import javax.json.Json;
import javax.json.JsonObjectBuilder;
/** /**
* Route step DTO. * Route step DTO.
@ -147,4 +151,23 @@ public class RouteStepDto {
this.validatorUserName = validatorUserName; this.validatorUserName = validatorUserName;
return this; return this;
} }
/**
* Transform in JSON.
*
* @return JSON object builder
*/
public JsonObjectBuilder toJson() {
return Json.createObjectBuilder()
.add("name", getName())
.add("type", getType().name())
.add("comment", JsonUtil.nullable(getComment()))
.add("end_date", JsonUtil.nullable(getEndDateTimestamp()))
.add("validator_username", JsonUtil.nullable(getValidatorUserName()))
.add("target", Json.createObjectBuilder()
.add("id", getTargetId())
.add("name", JsonUtil.nullable(getTargetName()))
.add("type", getTargetType()))
.add("transition", JsonUtil.nullable(getTransition()));
}
} }

View File

@ -1,4 +1,4 @@
package com.sismics.rest.util; package com.sismics.util;
import javax.json.Json; import javax.json.Json;
import javax.json.JsonValue; import javax.json.JsonValue;

View File

@ -4,6 +4,7 @@ import com.sismics.docs.core.constant.AclType;
import com.sismics.docs.core.constant.PermType; import com.sismics.docs.core.constant.PermType;
import com.sismics.docs.core.dao.jpa.AclDao; import com.sismics.docs.core.dao.jpa.AclDao;
import com.sismics.docs.core.dao.jpa.dto.AclDto; import com.sismics.docs.core.dao.jpa.dto.AclDto;
import com.sismics.util.JsonUtil;
import javax.json.Json; import javax.json.Json;
import javax.json.JsonArrayBuilder; import javax.json.JsonArrayBuilder;

View File

@ -10,7 +10,7 @@ import com.sismics.docs.core.util.jpa.PaginatedList;
import com.sismics.docs.core.util.jpa.PaginatedLists; import com.sismics.docs.core.util.jpa.PaginatedLists;
import com.sismics.docs.core.util.jpa.SortCriteria; import com.sismics.docs.core.util.jpa.SortCriteria;
import com.sismics.rest.exception.ForbiddenClientException; import com.sismics.rest.exception.ForbiddenClientException;
import com.sismics.rest.util.JsonUtil; import com.sismics.util.JsonUtil;
import javax.json.Json; import javax.json.Json;
import javax.json.JsonArrayBuilder; import javax.json.JsonArrayBuilder;

View File

@ -25,8 +25,8 @@ import com.sismics.rest.exception.ClientException;
import com.sismics.rest.exception.ForbiddenClientException; import com.sismics.rest.exception.ForbiddenClientException;
import com.sismics.rest.exception.ServerException; import com.sismics.rest.exception.ServerException;
import com.sismics.rest.util.AclUtil; import com.sismics.rest.util.AclUtil;
import com.sismics.rest.util.JsonUtil;
import com.sismics.rest.util.ValidationUtil; import com.sismics.rest.util.ValidationUtil;
import com.sismics.util.JsonUtil;
import com.sismics.util.context.ThreadLocalContext; import com.sismics.util.context.ThreadLocalContext;
import com.sismics.util.mime.MimeType; import com.sismics.util.mime.MimeType;
import org.joda.time.DateTime; import org.joda.time.DateTime;
@ -216,10 +216,9 @@ public class DocumentResource extends BaseResource {
// Add current route step // Add current route step
RouteStepDto routeStepDto = new RouteStepDao().getCurrentStep(documentId); RouteStepDto routeStepDto = new RouteStepDao().getCurrentStep(documentId);
if (routeStepDto != null && !principal.isAnonymous()) { if (routeStepDto != null && !principal.isAnonymous()) {
document.add("route_step", Json.createObjectBuilder() JsonObjectBuilder step = routeStepDto.toJson();
.add("name", routeStepDto.getName()) step.add("transitionable", getTargetIdList(null).contains(routeStepDto.getTargetId()));
.add("type", routeStepDto.getType().name()) document.add("route_step", step);
.add("transitionable", getTargetIdList(null).contains(routeStepDto.getTargetId())));
} }
return Response.ok().entity(document.build()).build(); return Response.ok().entity(document.build()).build();

View File

@ -22,9 +22,9 @@ import com.sismics.docs.core.util.PdfUtil;
import com.sismics.rest.exception.ClientException; import com.sismics.rest.exception.ClientException;
import com.sismics.rest.exception.ForbiddenClientException; import com.sismics.rest.exception.ForbiddenClientException;
import com.sismics.rest.exception.ServerException; import com.sismics.rest.exception.ServerException;
import com.sismics.rest.util.JsonUtil;
import com.sismics.rest.util.ValidationUtil; import com.sismics.rest.util.ValidationUtil;
import com.sismics.util.HttpUtil; import com.sismics.util.HttpUtil;
import com.sismics.util.JsonUtil;
import com.sismics.util.context.ThreadLocalContext; import com.sismics.util.context.ThreadLocalContext;
import com.sismics.util.mime.MimeType; import com.sismics.util.mime.MimeType;
import com.sismics.util.mime.MimeTypeUtil; import com.sismics.util.mime.MimeTypeUtil;

View File

@ -14,8 +14,8 @@ import com.sismics.docs.core.util.jpa.SortCriteria;
import com.sismics.docs.rest.constant.BaseFunction; import com.sismics.docs.rest.constant.BaseFunction;
import com.sismics.rest.exception.ClientException; import com.sismics.rest.exception.ClientException;
import com.sismics.rest.exception.ForbiddenClientException; import com.sismics.rest.exception.ForbiddenClientException;
import com.sismics.rest.util.JsonUtil;
import com.sismics.rest.util.ValidationUtil; import com.sismics.rest.util.ValidationUtil;
import com.sismics.util.JsonUtil;
import javax.json.Json; import javax.json.Json;
import javax.json.JsonArrayBuilder; import javax.json.JsonArrayBuilder;

View File

@ -18,7 +18,6 @@ import com.sismics.docs.core.util.SecurityUtil;
import com.sismics.docs.core.util.jpa.SortCriteria; import com.sismics.docs.core.util.jpa.SortCriteria;
import com.sismics.rest.exception.ClientException; import com.sismics.rest.exception.ClientException;
import com.sismics.rest.exception.ForbiddenClientException; import com.sismics.rest.exception.ForbiddenClientException;
import com.sismics.rest.util.JsonUtil;
import com.sismics.rest.util.ValidationUtil; import com.sismics.rest.util.ValidationUtil;
import javax.json.*; import javax.json.*;
@ -44,6 +43,7 @@ public class RouteResource extends BaseResource {
* @apiParam {String} documentId Document ID * @apiParam {String} documentId Document ID
* @apiSuccess {String} status Status OK * @apiSuccess {String} status Status OK
* @apiError (client) InvalidRouteModel Invalid route model * @apiError (client) InvalidRouteModel Invalid route model
* @apiError (client) RunningRoute A running route already exists on this document
* @apiError (client) ForbiddenError Access denied * @apiError (client) ForbiddenError Access denied
* @apiError (client) NotFound Route model or document not found * @apiError (client) NotFound Route model or document not found
* @apiPermission user * @apiPermission user
@ -72,6 +72,12 @@ public class RouteResource extends BaseResource {
throw new NotFoundException(); throw new NotFoundException();
} }
// Avoid creating 2 running routes on the same document
RouteStepDao routeStepDao = new RouteStepDao();
if (routeStepDao.getCurrentStep(documentId) != null) {
throw new ClientException("RunningRoute", "A running route already exists on this document");
}
// Create the route // Create the route
Route route = new Route() Route route = new Route()
.setDocumentId(documentId) .setDocumentId(documentId)
@ -80,7 +86,6 @@ public class RouteResource extends BaseResource {
routeDao.create(route, principal.getId()); routeDao.create(route, principal.getId());
// Create the steps // Create the steps
RouteStepDao routeStepDao = new RouteStepDao();
try (JsonReader reader = Json.createReader(new StringReader(routeModel.getSteps()))) { try (JsonReader reader = Json.createReader(new StringReader(routeModel.getSteps()))) {
JsonArray stepsJson = reader.readArray(); JsonArray stepsJson = reader.readArray();
for (int order = 0; order < stepsJson.size(); order++) { for (int order = 0; order < stepsJson.size(); order++) {
@ -108,9 +113,8 @@ public class RouteResource extends BaseResource {
RouteStepDto routeStep = routeStepDao.getCurrentStep(documentId); RouteStepDto routeStep = routeStepDao.getCurrentStep(documentId);
RoutingUtil.updateAcl(documentId, routeStep, null, principal.getId()); RoutingUtil.updateAcl(documentId, routeStep, null, principal.getId());
// Always return OK
JsonObjectBuilder response = Json.createObjectBuilder() JsonObjectBuilder response = Json.createObjectBuilder()
.add("status", "ok"); .add("route_step", routeStep.toJson());
return Response.ok().entity(response.build()).build(); return Response.ok().entity(response.build()).build();
} }
@ -173,10 +177,13 @@ public class RouteResource extends BaseResource {
RoutingUtil.updateAcl(documentId, newRouteStep, routeStep, principal.getId()); RoutingUtil.updateAcl(documentId, newRouteStep, routeStep, principal.getId());
// TODO Send an email to the new route step // TODO Send an email to the new route step
// Always return OK
// TODO Return if the document is still readable and return the new current step if any
JsonObjectBuilder response = Json.createObjectBuilder() JsonObjectBuilder response = Json.createObjectBuilder()
.add("status", "ok"); .add("readable", aclDao.checkPermission(documentId, PermType.READ, getTargetIdList(null)));
if (newRouteStep != null) {
JsonObjectBuilder step = newRouteStep.toJson();
step.add("transitionable", getTargetIdList(null).contains(newRouteStep.getTargetId()));
response.add("route_step", step);
}
return Response.ok().entity(response.build()).build(); return Response.ok().entity(response.build()).build();
} }
@ -232,17 +239,7 @@ public class RouteResource extends BaseResource {
JsonArrayBuilder steps = Json.createArrayBuilder(); JsonArrayBuilder steps = Json.createArrayBuilder();
for (RouteStepDto routeStepDto : routeStepDtoList) { for (RouteStepDto routeStepDto : routeStepDtoList) {
steps.add(Json.createObjectBuilder() steps.add(routeStepDto.toJson());
.add("name", routeStepDto.getName())
.add("type", routeStepDto.getType().name())
.add("comment", JsonUtil.nullable(routeStepDto.getComment()))
.add("end_date", JsonUtil.nullable(routeStepDto.getEndDateTimestamp()))
.add("validator_username", JsonUtil.nullable(routeStepDto.getValidatorUserName()))
.add("target", Json.createObjectBuilder()
.add("id", routeStepDto.getTargetId())
.add("name", JsonUtil.nullable(routeStepDto.getTargetName()))
.add("type", routeStepDto.getTargetType()))
.add("transition", JsonUtil.nullable(routeStepDto.getTransition())));
} }
routes.add(Json.createObjectBuilder() routes.add(Json.createObjectBuilder()
@ -253,7 +250,8 @@ public class RouteResource extends BaseResource {
JsonObjectBuilder json = Json.createObjectBuilder() JsonObjectBuilder json = Json.createObjectBuilder()
.add("routes", routes); .add("routes", routes);
return Response.ok().entity(json.build()).build(); return Response.ok().entity(json.build()).build();
} }
// TODO Workflow cancellation
} }

View File

@ -10,8 +10,8 @@ import com.sismics.docs.core.model.jpa.Acl;
import com.sismics.docs.core.model.jpa.Share; import com.sismics.docs.core.model.jpa.Share;
import com.sismics.rest.exception.ClientException; import com.sismics.rest.exception.ClientException;
import com.sismics.rest.exception.ForbiddenClientException; import com.sismics.rest.exception.ForbiddenClientException;
import com.sismics.rest.util.JsonUtil;
import com.sismics.rest.util.ValidationUtil; import com.sismics.rest.util.ValidationUtil;
import com.sismics.util.JsonUtil;
import javax.json.Json; import javax.json.Json;
import javax.json.JsonObjectBuilder; import javax.json.JsonObjectBuilder;

View File

@ -10,9 +10,9 @@ import com.sismics.docs.rest.constant.BaseFunction;
import com.sismics.rest.exception.ClientException; import com.sismics.rest.exception.ClientException;
import com.sismics.rest.exception.ForbiddenClientException; import com.sismics.rest.exception.ForbiddenClientException;
import com.sismics.rest.exception.ServerException; import com.sismics.rest.exception.ServerException;
import com.sismics.rest.util.JsonUtil;
import com.sismics.rest.util.ValidationUtil; import com.sismics.rest.util.ValidationUtil;
import com.sismics.util.HttpUtil; import com.sismics.util.HttpUtil;
import com.sismics.util.JsonUtil;
import com.sismics.util.css.Selector; import com.sismics.util.css.Selector;
import org.glassfish.jersey.media.multipart.FormDataBodyPart; import org.glassfish.jersey.media.multipart.FormDataBodyPart;
import org.glassfish.jersey.media.multipart.FormDataParam; import org.glassfish.jersey.media.multipart.FormDataParam;

View File

@ -21,9 +21,9 @@ import com.sismics.docs.rest.constant.BaseFunction;
import com.sismics.rest.exception.ClientException; import com.sismics.rest.exception.ClientException;
import com.sismics.rest.exception.ForbiddenClientException; import com.sismics.rest.exception.ForbiddenClientException;
import com.sismics.rest.exception.ServerException; import com.sismics.rest.exception.ServerException;
import com.sismics.rest.util.JsonUtil;
import com.sismics.rest.util.ValidationUtil; import com.sismics.rest.util.ValidationUtil;
import com.sismics.security.UserPrincipal; import com.sismics.security.UserPrincipal;
import com.sismics.util.JsonUtil;
import com.sismics.util.context.ThreadLocalContext; import com.sismics.util.context.ThreadLocalContext;
import com.sismics.util.filter.TokenBasedSecurityFilter; import com.sismics.util.filter.TokenBasedSecurityFilter;
import com.sismics.util.totp.GoogleAuthenticator; import com.sismics.util.totp.GoogleAuthenticator;

View File

@ -285,6 +285,15 @@ angular.module('docs',
} }
} }
}) })
.state('document.view.workflow', {
url: '/workflow',
views: {
'tab': {
templateUrl: 'partial/docs/document.view.workflow.html',
controller: 'DocumentViewWorkflow'
}
}
})
.state('document.view.content.file', { .state('document.view.content.file', {
url: '/file/:fileId', url: '/file/:fileId',
views: { views: {

View File

@ -5,16 +5,16 @@
*/ */
angular.module('docs').controller('DocumentView', function ($scope, $state, $stateParams, $location, $dialog, $uibModal, Restangular, $translate) { angular.module('docs').controller('DocumentView', function ($scope, $state, $stateParams, $location, $dialog, $uibModal, Restangular, $translate) {
// Load document data from server // Load document data from server
Restangular.one('document', $stateParams.id).get().then(function(data) { Restangular.one('document', $stateParams.id).get().then(function (data) {
$scope.document = data; $scope.document = data;
}, function(response) { }, function (response) {
$scope.error = response; $scope.error = response;
}); });
// Load comments from server // Load comments from server
Restangular.one('comment', $stateParams.id).get().then(function(data) { Restangular.one('comment', $stateParams.id).get().then(function (data) {
$scope.comments = data.comments; $scope.comments = data.comments;
}, function(response) { }, function (response) {
$scope.commentsError = response; $scope.commentsError = response;
}); });
@ -22,7 +22,7 @@ angular.module('docs').controller('DocumentView', function ($scope, $state, $sta
* Add a comment. * Add a comment.
*/ */
$scope.comment = ''; $scope.comment = '';
$scope.addComment = function() { $scope.addComment = function () {
if ($scope.comment.length === 0) { if ($scope.comment.length === 0) {
return; return;
} }
@ -30,7 +30,7 @@ angular.module('docs').controller('DocumentView', function ($scope, $state, $sta
Restangular.one('comment').put({ Restangular.one('comment').put({
id: $stateParams.id, id: $stateParams.id,
content: $scope.comment content: $scope.comment
}).then(function(data) { }).then(function (data) {
$scope.comment = ''; $scope.comment = '';
$scope.comments.push(data); $scope.comments.push(data);
}); });
@ -39,7 +39,7 @@ angular.module('docs').controller('DocumentView', function ($scope, $state, $sta
/** /**
* Delete a comment. * Delete a comment.
*/ */
$scope.deleteComment = function(comment) { $scope.deleteComment = function (comment) {
var title = $translate.instant('document.view.delete_comment_title'); var title = $translate.instant('document.view.delete_comment_title');
var msg = $translate.instant('document.view.delete_comment_message'); var msg = $translate.instant('document.view.delete_comment_message');
var btns = [ var btns = [
@ -49,7 +49,7 @@ angular.module('docs').controller('DocumentView', function ($scope, $state, $sta
$dialog.messageBox(title, msg, btns, function (result) { $dialog.messageBox(title, msg, btns, function (result) {
if (result === 'ok') { if (result === 'ok') {
Restangular.one('comment', comment.id).remove().then(function() { Restangular.one('comment', comment.id).remove().then(function () {
$scope.comments.splice($scope.comments.indexOf(comment), 1); $scope.comments.splice($scope.comments.indexOf(comment), 1);
}); });
} }
@ -69,7 +69,7 @@ angular.module('docs').controller('DocumentView', function ($scope, $state, $sta
$dialog.messageBox(title, msg, btns, function (result) { $dialog.messageBox(title, msg, btns, function (result) {
if (result === 'ok') { if (result === 'ok') {
Restangular.one('document', document.id).remove().then(function() { Restangular.one('document', document.id).remove().then(function () {
$scope.loadDocuments(); $scope.loadDocuments();
$state.go('document.default'); $state.go('document.default');
}); });
@ -105,7 +105,7 @@ angular.module('docs').controller('DocumentView', function ($scope, $state, $sta
/** /**
* Display a share. * Display a share.
*/ */
$scope.showShare = function(share) { $scope.showShare = function (share) {
// Show the link // Show the link
var link = $location.absUrl().replace($location.path(), '').replace('#', '') + 'share.html#/share/' + $stateParams.id + '/' + share.id; var link = $location.absUrl().replace($location.path(), '').replace('#', '') + 'share.html#/share/' + $stateParams.id + '/' + share.id;
var title = $translate.instant('document.view.shared_document_title'); var title = $translate.instant('document.view.shared_document_title');
@ -119,7 +119,7 @@ angular.module('docs').controller('DocumentView', function ($scope, $state, $sta
if (result === 'unshare') { if (result === 'unshare') {
// Unshare this document and update the local shares // Unshare this document and update the local shares
Restangular.one('share', share.id).remove().then(function () { Restangular.one('share', share.id).remove().then(function () {
$scope.document.acls = _.reject($scope.document.acls, function(s) { $scope.document.acls = _.reject($scope.document.acls, function (s) {
return share.id === s.id; return share.id === s.id;
}); });
}); });
@ -130,7 +130,7 @@ angular.module('docs').controller('DocumentView', function ($scope, $state, $sta
/** /**
* Export the current document to PDF. * Export the current document to PDF.
*/ */
$scope.exportPdf = function() { $scope.exportPdf = function () {
$uibModal.open({ $uibModal.open({
templateUrl: 'partial/docs/document.pdf.html', templateUrl: 'partial/docs/document.pdf.html',
controller: 'DocumentModalPdf' controller: 'DocumentModalPdf'
@ -138,4 +138,22 @@ angular.module('docs').controller('DocumentView', function ($scope, $state, $sta
return false; return false;
}; };
/**
* Validate the workflow.
*/
$scope.validateWorkflow = function (transition) {
Restangular.one('route').post('validate', {
documentId: $stateParams.id,
transition: transition,
comment: $scope.workflowComment
}).then(function (data) {
$scope.workflowComment = '';
if (data.readable) {
$scope.document.route_step = data.route_step;
} else {
$state.go('document.default');
}
});
};
}); });

View File

@ -0,0 +1,33 @@
'use strict';
/**
* Document view workflow controller.
*/
angular.module('docs').controller('DocumentViewWorkflow', function ($scope, $stateParams, Restangular) {
$scope.loadRoutes = function () {
Restangular.one('route').get({
documentId: $stateParams.id
}).then(function(data) {
$scope.routes = data.routes;
});
};
// Load route models
Restangular.one('routemodel').get().then(function(data) {
$scope.routemodels = data.routemodels;
});
// Start the selected workflow
$scope.startWorkflow = function () {
Restangular.one('route').post('start', {
routeModelId: $scope.routemodel,
documentId: $stateParams.id
}).then(function (data) {
$scope.document.route_step = data.route_step;
$scope.loadRoutes();
});
};
// Load routes
$scope.loadRoutes();
});

View File

@ -58,6 +58,7 @@
<script src="app/docs/controller/document/DocumentEdit.js" type="text/javascript"></script> <script src="app/docs/controller/document/DocumentEdit.js" type="text/javascript"></script>
<script src="app/docs/controller/document/DocumentView.js" type="text/javascript"></script> <script src="app/docs/controller/document/DocumentView.js" type="text/javascript"></script>
<script src="app/docs/controller/document/DocumentViewContent.js" type="text/javascript"></script> <script src="app/docs/controller/document/DocumentViewContent.js" type="text/javascript"></script>
<script src="app/docs/controller/document/DocumentViewWorkflow.js" type="text/javascript"></script>
<script src="app/docs/controller/document/DocumentViewPermissions.js" type="text/javascript"></script> <script src="app/docs/controller/document/DocumentViewPermissions.js" type="text/javascript"></script>
<script src="app/docs/controller/document/DocumentViewActivity.js" type="text/javascript"></script> <script src="app/docs/controller/document/DocumentViewActivity.js" type="text/javascript"></script>
<script src="app/docs/controller/document/DocumentModalShare.js" type="text/javascript"></script> <script src="app/docs/controller/document/DocumentModalShare.js" type="text/javascript"></script>

View File

@ -92,6 +92,8 @@
"no_comments": "No comments on this document yet", "no_comments": "No comments on this document yet",
"add_comment": "Add a comment", "add_comment": "Add a comment",
"error_loading_comments": "Error loading comments", "error_loading_comments": "Error loading comments",
"workflow_current": "Current workflow step",
"workflow_comment": "Add a workflow comment",
"content": { "content": {
"content": "Content", "content": "Content",
"delete_file_title": "Delete file", "delete_file_title": "Delete file",
@ -103,6 +105,14 @@
"drop_zone": "Drag & drop files here to upload", "drop_zone": "Drag & drop files here to upload",
"add_files": "Add files" "add_files": "Add files"
}, },
"workflow": {
"workflow": "Workflow",
"message": "Verify or validate your documents with people of your organization using workflows.",
"workflow_start_label": "Which workflow to start?",
"add_more_workflow": "Add more workflows",
"start_workflow_submit": "Start workflow",
"full_name": "<strong>{{ name }}</strong> started on {{ create_date | date }}"
},
"permissions": { "permissions": {
"permissions": "Permissions", "permissions": "Permissions",
"message": "Permissions can be applied directly to this document, or can come from <a href=\"#/tag\">tags</a>.", "message": "Permissions can be applied directly to this document, or can come from <a href=\"#/tag\">tags</a>.",
@ -428,6 +438,15 @@
"GROUP": "Group", "GROUP": "Group",
"SHARE": "Shared" "SHARE": "Shared"
}, },
"workflow_type": {
"VALIDATE": "Validation",
"APPROVE": "Approbation"
},
"workflow_transition": {
"APPROVED": "Approved",
"REJECTED": "Rejected",
"VALIDATED": "Validated"
},
"validation": { "validation": {
"required": "Required", "required": "Required",
"too_short": "Too short", "too_short": "Too short",

View File

@ -72,6 +72,11 @@
<span class="glyphicon glyphicon-file"></span> {{ 'document.view.content' | translate }} <span class="glyphicon glyphicon-file"></span> {{ 'document.view.content' | translate }}
</a> </a>
</li> </li>
<li ng-class="{ active: $state.current.name == 'document.view.workflow' }">
<a href="#/document/view/{{ document.id }}/workflow">
<span class="glyphicon glyphicon-random"></span> {{ 'document.view.workflow' | translate }}
</a>
</li>
<li ng-class="{ active: $state.current.name == 'document.view.permissions' }"> <li ng-class="{ active: $state.current.name == 'document.view.permissions' }">
<a href="#/document/view/{{ document.id }}/permissions"> <a href="#/document/view/{{ document.id }}/permissions">
<span class="glyphicon glyphicon-user"></span> {{ 'document.view.permissions' | translate }} <span class="glyphicon glyphicon-user"></span> {{ 'document.view.permissions' | translate }}
@ -86,7 +91,43 @@
<div ui-view="tab"></div> <div ui-view="tab"></div>
</div> </div>
<div class="col-md-3"> <div class="col-md-3">
<p class="page-header page-header-comments"> <div ng-show="document.route_step">
<p class="page-header page-header-side">
<span class="glyphicon glyphicon-random"></span>
{{ 'document.view.workflow_current' | translate }}
</p>
<div class="text-center card">
<p>{{ document.route_step.name }}</p>
<p>
<span class="glyphicon glyphicon-transfer" ng-if="document.route_step.type == 'APPROVE'"></span>
<span class="glyphicon glyphicon-ok-circle" ng-if="document.route_step.type == 'VALIDATE'"></span>
{{ 'workflow_type.' + document.route_step.type | translate }}
<span class="label label-default"><acl data="document.route_step.target"></acl></span>
</p>
<p ng-show="document.route_step.transitionable">
<textarea ng-model="workflowComment" maxlength="500" class="form-control mb-10"
ng-attr-placeholder="{{ 'document.view.workflow_comment' | translate }}"></textarea>
<span class="btn btn-primary"
ng-show="document.route_step.type == 'VALIDATE'"
ng-click="validateWorkflow('VALIDATED')">
{{ 'workflow_transition.VALIDATED' | translate }}
</span>
<span class="btn btn-success"
ng-show="document.route_step.type == 'APPROVE'"
ng-click="validateWorkflow('APPROVED')">
{{ 'workflow_transition.APPROVED' | translate }}
</span>
<span class="btn btn-danger"
ng-show="document.route_step.type == 'APPROVE'"
ng-click="validateWorkflow('REJECTED')">
{{ 'workflow_transition.REJECTED' | translate }}
</span>
</p>
</div>
</div>
<p class="page-header page-header-side">
<span class="glyphicon glyphicon-comment"></span> <span class="glyphicon glyphicon-comment"></span>
{{ 'document.view.comments' | translate }} {{ 'document.view.comments' | translate }}
</p> </p>

View File

@ -0,0 +1,72 @@
<p class="well-sm">{{ 'document.view.workflow.message' | translate }}</p>
<form name="startWorkflowForm" class="form-horizontal" novalidate ng-show="!document.route_step">
<div class="form-group" ng-class="{ 'has-error': !startWorkflowForm.routemodel.$valid && startWorkflowForm.$dirty }">
<label for="inputRouteModel" class="col-sm-3">
{{ 'document.view.workflow.workflow_start_label' | translate }}
<p ng-if="userInfo.base_functions.indexOf('ADMIN') != -1">
<a href="#/settings/workflow">{{ 'document.view.workflow.add_more_workflow' | translate }}</a>
</p>
</label>
<div class="col-sm-6">
<select required class="form-control" id="inputRouteModel" name="routemodel" ng-model="routemodel">
<option ng-repeat="routemodel in routemodels" value="{{ routemodel.id }}">{{ routemodel.name }}</option>
</select>
</div>
<div class="col-sm-3">
<span class="help-block" ng-show="startWorkflowForm.routemodel.$error.required && startWorkflowForm.$dirty">{{ 'validation.required' | translate }}</span>
</div>
</div>
<div class="form-group">
<div class="col-sm-offset-3 col-sm-9">
<button type="submit" class="btn btn-primary"
ng-disabled="!startWorkflowForm.$valid"
ng-click="startWorkflow()">
{{ 'document.view.workflow.start_workflow_submit' | translate }}
</button>
</div>
</div>
</form>
<table class="table" ng-repeat="route in routes">
<caption translate="document.view.workflow.full_name"
translate-values="{ name: route.name, create_date: route.create_date }"></caption>
<thead>
<tr>
<th>Type</th>
<th>Name</th>
<th>For</th>
<th>Validation</th>
</tr>
</thead>
<tbody>
<tr ng-repeat="step in route.steps" ng-class="{
'bg-success': step.transition == 'VALIDATED' || step.transition == 'APPROVED',
'bg-danger': step.transition == 'REJECTED'
}">
<td>
<span class="glyphicon glyphicon-transfer" ng-if="step.type == 'APPROVE'"></span>
<span class="glyphicon glyphicon-ok-circle" ng-if="step.type == 'VALIDATE'"></span>
{{ 'workflow_type.' + step.type | translate }}
</td>
<td>{{ step.name }}</td>
<td>
<acl data="step.target"></acl>
</td>
<td>
<span ng-show="step.end_date">
{{ 'workflow_transition.' + step.transition | translate }}
{{ step.end_date | timeAgo: dateTimeFormat }}
by
<a href="#/user/{{ step.validator_username }}" class="label label-default">{{ step.validator_username }}</a>
<span ng-show="step.comment" class="text-">
<br/>
<em>{{ step.comment }}</em>
</span>
</span>
</td>
</tr>
</tbody>
</table>

View File

@ -237,7 +237,7 @@ input[readonly].share-link {
} }
// Comments // Comments
.page-header-comments { .page-header-side {
margin: 14px 0 20px; margin: 14px 0 20px;
} }
@ -450,3 +450,21 @@ input[readonly].share-link {
.mb-10 { .mb-10 {
margin-bottom: 10px; margin-bottom: 10px;
} }
// Buttons
.btn {
transition: color .15s ease-in-out,background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out;
font-weight: 400;
border-radius: 4px;
}
// Cards
.card {
min-width: 0;
word-wrap: break-word;
background-color: #fff;
background-clip: border-box;
border: 1px solid rgba(0,0,0,.125);
border-radius: .25em;
padding: 1.25em;
}

View File

@ -52,11 +52,13 @@ public class TestRouteResource extends BaseJerseyTest {
String document1Id = json.getString("id"); String document1Id = json.getString("id");
// Start the default route on document 1 // Start the default route on document 1
target().path("/route/start").request() json = target().path("/route/start").request()
.cookie(TokenBasedSecurityFilter.COOKIE_NAME, route1Token) .cookie(TokenBasedSecurityFilter.COOKIE_NAME, route1Token)
.post(Entity.form(new Form() .post(Entity.form(new Form()
.param("documentId", document1Id) .param("documentId", document1Id)
.param("routeModelId", routeModels.getJsonObject(0).getString("id"))), JsonObject.class); .param("routeModelId", routeModels.getJsonObject(0).getString("id"))), JsonObject.class);
JsonObject step = json.getJsonObject("route_step");
Assert.assertEquals("Check the document's metadata", step.getString("name"));
// Get the route on document 1 // Get the route on document 1
json = target().path("/route") json = target().path("/route")
@ -71,7 +73,7 @@ public class TestRouteResource extends BaseJerseyTest {
Assert.assertNotNull(route.getJsonNumber("create_date")); Assert.assertNotNull(route.getJsonNumber("create_date"));
JsonArray steps = route.getJsonArray("steps"); JsonArray steps = route.getJsonArray("steps");
Assert.assertEquals(3, steps.size()); Assert.assertEquals(3, steps.size());
JsonObject step = steps.getJsonObject(0); step = steps.getJsonObject(0);
Assert.assertEquals("Check the document's metadata", step.getString("name")); Assert.assertEquals("Check the document's metadata", step.getString("name"));
Assert.assertEquals("VALIDATE", step.getString("type")); Assert.assertEquals("VALIDATE", step.getString("type"));
Assert.assertTrue(step.isNull("comment")); Assert.assertTrue(step.isNull("comment"));
@ -102,11 +104,14 @@ public class TestRouteResource extends BaseJerseyTest {
Assert.assertEquals("Check the document's metadata", routeStep.getString("name")); Assert.assertEquals("Check the document's metadata", routeStep.getString("name"));
// Validate the current step with admin // Validate the current step with admin
target().path("/route/validate").request() json = target().path("/route/validate").request()
.cookie(TokenBasedSecurityFilter.COOKIE_NAME, adminToken) .cookie(TokenBasedSecurityFilter.COOKIE_NAME, adminToken)
.post(Entity.form(new Form() .post(Entity.form(new Form()
.param("documentId", document1Id) .param("documentId", document1Id)
.param("transition", "VALIDATED")), JsonObject.class); .param("transition", "VALIDATED")), JsonObject.class);
step = json.getJsonObject("route_step");
Assert.assertEquals("Add relevant files to the document", step.getString("name"));
Assert.assertTrue(json.getBoolean("readable"));
// Get the route on document 1 // Get the route on document 1
json = target().path("/route") json = target().path("/route")
@ -136,12 +141,15 @@ public class TestRouteResource extends BaseJerseyTest {
Assert.assertEquals("Add relevant files to the document", routeStep.getString("name")); Assert.assertEquals("Add relevant files to the document", routeStep.getString("name"));
// Validate the current step with admin // Validate the current step with admin
target().path("/route/validate").request() json = target().path("/route/validate").request()
.cookie(TokenBasedSecurityFilter.COOKIE_NAME, adminToken) .cookie(TokenBasedSecurityFilter.COOKIE_NAME, adminToken)
.post(Entity.form(new Form() .post(Entity.form(new Form()
.param("documentId", document1Id) .param("documentId", document1Id)
.param("transition", "VALIDATED") .param("transition", "VALIDATED")
.param("comment", "OK")), JsonObject.class); .param("comment", "OK")), JsonObject.class);
step = json.getJsonObject("route_step");
Assert.assertEquals("Approve the document", step.getString("name"));
Assert.assertTrue(json.getBoolean("readable"));
// Get the route on document 1 // Get the route on document 1
json = target().path("/route") json = target().path("/route")
@ -171,11 +179,13 @@ public class TestRouteResource extends BaseJerseyTest {
Assert.assertEquals("Approve the document", routeStep.getString("name")); Assert.assertEquals("Approve the document", routeStep.getString("name"));
// Validate the current step with admin // Validate the current step with admin
target().path("/route/validate").request() json = target().path("/route/validate").request()
.cookie(TokenBasedSecurityFilter.COOKIE_NAME, adminToken) .cookie(TokenBasedSecurityFilter.COOKIE_NAME, adminToken)
.post(Entity.form(new Form() .post(Entity.form(new Form()
.param("documentId", document1Id) .param("documentId", document1Id)
.param("transition", "APPROVED")), JsonObject.class); .param("transition", "APPROVED")), JsonObject.class);
Assert.assertFalse(json.containsKey("route_step"));
Assert.assertFalse(json.getBoolean("readable"));
// Get the route on document 1 // Get the route on document 1
json = target().path("/route") json = target().path("/route")