mirror of
https://github.com/nextcloud/passman.git
synced 2024-09-20 23:06:24 +08:00
Stared to work on revisions
This commit is contained in:
parent
292fdef6ab
commit
a77ae301f5
|
@ -262,6 +262,12 @@ class CredentialController extends ApiController {
|
|||
*/
|
||||
public function updateRevision($credential_id, $revision_id, $credential_data){
|
||||
$revision = null;
|
||||
try {
|
||||
$credential = $this->credentialService->getCredentialById($credential_id, $this->userId);
|
||||
} catch (DoesNotExistException $e) {
|
||||
return new NotFoundResponse();
|
||||
}
|
||||
|
||||
try{
|
||||
$revision = $this->credentialRevisionService->getRevision($revision_id);
|
||||
} catch(DoesNotExistException $exception){
|
||||
|
@ -269,6 +275,8 @@ class CredentialController extends ApiController {
|
|||
}
|
||||
|
||||
$revision->setCredentialData($credential_data);
|
||||
|
||||
$this->credentialRevisionService->updateRevision($revision);
|
||||
return new JSONResponse(array());
|
||||
}
|
||||
}
|
|
@ -419,12 +419,14 @@ class ShareController extends ApiController {
|
|||
* @param $credential_guid
|
||||
* @param $file_guid
|
||||
* @NoAdminRequired
|
||||
* @return JSONResponse
|
||||
* @return NotFoundResponse
|
||||
*/
|
||||
public function getFile($item_guid, $file_guid){
|
||||
try {
|
||||
$credential = $this->credentialService->getCredentialByGUID($item_guid);
|
||||
} catch (DoesNotExistException $e){
|
||||
return new JSONResponse(array());
|
||||
return new NotFoundResponse();
|
||||
}
|
||||
|
||||
$acl = $this->shareService->getACL($this->userId->getUID(), $credential->getGuid());
|
||||
|
|
|
@ -8,8 +8,8 @@
|
|||
* Controller of the passmanApp
|
||||
*/
|
||||
angular.module('passmanApp')
|
||||
.controller('RevisionCtrl', ['$scope', 'SettingsService', 'VaultService', 'CredentialService', '$location', '$routeParams', '$rootScope', 'NotificationService', '$filter',
|
||||
function ($scope, SettingsService, VaultService, CredentialService, $location, $routeParams, $rootScope, NotificationService, $filter) {
|
||||
.controller('RevisionCtrl', ['$scope', 'SettingsService', 'VaultService', 'CredentialService', '$location', '$routeParams', '$rootScope', 'NotificationService', '$filter', 'ShareService','EncryptService',
|
||||
function ($scope, SettingsService, VaultService, CredentialService, $location, $routeParams, $rootScope, NotificationService, $filter, ShareService, EncryptService) {
|
||||
|
||||
if (!SettingsService.getSetting('defaultVault') || !SettingsService.getSetting('defaultVaultPass')) {
|
||||
if (!$scope.active_vault) {
|
||||
|
@ -52,8 +52,24 @@ angular.module('passmanApp')
|
|||
}
|
||||
|
||||
$scope.selectRevision = function (revision) {
|
||||
console.log(revision, $scope.storedCredential);
|
||||
var key;
|
||||
|
||||
return;
|
||||
$scope.selectedRevision = angular.copy(revision);
|
||||
$scope.selectedRevision.credential_data = CredentialService.decryptCredential(angular.copy(revision.credential_data));
|
||||
|
||||
if(!$scope.storedCredential.hasOwnProperty('acl') && $scope.storedCredential.hasOwnProperty('shared_key')){
|
||||
key = EncryptService.decryptString(angular.copy($scope.storedCredential.shared_key));
|
||||
}
|
||||
if($scope.storedCredential.hasOwnProperty('acl')){
|
||||
key = EncryptService.decryptString(angular.copy($scope.storedCredential.acl.shared_key));
|
||||
}
|
||||
if(key){
|
||||
$scope.selectedRevision.credential_data = CredentialService.decryptCredential(angular.copy(revision.credential_data));
|
||||
} else {
|
||||
$scope.selectedRevision.credential_data = ShareService.decryptSharedCredential(angular.copy(revision.credential_data), key);
|
||||
}
|
||||
|
||||
$rootScope.$emit('app_menu', true);
|
||||
};
|
||||
|
||||
|
|
|
@ -9,7 +9,7 @@
|
|||
* This file is part of passman, licensed under AGPLv3
|
||||
*/
|
||||
angular.module('passmanApp')
|
||||
.controller('ShareCtrl', ['$scope', 'VaultService', 'CredentialService', 'SettingsService', '$location', '$routeParams', 'ShareService', 'NotificationService', 'SharingACL','EncryptService', 'FileService',
|
||||
.controller('ShareCtrl', ['$scope', 'VaultService', 'CredentialService', 'SettingsService', '$location', '$routeParams', 'ShareService', 'NotificationService', 'SharingACL', 'EncryptService', 'FileService',
|
||||
function ($scope, VaultService, CredentialService, SettingsService, $location, $routeParams, ShareService, NotificationService, SharingACL, EncryptService, FileService) {
|
||||
$scope.active_vault = VaultService.getActiveVault();
|
||||
|
||||
|
@ -49,7 +49,7 @@ angular.module('passmanApp')
|
|||
}
|
||||
var storedCredential = SettingsService.getSetting('share_credential');
|
||||
|
||||
if(!storedCredential) {
|
||||
if (!storedCredential) {
|
||||
$location.path('/vault/' + $routeParams.vault_id);
|
||||
} else {
|
||||
$scope.storedCredential = CredentialService.decryptCredential(angular.copy(storedCredential));
|
||||
|
@ -94,7 +94,7 @@ angular.module('passmanApp')
|
|||
}
|
||||
};
|
||||
|
||||
var getAcl = function() {
|
||||
var getAcl = function () {
|
||||
ShareService.getSharedCredentialACL($scope.storedCredential).then(function (aclList) {
|
||||
var _list = []
|
||||
var enc_key = ($scope.storedCredential.shared_key) ? EncryptService.decryptString(angular.copy($scope.storedCredential.shared_key)) : false;
|
||||
|
@ -138,7 +138,7 @@ angular.module('passmanApp')
|
|||
|
||||
$scope.$watch('share_settings.upload_progress.done', function () {
|
||||
console.log();
|
||||
if($scope.share_settings.upload_progress.done == $scope.share_settings.upload_progress.total){
|
||||
if ($scope.share_settings.upload_progress.done == $scope.share_settings.upload_progress.total) {
|
||||
getAcl()
|
||||
}
|
||||
});
|
||||
|
@ -150,11 +150,11 @@ angular.module('passmanApp')
|
|||
return ShareService.search($query)
|
||||
};
|
||||
|
||||
$scope.hasPermission = function(acl, permission){
|
||||
$scope.hasPermission = function (acl, permission) {
|
||||
return acl.hasPermission(permission);
|
||||
};
|
||||
|
||||
$scope.setPermission = function(acl, permission){
|
||||
$scope.setPermission = function (acl, permission) {
|
||||
acl.togglePermission(permission);
|
||||
};
|
||||
$scope.shareWith = function (shareWith, selectedAccessLevel) {
|
||||
|
@ -185,8 +185,8 @@ angular.module('passmanApp')
|
|||
NotificationService.showNotification('Credential unshared', 4000)
|
||||
});
|
||||
|
||||
for(var f = 0; f < $scope.storedCredential.files.length; f++){
|
||||
var _file = $scope.storedCredential.files[f];
|
||||
for (var f = 0; f < $scope.storedCredential.files.length; f++) {
|
||||
var _file = $scope.storedCredential.files[f];
|
||||
FileService.getFile(_file).then(function (fileData) {
|
||||
//Decrypt with old key
|
||||
fileData.filename = EncryptService.decryptString(fileData.filename);
|
||||
|
@ -220,7 +220,7 @@ angular.module('passmanApp')
|
|||
user: data[0].user_id
|
||||
});
|
||||
user.vaults = result;
|
||||
if(!user.hasOwnProperty('acl_id')) {
|
||||
if (!user.hasOwnProperty('acl_id')) {
|
||||
$scope.uploadChanges(user);
|
||||
}
|
||||
$scope.$digest();
|
||||
|
@ -238,11 +238,11 @@ angular.module('passmanApp')
|
|||
$scope.share_settings.upload_progress.done = 0;
|
||||
$scope.share_settings.upload_progress.total = 0;
|
||||
//Credential is already shared
|
||||
if($scope.storedCredential.shared_key && $scope.storedCredential.shared_key != '' && $scope.storedCredential.shared_key != null){
|
||||
if ($scope.storedCredential.shared_key && $scope.storedCredential.shared_key != '' && $scope.storedCredential.shared_key != null) {
|
||||
console.log('Shared key found');
|
||||
var enc_key = EncryptService.decryptString(angular.copy($scope.storedCredential.shared_key));
|
||||
if($scope.share_settings.linkSharing.enabled){
|
||||
var expire_time = new Date(angular.copy( $scope.share_settings.linkSharing.settings.expire_time)).getTime()/1000;
|
||||
if ($scope.share_settings.linkSharing.enabled) {
|
||||
var expire_time = new Date(angular.copy($scope.share_settings.linkSharing.settings.expire_time)).getTime() / 1000;
|
||||
var shareObj = {
|
||||
item_id: $scope.storedCredential.credential_id,
|
||||
item_guid: $scope.storedCredential.guid,
|
||||
|
@ -251,7 +251,7 @@ angular.module('passmanApp')
|
|||
expire_views: $scope.share_settings.linkSharing.settings.expire_views
|
||||
};
|
||||
ShareService.createPublicSharedCredential(shareObj).then(function () {
|
||||
var hash = window.btoa($scope.storedCredential.guid + '<::>'+ enc_key)
|
||||
var hash = window.btoa($scope.storedCredential.guid + '<::>' + enc_key)
|
||||
$scope.share_link = $location.$$protocol + '://' + $location.$$host + OC.generateUrl('apps/passman/share/public#') + hash;
|
||||
})
|
||||
}
|
||||
|
@ -261,7 +261,7 @@ angular.module('passmanApp')
|
|||
for (var i = 0; i < list.length; i++) {
|
||||
var iterator = i;
|
||||
var target_user = list[i];
|
||||
if(target_user.hasOwnProperty('created')){
|
||||
if (target_user.hasOwnProperty('created')) {
|
||||
console.log('Updating permissions')
|
||||
|
||||
var acl = {
|
||||
|
@ -279,7 +279,7 @@ angular.module('passmanApp')
|
|||
|
||||
ShareService.generateSharedKey(20).then(function (key) {
|
||||
var encryptedSharedCredential = ShareService.encryptSharedCredential($scope.storedCredential, key);
|
||||
CredentialService.updateCredential(encryptedSharedCredential, true).then(function(sharedCredential){
|
||||
CredentialService.updateCredential(encryptedSharedCredential, true).then(function (sharedCredential) {
|
||||
$scope.storedCredential = ShareService.decryptSharedCredential(sharedCredential, key);
|
||||
});
|
||||
|
||||
|
@ -289,8 +289,8 @@ angular.module('passmanApp')
|
|||
// Then decrypt the data obtained with var EncryptService.decryptString(result.file_data);
|
||||
// To update a file you can use the FileService.updateFile
|
||||
|
||||
for(var f = 0; f < $scope.storedCredential.files.length; f++){
|
||||
var _file = $scope.storedCredential.files[f];
|
||||
for (var f = 0; f < $scope.storedCredential.files.length; f++) {
|
||||
var _file = $scope.storedCredential.files[f];
|
||||
FileService.getFile(_file).then(function (fileData) {
|
||||
//Decrypt with old key
|
||||
fileData.filename = EncryptService.decryptString(fileData.filename);
|
||||
|
@ -299,6 +299,16 @@ angular.module('passmanApp')
|
|||
})
|
||||
}
|
||||
|
||||
CredentialService.getRevisions($scope.storedCredential.credential_id).then(function (revisions) {
|
||||
console.log(revisions);
|
||||
for (var r = 0; r < revisions.length; r++) {
|
||||
var _revision = revisions[r];
|
||||
//Decrypt!
|
||||
_revision.credential_data = CredentialService.decryptCredential(_revision.credential_data);
|
||||
_revision.credential_data = ShareService.encryptSharedCredential(_revision.credential_data, key);
|
||||
CredentialService.updateRevision(_revision);
|
||||
}
|
||||
});
|
||||
|
||||
//@TODO Update revisions with new key (async)
|
||||
// With CredentialService.getRevisions we can get the revisions.
|
||||
|
@ -311,8 +321,8 @@ angular.module('passmanApp')
|
|||
}
|
||||
}
|
||||
|
||||
if($scope.share_settings.linkSharing.enabled){
|
||||
var expire_time = new Date(angular.copy( $scope.share_settings.linkSharing.settings.expire_time)).getTime()/1000;
|
||||
if ($scope.share_settings.linkSharing.enabled) {
|
||||
var expire_time = new Date(angular.copy($scope.share_settings.linkSharing.settings.expire_time)).getTime() / 1000;
|
||||
var shareObj = {
|
||||
item_id: $scope.storedCredential.credential_id,
|
||||
item_guid: $scope.storedCredential.guid,
|
||||
|
@ -320,8 +330,8 @@ angular.module('passmanApp')
|
|||
expire_timestamp: expire_time,
|
||||
expire_views: $scope.share_settings.linkSharing.settings.expire_views
|
||||
};
|
||||
ShareService.createPublicSharedCredential(shareObj).then(function(){
|
||||
var hash = window.btoa($scope.storedCredential.guid + '<::>'+ key);
|
||||
ShareService.createPublicSharedCredential(shareObj).then(function () {
|
||||
var hash = window.btoa($scope.storedCredential.guid + '<::>' + key);
|
||||
$scope.share_link = $location.$$protocol + '://' + $location.$$host + OC.generateUrl('apps/passman/share/public#') + hash;
|
||||
|
||||
});
|
||||
|
@ -332,14 +342,14 @@ angular.module('passmanApp')
|
|||
};
|
||||
|
||||
$scope.uploadChanges = function (user) {
|
||||
$scope.share_settings.upload_progress.total ++;
|
||||
$scope.share_settings.upload_progress.total++;
|
||||
|
||||
user.accessLevel = angular.copy(user.acl.getAccessLevel());
|
||||
ShareService.shareWithUser(storedCredential, user)
|
||||
.then(function(data){
|
||||
$scope.share_settings.upload_progress.done ++;
|
||||
$scope.share_settings.upload_progress.percent = $scope.share_settings.upload_progress.done / $scope.share_settings.upload_progress.total * 100;
|
||||
});
|
||||
.then(function (data) {
|
||||
$scope.share_settings.upload_progress.done++;
|
||||
$scope.share_settings.upload_progress.percent = $scope.share_settings.upload_progress.done / $scope.share_settings.upload_progress.total * 100;
|
||||
});
|
||||
};
|
||||
|
||||
$scope.calculate_total_time = function () {
|
||||
|
|
|
@ -141,9 +141,9 @@ angular.module('passmanApp')
|
|||
},
|
||||
updateRevision: function(revision){
|
||||
var _revision = angular.copy(revision);
|
||||
_revision.revision_data = window.btoa(_revision.revision_data);
|
||||
var queryUrl = OC.generateUrl('apps/passman/api/v2/credentials/' + id + '/revision/' + revision.id);
|
||||
return $http.patch(queryUrl, revision).then(function (response) {
|
||||
_revision.credential_data = window.btoa(JSON.stringify(_revision.credential_data));
|
||||
var queryUrl = OC.generateUrl('apps/passman/api/v2/credentials/' + revision.credential_data.credential_id + '/revision/' + revision.revision_id);
|
||||
return $http.patch(queryUrl, _revision).then(function (response) {
|
||||
if (response.data) {
|
||||
return response.data;
|
||||
} else {
|
||||
|
|
|
@ -3,7 +3,7 @@ angular.module('templates-main', ['views/credential_revisions.html', 'views/edit
|
|||
angular.module('views/credential_revisions.html', []).run(['$templateCache', function($templateCache) {
|
||||
'use strict';
|
||||
$templateCache.put('views/credential_revisions.html',
|
||||
'<div id="controls"><div class="actions creatable"><div class="breadcrumb"><div class="crumb svg ui-droppable" data-dir="/"><a ng-click="logout()"><i class="fa fa-home"></i></a></div><div class="crumb svg" data-dir="/Test"><a ng-click="cancel()">{{active_vault.name}}</a></div><div class="crumb svg last" data-dir="/Test"><a ng-if="storedCredential.credential_id">Showing revisions of "{{storedCredential.label}}"</a></div></div></div></div><div off-click="closeSelected()"><table class="credential-table" ng-init="menuOpen = false;"><tr ng-repeat="revision in revisions | orderBy:\'-created\'" ng-click="selectRevision(revision)" ng-class="{\'selected\': selectedRevision.revision_id == revision.revision_id}"><td><span class="icon"><i class="fa fa-lock"></i></span> <span class="label">Revision of {{revision.created * 1000 | date:\'dd-MM-yyyy @ HH:mm:ss\'}} <span ng-if="revision.edited_by">by {{revision.edited_by}}</span></span></td></tr><tr ng-show="revisions.length == 0"><td>No revisions found.</td></tr></table><div id="app-sidebar" class="detailsView scroll-container app_sidebar" ng-show="selectedRevision"><span class="close icon-close" ng-click="closeSelected()" alt="Close"></span> <b ng-show="selectedRevision">Revision of {{selectedRevision.created * 1000 | date:\'dd-MM-yyyy @ HH:mm:ss\'}}</b><table class="revision-details"><tr ng-show="selectedRevision.credential_data.label"><td>Label</td><td>{{selectedRevision.credential_data.label}}</td></tr><tr ng-show="selectedRevision.credential_data.username"><td>Account</td><td><span credential-field value="selectedRevision.credential_data.username"></span></td></tr><tr ng-show="selectedRevision.credential_data.password"><td>Password</td><td><span credential-field value="selectedRevision.credential_data.password" secret="\'true\'"></span></td></tr><tr ng-show="selectedRevision.credential_data.otp.secret"><td>OTP</td><td><span otp-generator secret="selectedRevision.credential_data.otp.secret"></span></td></tr><tr ng-show="selectedRevision.credential_data.email"><td>E-mail</td><td><span credential-field value="selectedRevision.credential_data.email"></span></td></tr><tr ng-show="selectedRevision.credential_data.url"><td>URL</td><td><span credential-field value="selectedRevision.url"></span></td></tr><tr ng-show="selectedRevision.credential_data.files.length > 0"><td>Files</td><td><div ng-repeat="file in selectedRevision.credential_data.files" class="link" ng-click="downloadFile(file)">{{file.filename}} ({{file.size | bytes}})</div></td></tr><tr ng-repeat="field in selectedRevision.credential_data.custom_fields"><td>{{field.label}}</td><td><span credential-field value="field.value" secret="field.secret"></span></td></tr><tr ng-show="selectedRevision.credential_data.changed"><td>Changed</td><td>{{selectedRevision.credential_data.changed * 1000 | date:\'dd-MM-yyyy @ HH:mm:ss\'}}</td></tr><tr ng-show="selectedRevision.credential_data.created"><td>Created</td><td>{{selectedRevision.credential_data.created * 1000 | date:\'dd-MM-yyyy @ HH:mm:ss\'}}</td></tr></table><div ng-show="selectedRevision"><button class="button" ng-click="restoreRevision(selectedRevision)"><span class="fa fa-edit"></span> Restore revision</button> <button class="button" ng-click="deleteRevision(selectedRevision)"><span class="fa fa-trash"></span> Delete revision</button></div></div></div>');
|
||||
'<div id="controls"><div class="actions creatable"><div class="breadcrumb"><div class="crumb svg ui-droppable" data-dir="/"><a ng-click="logout()"><i class="fa fa-home"></i></a></div><div class="crumb svg" data-dir="/Test"><a ng-click="cancel()">{{active_vault.name}}</a></div><div class="crumb svg last" data-dir="/Test"><a ng-if="storedCredential.credential_id">Showing revisions of "{{storedCredential.label}}"</a></div></div></div></div><div off-click="closeSelected()"><table class="credential-table" ng-init="menuOpen = false;"><tr ng-repeat="revision in revisions | orderBy:\'-created\'" ng-click="selectRevision(revision)" ng-class="{\'selected\': selectedRevision.revision_id == revision.revision_id}"><td><span class="icon"><i class="fa fa-lock"></i></span> <span class="label">Revision of {{revision.created * 1000 | date:\'dd-MM-yyyy @ HH:mm:ss\'}} ({{revision.credential_data.label}}) <span ng-if="revision.edited_by">by {{revision.edited_by}}</span></span></td></tr><tr ng-show="revisions.length == 0"><td>No revisions found.</td></tr></table><div id="app-sidebar" class="detailsView scroll-container app_sidebar" ng-show="selectedRevision"><span class="close icon-close" ng-click="closeSelected()" alt="Close"></span> <b ng-show="selectedRevision">Revision of {{selectedRevision.created * 1000 | date:\'dd-MM-yyyy @ HH:mm:ss\'}}</b><table class="revision-details"><tr ng-show="selectedRevision.credential_data.label"><td>Label</td><td>{{selectedRevision.credential_data.label}}</td></tr><tr ng-show="selectedRevision.credential_data.username"><td>Account</td><td><span credential-field value="selectedRevision.credential_data.username"></span></td></tr><tr ng-show="selectedRevision.credential_data.password"><td>Password</td><td><span credential-field value="selectedRevision.credential_data.password" secret="\'true\'"></span></td></tr><tr ng-show="selectedRevision.credential_data.otp.secret"><td>OTP</td><td><span otp-generator secret="selectedRevision.credential_data.otp.secret"></span></td></tr><tr ng-show="selectedRevision.credential_data.email"><td>E-mail</td><td><span credential-field value="selectedRevision.credential_data.email"></span></td></tr><tr ng-show="selectedRevision.credential_data.url"><td>URL</td><td><span credential-field value="selectedRevision.url"></span></td></tr><tr ng-show="selectedRevision.credential_data.files.length > 0"><td>Files</td><td><div ng-repeat="file in selectedRevision.credential_data.files" class="link" ng-click="downloadFile(file)">{{file.filename}} ({{file.size | bytes}})</div></td></tr><tr ng-repeat="field in selectedRevision.credential_data.custom_fields"><td>{{field.label}}</td><td><span credential-field value="field.value" secret="field.secret"></span></td></tr><tr ng-show="selectedRevision.credential_data.changed"><td>Changed</td><td>{{selectedRevision.credential_data.changed * 1000 | date:\'dd-MM-yyyy @ HH:mm:ss\'}}</td></tr><tr ng-show="selectedRevision.credential_data.created"><td>Created</td><td>{{selectedRevision.credential_data.created * 1000 | date:\'dd-MM-yyyy @ HH:mm:ss\'}}</td></tr></table><div ng-show="selectedRevision"><button class="button" ng-click="restoreRevision(selectedRevision)"><span class="fa fa-edit"></span> Restore revision</button> <button class="button" ng-click="deleteRevision(selectedRevision)"><span class="fa fa-trash"></span> Delete revision</button></div></div></div>');
|
||||
}]);
|
||||
|
||||
angular.module('views/edit_credential.html', []).run(['$templateCache', function($templateCache) {
|
||||
|
|
|
@ -55,7 +55,7 @@ class CredentialRevision extends Entity implements \JsonSerializable {
|
|||
'revision_id' => $this->getId(),
|
||||
'guid' => $this->getGuid(),
|
||||
'created' => $this->getCreated(),
|
||||
'credential_data' => unserialize(base64_decode($this->getCredentialData())),
|
||||
'credential_data' => json_decode(base64_decode($this->getCredentialData())),
|
||||
'edited_by' => $this->getEditedBy(),
|
||||
];
|
||||
}
|
||||
|
|
|
@ -61,7 +61,7 @@ class CredentialRevisionMapper extends Mapper {
|
|||
$revision->setCreated($this->utils->getTime());
|
||||
$revision->setCredentialId($credential_id);
|
||||
$revision->setEditedBy($edited_by);
|
||||
$revision->setCredentialData(base64_encode(serialize($credential)));
|
||||
$revision->setCredentialData(base64_encode(json_encode($credential)));
|
||||
return $this->insert($revision);
|
||||
}
|
||||
|
||||
|
|
|
@ -23,7 +23,7 @@
|
|||
ng-class="{'selected': selectedRevision.revision_id == revision.revision_id}">
|
||||
<td>
|
||||
<span class="icon"><i class="fa fa-lock"></i> </span>
|
||||
<span class="label">Revision of {{revision.created * 1000 | date:'dd-MM-yyyy @ HH:mm:ss'}} <span ng-if="revision.edited_by">by {{revision.edited_by}}</span></span>
|
||||
<span class="label">Revision of {{revision.created * 1000 | date:'dd-MM-yyyy @ HH:mm:ss'}} ({{revision.credential_data.label}}) <span ng-if="revision.edited_by">by {{revision.edited_by}}</span></span>
|
||||
</td>
|
||||
</tr>
|
||||
<tr ng-show="revisions.length == 0">
|
||||
|
|
Loading…
Reference in a new issue