Merge pull request #39 from sismics/master

Push to production
This commit is contained in:
Benjamin Gamard 2015-11-02 21:36:14 +01:00
commit f01d78a9ea
45 changed files with 663 additions and 412 deletions

View File

@ -24,13 +24,14 @@ Features
- Support image and PDF files
- Flexible search engine
- Full text search in image and PDF
- SHA-256 encryption
- 256-bit AES encryption
- Tag system
- Multi-users ACL system
- Audit log
- Document sharing by URL
- RESTful Web API
- Fully featured Android client
- Tested to 100k documents
Download
--------

View File

@ -12,10 +12,12 @@
<option name="SELECTED_TEST_ARTIFACT" value="_android_test_" />
<option name="ASSEMBLE_TASK_NAME" value="assembleDebug" />
<option name="COMPILE_JAVA_TASK_NAME" value="compileDebugSources" />
<option name="SOURCE_GEN_TASK_NAME" value="generateDebugSources" />
<option name="ASSEMBLE_TEST_TASK_NAME" value="assembleDebugAndroidTest" />
<option name="COMPILE_JAVA_TEST_TASK_NAME" value="compileDebugAndroidTestSources" />
<option name="TEST_SOURCE_GEN_TASK_NAME" value="generateDebugAndroidTestSources" />
<afterSyncTasks>
<task>generateDebugAndroidTestSources</task>
<task>generateDebugSources</task>
</afterSyncTasks>
<option name="ALLOW_USER_CONFIGURATION" value="false" />
<option name="MANIFEST_FILE_RELATIVE_PATH" value="/src/main/AndroidManifest.xml" />
<option name="RES_FOLDER_RELATIVE_PATH" value="/src/main/res" />
@ -24,7 +26,7 @@
</configuration>
</facet>
</component>
<component name="NewModuleRootManager" inherit-compiler-output="false">
<component name="NewModuleRootManager" LANGUAGE_LEVEL="JDK_1_7" inherit-compiler-output="false">
<output url="file://$MODULE_DIR$/build/intermediates/classes/debug" />
<output-test url="file://$MODULE_DIR$/build/intermediates/classes/androidTest/debug" />
<exclude-output />
@ -34,13 +36,13 @@
<sourceFolder url="file://$MODULE_DIR$/build/generated/source/buildConfig/debug" isTestSource="false" generated="true" />
<sourceFolder url="file://$MODULE_DIR$/build/generated/source/rs/debug" isTestSource="false" generated="true" />
<sourceFolder url="file://$MODULE_DIR$/build/generated/res/rs/debug" type="java-resource" />
<sourceFolder url="file://$MODULE_DIR$/build/generated/res/generated/debug" type="java-resource" />
<sourceFolder url="file://$MODULE_DIR$/build/generated/res/resValues/debug" type="java-resource" />
<sourceFolder url="file://$MODULE_DIR$/build/generated/source/r/androidTest/debug" isTestSource="true" generated="true" />
<sourceFolder url="file://$MODULE_DIR$/build/generated/source/aidl/androidTest/debug" isTestSource="true" generated="true" />
<sourceFolder url="file://$MODULE_DIR$/build/generated/source/buildConfig/androidTest/debug" isTestSource="true" generated="true" />
<sourceFolder url="file://$MODULE_DIR$/build/generated/source/rs/androidTest/debug" isTestSource="true" generated="true" />
<sourceFolder url="file://$MODULE_DIR$/build/generated/res/rs/androidTest/debug" type="java-test-resource" />
<sourceFolder url="file://$MODULE_DIR$/build/generated/res/generated/androidTest/debug" type="java-test-resource" />
<sourceFolder url="file://$MODULE_DIR$/build/generated/res/resValues/androidTest/debug" type="java-test-resource" />
<sourceFolder url="file://$MODULE_DIR$/src/debug/res" type="java-resource" />
<sourceFolder url="file://$MODULE_DIR$/src/debug/resources" type="java-resource" />
<sourceFolder url="file://$MODULE_DIR$/src/debug/assets" type="java-resource" />
@ -75,9 +77,9 @@
<excludeFolder url="file://$MODULE_DIR$/build/intermediates/dependency-cache" />
<excludeFolder url="file://$MODULE_DIR$/build/intermediates/dex" />
<excludeFolder url="file://$MODULE_DIR$/build/intermediates/dex-cache" />
<excludeFolder url="file://$MODULE_DIR$/build/intermediates/exploded-aar/com.android.support/appcompat-v7/22.1.1/jars" />
<excludeFolder url="file://$MODULE_DIR$/build/intermediates/exploded-aar/com.android.support/recyclerview-v7/22.0.0/jars" />
<excludeFolder url="file://$MODULE_DIR$/build/intermediates/exploded-aar/com.android.support/support-v4/22.1.1/jars" />
<excludeFolder url="file://$MODULE_DIR$/build/intermediates/exploded-aar/com.android.support/appcompat-v7/22.2.1/jars" />
<excludeFolder url="file://$MODULE_DIR$/build/intermediates/exploded-aar/com.android.support/recyclerview-v7/22.2.1/jars" />
<excludeFolder url="file://$MODULE_DIR$/build/intermediates/exploded-aar/com.android.support/support-v4/22.2.1/jars" />
<excludeFolder url="file://$MODULE_DIR$/build/intermediates/exploded-aar/com.shamanland/fab/0.0.6/jars" />
<excludeFolder url="file://$MODULE_DIR$/build/intermediates/exploded-aar/it.sephiroth.android.library.easing/android-easing/1.0.3/jars" />
<excludeFolder url="file://$MODULE_DIR$/build/intermediates/exploded-aar/it.sephiroth.android.library.imagezoom/imagezoom/1.0.5/jars" />
@ -106,16 +108,16 @@
</content>
<orderEntry type="jdk" jdkName="Android API 22 Platform" jdkType="Android SDK" />
<orderEntry type="sourceFolder" forTests="false" />
<orderEntry type="library" exported="" name="appcompat-v7-22.1.1" level="project" />
<orderEntry type="library" exported="" name="fab-0.0.6" level="project" />
<orderEntry type="library" exported="" name="android-easing-1.0.3" level="project" />
<orderEntry type="library" exported="" name="imagezoom-1.0.5" level="project" />
<orderEntry type="library" exported="" name="support-v4-22.2.1" level="project" />
<orderEntry type="library" exported="" name="eventbus-2.4.0" level="project" />
<orderEntry type="library" exported="" name="recyclerview-v7-22.2.1" level="project" />
<orderEntry type="library" exported="" name="android-query.0.26.8" level="project" />
<orderEntry type="library" exported="" name="tokenautocomplete-1.2.1" level="project" />
<orderEntry type="library" exported="" name="support-v4-22.1.1" level="project" />
<orderEntry type="library" exported="" name="support-annotations-22.1.1" level="project" />
<orderEntry type="library" exported="" name="recyclerview-v7-22.0.0" level="project" />
<orderEntry type="library" exported="" name="support-annotations-22.2.1" level="project" />
<orderEntry type="library" exported="" name="appcompat-v7-22.2.1" level="project" />
<orderEntry type="library" exported="" name="android-async-http-1.4.6" level="project" />
</component>
</module>

