Merge pull request #327 from Alfresco/development

1.2.0
This commit is contained in:
Denys Vuika
2018-04-20 15:09:05 +01:00
committed by GitHub
136 changed files with 5021 additions and 12778 deletions
+1 -1
View File
@@ -89,7 +89,7 @@
"component": {},
"serve": {
"proxyConfig": "proxy.conf.js",
"port": 3000,
"port": 4200,
"host": "0.0.0.0"
}
}
+16
View File
@@ -0,0 +1,16 @@
version: 2
jobs:
build:
working_directory: ~/alfresco-content-app
docker:
- image: circleci/node:8-browsers
steps:
- checkout
- restore_cache:
key: alfresco-content-app-{{ .Branch }}-{{ checksum "package.json" }}
- run: npm install
- save_cache:
key: alfresco-content-app-{{ .Branch }}-{{ checksum "package.json" }}
paths:
- "node_modules"
- run: xvfb-run -a npm run test:ci
+11
View File
@@ -0,0 +1,11 @@
.circleci
.git
.github
.vscode
coverage
docs
e2e
e2e-output
node_modules
scripts
src
+5
View File
@@ -0,0 +1,5 @@
ALFRESCO_TAG=6.0.4-ea
SHARE_TAG=6.0.a
SOLR6_TAG=1.1.0
POSTGRES_TAG=10.1
ACA_TAG=development
+53
View File
@@ -0,0 +1,53 @@
<!--
PLEASE HELP US PROCESS GITHUB ISSUES FASTER BY PROVIDING THE FOLLOWING INFORMATION.
ISSUES MISSING IMPORTANT INFORMATION MAY BE CLOSED WITHOUT INVESTIGATION.
-->
## I'm submitting a...
<!-- Check one of the following options with "x" -->
<pre><code>
[ ] Regression (a behavior that used to work and stopped working in a new release)
[ ] Bug report <!-- Please search GitHub for a similar issue or PR before submitting -->
[ ] Feature request
[ ] Documentation issue or request
[ ] Support request => Please do not submit support request here, instead see https://github.com/Alfresco/alfresco-content-app/blob/master/CONTRIBUTING.md#question
</code></pre>
## Current behavior
<!-- Describe how the issue manifests. -->
## Expected behavior
<!-- Describe what the desired behavior would be. -->
## Minimal reproduction of the problem with instructions
<!--
For bug reports please provide the *STEPS TO REPRODUCE* and if possible a *MINIMAL DEMO* of the problem.
-->
## What is the motivation / use case for changing the behavior?
<!-- Describe the motivation or the concrete use case. -->
## Environment
<pre><code>
ACA version: X.Y.Z
<!-- Check whether this is still an issue in the most recent ACA version -->
Browser:
- [ ] Chrome version XX
- [ ] Firefox version XX
- [ ] Safari version XX
- [ ] Edge version XX
- [ ] IE11
For Tooling issues:
- Node version: XX <!-- run `node --version` -->
- Platform: <!-- Mac, Linux, Windows -->
Others:
<!-- Anything else relevant? Operating system version, IDE, package manager, HTTP server, ... -->
</code></pre>
+43
View File
@@ -0,0 +1,43 @@
## PR Checklist
Please check if your PR fulfills the following requirements:
- [ ] The commit message follows our guidelines: https://github.com/Alfresco/alfresco-content-app/blob/master/CONTRIBUTING.md#commit
- [ ] Tests for the changes have been added (for bug fixes / features)
- [ ] Docs have been added / updated (for bug fixes / features)
## PR Type
What kind of change does this PR introduce?
<!-- Please check the one that applies to this PR using "x". -->
```
[ ] Bugfix
[ ] Feature
[ ] Code style update (formatting, local variables)
[ ] Refactoring (no functional changes, no api changes)
[ ] Build related changes
[ ] CI related changes
[ ] Documentation content changes
[ ] Application / Infrastructure changes
[ ] Other... Please describe:
```
## What is the current behavior?
<!-- Please describe the current behavior that you are modifying, or link to a relevant issue. -->
Issue Number: N/A
## What is the new behavior?
## Does this PR introduce a breaking change?
```
[ ] Yes
[ ] No
```
<!-- If this PR contains a breaking change, please describe the impact and migration path for existing applications below. -->
## Other information
+1
View File
@@ -24,6 +24,7 @@
!.vscode/tasks.json
!.vscode/launch.json
!.vscode/extensions.json
.history
# misc
/.sass-cache
+3 -23
View File
@@ -5,29 +5,9 @@ language: node_js
node_js:
- "8"
cache:
directories:
- ./node_modules
services:
- docker
install:
- npm install
- npm install -g npm@latest
- npm ci
script:
- npm run build
- npm run test -- --single-run --no-progress && cat ./coverage/lcov.info | ./node_modules/.bin/codacy-coverage && rm -rf ./coverage
# Send coverage data to codecov
after_success:
- bash <(curl -s https://codecov.io/bash) -X gcov
- export BRANCH=$(if [ "$TRAVIS_PULL_REQUEST" == "false" ]; then echo $TRAVIS_BRANCH; else echo $TRAVIS_PULL_REQUEST_BRANCH; fi)
- echo "TRAVIS_BRANCH=$TRAVIS_BRANCH, PR=$PR, BRANCH=$BRANCH"
- export TAG=`if [ "$BRANCH" == "master" ]; then echo "latest"; else echo $BRANCH ; fi`
- sed -i 's/{:port}/:8080/1' ./dist/app.config.json
- docker build -t $DOCKER_REPO:$TAG .
# Publish extra image based on Travis build number
- docker tag $DOCKER_REPO:$TAG $DOCKER_REPO:travis-$TRAVIS_BUILD_NUMBER
- docker login -u "$DOCKER_USERNAME" -p "$DOCKER_PASSWORD"
- docker push $DOCKER_REPO
- npm run test:ci
+5
View File
@@ -0,0 +1,5 @@
{
"cSpell.words": [
"sidenav"
]
}
+23
View File
@@ -0,0 +1,23 @@
# Contributor Code of Conduct
We want to make everyone feel welcome to contribute to ACA.
We promise to respect everyone who posts issues, updates documentation,
submits pull requests, provides feedback in comments, or makes any other contribution to the project.
We expect all discussion about the project (on GitHub, social media and other channels) to be positive and constructive.
We do not accept personal attacks, trolling, public or private harassment, insults,
or other unprofessional conduct towards anyone.
We promise to show respect to all contributors regardless of their gender, gender identity,
sexual orientation, disability, age, race, ethnicity, religion, or level of experience.
We expect all contributors to understand and follow these standards
If any member of the community does not follow the code of conduct,
the maintainers will do whatever they consider appropriate to stop the unacceptable behavior.
This may include removing issues, comments, and PRs or blocking accounts.
Any bans may be temporary or permanent, at the maintainers discretion.
Contact us if you see any behavior that breaks the code of conduct, whether it is directed at you or at anyone else.
We take responsibility for making sure the code of conduct is clear and understandable,
so you should also contact us if you think there is anything that needs to be explained better.
Our contact email address is: [conduct@alfresco.com](mailto:conduct@alfresco.com)
+147
View File
@@ -0,0 +1,147 @@
# Contributing to Alfresco Content Application (ACA)
Alfresco actively encourages external contributions to this Content Application so that the project can move develop in such a way that benefits the wider community.
As a contributor, here are the guidelines we would like you to follow:
* [Code of Conduct](#coc)
* [Question or Problem?](#question)
* [Issues and Bugs](#issue)
* [Feature Requests](#feature)
* [Submission Guidelines](#submit)
* [Coding Rules](#rules)
* [Signing the CLA](#cla)
## <a name="coc"></a> Code of Conduct
Help us keep Alfresco Content Application open and inclusive.
Please read and follow our [Code of Conduct][coc].
## <a name="question"></a> Got a Question or Problem?
Please do not open issues for general support questions as we want to keep GitHub issues for bug reports and feature requests.
The ADF [Gitter][gitter] channel is a much better place to ask questions:
* direct access to many experienced Angular and ADF developers
* questions and answers stay available for public viewing so your question / answer might help someone else
* all discussions are saved and indexed, you can search for previous answers
## <a name="issue"></a> Found a Bug?
If you find a bug in the source code, you can help us by:
* [submitting an issue](#submit-issue) to our [ACA JIRA project][jira]
* [submitting an issue](#submit-issue) to our [GitHub Repository][github].
Even better, [submit a Pull Request](#submit-pr) with a fix.
## <a name="feature"></a> Missing a Feature?
You can *request* a new feature by [submitting an issue](#submit-issue)
to our [JIRA project][jira] or GitHub Repository.
If you would like to *implement* a new feature, please submit an issue with a proposal for your work first,
to be sure that we can use it.
Please consider what kind of change it is:
* For a **Major Feature**, first open an issue and outline your proposal so that it can be
discussed. This will also allow us to better coordinate our efforts, prevent duplication of work,
and help you to craft the change so that it is successfully accepted into the project.
* **Small Features** can be crafted and directly [submitted as a Pull Request](#submit-pr).
## <a name="submit"></a> Submission Guidelines
Before you submit an issue, search the issue tracker, an issue for your problem may already exist
and the discussion might inform you of potential workarounds.
We want to fix all the issues as soon as possible, but before fixing a bug we need to reproduce and confirm it.
In order to reproduce bugs, we require you to provide a description of the problem, steps to reproduce and other supporting information that will help us recreate the problem you experianced.
Having a reproducible scenario gives us a wealth of important information without going back & forth to you with additional questions like:
* version of node.js used
* version of NPM used
* version of Angular used
* versions of ADF libraries used
* 3rd-party libraries and their versions
* and most importantly - a use-case that fails
You can file new issues by filling out our [new issue form](https://github.com/Alfresco/alfresco-content-app/issues/new).
### <a name="submit-pr"></a> Submitting a Pull Request (PR)
Before you submit your Pull Request (PR) consider the following guidelines:
1. Search [GitHub](https://github.com/Alfresco/alfresco-content-app/pulls) for an open or closed PR
that relates to your submission. You don't want to duplicate effort.
1. Sign the [Contributor License Agreement (CLA)](#cla) before sending PRs.
We cannot accept code without this.
Make sure you sign with the primary email address of the Git identity that has been granted access to the Angular repository.
1. Fork the ACA repository.
1. Make your changes in a new git branch based on **development**:
```shell
git checkout -b my-fix-branch development
```
1. Create your patch, **including appropriate test cases**.
1. Follow our [Coding Rules](#rules).
1. Run the full ACA test suite and ensure that all tests pass.
1. Commit your changes using a descriptive commit message.
1. Push your branch to GitHub:
```shell
git push origin my-fix-branch
```
1. In GitHub, send a pull request to `alfresco-content-app:development`.
* If we suggest changes then:
* Make the required updates.
* Re-run the ACA test suites to ensure tests are still passing.
* Rebase your branch and force push to your GitHub repository (this will update your Pull Request):
```shell
git rebase development -i
git push -f
```
That's it! Thank you for your contribution!
## <a name="rules"></a> Coding Rules
To ensure consistency throughout the source code, keep these rules in mind as you are working:
* All features or bug fixes **must be tested** by one or more specs (unit-tests).
* All public API methods **must be documented**.
## <a name="cla"></a> Signing the CLA
Sign the [Contributor License Agreement (CLA)][cla] before sending pull requests.
In addition, you will get the CLA link automatically generated for your first pull request.
For any code changes to be accepted, the CLA must be signed.
<hr>
If you have more than one Git identity, you must make sure that you sign the CLA using the primary
email address associated with the ID that has been granted access to the Angular repository.
Git identities can be associated with more than one email address, and only one is primary.
Here are some links to help you sort out multiple Git identities and email addresses:
* https://help.github.com/articles/setting-your-commit-email-address-in-git/
* https://stackoverflow.com/questions/37245303/what-does-usera-committed-with-userb-13-days-ago-on-github-mean
* https://help.github.com/articles/about-commit-email-addresses/
* https://help.github.com/articles/blocking-command-line-pushes-that-expose-your-personal-email-address/
Note that if you have more than one Git identity, it is important to verify that you are logged in
with the same ID with which you signed the CLA, before you commit changes. If not, your PR will fail the CLA check.
<hr>
[coc]: https://github.com/Alfresco/alfresco-content-app/blob/master/CODE_OF_CONDUCT.md
[github]: https://github.com/Alfresco/alfresco-content-app
[gitter]: https://gitter.im/Alfresco/alfresco-ng2-components
[jira]: https://issues.alfresco.com/jira/projects/ACA
[cla]: https://cla-assistant.io/Alfresco/alfresco-content-app
+2
View File
@@ -1,4 +1,6 @@
FROM nginx:alpine
LABEL version="1.2"
LABEL maintainer="Denys Vuika <denys.vuika@alfresco.com>"
COPY nginx.conf /etc/nginx/nginx.conf
+13 -25
View File
@@ -27,18 +27,21 @@ please include a clear description, steps to reproduce and screenshots where app
All issues will be reviewed; bugs will be categorized if reproducible and enhancement/feature suggestions
will be considered against existing priorities if the use case serves a general-purpose need.
## Want to help?
Want to file a bug, contribute some code, or improve documentation? Excellent!
Read up on our guidelines for [contributing][contributing]
and then check out one of our issues in the [Jira][jira] or [GitHub][github]
## Development server
Run `npm start` for a dev server. Navigate to `http://localhost:3000/` (opens by default).
Run `npm start` for a dev server. Navigate to `http://localhost:4200/` (opens by default).
The app will automatically reload if you change any of the source files.
## Code scaffolding
Run `ng generate component component-name` to generate a new component. You can also use `ng generate directive|pipe|service|class|guard|interface|enum|module`.
## Build
Run `ng build` to build the project. The build artifacts will be stored in the `dist/` directory. Use the `-prod` flag for a production build.
Run `ng build` to build the project. The build artifacts will be stored in the `dist/` directory.
Use the `--prod` flag for a production build.
## Running unit tests
@@ -48,25 +51,10 @@ Run `ng test` to execute the unit tests via [Karma](https://karma-runner.github.
Run `ng e2e` to execute the end-to-end tests via [Protractor](http://www.protractortest.org/).
## Running documentation locally
For development purposes, you can run and test documentation locally.
That is useful when working in different branches instead of a `master` one.
Run the following command to install the lightweight development server [wsrv](https://denysvuika.gitlab.io/wsrv/#/):
```sh
npm install -g wsrv
```
Now you can use the next command to serve the documentation folder in the browser:
```sh
wsrv docs/ -s -l -o
```
The browser page is going to automatically reload upon changes.
## Further help
To get more help on the Angular CLI use `ng help` or go check out the [Angular CLI README](https://github.com/angular/angular-cli/blob/master/README.md).
[contributing]: ttps://github.com/Alfresco/alfresco-content-app/blob/master/CONTRIBUTING.md
[github]: https://github.com/Alfresco/alfresco-content-app/issues
[jira]: https://issues.alfresco.com/jira/projects/ACA
+91 -4
View File
@@ -1,8 +1,95 @@
version: '3.1'
version: "3"
services:
app:
image: 'alfresco/content-app'
build: '.'
alfresco:
image: alfresco/alfresco-content-repository-community:${ALFRESCO_TAG}
depends_on:
- postgres
environment:
CATALINA_OPTS : "
-agentlib:jdwp=transport=dt_socket,address=8000,server=y,suspend=n
"
JAVA_OPTS : "
-Ddb.driver=org.postgresql.Driver
-Ddb.username=alfresco
-Ddb.password=alfresco
-Ddb.url=jdbc:postgresql://postgres:5432/alfresco
-Dsolr.host=solr6
-Dsolr.port=8983
-Dsolr.secureComms=none
-Dsolr.base.url=/solr
-Dindex.subsystem.name=solr6
"
networks:
- internal
ports:
- 8080:8080 #Browser port
- 8000:8000 #Debug port
share:
image: alfresco/alfresco-share:${SHARE_TAG}
depends_on:
- alfresco
environment:
- REPO_HOST=alfresco
- REPO_PORT=8080
networks:
- internal
ports:
- 8081:8080
postgres:
image: postgres:${POSTGRES_TAG}
environment:
- POSTGRES_PASSWORD=alfresco
- POSTGRES_USER=alfresco
- POSTGRES_DB=alfresco
networks:
- internal
ports:
- 5432:5432
solr6:
image: alfresco/alfresco-search-services:${SOLR6_TAG}
depends_on:
- alfresco
environment:
#Solr needs to know how to register itself with Alfresco
- SOLR_ALFRESCO_HOST=alfresco
- SOLR_ALFRESCO_PORT=8080
#Alfresco needs to know how to call solr
- SOLR_SOLR_HOST=solr6
- SOLR_SOLR_PORT=8983
#Create the default alfresco and archive cores
- SOLR_CREATE_ALFRESCO_DEFAULTS=alfresco,archive
networks:
- internal
ports:
- 8983:8983 #Browser port
content-app:
image: alfresco/alfresco-content-app:${ACA_TAG}
build: .
depends_on:
- alfresco
networks:
- internal
ports:
- 3001:80
# volumes:
# - ./app.config.json:/usr/share/nginx/html/app.config.json
# - ./nginx.conf:/etc/nginx/conf.d/default.conf
proxy:
image: nginx
depends_on:
- content-app
volumes:
- ./docker-compose/nginx.conf:/etc/nginx/conf.d/default.conf
networks:
- internal
ports:
- 3000:80
networks:
internal:
+5
View File
@@ -0,0 +1,5 @@
ALFRESCO_TAG=6.0.4-ea
SHARE_TAG=6.0.a
SOLR6_TAG=1.1.0
POSTGRES_TAG=10.1
ACA_TAG=development
+15
View File
@@ -0,0 +1,15 @@
# ACA with ACS Community 6.0ea
To run ACA together with the latest ACS community (6.0) use the following command:
```sh
docker-compose up
```
The ACA is served on the port 3000.
If you want to teardown the environment, use the following command:
```sh
docker-compose down
```
+94
View File
@@ -0,0 +1,94 @@
version: "3"
services:
alfresco:
image: alfresco/alfresco-content-repository-community:${ALFRESCO_TAG}
depends_on:
- postgres
environment:
CATALINA_OPTS : "
-agentlib:jdwp=transport=dt_socket,address=8000,server=y,suspend=n
"
JAVA_OPTS : "
-Ddb.driver=org.postgresql.Driver
-Ddb.username=alfresco
-Ddb.password=alfresco
-Ddb.url=jdbc:postgresql://postgres:5432/alfresco
-Dsolr.host=solr6
-Dsolr.port=8983
-Dsolr.secureComms=none
-Dsolr.base.url=/solr
-Dindex.subsystem.name=solr6
"
networks:
- internal
ports:
- 8080:8080 #Browser port
- 8000:8000 #Debug port
share:
image: alfresco/alfresco-share:${SHARE_TAG}
depends_on:
- alfresco
environment:
- REPO_HOST=alfresco
- REPO_PORT=8080
networks:
- internal
ports:
- 8081:8080
postgres:
image: postgres:${POSTGRES_TAG}
environment:
- POSTGRES_PASSWORD=alfresco
- POSTGRES_USER=alfresco
- POSTGRES_DB=alfresco
networks:
- internal
ports:
- 5432:5432
solr6:
image: alfresco/alfresco-search-services:${SOLR6_TAG}
depends_on:
- alfresco
environment:
#Solr needs to know how to register itself with Alfresco
- SOLR_ALFRESCO_HOST=alfresco
- SOLR_ALFRESCO_PORT=8080
#Alfresco needs to know how to call solr
- SOLR_SOLR_HOST=solr6
- SOLR_SOLR_PORT=8983
#Create the default alfresco and archive cores
- SOLR_CREATE_ALFRESCO_DEFAULTS=alfresco,archive
networks:
- internal
ports:
- 8983:8983 #Browser port
content-app:
image: alfresco/alfresco-content-app:${ACA_TAG}
depends_on:
- alfresco
networks:
- internal
ports:
- 3001:80
# volumes:
# - ./app.config.json:/usr/share/nginx/html/app.config.json
# - ./nginx.conf:/etc/nginx/conf.d/default.conf
proxy:
image: nginx
depends_on:
- content-app
volumes:
- ./nginx.conf:/etc/nginx/conf.d/default.conf
networks:
- internal
ports:
- 3000:80
networks:
internal:
+43
View File
@@ -0,0 +1,43 @@
server {
listen *:80;
set $allowOriginSite *;
proxy_pass_request_headers on;
proxy_pass_header Set-Cookie;
location / {
proxy_pass http://content-app;
proxy_next_upstream error timeout invalid_header http_500 http_502 http_503 http_504;
proxy_redirect off;
proxy_buffering off;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_pass_header Set-Cookie;
}
location /alfresco/ {
proxy_pass http://alfresco:8080;
proxy_next_upstream error timeout invalid_header http_500 http_502 http_503 http_504;
proxy_redirect off;
proxy_buffering off;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_pass_header Set-Cookie;
}
location /share/ {
proxy_pass http://share:8080;
proxy_next_upstream error timeout invalid_header http_500 http_502 http_503 http_504;
proxy_redirect off;
proxy_buffering off;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_pass_header Set-Cookie;
}
}
+1 -1
View File
@@ -17,7 +17,7 @@ with a simple and easy to use interface for working with files stored in the Alf
This application uses the latest releases from Alfresco:
- [Alfresco ADF version 2.2](https://community.alfresco.com/community/application-development-framework/pages/get-started)
- [Alfresco ADF version 2.3](https://community.alfresco.com/community/application-development-framework/pages/get-started)
- [Alfresco Content Services version 5.2.3](https://www.alfresco.com/platform/content-services-ecm)
- [Alfresco Community Edition 201802 EA](https://www.alfresco.com/products/community/download)
+1 -1
View File
@@ -18,7 +18,7 @@ npm install
npm start
```
The application runs at port 3000 by default, and should automatically open in the default browser once project compilation finishes.
The application runs at port 4200 by default, and should automatically open in the default browser once project compilation finishes.
## Proxy settings
+4 -4
View File
@@ -21,11 +21,11 @@ The example below demonstrates the most common dynamic format for development en
The configuration above assumes you are running ACS and Content App on the same server and port
and allows deploying to different servers having the same unified configuration file.
For example, a proxy server at `localhost:3000` hosting the Content App as the root application,
and `localhost:3000/alfresco` for the ACS repository.
For example, a proxy server at `localhost:4200` hosting the Content App as the root application,
and `localhost:4200/alfresco` for the ACS repository.
At runtime, the application is going to automatically substitute the "{hostname}" value with the original hostname.
Optionally it can also use the value of the original port if present, for example, "3000" at local machines, or skip the value for port 80.
Optionally it can also use the value of the original port if present, for example, "4200" at local machines, or skip the value for port 80.
### Static address
@@ -33,7 +33,7 @@ Alternatively, you can provide a static address for the ACS server if necessary:
```json
{
"ecmHost": "http://localhost:3000",
"ecmHost": "http://localhost:4200",
...
}
```
+8
View File
@@ -128,6 +128,14 @@ actions are automatically hidden when the user does not have permission.
once the delete action has completed the user is notified and can undo the action (which restores the items from the trash).
</td>
</tr>
<tr>
<td>Manage Versions</td>
<td>
Versions of files can be viewed, uploaded, restored, downloaded and deleted by using the version manager dialog;
once each action has completed the list of versions is updated according to the change.
</td>
<td>Not applicable</td>
</tr>
</tbody>
</table>
+2 -2
View File
@@ -77,7 +77,7 @@ To perform a cleanup operation, use the next command:
docker-compose down --rmi all
```
Navigate to "http://localhost:3000" to access the running application.
Navigate to "http://localhost:4200" to access the running application.
<p class="warning">
Please keep in mind that you should manually build the project every time you want to publish the image or run it locally with the container.
@@ -101,7 +101,7 @@ npm run build:dev
docker-compose up
```
Navigate to "http://localhost:3000" to access the running application.
Navigate to "http://localhost:4200" to access the running application.
To perform a cleanup operation, use the next command:
+1 -1
View File
@@ -9,7 +9,7 @@ The Header & Toolbar section of the viewer contains a number of features that re
- Close 'X' will return the user to the folder that contains the file.
- The name and file type icon is shown in the middle.
- Next and previous buttons will be displayed either side of the file name so that users can navigate to other files in the folder without navigating away from the viewer.
- Finally, on the right hand side an actions toolbar provides users with the ability to download, favorite, move, copy and delete.
- Finally, on the right hand side an actions toolbar provides users with the ability to download, favorite, move, copy, delete, manage versions and view info panel.
#### Content (2)
The File Viewer consists of four separate views that handle displaying the content based on four types of content, covering various [file/mime](https://alfresco.github.io/adf-component-catalog/components/ViewerComponent.html#supported-file-formats) types:
Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.2 MiB

After

Width:  |  Height:  |  Size: 1.3 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 101 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 49 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 92 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 81 KiB

+9 -1
View File
@@ -24,7 +24,7 @@
items: [
{
title: 'Introduction',
path: 'features',
path: 'features'
},
{
title: 'Application Header',
@@ -41,6 +41,14 @@
{
title: 'File Viewer',
path: 'file-viewer'
},
{
title: 'Info Drawer',
path: 'info-drawer'
},
{
title: 'Version Manager',
path: 'version-manager'
}
]
},
+23
View File
@@ -0,0 +1,23 @@
### Info Drawer
The Info Drawer displays node information in the right sidebar panel. It is created by using the [InfoDrawerComponent](https://alfresco.github.io/adf-component-catalog/components/InfoDrawerComponent.html). This info is available for both folder and file nodes.
Currently, there are 2 tabs available: Properties and Versions.
#### Properties tab
The Properties tab displays the node's metadata info by using the [ContentMetadataCardComponent](https://alfresco.github.io/adf-component-catalog/components/ContentMetadataCardComponent.html).
![](images/content-metadata.png)
For more information, please check also the ADF's [ContentMetadataComponent](https://alfresco.github.io/adf-component-catalog/components/ContentMetadataComponent.html).
#### Versions tab
The Versions tab displays info about the node's versions and allows users to [manage versions](/version-manager), according to their permissions. Only the file nodes have version data available.
![](images/version-manager-tab.png)
It uses the [VersionManagerComponent](https://alfresco.github.io/adf-component-catalog/components/VersionManagerComponent.html) from ADF framework.
Managing versions of a file can be possible also by accessing the 'Manage Versions' option from the 'More actions' menu. For more info on manage versions, please check the [version manager](/version-manager) page.
+33
View File
@@ -0,0 +1,33 @@
## Version Manager
The versions of a file can be viewed & managed by using the [VersionManagerComponent](https://alfresco.github.io/adf-component-catalog/components/VersionManagerComponent.html).
There are 2 ways users can access the Version Manager:
1. From the 'Manage Versions' option of the 'More actions' menu (check [Actions and the Actions Toolbar](/doc-list?id=actions-and-the-actions-toolbar)):
![](images/version-manager-action.png)
![](images/version-manager-dialog.png)
2. From the [Info Drawer](/info-drawer) (the Details right panel):
![](images/version-manager-tab.png)
### Upload new version
A new version for the selected file can be added by using this button. There is a restriction currently to only upload files of the same extension as the old version. The new version file will be automatically renamed to have the same name as the old version has. Please also check the [UploadVersionButtonComponent](https://alfresco.github.io/adf-component-catalog/components/UploadVersionButtonComponent.html).
### Actions Menu
Each item in the version list has a couple of actions available: Restore, Download and Delete. These are displayed if user has permission to do that specific action. The 'Download' and 'Delete' can be also disabled from the app.config.
In the app.config.json file, these are the current settings for the ACA version manager:
```
"adf-version-manager": {
"allowComments": true,
"allowDownload": true,
"allowDelete": true
},
...
```
Set the allowComments to false if the version comments should not be displayed on the version list.
Clicking to delete a version of a file triggers a confirmation dialog. Please see the [ConfirmDialogComponent](https://alfresco.github.io/adf-component-catalog/components/ConfirmDialogComponent.html) for more info.
+975 -3350
View File
File diff suppressed because it is too large Load Diff
+9 -6
View File
@@ -5,19 +5,22 @@
"scripts": {
"ng": "ng",
"start": "npm run server-versions && ng serve --open",
"build": "npm run server-versions && ng build",
"start:prod": "npm run server-versions && ng serve --prod --open",
"start:docker": "docker-compose up --build",
"build": "npm run server-versions && ng build --prod",
"build:prod": "npm run server-versions && ng build --prod",
"build:dev": "npm run server-versions && ng build && node postbuild-dev.js",
"build:dev": "npm run server-versions && ng build",
"build:tomcat": "npm run server-versions && ng build --base-href ./",
"test": "ng test --code-coverage",
"test:ci": "ng test --code-coverage --single-run --no-progress && cat ./coverage/lcov.info | ./node_modules/.bin/codacy-coverage && rm -rf ./coverage",
"lint": "ng lint",
"e2e": "ng e2e",
"server-versions": "rimraf ./src/versions.json && npm list --depth=0 --json=true --prod=true > ./src/versions.json || exit 0"
},
"private": true,
"dependencies": {
"@alfresco/adf-content-services": "2.2.0",
"@alfresco/adf-core": "2.2.0",
"@alfresco/adf-content-services": "2.3.0",
"@alfresco/adf-core": "2.3.0",
"@angular/animations": "5.1.1",
"@angular/cdk": "5.0.1",
"@angular/common": "5.1.1",
@@ -34,7 +37,7 @@
"@mat-datetimepicker/core": "1.0.1",
"@mat-datetimepicker/moment": "1.0.1",
"@ngx-translate/core": "9.1.1",
"alfresco-js-api": "2.2.0",
"alfresco-js-api": "2.3.0",
"core-js": "2.5.3",
"hammerjs": "2.0.8",
"moment-es6": "1.0.0",
@@ -44,7 +47,7 @@
"zone.js": "0.8.20"
},
"devDependencies": {
"@angular/cli": "1.6.6",
"@angular/cli": "1.7.3",
"@angular/compiler-cli": "5.1.1",
"@angular/language-service": "5.1.1",
"@types/jasmine": "^2.5.53",
-34
View File
@@ -1,34 +0,0 @@
/*!
* @license
* Alfresco Example Content Application
*
* Copyright (C) 2005 - 2018 Alfresco Software Limited
*
* This file is part of the Alfresco Example Content Application.
* If the software was purchased under a paid Alfresco license, the terms of
* the paid license agreement will prevail. Otherwise, the software is
* provided under the following open source license terms:
*
* The Alfresco Example Content Application is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* The Alfresco Example Content Application is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with Alfresco. If not, see <http://www.gnu.org/licenses/>.
*/
var fs = require('fs');
var config = require('./dist/app.config.json');
config.ecmHost = 'http://localhost:8080';
fs.writeFileSync(
'./dist/app.config.json',
JSON.stringify(config, null, 4)
);
+1 -1
View File
@@ -28,7 +28,7 @@ exports.config = {
directConnect: true,
baseUrl: 'http://localhost:3000',
baseUrl: 'http://localhost:4200',
framework: 'jasmine2',
jasmineNodeOpts: {
+61
View File
@@ -0,0 +1,61 @@
#!/bin/bash
show_help() {
echo "Usage: docker_publish.sh"
echo ""
echo "--access_key rancher access key"
echo "--secret_key rancher secret key"
echo "--url rancher_url"
echo "--environment_name service name to replace in rancher"
echo "--image image to gater and load in the service, example: docker:alfresco/demo-shell:latest"
}
access_key(){
ACCESSKEY=$1
}
secret_key(){
SECRETKEY=$1
}
url(){
RANCHERURL=$1
}
environment_name(){
ENVIRONMENTNAME=$1
}
image_name(){
IMAGE=$1
}
while [[ $1 == -* ]]; do
case "$1" in
-h|--help|-\?) show_help; exit 0;;
--access_key) access_key $2; shift 2;;
--secret_key) secret_key $2; shift 2;;
--url) url $2; shift 2;;
--environment_name) environment_name $2; shift 2;;
--image) image_name $2; shift 2;;
-*) echo "invalid option: $1" 1>&2; show_help; exit 1;;
esac
done
docker pull etlweather/gaucho:alpine
docker=$(which docker)
echo "getting the id"
ENVIRONMENTID=$($docker run --rm -e CATTLE_ACCESS_KEY=$ACCESSKEY \
-e CATTLE_SECRET_KEY=$SECRETKEY \
-e CATTLE_URL=$RANCHERURL \
etlweather/gaucho:alpine id_of $ENVIRONMENTNAME)
echo "id retrieved! is ${environment_id}"
$docker run --rm -e CATTLE_ACCESS_KEY=$ACCESSKEY \
-e CATTLE_SECRET_KEY=$SECRETKEY \
-e CATTLE_URL=$RANCHERURL \
etlweather/gaucho:alpine upgrade $ENVIRONMENTID --imageUuid $IMAGE --auto_complete true --timeout 600
+134
View File
@@ -0,0 +1,134 @@
#!/usr/bin/env bash
DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"
eval JS_API=true
eval GNU=false
eval EXEC_COMPONENT=true
eval DIFFERENT_JS_API=false
eval AUTO=false
eval libs=( "core"
"content-services"
#"process-services"
#"insights"
)
cd ${DIR}/..
prefix="@alfresco/adf-"
show_help() {
echo "Usage: update-version.sh"
echo ""
echo "-sj or -sjsapi don't update js-api version"
echo "-vj or -versionjsapi to use a different version of js-api"
echo "-v or -version version to update"
echo "-alpha update last alpha version of js-api and lib automatically"
echo "-beta update beta alpha version of js-api and lib automatically"
echo "-gnu for gnu"
}
skip_js() {
echo "====== Skip JS-API change version $1 ====="
JS_API=false
}
last_alpha_mode() {
echo "====== Auto find last ALPHA version ====="
VERSION=$(npm view @alfresco/adf-core@alpha version)
echo "====== version lib ${VERSION} ====="
DIFFERENT_JS_API=true
VERSION_JS_API=$(npm view alfresco-js-api@alpha version)
echo "====== version js-api ${DIFFERENT_JS_API} ====="
}
last_beta_mode() {
echo "====== Auto find last BETA version ====="
VERSION=$(npm view @alfresco/adf-core@beta version)
echo "====== version lib ${VERSION} ====="
DIFFERENT_JS_API=true
VERSION_JS_API=$(npm view alfresco-js-api@beta version)
echo "====== version js-api ${DIFFERENT_JS_API} ====="
}
gnu_mode() {
echo "====== GNU MODE ====="
GNU=true
}
version_change() {
echo "====== New version $1 ====="
VERSION=$1
}
version_js_change() {
echo "====== Alfresco JS-API version $1 ====="
VERSION_JS_API=$1
DIFFERENT_JS_API=true
}
update_component_dependency_version(){
for (( j=0; j<${libslength}; j++ ));
do
echo "====== UPDATE ${prefix}${libs[$j]} to ${VERSION}======"
EXACT_VERSION="${prefix}${libs[$j]}@${VERSION}"
npm install -E ${EXACT_VERSION}
done
}
update_component_js_version(){
echo "====== UPDATE alfresco-js-api to ${1} ======"
PACKAGETOCHANGE="alfresco-js-api"
EXACT_VERSION="${PACKAGETOCHANGE}@${1}"
npm install -E ${EXACT_VERSION}
}
while [[ $1 == -* ]]; do
case "$1" in
-h|--help|-\?) show_help; exit 0;;
-v|version) version_change $2; shift 2;;
-sj|sjsapi) skip_js; shift;;
-vj|versionjsapi) version_js_change $2; shift 2;;
-gnu) gnu_mode; shift;;
-alpha) last_alpha_mode; shift;;
-beta) last_beta_mode; shift;;
-*) shift;;
esac
done
if $GNU; then
sedi='-i'
else
sedi=('-i' '')
fi
if [[ "${VERSION}" == "" ]]
then
echo "Version number required"
exit 1
fi
projectslength=${#projects[@]}
libslength=${#libs[@]}
if $EXEC_COMPONENT == true; then
echo "====== UPDATE ======"
update_component_dependency_version
if $JS_API == true; then
if $DIFFERENT_JS_API == true; then
update_component_js_version ${VERSION_JS_API}
else
update_component_js_version ${VERSION}
fi
fi
fi
+148 -2
View File
@@ -2,11 +2,12 @@
"ecmHost": "http://{hostname}{:port}",
"application": {
"name": "Alfresco Example Content Application",
"logo": "/assets/images/alfresco-logo-white.svg"
"logo": "assets/images/alfresco-logo-white.svg"
},
"headerColor": "#2196F3",
"languagePicker": false,
"pagination": {
"size": 25,
"supportedPageSizes": [
25,
50,
@@ -21,6 +22,11 @@
".git"
]
},
"adf-version-manager": {
"allowComments": true,
"allowDownload": true,
"allowDelete": true
},
"navigation": {
"main": [
{
@@ -126,5 +132,145 @@
"key": "zh-CN",
"label": "Simplified Chinese"
}
]
],
"content-metadata": {
"presets": {
"custom": [
{
"title": "APP.CONTENT_METADATA.EXIF_GROUP_TITLE",
"items": [
{
"aspect": "exif:exif",
"properties": [
"exif:pixelXDimension",
"exif:pixelYDimension",
"exif:dateTimeOriginal",
"exif:exposureTime",
"exif:fNumber",
"exif:flash",
"exif:focalLength",
"exif:isoSpeedRatings",
"exif:orientation",
"exif:manufacturer",
"exif:model",
"exif:software"
]
}
]
}
]
}
},
"search": {
"limits": {
"permissionEvaluationTime": null,
"permissionEvaluationCount": null
},
"filterQueries": [
{ "query": "TYPE:'cm:folder' OR TYPE:'cm:content'" },
{ "query": "NOT cm:creator:System" }
],
"facetFields": {
"facets": [
{ "field": "content.mimetype", "mincount": 1, "label": "Type" },
{ "field": "content.size", "mincount": 1, "label": "Size" },
{ "field": "creator", "mincount": 1, "label": "Creator" },
{ "field": "modifier", "mincount": 1, "label": "Modifier" }
]
},
"facetQueries": [
{ "query": "created:2018", "label": "Created This Year" },
{ "query": "content.mimetype", "label": "Type" },
{ "query": "content.size:[0 TO 10240]", "label": "Size: xtra small"},
{ "query": "content.size:[10240 TO 102400]", "label": "Size: small"},
{ "query": "content.size:[102400 TO 1048576]", "label": "Size: medium" },
{ "query": "content.size:[1048576 TO 16777216]", "label": "Size: large" },
{ "query": "content.size:[16777216 TO 134217728]", "label": "Size: xtra large" },
{ "query": "content.size:[134217728 TO MAX]", "label": "Size: XX large" }
],
"query": {
"categories": [
{
"id": "broken",
"name": "Broken Facet",
"enabled": false,
"expanded": false,
"component": {
"selector": "adf-search-text",
"settings": {
"field": "fieldname"
}
}
},
{
"id": "queryName",
"name": "Name",
"enabled": true,
"expanded": true,
"component": {
"selector": "adf-search-text",
"settings": {
"pattern": "cm:name:'(.*?)'",
"field": "cm:name",
"placeholder": "Enter the name"
}
}
},
{
"id": "queryFields",
"name": "Fields",
"enabled": true,
"expanded": false,
"component": {
"selector": "adf-search-fields",
"settings": {
"field": null,
"options": [
{ "name": "Name", "value": "name", "fields": ["name"], "default": true },
{ "name": "File Size", "value": "content.sizeInBytes", "fields": ["content"], "default": true },
{ "name": "Modified On", "value": "modifiedAt", "fields": ["modifiedAt"], "default": true },
{ "name": "Modified By", "value": "modifiedByUser.displayName", "fields": ["modifiedByUser"], "default": true }
]
}
}
},
{
"id": "queryType",
"name": "Type",
"enabled": true,
"expanded": false,
"component": {
"selector": "adf-search-radio",
"settings": {
"field": null,
"options": [
{ "name": "None", "value": "", "default": true },
{ "name": "All", "value": "TYPE:'cm:folder' OR TYPE:'cm:content'" },
{ "name": "Folder", "value": "TYPE:'cm:folder'" },
{ "name": "Document", "value": "TYPE:'cm:content'" }
]
}
}
},
{
"id": "queryLocations",
"name": "Locations",
"enabled": true,
"expanded": false,
"component": {
"selector": "adf-search-scope-locations",
"settings": {
"field": null,
"options": [
{ "name": "Default", "value": "nodes", "default": true },
{ "name": "Nodes", "value": "nodes" },
{ "name": "Deleted Nodes", "value": "deleted-nodes" },
{ "name": "Versions", "value": "versions" }
]
}
}
}
]
}
}
}
+1
View File
@@ -42,6 +42,7 @@ export class AppComponent implements OnInit {
config: AppConfigService) {
// TODO: remove once ADF 2.3.0 is out (needs bug fixes)
preferences.defaults.supportedPageSizes = config.get('pagination.supportedPageSizes');
preferences.defaults.paginationSize = config.get('pagination.size');
}
ngOnInit() {
+55 -12
View File
@@ -26,11 +26,10 @@
import { BrowserModule } from '@angular/platform-browser';
import { NgModule } from '@angular/core';
import { RouterModule } from '@angular/router';
import { TRANSLATION_PROVIDER } from '@alfresco/adf-core';
import { AdfModule } from './adf.module';
import { CommonModule } from './common/common.module';
import { MaterialModule } from './common/material.module';
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
import { FormsModule, ReactiveFormsModule } from '@angular/forms';
import { TRANSLATION_PROVIDER, CoreModule } from '@alfresco/adf-core';
import { ContentModule } from '@alfresco/adf-content-services';
import { AppComponent } from './app.component';
import { APP_ROUTES } from './app.routes';
@@ -47,22 +46,46 @@ import { TrashcanComponent } from './components/trashcan/trashcan.component';
import { LayoutComponent } from './components/layout/layout.component';
import { HeaderComponent } from './components/header/header.component';
import { CurrentUserComponent } from './components/current-user/current-user.component';
import { SearchComponent } from './components/search/search.component';
import { SearchInputComponent } from './components/search-input/search-input.component';
import { SidenavComponent } from './components/sidenav/sidenav.component';
import { AboutComponent } from './components/about/about.component';
import { LocationLinkComponent } from './components/location-link/location-link.component';
import { EmptyFolderComponent } from './components/empty-folder/empty-folder.component';
import { NodeCopyDirective } from './common/directives/node-copy.directive';
import { NodeDeleteDirective } from './common/directives/node-delete.directive';
import { NodeMoveDirective } from './common/directives/node-move.directive';
import { NodeRestoreDirective } from './common/directives/node-restore.directive';
import { NodePermanentDeleteDirective } from './common/directives/node-permanent-delete.directive';
import { NodeUnshareDirective } from './common/directives/node-unshare.directive';
import { NodeInfoDirective } from './common/directives/node-info.directive';
import { NodeVersionsDirective } from './common/directives/node-versions.directive';
import { AppConfigPipe } from './common/pipes/app-config.pipe';
import { VersionManagerDialogAdapterComponent } from './components/versions-dialog/version-manager-dialog-adapter.component';
import { BrowsingFilesService } from './common/services/browsing-files.service';
import { ContentManagementService } from './common/services/content-management.service';
import { NodeActionsService } from './common/services/node-actions.service';
import { NodePermissionService } from './common/services/node-permission.service';
import { MatMenuModule, MatIconModule, MatButtonModule, MatDialogModule, MatInputModule } from '@angular/material';
import { SearchComponent } from './components/search/search.component';
import { NodeDownloadDirective } from './common/directives/node-download.directive';
@NgModule({
imports: [
BrowserModule,
BrowserAnimationsModule,
FormsModule,
ReactiveFormsModule,
RouterModule.forRoot(APP_ROUTES, {
useHash: true,
enableTracing: false // enable for debug only
}),
AdfModule,
CommonModule,
MaterialModule
MatMenuModule,
MatIconModule,
MatButtonModule,
MatDialogModule,
MatInputModule,
CoreModule,
ContentModule
],
declarations: [
AppComponent,
@@ -71,7 +94,7 @@ import { EmptyFolderComponent } from './components/empty-folder/empty-folder.com
LayoutComponent,
HeaderComponent,
CurrentUserComponent,
SearchComponent,
SearchInputComponent,
SidenavComponent,
FilesComponent,
FavoritesComponent,
@@ -82,7 +105,20 @@ import { EmptyFolderComponent } from './components/empty-folder/empty-folder.com
PreviewComponent,
AboutComponent,
LocationLinkComponent,
EmptyFolderComponent
EmptyFolderComponent,
NodeCopyDirective,
NodeDeleteDirective,
NodeMoveDirective,
NodeRestoreDirective,
NodePermanentDeleteDirective,
NodeUnshareDirective,
NodeInfoDirective,
NodeVersionsDirective,
AppConfigPipe,
VersionManagerDialogAdapterComponent,
SearchComponent,
// Workarounds for ADF 2.3.0
NodeDownloadDirective
],
providers: [
{
@@ -92,7 +128,14 @@ import { EmptyFolderComponent } from './components/empty-folder/empty-folder.com
name: 'app',
source: 'assets'
}
}
},
BrowsingFilesService,
ContentManagementService,
NodeActionsService,
NodePermissionService
],
entryComponents: [
VersionManagerDialogAdapterComponent
],
bootstrap: [AppComponent]
})
+5
View File
@@ -39,6 +39,7 @@ import { AboutComponent } from './components/about/about.component';
import { LoginComponent } from './components/login/login.component';
import { PreviewComponent } from './components/preview/preview.component';
import { GenericErrorComponent } from './components/generic-error/generic-error.component';
import { SearchComponent } from './components/search/search.component';
export const APP_ROUTES: Routes = [
{
@@ -215,6 +216,10 @@ export const APP_ROUTES: Routes = [
i18nTitle: 'APP.BROWSE.ABOUT.TITLE'
}
},
{
path: 'search',
component: SearchComponent
},
{
path: '**',
component: GenericErrorComponent
-87
View File
@@ -1,87 +0,0 @@
/*!
* @license
* Alfresco Example Content Application
*
* Copyright (C) 2005 - 2018 Alfresco Software Limited
*
* This file is part of the Alfresco Example Content Application.
* If the software was purchased under a paid Alfresco license, the terms of
* the paid license agreement will prevail. Otherwise, the software is
* provided under the following open source license terms:
*
* The Alfresco Example Content Application is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* The Alfresco Example Content Application is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with Alfresco. If not, see <http://www.gnu.org/licenses/>.
*/
import { NgModule } from '@angular/core';
import { DatePipe } from '@angular/common';
import { RouterModule } from '@angular/router';
import { FormsModule, ReactiveFormsModule } from '@angular/forms';
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
import { AdfModule } from '../adf.module';
import { MaterialModule } from './material.module';
import { NodeCopyDirective } from './directives/node-copy.directive';
import { NodeDeleteDirective } from './directives/node-delete.directive';
import { NodeMoveDirective } from './directives/node-move.directive';
import { NodeRestoreDirective } from './directives/node-restore.directive';
import { NodePermanentDeleteDirective } from './directives/node-permanent-delete.directive';
import { NodeUnshareDirective } from './directives/node-unshare.directive';
import { ContentManagementService } from './services/content-management.service';
import { BrowsingFilesService } from './services/browsing-files.service';
import { NodeActionsService } from './services/node-actions.service';
export function modules() {
return [
BrowserAnimationsModule,
FormsModule,
ReactiveFormsModule,
MaterialModule,
RouterModule,
AdfModule
];
}
export function declarations() {
return [
NodeCopyDirective,
NodeDeleteDirective,
NodeMoveDirective,
NodeRestoreDirective,
NodePermanentDeleteDirective,
NodeUnshareDirective
];
}
export function providers() {
return [
DatePipe,
BrowsingFilesService,
ContentManagementService,
NodeActionsService
];
}
@NgModule({
imports: modules(),
declarations: declarations(),
entryComponents: [],
providers: providers(),
exports: [
...modules(),
...declarations()
]
})
export class CommonModule {}
@@ -25,13 +25,15 @@
import { TestBed, ComponentFixture } from '@angular/core/testing';
import { By } from '@angular/platform-browser';
import { TranslationService, NodesApiService, NotificationService } from '@alfresco/adf-core';
import { TranslationService, NodesApiService, NotificationService, CoreModule } from '@alfresco/adf-core';
import { Component, DebugElement } from '@angular/core';
import { Observable } from 'rxjs/Rx';
import { CommonModule } from '../common.module';
import { NodeDeleteDirective } from './node-delete.directive';
import { ContentManagementService } from '../services/content-management.service';
import { MatSnackBarModule } from '@angular/material';
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
import { FormsModule, ReactiveFormsModule } from '@angular/forms';
@Component({
template: '<div [app-delete-node]="selection"></div>'
@@ -53,10 +55,18 @@ describe('NodeDeleteDirective', () => {
beforeEach(() => {
TestBed.configureTestingModule({
imports: [
CommonModule
BrowserAnimationsModule,
FormsModule,
ReactiveFormsModule,
CoreModule,
MatSnackBarModule
],
declarations: [
NodeDeleteDirective,
TestComponent
],
providers: [
ContentManagementService
]
});
@@ -0,0 +1,126 @@
/*!
* @license
* Copyright 2016 Alfresco Software, Ltd.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { Directive, Input, HostListener } from '@angular/core';
import { MatDialog } from '@angular/material';
import { MinimalNodeEntity } from 'alfresco-js-api';
import { AlfrescoApiService } from '@alfresco/adf-core';
import { DownloadZipDialogComponent } from '@alfresco/adf-content-services';
/** @deprecated workaround for the ADF 2.3.0 regression. */
@Directive({
selector: '[appNodeDownload]'
})
export class NodeDownloadDirective {
/** Nodes to download. */
// tslint:disable-next-line:no-input-rename
@Input('appNodeDownload')
nodes: MinimalNodeEntity[];
@HostListener('click')
onClick() {
this.downloadNodes(this.nodes);
}
constructor(
private apiService: AlfrescoApiService,
private dialog: MatDialog) {
}
/**
* Downloads multiple selected nodes.
* Packs result into a .ZIP archive if there is more than one node selected.
* @param selection Multiple selected nodes to download
*/
downloadNodes(selection: Array<MinimalNodeEntity>) {
if (!selection || selection.length === 0) {
return;
}
if (selection.length === 1) {
this.downloadNode(selection[0]);
} else {
this.downloadZip(selection);
}
}
/**
* Downloads a single node.
* Packs result into a .ZIP archive is the node is a Folder.
* @param node Node to download
*/
downloadNode(node: MinimalNodeEntity) {
if (node && node.entry) {
const entry = node.entry;
if (entry.isFile) {
this.downloadFile(node);
}
if (entry.isFolder) {
this.downloadZip([node]);
}
// Check if there's nodeId for Shared Files
if (!entry.isFile && !entry.isFolder && (<any> entry).nodeId) {
this.downloadFile(node);
}
}
}
private downloadFile(node: MinimalNodeEntity) {
if (node && node.entry) {
const contentApi = this.apiService.getInstance().content;
const id = (<any> node.entry).nodeId || node.entry.id;
const url = contentApi.getContentUrl(id, true);
const fileName = node.entry.name;
this.download(url, fileName);
}
}
private downloadZip(selection: Array<MinimalNodeEntity>) {
if (selection && selection.length > 0) {
// nodeId for Shared node
const nodeIds = selection.map((node: any) => (node.entry.nodeId || node.entry.id));
this.dialog.open(DownloadZipDialogComponent, {
width: '600px',
disableClose: true,
data: {
nodeIds
}
});
}
}
private download(url: string, fileName: string) {
if (url && fileName) {
const link = document.createElement('a');
link.style.display = 'none';
link.download = fileName;
link.href = url;
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
}
}
}
@@ -0,0 +1,121 @@
/*!
* @license
* Alfresco Example Content Application
*
* Copyright (C) 2005 - 2018 Alfresco Software Limited
*
* This file is part of the Alfresco Example Content Application.
* If the software was purchased under a paid Alfresco license, the terms of
* the paid license agreement will prevail. Otherwise, the software is
* provided under the following open source license terms:
*
* The Alfresco Example Content Application is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* The Alfresco Example Content Application is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with Alfresco. If not, see <http://www.gnu.org/licenses/>.
*/
import { Component } from '@angular/core';
import { ComponentFixture, TestBed, async, fakeAsync, tick } from '@angular/core/testing';
import { AlfrescoApiService, CoreModule } from '@alfresco/adf-core';
import { NodeInfoDirective } from './node-info.directive';
@Component({
template: '<div [app-node-info]="selection"></div>'
})
class TestComponent {
selection;
}
describe('NodeInfoDirective', () => {
let fixture: ComponentFixture<TestComponent>;
let component: TestComponent;
let apiService: AlfrescoApiService;
let nodeService;
beforeEach(async(() => {
TestBed.configureTestingModule({
imports: [
CoreModule
],
declarations: [
TestComponent,
NodeInfoDirective
]
});
fixture = TestBed.createComponent(TestComponent);
component = fixture.componentInstance;
apiService = TestBed.get(AlfrescoApiService);
}));
beforeEach(() => {
nodeService = apiService.getInstance().nodes;
spyOn(nodeService, 'getNodeInfo').and.returnValue(Promise.resolve({
entry: { name: 'borg' }
}));
});
it('should not get node info onInit when selection is empty', () => {
component.selection = [];
fixture.detectChanges();
expect(nodeService.getNodeInfo).not.toHaveBeenCalled();
});
it('should get node info onInit when selection is not empty', () => {
component.selection = [{ entry: { id: 'id' } }];
fixture.detectChanges();
expect(nodeService.getNodeInfo).toHaveBeenCalled();
});
it('should not get node info on event when selection is empty', () => {
component.selection = [];
fixture.detectChanges();
document.dispatchEvent(new CustomEvent('click'));
expect(nodeService.getNodeInfo).not.toHaveBeenCalled();
});
it('should get node info on event from selection', () => {
component.selection = [{ entry: { id: 'id' } }];
fixture.detectChanges();
document.dispatchEvent(new CustomEvent('click'));
expect(nodeService.getNodeInfo).toHaveBeenCalledWith('id', { include: [ 'allowableOperations' ] });
});
it('should get node info of last entry when selecting multiple nodes', fakeAsync(() => {
component.selection = [
{ entry: { id: 'id1', isFile: true } },
{ entry: { id: 'id2', isFile: true } },
{ entry: { id: 'id3', isFile: true } }
];
fixture.detectChanges();
document.dispatchEvent(new CustomEvent('click'));
fixture.detectChanges();
tick();
expect(nodeService.getNodeInfo).toHaveBeenCalledWith('id3', { include: [ 'allowableOperations' ] });
}));
});
@@ -0,0 +1,82 @@
/*!
* @license
* Alfresco Example Content Application
*
* Copyright (C) 2005 - 2018 Alfresco Software Limited
*
* This file is part of the Alfresco Example Content Application.
* If the software was purchased under a paid Alfresco license, the terms of
* the paid license agreement will prevail. Otherwise, the software is
* provided under the following open source license terms:
*
* The Alfresco Example Content Application is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* The Alfresco Example Content Application is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with Alfresco. If not, see <http://www.gnu.org/licenses/>.
*/
import { Directive, HostListener, Input, Output, EventEmitter, OnInit } from '@angular/core';
import { AlfrescoApiService } from '@alfresco/adf-core';
import { MinimalNodeEntity, MinimalNodeEntryEntity } from 'alfresco-js-api';
@Directive({
selector: '[app-node-info]',
exportAs: 'nodeInfo'
})
export class NodeInfoDirective implements OnInit {
@Input('app-node-info') selection: MinimalNodeEntity[];
@Output() changed: EventEmitter<null|MinimalNodeEntryEntity> = new EventEmitter<null|MinimalNodeEntryEntity>();
@Output() error: EventEmitter<null> = new EventEmitter<null>();
node: MinimalNodeEntryEntity;
loading: boolean = null;
@HostListener('document:node-click', ['$event'])
onClick(event) {
this.getNodeInfo();
}
constructor(private apiService: AlfrescoApiService) {}
ngOnInit() {
this.getNodeInfo();
}
getNodeInfo() {
if (!this.selection.length) {
this.node = null;
this.loading = false;
this.changed.emit(null);
return;
}
const node = this.selection[this.selection.length - 1];
if (node) {
this.loading = true;
this.apiService.getInstance().nodes
.getNodeInfo((<any>node.entry).nodeId || node.entry.id, {
include: ['allowableOperations']
})
.then((data: MinimalNodeEntryEntity) => {
this.node = data;
this.changed.emit(data);
this.loading = false;
})
.catch(() => {
this.error.emit();
this.loading = false;
});
}
}
}
@@ -27,12 +27,13 @@ import { Component, DebugElement } from '@angular/core';
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { By } from '@angular/platform-browser';
import { Observable } from 'rxjs/Rx';
import { TranslationService, NodesApiService, NotificationService, CoreModule } from '@alfresco/adf-core';
import { DocumentListService } from '@alfresco/adf-content-services';
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
import { TranslationService, NodesApiService, NotificationService } from '@alfresco/adf-core';
import { CommonModule } from '../common.module';
import { NodeActionsService } from '../services/node-actions.service';
import { NodeMoveDirective } from './node-move.directive';
import { ContentManagementService } from '../services/content-management.service';
@Component({
template: '<div [app-move-node]="selection"></div>'
@@ -53,10 +54,17 @@ describe('NodeMoveDirective', () => {
beforeEach(() => {
TestBed.configureTestingModule({
imports: [
CommonModule
BrowserAnimationsModule,
CoreModule
],
declarations: [
NodeMoveDirective,
TestComponent
],
providers: [
DocumentListService,
ContentManagementService,
NodeActionsService
]
});
@@ -27,10 +27,10 @@ import { Component, DebugElement } from '@angular/core';
import { TestBed, ComponentFixture, async, fakeAsync, tick } from '@angular/core/testing';
import { By } from '@angular/platform-browser';
import { Observable } from 'rxjs/Rx';
import { AlfrescoApiService, TranslationService, NotificationService } from '@alfresco/adf-core';
import { AlfrescoApiService, TranslationService, NotificationService, CoreModule } from '@alfresco/adf-core';
import { CommonModule } from '../common.module';
import { NodePermanentDeleteDirective } from './node-permanent-delete.directive';
import { MatDialogModule, MatDialog } from '@angular/material';
@Component({
template: `<div [app-permanent-delete-node]="selection"></div>`
@@ -48,13 +48,16 @@ describe('NodePermanentDeleteDirective', () => {
let notificationService: NotificationService;
let nodesService;
let directiveInstance;
let dialog: MatDialog;
beforeEach(async(() => {
TestBed.configureTestingModule({
imports: [
CommonModule
CoreModule,
MatDialogModule
],
declarations: [
NodePermanentDeleteDirective,
TestComponent
]
})
@@ -65,16 +68,24 @@ describe('NodePermanentDeleteDirective', () => {
element = fixture.debugElement.query(By.directive(NodePermanentDeleteDirective));
directiveInstance = element.injector.get(NodePermanentDeleteDirective);
dialog = TestBed.get(MatDialog);
alfrescoService = TestBed.get(AlfrescoApiService);
nodesService = alfrescoService.getInstance().nodes;
translation = TestBed.get(TranslationService);
notificationService = TestBed.get(NotificationService);
});
}));
beforeEach(() => {
nodesService = alfrescoService.getInstance().nodes;
spyOn(translation, 'get').and.returnValue(Observable.of('message'));
spyOn(notificationService, 'openSnackMessage').and.returnValue({});
spyOn(dialog, 'open').and.returnValue({
afterClosed() {
return Observable.of(true)
}
});
});
it('does not purge nodes if no selection', () => {
@@ -28,25 +28,44 @@ import { Observable } from 'rxjs/Rx';
import { TranslationService, AlfrescoApiService, NotificationService } from '@alfresco/adf-core';
import { MinimalNodeEntity } from 'alfresco-js-api';
import { MatDialog } from '@angular/material';
import { ConfirmDialogComponent } from '@alfresco/adf-content-services';
@Directive({
// tslint:disable-next-line:directive-selector
selector: '[app-permanent-delete-node]'
})
export class NodePermanentDeleteDirective {
// tslint:disable-next-line:no-input-rename
@Input('app-permanent-delete-node')
selection: MinimalNodeEntity[];
@HostListener('click')
onClick() {
this.purge();
const dialogRef = this.dialog.open(ConfirmDialogComponent, {
data: {
title: 'APP.DIALOGS.CONFIRM_PURGE.TITLE',
message: 'APP.DIALOGS.CONFIRM_PURGE.MESSAGE',
yesLabel: 'APP.DIALOGS.CONFIRM_PURGE.YES_LABEL',
noLabel: 'APP.DIALOGS.CONFIRM_PURGE.NO_LABEL'
},
minWidth: '250px'
});
dialogRef.afterClosed().subscribe(result => {
if (result === true) {
this.purge();
}
});
}
constructor(
private alfrescoApiService: AlfrescoApiService,
private translation: TranslationService,
private notification: NotificationService,
private el: ElementRef
private el: ElementRef,
private dialog: MatDialog
) {}
private purge() {
@@ -30,10 +30,10 @@ import { TestBed, ComponentFixture, async, fakeAsync, tick } from '@angular/core
import { By } from '@angular/platform-browser';
import { Observable } from 'rxjs/Rx';
import { CommonModule } from '../common.module';
import { AlfrescoApiService, TranslationService, NotificationService } from '@alfresco/adf-core';
import { AlfrescoApiService, TranslationService, NotificationService, CoreModule } from '@alfresco/adf-core';
import { NodeRestoreDirective } from './node-restore.directive';
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
@Component({
template: `<div [app-restore-node]="selection"></div>`
@@ -57,10 +57,12 @@ describe('NodeRestoreDirective', () => {
beforeEach(async(() => {
TestBed.configureTestingModule({
imports: [
CommonModule,
RouterTestingModule
BrowserAnimationsModule,
RouterTestingModule,
CoreModule
],
declarations: [
NodeRestoreDirective,
TestComponent
]
})
@@ -72,8 +74,6 @@ describe('NodeRestoreDirective', () => {
directiveInstance = element.injector.get(NodeRestoreDirective);
alfrescoService = TestBed.get(AlfrescoApiService);
nodesService = alfrescoService.getInstance().nodes;
coreApi = alfrescoService.getInstance().core;
translation = TestBed.get(TranslationService);
notificationService = TestBed.get(NotificationService);
router = TestBed.get(Router);
@@ -81,6 +81,9 @@ describe('NodeRestoreDirective', () => {
}));
beforeEach(() => {
nodesService = alfrescoService.getInstance().nodes;
coreApi = alfrescoService.getInstance().core;
spyOn(translation, 'get').and.returnValue(Observable.of('message'));
});
@@ -0,0 +1,79 @@
/*!
* @license
* Alfresco Example Content Application
*
* Copyright (C) 2005 - 2018 Alfresco Software Limited
*
* This file is part of the Alfresco Example Content Application.
* If the software was purchased under a paid Alfresco license, the terms of
* the paid license agreement will prevail. Otherwise, the software is
* provided under the following open source license terms:
*
* The Alfresco Example Content Application is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* The Alfresco Example Content Application is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with Alfresco. If not, see <http://www.gnu.org/licenses/>.
*/
import { Directive, EventEmitter, HostListener, Input, Output } from '@angular/core';
import { TranslationService, NotificationService, AlfrescoApiService } from '@alfresco/adf-core';
import { MinimalNodeEntity } from 'alfresco-js-api';
import { VersionManagerDialogAdapterComponent } from '../../components/versions-dialog/version-manager-dialog-adapter.component';
import { MatDialog } from '@angular/material';
@Directive({
selector: '[app-node-versions]'
})
export class NodeVersionsDirective {
@Input('app-node-versions')
selection: MinimalNodeEntity[];
@Output()
nodeVersionError: EventEmitter<any> = new EventEmitter();
@HostListener('click')
onClick() {
this.onManageVersions();
}
constructor(
private apiService: AlfrescoApiService,
private dialog: MatDialog,
private notification: NotificationService,
private translation: TranslationService
) {}
onManageVersions() {
const contentEntry = this.selection[0].entry;
const nodeId = (<any>contentEntry).nodeId;
this.apiService.getInstance().nodes.getNodeInfo(nodeId || contentEntry.id, {
include: ['allowableOperations']
}).then(entry => this.openVersionManagerDialog(entry));
}
openVersionManagerDialog(contentEntry) {
if (contentEntry.isFile) {
this.dialog.open(
VersionManagerDialogAdapterComponent,
<any>{ data: { contentEntry }, panelClass: 'adf-version-manager-dialog', width: '630px' });
} else {
const translatedErrorMessage: any = this.translation.get('APP.MESSAGES.ERRORS.PERMISSION');
this.notification.openSnackMessage(translatedErrorMessage.value, 4000);
this.nodeVersionError.emit();
}
}
}
@@ -23,21 +23,17 @@
* along with Alfresco. If not, see <http://www.gnu.org/licenses/>.
*/
import { NgModule } from '@angular/core';
import { Pipe, PipeTransform } from '@angular/core';
import { AppConfigService } from '@alfresco/adf-core';
// ADF modules
import { CoreModule } from '@alfresco/adf-core';
import { ContentModule } from '@alfresco/adf-content-services';
export function modules() {
return [
CoreModule,
ContentModule
];
}
@NgModule({
imports: modules(),
exports: modules()
@Pipe({
name: 'appConfig',
pure: true
})
export class AdfModule { }
export class AppConfigPipe implements PipeTransform {
constructor(private config: AppConfigService) {}
transform(value: string, fallback: any): any {
return this.config.get(value, fallback);
}
}
@@ -135,6 +135,7 @@ describe('NodeActionsService', () => {
service = TestBed.get(NodeActionsService);
apiService = TestBed.get(AlfrescoApiService);
apiService.reset();
nodesApiService = TestBed.get(NodesApiService);
nodesApi = apiService.getInstance().nodes;
});
@@ -196,26 +196,12 @@ export class NodeActionsService {
select: new Subject<MinimalNodeEntryEntity[]>()
};
/*const matDialogRef =*/ this.dialog.open(ContentNodeSelectorComponent, <any>{
this.dialog.open(ContentNodeSelectorComponent, <any>{
data,
panelClass: 'adf-content-node-selector-dialog',
width: '630px'
});
// todo: add back the fix for [ACA-1054]:
/* const destinationPicker = matDialogRef.componentInstance;
const initialSiteChanged = destinationPicker.siteChanged;
destinationPicker.siteChanged = (chosenSite) => {
initialSiteChanged.call(destinationPicker, chosenSite);
if (chosenSite.guid === '-mysites-') {
destinationPicker.documentList.data.setSorting(new DataSorting('title', 'asc'));
} else {
destinationPicker.documentList.data.setSorting(new DataSorting('name', 'asc'));
}
};*/
data.select.subscribe({
complete: this.close.bind(this)
});
@@ -299,7 +285,7 @@ export class NodeActionsService {
// replace first item with 'File Libraries'
elements[0].name = this.translation.instant('APP.BROWSE.LIBRARIES.TITLE');
// elements[0].id = '-mysites-'; // commented this until navigation on custom sources is enabled on document-list
elements[0].id = '-mysites-';
if (this.isSiteContainer(node)) {
// rename 'documentLibrary' entry to the target site display name
@@ -0,0 +1,190 @@
/*!
* @license
* Alfresco Example Content Application
*
* Copyright (C) 2005 - 2018 Alfresco Software Limited
*
* This file is part of the Alfresco Example Content Application.
* If the software was purchased under a paid Alfresco license, the terms of
* the paid license agreement will prevail. Otherwise, the software is
* provided under the following open source license terms:
*
* The Alfresco Example Content Application is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* The Alfresco Example Content Application is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with Alfresco. If not, see <http://www.gnu.org/licenses/>.
*/
import { NodePermissionService } from './node-permission.service';
describe('NodePermissionService', () => {
let permission: NodePermissionService;
beforeEach(() => {
permission = new NodePermissionService();
});
it('should return false when source is null', () => {
const source = null;
expect(permission.check(source, ['update'])).toBe(false);
});
describe('Multiple source permission', () => {
it('should return true when source has allowableOperations permission', () => {
const source = [
{ entry: { allowableOperations: ['update'] } },
{ entry: { allowableOperations: ['update'] } },
{ entry: { allowableOperations: ['update'] } }
];
expect(permission.check(source, ['update'])).toBe(true);
});
it('should return true when source has allowableOperationsOnTarget permission', () => {
const source = [
{ entry: { allowableOperationsOnTarget: ['update'] } },
{ entry: { allowableOperationsOnTarget: ['update'] } },
{ entry: { allowableOperationsOnTarget: ['update'] } }
];
expect(permission.check(source, ['update'], { target: 'allowableOperationsOnTarget' })).toBe(true);
});
it('should return false when source does not have allowableOperations permission', () => {
const source = [
{ entry: { allowableOperations: ['update'] } },
{ entry: { allowableOperations: ['update'] } },
{ entry: { allowableOperations: ['delete'] } }
];
expect(permission.check(source, ['update'], { target: 'allowableOperationsOnTarget' })).toBe(false);
});
it('should return false when source does not have allowableOperationsOnTarget permission', () => {
const source = [
{ entry: { allowableOperationsOnTarget: ['update'] } },
{ entry: { allowableOperationsOnTarget: ['update'] } },
{ entry: { allowableOperationsOnTarget: ['delete'] } }
];
expect(permission.check(source, ['update'], { target: 'allowableOperationsOnTarget' })).toBe(false);
});
it('should return true when source has `OR` allowableOperations permission', () => {
const source = [
{ entry: { allowableOperations: ['update' , 'delete'] } },
{ entry: { allowableOperations: ['update', 'create'] } },
{ entry: { allowableOperations: ['update', 'updatePermissions'] } }
];
expect(permission.check(source, ['update', 'create'])).toBe(true);
});
it('should return true when source has `AND` allowableOperations permission', () => {
const source = [
{ entry: { allowableOperations: ['update' , 'delete', 'other'] } },
{ entry: { allowableOperations: ['update', 'create', 'other'] } },
{ entry: { allowableOperations: ['update', 'updatePermissions', 'other'] } }
];
expect(permission.check(source, ['update', 'other'], { operation: 'AND' })).toBe(true);
});
it('should return false when source has no `AND` allowableOperations permission', () => {
const source = [
{ entry: { allowableOperations: ['update' , 'delete', 'other'] } },
{ entry: { allowableOperations: ['update', 'create', 'other'] } },
{ entry: { allowableOperations: ['update', 'updatePermissions', 'other'] } }
];
expect(permission.check(source, ['update', 'bogus'], { operation: 'AND' })).toBe(false);
});
it('should return false when source has no allowableOperations', () => {
const source = [
{ entry: { allowableOperations: [] } },
{ entry: { allowableOperations: [] } },
{ entry: { allowableOperations: ['update'] } }
];
expect(permission.check(source, ['update'])).toBe(false);
});
it('should return false when source has no allowableOperations property', () => {
const source = [
{ entry: { } },
{ entry: { } },
{ entry: { allowableOperations: ['update'] } }
];
expect(permission.check(source, ['update'])).toBe(false);
});
});
describe('Single source permission', () => {
it('should return true when source has allowableOperations permission', () => {
const source = { entry: { allowableOperations: ['update'] } };
expect(permission.check(source, ['update'])).toBe(true);
});
it('should return true when source has allowableOperationsOnTarget permission', () => {
const source = { entry: { allowableOperationsOnTarget: ['update'] } };
expect(permission.check(source, ['update'], { target: 'allowableOperationsOnTarget' })).toBe(true);
});
it('should return false when source does not have allowableOperations permission', () => {
const source = { entry: { allowableOperations: ['delete'] } };
expect(permission.check(source, ['update'])).toBe(false);
});
it('should return false when source does not have allowableOperationsOnTarget permission', () => {
const source = { entry: { allowableOperationsOnTarget: ['delete'] } };
expect(permission.check(source, ['update'], { target: 'allowableOperationsOnTarget' })).toBe(false);
});
it('should return true when source has `OR` allowableOperations permission', () => {
const source = { entry: { allowableOperations: ['update'] } };
expect(permission.check(source, ['update', 'create'])).toBe(true);
});
it('should return true when source has `AND` allowableOperations permission', () => {
const source = { entry: { allowableOperations: ['update', 'other'] } };
expect(permission.check(source, ['update', 'other'], { operation: 'AND' })).toBe(true);
});
it('should return false when source has no `AND` allowableOperations permission', () => {
const source = { entry: { allowableOperations: ['update', 'updatePermissions', 'other'] } };
expect(permission.check(source, ['update', 'bogus'], { operation: 'AND' })).toBe(false);
});
it('should return false when source has no allowableOperations', () => {
const source = { entry: { allowableOperations: [] } };
expect(permission.check(source, ['update'])).toBe(false);
});
it('should return false when source has no allowableOperations property', () => {
const source = { entry: { } };
expect(permission.check(source, ['update'])).toBe(false);
});
});
});
@@ -0,0 +1,84 @@
/*!
* @license
* Alfresco Example Content Application
*
* Copyright (C) 2005 - 2018 Alfresco Software Limited
*
* This file is part of the Alfresco Example Content Application.
* If the software was purchased under a paid Alfresco license, the terms of
* the paid license agreement will prevail. Otherwise, the software is
* provided under the following open source license terms:
*
* The Alfresco Example Content Application is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* The Alfresco Example Content Application is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with Alfresco. If not, see <http://www.gnu.org/licenses/>.
*/
import { Injectable } from '@angular/core';
@Injectable()
export class NodePermissionService {
static DEFAULT_OPERATION = 'OR';
private defaultOptions = {
operation: NodePermissionService.DEFAULT_OPERATION,
target: null
};
check(source: any, permissions: string[], options: any = {}): boolean {
const opts = Object.assign({}, this.defaultOptions, options);
if (source) {
if (Array.isArray(source) && source.length) {
const arr = this.sanitize(source);
return !!arr.length && source.every(node => this.hasPermission(node, permissions, opts));
}
return this.hasPermission(source, permissions, opts);
}
return false;
}
private hasPermission(node, permissions, options): boolean {
const allowableOperations = this.getAllowableOperations(node, options.target);
if (allowableOperations.length) {
if (options.operation === NodePermissionService.DEFAULT_OPERATION) {
return permissions.some(permission => allowableOperations.includes(permission));
} else {
return permissions.every(permission => allowableOperations.includes(permission));
}
}
return false;
}
private getAllowableOperations(node, target): string[] {
const entry = node.entry || node;
if (!target && entry.allowableOperations) {
return entry.allowableOperations;
}
if (target && entry[target]) {
return entry[target];
}
return [];
}
private sanitize(selection): any[] {
return (selection || []).filter(item => item);
}
}
+35 -33
View File
@@ -1,46 +1,48 @@
<div class="inner-layout inner-layout--scroll">
<div class="inner-layout__content inner-layout__content--scroll">
<article *ngIf="ecmVersion" class="padding">
<header class="header padding-left">Alfresco Content Services</header>
<p class="padding-left"> version: {{ ecmVersion.edition }} {{ ecmVersion.version.display }} </p>
</article>
<div class="inner-layout__content">
<div class="inner-layout__panel content--scroll">
<article *ngIf="ecmVersion" class="padding">
<header class="header padding-left">Alfresco Content Services</header>
<p class="padding-left"> version: {{ ecmVersion.edition }} {{ ecmVersion.version.display }} </p>
</article>
<article class="padding-top-bottom">
<header class="header padding-left-right">License</header>
<adf-datatable [data]="license"></adf-datatable>
</article>
<article class="padding-top-bottom">
<header class="header padding-left-right">License</header>
<adf-datatable [data]="license"></adf-datatable>
</article>
<article class="padding-top-bottom">
<header class="header padding-left-right">Status</header>
<adf-datatable [data]="status"></adf-datatable>
</article>
<article class="padding-top-bottom">
<header class="header padding-left-right">Status</header>
<adf-datatable [data]="status"></adf-datatable>
</article>
<article class="padding-top-bottom">
<header class="header padding-left-right">Modules</header>
<adf-datatable [data]="modules"></adf-datatable>
</article>
<article class="padding-top-bottom">
<header class="header padding-left-right">Modules</header>
<adf-datatable [data]="modules"></adf-datatable>
</article>
<article class="padding">
<header class="header padding-left">Alfresco Content Application</header>
<p class="padding-left"> version: 1.0 </p>
</article>
<article class="padding">
<header class="header padding-left">Alfresco Content Application</header>
<p class="padding-left"> version: {{ releaseVersion }} </p>
</article>
<article *ngIf="githubUrlCommitAlpha" class="padding">
<header class="header">Source code</header>
<article *ngIf="githubUrlCommitAlpha" class="padding">
<header class="header">Source code</header>
<small>You are running the project based on the following commit:</small>
<small>You are running the project based on the following commit:</small>
<p>
<a [href]="githubUrlCommitAlpha" target="blank">{{githubUrlCommitAlpha}}</a>
</p>
</article>
<p>
<a [href]="githubUrlCommitAlpha" target="blank">{{githubUrlCommitAlpha}}</a>
</p>
</article>
<article>
<header class="header padding-left-right">Packages</header>
<article>
<header class="header padding-left-right">Packages</header>
<small class="padding-left-right">Current project is using the following ADF libraries:</small>
<small class="padding-left-right">Current project is using the following ADF libraries:</small>
<adf-datatable [data]="data"></adf-datatable>
</article>
<adf-datatable [data]="data"></adf-datatable>
</article>
</div>
</div>
</div>
+21 -17
View File
@@ -40,7 +40,7 @@ export class AboutComponent implements OnInit {
license: ObjectDataTableAdapter;
modules: ObjectDataTableAdapter;
githubUrlCommitAlpha = 'https://github.com/Alfresco/alfresco-content-app/commits';
releaseVersion = '';
constructor(
private discovery: DiscoveryApiService,
@@ -55,7 +55,7 @@ export class AboutComponent implements OnInit {
{type: 'text', key: 'id', title: 'ID', sortable: true},
{type: 'text', key: 'title', title: 'Title', sortable: true},
{type: 'text', key: 'version', title: 'Description', sortable: true},
{type: 'text', key: 'installDate', title: 'Install Date', sortable: true},
{type: 'date', key: 'installDate', title: 'Install Date', sortable: true},
{type: 'text', key: 'installState', title: 'Install State', sortable: true},
{type: 'text', key: 'versionMin', title: 'Version Minor', sortable: true},
{type: 'text', key: 'versionMax', title: 'Version Max', sortable: true}
@@ -69,8 +69,8 @@ export class AboutComponent implements OnInit {
]);
this.license = new ObjectDataTableAdapter([this.ecmVersion.license], [
{type: 'text', key: 'issuedAt', title: 'Issued At', sortable: true},
{type: 'text', key: 'expiresAt', title: 'Expires At', sortable: true},
{type: 'date', key: 'issuedAt', title: 'Issued At', sortable: true},
{type: 'date', key: 'expiresAt', title: 'Expires At', sortable: true},
{type: 'text', key: 'remainingDays', title: 'Remaining Days', sortable: true},
{type: 'text', key: 'holder', title: 'Holder', sortable: true},
{type: 'text', key: 'mode', title: 'Type', sortable: true},
@@ -79,20 +79,24 @@ export class AboutComponent implements OnInit {
]);
});
this.http.get('/versions.json').subscribe(response => {
const regexp = new RegExp('^(@alfresco|alfresco-)');
this.http.get('/versions.json')
.map(response => response.json())
.subscribe(response => {
const regexp = new RegExp('^(@alfresco|alfresco-)');
const alfrescoPackagesTableRepresentation = Object.keys(response.json().dependencies)
.filter((val) => regexp.test(val))
.map((val) => ({
name: val,
version: response.json().dependencies[val].version
}));
const alfrescoPackagesTableRepresentation = Object.keys(response.dependencies)
.filter((val) => regexp.test(val))
.map((val) => ({
name: val,
version: response.dependencies[val].version
}));
this.data = new ObjectDataTableAdapter(alfrescoPackagesTableRepresentation, [
{type: 'text', key: 'name', title: 'Name', sortable: true},
{type: 'text', key: 'version', title: 'Version', sortable: true}
]);
});
this.data = new ObjectDataTableAdapter(alfrescoPackagesTableRepresentation, [
{type: 'text', key: 'name', title: 'Name', sortable: true},
{type: 'text', key: 'version', title: 'Version', sortable: true}
]);
this.releaseVersion = response.version;
});
}
}
@@ -7,6 +7,7 @@
<button
mat-icon-button
color="primary"
*ngIf="canPreviewFile(documentList.selection)"
title="{{ 'APP.ACTIONS.VIEW' | translate }}"
(click)="onNodeDoubleClick(documentList.selection[0]?.entry)">
@@ -15,6 +16,7 @@
<button
mat-icon-button
color="primary"
*ngIf="hasSelection(documentList.selection)"
title="{{ 'APP.ACTIONS.DOWNLOAD' | translate }}"
[adfNodeDownload]="documentList.selection">
@@ -23,13 +25,24 @@
<button
mat-icon-button
color="primary"
*ngIf="showEditOption(documentList.selection)"
title="{{ 'APP.ACTIONS.EDIT' | translate }}"
(error)="openSnackMessage($event)"
[adf-edit-folder]="documentList.selection[0]?.entry">
<mat-icon>create</mat-icon>
</button>
<button mat-icon-button
[color]="infoDrawerOpened ? 'accent' : 'primary'"
*ngIf="documentList.selection.length"
title="{{ 'APP.ACTIONS.DETAILS' | translate }}"
(click)="toggleSidebar()">
<mat-icon>info_outline</mat-icon>
</button>
<button
color="primary"
mat-icon-button
*ngIf="hasSelection(documentList.selection)"
title="{{ 'APP.ACTIONS.MORE' | translate }}"
@@ -44,7 +57,10 @@
#selection="adfFavorite"
(toggle)="refresh()"
[adf-node-favorite]="documentList.selection">
<mat-icon [ngClass]="{ 'icon-highlight': selection.hasFavorites() }">
<mat-icon [ngClass]="{
'toolbar__option--active': selection.hasFavorites(),
'toolbar__option--default': !selection.hasFavorites()
}">
{{ selection.hasFavorites() ? 'star' :'star_border' }}
</mat-icon>
<span>{{ 'APP.ACTIONS.FAVORITE' | translate }}</span>
@@ -53,100 +69,148 @@
<button
mat-menu-item
[app-copy-node]="documentList.selection">
<mat-icon>content_copy</mat-icon>
<mat-icon color="primary">content_copy</mat-icon>
<span>{{ 'APP.ACTIONS.COPY' | translate }}</span>
</button>
<button
mat-menu-item
[app-move-node]="documentList.selection">
<mat-icon>library_books</mat-icon>
<mat-icon color="primary">library_books</mat-icon>
<span>{{ 'APP.ACTIONS.MOVE' | translate }}</span>
</button>
<button
mat-menu-item
[app-delete-node]="documentList.selection">
<mat-icon>delete</mat-icon>
<mat-icon color="primary">delete</mat-icon>
<span>{{ 'APP.ACTIONS.DELETE' | translate }}</span>
</button>
<button
mat-menu-item
*ngIf="isFileSelected(documentList.selection)"
[app-node-versions]="documentList.selection">
<mat-icon color="primary">history</mat-icon>
<span>{{ 'APP.ACTIONS.VERSIONS' | translate }}</span>
</button>
</mat-menu>
</adf-toolbar>
</div>
<div class="inner-layout__content">
<div class="inner-layout__panel" [ngClass]="{'adf-document-list--loading': documentList.isEmpty()}">
<adf-document-list #documentList
[attr.class]="documentList.isEmpty() ? 'adf-document-list--empty' : ''"
currentFolderId="-favorites-"
selectionMode="multiple"
[navigate]="false"
[sorting]="sorting"
[contextMenuActions]="true"
[contentActions]="false"
(sorting-changed)="onSortingChanged($event)"
(node-dblclick)="onNodeDoubleClick($event.detail?.node?.entry)">
<adf-document-list #documentList
[attr.class]="documentList.isEmpty() ? 'empty-list' : ''"
currentFolderId="-favorites-"
selectionMode="multiple"
[navigate]="false"
[sorting]="sorting"
[contextMenuActions]="true"
[contentActions]="false"
(sorting-changed)="onSortingChanged($event)"
(node-dblclick)="onNodeDoubleClick($event.detail?.node?.entry)">
<empty-folder-content>
<ng-template>
<app-empty-folder
icon="star_rate"
title="APP.BROWSE.FAVORITES.EMPTY_STATE.TITLE"
subtitle="APP.BROWSE.FAVORITES.EMPTY_STATE.TEXT">
</app-empty-folder>
</ng-template>
</empty-folder-content>
<data-columns>
<data-column
key="$thumbnail"
type="image"
[sortable]="false"
class="image-table-cell">
</data-column>
<data-column
class="adf-data-table-cell--ellipsis__name"
key="name"
title="APP.DOCUMENT_LIST.COLUMNS.NAME">
<ng-template let-value="value" let-context>
<span class="adf-datatable-cell" title="{{ context?.row?.obj | adfNodeNameTooltip }}">{{ value }}</span>
<empty-folder-content>
<ng-template>
<app-empty-folder
icon="star_rate"
title="APP.BROWSE.FAVORITES.EMPTY_STATE.TITLE"
subtitle="APP.BROWSE.FAVORITES.EMPTY_STATE.TEXT">
</app-empty-folder>
</ng-template>
</data-column>
</empty-folder-content>
<data-column
key="path"
title="APP.DOCUMENT_LIST.COLUMNS.LOCATION">
<ng-template let-context>
<app-location-link [context]="context"></app-location-link>
<data-columns>
<data-column
key="$thumbnail"
type="image"
[sortable]="false"
class="image-table-cell">
</data-column>
<data-column
class="adf-data-table-cell--ellipsis__name"
key="name"
title="APP.DOCUMENT_LIST.COLUMNS.NAME">
<ng-template let-value="value" let-context>
<span class="adf-datatable-cell" title="{{ context?.row?.obj | adfNodeNameTooltip }}">{{ value }}</span>
</ng-template>
</data-column>
<data-column
key="path"
title="APP.DOCUMENT_LIST.COLUMNS.LOCATION">
<ng-template let-context>
<app-location-link [context]="context"></app-location-link>
</ng-template>
</data-column>
<data-column
key="sizeInBytes"
title="APP.DOCUMENT_LIST.COLUMNS.SIZE"
type="fileSize">
</data-column>
<data-column
key="modifiedAt"
title="APP.DOCUMENT_LIST.COLUMNS.MODIFIED_ON">
<ng-template let-value="value">
<span title="{{ value | date:'medium' }}">{{ value | adfTimeAgo }}</span>
</ng-template>
</data-column>
<data-column
class="adf-data-table-cell--ellipsis"
key="modifiedByUser.displayName"
title="APP.DOCUMENT_LIST.COLUMNS.MODIFIED_BY">
</data-column>
</data-columns>
</adf-document-list>
<ng-container *ngIf="!documentList.isEmpty()">
<adf-pagination
[supportedPageSizes]="'pagination.supportedPageSizes' | appConfig"
[target]="documentList"
(changePageSize)="onChangePageSize($event)">
</adf-pagination>
</ng-container>
</div>
<div class="inner-layout__side-panel"
*ngIf="infoDrawerOpened"
[app-node-info]="documentList.selection"
(changed)="toggleSidebar($event)"
#infoInstance=nodeInfo>
<adf-info-drawer [title]="'APP.INFO_DRAWER.TITLE' | translate">
<adf-info-drawer-tab [label]="'APP.INFO_DRAWER.TABS.PROPERTIES' | translate">
<div *ngIf="infoInstance.loading">
<mat-progress-bar mode="indeterminate"></mat-progress-bar>
</div>
<adf-content-metadata-card
[readOnly]="!permission.check(infoInstance.node, ['update'])"
[displayEmpty]="permission.check(infoInstance.node, ['update'])"
[preset]="'custom'"
[node]="infoInstance.node">
</adf-content-metadata-card>
</adf-info-drawer-tab>
<adf-info-drawer-tab [label]="'APP.INFO_DRAWER.TABS.VERSIONS' | translate">
<ng-container *ngIf="infoInstance.node?.isFile;else choose_document_template">
<adf-version-manager [node]="infoInstance.node"></adf-version-manager>
</ng-container>
<ng-template #choose_document_template>
<div class="adf-manage-versions-empty">
<mat-icon class="adf-manage-versions-empty-icon">face</mat-icon>
{{ 'VERSION.SELECTION.EMPTY' | translate }}
</div>
</ng-template>
</data-column>
<data-column
key="sizeInBytes"
title="APP.DOCUMENT_LIST.COLUMNS.SIZE"
type="fileSize">
</data-column>
<data-column
key="modifiedAt"
title="APP.DOCUMENT_LIST.COLUMNS.MODIFIED_ON">
<ng-template let-value="value">
<span title="{{ value | date:'medium' }}">{{ value | adfTimeAgo }}</span>
</ng-template>
</data-column>
<data-column
class="adf-data-table-cell--ellipsis"
key="modifiedByUser.displayName"
title="APP.DOCUMENT_LIST.COLUMNS.MODIFIED_BY">
</data-column>
</data-columns>
</adf-document-list>
<adf-pagination [ngClass]="{ 'no-border' : documentList.isEmpty()}"
[target]="documentList"
(changePageSize)="onChangePageSize($event)">
</adf-pagination>
</adf-info-drawer-tab>
</adf-info-drawer>
</div>
</div>
</div>
@@ -37,13 +37,16 @@ import {
AuthenticationService, TimeAgoPipe, NodeNameTooltipPipe,
NodeFavoriteDirective, DataTableComponent
} from '@alfresco/adf-core';
import { DocumentListComponent } from '@alfresco/adf-content-services';
import { DocumentListComponent, CustomResourcesService } from '@alfresco/adf-content-services';
import { TranslateModule } from '@ngx-translate/core';
import { NoopAnimationsModule } from '@angular/platform-browser/animations';
import { MatMenuModule, MatSnackBarModule, MatIconModule } from '@angular/material';
import { DocumentListService } from '@alfresco/adf-content-services';
import { ContentManagementService } from '../../common/services/content-management.service';
import { NodeInfoDirective } from '../../common/directives/node-info.directive';
import { NodePermissionService } from '../../common/services/node-permission.service';
import { AppConfigPipe } from '../../common/pipes/app-config.pipe';
import { FavoritesComponent } from './favorites.component';
@@ -55,6 +58,7 @@ describe('Favorites Routed Component', () => {
let alfrescoContentService: ContentService;
let contentService: ContentManagementService;
let preferenceService: UserPreferencesService;
let notificationService: NotificationService;
let router: Router;
let page;
let node;
@@ -100,8 +104,10 @@ describe('Favorites Routed Component', () => {
TimeAgoPipe,
NodeNameTooltipPipe,
NodeFavoriteDirective,
NodeInfoDirective,
DocumentListComponent,
FavoritesComponent
FavoritesComponent,
AppConfigPipe
],
providers: [
{ provide: ActivatedRoute, useValue: {
@@ -112,11 +118,13 @@ describe('Favorites Routed Component', () => {
UserPreferencesService,
AppConfigService, StorageService, CookieService,
AlfrescoApiService,
CustomResourcesService,
LogService,
NotificationService,
ContentManagementService,
ContentService,
NodesApiService,
NodePermissionService,
DocumentListService,
ThumbnailService
],
@@ -127,7 +135,9 @@ describe('Favorites Routed Component', () => {
component = fixture.componentInstance;
nodesApi = TestBed.get(NodesApiService);
notificationService = TestBed.get(NotificationService);
alfrescoApi = TestBed.get(AlfrescoApiService);
alfrescoApi.reset();
alfrescoContentService = TestBed.get(ContentService);
contentService = TestBed.get(ContentManagementService);
preferenceService = TestBed.get(UserPreferencesService);
@@ -319,4 +329,16 @@ describe('Favorites Routed Component', () => {
expect(preferenceService.set).toHaveBeenCalledWith('prefix.sorting.direction', 'desc');
});
});
describe('openSnackMessage', () => {
it('should call notification service', () => {
const message = 'notification message';
spyOn(notificationService, 'openSnackMessage');
component.openSnackMessage(message);
expect(notificationService.openSnackMessage).toHaveBeenCalledWith(message, 4000);
});
});
});
@@ -28,10 +28,11 @@ import { Router, ActivatedRoute } from '@angular/router';
import { Subscription } from 'rxjs/Rx';
import { MinimalNodeEntryEntity, MinimalNodeEntity, PathElementEntity, PathInfo } from 'alfresco-js-api';
import { ContentService, NodesApiService, UserPreferencesService } from '@alfresco/adf-core';
import { ContentService, NodesApiService, UserPreferencesService, NotificationService } from '@alfresco/adf-core';
import { DocumentListComponent } from '@alfresco/adf-content-services';
import { ContentManagementService } from '../../common/services/content-management.service';
import { NodePermissionService } from '../../common/services/node-permission.service';
import { PageComponent } from '../page.component';
@Component({
@@ -51,6 +52,8 @@ export class FavoritesComponent extends PageComponent implements OnInit, OnDestr
private nodesApi: NodesApiService,
private contentService: ContentService,
private content: ContentManagementService,
private notificationService: NotificationService,
public permission: NodePermissionService,
preferences: UserPreferencesService) {
super(preferences);
@@ -122,6 +125,13 @@ export class FavoritesComponent extends PageComponent implements OnInit, OnDestr
this.preferences.set(`${this.prefix}.sorting.direction`, event.detail.direction || 'desc');
}
openSnackMessage(event: any) {
this.notificationService.openSnackMessage(
event,
4000
);
}
private get prefix() {
return this.route.snapshot.data.preferencePrefix;
}
+131 -68
View File
@@ -8,6 +8,7 @@
<adf-toolbar class="inline">
<button
color="primary"
mat-icon-button
*ngIf="canPreviewFile(documentList.selection)"
title="{{ 'APP.ACTIONS.VIEW' | translate }}"
@@ -16,6 +17,7 @@
</button>
<button
color="primary"
mat-icon-button
*ngIf="hasSelection(documentList.selection)"
title="{{ 'APP.ACTIONS.DOWNLOAD' | translate }}"
@@ -24,14 +26,25 @@
</button>
<button
color="primary"
mat-icon-button
*ngIf="canEditFolder(documentList.selection)"
title="{{ 'APP.ACTIONS.EDIT' | translate }}"
(error)="openSnackMessage($event)"
[adf-edit-folder]="documentList.selection[0]?.entry">
<mat-icon>create</mat-icon>
</button>
<button mat-icon-button
[color]="infoDrawerOpened ? 'accent' : 'primary'"
*ngIf="documentList.selection.length"
title="{{ 'APP.ACTIONS.DETAILS' | translate }}"
(click)="toggleSidebar()">
<mat-icon>info_outline</mat-icon>
</button>
<button
color="primary"
mat-icon-button
*ngIf="hasSelection(documentList.selection)"
title="{{ 'APP.ACTIONS.MORE' | translate }}"
@@ -46,7 +59,10 @@
mat-menu-item
#selection="adfFavorite"
[adf-node-favorite]="documentList.selection">
<mat-icon [ngClass]="{ 'icon-highlight': selection.hasFavorites() }">
<mat-icon [ngClass]="{
'toolbar__option--active': selection.hasFavorites(),
'toolbar__option--default': !selection.hasFavorites()
}">
{{ selection.hasFavorites() ? 'star' :'star_border' }}
</mat-icon>
<span>{{ 'APP.ACTIONS.FAVORITE' | translate }}</span>
@@ -55,7 +71,7 @@
<button
mat-menu-item
[app-copy-node]="documentList.selection">
<mat-icon>content_copy</mat-icon>
<mat-icon color="primary">content_copy</mat-icon>
<span>{{ 'APP.ACTIONS.COPY' | translate }}</span>
</button>
@@ -63,7 +79,7 @@
mat-menu-item
*ngIf="canMove(documentList.selection)"
[app-move-node]="documentList.selection">
<mat-icon>library_books</mat-icon>
<mat-icon color="primary">library_books</mat-icon>
<span>{{ 'APP.ACTIONS.MOVE' | translate }}</span>
</button>
@@ -71,88 +87,135 @@
mat-menu-item
*ngIf="canDelete(documentList.selection)"
[app-delete-node]="documentList.selection">
<mat-icon>delete</mat-icon>
<mat-icon color="primary">delete</mat-icon>
<span>{{ 'APP.ACTIONS.DELETE' | translate }}</span>
</button>
<button
mat-menu-item
*ngIf="canManageVersions(documentList.selection)"
[app-node-versions]="documentList.selection">
<mat-icon color="primary">history</mat-icon>
<span>{{ 'APP.ACTIONS.VERSIONS' | translate }}</span>
</button>
</mat-menu>
</adf-toolbar>
</div>
<div [attr.class]="isValidPath ? 'content--hide' : 'inner-layout__content'">
<div [attr.class]="isValidPath ? 'inner-layout__content--hide' : 'inner-layout__content'">
<app-generic-error></app-generic-error>
</div>
<div [attr.class]="!isValidPath ? 'content--hide' : 'inner-layout__content'">
<adf-upload-drag-area
[parentId]="node?.id"
[disabled]="!canCreateContent(node)">
<div [attr.class]="!isValidPath ? 'inner-layout__content--hide' : 'inner-layout__content'">
<div class="inner-layout__panel" [ngClass]="{'adf-document-list--loading': isLoading}">
<adf-upload-drag-area
[parentId]="node?.id"
[disabled]="!permission.check(node, ['create'])">
<adf-document-list #documentList
[attr.class]="documentList.isEmpty() ? 'empty-list' : ''"
[loading]="isLoading"
[node]="paging"
[sorting]="sorting"
[allowDropFiles]="true"
<adf-document-list #documentList
[attr.class]="documentList.isEmpty() ? 'adf-document-list--empty' : ''"
[loading]="isLoading"
[node]="paging"
[sorting]="sorting"
[allowDropFiles]="true"
[contextMenuActions]="true"
[contentActions]="false"
[navigate]="false"
[selectionMode]="'multiple'"
[imageResolver]="imageResolver"
[contextMenuActions]="true"
[contentActions]="false"
[navigate]="false"
[selectionMode]="'multiple'"
[imageResolver]="imageResolver"
(sorting-changed)="onSortingChanged($event)"
(node-dblclick)="onNodeDoubleClick($event)"
(node-select)="onNodeSelect($event, documentList)">
(sorting-changed)="onSortingChanged($event)"
(node-dblclick)="onNodeDoubleClick($event)"
(node-select)="onNodeSelect($event, documentList)">
<data-columns>
<data-column
key="$thumbnail"
type="image"
[sortable]="false"
class="image-table-cell">
</data-column>
<data-columns>
<data-column
key="$thumbnail"
type="image"
[sortable]="false"
class="image-table-cell">
</data-column>
<data-column
class="adf-data-table-cell--ellipsis__name"
key="name"
title="APP.DOCUMENT_LIST.COLUMNS.NAME">
<ng-template let-value="value" let-context>
<span class="adf-datatable-cell" title="{{ context?.row?.obj | adfNodeNameTooltip }}">{{ value }}</span>
</ng-template>
</data-column>
<data-column
class="adf-data-table-cell--ellipsis__name"
key="name"
title="APP.DOCUMENT_LIST.COLUMNS.NAME">
<ng-template let-value="value" let-context>
<span class="adf-datatable-cell" title="{{ context?.row?.obj | adfNodeNameTooltip }}">{{ value }}</span>
</ng-template>
</data-column>
<data-column
key="content.sizeInBytes"
title="APP.DOCUMENT_LIST.COLUMNS.SIZE">
<ng-template let-value="value">
<span title="{{ value }} bytes">{{ value | adfFileSize }}</span>
</ng-template>
</data-column>
<data-column
key="content.sizeInBytes"
title="APP.DOCUMENT_LIST.COLUMNS.SIZE">
<ng-template let-value="value">
<span title="{{ value }} bytes">{{ value | adfFileSize }}</span>
</ng-template>
</data-column>
<data-column
key="modifiedAt"
title="APP.DOCUMENT_LIST.COLUMNS.MODIFIED_ON">
<ng-template let-value="value">
<span title="{{ value | date:'medium' }}">{{ value | adfTimeAgo }}</span>
</ng-template>
</data-column>
<data-column
key="modifiedAt"
title="APP.DOCUMENT_LIST.COLUMNS.MODIFIED_ON">
<ng-template let-value="value">
<span title="{{ value | date:'medium' }}">{{ value | adfTimeAgo }}</span>
</ng-template>
</data-column>
<data-column
class="adf-data-table-cell--ellipsis"
key="modifiedByUser.displayName"
title="APP.DOCUMENT_LIST.COLUMNS.MODIFIED_BY">
</data-column>
</data-columns>
</adf-document-list>
<data-column
class="adf-data-table-cell--ellipsis"
key="modifiedByUser.displayName"
title="APP.DOCUMENT_LIST.COLUMNS.MODIFIED_BY">
</data-column>
</data-columns>
</adf-document-list>
<ng-container *ngIf="!isEmpty">
<adf-pagination
[supportedPageSizes]="documentList.supportedPageSizes"
[pagination]="pagination"
(change)="load(true, $event)"
(changePageSize)="onChangePageSize($event)">
</adf-pagination>
</ng-container>
</adf-upload-drag-area>
<ng-container *ngIf="!isEmpty">
<adf-pagination
[supportedPageSizes]="'pagination.supportedPageSizes' | appConfig"
[pagination]="pagination"
(change)="load(true, $event)"
(changePageSize)="onChangePageSize($event)">
</adf-pagination>
</ng-container>
</adf-upload-drag-area>
</div>
<div class="inner-layout__side-panel"
*ngIf="infoDrawerOpened"
[app-node-info]="documentList.selection"
(changed)="toggleSidebar($event)"
#infoInstance=nodeInfo>
<adf-info-drawer [title]="'APP.INFO_DRAWER.TITLE' | translate">
<adf-info-drawer-tab [label]="'APP.INFO_DRAWER.TABS.PROPERTIES' | translate">
<div *ngIf="infoInstance.loading">
<mat-progress-bar mode="indeterminate"></mat-progress-bar>
</div>
<adf-content-metadata-card
[readOnly]="!canUpdate(documentList.selection)"
[displayEmpty]="canUpdate(documentList.selection)"
[preset]="'custom'"
[node]="infoInstance.node">
</adf-content-metadata-card>
</adf-info-drawer-tab>
<adf-info-drawer-tab [label]="'APP.INFO_DRAWER.TABS.VERSIONS' | translate">
<ng-container *ngIf="isFileSelected(documentList.selection);else choose_document_template">
<adf-version-manager
*ngIf="infoInstance.node"
[node]="infoInstance.node">
</adf-version-manager>
</ng-container>
<ng-template #choose_document_template>
<div class="adf-manage-versions-empty">
<mat-icon class="adf-manage-versions-empty-icon">face</mat-icon>
{{ 'VERSION.SELECTION.EMPTY' | translate }}
</div>
</ng-template>
</adf-info-drawer-tab>
</adf-info-drawer>
</div>
</div>
</div>
@@ -39,12 +39,15 @@ import {
TimeAgoPipe, NodeNameTooltipPipe, FileSizePipe, NodeFavoriteDirective,
DataTableComponent, UploadService
} from '@alfresco/adf-core';
import { DocumentListComponent } from '@alfresco/adf-content-services';
import { DocumentListComponent, CustomResourcesService } from '@alfresco/adf-content-services';
import { MatMenuModule, MatSnackBarModule, MatIconModule, MatDialogModule } from '@angular/material';
import { DocumentListService } from '@alfresco/adf-content-services';
import { ContentManagementService } from '../../common/services/content-management.service';
import { BrowsingFilesService } from '../../common/services/browsing-files.service';
import { NodeActionsService } from '../../common/services/node-actions.service';
import { NodePermissionService } from '../../common/services/node-permission.service';
import { NodeInfoDirective } from '../../common/directives/node-info.directive';
import { AppConfigPipe } from '../../common/pipes/app-config.pipe';
import { FilesComponent } from './files.component';
@@ -61,6 +64,7 @@ describe('FilesComponent', () => {
let browsingFilesService: BrowsingFilesService;
let nodeActionsService: NodeActionsService;
let preferenceService: UserPreferencesService;
let notificationService: NotificationService;
beforeEach(async(() => {
TestBed.configureTestingModule({
@@ -79,8 +83,10 @@ describe('FilesComponent', () => {
TimeAgoPipe,
NodeNameTooltipPipe,
NodeFavoriteDirective,
NodeInfoDirective,
DocumentListComponent,
FileSizePipe
FileSizePipe,
AppConfigPipe
],
providers: [
{ provide: ActivatedRoute, useValue: {
@@ -100,8 +106,10 @@ describe('FilesComponent', () => {
DocumentListService,
ThumbnailService,
NodeActionsService,
NodePermissionService,
UploadService,
BrowsingFilesService
BrowsingFilesService,
CustomResourcesService
],
schemas: [ NO_ERRORS_SCHEMA ]
}).compileComponents()
@@ -116,6 +124,7 @@ describe('FilesComponent', () => {
router = TestBed.get(Router);
alfrescoContentService = TestBed.get(ContentService);
browsingFilesService = TestBed.get(BrowsingFilesService);
notificationService = TestBed.get(NotificationService);
nodeActionsService = TestBed.get(NodeActionsService);
preferenceService = TestBed.get(UserPreferencesService);
});
@@ -324,31 +333,6 @@ describe('FilesComponent', () => {
});
});
describe('Create permission', () => {
beforeEach(() => {
spyOn(component, 'fetchNode').and.returnValue(Observable.of(node));
spyOn(component, 'fetchNodes').and.returnValue(Observable.of(page));
fixture.detectChanges();
});
it('returns false when node is not provided', () => {
expect(component.canCreateContent(null)).toBe(false);
});
it('returns false when node does not have permission', () => {
spyOn(alfrescoContentService, 'hasPermission').and.returnValue(false);
expect(component.canCreateContent(node)).toBe(false);
});
it('returns false when node has permission', () => {
spyOn(alfrescoContentService, 'hasPermission').and.returnValue(true);
expect(component.canCreateContent(node)).toBe(true);
});
});
describe('onNodeDoubleClick()', () => {
beforeEach(() => {
spyOn(component, 'fetchNode').and.returnValue(Observable.of(node));
@@ -436,7 +420,7 @@ describe('FilesComponent', () => {
component.load();
expect(component.paging).toBe(page);
expect(component.pagination).toBe(page.list.pagination);
expect(component.pagination).toEqual(page.list.pagination);
});
it('raise error on fail', () => {
@@ -551,4 +535,16 @@ describe('FilesComponent', () => {
expect(preferenceService.set).toHaveBeenCalledWith('prefix.sorting.direction', 'desc');
});
});
describe('openSnackMessage', () => {
it('should call notification service', () => {
const message = 'notification message';
spyOn(notificationService, 'openSnackMessage');
component.openSnackMessage(message);
expect(notificationService.openSnackMessage).toHaveBeenCalledWith(message, 4000);
});
});
});
+18 -16
View File
@@ -29,12 +29,13 @@ import { Router, ActivatedRoute, Params } from '@angular/router';
import { MinimalNodeEntity, MinimalNodeEntryEntity, PathElementEntity, NodePaging, PathElement } from 'alfresco-js-api';
import {
UploadService, FileUploadEvent, NodesApiService,
ContentService, AlfrescoApiService, UserPreferencesService
ContentService, AlfrescoApiService, UserPreferencesService, NotificationService
} from '@alfresco/adf-core';
import { BrowsingFilesService } from '../../common/services/browsing-files.service';
import { ContentManagementService } from '../../common/services/content-management.service';
import { NodeActionsService } from '../../common/services/node-actions.service';
import { NodePermissionService } from '../../common/services/node-permission.service';
import { PageComponent } from '../page.component';
@@ -43,7 +44,6 @@ import { PageComponent } from '../page.component';
})
export class FilesComponent extends PageComponent implements OnInit, OnDestroy {
private routeData: any = {};
isValidPath = true;
private nodePath: PathElement[];
@@ -61,6 +61,8 @@ export class FilesComponent extends PageComponent implements OnInit, OnDestroy {
private browsingFilesService: BrowsingFilesService,
private contentService: ContentService,
private apiService: AlfrescoApiService,
private notificationService: NotificationService,
public permission: NodePermissionService,
preferences: UserPreferencesService) {
super(preferences);
@@ -74,7 +76,6 @@ export class FilesComponent extends PageComponent implements OnInit, OnDestroy {
const { route, contentManagementService, contentService, nodeActionsService, uploadService } = this;
const { data } = route.snapshot;
this.routeData = data;
this.title = data.i18nTitle;
route.params.subscribe(({ folderId }: Params) => {
@@ -125,7 +126,7 @@ export class FilesComponent extends PageComponent implements OnInit, OnDestroy {
return this.nodesApi.getNode(nodeId);
}
fetchNodes(parentNodeId?: string, options: any = {}): Observable<NodePaging> {
fetchNodes(parentNodeId?: string, options: { maxItems?: number, skipCount?: number } = {}): Observable<NodePaging> {
const defaults = {
include: [ 'isLocked', 'path', 'properties', 'allowableOperations' ]
};
@@ -202,23 +203,17 @@ export class FilesComponent extends PageComponent implements OnInit, OnDestroy {
}
}
canCreateContent(parentNode: MinimalNodeEntryEntity): boolean {
if (parentNode) {
return this.contentService.hasPermission(parentNode, 'create');
}
return false;
}
load(showIndicator: boolean = false, pagination: any = {}) {
load(showIndicator: boolean = false, options: { maxItems?: number, skipCount?: number } = {}) {
this.isLoading = showIndicator;
this.fetchNodes(this.getParentNodeId(), pagination)
this.fetchNodes(this.getParentNodeId(), options)
.flatMap((page) => {
if (this.isCurrentPageEmpty(page) && this.isNotFirstPage(page)) {
const newSkipCount = pagination.skipCount - pagination.maxItems;
const newSkipCount = options.skipCount - options.maxItems;
return this.fetchNodes(this.getParentNodeId(), {skipCount: newSkipCount, maxItems: pagination.maxItems});
return this.fetchNodes(this.getParentNodeId(), {
skipCount: newSkipCount, maxItems: options.maxItems
});
}
return Observable.of(page);
@@ -313,6 +308,13 @@ export class FilesComponent extends PageComponent implements OnInit, OnDestroy {
this.preferences.set(`${this.prefix}.sorting.direction`, event.detail.direction || 'desc');
}
openSnackMessage(event: any) {
this.notificationService.openSnackMessage(
event,
4000
);
}
private get prefix() {
return this.route.snapshot.data.preferencePrefix;
}
@@ -1,5 +1,7 @@
@import 'variables';
$alfresco-warn-color--hue-2: #ff5252;
.generic-error {
color: $alfresco-secondary-text-color;
display: flex;
@@ -1,5 +1,9 @@
<adf-toolbar class="app-menu" [style.background-color]="backgroundColor">
<adf-toolbar-title>
<button mat-icon-button (click)="toggleMenu()">
<mat-icon>menu</mat-icon>
</button>
<a class="app-menu__title"
title="{{ appName }}"
[routerLink]="[ '/' ]">
@@ -7,7 +11,7 @@
</a>
</adf-toolbar-title>
<app-search></app-search>
<app-search-input></app-search-input>
<adf-toolbar-divider></adf-toolbar-divider>
@@ -1,6 +1,9 @@
@import 'variables';
$app-menu-height: 64px;
.app-menu {
height: $app-menu-height;
&.adf-toolbar {
.mat-toolbar {
@@ -26,12 +29,20 @@
background-color: $alfresco-white !important;
}
}
.adf-toolbar-title {
color: $alfresco-white;
display: flex;
flex-direction: row;
align-items: center;
justify-content: center;
}
}
.app-menu__title {
width: 100px;
height: 50px;
margin-left: 5px;
margin-left: 40px;
display: flex;
justify-content: center;
align-items: stretch;
@@ -24,7 +24,7 @@
*/
import { DomSanitizer } from '@angular/platform-browser';
import { Component, ViewEncapsulation, SecurityContext } from '@angular/core';
import { Component, Output, EventEmitter, ViewEncapsulation, SecurityContext } from '@angular/core';
import { AppConfigService } from '@alfresco/adf-core';
@Component({
@@ -34,6 +34,8 @@ import { AppConfigService } from '@alfresco/adf-core';
encapsulation: ViewEncapsulation.None
})
export class HeaderComponent {
@Output() menu: EventEmitter<any> = new EventEmitter<any>();
private defaultPath = '/assets/images/alfresco-logo-white.svg';
private defaultBackgroundColor = '#2196F3';
@@ -42,6 +44,10 @@ export class HeaderComponent {
private sanitizer: DomSanitizer
) {}
toggleMenu() {
this.menu.emit();
}
get appName(): string {
return <string>this.appConfig.get('application.name');
}
@@ -1,14 +1,37 @@
<div class="layout">
<adf-upload-drag-area
[parentId]="node?.id"
[disabled]="!canCreateContent(node)">
<app-header></app-header>
[disabled]="!permission.check(node, ['create'])">
<div class="layout__content">
<app-sidenav class="layout__content-side"></app-sidenav>
<router-outlet></router-outlet>
</div>
<adf-sidenav-layout
[sidenavMin]="70"
[sidenavMax]="320"
[stepOver]="600"
[hideSidenav]="isPreview"
[expandedSidenav]="true">
<adf-sidenav-layout-header>
<ng-template let-toggleMenu="toggleMenu">
<app-header (menu)="toggleMenu()"></app-header>
</ng-template>
</adf-sidenav-layout-header>
<adf-sidenav-layout-navigation>
<ng-template let-isMenuMinimized="isMenuMinimized">
<app-sidenav [showLabel]="!isMenuMinimized()"></app-sidenav>
</ng-template>
</adf-sidenav-layout-navigation>
<adf-sidenav-layout-content>
<ng-template>
<router-outlet></router-outlet>
</ng-template>
</adf-sidenav-layout-content>
</adf-sidenav-layout>
<adf-file-uploading-dialog position="left"></adf-file-uploading-dialog>
</adf-upload-drag-area>
</div>
</div>
@@ -1,8 +1,4 @@
:host {
display: flex;
flex: 1;
router-outlet {
flex: 0 0;
}
}
@@ -30,7 +30,7 @@ import { MinimalNodeEntryEntity } from 'alfresco-js-api';
import { TranslateModule } from '@ngx-translate/core';
import { HttpClientModule } from '@angular/common/http';
import {
ContentService, PeopleContentService, AppConfigService,
PeopleContentService, AppConfigService,
AuthenticationService, UserPreferencesService, TranslationService,
TranslationMock, StorageService, AlfrescoApiService, CookieService,
LogService
@@ -38,14 +38,13 @@ import {
import { Observable } from 'rxjs/Observable';
import { BrowsingFilesService } from '../../common/services/browsing-files.service';
import { NodePermissionService } from '../../common/services/node-permission.service';
import { LayoutComponent } from './layout.component';
describe('LayoutComponent', () => {
let fixture: ComponentFixture<LayoutComponent>;
let component: LayoutComponent;
let browsingFilesService: BrowsingFilesService;
let contentService: ContentService;
let node;
const navItem = {
label: 'some-label',
route: {
@@ -54,8 +53,6 @@ describe('LayoutComponent', () => {
};
beforeEach(() => {
node = { id: 'node-id' };
TestBed.configureTestingModule({
imports: [
HttpClientModule,
@@ -73,8 +70,8 @@ describe('LayoutComponent', () => {
LogService,
UserPreferencesService,
AuthenticationService,
ContentService,
AppConfigService,
NodePermissionService,
BrowsingFilesService,
{
provide: PeopleContentService,
@@ -89,7 +86,6 @@ describe('LayoutComponent', () => {
fixture = TestBed.createComponent(LayoutComponent);
component = fixture.componentInstance;
browsingFilesService = TestBed.get(BrowsingFilesService);
contentService = TestBed.get(ContentService);
const appConfig = TestBed.get(AppConfigService);
spyOn(appConfig, 'get').and.returnValue([navItem]);
@@ -104,28 +100,4 @@ describe('LayoutComponent', () => {
expect(component.node).toEqual(currentNode);
});
describe('canCreateContent()', () => {
it('returns true if node has permission', () => {
spyOn(contentService, 'hasPermission').and.returnValue(true);
const permission = component.canCreateContent(<any>{});
expect(permission).toBe(true);
});
it('returns false if node does not have permission', () => {
spyOn(contentService, 'hasPermission').and.returnValue(false);
const permission = component.canCreateContent(<any>{});
expect(permission).toBe(false);
});
it('returns false if node is null', () => {
const permission = component.canCreateContent(null);
expect(permission).toBe(false);
});
});
});
+12 -10
View File
@@ -24,10 +24,11 @@
*/
import { Component, OnInit, OnDestroy } from '@angular/core';
import { Router, NavigationEnd } from '@angular/router';
import { Subscription } from 'rxjs/Rx';
import { MinimalNodeEntryEntity } from 'alfresco-js-api';
import { ContentService } from '@alfresco/adf-core';
import { BrowsingFilesService } from '../../common/services/browsing-files.service';
import { NodePermissionService } from '../../common/services/node-permission.service';
@Component({
selector: 'app-layout',
@@ -36,12 +37,20 @@ import { BrowsingFilesService } from '../../common/services/browsing-files.servi
})
export class LayoutComponent implements OnInit, OnDestroy {
node: MinimalNodeEntryEntity;
isPreview = false;
private subscriptions: Subscription[] = [];
constructor(
private contentService: ContentService,
private browsingFilesService: BrowsingFilesService) {}
private router: Router,
private browsingFilesService: BrowsingFilesService,
public permission: NodePermissionService) {
this.router.events
.filter(event => event instanceof NavigationEnd)
.subscribe( (event: any ) => {
this.isPreview = event.urlAfterRedirects.includes('preview');
});
}
ngOnInit() {
this.subscriptions.concat([
@@ -52,11 +61,4 @@ export class LayoutComponent implements OnInit, OnDestroy {
ngOnDestroy() {
this.subscriptions.forEach(s => s.unsubscribe());
}
canCreateContent(node: MinimalNodeEntryEntity): boolean {
if (node) {
return this.contentService.hasPermission(node, 'create');
}
return false;
}
}
@@ -7,68 +7,72 @@
</div>
<div class="inner-layout__content">
<div class="inner-layout__panel" [ngClass]="{'adf-document-list--loading': isLoading}">
<adf-document-list #documentList
[attr.class]="documentList.isEmpty() ? 'adf-document-list--empty' : ''"
currentFolderId="-mysites-"
selectionMode="none"
[navigate]="false"
[sorting]="sorting"
[contextMenuActions]="true"
[contentActions]="false"
(sorting-changed)="onSortingChanged($event)"
(node-dblclick)="onNodeDoubleClick($event)">
<adf-document-list #documentList
[attr.class]="documentList.isEmpty() ? 'empty-list' : ''"
currentFolderId="-mysites-"
selectionMode="none"
[navigate]="false"
[sorting]="sorting"
[contextMenuActions]="true"
[contentActions]="false"
(sorting-changed)="onSortingChanged($event)"
(node-dblclick)="onNodeDoubleClick($event)">
<empty-folder-content>
<ng-template>
<app-empty-folder
icon="group_work"
title="APP.BROWSE.LIBRARIES.EMPTY_STATE.TITLE"
subtitle="APP.BROWSE.LIBRARIES.EMPTY_STATE.TEXT">
</app-empty-folder>
</ng-template>
</empty-folder-content>
<data-columns>
<data-column
key="$thumbnail"
type="image"
[sortable]="false"
class="image-table-cell">
</data-column>
<data-column
class="adf-data-table-cell--ellipsis__name"
key="title"
title="APP.DOCUMENT_LIST.COLUMNS.TITLE">
<ng-template let-context>
<span class="adf-datatable-cell" title="{{ makeLibraryTooltip(context.row.obj.entry) }}">
{{ makeLibraryTitle(context.row.obj.entry) }}
</span>
<empty-folder-content>
<ng-template>
<app-empty-folder
icon="group_work"
title="APP.BROWSE.LIBRARIES.EMPTY_STATE.TITLE"
subtitle="APP.BROWSE.LIBRARIES.EMPTY_STATE.TEXT">
</app-empty-folder>
</ng-template>
</data-column>
</empty-folder-content>
<data-column
key="visibility"
title="APP.DOCUMENT_LIST.COLUMNS.STATUS">
<ng-template let-value="value">
<span *ngIf="(value == 'PUBLIC')" title="{{ 'APP.SITES_VISIBILITY.PUBLIC' | translate }}">
{{ 'APP.SITES_VISIBILITY.PUBLIC' | translate }}
</span>
<span *ngIf="(value == 'PRIVATE')" title="{{ 'APP.SITES_VISIBILITY.PRIVATE' | translate }}">
{{ 'APP.SITES_VISIBILITY.PRIVATE' | translate }}
</span>
<span *ngIf="(value == 'MODERATED')" title="{{ 'APP.SITES_VISIBILITY.MODERATED' | translate }}">
{{ 'APP.SITES_VISIBILITY.MODERATED' | translate }}
</span>
</ng-template>
</data-column>
</data-columns>
</adf-document-list>
<data-columns>
<data-column
key="$thumbnail"
type="image"
[sortable]="false"
class="image-table-cell">
</data-column>
<adf-pagination [ngClass]="{ 'no-border' : documentList.isEmpty()}"
[target]="documentList"
(changePageSize)="onChangePageSize($event)">
</adf-pagination>
<data-column
class="adf-data-table-cell--ellipsis__name"
key="title"
title="APP.DOCUMENT_LIST.COLUMNS.TITLE">
<ng-template let-context>
<span class="adf-datatable-cell" title="{{ makeLibraryTooltip(context.row.obj.entry) }}">
{{ makeLibraryTitle(context.row.obj.entry) }}
</span>
</ng-template>
</data-column>
<data-column
key="visibility"
title="APP.DOCUMENT_LIST.COLUMNS.STATUS">
<ng-template let-value="value">
<span *ngIf="(value == 'PUBLIC')" title="{{ 'APP.SITES_VISIBILITY.PUBLIC' | translate }}">
{{ 'APP.SITES_VISIBILITY.PUBLIC' | translate }}
</span>
<span *ngIf="(value == 'PRIVATE')" title="{{ 'APP.SITES_VISIBILITY.PRIVATE' | translate }}">
{{ 'APP.SITES_VISIBILITY.PRIVATE' | translate }}
</span>
<span *ngIf="(value == 'MODERATED')" title="{{ 'APP.SITES_VISIBILITY.MODERATED' | translate }}">
{{ 'APP.SITES_VISIBILITY.MODERATED' | translate }}
</span>
</ng-template>
</data-column>
</data-columns>
</adf-document-list>
<ng-container *ngIf="!documentList.isEmpty()">
<adf-pagination
[supportedPageSizes]="'pagination.supportedPageSizes' | appConfig"
[target]="documentList"
(changePageSize)="onChangePageSize($event)">
</adf-pagination>
</ng-container>
</div>
</div>
</div>
@@ -36,12 +36,13 @@ import {
StorageService, CookieService, ThumbnailService, AuthenticationService,
TimeAgoPipe, NodeNameTooltipPipe, NodeFavoriteDirective, DataTableComponent
} from '@alfresco/adf-core';
import { DocumentListComponent } from '@alfresco/adf-content-services';
import { DocumentListComponent, CustomResourcesService } from '@alfresco/adf-content-services';
import { TranslateModule } from '@ngx-translate/core';
import { NoopAnimationsModule } from '@angular/platform-browser/animations';
import { MatMenuModule, MatSnackBarModule, MatIconModule } from '@angular/material';
import { DocumentListService } from '@alfresco/adf-content-services';
import { ShareDataTableAdapter } from '@alfresco/adf-content-services';
import { AppConfigPipe } from '../../common/pipes/app-config.pipe';
import { LibrariesComponent } from './libraries.component';
@@ -87,7 +88,8 @@ describe('Libraries Routed Component', () => {
NodeNameTooltipPipe,
NodeFavoriteDirective,
DocumentListComponent,
LibrariesComponent
LibrariesComponent,
AppConfigPipe
],
providers: [
{ provide: ActivatedRoute, useValue: {
@@ -103,7 +105,8 @@ describe('Libraries Routed Component', () => {
ContentService,
NodesApiService,
DocumentListService,
ThumbnailService
ThumbnailService,
CustomResourcesService
],
schemas: [ NO_ERRORS_SCHEMA ]
})
@@ -113,6 +116,7 @@ describe('Libraries Routed Component', () => {
nodesApi = TestBed.get(NodesApiService);
alfrescoApi = TestBed.get(AlfrescoApiService);
alfrescoApi.reset();
router = TestBed.get(Router);
preferenceService = TestBed.get(UserPreferencesService);
});
@@ -191,8 +195,6 @@ describe('Libraries Routed Component', () => {
component.navigate(node.id);
fixture.detectChanges();
expect(routerSpy.calls.argsFor(0)[0]).toEqual(['./', document.id]);
});
});
@@ -39,7 +39,7 @@ import { Observable } from 'rxjs/Rx';
encapsulation: ViewEncapsulation.None,
// tslint:disable-next-line:use-host-property-decorator
host: {
'class': 'app-location-link'
'class': 'app-location-link adf-location-cell'
}
})
export class LocationLinkComponent implements OnInit {
+8 -38
View File
@@ -104,7 +104,7 @@ describe('PageComponent', () => {
});
it('sets component pagination data', () => {
expect(component.pagination).toBe(page.list.pagination);
expect(component.pagination).toEqual(page.list.pagination);
});
it('sets component isEmpty state', () => {
@@ -299,51 +299,21 @@ describe('PageComponent', () => {
});
});
describe('canDeleteShared()', () => {
it('should return true if shared node can be deleted', () => {
describe('canUpdate()', () => {
it('should return true if node can be edited', () => {
const selection = [ { entry: {
allowableOperationsOnTarget: ['delete']
allowableOperations: ['update']
} } ];
expect(component.canDeleteShared(selection)).toBe(true);
expect(component.canUpdate(selection)).toBe(true);
});
it(`should return false if shared node doesn't have permission`, () => {
it(`should return false if node cannot be edited`, () => {
const selection = [ { entry: {
allowableOperationsOnTarget: ['something']
allowableOperations: ['other-permission']
} } ];
expect(component.canDeleteShared(selection)).toBe(false);
});
it(`should return false if shared node doesn't have permissions property`, () => {
const selection = [ { entry: { } } ];
expect(component.canDeleteShared(selection)).toBe(false);
});
});
describe('canMoveShared()', () => {
it('should return true if shared node can be moved', () => {
const selection = [ { entry: {
allowableOperationsOnTarget: ['delete']
} } ];
expect(component.canMoveShared(selection)).toBe(true);
});
it(`should return false if shared node doesn't have permission`, () => {
const selection = [ { entry: {
allowableOperationsOnTarget: ['something']
} } ];
expect(component.canMoveShared(selection)).toBe(false);
});
it(`should return false if shared node doesn't have permissions property`, () => {
const selection = [ { entry: { } } ];
expect(component.canMoveShared(selection)).toBe(false);
expect(component.canUpdate(selection)).toBe(false);
});
});
});
+19 -19
View File
@@ -33,6 +33,7 @@ export abstract class PageComponent {
isLoading = false;
isEmpty = true;
infoDrawerOpened = false;
paging: NodePaging;
pagination: Pagination;
@@ -63,7 +64,7 @@ export abstract class PageComponent {
onPageLoaded(page: NodePaging) {
this.isLoading = false;
this.paging = page;
this.pagination = page.list.pagination;
this.pagination = { ...page.list.pagination };
this.isEmpty = !(page.list.entries && page.list.entries.length > 0);
}
@@ -111,16 +112,12 @@ export abstract class PageComponent {
return selection.every(node => node.entry && this.nodeHasPermission(node.entry, 'delete'));
}
canDeleteShared(selection: Array<MinimalNodeEntity> = []): boolean {
return selection.every(node => node.entry && this.nodeSharedHasPermission(node.entry, 'delete'));
}
canMove(selection: Array<MinimalNodeEntity>): boolean {
return this.canDelete(selection);
}
canMoveShared(selection: Array<MinimalNodeEntity> = []): boolean {
return selection.every(node => node.entry && this.nodeSharedHasPermission(node.entry, 'delete'));
canUpdate(selection: Array<MinimalNodeEntity> = []): boolean {
return selection.every(node => node.entry && this.nodeHasPermission(node.entry, 'update'));
}
canPreviewFile(selection: Array<MinimalNodeEntity>): boolean {
@@ -135,6 +132,14 @@ export abstract class PageComponent {
return this.isFileSelected(selection);
}
canUpdateFile(selection: Array<MinimalNodeEntity>): boolean {
return this.isFileSelected(selection) && this.nodeHasPermission(selection[0].entry, 'update');
}
canManageVersions(selection: Array<MinimalNodeEntity>): boolean {
return this.canUpdateFile(selection);
}
nodeHasPermission(node: MinimalNodeEntryEntity, permission: string): boolean {
if (node && permission) {
const { allowableOperations = [] } = <any>(node || {});
@@ -147,18 +152,6 @@ export abstract class PageComponent {
return false;
}
nodeSharedHasPermission(node: MinimalNodeEntryEntity, permission: string): boolean {
if (node && permission) {
const { allowableOperationsOnTarget } = <any>(node || {});
if (allowableOperationsOnTarget && allowableOperationsOnTarget.indexOf(permission) > -1) {
return true;
}
}
return false;
}
onChangePageSize(event: Pagination): void {
this.preferences.paginationSize = event.maxItems;
}
@@ -204,4 +197,11 @@ export abstract class PageComponent {
return null;
}
toggleSidebar(event) {
if (event) {
return;
}
this.infoDrawerOpened = !this.infoDrawerOpened;
}
}
@@ -1,7 +1,29 @@
<ng-container *ngIf="nodeId">
<ng-template #sidebarTemplate>
<adf-info-drawer [title]="'APP.INFO_DRAWER.TITLE' | translate">
<adf-info-drawer-tab [label]="'APP.INFO_DRAWER.TABS.PROPERTIES' | translate">
<adf-content-metadata-card
[readOnly]="!permission.check(node, ['update'])"
[displayEmpty]="permission.check(node, ['update'])"
[preset]="'custom'"
[node]="node">
</adf-content-metadata-card>
</adf-info-drawer-tab>
<adf-info-drawer-tab [label]="'APP.INFO_DRAWER.TABS.VERSIONS' | translate">
<ng-container>
<adf-version-manager [node]="node"></adf-version-manager>
</ng-container>
</adf-info-drawer-tab>
</adf-info-drawer>
</ng-template>
<adf-viewer
[fileNodeId]="nodeId"
[allowNavigate]="navigateMultiple"
[allowSidebar]="true"
[sidebarTemplate]="sidebarTemplate"
[canNavigateBefore]="previousNodeId"
[canNavigateNext]="nextNodeId"
[overlayMode]="true"
@@ -15,7 +37,10 @@
mat-menu-item
#selection="adfFavorite"
[adf-node-favorite]="selectedEntities">
<mat-icon [ngClass]="{ 'icon-highlight': selection.hasFavorites() }">
<mat-icon [ngClass]="{
'toolbar__option--active': selection.hasFavorites(),
'toolbar__option--default': !selection.hasFavorites()
}">
{{ selection.hasFavorites() ? 'star' :'star_border' }}
</mat-icon>
<span>{{ 'APP.ACTIONS.FAVORITE' | translate }}</span>
@@ -23,28 +48,34 @@
<button
mat-menu-item
*ngIf="canCopyFile()"
[app-copy-node]="selectedEntities">
<mat-icon>content_copy</mat-icon>
<mat-icon color="primary">content_copy</mat-icon>
<span>{{ 'APP.ACTIONS.COPY' | translate }}</span>
</button>
<button
mat-menu-item
*ngIf="canMoveFile()"
*ngIf="permission.check(node, ['delete'])"
[app-move-node]="selectedEntities">
<mat-icon>library_books</mat-icon>
<mat-icon color="primary">library_books</mat-icon>
<span>{{ 'APP.ACTIONS.MOVE' | translate }}</span>
</button>
<button
mat-menu-item
*ngIf="canDeleteFile()"
*ngIf="permission.check(node, ['delete'])"
(click)="deleteFile()">
<mat-icon>delete</mat-icon>
<mat-icon color="primary">delete</mat-icon>
<span>{{ 'APP.ACTIONS.DELETE' | translate }}</span>
</button>
<button
mat-menu-item
*ngIf="permission.check(node, ['update'])"
[app-node-versions]="selectedEntities">
<mat-icon color="primary">history</mat-icon>
<span>{{ 'APP.ACTIONS.VERSIONS' | translate }}</span>
</button>
</adf-viewer-more-actions>
</adf-viewer>
</ng-container>
@@ -36,6 +36,7 @@ import { HttpClientModule } from '@angular/common/http';
import { PreviewComponent } from './preview.component';
import { Observable } from 'rxjs/Rx';
import { NodePermissionService } from '../../common/services/node-permission.service';
import { ContentManagementService } from '../../common/services/content-management.service';
import { MatSnackBarModule } from '@angular/material';
@@ -64,6 +65,7 @@ describe('PreviewComponent', () => {
CookieService,
NotificationService,
UserPreferencesService,
NodePermissionService,
ContentManagementService
],
declarations: [
@@ -79,6 +81,7 @@ describe('PreviewComponent', () => {
router = TestBed.get(Router);
route = TestBed.get(ActivatedRoute);
alfrescoApi = TestBed.get(AlfrescoApiService);
alfrescoApi.reset();
preferences = TestBed.get(UserPreferencesService);
});
}));
@@ -27,6 +27,7 @@ import { Component, OnInit, ViewEncapsulation } from '@angular/core';
import { ActivatedRoute, Router } from '@angular/router';
import { AlfrescoApiService, UserPreferencesService, ObjectUtils } from '@alfresco/adf-core';
import { Node, MinimalNodeEntity } from 'alfresco-js-api';
import { NodePermissionService } from '../../common/services/node-permission.service';
import { ContentManagementService } from '../../common/services/content-management.service';
@Component({
@@ -54,10 +55,11 @@ export class PreviewComponent implements OnInit {
selectedEntities: MinimalNodeEntity[] = [];
constructor(private router: Router,
private route: ActivatedRoute,
private apiService: AlfrescoApiService,
private preferences: UserPreferencesService,
private content: ContentManagementService) {
private route: ActivatedRoute,
private apiService: AlfrescoApiService,
private preferences: UserPreferencesService,
private content: ContentManagementService,
public permission: NodePermissionService) {
}
ngOnInit() {
@@ -324,10 +326,6 @@ export class PreviewComponent implements OnInit {
return path;
}
canDeleteFile(): boolean {
return this.content.canDeleteNode(this.node);
}
async deleteFile() {
try {
await this.content.deleteNode(this.node);
@@ -335,12 +333,4 @@ export class PreviewComponent implements OnInit {
} catch {
}
}
canMoveFile(): boolean {
return this.content.canMoveNode(this.node);
}
canCopyFile(): boolean {
return this.content.canCopyNode(this.node);
}
}
@@ -7,6 +7,7 @@
<button
mat-icon-button
color="primary"
*ngIf="canPreviewFile(documentList.selection)"
title="{{ 'APP.ACTIONS.VIEW' | translate }}"
(click)="onNodeDoubleClick(documentList.selection[0]?.entry)">
@@ -15,19 +16,30 @@
<button
mat-icon-button
color="primary"
*ngIf="hasSelection(documentList.selection)"
title="{{ 'APP.ACTIONS.DOWNLOAD' | translate }}"
[adfNodeDownload]="documentList.selection">
<mat-icon>get_app</mat-icon>
</button>
<button mat-icon-button
[color]="infoDrawerOpened ? 'accent' : 'primary'"
*ngIf="documentList.selection.length"
title="{{ 'APP.ACTIONS.DETAILS' | translate }}"
(click)="toggleSidebar()">
<mat-icon>info_outline</mat-icon>
</button>
<button
color="primary"
mat-icon-button
*ngIf="hasSelection(documentList.selection)"
title="{{ 'APP.ACTIONS.MORE' | translate }}"
[matMenuTriggerFor]="actionsMenu">
<mat-icon>more_vert</mat-icon>
</button>
<mat-menu #actionsMenu="matMenu"
[overlapTrigger]="false"
class="secondary-options">
@@ -35,7 +47,10 @@
mat-menu-item
#selection="adfFavorite"
[adf-node-favorite]="documentList.selection">
<mat-icon [ngClass]="{ 'icon-highlight': selection.hasFavorites() }">
<mat-icon [ngClass]="{
'toolbar__option--active': selection.hasFavorites(),
'toolbar__option--default': !selection.hasFavorites()
}">
{{ selection.hasFavorites() ? 'star' :'star_border' }}
</mat-icon>
<span>{{ 'APP.ACTIONS.FAVORITE' | translate }}</span>
@@ -44,7 +59,7 @@
<button
mat-menu-item
[app-copy-node]="documentList.selection">
<mat-icon>content_copy</mat-icon>
<mat-icon color="primary">content_copy</mat-icon>
<span>{{ 'APP.ACTIONS.COPY' | translate }}</span>
</button>
@@ -52,7 +67,7 @@
mat-menu-item
*ngIf="canMove(documentList.selection)"
[app-move-node]="documentList.selection">
<mat-icon>library_books</mat-icon>
<mat-icon color="primary">library_books</mat-icon>
<span>{{ 'APP.ACTIONS.MOVE' | translate }}</span>
</button>
@@ -60,82 +75,132 @@
mat-menu-item
*ngIf="canDelete(documentList.selection)"
[app-delete-node]="documentList.selection">
<mat-icon>delete</mat-icon>
<mat-icon color="primary">delete</mat-icon>
<span>{{ 'APP.ACTIONS.DELETE' | translate }}</span>
</button>
<button
mat-menu-item
*ngIf="canManageVersions(documentList.selection)"
[app-node-versions]="documentList.selection">
<mat-icon color="primary">history</mat-icon>
<span>{{ 'APP.ACTIONS.VERSIONS' | translate }}</span>
</button>
</mat-menu>
</adf-toolbar>
</div>
<div class="inner-layout__content">
<div class="inner-layout__panel" [ngClass]="{'adf-document-list--loading': documentList.isEmpty()}">
<adf-document-list #documentList
[attr.class]="documentList.isEmpty() ? 'adf-document-list--empty' : ''"
currentFolderId="-recent-"
selectionMode="multiple"
[navigate]="false"
[sorting]="sorting"
[contextMenuActions]="true"
[contentActions]="false"
[imageResolver]="imageResolver"
(sorting-changed)="onSortingChanged($event)"
(node-dblclick)="onNodeDoubleClick($event.detail?.node?.entry)"
(node-select)="onNodeSelect($event, documentList)">
<adf-document-list #documentList
[attr.class]="documentList.isEmpty() ? 'empty-list' : ''"
currentFolderId="-recent-"
selectionMode="multiple"
[navigate]="false"
[sorting]="sorting"
[contextMenuActions]="true"
[contentActions]="false"
[imageResolver]="imageResolver"
(sorting-changed)="onSortingChanged($event)"
(node-dblclick)="onNodeDoubleClick($event.detail?.node?.entry)"
(node-select)="onNodeSelect($event, documentList)">
<empty-folder-content>
<ng-template>
<app-empty-folder
icon="access_time"
title="APP.BROWSE.RECENT.EMPTY_STATE.TITLE"
subtitle="APP.BROWSE.RECENT.EMPTY_STATE.TEXT">
</app-empty-folder>
</ng-template>
</empty-folder-content>
<data-columns>
<data-column
key="$thumbnail"
type="image"
[sortable]="false"
class="image-table-cell">
</data-column>
<data-column
class="adf-data-table-cell--ellipsis__name"
key="name"
title="APP.DOCUMENT_LIST.COLUMNS.NAME">
<ng-template let-value="value" let-context>
<span class="adf-datatable-cell" title="{{ context?.row?.obj | adfNodeNameTooltip }}">{{ value }}</span>
<empty-folder-content>
<ng-template>
<app-empty-folder
icon="access_time"
title="APP.BROWSE.RECENT.EMPTY_STATE.TITLE"
subtitle="APP.BROWSE.RECENT.EMPTY_STATE.TEXT">
</app-empty-folder>
</ng-template>
</data-column>
</empty-folder-content>
<data-column
key="path"
title="APP.DOCUMENT_LIST.COLUMNS.LOCATION">
<ng-template let-context>
<app-location-link [context]="context"></app-location-link>
<data-columns>
<data-column
key="$thumbnail"
type="image"
[sortable]="false"
class="image-table-cell">
</data-column>
<data-column
class="adf-data-table-cell--ellipsis__name"
key="name"
title="APP.DOCUMENT_LIST.COLUMNS.NAME">
<ng-template let-value="value" let-context>
<span class="adf-datatable-cell" title="{{ context?.row?.obj | adfNodeNameTooltip }}">{{ value }}</span>
</ng-template>
</data-column>
<data-column
key="path"
title="APP.DOCUMENT_LIST.COLUMNS.LOCATION">
<ng-template let-context>
<app-location-link [context]="context"></app-location-link>
</ng-template>
</data-column>
<data-column
key="content.sizeInBytes"
type="fileSize"
title="APP.DOCUMENT_LIST.COLUMNS.SIZE">
</data-column>
<data-column
key="modifiedAt"
title="APP.DOCUMENT_LIST.COLUMNS.MODIFIED_ON">
<ng-template let-value="value">
<span title="{{ value | date:'medium' }}">{{ value | adfTimeAgo }}</span>
</ng-template>
</data-column>
</data-columns>
</adf-document-list>
<ng-container *ngIf="!documentList.isEmpty()">
<adf-pagination
[supportedPageSizes]="'pagination.supportedPageSizes' | appConfig"
[target]="documentList"
(changePageSize)="onChangePageSize($event)">
</adf-pagination>
</ng-container>
</div>
<div class="inner-layout__side-panel"
*ngIf="infoDrawerOpened"
[app-node-info]="documentList.selection"
(changed)="toggleSidebar($event)"
#infoInstance=nodeInfo>
<adf-info-drawer [title]="'APP.INFO_DRAWER.TITLE' | translate">
<adf-info-drawer-tab [label]="'APP.INFO_DRAWER.TABS.PROPERTIES' | translate">
<div *ngIf="infoInstance.loading">
<mat-progress-bar mode="indeterminate"></mat-progress-bar>
</div>
<adf-content-metadata-card
[readOnly]="!canUpdate(documentList.selection)"
[displayEmpty]="canUpdate(documentList.selection)"
[preset]="'custom'"
[node]="infoInstance.node">
</adf-content-metadata-card>
</adf-info-drawer-tab>
<adf-info-drawer-tab [label]="'APP.INFO_DRAWER.TABS.VERSIONS' | translate">
<ng-container *ngIf="isFileSelected(documentList.selection);else choose_document_template">
<adf-version-manager
*ngIf="infoInstance.node"
[node]="infoInstance.node">
</adf-version-manager>
</ng-container>
<ng-template #choose_document_template>
<div class="adf-manage-versions-empty">
<mat-icon class="adf-manage-versions-empty-icon">face</mat-icon>
{{ 'VERSION.SELECTION.EMPTY' | translate }}
</div>
</ng-template>
</data-column>
<data-column
key="content.sizeInBytes"
type="fileSize"
title="APP.DOCUMENT_LIST.COLUMNS.SIZE">
</data-column>
<data-column
key="modifiedAt"
title="APP.DOCUMENT_LIST.COLUMNS.MODIFIED_ON">
<ng-template let-value="value">
<span title="{{ value | date:'medium' }}">{{ value | adfTimeAgo }}</span>
</ng-template>
</data-column>
</data-columns>
</adf-document-list>
<adf-pagination [ngClass]="{'no-border' : documentList.isEmpty()}"
[target]="documentList"
(changePageSize)="onChangePageSize($event)">
</adf-pagination>
</adf-info-drawer-tab>
</adf-info-drawer>
</div>
</div>
</div>
@@ -35,12 +35,14 @@ import {
StorageService, CookieService, ThumbnailService, AuthenticationService,
TimeAgoPipe, NodeNameTooltipPipe, NodeFavoriteDirective, DataTableComponent
} from '@alfresco/adf-core';
import { DocumentListComponent } from '@alfresco/adf-content-services';
import { DocumentListComponent, CustomResourcesService } from '@alfresco/adf-content-services';
import { TranslateModule } from '@ngx-translate/core';
import { NoopAnimationsModule } from '@angular/platform-browser/animations';
import { MatMenuModule, MatSnackBarModule, MatIconModule } from '@angular/material';
import { DocumentListService } from '@alfresco/adf-content-services';
import { ContentManagementService } from '../../common/services/content-management.service';
import { NodeInfoDirective } from '../../common/directives/node-info.directive';
import { AppConfigPipe } from '../../common/pipes/app-config.pipe';
import { RecentFilesComponent } from './recent-files.component';
@@ -52,7 +54,6 @@ describe('RecentFiles Routed Component', () => {
let contentService: ContentManagementService;
let preferenceService: UserPreferencesService;
let page;
let person;
beforeEach(() => {
page = {
@@ -61,8 +62,6 @@ describe('RecentFiles Routed Component', () => {
pagination: { data: 'data'}
}
};
person = { entry: { id: 'bogus' } };
});
beforeEach(async(() => {
@@ -80,8 +79,10 @@ describe('RecentFiles Routed Component', () => {
TimeAgoPipe,
NodeNameTooltipPipe,
NodeFavoriteDirective,
NodeInfoDirective,
DocumentListComponent,
RecentFilesComponent
RecentFilesComponent,
AppConfigPipe
],
providers: [
{ provide: ActivatedRoute, useValue: {
@@ -98,7 +99,8 @@ describe('RecentFiles Routed Component', () => {
ContentService,
NodesApiService,
DocumentListService,
ThumbnailService
ThumbnailService,
CustomResourcesService
],
schemas: [ NO_ERRORS_SCHEMA ]
})
@@ -110,6 +112,7 @@ describe('RecentFiles Routed Component', () => {
contentService = TestBed.get(ContentManagementService);
preferenceService = TestBed.get(UserPreferencesService);
alfrescoApi = TestBed.get(AlfrescoApiService);
alfrescoApi.reset();
});
}));
@@ -0,0 +1,5 @@
<adf-search-control
[highlight]="true"
(optionClicked)="onItemClicked($event)"
(submit)="onSearchSubmit($event)">
</adf-search-control>
@@ -0,0 +1,10 @@
@import 'variables';
// todo: remove once ADF 2.0 is out
:host {
overflow: hidden;
}
adf-search-control {
color: $alfresco-white;
}
@@ -0,0 +1,78 @@
/*!
* @license
* Alfresco Example Content Application
*
* Copyright (C) 2005 - 2018 Alfresco Software Limited
*
* This file is part of the Alfresco Example Content Application.
* If the software was purchased under a paid Alfresco license, the terms of
* the paid license agreement will prevail. Otherwise, the software is
* provided under the following open source license terms:
*
* The Alfresco Example Content Application is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* The Alfresco Example Content Application is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with Alfresco. If not, see <http://www.gnu.org/licenses/>.
*/
import { NO_ERRORS_SCHEMA } from '@angular/core';
import { TestBed, async } from '@angular/core/testing';
import { Router } from '@angular/router';
import { RouterTestingModule } from '@angular/router/testing';
import { SearchInputComponent } from './search-input.component';
describe('SearchInputComponent', () => {
let fixture;
let component;
let router: Router;
beforeEach(async(() => {
TestBed.configureTestingModule({
imports: [
RouterTestingModule
],
declarations: [
SearchInputComponent
],
schemas: [ NO_ERRORS_SCHEMA ]
})
.compileComponents()
.then(() => {
fixture = TestBed.createComponent(SearchInputComponent);
component = fixture.componentInstance;
router = TestBed.get(Router);
fixture.detectChanges();
});
}));
describe('onItemClicked()', () => {
it('opens preview if node is file', () => {
spyOn(router, 'navigate').and.stub();
const node = { entry: { isFile: true, id: 'node-id', parentId: 'parent-id' } };
component.onItemClicked(node);
expect(router.navigate['calls'].argsFor(0)[0])
.toEqual([`/personal-files/${node.entry.parentId}/preview/`, node.entry.id]);
});
it('navigates if node is folder', () => {
const node = { entry: { isFolder: true } };
spyOn(router, 'navigate');
component.onItemClicked(node);
expect(router.navigate).toHaveBeenCalled();
});
});
});
@@ -23,27 +23,40 @@
* along with Alfresco. If not, see <http://www.gnu.org/licenses/>.
*/
import { NgModule } from '@angular/core';
import {
MatMenuModule,
MatIconModule,
MatButtonModule,
MatDialogModule,
MatInputModule
} from '@angular/material';
import { Component } from '@angular/core';
import { Router } from '@angular/router';
import { MinimalNodeEntity } from 'alfresco-js-api';
export function modules() {
return [
MatMenuModule,
MatIconModule,
MatButtonModule,
MatDialogModule,
MatInputModule
];
}
@NgModule({
imports: modules(),
exports: modules()
@Component({
selector: 'app-search-input',
templateUrl: 'search-input.component.html',
styleUrls: ['search-input.component.scss']
})
export class MaterialModule {}
export class SearchInputComponent {
constructor(
private router: Router) {
}
onItemClicked(node: MinimalNodeEntity) {
if (node && node.entry) {
if (node.entry.isFile) {
this.router.navigate([`/personal-files/${node.entry.parentId}/preview/`, node.entry.id]);
} else if (node.entry.isFolder) {
this.router.navigate([ '/personal-files', node.entry.id ]);
}
}
}
/**
* Called when the user submits the search, e.g. hits enter or clicks submit
*
* @param event Parameters relating to the search
*/
onSearchSubmit(event: KeyboardEvent) {
const value = (event.target as HTMLInputElement).value;
this.router.navigate(['/search', {
q: value
}]);
}
}
@@ -1,4 +1,32 @@
<adf-search-control
[highlight]="true"
(optionClicked)="onItemClicked($event)">
</adf-search-control>
<adf-search
#search
[searchTerm]="searchedWord"
[maxResults]="maxItems"
[skipResults]="skipCount"
(resultLoaded)="onSearchResultLoaded($event)">
</adf-search>
<div class="adf-search-results__facets">
<adf-search-chip-list
[searchFilter]="searchFilter">
</adf-search-chip-list>
</div>
<div class="adf-search-results">
<adf-search-filter
#searchFilter>
</adf-search-filter>
<div class="adf-search-results__content">
<adf-document-list
#documentList
[attr.class]="documentList.isEmpty() ? 'adf-document-list--empty' : ''"
[node]="data">
</adf-document-list>
<adf-pagination *ngIf="!documentList.isEmpty()"
[target]="documentList"
(changePageSize)="onRefreshPagination($event)">
</adf-pagination>
</div>
</div>
@@ -1,10 +1,26 @@
@import 'variables';
@import 'mixins';
// todo: remove once ADF 2.0 is out
:host {
overflow: hidden;
}
.adf-search-results {
@include flex-row;
adf-search-control {
color: $alfresco-white;
&__facets {
display: flex;
height: 35px;
flex-direction: column;
justify-content: center;
padding: 5px;
border-bottom: 1px solid #eee;
}
&__content {
@include flex-column;
border-left: 1px solid #eee;
}
.adf-search-filter {
min-width: 260px;
padding: 5px;
height: 100%;
overflow: scroll;
}
}
@@ -1,78 +1,25 @@
/*!
* @license
* Alfresco Example Content Application
*
* Copyright (C) 2005 - 2018 Alfresco Software Limited
*
* This file is part of the Alfresco Example Content Application.
* If the software was purchased under a paid Alfresco license, the terms of
* the paid license agreement will prevail. Otherwise, the software is
* provided under the following open source license terms:
*
* The Alfresco Example Content Application is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* The Alfresco Example Content Application is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with Alfresco. If not, see <http://www.gnu.org/licenses/>.
*/
import { NO_ERRORS_SCHEMA } from '@angular/core';
import { TestBed, async } from '@angular/core/testing';
import { Router } from '@angular/router';
import { RouterTestingModule } from '@angular/router/testing';
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { SearchComponent } from './search.component';
describe('SearchComponent', () => {
let fixture;
let component;
let router: Router;
let component: SearchComponent;
let fixture: ComponentFixture<SearchComponent>;
beforeEach(async(() => {
TestBed.configureTestingModule({
imports: [
RouterTestingModule
],
declarations: [
SearchComponent
],
schemas: [ NO_ERRORS_SCHEMA ]
})
.compileComponents()
.then(() => {
fixture = TestBed.createComponent(SearchComponent);
component = fixture.componentInstance;
router = TestBed.get(Router);
beforeEach(async(() => {
TestBed.configureTestingModule({
declarations: [ SearchComponent ]
})
.compileComponents();
}));
fixture.detectChanges();
});
}));
beforeEach(() => {
fixture = TestBed.createComponent(SearchComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
describe('onItemClicked()', () => {
it('opens preview if node is file', () => {
spyOn(router, 'navigate').and.stub();
const node = { entry: { isFile: true, id: 'node-id', parentId: 'parent-id' } };
component.onItemClicked(node);
expect(router.navigate['calls'].argsFor(0)[0])
.toEqual([`/personal-files/${node.entry.parentId}/preview/`, node.entry.id]);
});
it('navigates if node is folder', () => {
const node = { entry: { isFolder: true } };
spyOn(router, 'navigate');
component.onItemClicked(node);
expect(router.navigate).toHaveBeenCalled();
});
});
xit('should create', () => {
expect(component).toBeTruthy();
});
});
+46 -15
View File
@@ -23,28 +23,59 @@
* along with Alfresco. If not, see <http://www.gnu.org/licenses/>.
*/
import { Component } from '@angular/core';
import { Router } from '@angular/router';
import { MinimalNodeEntity } from 'alfresco-js-api';
import { Component, OnInit, Optional, ViewChild } from '@angular/core';
import { NodePaging, Pagination } from 'alfresco-js-api';
import { Router, ActivatedRoute, Params } from '@angular/router';
import { SearchQueryBuilderService, SearchComponent as AdfSearchComponent } from '@alfresco/adf-content-services';
@Component({
selector: 'app-search',
templateUrl: 'search.component.html',
styleUrls: ['search.component.scss']
selector: 'app-search',
templateUrl: './search.component.html',
styleUrls: ['./search.component.scss']
})
export class SearchComponent {
export class SearchComponent implements OnInit {
@ViewChild('search')
search: AdfSearchComponent;
queryParamName = 'q';
searchedWord = '';
data: NodePaging;
maxItems = 5;
skipCount = 0;
constructor(
private router: Router) {
public router: Router,
private queryBuilder: SearchQueryBuilderService,
@Optional() private route: ActivatedRoute) {
queryBuilder.paging = {
skipCount: 0,
maxItems: 25
};
}
onItemClicked(node: MinimalNodeEntity) {
if (node && node.entry) {
if (node.entry.isFile) {
this.router.navigate([`/personal-files/${node.entry.parentId}/preview/`, node.entry.id]);
} else if (node.entry.isFolder) {
this.router.navigate([ '/personal-files', node.entry.id ]);
}
ngOnInit() {
if (this.route) {
this.route.params.forEach((params: Params) => {
this.searchedWord = params.hasOwnProperty(this.queryParamName) ? params[this.queryParamName] : null;
this.queryBuilder.queryFragments['queryName'] = `cm:name:'${this.searchedWord}'`;
this.queryBuilder.update();
});
}
}
onSearchResultLoaded(nodePaging: NodePaging) {
this.data = nodePaging;
}
onRefreshPagination(pagination: Pagination) {
this.maxItems = pagination.maxItems;
this.skipCount = pagination.skipCount;
this.queryBuilder.paging = {
maxItems: pagination.maxItems,
skipCount: pagination.skipCount
};
this.queryBuilder.update();
}
}
@@ -4,8 +4,8 @@
</adf-breadcrumb>
<adf-toolbar class="inline">
<button
color="primary"
mat-icon-button
*ngIf="canPreviewFile(documentList.selection)"
title="{{ 'APP.ACTIONS.VIEW' | translate }}"
@@ -14,20 +14,31 @@
</button>
<button
color="primary"
mat-icon-button
*ngIf="hasSelection(documentList.selection)"
title="{{ 'APP.ACTIONS.DOWNLOAD' | translate }}"
[adfNodeDownload]="documentList.selection">
[appNodeDownload]="documentList.selection">
<mat-icon>get_app</mat-icon>
</button>
<button mat-icon-button
[color]="infoDrawerOpened ? 'accent' : 'primary'"
*ngIf="documentList.selection.length"
title="{{ 'APP.ACTIONS.DETAILS' | translate }}"
(click)="toggleSidebar()">
<mat-icon>info_outline</mat-icon>
</button>
<button
color="primary"
mat-icon-button
*ngIf="hasSelection(documentList.selection)"
title="{{ 'APP.ACTIONS.MORE' | translate }}"
[matMenuTriggerFor]="actionsMenu">
<mat-icon>more_vert</mat-icon>
</button>
<mat-menu #actionsMenu="matMenu"
[overlapTrigger]="false"
class="secondary-options">
@@ -35,7 +46,10 @@
mat-menu-item
#selection="adfFavorite"
[adf-node-favorite]="documentList.selection">
<mat-icon [ngClass]="{ 'icon-highlight': selection.hasFavorites() }">
<mat-icon [ngClass]="{
'toolbar__option--active': selection.hasFavorites(),
'toolbar__option--default': !selection.hasFavorites()
}">
{{ selection.hasFavorites() ? 'star' :'star_border' }}
</mat-icon>
<span>{{ 'APP.ACTIONS.FAVORITE' | translate }}</span>
@@ -44,115 +58,166 @@
<button
mat-menu-item
[app-copy-node]="documentList.selection">
<mat-icon>content_copy</mat-icon>
<mat-icon color="primary">content_copy</mat-icon>
<span>{{ 'APP.ACTIONS.COPY' | translate }}</span>
</button>
<button
mat-menu-item
*ngIf="canMoveShared(documentList.selection)"
*ngIf="permission.check(documentList.selection, ['delete'], { target: 'allowableOperationsOnTarget' })"
[app-move-node]="documentList.selection">
<mat-icon>library_books</mat-icon>
<mat-icon color="primary">library_books</mat-icon>
<span>{{ 'APP.ACTIONS.MOVE' | translate }}</span>
</button>
<button
mat-menu-item
*ngIf="canDelete(documentList.selection)"
*ngIf="permission.check(documentList.selection, ['delete'])"
[appUnshareNode]="documentList.selection"
(links-unshared)="refresh()">
<mat-icon>stop_screen_share</mat-icon>
<mat-icon color="primary">stop_screen_share</mat-icon>
<span>{{ 'APP.ACTIONS.UNSHARE' | translate }}</span>
</button>
<button
mat-menu-item
*ngIf="canDeleteShared(documentList.selection)"
*ngIf="permission.check(documentList.selection, ['delete'], { target: 'allowableOperationsOnTarget' })"
[app-delete-node]="documentList.selection">
<mat-icon>delete</mat-icon>
<mat-icon color="primary">delete</mat-icon>
<span>{{ 'APP.ACTIONS.DELETE' | translate }}</span>
</button>
<button
mat-menu-item
*ngIf="permission.check(documentList.selection[0], ['update'], { target: 'allowableOperationsOnTarget' })"
[app-node-versions]="documentList.selection">
<mat-icon color="primary">history</mat-icon>
<span>{{ 'APP.ACTIONS.VERSIONS' | translate }}</span>
</button>
</mat-menu>
</adf-toolbar>
</div>
<div class="inner-layout__content">
<adf-document-list #documentList
[attr.class]="documentList.isEmpty() ? 'empty-list' : ''"
currentFolderId="-sharedlinks-"
selectionMode="multiple"
[sorting]="sorting"
[contextMenuActions]="true"
[contentActions]="false"
(sorting-changed)="onSortingChanged($event)"
(node-dblclick)="onNodeDoubleClick($event.detail?.node?.entry)">
<div class="inner-layout__panel" [ngClass]="{'adf-document-list--loading': documentList.isEmpty()}">
<adf-document-list #documentList
[attr.class]="documentList.isEmpty() ? 'adf-document-list--empty' : ''"
currentFolderId="-sharedlinks-"
selectionMode="multiple"
[sorting]="sorting"
[contextMenuActions]="true"
[contentActions]="false"
(sorting-changed)="onSortingChanged($event)"
(node-dblclick)="onNodeDoubleClick($event.detail?.node?.entry)">
<empty-folder-content>
<ng-template>
<app-empty-folder
icon="people"
title="APP.BROWSE.SHARED.EMPTY_STATE.TITLE"
subtitle="APP.BROWSE.SHARED.EMPTY_STATE.TEXT">
</app-empty-folder>
</ng-template>
</empty-folder-content>
<data-columns>
<data-column
key="$thumbnail"
type="image"
[sortable]="false"
class="image-table-cell">
</data-column>
<data-column
class="adf-data-table-cell--ellipsis__name"
key="name"
title="APP.DOCUMENT_LIST.COLUMNS.NAME">
<ng-template let-value="value" let-context>
<span class="adf-datatable-cell" title="{{ context?.row?.obj | adfNodeNameTooltip }}">{{ value }}</span>
<empty-folder-content>
<ng-template>
<app-empty-folder
icon="people"
title="APP.BROWSE.SHARED.EMPTY_STATE.TITLE"
subtitle="APP.BROWSE.SHARED.EMPTY_STATE.TEXT">
</app-empty-folder>
</ng-template>
</data-column>
</empty-folder-content>
<data-column
key="path"
title="APP.DOCUMENT_LIST.COLUMNS.LOCATION">
<ng-template let-context>
<app-location-link [context]="context"></app-location-link>
<data-columns>
<data-column
key="$thumbnail"
type="image"
[sortable]="false"
class="image-table-cell">
</data-column>
<data-column
class="adf-data-table-cell--ellipsis__name"
key="name"
title="APP.DOCUMENT_LIST.COLUMNS.NAME">
<ng-template let-value="value" let-context>
<span class="adf-datatable-cell" title="{{ context?.row?.obj | adfNodeNameTooltip }}">{{ value }}</span>
</ng-template>
</data-column>
<data-column
key="path"
title="APP.DOCUMENT_LIST.COLUMNS.LOCATION">
<ng-template let-context>
<app-location-link [context]="context"></app-location-link>
</ng-template>
</data-column>
<data-column
key="content.sizeInBytes"
title="APP.DOCUMENT_LIST.COLUMNS.SIZE"
type="fileSize">
</data-column>
<data-column
key="modifiedAt"
title="APP.DOCUMENT_LIST.COLUMNS.MODIFIED_ON">
<ng-template let-value="value">
<span title="{{ value | date:'medium' }}">{{ value | adfTimeAgo }}</span>
</ng-template>
</data-column>
<data-column
class="adf-data-table-cell--ellipsis"
key="modifiedByUser.displayName"
title="APP.DOCUMENT_LIST.COLUMNS.MODIFIED_BY">
</data-column>
<data-column
class="adf-data-table-cell--ellipsis"
key="sharedByUser.displayName"
title="APP.DOCUMENT_LIST.COLUMNS.SHARED_BY">
</data-column>
</data-columns>
</adf-document-list>
<ng-container *ngIf="!documentList.isEmpty()">
<adf-pagination
[supportedPageSizes]="'pagination.supportedPageSizes' | appConfig"
[target]="documentList"
(changePageSize)="onChangePageSize($event)">
</adf-pagination>
</ng-container>
</div>
<div class="inner-layout__side-panel"
*ngIf="infoDrawerOpened"
[app-node-info]="documentList.selection"
(changed)="toggleSidebar($event)"
#infoInstance=nodeInfo>
<adf-info-drawer [title]="'APP.INFO_DRAWER.TITLE' | translate">
<adf-info-drawer-tab [label]="'APP.INFO_DRAWER.TABS.PROPERTIES' | translate">
<div *ngIf="infoInstance.loading">
<mat-progress-bar mode="indeterminate"></mat-progress-bar>
</div>
<adf-content-metadata-card
[readOnly]="!permission.check(documentList.selection, ['update'], { target: 'allowableOperationsOnTarget' })"
[displayEmpty]="permission.check(documentList.selection, ['update'], { target: 'allowableOperationsOnTarget' })"
[preset]="'custom'"
[node]="infoInstance.node">
</adf-content-metadata-card>
</adf-info-drawer-tab>
<adf-info-drawer-tab [label]="'APP.INFO_DRAWER.TABS.VERSIONS' | translate">
<ng-container *ngIf="isFileSelected(documentList.selection);else choose_document_template">
<adf-version-manager
*ngIf="infoInstance.node"
[node]="infoInstance.node">
</adf-version-manager>
</ng-container>
<ng-template #choose_document_template>
<div class="adf-manage-versions-empty">
<mat-icon class="adf-manage-versions-empty-icon">face</mat-icon>
{{ 'VERSION.SELECTION.EMPTY' | translate }}
</div>
</ng-template>
</data-column>
<data-column
key="content.sizeInBytes"
title="APP.DOCUMENT_LIST.COLUMNS.SIZE"
type="fileSize">
</data-column>
<data-column
key="modifiedAt"
title="APP.DOCUMENT_LIST.COLUMNS.MODIFIED_ON">
<ng-template let-value="value">
<span title="{{ value | date:'medium' }}">{{ value | adfTimeAgo }}</span>
</ng-template>
</data-column>
<data-column
class="adf-data-table-cell--ellipsis"
key="modifiedByUser.displayName"
title="APP.DOCUMENT_LIST.COLUMNS.MODIFIED_BY">
</data-column>
<data-column
class="adf-data-table-cell--ellipsis"
key="sharedByUser.displayName"
title="APP.DOCUMENT_LIST.COLUMNS.SHARED_BY">
</data-column>
</data-columns>
</adf-document-list>
<adf-pagination [ngClass]="{ 'no-border' :documentList.isEmpty()}"
[target]="documentList"
(changePageSize)="onChangePageSize($event)">
</adf-pagination>
</adf-info-drawer-tab>
</adf-info-drawer>
</div>
</div>
</div>
@@ -35,12 +35,15 @@ import {
StorageService, CookieService, ThumbnailService, AuthenticationService,
TimeAgoPipe, NodeNameTooltipPipe, NodeFavoriteDirective,DataTableComponent
} from '@alfresco/adf-core';
import { DocumentListComponent } from '@alfresco/adf-content-services';
import { DocumentListComponent, CustomResourcesService } from '@alfresco/adf-content-services';
import { TranslateModule } from '@ngx-translate/core';
import { NoopAnimationsModule } from '@angular/platform-browser/animations';
import { MatMenuModule, MatSnackBarModule, MatIconModule } from '@angular/material';
import { DocumentListService } from '@alfresco/adf-content-services';
import { ContentManagementService } from '../../common/services/content-management.service';
import { NodeInfoDirective } from '../../common/directives/node-info.directive';
import { NodePermissionService } from '../../common/services/node-permission.service';
import { AppConfigPipe } from '../../common/pipes/app-config.pipe';
import { SharedFilesComponent } from './shared-files.component';
@@ -79,8 +82,10 @@ describe('SharedFilesComponent', () => {
TimeAgoPipe,
NodeNameTooltipPipe,
NodeFavoriteDirective,
NodeInfoDirective,
DocumentListComponent,
SharedFilesComponent
SharedFilesComponent,
AppConfigPipe
],
providers: [
{ provide: ActivatedRoute, useValue: {
@@ -94,10 +99,12 @@ describe('SharedFilesComponent', () => {
LogService,
NotificationService,
ContentManagementService,
NodePermissionService,
ContentService,
NodesApiService,
DocumentListService,
ThumbnailService
ThumbnailService,
CustomResourcesService
],
schemas: [ NO_ERRORS_SCHEMA ]
})
@@ -108,6 +115,7 @@ describe('SharedFilesComponent', () => {
contentService = TestBed.get(ContentManagementService);
alfrescoApi = TestBed.get(AlfrescoApiService);
alfrescoApi.reset();
nodeService = alfrescoApi.getInstance().nodes;
preferenceService = TestBed.get(UserPreferencesService);
router = TestBed.get(Router);
@@ -31,6 +31,7 @@ import { AlfrescoApiService, UserPreferencesService } from '@alfresco/adf-core';
import { DocumentListComponent } from '@alfresco/adf-content-services';
import { ContentManagementService } from '../../common/services/content-management.service';
import { NodePermissionService } from '../../common/services/node-permission.service';
import { PageComponent } from '../page.component';
@Component({
@@ -49,6 +50,7 @@ export class SharedFilesComponent extends PageComponent implements OnInit, OnDes
private route: ActivatedRoute,
private content: ContentManagementService,
private apiService: AlfrescoApiService,
public permission: NodePermissionService,
preferences: UserPreferencesService) {
super(preferences);
@@ -1,32 +1,45 @@
<div class="sidenav">
<div class="sidenav__section sidenav__section--new">
<button class="sidenav__section--new__button" mat-button [matMenuTriggerFor]="menu">
<div class="sidenav__section sidenav__section--new" [ngClass]="{ 'section--new--mini': !showLabel }">
<button *ngIf="showLabel"
[matMenuTriggerFor]="menu"
class="sidenav__section--new__button"
[disableRipple]="true"
mat-raised-button
color="accent">
<span>{{ 'APP.NEW_MENU.LABEL' | translate }}</span>
<mat-icon>arrow_drop_down</mat-icon>
</button>
<button [matMenuTriggerFor]="menu"
*ngIf="!showLabel"
color="accent"
mat-icon-button>
<mat-icon>add_box</mat-icon>
</button>
<mat-menu #menu="matMenu" [overlapTrigger]="false">
<button
mat-menu-item
[disabled]="!canCreateContent(node)"
[disabled]="!permission.check(node, ['create'])"
(error)="openSnackMessage($event)"
[adf-create-folder]="node?.id"
title="{{
( canCreateContent(node)
[title]="
( permission.check(node, ['create'])
? 'APP.NEW_MENU.TOOLTIPS.CREATE_FOLDER'
: 'APP.NEW_MENU.TOOLTIPS.CREATE_FOLDER_NOT_ALLOWED'
) | translate
}}">
) | translate">
<mat-icon>create_new_folder</mat-icon>
<span>{{ 'APP.NEW_MENU.MENU_ITEMS.CREATE_FOLDER' | translate }}</span>
</button>
<adf-upload-button
tooltip="{{
(canCreateContent(node)
[tooltip]="
(permission.check(node, ['create'])
? 'APP.NEW_MENU.TOOLTIPS.UPLOAD_FILES'
: 'APP.NEW_MENU.TOOLTIPS.UPLOAD_FILES_NOT_ALLOWED'
) | translate }}"
[disabled]="!canCreateContent(node)"
) | translate"
[disabled]="!permission.check(node, ['create'])"
[rootFolderId]="node?.id"
[multipleFiles]="true"
[uploadFolders]="false"
@@ -34,12 +47,12 @@
</adf-upload-button>
<adf-upload-button
tooltip="{{
(canCreateContent(node)
[tooltip]="
(permission.check(node, ['create'])
? 'APP.NEW_MENU.TOOLTIPS.UPLOAD_FOLDERS'
: 'APP.NEW_MENU.TOOLTIPS.UPLOAD_FOLDERS_NOT_ALLOWED'
) | translate }}"
[disabled]="!canCreateContent(node)"
) | translate"
[disabled]="!permission.check(node, ['create'])"
[rootFolderId]="node?.id"
[multipleFiles]="true"
[uploadFolders]="true"
@@ -48,20 +61,33 @@
</mat-menu>
</div>
<div class="sidenav__section" *ngFor="let list of navigation">
<div class="sidenav__section sidenav__section--menu" *ngFor="let list of navigation">
<ul class="sidenav-menu">
<li class="sidenav-menu__item" *ngFor="let item of list">
<a
class="sidenav-menu__item-link"
[ngClass]="{ 'sidenav-menu__item-link--noicon' : !item.icon }"
routerLinkActive="sidenav-menu__item-link--active"
[routerLink]="item.disabled? null : item.route.url"
[ngClass]="{ 'disabled': item.disabled }"
title="{{ item.title || '' | translate }}">
<mat-icon *ngIf="item.icon">{{ item.icon }}</mat-icon>
{{ item.label | translate }}
</a>
<li *ngFor="let item of list" class="sidenav-menu__item"
routerLinkActive
#rla="routerLinkActive"
title="{{ item.title || '' | translate }}">
<button [routerLink]="item.route.url"
[color]="rla.isActive ? 'accent': 'primary'"
mat-icon-button
mat-ripple
matRippleColor="primary"
[matRippleTrigger]="rippleTrigger"
[matRippleCentered]="true">
<mat-icon>{{ item.icon }}</mat-icon>
</button>
<span #rippleTrigger
class="menu__item--label"
[routerLink]="item.route.url"
[hidden]="!showLabel"
[ngClass]="{
'menu__item--active': rla.isActive,
'menu__item--default': !rla.isActive
}">{{ item.label | translate }}</span>
</li>
</ul>
</div>
</div>
</div>
@@ -1,85 +1,62 @@
@import 'variables';
.sidenav {
display: flex;
flex: 1;
flex-direction: column;
height: 100%;
$sidenav-section--h-padding: 24px;
$sidenav-section--v-padding: 8px;
&__section:last-child {
border-bottom: 0;
}
$sidenav-menu-item--h-padding: 24px;
$sidenav-menu-item--v-padding: 12px;
$sidenav-menu-item--icon-size: 24px;
:host {
.sidenav {
.section--new--mini {
display: flex;
flex: 1;
flex-direction: column;
justify-content: center;
align-items: center;
}
&__section:last-child {
border-bottom: 0;
&__section {
padding: 8px 14px;
position: relative;
&--new {
padding: 16px 24px;
height: 40px;
}
&__section {
padding:
$sidenav-section--v-padding
$sidenav-section--h-padding;
border-bottom: 1px solid $alfresco-divider-color;
position: relative;
&--new {
padding-top: 2 * $sidenav-section--v-padding;
padding-bottom: 2 * $sidenav-section--v-padding;
}
&--new__button {
width: 100%;
color: $alfresco-white;
background-color: $alfresco-primary-accent--default;
}
&--new__button {
width: 100%;
}
&-menu {
margin: 0 -1 * $sidenav-section--h-padding;
padding: 0;
list-style-type: none;
&__item {
padding: $sidenav-menu-item--v-padding 0;
&-link {
padding-left: $sidenav-menu-item--h-padding + 16px + 24px;
position: relative;
display: block;
color: $alfresco-secondary-text-color;
text-decoration: none;
& > .material-icons {
position: absolute;
top: 50%;
left: $sidenav-menu-item--h-padding;
margin-top: -14px;
}
&--active {
color: $alfresco-primary-accent--default;
}
&:not(&--active):hover {
color: $alfresco-primary-text-color;
}
&.disabled {
cursor: default !important;
color: $alfresco-secondary-text-color !important;
opacity: .25;
}
&--noicon {
padding-left: 26px;
}
}
}
&--new__button.mat-raised-button {
box-shadow: none !important;
}
}
&-menu {
display: inline-flex;
flex-direction: column;
padding: 0;
margin: 0;
list-style-type: none;
}
&-menu__item {
padding: 12px 0;
flex-direction: row;
display: flex;
align-items: center;
text-decoration: none;
text-decoration: none;
height: 24px;
}
.menu__item--label {
cursor: pointer;
width: 240px;
padding-left: 10px;
}
.menu__item--label:focus {
outline: none;
}
}
@@ -27,23 +27,24 @@ import { NO_ERRORS_SCHEMA } from '@angular/core';
import { TestBed, async } from '@angular/core/testing';
import { RouterTestingModule } from '@angular/router/testing';
import { TranslateModule } from '@ngx-translate/core';
import { MatMenuModule } from '@angular/material';
import { MatMenuModule, MatSnackBarModule } from '@angular/material';
import { HttpClientModule } from '@angular/common/http';
import {
ContentService, AppConfigService, AuthenticationService,
AppConfigService, AuthenticationService,
UserPreferencesService, StorageService, AlfrescoApiService,
CookieService, LogService
CookieService, LogService, NotificationService
} from '@alfresco/adf-core';
import { BrowsingFilesService } from '../../common/services/browsing-files.service';
import { NodePermissionService } from '../../common/services/node-permission.service';
import { SidenavComponent } from './sidenav.component';
describe('SidenavComponent', () => {
let fixture;
let component: SidenavComponent;
let contentService: ContentService;
let browsingService: BrowsingFilesService;
let appConfig: AppConfigService;
let notificationService: NotificationService;
let appConfigSpy;
const navItem = {
@@ -58,6 +59,7 @@ describe('SidenavComponent', () => {
imports: [
HttpClientModule,
MatMenuModule,
MatSnackBarModule,
TranslateModule.forRoot(),
RouterTestingModule
],
@@ -71,17 +73,18 @@ describe('SidenavComponent', () => {
StorageService,
UserPreferencesService,
AuthenticationService,
ContentService,
NodePermissionService,
AppConfigService,
BrowsingFilesService
BrowsingFilesService,
NotificationService
],
schemas: [ NO_ERRORS_SCHEMA ]
})
.compileComponents()
.then(() => {
contentService = TestBed.get(ContentService);
browsingService = TestBed.get(BrowsingFilesService);
appConfig = TestBed.get(AppConfigService);
notificationService = TestBed.get(NotificationService);
fixture = TestBed.createComponent(SidenavComponent);
component = fixture.componentInstance;
@@ -99,32 +102,6 @@ describe('SidenavComponent', () => {
expect(component.node).toBe(node);
});
it('should have permission to create content', () => {
fixture.detectChanges();
spyOn(contentService, 'hasPermission').and.returnValue(true);
const node: any = {};
expect(component.canCreateContent(node)).toBe(true);
expect(contentService.hasPermission).toHaveBeenCalledWith(node, 'create');
});
it('should not have permission to create content for missing node', () => {
fixture.detectChanges();
spyOn(contentService, 'hasPermission').and.returnValue(true);
expect(component.canCreateContent(null)).toBe(false);
expect(contentService.hasPermission).not.toHaveBeenCalled();
});
it('should not have permission to create content based on node permission', () => {
fixture.detectChanges();
spyOn(contentService, 'hasPermission').and.returnValue(false);
const node: any = {};
expect(component.canCreateContent(node)).toBe(false);
expect(contentService.hasPermission).toHaveBeenCalledWith(node, 'create');
});
describe('menu', () => {
it('should build menu from array', () => {
appConfigSpy.and.returnValue([navItem, navItem]);
@@ -140,4 +117,16 @@ describe('SidenavComponent', () => {
expect(component.navigation).toEqual([[navItem, navItem], [navItem, navItem]]);
});
});
describe('openSnackMessage', () => {
it('should call notification service', () => {
const message = 'notification message';
spyOn(notificationService, 'openSnackMessage');
component.openSnackMessage(message);
expect(notificationService.openSnackMessage).toHaveBeenCalledWith(message, 4000);
});
});
});
@@ -0,0 +1,32 @@
@mixin sidenav-component-theme($theme) {
$primary: map-get($theme, primary);
$accent: map-get($theme, accent);
$foreground: map-get($theme, foreground);
$background: map-get($theme, background);
$border: 1px solid mat-color($foreground, divider, .07);
.sidenav {
@include angular-material-theme($theme);
background-color: mat-color($background, background);
border-right: $border;
&__section {
border-bottom: $border;
}
.menu__item--label:not(.menu__item--active):hover {
color: mat-color($foreground, text);
}
.menu__item--active {
color: mat-color($accent);
}
.menu__item--default {
color: mat-color($primary, .87);
}
}
}
+16 -13
View File
@@ -24,13 +24,13 @@
*/
import { Subscription } from 'rxjs/Rx';
import { Component, OnInit, OnDestroy } from '@angular/core';
import { Component, Input, OnInit, OnDestroy } from '@angular/core';
import { MinimalNodeEntryEntity } from 'alfresco-js-api';
import { ContentService, AppConfigService } from '@alfresco/adf-core';
import { AppConfigService, NotificationService } from '@alfresco/adf-core';
import { BrowsingFilesService } from '../../common/services/browsing-files.service';
import { NodePermissionService } from '../../common/services/node-permission.service';
@Component({
selector: 'app-sidenav',
@@ -38,15 +38,18 @@ import { BrowsingFilesService } from '../../common/services/browsing-files.servi
styleUrls: ['./sidenav.component.scss']
})
export class SidenavComponent implements OnInit, OnDestroy {
@Input() showLabel: boolean;
node: MinimalNodeEntryEntity = null;
navigation = [];
private subscriptions: Subscription[] = [];
constructor(
private notificationService: NotificationService,
private browsingFilesService: BrowsingFilesService,
private contentService: ContentService,
private appConfig: AppConfigService
private appConfig: AppConfigService,
public permission: NodePermissionService
) {}
ngOnInit() {
@@ -58,15 +61,15 @@ export class SidenavComponent implements OnInit, OnDestroy {
]);
}
ngOnDestroy() {
this.subscriptions.forEach(s => s.unsubscribe());
openSnackMessage(event: any) {
this.notificationService.openSnackMessage(
event,
4000
);
}
canCreateContent(parentNode: MinimalNodeEntryEntity): boolean {
if (parentNode) {
return this.contentService.hasPermission(parentNode, 'create');
}
return false;
ngOnDestroy() {
this.subscriptions.forEach(s => s.unsubscribe());
}
private buildMenu() {
@@ -5,6 +5,7 @@
<adf-toolbar class="inline">
<button
color="primary"
mat-icon-button
[app-permanent-delete-node]="documentList.selection"
(selection-node-deleted)="refresh()"
@@ -14,6 +15,7 @@
</button>
<button
color="primary"
mat-icon-button
(selection-node-restored)="refresh()"
[app-restore-node]="documentList.selection"
@@ -25,80 +27,84 @@
</div>
<div class="inner-layout__content">
<div class="inner-layout__panel" [ngClass]="{'adf-document-list--loading': documentList.isEmpty()}">
<adf-document-list #documentList
[attr.class]="documentList.isEmpty() ? 'adf-document-list--empty' : ''"
currentFolderId="-trashcan-"
selectionMode="multiple"
[navigate]="false"
[sorting]="sorting"
[contextMenuActions]="true"
[contentActions]="false"
(sorting-changed)="onSortingChanged($event)">
<adf-document-list #documentList
[attr.class]="documentList.isEmpty() ? 'empty-list' : ''"
currentFolderId="-trashcan-"
selectionMode="multiple"
[navigate]="false"
[sorting]="sorting"
[contextMenuActions]="true"
[contentActions]="false"
(sorting-changed)="onSortingChanged($event)">
<empty-folder-content>
<ng-template>
<app-empty-folder
icon="delete"
title="APP.BROWSE.TRASHCAN.EMPTY_STATE.TITLE">
<p class="app-empty-folder__text">{{ 'APP.BROWSE.TRASHCAN.EMPTY_STATE.FIRST_TEXT' | translate }}</p>
<p class="app-empty-folder__text">{{ 'APP.BROWSE.TRASHCAN.EMPTY_STATE.SECOND_TEXT' | translate }}</p>
</app-empty-folder>
</ng-template>
</empty-folder-content>
<data-columns>
<data-column
key="$thumbnail"
type="image"
[sortable]="false"
class="image-table-cell">
</data-column>
<data-column
class="adf-data-table-cell--ellipsis__name"
key="name"
title="APP.DOCUMENT_LIST.COLUMNS.NAME">
<ng-template let-value="value" let-context>
<span class="adf-datatable-cell" title="{{ context?.row?.obj | adfNodeNameTooltip }}">{{ value }}</span>
<empty-folder-content>
<ng-template>
<app-empty-folder
icon="delete"
title="APP.BROWSE.TRASHCAN.EMPTY_STATE.TITLE">
<p class="app-empty-folder__text">{{ 'APP.BROWSE.TRASHCAN.EMPTY_STATE.FIRST_TEXT' | translate }}</p>
<p class="app-empty-folder__text">{{ 'APP.BROWSE.TRASHCAN.EMPTY_STATE.SECOND_TEXT' | translate }}</p>
</app-empty-folder>
</ng-template>
</data-column>
</empty-folder-content>
<data-column
key="path"
title="APP.DOCUMENT_LIST.COLUMNS.LOCATION">
<ng-template let-context>
<app-location-link [context]="context"></app-location-link>
</ng-template>
</data-column>
<data-columns>
<data-column
key="content.sizeInBytes"
title="APP.DOCUMENT_LIST.COLUMNS.SIZE"
type="fileSize">
</data-column>
<data-column
key="$thumbnail"
type="image"
[sortable]="false"
class="image-table-cell">
</data-column>
<data-column
key="archivedAt"
title="APP.DOCUMENT_LIST.COLUMNS.DELETED_ON">
<ng-template let-value="value">
<span title="{{ value | date:'medium' }}">{{ value | adfTimeAgo }}</span>
</ng-template>
</data-column>
<data-column
class="adf-data-table-cell--ellipsis__name"
key="name"
title="APP.DOCUMENT_LIST.COLUMNS.NAME">
<ng-template let-value="value" let-context>
<span class="adf-datatable-cell" title="{{ context?.row?.obj | adfNodeNameTooltip }}">{{ value }}</span>
</ng-template>
</data-column>
<data-column
class="adf-data-table-cell--ellipsis"
key="archivedByUser.displayName"
title="APP.DOCUMENT_LIST.COLUMNS.DELETED_BY">
</data-column>
<data-column
key="path"
title="APP.DOCUMENT_LIST.COLUMNS.LOCATION">
<ng-template let-context>
<app-location-link [context]="context"></app-location-link>
</ng-template>
</data-column>
</data-columns>
<data-column
key="content.sizeInBytes"
title="APP.DOCUMENT_LIST.COLUMNS.SIZE"
type="fileSize">
</data-column>
</adf-document-list>
<adf-pagination [ngClass]="{ 'no-border' : documentList.isEmpty()}"
[target]="documentList"
(changePageSize)="onChangePageSize($event)">
</adf-pagination>
<data-column
key="archivedAt"
title="APP.DOCUMENT_LIST.COLUMNS.DELETED_ON">
<ng-template let-value="value">
<span title="{{ value | date:'medium' }}">{{ value | adfTimeAgo }}</span>
</ng-template>
</data-column>
<data-column
class="adf-data-table-cell--ellipsis"
key="archivedByUser.displayName"
title="APP.DOCUMENT_LIST.COLUMNS.DELETED_BY">
</data-column>
</data-columns>
</adf-document-list>
<ng-container *ngIf="!documentList.isEmpty()">
<adf-pagination
[supportedPageSizes]="'pagination.supportedPageSizes' | appConfig"
[target]="documentList"
(changePageSize)="onChangePageSize($event)">
</adf-pagination>
</ng-container>
</div>
</div>
</div>

Some files were not shown because too many files have changed in this diff Show More