View File

@ -3,7 +3,7 @@ buildscript {
mavenCentral()
}
dependencies {
classpath 'com.android.tools.build:gradle:1.2.3'
classpath 'com.android.tools.build:gradle:1.3.0'
}
}
apply plugin: 'com.android.application'
@ -14,7 +14,7 @@ repositories {
android {
compileSdkVersion 22
buildToolsVersion "22.0.1"
buildToolsVersion "23.0.2"
defaultConfig {
minSdkVersion 14
@ -50,8 +50,8 @@ android {
dependencies {
compile fileTree(dir: 'libs', include: '*.jar')
compile 'com.android.support:appcompat-v7:22.1.1'
compile 'com.android.support:recyclerview-v7:22.0.0'
compile 'com.android.support:appcompat-v7:22.+'
compile 'com.android.support:recyclerview-v7:22.+'
compile 'com.loopj.android:android-async-http:1.4.6'
compile 'it.sephiroth.android.library.imagezoom:imagezoom:1.0.5'
compile 'de.greenrobot:eventbus:2.4.0'

View File

@ -168,7 +168,7 @@ public class DocumentViewActivity extends AppCompatActivity {
createdDateTextView.setText(date);
TextView descriptionTextView = (TextView) findViewById(R.id.descriptionTextView);
if (description == null || description.isEmpty()) {
if (description == null || description.isEmpty() || description.equals(JSONObject.NULL.toString())) {
descriptionTextView.setVisibility(View.GONE);
} else {
descriptionTextView.setVisibility(View.VISIBLE);

View File

@ -11,7 +11,7 @@ public class RecyclerItemClickListener implements RecyclerView.OnItemTouchListen
private OnItemClickListener mListener;
public interface OnItemClickListener {
public void onItemClick(View view, int position);
void onItemClick(View view, int position);
}
GestureDetector mGestureDetector;
@ -25,7 +25,8 @@ public class RecyclerItemClickListener implements RecyclerView.OnItemTouchListen
});
}
@Override public boolean onInterceptTouchEvent(RecyclerView view, MotionEvent e) {
@Override
public boolean onInterceptTouchEvent(RecyclerView view, MotionEvent e) {
View childView = view.findChildViewUnder(e.getX(), e.getY());
if (childView != null && mListener != null && mGestureDetector.onTouchEvent(e)) {
mListener.onItemClick(childView, view.getChildPosition(childView));
@ -33,5 +34,9 @@ public class RecyclerItemClickListener implements RecyclerView.OnItemTouchListen
return false;
}
@Override public void onTouchEvent(RecyclerView view, MotionEvent motionEvent) { }
@Override
public void onTouchEvent(RecyclerView view, MotionEvent motionEvent) { }
@Override
public void onRequestDisallowInterceptTouchEvent(boolean disallowIntercept) { }
}

View File

@ -113,6 +113,11 @@
<artifactId>bcprov-jdk15on</artifactId>
</dependency>
<dependency>
<groupId>com.levigo.jbig2</groupId>
<artifactId>levigo-jbig2-imageio</artifactId>
</dependency>
<!-- OCR dependencies -->
<dependency>
<groupId>jna</groupId>

View File

@ -103,7 +103,7 @@ public class TagDao {
@SuppressWarnings("unchecked")
public List<TagDto> getByDocumentId(String documentId, String userId) {
EntityManager em = ThreadLocalContext.get().getEntityManager();
StringBuilder sb = new StringBuilder("select t.TAG_ID_C, t.TAG_NAME_C, t.TAG_COLOR_C from T_DOCUMENT_TAG dt ");
StringBuilder sb = new StringBuilder("select t.TAG_ID_C, t.TAG_NAME_C, t.TAG_COLOR_C, t.TAG_IDPARENT_C from T_DOCUMENT_TAG dt ");
sb.append(" join T_TAG t on t.TAG_ID_C = dt.DOT_IDTAG_C ");
sb.append(" where dt.DOT_IDDOCUMENT_C = :documentId and t.TAG_DELETEDATE_D is null ");
sb.append(" and t.TAG_IDUSER_C = :userId and dt.DOT_DELETEDATE_D is null ");
@ -123,6 +123,7 @@ public class TagDao {
tagDto.setId((String) o[i++]);
tagDto.setName((String) o[i++]);
tagDto.setColor((String) o[i++]);
tagDto.setParentId((String) o[i++]);
tagDtoList.add(tagDto);
}
return tagDtoList;
@ -137,7 +138,7 @@ public class TagDao {
@SuppressWarnings("unchecked")
public List<TagStatDto> getStats(String userId) {
EntityManager em = ThreadLocalContext.get().getEntityManager();
StringBuilder sb = new StringBuilder("select t.TAG_ID_C, t.TAG_NAME_C, t.TAG_COLOR_C, count(d.DOC_ID_C) ");
StringBuilder sb = new StringBuilder("select t.TAG_ID_C, t.TAG_NAME_C, t.TAG_COLOR_C, t.TAG_IDPARENT_C, count(d.DOC_ID_C) ");
sb.append(" from T_TAG t ");
sb.append(" left join T_DOCUMENT_TAG dt on t.TAG_ID_C = dt.DOT_IDTAG_C and dt.DOT_DELETEDATE_D is null ");
sb.append(" left join T_DOCUMENT d on d.DOC_ID_C = dt.DOT_IDDOCUMENT_C and d.DOC_DELETEDATE_D is null and d.DOC_IDUSER_C = :userId ");
@ -158,6 +159,7 @@ public class TagDao {
tagDto.setId((String) o[i++]);
tagDto.setName((String) o[i++]);
tagDto.setColor((String) o[i++]);
tagDto.setParentId((String) o[i++]);
tagDto.setCount(((Number) o[i++]).intValue());
tagStatDtoList.add(tagDto);
}
@ -281,6 +283,7 @@ public class TagDao {
// Update the tag
tagFromDb.setName(tag.getName());
tagFromDb.setColor(tag.getColor());
tagFromDb.setParentId(tag.getParentId());
// Create audit log
AuditLogUtil.create(tagFromDb, AuditLogType.UPDATE);

View File

@ -24,6 +24,11 @@ public class TagDto {
*/
private String color;
/**
* Parent ID.
*/
private String parentId;
/**
* Getter of id.
*
@ -77,4 +82,22 @@ public class TagDto {
public void setColor(String color) {
this.color = color;
}
/**
* Getter of parentId.
*
* @return the parentId
*/
public String getParentId() {
return parentId;
}
/**
* Setter of parentId.
*
* @param color parentId
*/
public void setParentId(String parentId) {
this.parentId = parentId;
}
}

View File

@ -2,7 +2,7 @@ package com.sismics.docs.core.dao.jpa.dto;
/**
* Tag DTO.
* Tag stat DTO.
*
* @author bgamard
*/

View File

@ -39,6 +39,12 @@ public class Tag implements Loggable {
@Column(name = "TAG_IDUSER_C", nullable = false, length = 36)
private String userId;
/**
* User ID.
*/
@Column(name = "TAG_IDPARENT_C", length = 36)
private String parentId;
/**
* Creation date.
*/
@ -166,11 +172,30 @@ public class Tag implements Loggable {
this.deleteDate = deleteDate;
}
/**
* Getter of parentId.
*
* @return parentId
*/
public String getParentId() {
return parentId;
}
/**
* Setter of parentId.
*
* @param parentId parentId
*/
public void setParentId(String parentId) {
this.parentId = parentId;
}
@Override
public String toString() {
return MoreObjects.toStringHelper(this)
.add("id", id)
.add("name", name)
.add("parentId", parentId)
.toString();
}

View File

@ -105,7 +105,7 @@ public class FileUtil {
PDDocument pdfDocument = null;
try {
PDFTextStripper stripper = new PDFTextStripper();
pdfDocument = PDDocument.load(inputStream, true);
pdfDocument = PDDocument.load(inputStream);
content = stripper.getText(pdfDocument);
} catch (IOException e) {
log.error("Error while extracting text from the PDF", e);
@ -157,7 +157,7 @@ public class FileUtil {
// Generate preview from the first page of the PDF
PDDocument pdfDocument = null;
try {
pdfDocument = PDDocument.load(inputStream, true);
pdfDocument = PDDocument.load(inputStream);
PDFRenderer renderer = new PDFRenderer(pdfDocument);
image = renderer.renderImage(0);
} finally {

View File

@ -1 +1 @@
db.version=1
db.version=2

View File

@ -0,0 +1,2 @@
alter table T_TAG add column TAG_IDPARENT_C varchar(36);
update T_CONFIG set CFG_VALUE_C = '2' where CFG_ID_C = 'DB_VERSION';

View File

@ -35,6 +35,7 @@
<joda-time.joda-time.version>2.8.2</joda-time.joda-time.version>
<org.hibernate.hibernate.version>4.1.0.Final</org.hibernate.hibernate.version>
<javax.servlet.javax.servlet-api.version>3.1.0</javax.servlet.javax.servlet-api.version>
<com.levigo.jbig2.levigo-jbig2-imageio.version>1.6.3</com.levigo.jbig2.levigo-jbig2-imageio.version>
<org.eclipse.jetty.jetty-server.version>9.2.13.v20150730</org.eclipse.jetty.jetty-server.version>
<org.eclipse.jetty.jetty-webapp.version>9.2.13.v20150730</org.eclipse.jetty.jetty-webapp.version>
@ -66,6 +67,12 @@
<enabled>true</enabled>
</snapshots>
</repository>
<repository>
<id>jbig2.googlecode</id>
<name>JBIG2 ImageIO-Plugin repository at googlecode.com</name>
<url>http://jbig2-imageio.googlecode.com/svn/maven-repository</url>
</repository>
</repositories>
@ -354,6 +361,13 @@
<version>${org.bouncycastle.bcprov-jdk15on.version}</version>
</dependency>
<!-- Used to read JBIG2 images. See https://github.com/sismics/docs/issues/38 -->
<dependency>
<groupId>com.levigo.jbig2</groupId>
<artifactId>levigo-jbig2-imageio</artifactId>
<version>${com.levigo.jbig2.levigo-jbig2-imageio.version}</version>
</dependency>
<!-- OCR dependencies -->
<dependency>
<groupId>jna</groupId>

View File

@ -1,3 +1,3 @@
api.current_version=${project.version}
api.min_version=1.0
db.version=1
db.version=2

View File

@ -19,6 +19,7 @@ import com.sismics.docs.core.util.jpa.PaginatedLists;
import com.sismics.docs.core.util.jpa.SortCriteria;
import com.sismics.rest.exception.ForbiddenClientException;
import com.sismics.rest.exception.ServerException;
import com.sismics.rest.util.JsonUtil;
/**
* Audit log REST resources.
@ -70,7 +71,7 @@ public class AuditLogResource extends BaseResource {
.add("target", auditLogDto.getEntityId())
.add("class", auditLogDto.getEntityClass())
.add("type", auditLogDto.getType().name())
.add("message", auditLogDto.getMessage())
.add("message", JsonUtil.nullable(auditLogDto.getMessage()))
.add("create_date", auditLogDto.getCreateTimestamp()));
}

View File

@ -22,6 +22,7 @@ import com.sismics.docs.core.dao.jpa.dto.TagStatDto;
import com.sismics.docs.core.model.jpa.Tag;
import com.sismics.rest.exception.ClientException;
import com.sismics.rest.exception.ForbiddenClientException;
import com.sismics.rest.util.JsonUtil;
import com.sismics.rest.util.ValidationUtil;
/**
@ -50,7 +51,8 @@ public class TagResource extends BaseResource {
items.add(Json.createObjectBuilder()
.add("id", tag.getId())
.add("name", tag.getName())
.add("color", tag.getColor()));
.add("color", tag.getColor())
.add("parent", JsonUtil.nullable(tag.getParentId())));
}
JsonObjectBuilder response = Json.createObjectBuilder()
@ -96,7 +98,8 @@ public class TagResource extends BaseResource {
@PUT
public Response add(
@FormParam("name") String name,
@FormParam("color") String color) {
@FormParam("color") String color,
@FormParam("parent") String parentId) {
if (!authenticate()) {
throw new ForbiddenClientException();
}
@ -117,11 +120,22 @@ public class TagResource extends BaseResource {
throw new ClientException("AlreadyExistingTag", MessageFormat.format("Tag already exists: {0}", name));
}
// Check the parent
if (StringUtils.isEmpty(parentId)) {
parentId = null;
} else {
Tag parentTag = tagDao.getByTagId(principal.getId(), parentId);
if (parentTag == null) {
throw new ClientException("ParentNotFound", MessageFormat.format("Parent not found: {0}", parentId));
}
}
// Create the tag
tag = new Tag();
tag.setName(name);
tag.setColor(color);
tag.setUserId(principal.getId());
tag.setParentId(parentId);
String id = tagDao.create(tag);
JsonObjectBuilder response = Json.createObjectBuilder()
@ -140,7 +154,8 @@ public class TagResource extends BaseResource {
public Response update(
@PathParam("id") String id,
@FormParam("name") String name,
@FormParam("color") String color) {
@FormParam("color") String color,
@FormParam("parent") String parentId) {
if (!authenticate()) {
throw new ForbiddenClientException();
}
@ -161,6 +176,16 @@ public class TagResource extends BaseResource {
throw new ClientException("TagNotFound", MessageFormat.format("Tag not found: {0}", id));
}
// Check the parent
if (StringUtils.isEmpty(parentId)) {
parentId = null;
} else {
Tag parentTag = tagDao.getByTagId(principal.getId(), parentId);
if (parentTag == null) {
throw new ClientException("ParentNotFound", MessageFormat.format("Parent not found: {0}", parentId));
}
}
// Check for name duplicate
Tag tagDuplicate = tagDao.getByName(principal.getId(), name);
if (tagDuplicate != null && !tagDuplicate.getId().equals(id)) {
@ -174,6 +199,8 @@ public class TagResource extends BaseResource {
if (!StringUtils.isEmpty(color)) {
tag.setColor(color);
}
// Parent tag is always updated to have the possibility to delete it
tag.setParentId(parentId);
tagDao.update(tag);

View File

@ -40,6 +40,7 @@ import com.sismics.docs.rest.constant.BaseFunction;
import com.sismics.rest.exception.ClientException;
import com.sismics.rest.exception.ForbiddenClientException;
import com.sismics.rest.exception.ServerException;
import com.sismics.rest.util.JsonUtil;
import com.sismics.rest.util.ValidationUtil;
import com.sismics.security.UserPrincipal;
import com.sismics.util.filter.TokenBasedSecurityFilter;
@ -513,8 +514,8 @@ public class UserResource extends BaseResource {
for (AuthenticationToken authenticationToken : authenticationTokenDao.getByUserId(principal.getId())) {
JsonObjectBuilder session = Json.createObjectBuilder()
.add("create_date", authenticationToken.getCreationDate().getTime())
.add("ip", authenticationToken.getIp())
.add("user_agent", authenticationToken.getUserAgent());
.add("ip", JsonUtil.nullable(authenticationToken.getIp()))
.add("user_agent", JsonUtil.nullable(authenticationToken.getUserAgent()));
if (authenticationToken.getLastConnectionDate() != null) {
session.add("last_connection_date", authenticationToken.getLastConnectionDate().getTime());
}

View File

@ -124,6 +124,9 @@ angular.module('docs',
controller: 'DocumentDefault'
}
}
})
.state('document.default.search', {
url: '/search/:search'
})
.state('document.default.file', {
url: '/file/:fileId',
@ -153,12 +156,40 @@ angular.module('docs',
})
.state('document.view', {
url: '/view/:id',
redirectTo: 'document.view.content',
views: {
'document': {
templateUrl: 'partial/docs/document.view.html',
controller: 'DocumentView'
}
}
})
.state('document.view.content', {
url: '/content',
views: {
'tab': {
templateUrl: 'partial/docs/document.view.content.html',
controller: 'DocumentViewContent'
}
}
})
.state('document.view.permissions', {
url: '/permissions',
views: {
'tab': {
templateUrl: 'partial/docs/document.view.permissions.html',
controller: 'DocumentViewPermissions'
}
}
})
.state('document.view.activity', {
url: '/activity',
views: {
'tab': {
templateUrl: 'partial/docs/document.view.activity.html',
controller: 'DocumentViewActivity'
}
}
})
.state('document.view.file', {
url: '/file/:fileId',
@ -228,4 +259,18 @@ angular.module('docs',
$rootScope.$state = $state;
$rootScope.$stateParams = $stateParams;
$rootScope.pageTitle = 'Sismics Docs';
})
/**
* Redirection support for ui-router.
* Thanks to https://github.com/acollard
* See https://github.com/angular-ui/ui-router/issues/1584#issuecomment-76993045
*/
.run(function($rootScope, $state){
$rootScope.$on('$stateChangeStart', function(event, toState, toParams) {
var redirect = toState.redirectTo;
if (redirect) {
event.preventDefault();
$state.go(redirect, toParams);
}
});
});

View File

@ -12,7 +12,8 @@ angular.module('docs').controller('Document', function($scope, $timeout, $state,
$scope.offset = 0;
$scope.currentPage = 1;
$scope.limit = _.isUndefined(localStorage.documentsPageSize) ? 10 : localStorage.documentsPageSize;
$scope.search = '';
$scope.search = $state.params.search ? $state.params.search : '';
$scope.setSearch = function(search) { $scope.search = search };
// A timeout promise is used to slow down search requests to the server
// We keep track of it for cancellation purpose
@ -65,6 +66,17 @@ angular.module('docs').controller('Document', function($scope, $timeout, $state,
$timeout.cancel(timeoutPromise);
}
if ($state.current.name == 'document.default'
|| $state.current.name == 'document.default.search') {
$state.go($scope.search == '' ?
'document.default' : 'document.default.search', {
search: $scope.search
}, {
location: 'replace',
notify: false
});
}
// Call API later
timeoutPromise = $timeout(function () {
$scope.loadDocuments();
@ -99,6 +111,22 @@ angular.module('docs').controller('Document', function($scope, $timeout, $state,
* Display a document.
*/
$scope.viewDocument = function(id) {
$state.transitionTo('document.view', { id: id });
$state.go('document.view', { id: id });
};
// Load tags
var tags = [];
Restangular.one('tag/list').getList().then(function(data) {
tags = data.tags;
});
/**
* Find children tags.
* @param parent
*/
$scope.getChildrenTags = function(parent) {
return _.filter(tags, function(tag) {
return tag.parent == parent;
});
};
});

View File

@ -81,7 +81,7 @@ angular.module('docs').controller('DocumentDefault', function($scope, $state, Re
* Navigate to the selected file.
*/
$scope.openFile = function (file) {
$state.transitionTo('document.default.file', { fileId: file.id })
$state.go('document.default.file', { fileId: file.id })
};
/**
@ -107,6 +107,6 @@ angular.module('docs').controller('DocumentDefault', function($scope, $state, Re
* Add a document with checked files.
*/
$scope.addDocument = function() {
$state.transitionTo('document.add', { files: _.pluck($scope.checkedFiles(), 'id') });
$state.go('document.add', { files: _.pluck($scope.checkedFiles(), 'id') });
};
});

View File

@ -95,7 +95,7 @@ angular.module('docs').controller('DocumentEdit', function($rootScope, $scope, $
if ($scope.isEdit()) {
// Go back to the edited document
$scope.pageDocuments();
$state.transitionTo('document.view', { id: $stateParams.id });
$state.go('document.view', { id: $stateParams.id });
} else {
// Reset the scope and stay here
var fileUploadCount = _.size($scope.newFiles) + resolve.length;
@ -188,9 +188,9 @@ angular.module('docs').controller('DocumentEdit', function($rootScope, $scope, $
*/
$scope.cancel = function() {
if ($scope.isEdit()) {
$state.transitionTo('document.view', { id: $stateParams.id });
$state.go('document.view', { id: $stateParams.id });
} else {
$state.transitionTo('document.default');
$state.go('document.default');
}
};

View File

@ -3,7 +3,7 @@
/**
* Document view controller.
*/
angular.module('docs').controller('DocumentView', function ($scope, $state, $stateParams, $location, $dialog, $modal, Restangular, $upload, $q) {
angular.module('docs').controller('DocumentView', function ($scope, $state, $stateParams, $location, $dialog, $modal, Restangular, $timeout) {
// Load document data from server
Restangular.one('document', $stateParams.id).get().then(function(data) {
$scope.document = data;
@ -11,60 +11,6 @@ angular.module('docs').controller('DocumentView', function ($scope, $state, $sta
$scope.error = response;
});
// Load audit log data from server
Restangular.one('auditlog').get({
document: $stateParams.id
}).then(function(data) {
$scope.logs = data.logs;
});
// Watch for ACLs change and group them for easy displaying
$scope.$watch('document.acls', function(acls) {
$scope.acls = _.groupBy(acls, function(acl) {
return acl.id;
});
});
// Initialize add ACL
$scope.acl = { perm: 'READ' };
/**
* Configuration for file sorting.
*/
$scope.fileSortableOptions = {
forceHelperSize: true,
forcePlaceholderSize: true,
tolerance: 'pointer',
handle: '.handle',
stop: function () {
// Send new positions to server
$scope.$apply(function () {
Restangular.one('file').post('reorder', {
id: $stateParams.id,
order: _.pluck($scope.files, 'id')
});
});
}
};
/**
* Load files from server.
*/
$scope.loadFiles = function () {
Restangular.one('file').getList('list', { id: $stateParams.id }).then(function (data) {
$scope.files = data.files;
// TODO Keep currently uploading files
});
};
$scope.loadFiles();
/**
* Navigate to the selected file.
*/
$scope.openFile = function (file) {
$state.transitionTo('document.view.file', { id: $stateParams.id, fileId: file.id })
};
/**
* Delete a document.
*/
@ -80,27 +26,7 @@ angular.module('docs').controller('DocumentView', function ($scope, $state, $sta
if (result == 'ok') {
Restangular.one('document', document.id).remove().then(function () {
$scope.loadDocuments();
$state.transitionTo('document.default');
});
}
});
};
/**
* Delete a file.
*/
$scope.deleteFile = function (file) {
var title = 'Delete file';
var msg = 'Do you really want to delete this file?';
var btns = [
{result: 'cancel', label: 'Cancel'},
{result: 'ok', label: 'OK', cssClass: 'btn-primary'}
];
$dialog.messageBox(title, msg, btns, function (result) {
if (result == 'ok') {
Restangular.one('file', file.id).remove().then(function () {
$scope.loadFiles();
$state.go('document.default');
});
}
});
@ -157,125 +83,4 @@ angular.module('docs').controller('DocumentView', function ($scope, $state, $sta
}
});
};
/**
* File has been drag & dropped.
*/
$scope.fileDropped = function(files) {
if (!$scope.document.writable) {
return;
}
if (files && files.length) {
// Adding files to the UI
var newfiles = [];
_.each(files, function(file) {
var newfile = {
progress: 0,
name: file.name,
create_date: new Date().getTime(),
mimetype: file.type,
status: 'Pending...'
};
$scope.files.push(newfile);
newfiles.push(newfile);
});
// Uploading files sequentially
var key = 0;
var then = function() {
if (files[key]) {
$scope.uploadFile(files[key], newfiles[key++]).then(then);
}
};
then();
}
};
/**
* Upload a file.
*/
$scope.uploadFile = function(file, newfile) {
// Upload the file
newfile.status = 'Uploading...';
return $upload.upload({
method: 'PUT',
url: '../api/file',
file: file,
fields: {
id: $stateParams.id
}
})
.progress(function (e) {
newfile.progress = parseInt(100.0 * e.loaded / e.total);
})
.success(function (data) {
newfile.id = data.id;
});
};
/**
* Delete an ACL.
*/
$scope.deleteAcl = function(acl) {
Restangular.one('acl/' + $stateParams.id + '/' + acl.perm + '/' + acl.id, null).remove().then(function () {
$scope.document.acls = _.reject($scope.document.acls, function(s) {
return angular.equals(acl, s);
});
});
};
/**
* Add an ACL.
*/
$scope.addAcl = function() {
// Compute ACLs to add
$scope.acl.source = $stateParams.id;
var acls = [];
if ($scope.acl.perm == 'READWRITE') {
acls = [{
source: $stateParams.id,
username: $scope.acl.username,
perm: 'READ'
}, {
source: $stateParams.id,
username: $scope.acl.username,
perm: 'WRITE'
}];
} else {
acls = [{
source: $stateParams.id,
username: $scope.acl.username,
perm: $scope.acl.perm
}];
}
// Add ACLs
_.each(acls, function(acl) {
Restangular.one('acl').put(acl).then(function(acl) {
if (_.isUndefined(acl.id)) {
return;
}
$scope.document.acls.push(acl);
$scope.document.acls = angular.copy($scope.document.acls);
});
});
// Reset form
$scope.acl = { perm: 'READ' };
};
/**
* Auto-complete on ACL target.
*/
$scope.getTargetAclTypeahead = function($viewValue) {
var deferred = $q.defer();
Restangular.one('acl/target/search')
.get({
search: $viewValue
}).then(function(data) {
deferred.resolve(_.pluck(data.users, 'username'), true);
});
return deferred.promise;
};
});

View File

@ -0,0 +1,13 @@
'use strict';
/**
* Document view activity controller.
*/
angular.module('docs').controller('DocumentViewActivity', function ($scope, $stateParams, Restangular) {
// Load audit log data from server
Restangular.one('auditlog').get({
document: $stateParams.id
}).then(function(data) {
$scope.logs = data.logs;
});
});

View File

@ -0,0 +1,119 @@
'use strict';
/**
* Document view content controller.
*/
angular.module('docs').controller('DocumentViewContent', function ($scope, $stateParams, Restangular, $dialog, $state, $upload) {
/**
* Configuration for file sorting.
*/
$scope.fileSortableOptions = {
forceHelperSize: true,
forcePlaceholderSize: true,
tolerance: 'pointer',
handle: '.handle',
stop: function () {
// Send new positions to server
$scope.$apply(function () {
Restangular.one('file').post('reorder', {
id: $stateParams.id,
order: _.pluck($scope.files, 'id')
});
});
}
};
/**
* Load files from server.
*/
$scope.loadFiles = function () {
Restangular.one('file').getList('list', { id: $stateParams.id }).then(function (data) {
$scope.files = data.files;
// TODO Keep currently uploading files
});
};
$scope.loadFiles();
/**
* Navigate to the selected file.
*/
$scope.openFile = function (file) {
$state.go('document.view.file', { id: $stateParams.id, fileId: file.id })
};
/**
* Delete a file.
*/
$scope.deleteFile = function (file) {
var title = 'Delete file';
var msg = 'Do you really want to delete this file?';
var btns = [
{result: 'cancel', label: 'Cancel'},
{result: 'ok', label: 'OK', cssClass: 'btn-primary'}
];
$dialog.messageBox(title, msg, btns, function (result) {
if (result == 'ok') {
Restangular.one('file', file.id).remove().then(function () {
$scope.loadFiles();
});
}
});
};
/**
* File has been drag & dropped.
*/
$scope.fileDropped = function(files) {
if (!$scope.document.writable) {
return;
}
if (files && files.length) {
// Adding files to the UI
var newfiles = [];
_.each(files, function(file) {
var newfile = {
progress: 0,
name: file.name,
create_date: new Date().getTime(),
mimetype: file.type,
status: 'Pending...'
};
$scope.files.push(newfile);
newfiles.push(newfile);
});
// Uploading files sequentially
var key = 0;
var then = function() {
if (files[key]) {
$scope.uploadFile(files[key], newfiles[key++]).then(then);
}
};
then();
}
};
/**
* Upload a file.
*/
$scope.uploadFile = function(file, newfile) {
// Upload the file
newfile.status = 'Uploading...';
return $upload.upload({
method: 'PUT',
url: '../api/file',
file: file,
fields: {
id: $stateParams.id
}
})
.progress(function (e) {
newfile.progress = parseInt(100.0 * e.loaded / e.total);
})
.success(function (data) {
newfile.id = data.id;
});
};
});

View File

@ -0,0 +1,81 @@
'use strict';
/**
* Document view permissions controller.
*/
angular.module('docs').controller('DocumentViewPermissions', function ($scope, $stateParams, Restangular, $q) {
// Watch for ACLs change and group them for easy displaying
$scope.$watch('document.acls', function(acls) {
$scope.acls = _.groupBy(acls, function(acl) {
return acl.id;
});
});
// Initialize add ACL
$scope.acl = { perm: 'READ' };
/**
* Delete an ACL.
*/
$scope.deleteAcl = function(acl) {
Restangular.one('acl/' + $stateParams.id + '/' + acl.perm + '/' + acl.id, null).remove().then(function () {
$scope.document.acls = _.reject($scope.document.acls, function(s) {
return angular.equals(acl, s);
});
});
};
/**
* Add an ACL.
*/
$scope.addAcl = function() {
// Compute ACLs to add
$scope.acl.source = $stateParams.id;
var acls = [];
if ($scope.acl.perm == 'READWRITE') {
acls = [{
source: $stateParams.id,
username: $scope.acl.username,
perm: 'READ'
}, {
source: $stateParams.id,
username: $scope.acl.username,
perm: 'WRITE'
}];
} else {
acls = [{
source: $stateParams.id,
username: $scope.acl.username,
perm: $scope.acl.perm
}];
}
// Add ACLs
_.each(acls, function(acl) {
Restangular.one('acl').put(acl).then(function(acl) {
if (_.isUndefined(acl.id)) {
return;
}
$scope.document.acls.push(acl);
$scope.document.acls = angular.copy($scope.document.acls);
});
});
// Reset form
$scope.acl = { perm: 'READ' };
};
/**
* Auto-complete on ACL target.
*/
$scope.getTargetAclTypeahead = function($viewValue) {
var deferred = $q.defer();
Restangular.one('acl/target/search')
.get({
search: $viewValue
}).then(function(data) {
deferred.resolve(_.pluck(data.users, 'username'), true);
});
return deferred.promise;
};
});

View File

@ -9,7 +9,7 @@ angular.module('docs').controller('Login', function($scope, $rootScope, $state,
User.userInfo(true).then(function(data) {
$rootScope.userInfo = data;
});
$state.transitionTo('document.default');
$state.go('document.default');
}, function() {
var title = 'Login failed';
var msg = 'Username or password invalid';

View File

@ -6,9 +6,13 @@
angular.module('docs').controller('Main', function($scope, $rootScope, $state, User) {
User.userInfo().then(function(data) {
if (data.anonymous) {
$state.transitionTo('login');
$state.go('login', {}, {
location: 'replace'
});
} else {
$state.transitionTo('document.default');
$state.go('document.default', {}, {
location: 'replace'
});
}
});
});

View File

@ -41,7 +41,7 @@ angular.module('docs').controller('Navigation', function($scope, $http, $state,
*/
$scope.openLogs = function() {
$scope.errorNumber = 0;
$state.transitionTo('settings.log');
$state.go('settings.log');
};
/**
@ -52,7 +52,7 @@ angular.module('docs').controller('Navigation', function($scope, $http, $state,
User.userInfo(true).then(function(data) {
$rootScope.userInfo = data;
});
$state.transitionTo('main');
$state.go('main');
});
$event.preventDefault();
};

View File

@ -19,6 +19,6 @@ angular.module('docs').controller('SettingsUser', function($scope, $state, Resta
* Edit a user.
*/
$scope.editUser = function(user) {
$state.transitionTo('settings.user.edit', { username: user.username });
$state.go('settings.user.edit', { username: user.username });
};
});

View File

@ -38,7 +38,7 @@ angular.module('docs').controller('SettingsUserEdit', function($scope, $dialog,
promise.then(function() {
$scope.loadUsers();
$state.transitionTo('settings.user');
$state.go('settings.user');
});
};
@ -54,9 +54,9 @@ angular.module('docs').controller('SettingsUserEdit', function($scope, $dialog,
if (result == 'ok') {
Restangular.one('user', $stateParams.username).remove().then(function() {
$scope.loadUsers();
$state.transitionTo('settings.user');
$state.go('settings.user');
}, function () {
$state.transitionTo('settings.user');
$state.go('settings.user');
});
}
});

View File

@ -24,7 +24,7 @@ angular.module('share').controller('FileModalView', function($rootScope, $modalI
if (value.id == $stateParams.fileId) {
var next = $scope.files[key + 1];
if (next) {
$state.transitionTo('share.file', { documentId: $stateParams.documentId, shareId: $stateParams.shareId, fileId: next.id });
$state.go('share.file', { documentId: $stateParams.documentId, shareId: $stateParams.shareId, fileId: next.id });
}
}
});
@ -38,7 +38,7 @@ angular.module('share').controller('FileModalView', function($rootScope, $modalI
if (value.id == $stateParams.fileId) {
var previous = $scope.files[key - 1];
if (previous) {
$state.transitionTo('share.file', { documentId: $stateParams.documentId, shareId: $stateParams.shareId, fileId: previous.id });
$state.go('share.file', { documentId: $stateParams.documentId, shareId: $stateParams.shareId, fileId: previous.id });
}
}
});

View File

@ -16,6 +16,6 @@ angular.module('share').controller('FileView', function($modal, $state, $statePa
modal.closed = true;
},function(result) {
modal.closed = true;
$state.transitionTo('share', { documentId: $stateParams.documentId, shareId: $stateParams.shareId });
$state.go('share', { documentId: $stateParams.documentId, shareId: $stateParams.shareId });
});
});

View File

@ -10,7 +10,7 @@ angular.module('share').controller('Share', function($scope, $state, $stateParam
$scope.document = data;
}, function (response) {
if (response.status == 403) {
$state.transitionTo('403');
$state.go('403');
}
});
@ -24,6 +24,6 @@ angular.module('share').controller('Share', function($scope, $state, $stateParam
* Navigate to the selected file.
*/
$scope.openFile = function (file) {
$state.transitionTo('share.file', { documentId: $stateParams.documentId, shareId: $stateParams.shareId, fileId: file.id })
$state.go('share.file', { documentId: $stateParams.documentId, shareId: $stateParams.shareId, fileId: file.id })
};
});

View File

@ -43,6 +43,9 @@
<script src="app/docs/controller/DocumentDefault.js" type="text/javascript"></script>
<script src="app/docs/controller/DocumentEdit.js" type="text/javascript"></script>
<script src="app/docs/controller/DocumentView.js" type="text/javascript"></script>
<script src="app/docs/controller/DocumentViewContent.js" type="text/javascript"></script>
<script src="app/docs/controller/DocumentViewPermissions.js" type="text/javascript"></script>
<script src="app/docs/controller/DocumentViewActivity.js" type="text/javascript"></script>
<script src="app/docs/controller/DocumentModalShare.js" type="text/javascript"></script>
<script src="app/docs/controller/FileView.js" type="text/javascript"></script>
<script src="app/docs/controller/FileModalView.js" type="text/javascript"></script>

View File

@ -5,7 +5,18 @@
<a href="#/document/add" class="btn btn-primary"><span class="glyphicon glyphicon-plus"></span> Add a document</a>
</p>
<div class="input-group">
<div class="row">
<div class="dropdown col-md-2 tag-tree-dropdown" dropdown>
<button class="btn btn-block btn-default" dropdown-toggle ng-disabled="disabled">
Tags <span class="caret"></span>
</button>
<ul class="dropdown-menu tag-tree">
<li ng-if="getChildrenTags().length == 0">No tags</li>
<li ng-repeat="tag in getChildrenTags()" ng-include="'tag-tree-item'"></li>
</ul>
</div>
<div class="col-md-10 input-group">
<span class="input-group-addon">
<span class="glyphicon glyphicon glyphicon-info-sign"
tooltip-placement="bottom"
@ -23,6 +34,7 @@
<span class="glyphicon glyphicon-remove" ng-show="search.length > 0" ng-click="search = ''"></span>
</span>
</div>
</div>
<table class="row table table-striped table-hover table-documents">
<thead>
@ -37,7 +49,7 @@
<td>
{{ document.title }} ({{ document.file_count }})
<span class="glyphicon glyphicon-share" ng-if="document.shared" tooltip="Shared"></span>
<a href="#/document/view/{{ document.id }}" ng-click="$event.stopPropagation()"><span class="glyphicon glyphicon-link"></span></a>
<a href="#/document/view/{{ document.id }}" target="_blank" ng-click="$event.stopPropagation()"><span class="glyphicon glyphicon-link"></span></a>
</td>
<td>{{ document.create_date | date: 'yyyy-MM-dd' }}</td>
<td class="hidden-xs cell-tags">
@ -71,3 +83,13 @@
<div ui-view="document"></div>
</div>
</div>
<script type="text/ng-template" id="tag-tree-item">
<span class="btn" ng-style="{ 'background-color': tag.color }"></span>
<span class="btn btn-link" ng-click="setSearch('tag:' + tag.name)">
{{ tag.name }}
</span>
<ul class="list-unstyled">
<li ng-repeat="tag in getChildrenTags(tag.id)" ng-include="'tag-tree-item'"></li>
</ul>
</script>

View File

@ -0,0 +1 @@
<audit-log logs="logs" />

View File

@ -0,0 +1,37 @@
<p ng-bind-html="document.description | newline"></p>
<div ng-file-drop drag-over-class="bg-success" ng-multiple="true" allow-dir="false" ng-model="dropFiles"
accept="image/*,application/pdf,application/zip" ng-file-change="fileDropped($files, $event, $rejectedFiles)">
<div class="row upload-zone" ui-sortable="fileSortableOptions" ng-model="files">
<div class="col-xs-6 col-sm-4 col-md-3 col-lg-2 text-center" ng-repeat="file in files">
<div class="thumbnail" ng-if="file.id">
<a ng-click="openFile(file)">
<img class="thumbnail-file" ng-src="../api/file/{{ file.id }}/data?size=thumb" tooltip="{{ file.mimetype }}" tooltip-placement="top" />
</a>
<div class="caption" ng-show="document.writable">
<div class="pull-left">
<div class="btn btn-default handle"><span class="glyphicon glyphicon-resize-horizontal"></span></div>
</div>
<div class="pull-right">
<button class="btn btn-danger" ng-click="deleteFile(file)"><span class="glyphicon glyphicon-trash"></span></button>
</div>
<div class="clearfix"></div>
</div>
</div>
<div class="thumbnail" ng-if="!file.id">
<p class="text-center lead">
{{ file.status }}
</p>
<div class="caption">
<progressbar value="file.progress" class="progress-info active"></progressbar>
</div>
</div>
</div>
<p class="text-center well-lg" ng-if="files.length == 0">
<span class="glyphicon glyphicon-move"></span>
Drag &amp; drop files here to upload
</p>
</div>
</div>

View File

@ -38,119 +38,24 @@
</ul>
</div>
<tabset>
<tab>
<tab-heading class="pointer">
<ul class="nav nav-tabs">
<li ng-class="{ active: $state.current.name == 'document.view.content' }">
<a href="#/document/view/{{ document.id }}/content">
<span class="glyphicon glyphicon-file"></span> Content
</tab-heading>
<p ng-bind-html="document.description | newline"></p>
<div ng-file-drop drag-over-class="bg-success" ng-multiple="true" allow-dir="false" ng-model="dropFiles"
accept="image/*,application/pdf,application/zip" ng-file-change="fileDropped($files, $event, $rejectedFiles)">
<div class="row upload-zone" ui-sortable="fileSortableOptions" ng-model="files">
<div class="col-xs-6 col-sm-4 col-md-3 col-lg-2 text-center" ng-repeat="file in files">
<div class="thumbnail" ng-if="file.id">
<a ng-click="openFile(file)">
<img class="thumbnail-file" ng-src="../api/file/{{ file.id }}/data?size=thumb" tooltip="{{ file.mimetype }}" tooltip-placement="top" />
</a>
<div class="caption" ng-show="document.writable">
<div class="pull-left">
<div class="btn btn-default handle"><span class="glyphicon glyphicon-resize-horizontal"></span></div>
</div>
<div class="pull-right">
<button class="btn btn-danger" ng-click="deleteFile(file)"><span class="glyphicon glyphicon-trash"></span></button>
</div>
<div class="clearfix"></div>
</div>
</div>
<div class="thumbnail" ng-if="!file.id">
<p class="text-center lead">
{{ file.status }}
</p>
<div class="caption">
<progressbar value="file.progress" class="progress-info active"></progressbar>
</div>
</div>
</div>
<p class="text-center well-lg" ng-if="files.length == 0">
<span class="glyphicon glyphicon-move"></span>
Drag &amp; drop files here to upload
</p>
</div>
</div>
</tab>
<tab>
<tab-heading class="pointer">
</li>
<li ng-class="{ active: $state.current.name == 'document.view.permissions' }">
<a href="#/document/view/{{ document.id }}/permissions">
<span class="glyphicon glyphicon-user"></span> Permissions
</tab-heading>
<table class="table">
<tr>
<th style="width: 40%">For</th>
<th style="width: 40%">Permission</th>
</tr>
<tr ng-repeat="(id, acl) in acls">
<td><em>{{ acl[0].type == 'SHARE' ? 'Shared' : 'User' }}</em> {{ acl[0].name }}</td>
<td>
<span class="label label-default" style="margin-right: 6px;" ng-repeat="a in acl | orderBy: 'perm'">
{{ a.perm }}
<span ng-show="(document.creator != a.name && a.type == 'USER' || a.type != 'USER') && document.writable"
class="glyphicon glyphicon-remove pointer"
ng-click="deleteAcl(a)"></span>
</span>
</td>
</tr>
</table>
<div ng-show="document.writable">
<h4>Add a permission</h4>
<form name="aclForm" class="form-horizontal">
<div class="form-group">
<label class=" col-sm-2 control-label" for="inputTarget">User</label>
<div class="col-sm-3">
<input required ng-maxlength="50" class="form-control" type="text" id="inputTarget"
placeholder="Type a username" name="username" ng-model="acl.username" autocomplete="off"
typeahead="username for username in getTargetAclTypeahead($viewValue) | filter: $viewValue"
typeahead-wait-ms="200" />
</div>
</div>
<div class="form-group">
<label class=" col-sm-2 control-label" for="inputPermission">Permission</label>
<div class="col-sm-3">
<select class="form-control" ng-model="acl.perm" id="inputPermission">
<option value="READ">Can read</option>
<option value="READWRITE">Can edit</option>
</select>
</div>
</div>
<div class="form-group">
<div class="col-sm-offset-2 col-sm-10">
<button type="submit" class="btn btn-primary" ng-disabled="!aclForm.$valid" ng-click="addAcl()">
<span class="glyphicon glyphicon-plus"></span>
Add
</button>
</div>
</div>
</form>
</div>
</tab>
<tab>
<tab-heading class="pointer">
</a>
</li>
<li ng-class="{ active: $state.current.name == 'document.view.activity' }">
<a href="#/document/view/{{ document.id }}/activity">
<span class="glyphicon glyphicon-tasks"></span> Activity
</tab-heading>
<audit-log logs="logs" />
</tab>
</tabset>
</a>
</li>
</ul>
<div ui-view="tab"></div>
<div ui-view="file"></div>
</div>

View File

@ -0,0 +1,53 @@
<table class="table">
<tr>
<th style="width: 40%">For</th>
<th style="width: 40%">Permission</th>
</tr>
<tr ng-repeat="(id, acl) in acls">
<td><em>{{ acl[0].type == 'SHARE' ? 'Shared' : 'User' }}</em> {{ acl[0].name }}</td>
<td>
<span class="label label-default" style="margin-right: 6px;" ng-repeat="a in acl | orderBy: 'perm'">
{{ a.perm }}
<span ng-show="(document.creator != a.name && a.type == 'USER' || a.type != 'USER') && document.writable"
class="glyphicon glyphicon-remove pointer"
ng-click="deleteAcl(a)"></span>
</span>
</td>
</tr>
</table>
<div ng-show="document.writable">
<h4>Add a permission</h4>
<form name="aclForm" class="form-horizontal">
<div class="form-group">
<label class=" col-sm-2 control-label" for="inputTarget">User</label>
<div class="col-sm-3">
<input required ng-maxlength="50" class="form-control" type="text" id="inputTarget"
placeholder="Type a username" name="username" ng-model="acl.username" autocomplete="off"
typeahead="username for username in getTargetAclTypeahead($viewValue) | filter: $viewValue"
typeahead-wait-ms="200" />
</div>
</div>
<div class="form-group">
<label class=" col-sm-2 control-label" for="inputPermission">Permission</label>
<div class="col-sm-3">
<select class="form-control" ng-model="acl.perm" id="inputPermission">
<option value="READ">Can read</option>
<option value="READWRITE">Can edit</option>
</select>
</div>
</div>
<div class="form-group">
<div class="col-sm-offset-2 col-sm-10">
<button type="submit" class="btn btn-primary" ng-disabled="!aclForm.$valid" ng-click="addAcl()">
<span class="glyphicon glyphicon-plus"></span>
Add
</button>
</div>
</div>
</form>
</div>

View File

@ -21,6 +21,15 @@
<tbody>
<tr ng-repeat="tag in tags | filter:search">
<td><inline-edit value="tag.name" on-edit="updateTag(tag)" /></td>
<td class="col-xs-4">
<select class="form-control" ng-model="tag.parent" ng-change="updateTag(tag)">
<option value="" ng-selected="!tag.parent"></option>
<option ng-repeat="tag0 in tags"
ng-if="tag0.id != tag.id"
ng-selected="tag.parent == tag0.id"
value="{{ tag0.id }}">Parent: {{ tag0.name }}</option>
</select>
</td>
<td class="col-xs-1"><span colorpicker class="btn" on-hide="updateTag(tag)" data-color="" ng-model="tag.color" ng-style="{ 'background': tag.color }">&nbsp;</span></td>
<td class="col-xs-1"><button class="btn btn-danger pull-right" ng-click="deleteTag(tag)"><span class="glyphicon glyphicon-trash"></span></button></td>
</tr>

View File

@ -184,3 +184,16 @@ input[readonly].share-link {
.tab-pane {
margin-top: 20px;
}
// Tag tree
.tag-tree-dropdown {
padding-left: 0;
.tag-tree {
li {
margin-left: 20px;
margin-top: 8px;
margin-bottom: 8px;
}
}
}

View File

@ -1,3 +1,3 @@
api.current_version=${project.version}
api.min_version=1.0
db.version=1
db.version=2

View File

@ -1,3 +1,3 @@
api.current_version=${project.version}
api.min_version=1.0
db.version=1
db.version=2

View File

@ -2,6 +2,7 @@ package com.sismics.docs.rest;
import javax.json.JsonArray;
import javax.json.JsonObject;
import javax.json.JsonValue;
import javax.ws.rs.client.Entity;
import javax.ws.rs.core.Form;
import javax.ws.rs.core.Response;
@ -44,7 +45,8 @@ public class TestTagResource extends BaseJerseyTest {
.cookie(TokenBasedSecurityFilter.COOKIE_NAME, tag1Token)
.put(Entity.form(new Form()
.param("name", "Tag4")
.param("color", "#00ff00")), JsonObject.class);
.param("color", "#00ff00")
.param("parent", tag3Id)), JsonObject.class);
String tag4Id = json.getString("id");
Assert.assertNotNull(tag4Id);
@ -129,6 +131,7 @@ public class TestTagResource extends BaseJerseyTest {
Assert.assertTrue(tags.size() > 0);
Assert.assertEquals("Tag4", tags.getJsonObject(1).getString("name"));
Assert.assertEquals("#00ff00", tags.getJsonObject(1).getString("color"));
Assert.assertEquals(tag3Id, tags.getJsonObject(1).getString("parent"));
// Update a tag
json = target().path("/tag/" + tag4Id).request()
@ -146,6 +149,7 @@ public class TestTagResource extends BaseJerseyTest {
Assert.assertTrue(tags.size() > 0);
Assert.assertEquals("UpdatedName", tags.getJsonObject(1).getString("name"));
Assert.assertEquals("#0000ff", tags.getJsonObject(1).getString("color"));
Assert.assertEquals(JsonValue.NULL, tags.getJsonObject(1).get("parent"));
// Deletes a tag
target().path("/tag/" + tag4Id).request()