New packages org (#2639)

New packages org
This commit is contained in:
Eugenio Romano
2017-11-16 14:12:52 +00:00
committed by GitHub
parent 6a24c6ef75
commit a52bb5600a
1984 changed files with 17179 additions and 40423 deletions

36
lib/.gitignore vendored Normal file
View File

@@ -0,0 +1,36 @@
npm-debug.log
node_modules
.idea
.npmrc
typings
coverage
dist
content-services/**/*.js
!content-services/karma-test-shim.js
content-services/**/*.js.map
content-services/**/*.d.ts
process-services/**/*.js
!process-services/karma-test-shim.js
process-services/**/*.js.map
process-services/**/*.d.ts
core/**/*.js
!core/karma-test-shim.js
core/**/*.js.map
core/**/*.d.ts
analytics/**/*.js
!analytics/karma-test-shim.js
analytics/**/*.js.map
analytics/**/*.d.ts
!config/karma-test-shim.js!
*.tgzf
core/prebuilt-themes/
/package/
/bundles/
index.d.ts
/.happypack

40
lib/README.md Normal file
View File

@@ -0,0 +1,40 @@
# Alfresco Angular Components
<!-- markdown-toc start - Don't edit this section. npm run toc to generate it-->
<!-- toc -->
- [Documentation](#documentation)
- [Build from sources](#build-from-sources)
- [NPM scripts](#npm-scripts)
<!-- tocstop -->
<!-- markdown-toc end -->
## Documentation
The [docs index](../docs/README.md) lists all available documentation for components and
also includes a user guide that explains techniques in greater detail.
## Build from sources
You can build component from sources with the following commands:
```sh
npm install
npm run build
```
> The `build` task rebuilds all the code, runs tslint, license checks
> and other quality check tools before performing unit testing.
## NPM scripts
| Command | Description |
| --- | --- |
| npm run build | Build component |
| npm run test | Run unit tests in the console |
| npm run test-browser | Run unit tests in the browser
| npm run coverage | Run unit tests and display code coverage report |

View File

@@ -0,0 +1,5 @@
{
"ecmHost": "http://{hostname}:{port}/ecm",
"bpmHost": "http://{hostname}:{port}/bpm",
"logLevel" : "silent"
}

View File

@@ -0,0 +1,16 @@
/*!
* @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.
*/

View File

@@ -0,0 +1,14 @@
@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.

View File

@@ -0,0 +1,238 @@
var fs = require('fs');
var path = require('path');
var angFilenameRegex = /([a-zA-Z0-9\-]+)\.((component)|(directive)|(model)|(pipe)|(service)|(widget))\.ts/;
var indexFileName = path.resolve('..', 'docs', 'README.md');
var summaryFileName = path.resolve('..', 'docs', 'summary.json');
var undocStoplistFileName = path.resolve('..', 'docs', 'undocStoplist.json');
// Search source folders for .ts files to discover all components, directives, etc,
// that are in the supplied library.
function searchLibraryRecursive(libData, folderPath) {
var items = fs.readdirSync(folderPath);
for (var i = 0; i < items.length; i++) {
var itemPath = path.resolve(folderPath, items[i]);
var info = fs.statSync(itemPath);
if (info.isFile() && (items[i].match(angFilenameRegex))) {
var nameNoSuffix = path.basename(items[i], '.ts');
var itemCategory = nameNoSuffix.split('.')[1];
if(nameNoSuffix in docDict) {
switch (itemCategory) {
case "component":
libData.componentsWithDocs.push(itemPath);
break;
case "directive":
libData.directivesWithDocs.push(itemPath);
break;
case "model":
libData.modelsWithDocs.push(itemPath);
break;
case "pipe":
libData.pipesWithDocs.push(itemPath);
break;
case "service":
libData.servicesWithDocs.push(itemPath);
break;
case "widget":
libData.widgetsWithDocs.push(itemPath);
break;
default:
break;
}
} else if (!rejectItemViaStoplist(undocStoplist, items[i])) {
switch (itemCategory) {
case "component":
libData.componentsWithoutDocs.push(itemPath);
break;
case "directive":
libData.directivesWithoutDocs.push(itemPath);
break;
case "model":
libData.modelsWithoutDocs.push(itemPath);
break;
case "pipe":
libData.pipesWithoutDocs.push(itemPath);
break;
case "service":
libData.servicesWithoutDocs.push(itemPath);
break;
case "widget":
libData.widgetsWithoutDocs.push(itemPath);
break;
default:
break;
}
}
} else if (info.isDirectory()) {
searchLibraryRecursive(libData, itemPath);
}
}
}
// Get a list of all items that have a file in the docs folder.
function getDocFolderItems(docFolderPath) {
var result = {};
var items = fs.readdirSync(path.resolve(docFolderPath));
for (var i = 0; i < items.length; i++) {
if (items[i].endsWith('.md')) {
var nameNoSuffix = path.basename(items[i], '.md');
result[nameNoSuffix] = 1;
}
}
return result;
}
// Convert an Angular-style name (eg, "card-view") into one with correct spaces and uppercase (eg, "Card View").
function tidyName(name) {
var result = name.replace(/-/g, " ");
result = result.substr(0, 1).toUpperCase() + result.substr(1);
return result;
}
// Generate the Markdown index for the files from the guide summary.
function makeSummaryIndex() {
var summaryJson = fs.readFileSync(summaryFileName, 'utf8');
var summary = JSON.parse(summaryJson);
var result = '';
for (var i = 0; i < summary.length; i++) {
var item = summary[i];
result += '- [' + item.title + '](' + item.file + ')\n';
}
return result;
}
// Create a stoplist of regular expressions.
function makeStoplist(slFilePath) {
var listExpressions = JSON.parse(fs.readFileSync(slFilePath, 'utf8'));
var result = [];
for (var i = 0; i < listExpressions.length; i++) {
result.push(new RegExp(listExpressions[i]));
}
return result;
}
// Check if an item is covered by the stoplist and reject it if so.
function rejectItemViaStoplist(stoplist, itemName) {
for (var i = 0; i < stoplist.length; i++) {
if (stoplist[i].test(itemName)) {
return true;
}
}
return false;
}
function buildIndexSection(name, documented, undocumented) {
var listItems = [];
if ((documented.length > 0) || (undocumented.length > 0)) {
listItems.push('\n### ' + name + '\n');
}
for (var i = 0; i < documented.length; i++) {
var libFilePath = documented[i];
var libFileName = path.basename(libFilePath, '.ts');
var nameSections = libFileName.split('.');
var visibleName = tidyName(nameSections[0]) + ' ' + nameSections[1];
var mdListItem = '- [' + visibleName + '](' + libFileName + '.md)';
listItems.push(mdListItem);
}
for (var i = 0; i < undocumented.length; i++) {
var libFilePath = undocumented[i].replace(/\\/g, '/');
var libFileName = path.basename(libFilePath, '.ts');
var nameSections = libFileName.split('.');
var visibleName = tidyName(nameSections[0]) + ' ' + nameSections[1];
var relPath = libFilePath.substr(libFilePath.indexOf('/ng2-') + 1);
var mdListItem = '- [*' + visibleName + '](../' + relPath + ')';
listItems.push(mdListItem);
}
return listItems;
}
var undocStoplist = makeStoplist(undocStoplistFileName);
var docDict = getDocFolderItems(path.resolve('..', 'docs'));
var rootItems = fs.readdirSync(path.resolve('.'));
var libs = {}
for (var i = 0; i < rootItems.length; i++) {
var itemPath = path.resolve(rootItems[i]);
var info = fs.statSync(itemPath);
if (info.isDirectory() && rootItems[i].match(/ng2-/)) {
libs[rootItems[i]] = {
componentsWithDocs: [], componentsWithoutDocs: [],
directivesWithDocs: [], directivesWithoutDocs: [],
modelsWithDocs: [], modelsWithoutDocs: [],
pipesWithDocs: [], pipesWithoutDocs: [],
servicesWithDocs: [], servicesWithoutDocs: [],
widgetsWithDocs: [], widgetsWithoutDocs: [],
};
searchLibraryRecursive(libs[rootItems[i]], path.resolve(itemPath, 'src'));
}
}
var indexFileText = fs.readFileSync(indexFileName, 'utf8');
var libNames = Object.keys(libs);
for (var i = 0; i < libNames.length; i++) {
var libName = libNames[i];
var libData = libs[libName];
var listItems = buildIndexSection('Components', libData.componentsWithDocs, libData.componentsWithoutDocs);
listItems = listItems.concat(buildIndexSection('Directives', libData.directivesWithDocs, libData.directivesWithoutDocs));
listItems = listItems.concat(buildIndexSection('Models', libData.modelsWithDocs, libData.modelsWithoutDocs));
listItems = listItems.concat(buildIndexSection('Pipes', libData.pipesWithDocs, libData.pipesWithoutDocs));
listItems = listItems.concat(buildIndexSection('Services', libData.servicesWithDocs, libData.servicesWithoutDocs));
listItems = listItems.concat(buildIndexSection('Widgets', libData.widgetsWithDocs, libData.widgetsWithoutDocs));
var libText = listItems.join('\n');
var libStartMarker = '<!-- ' + libName + ' start -->';
var libEndMarker = '<!-- ' + libName + ' end -->';
var contentRegex = new RegExp('(?:' + libStartMarker + ')([\\s\\S]*)(?:' + libEndMarker + ')');
indexFileText = indexFileText.replace(contentRegex, libStartMarker + '\n' + libText + '\n' + libEndMarker);
}
var guideStartMarker = '<!-- guide start -->';
var guideEndMarker = '<!-- guide end -->';
var contentRegex = new RegExp('(?:' + guideStartMarker + ')([\\s\\S]*)(?:' + guideEndMarker + ')');
indexFileText = indexFileText.replace(contentRegex, guideStartMarker + '\n' + makeSummaryIndex() + '\n' + guideEndMarker);
fs.writeFileSync(indexFileName, indexFileText, 'utf-8');

View File

@@ -0,0 +1,70 @@
var path = require('path');
var loaderUtils = require('loader-utils');
module.exports = function(content) {
this.cacheable && this.cacheable();
if(!this.emitFile) throw new Error('emitFile is required from module system');
var query = loaderUtils.getOptions(this) || {};
var configKey = query.config || 'multiFileLoader';
var options = this.options[configKey] || {};
var config = {
publicPath: false,
useRelativePath: false,
name: '[hash].[ext]'
};
// options takes precedence over config
Object.keys(options).forEach(function(attr) {
config[attr] = options[attr];
});
// query takes precedence over config and options
Object.keys(query).forEach(function(attr) {
config[attr] = query[attr];
});
var context = config.context || this.options.context;
var url = loaderUtils.interpolateName(this, config.name, {
context: context,
content: content,
regExp: config.regExp
});
var path = loaderUtils.interpolateName(this, '[path]', {
context: context,
content: content,
regExp: config.regExp
});
var outputPath = '';
if (config.outputPath) {
outputPath = (
typeof config.outputPath === 'function'
? config.outputPath(url, path)
: config.outputPath + url
);
} else {
outputPath = url;
}
var publicPath = JSON.stringify(url);
if (config.publicPath) {
publicPath = JSON.stringify(
typeof config.publicPath === 'function'
? config.publicPath(url, path)
: config.publicPath + url
);
}
publicPath = '__webpack_public_path__ + ' + publicPath;
if (query.emitFile === undefined || query.emitFile) {
this.emitFile(outputPath, content);
}
return 'module.exports = ' + publicPath + ';';
};
module.exports.raw = true;

View File

@@ -0,0 +1,155 @@
var path = require('path');
var fs = require('fs');
var erase = true;
var readmeContent = null;
var readmeFilePath = '';
function isFileEmpty(fileContents) {
return fileContents.toString('utf8').trim() === '';
}
function writeFile(file, newValue) {
fs.writeFileSync(file, newValue, 'utf-8');
}
function readFile(file) {
return fs.readFileSync(file, 'utf8');
}
function eraseContentList() {
if (erase) {
erase = false;
var businessRegex = /(?:<!-- BUSINESS START-->)([\s\S]*?)(?:<!-- BUSINESS END-->)/;
var contentRegex = /(?:<!-- CONTENT START-->)([\s\S]*?)(?:<!-- CONTENT END-->)/;
var coreRegex = /(?:<!-- CORE START-->)([\s\S]*?)(?:<!-- CORE END-->)/;
var businessRegexDirective = /(?:<!-- BUSINESS DIRECTIVE START-->)([\s\S]*?)(?:<!-- BUSINESS DIRECTIVE END-->)/;
var contentRegexDirective = /(?:<!-- CONTENT DIRECTIVE START-->)([\s\S]*?)(?:<!-- CONTENT DIRECTIVE END-->)/;
var coreRegexDirective = /(?:<!-- CORE DIRECTIVE START-->)([\s\S]*?)(?:<!-- CORE DIRECTIVE END-->)/;
var servicessRegex = /(?:<!-- SERVICES START-->)([\s\S]*?)(?:<!-- SERVICES END-->)/;
readmeContent = readmeContent.replace(businessRegex, '<!-- BUSINESS START--><!-- BUSINESS END-->');
readmeContent = readmeContent.replace(contentRegex, '<!-- CONTENT START--><!-- CONTENT END-->');
readmeContent = readmeContent.replace(coreRegex, '<!-- CORE START--><!-- CORE END-->');
readmeContent = readmeContent.replace(businessRegexDirective, '<!-- BUSINESS DIRECTIVE START--><!-- BUSINESS DIRECTIVE END-->');
readmeContent = readmeContent.replace(contentRegexDirective, '<!-- CONTENT DIRECTIVE START--><!-- CONTENT DIRECTIVE END-->');
readmeContent = readmeContent.replace(coreRegexDirective, '<!-- CORE DIRECTIVE START--><!-- CORE DIRECTIVE END-->');
readmeContent = readmeContent.replace(servicessRegex, '<!-- SERVICES START--><!-- SERVICES END-->');
writeFile(readmeFilePath, readmeContent)
}
}
function generateListComponent(currentFileContent, webpackInstance) {
if (!isFileEmpty(currentFileContent)) {
var componentReg = /(@Component)(\s?)\((\s?){(\s?)((.|[\n\r])*)}(\s?)\)/gm;
var componentSection = componentReg.exec(currentFileContent);
if (componentSection) {
var selectorReg = /(adf)([a-zA-Z]|-)+((?!,)|(?! ))/g;
var selector = selectorReg.exec(componentSection[0]);
if (selector) {
var rawPath = webpackInstance.resourcePath.replace(/\\/g, "/");
var removeRoot = rawPath.substr(rawPath.indexOf('/ng2-components') + 16, rawPath.length);
var url = removeRoot.substr(0, removeRoot.indexOf('src')) + 'README.md';
var link = '- [' + selector[0] + '](' + url + ')';
if (webpackInstance.resourcePath.match('ng2-alfresco-core')) {
readmeContent = readmeContent.replace('<!-- CORE START-->', '<!-- CORE START-->\n' + link);
} else if (webpackInstance.resourcePath.match('ng2-alfresco-')) {
readmeContent = readmeContent.replace('<!-- CONTENT START-->', '<!-- CONTENT START-->\n' + link);
} else if (webpackInstance.resourcePath.match('ng2-activiti-')) {
readmeContent = readmeContent.replace('<!-- BUSINESS START-->', '<!-- BUSINESS START-->\n' + link);
}
}
}
var directiveReg = /(@Directive)(\s?)\((\s?){(\s?)((.|[\r\n])*)}(\s?)\)/gm;
var directiveSection = directiveReg.exec(currentFileContent);
if (directiveSection) {
var selectorReg = /(adf)([a-zA-Z]|-)+((?!,)|(?! ))/g;
var selector = selectorReg.exec(directiveSection[0]);
if (selector) {
var selector = selector[0].replace("selector: '[", "").replace("']", '').replace("]", '').replace("selector: '", "").replace("'", '');
var rawPath = webpackInstance.resourcePath.replace(/\\/g, "/");
var removeRoot = rawPath.substr(rawPath.indexOf('/ng2-components') + 16, rawPath.length);
var url = removeRoot.substr(0, removeRoot.indexOf('src')) + 'README.md';
var link = '- [' + selector + '](' + url + ')';
if (webpackInstance.resourcePath.match('ng2-alfresco-core')) {
readmeContent = readmeContent.replace('<!-- CORE DIRECTIVE START-->', '<!-- CORE DIRECTIVE START-->\n' + link);
}
//else if (webpackInstance.resourcePath.match('ng2-alfresco-')) {
// readmeContent = readmeContent.replace('<!-- CONTENT DIRECTIVE START-->', '<!-- CONTENT DIRECTIVE START-->\n' + link);
//}
//else if (webpackInstance.resourcePath.match('ng2-activiti-')) {
// readmeContent = readmeContent.replace('<!-- BUSINESS DIRECTIVE START-->', '<!-- BUSINESS DIRECTIVE START-->\n' + link);
//}
}
}
writeFile(readmeFilePath, readmeContent);
return true;
}
}
function generateListservices(currentFileContent, webpackInstance) {
if (!isFileEmpty(currentFileContent)) {
var servicesReg = /(@Injectable\(\))(([a-zA-Z ]|[\r\n])*)/gm;
var servicesSection = servicesReg.exec(currentFileContent);
if (servicesSection) {
var selectorReg = /([a-zA-Z])+Service/g;
var selector = selectorReg.exec(servicesSection[0]);
if (selector) {
var rawPath = webpackInstance.resourcePath.replace(/\\/g, "/");
var url = rawPath.substr(rawPath.indexOf('/ng2-components') + 16, rawPath.length);
var link = '- [' + selector[0] + '](' + url + ')';
readmeContent = readmeContent.replace('<!-- SERVICES START-->', '<!-- SERVICES START-->\n' + link);
}
}
writeFile(readmeFilePath, readmeContent);
return true;
}
}
module.exports = function (input, map) {
this.cacheable && this.cacheable();
var callback = this.async();
readmeFilePath = path.resolve(__dirname, '../../README.md');
if (!readmeContent) {
readmeContent = readFile(readmeFilePath);
}
if (readmeContent) {
eraseContentList();
generateListComponent(input, this);
generateListservices(input, this);
}
callback(null, input, map);
}

View File

@@ -0,0 +1,67 @@
var path = require('path');
var loaderUtils = require('loader-utils');
var fs = require('fs');
var licenseFileUtf8Store = undefined;
function readLicenseHeaderFile(licenseFilePath) {
if (licenseFileUtf8Store) {
return licenseFileUtf8Store;
}
if (fs.existsSync(licenseFilePath)) {
licenseFileUtf8Store = fs.readFileSync(licenseFilePath, 'utf8').split(/\r?\n/);
return licenseFileUtf8Store;
}
throw new Error('The license header file path is wrong ' + licenseFilePath);
}
function isFileEmpty(fileContents) {
return fileContents.toString('utf8').trim() === '';
}
function readCurrentFile(fileContent) {
return fileContent.toString('utf8').split(/\r?\n/);
}
function isLicenseHeaderPresent(currentFileContent, licenseFilePath) {
if (!isFileEmpty(currentFileContent)) {
var currentFileUtf8 = readCurrentFile(currentFileContent),
licenseFileUtf8 = readLicenseHeaderFile(licenseFilePath);
skipStrict = 0;
if(currentFileUtf8[0] === '"use strict";' ) {
skipStrict = 1;
}
for (var i = skipStrict; i < licenseFileUtf8.length; i++) {
if (currentFileUtf8[i + skipStrict] !== licenseFileUtf8[i]) {
return false;
}
}
}
return true;
}
function report(hasHeader, emitter, filename) {
if (hasHeader) return;
emitter('Missing license header file : ' + filename);
}
function licenseCheck(webpackInstance, input, options) {
var isLicensePresent = isLicenseHeaderPresent(input, options.licenseFile);
var emitter = options.emitErrors ? webpackInstance.emitError : webpackInstance.emitWarning;
report(isLicensePresent, emitter, webpackInstance.resourcePath);
}
module.exports = function(input, map) {
this.cacheable && this.cacheable();
var callback = this.async();
var options = loaderUtils.getOptions(this);
licenseCheck(this, input, options);
callback(null, input, map);
};

View File

@@ -0,0 +1,96 @@
var fs = require('fs');
var path = require('path');
var docFolderPath = path.resolve("..", "docs");
var graphFileName = path.resolve(docFolderPath, "seeAlsoGraph.json");
// Get a list of all items that have a file in the docs folder.
function getDocFolderItems(docFolderPath) {
var result = {};
var items = fs.readdirSync(path.resolve(docFolderPath));
for (var i = 0; i < items.length; i++) {
if (items[i].endsWith('.md')) {
var nameNoSuffix = path.basename(items[i], '.md');
result[nameNoSuffix] = 1;
}
}
return result;
}
// Convert an Angular-style name (eg, "card-view") into one with correct spaces and uppercase (eg, "Card view").
function tidyName(name) {
var result = name.replace(/-/g, " ");
result = result.substr(0, 1).toUpperCase() + result.substr(1);
return result;
}
function buildSeeAlsoList(arcs) {
var listItems = [];
for (var i = 0; i < arcs.length; i++) {
var parts = arcs[i].split('.');
var itemName = tidyName(parts[0]);
if (parts[1]) {
itemName += ' ' + parts[1];
}
listItems.push('- [' + itemName + '](' + arcs[i] + '.md)');
}
return listItems.join('\n');
}
// If item is not in the arcs array then add it at
// the end.
function fixArcs(arcsArray, item) {
if (arcsArray.indexOf(item) == -1) {
arcsArray.push(item);
}
}
// Makes link symmetrical between items (ie, if A links to B but not the other way
// around then it adds the missing link).
function tidyGraph(graph) {
var nodeNames = Object.keys(graph);
for (var n = 0; n < nodeNames.length; n++) {
var currNodeName = nodeNames[n];
var currNodeArcs = graph[currNodeName];
for (var a = 0; a < currNodeArcs.length; a++) {
var linkedNode = graph[currNodeArcs[a]];
fixArcs(linkedNode, currNodeName);
}
}
}
var graphJson = fs.readFileSync(graphFileName, 'utf8');
var graph = JSON.parse(graphJson);
tidyGraph(graph);
var nodeNames = Object.keys(graph);
for (var i = 0; i < nodeNames.length; i++) {
var seeAlsoText = '## See also\n\n' + buildSeeAlsoList(graph[nodeNames[i]]);
var docFileName = path.resolve(docFolderPath, nodeNames[i] + '.md');
var docFileText = fs.readFileSync(docFileName, 'utf8');
var seeAlsoStartMarker = '<!-- seealso start -->';
var seeAlsoEndMarker = '<!-- seealso end -->';
var seeAlsoRegex = new RegExp('(?:' + seeAlsoStartMarker + ')([\\s\\S]*)(?:' + seeAlsoEndMarker + ')');
docFileText = docFileText.replace(seeAlsoRegex, seeAlsoStartMarker + '\n' + seeAlsoText + '\n' + seeAlsoEndMarker);
fs.writeFileSync(docFileName, docFileText, 'utf-8');
}

10
lib/config/helpers.js Normal file
View File

@@ -0,0 +1,10 @@
var path = require('path');
var _root = path.resolve(__dirname, '..');
function root(args) {
args = Array.prototype.slice.call(arguments, 0);
return path.join.apply(path, [_root].concat(args));
}
exports.root = root;

View File

@@ -0,0 +1,44 @@
Error.stackTraceLimit = Infinity;
require('core-js/es6');
require('core-js/es7/reflect');
require('zone.js/dist/zone');
require('zone.js/dist/long-stack-trace-zone');
require('zone.js/dist/proxy');
require('zone.js/dist/sync-test');
require('zone.js/dist/jasmine-patch');
require('zone.js/dist/async-test');
require('zone.js/dist/fake-async-test');
jasmine.DEFAULT_TIMEOUT_INTERVAL = 20000;
var appContext = require.context(".", true, /.spec.ts/);appContext.keys().forEach(appContext);
const TestBed = require('@angular/core/testing').TestBed;
const browser = require('@angular/platform-browser-dynamic/testing');
const CoreModule = require('@alfresco/core').CoreModule;
const AppConfigService = require('@alfresco/core').AppConfigService;
const AppConfigServiceMock = require('@alfresco/core').AppConfigServiceMock;
const TranslationService = require('@alfresco/core').TranslationService;
const TranslationMock = require('@alfresco/core').TranslationMock;
const TranslateModule = require('@ngx-translate/core').TranslateModule;
const CommonModule = require('@angular/common').CommonModule;
const FormsModule = require('@angular/forms').FormsModule;
const ReactiveFormsModule = require('@angular/forms').ReactiveFormsModule;
TestBed.initTestEnvironment(browser.BrowserDynamicTestingModule, browser.platformBrowserDynamicTesting());
beforeEach(() => {
TestBed.configureTestingModule({
imports: [CoreModule, TranslateModule, CommonModule, FormsModule, ReactiveFormsModule],
providers: [
{provide: AppConfigService, useClass: AppConfigServiceMock},
{provide: TranslationService, useClass: TranslationMock}
]
});
});
afterEach(() => {
TestBed.resetTestingModule();
});

View File

@@ -0,0 +1,123 @@
const webpackCoverage = require('./webpack.coverage');
module.exports = function (config) {
var _config = {
basePath: './',
frameworks: ['jasmine-ajax', 'jasmine'],
files: [
{pattern: './node_modules/core-js/client/core.js', included: true, watched: false},
{pattern: './node_modules/tslib/tslib.js', included: true, watched: false},
{pattern: './node_modules/hammerjs/hammer.min.js', included: true, watched: false},
{pattern: './node_modules/hammerjs/hammer.min.js.map', included: false, watched: false},
{
pattern: './node_modules/@angular/material/prebuilt-themes/indigo-pink.css',
included: true,
watched: false
},
//diagrams
{pattern: './node_modules/chart.js/dist/Chart.js', included: true, watched: false},
{pattern: './node_modules/alfresco-js-api/dist/alfresco-js-api.min.js', included: true, watched: false},
{pattern: './node_modules/raphael/raphael.min.js', included: true, watched: false},
{pattern: './node_modules/moment/min/moment.min.js', included: true, watched: false},
{
pattern: './node_modules/ng2-charts/bundles/ng2-charts.umd.js',
included: false,
served: true,
watched: false
},
// pdf-js
{pattern: './node_modules/pdfjs-dist/build/pdf.js', included: true, watched: false},
{pattern: './node_modules/pdfjs-dist/build/pdf.worker.js', included: true, watched: false},
{pattern: './node_modules/pdfjs-dist/web/pdf_viewer.js', included: true, watched: false},
{pattern: config.component + '/karma-test-shim.js', watched: false},
{pattern: './core/i18n/**/en.json', included: false, served: true, watched: false},
{pattern: './content-services/i18n/**/en.json', included: false, served: true, watched: false},
{pattern: './process-services/i18n/**/en.json', included: false, served: true, watched: false},
{pattern: config.component + '/**/*.ts', included: false, served: true, watched: false},
{pattern: './config/app.config.json', included: false, served: true, watched: false}
],
webpack: webpackCoverage(config),
webpackMiddleware: {
noInfo: true,
stats: {
chunks: false
}
},
port: 9876,
proxies: {
'/app.config.json': '/base/config/app.config.json'
},
// level of logging
// possible values: config.LOG_DISABLE || config.LOG_ERROR || config.LOG_WARN || config.LOG_INFO || config.LOG_DEBUG
logLevel: config.LOG_DISABLE,
colors: true,
autoWatch: false,
captureTimeout: 4800000,
browserDisconnectTimeout: 4800000,
browserDisconnectTolerance: 10,
browserNoActivityTimeout: 3000000,
browsers: ['Chrome'],
// Karma plugins loaded
plugins: [
require('../node_modules/karma-jasmine'),
require('../node_modules/karma-coverage'),
require('../node_modules/karma-sourcemap-loader'),
require('../node_modules/karma-jasmine-ajax'),
require('../node_modules/karma-chrome-launcher'),
require('../node_modules/karma-webpack'),
require('../node_modules/karma-jasmine-html-reporter'),
require('../node_modules/karma-mocha-reporter')
],
webpackServer: {
noInfo: true
},
// Coverage reporter generates the coverage
reporters: ['mocha', 'coverage', 'kjhtml'],
preprocessors: {
'**/karma-test-shim.js': ['webpack'],
'(core|content-services|process-services)/**/!(*spec|index|*mock|*model|*event).js': 'coverage'
},
coverageReporter: {
includeAllSources: true,
dir: './coverage/' + config.component + '/',
subdir: 'report',
reporters: [
{type: 'text'},
{type: 'text-summary'},
{type: 'json', file: 'coverage-final.json'},
{type: 'html'},
{type: 'lcov'}
]
}
};
if (process.env.TRAVIS) {
config.browsers = ['ChromeHeadless'];
config.reporters = ['dots', 'coverage'];
}
config.set(_config);
};

View File

@@ -0,0 +1,60 @@
const webpackMerge = require('webpack-merge');
const UglifyJSPlugin = require('uglifyjs-webpack-plugin')
const commonConfig = require('./webpack.common.js');
const fs = require('fs');
const webpack = require('webpack');
const path = require('path');
module.exports = webpackMerge(commonConfig, {
// require those dependencies but don't bundle them
externals: [
/^\@angular\//,
/^rxjs\//,
/^\@ngx-translate\//,
'moment',
'minimatch',
'raphael',
'ng2-charts',
'alfresco-js-api',
/^\@alfresco\//,
'content-services',
'process-services',
'core'
],
output: {
filename: '[name]/bundles/[name].js',
library: '[name]',
libraryTarget: 'umd',
chunkFilename: '[id].chunk.js'
},
entry: {
"core": "./core/index.ts",
"insights": "./insights/index.ts",
"content-services": "./content-services/index.ts",
"process-services": "./process-services/index.ts"
},
plugins: [
new UglifyJSPlugin({
sourceMap: true,
uglifyOptions: {
ie8: false,
ecma: 6,
output: {
comments: false,
beautify: false
},
warnings: false
}
}),
new webpack.BannerPlugin({
banner: fs.readFileSync(path.resolve(__dirname, './assets/license_header_add.txt'), 'utf8'),
entryOnly: true
})
]
});

View File

@@ -0,0 +1,10 @@
const webpackMerge = require('webpack-merge');
const webpackBuild = require('./webpack.build.js');
const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin;
module.exports = webpackMerge(webpackBuild, {
plugins: [
new BundleAnalyzerPlugin()
]
});

View File

@@ -0,0 +1,173 @@
const webpack = require('webpack');
const helpers = require('./helpers');
const path = require('path');
const CopyWebpackPlugin = require('copy-webpack-plugin');
var HappyPack = require('happypack');
const ForkTsCheckerWebpackPlugin = require('fork-ts-checker-webpack-plugin');
const alfrescoLibs = [
'content-services',
'process-services',
'core'
];
const ENV = process.env.NODE_ENV = process.env.ENV = 'production';
module.exports = {
devtool: 'source-map',
resolveLoader: {
alias: {
"file-multi-loader": path.resolve(__dirname, "./custom-loaders/file-loader-multi"),
"license-check": path.resolve(__dirname, "./custom-loaders/license-check")
}
},
resolve: {
alias: {
"@alfresco/content-services": path.resolve(__dirname, '../content-services/'),
"@alfresco/content-services$": path.resolve(__dirname, '../content-services/index.ts'),
"@alfresco/process-services": path.resolve(__dirname, '../process-services/'),
"@alfresco/process-services$": path.resolve(__dirname, '../process-services/index.ts'),
"@alfresco/core": path.resolve(__dirname, '../core/'),
"@alfresco/core$": path.resolve(__dirname, '../core/index.ts'),
"@alfresco/insights": path.resolve(__dirname, '../insights/'),
"@alfresco/insights": path.resolve(__dirname, '../insights/index.ts')
},
extensions: ['.ts', '.js', '.scss'],
modules: [helpers.root('node_modules')]
},
module: {
rules: [
{
enforce: 'pre',
test: /\.js$/,
loader: 'source-map-loader',
exclude: [/node_modules/, /bundles/, /dist/, /demo/]
},
{
enforce: 'pre',
test: /\.ts$/,
use: 'source-map-loader',
exclude: [/node_modules/, /bundles/, /dist/, /demo/]
},
{
enforce: 'pre',
test: /\.ts$/,
loader: 'tslint-loader',
options: {
configFile : helpers.root('tslint.json'),
emitErrors: true,
failOnHint: true,
fix: true
},
exclude: [/node_modules/, /bundles/, /dist/, /demo/]
},
{
test: /\.ts$/,
loader: ['happypack/loader?id=ts', 'angular2-template-loader'],
exclude: [/node_modules/, /bundles/, /dist/, /demo/]
},
{
test: /\.html$/,
loader: 'html-loader',
exclude: [/node_modules/, /bundles/, /dist/, /demo/]
},
{
test: /\.css$/,
loader: ['happypack/loader?id=css'],
exclude: [/node_modules/, /bundles/, /dist/, /demo/]
},
{
test: /\.scss$/,
use: [{
loader: "to-string-loader"
}, {
loader: "raw-loader"
}, {
loader: "sass-loader"
}]
},
{
enforce: 'pre',
test: /\.ts$/,
loader: 'license-check',
options: {
emitErrors: true,
licenseFile: path.resolve(__dirname, './assets/license_header.txt')
},
exclude: [/node_modules/, /bundles/, /dist/, /demo/, /rendering-queue.services.ts/]
},
{
test: /\.(png|jpe?g|gif|svg|woff|woff2|ttf|eot|ico)$/,
loader: 'file-multi-loader',
query: {
name: '[name].[hash].[ext]',
outputPath: (url, resourcePath)=> {
return resourcePath.replace('assets/', 'bundles/assets/') + url;
},
publicPath: (url, resourcePath)=> {
return resourcePath + url;
}
}
}
]
},
plugins: [
new ForkTsCheckerWebpackPlugin(),
new HappyPack({
id: 'ts',
threads: 4,
loaders: [
{
path: 'ts-loader',
query: {
happyPackMode: true,
"compilerOptions": {
"paths": {
}
}
}
}
]
}),
new HappyPack({
id: 'css',
threads: 4,
loaders: ['to-string-loader', 'css-loader' ]
}),
new CopyWebpackPlugin([
... alfrescoLibs.map(lib => {
return {
from: `${lib}/i18n/`,
to: `${lib}/bundles/assets/${lib}/i18n/`
}
})
]),
new webpack.NoEmitOnErrorsPlugin(),
new webpack.ContextReplacementPlugin( /angular(\\|\/)core(\\|\/)/, path.resolve(__dirname, './src') ),
new webpack.DefinePlugin({
'process.env': {
'ENV': JSON.stringify(ENV)
}
}),
new webpack.LoaderOptionsPlugin({
htmlLoader: {
minimize: false // workaround for ng2
}
})
],
node: {
fs: 'empty',
module: false
}
};

View File

@@ -0,0 +1,26 @@
const webpack = require('webpack');
const webpackMerge = require('webpack-merge');
const testConfig = require('./webpack.test.js');
const helpers = require('./helpers');
module.exports = function (config) {
return webpackMerge(testConfig, {
devtool: 'inline-source-map',
module: {
rules: [
{
enforce: 'post',
test: /^(?!(.*spec|index|.*mock|.*model|.*event)).*\.ts?$/,
loader: 'istanbul-instrumenter-loader',
include: [helpers.root(config.component)],
exclude: [
/node_modules/,
/test/
]
}
]
}
});
};

23
lib/config/webpack.doc.js Normal file
View File

@@ -0,0 +1,23 @@
const helpers = require('./helpers');
const webpackMerge = require('webpack-merge');
const webpackBuild = require('./webpack.build');
const path = require('path');
module.exports = webpackMerge(webpackBuild, {
resolveLoader: {
alias: {
"generate-list-component-loader": path.resolve(__dirname, "./custom-loaders/generateListComponent")
}
},
module: {
rules: [
{
test: /\.ts/,
loader: 'generate-list-component-loader',
exclude: [/node_modules/, /bundles/, /dist/, /demo/]
}
]
}
});

View File

@@ -0,0 +1,36 @@
const ExtractTextPlugin = require("extract-text-webpack-plugin");
const extractScss = new ExtractTextPlugin('./core/prebuilt-themes/[name].css');
module.exports = {
entry: {
'adf-blue-orange': './core/styles/prebuilt/adf-blue-orange.scss',
'adf-blue-purple': './core/styles/prebuilt/adf-blue-purple.scss',
'adf-cyan-orange': './core/styles/prebuilt/adf-cyan-orange.scss',
'adf-cyan-purple': './core/styles/prebuilt/adf-cyan-purple.scss',
'adf-green-purple': './core/styles/prebuilt/adf-green-purple.scss',
'adf-green-orange': './core/styles/prebuilt/adf-green-orange.scss',
'adf-pink-bluegrey': './core/styles/prebuilt/adf-pink-bluegrey.scss',
'adf-indigo-pink': './core/styles/prebuilt/adf-indigo-pink.scss',
'adf-purple-green': './core/styles/prebuilt/adf-purple-green.scss'
},
output: {
filename: './dist/[name].js'
},
module: {
rules: [{
test: /\.scss$/,
use: extractScss.extract([{
loader: "raw-loader"
}, {
loader: "sass-loader"
}])
}]
},
plugins: [
extractScss
]
};

View File

@@ -0,0 +1,22 @@
const webpackMerge = require('webpack-merge');
const commonConfig = require('./webpack.common.js');
module.exports = webpackMerge(commonConfig, {
devtool: 'inline-source-map',
module: {
rules: [
{
test: /\.(txt|pdf)$/,
loader: 'file-loader',
query: {
name: '[path][name].[ext]',
outputPath: (url)=> {
return url.replace('src', 'dist');
}
}
}
]
}
});

View File

@@ -0,0 +1,38 @@
# Alfresco Content services Library
Contains a variety of components, directives and services used throughout ADF
<!-- markdown-toc start - Don't edit this section. npm run toc to generate it-->
<!-- toc -->
- [Documentation](#documentation)
- [Prerequisites](#prerequisites)
- [Install](#install)
- [License](#license)
<!-- tocstop -->
<!-- markdown-toc end -->
## Documentation
See the [ADF Content service](../../docs/README.md#content-services) section of the [docs index](../../docs/README.md)
for all available documentation on this library.
## Prerequisites
Before you start using this development framework, make sure you have installed all required software and done all the
necessary configuration, see this [page](https://github.com/Alfresco/alfresco-ng2-components/blob/master/PREREQUISITES.md).
> If you plan using this component with projects generated by Angular CLI, please refer to the following article: [Using ADF with Angular CLI](https://github.com/Alfresco/alfresco-ng2-components/wiki/Angular-CLI)
## Install
```sh
npm install @alfresco/content-services
```
## License
[Apache Version 2.0](https://github.com/Alfresco/alfresco-ng2-components/blob/master/LICENSE)

View File

@@ -0,0 +1,208 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg width="566px" height="165px" viewBox="0 0 566 165" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<!-- Generator: Sketch 3.8.3 (29802) - http://www.bohemiancoding.com/sketch -->
<title>empty_doc_lib</title>
<desc>Created with Sketch.</desc>
<defs>
<rect id="path-1" x="5.68434189e-14" y="-1.01962883e-12" width="78.1679389" height="78.1679389" rx="2"></rect>
<filter x="-50%" y="-50%" width="200%" height="200%" filterUnits="objectBoundingBox" id="filter-2">
<feOffset dx="0" dy="2" in="SourceAlpha" result="shadowOffsetOuter1"></feOffset>
<feGaussianBlur stdDeviation="1" in="shadowOffsetOuter1" result="shadowBlurOuter1"></feGaussianBlur>
<feColorMatrix values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.24 0" type="matrix" in="shadowBlurOuter1" result="shadowMatrixOuter1"></feColorMatrix>
<feOffset dx="0" dy="0" in="SourceAlpha" result="shadowOffsetOuter2"></feOffset>
<feGaussianBlur stdDeviation="1" in="shadowOffsetOuter2" result="shadowBlurOuter2"></feGaussianBlur>
<feColorMatrix values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.12 0" type="matrix" in="shadowBlurOuter2" result="shadowMatrixOuter2"></feColorMatrix>
<feMerge>
<feMergeNode in="shadowMatrixOuter1"></feMergeNode>
<feMergeNode in="shadowMatrixOuter2"></feMergeNode>
</feMerge>
</filter>
<rect id="path-3" x="-4.54747351e-13" y="5.68434189e-13" width="78.1679389" height="78.1679389" rx="2"></rect>
<filter x="-50%" y="-50%" width="200%" height="200%" filterUnits="objectBoundingBox" id="filter-4">
<feOffset dx="0" dy="2" in="SourceAlpha" result="shadowOffsetOuter1"></feOffset>
<feGaussianBlur stdDeviation="1" in="shadowOffsetOuter1" result="shadowBlurOuter1"></feGaussianBlur>
<feColorMatrix values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.24 0" type="matrix" in="shadowBlurOuter1" result="shadowMatrixOuter1"></feColorMatrix>
<feOffset dx="0" dy="0" in="SourceAlpha" result="shadowOffsetOuter2"></feOffset>
<feGaussianBlur stdDeviation="1" in="shadowOffsetOuter2" result="shadowBlurOuter2"></feGaussianBlur>
<feColorMatrix values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.12 0" type="matrix" in="shadowBlurOuter2" result="shadowMatrixOuter2"></feColorMatrix>
<feMerge>
<feMergeNode in="shadowMatrixOuter1"></feMergeNode>
<feMergeNode in="shadowMatrixOuter2"></feMergeNode>
</feMerge>
</filter>
<rect id="path-5" x="-1.08002496e-12" y="7.81597009e-14" width="78.1679389" height="78.1679389" rx="2"></rect>
<filter x="-50%" y="-50%" width="200%" height="200%" filterUnits="objectBoundingBox" id="filter-6">
<feOffset dx="0" dy="2" in="SourceAlpha" result="shadowOffsetOuter1"></feOffset>
<feGaussianBlur stdDeviation="1" in="shadowOffsetOuter1" result="shadowBlurOuter1"></feGaussianBlur>
<feColorMatrix values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.24 0" type="matrix" in="shadowBlurOuter1" result="shadowMatrixOuter1"></feColorMatrix>
<feOffset dx="0" dy="0" in="SourceAlpha" result="shadowOffsetOuter2"></feOffset>
<feGaussianBlur stdDeviation="1" in="shadowOffsetOuter2" result="shadowBlurOuter2"></feGaussianBlur>
<feColorMatrix values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.12 0" type="matrix" in="shadowBlurOuter2" result="shadowMatrixOuter2"></feColorMatrix>
<feMerge>
<feMergeNode in="shadowMatrixOuter1"></feMergeNode>
<feMergeNode in="shadowMatrixOuter2"></feMergeNode>
</feMerge>
</filter>
<rect id="path-7" x="1.29318778e-12" y="9.23705556e-14" width="78.1679389" height="78.1679389" rx="2"></rect>
<filter x="-50%" y="-50%" width="200%" height="200%" filterUnits="objectBoundingBox" id="filter-8">
<feOffset dx="0" dy="2" in="SourceAlpha" result="shadowOffsetOuter1"></feOffset>
<feGaussianBlur stdDeviation="1" in="shadowOffsetOuter1" result="shadowBlurOuter1"></feGaussianBlur>
<feColorMatrix values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.24 0" type="matrix" in="shadowBlurOuter1" result="shadowMatrixOuter1"></feColorMatrix>
<feOffset dx="0" dy="0" in="SourceAlpha" result="shadowOffsetOuter2"></feOffset>
<feGaussianBlur stdDeviation="1" in="shadowOffsetOuter2" result="shadowBlurOuter2"></feGaussianBlur>
<feColorMatrix values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.12 0" type="matrix" in="shadowBlurOuter2" result="shadowMatrixOuter2"></feColorMatrix>
<feMerge>
<feMergeNode in="shadowMatrixOuter1"></feMergeNode>
<feMergeNode in="shadowMatrixOuter2"></feMergeNode>
</feMerge>
</filter>
<rect id="path-9" x="-2.96651592e-13" y="-7.60280727e-13" width="78.1679389" height="78.1679389" rx="2"></rect>
<filter x="-50%" y="-50%" width="200%" height="200%" filterUnits="objectBoundingBox" id="filter-10">
<feOffset dx="0" dy="2" in="SourceAlpha" result="shadowOffsetOuter1"></feOffset>
<feGaussianBlur stdDeviation="1" in="shadowOffsetOuter1" result="shadowBlurOuter1"></feGaussianBlur>
<feColorMatrix values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.24 0" type="matrix" in="shadowBlurOuter1" result="shadowMatrixOuter1"></feColorMatrix>
<feOffset dx="0" dy="0" in="SourceAlpha" result="shadowOffsetOuter2"></feOffset>
<feGaussianBlur stdDeviation="1" in="shadowOffsetOuter2" result="shadowBlurOuter2"></feGaussianBlur>
<feColorMatrix values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.12 0" type="matrix" in="shadowBlurOuter2" result="shadowMatrixOuter2"></feColorMatrix>
<feMerge>
<feMergeNode in="shadowMatrixOuter1"></feMergeNode>
<feMergeNode in="shadowMatrixOuter2"></feMergeNode>
</feMerge>
</filter>
<rect id="path-11" x="3.48165941e-13" y="2.27373675e-13" width="78.1679389" height="78.1679389" rx="2"></rect>
<filter x="-50%" y="-50%" width="200%" height="200%" filterUnits="objectBoundingBox" id="filter-12">
<feOffset dx="0" dy="2" in="SourceAlpha" result="shadowOffsetOuter1"></feOffset>
<feGaussianBlur stdDeviation="1" in="shadowOffsetOuter1" result="shadowBlurOuter1"></feGaussianBlur>
<feColorMatrix values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.24 0" type="matrix" in="shadowBlurOuter1" result="shadowMatrixOuter1"></feColorMatrix>
<feOffset dx="0" dy="0" in="SourceAlpha" result="shadowOffsetOuter2"></feOffset>
<feGaussianBlur stdDeviation="1" in="shadowOffsetOuter2" result="shadowBlurOuter2"></feGaussianBlur>
<feColorMatrix values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.12 0" type="matrix" in="shadowBlurOuter2" result="shadowMatrixOuter2"></feColorMatrix>
<feMerge>
<feMergeNode in="shadowMatrixOuter1"></feMergeNode>
<feMergeNode in="shadowMatrixOuter2"></feMergeNode>
</feMerge>
</filter>
<rect id="path-13" x="0" y="-5.40012479e-13" width="78.1679389" height="78.1679389" rx="2"></rect>
<filter x="-50%" y="-50%" width="200%" height="200%" filterUnits="objectBoundingBox" id="filter-14">
<feOffset dx="0" dy="2" in="SourceAlpha" result="shadowOffsetOuter1"></feOffset>
<feGaussianBlur stdDeviation="1" in="shadowOffsetOuter1" result="shadowBlurOuter1"></feGaussianBlur>
<feColorMatrix values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.24 0" type="matrix" in="shadowBlurOuter1" result="shadowMatrixOuter1"></feColorMatrix>
<feOffset dx="0" dy="0" in="SourceAlpha" result="shadowOffsetOuter2"></feOffset>
<feGaussianBlur stdDeviation="1" in="shadowOffsetOuter2" result="shadowBlurOuter2"></feGaussianBlur>
<feColorMatrix values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.12 0" type="matrix" in="shadowBlurOuter2" result="shadowMatrixOuter2"></feColorMatrix>
<feMerge>
<feMergeNode in="shadowMatrixOuter1"></feMergeNode>
<feMergeNode in="shadowMatrixOuter2"></feMergeNode>
</feMerge>
</filter>
<rect id="path-15" x="0" y="0" width="78.1679389" height="78.1679389" rx="2"></rect>
<filter x="-50%" y="-50%" width="200%" height="200%" filterUnits="objectBoundingBox" id="filter-16">
<feOffset dx="0" dy="2" in="SourceAlpha" result="shadowOffsetOuter1"></feOffset>
<feGaussianBlur stdDeviation="1" in="shadowOffsetOuter1" result="shadowBlurOuter1"></feGaussianBlur>
<feColorMatrix values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.24 0" type="matrix" in="shadowBlurOuter1" result="shadowMatrixOuter1"></feColorMatrix>
<feOffset dx="0" dy="0" in="SourceAlpha" result="shadowOffsetOuter2"></feOffset>
<feGaussianBlur stdDeviation="1" in="shadowOffsetOuter2" result="shadowBlurOuter2"></feGaussianBlur>
<feColorMatrix values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.12 0" type="matrix" in="shadowBlurOuter2" result="shadowMatrixOuter2"></feColorMatrix>
<feMerge>
<feMergeNode in="shadowMatrixOuter1"></feMergeNode>
<feMergeNode in="shadowMatrixOuter2"></feMergeNode>
</feMerge>
</filter>
</defs>
<g id="Symbols" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
<g id="empty-folder-state-desktop" transform="translate(-37.000000, -168.000000)">
<g id="empty_doc_lib" transform="translate(38.000000, 169.000000)">
<g id="Group-5" transform="translate(241.569490, 92.634375) rotate(-355.000000) translate(-241.569490, -92.634375) translate(202.069490, 53.134375)">
<g id="Rectangle-1196-Copy-2">
<use fill="black" fill-opacity="1" filter="url(#filter-2)" xlink:href="#path-1"></use>
<use fill="#FFFFFF" fill-rule="evenodd" xlink:href="#path-1"></use>
</g>
<g id="filetype_video" transform="translate(9.770992, 9.770992)">
<polygon id="Fill-1" points="0 58.6259542 58.6259542 58.6259542 58.6259542 0 0 0"></polygon>
<path d="M39.0839695,21.9847328 L43.9694656,21.9847328 L43.9694656,17.0992366 L39.0839695,17.0992366 L39.0839695,21.9847328 Z M39.0839695,31.7557252 L43.9694656,31.7557252 L43.9694656,26.870229 L39.0839695,26.870229 L39.0839695,31.7557252 Z M39.0839695,41.5267176 L43.9694656,41.5267176 L43.9694656,36.6412214 L39.0839695,36.6412214 L39.0839695,41.5267176 Z M14.6564885,21.9847328 L19.5419847,21.9847328 L19.5419847,17.0992366 L14.6564885,17.0992366 L14.6564885,21.9847328 Z M14.6564885,31.7557252 L19.5419847,31.7557252 L19.5419847,26.870229 L14.6564885,26.870229 L14.6564885,31.7557252 Z M14.6564885,41.5267176 L19.5419847,41.5267176 L19.5419847,36.6412214 L14.6564885,36.6412214 L14.6564885,41.5267176 Z M43.9694656,7.32824427 L43.9694656,12.2137405 L39.0839695,12.2137405 L39.0839695,7.32824427 L19.5419847,7.32824427 L19.5419847,12.2137405 L14.6564885,12.2137405 L14.6564885,7.32824427 L9.77099237,7.32824427 L9.77099237,51.2977099 L14.6564885,51.2977099 L14.6564885,46.4122137 L19.5419847,46.4122137 L19.5419847,51.2977099 L39.0839695,51.2977099 L39.0839695,46.4122137 L43.9694656,46.4122137 L43.9694656,51.2977099 L48.8549618,51.2977099 L48.8549618,7.32824427 L43.9694656,7.32824427 Z" id="Fill-2" fill="#FFC107"></path>
</g>
</g>
<g id="Group-7" transform="translate(515.948329, 81.354522) rotate(-345.000000) translate(-515.948329, -81.354522) translate(476.448329, 41.854522)">
<g id="Rectangle-1196-Copy-3">
<use fill="black" fill-opacity="1" filter="url(#filter-4)" xlink:href="#path-3"></use>
<use fill="#FFFFFF" fill-rule="evenodd" xlink:href="#path-3"></use>
</g>
<g id="filetype_image" transform="translate(9.770992, 9.770992)">
<polygon id="Fill-1" points="0 58.6259542 58.6259542 58.6259542 58.6259542 0 0 0"></polygon>
<path d="M20.7636031,32.9773435 L26.8704733,40.3178015 L35.4200916,29.3132214 L46.412458,43.9697099 L12.2139847,43.9697099 L20.7636031,32.9773435 Z M51.2979542,46.412458 L51.2979542,12.2139847 C51.2979542,9.51474809 49.1116947,7.32848855 46.412458,7.32848855 L12.2139847,7.32848855 C9.51474809,7.32848855 7.32848855,9.51474809 7.32848855,12.2139847 L7.32848855,46.412458 C7.32848855,49.1116947 9.51474809,51.2979542 12.2139847,51.2979542 L46.412458,51.2979542 C49.1116947,51.2979542 51.2979542,49.1116947 51.2979542,46.412458 L51.2979542,46.412458 Z" id="Fill-2" fill="#22BE73"></path>
</g>
</g>
<g id="Group-8" transform="translate(309.051884, 62.261808) rotate(-5.000000) translate(-309.051884, -62.261808) translate(269.551884, 22.761808)">
<g id="Rectangle-1196-Copy-4">
<use fill="black" fill-opacity="1" filter="url(#filter-6)" xlink:href="#path-5"></use>
<use fill="#FFFFFF" fill-rule="evenodd" xlink:href="#path-5"></use>
</g>
<g id="filetype_googledocs" transform="translate(9.770992, 9.770992)">
<polygon id="Fill-1" points="6.82121026e-13 58.6259542 58.6259542 58.6259542 58.6259542 -2.98427949e-13 6.82121026e-13 -2.98427949e-13"></polygon>
<g id="Group-6" transform="translate(9.770992, 4.885496)">
<path d="M7.32824427,21.9847328 L31.7557252,21.9847328 L31.7557252,19.5419847 L7.32824427,19.5419847 L7.32824427,21.9847328 Z M7.32824427,26.870229 L31.7557252,26.870229 L31.7557252,24.4274809 L7.32824427,24.4274809 L7.32824427,26.870229 Z M7.32824427,31.7557252 L31.7557252,31.7557252 L31.7557252,29.3129771 L7.32824427,29.3129771 L7.32824427,31.7557252 Z M7.32824427,36.6412214 L21.9847328,36.6412214 L21.9847328,34.1984733 L7.32824427,34.1984733 L7.32824427,36.6412214 Z M29.3129771,0 L4.88549618,0 C2.18625954,0 0.0244274809,2.18625954 0.0244274809,4.88549618 L0,43.9694656 C0,46.6687023 2.16183206,48.8549618 4.8610687,48.8549618 L34.1984733,48.8549618 C36.8977099,48.8549618 39.0839695,46.6687023 39.0839695,43.9694656 L39.0839695,9.77099237 L29.3129771,0 Z" id="Fill-2" fill="#2979FF"></path>
<polygon id="Fill-4" fill-opacity="0.5" fill="#FFFFFF" points="29.3129771 9.77099237 29.3129771 -2.84217094e-14 39.0839695 9.77099237"></polygon>
<polygon id="Fill-5" fill-opacity="0.2" fill="#000000" points="39.0839695 9.77099237 39.0839695 19.5419847 29.3129771 9.77099237"></polygon>
</g>
</g>
</g>
<g id="Group-9" transform="translate(155.408682, 49.364493) rotate(-345.000000) translate(-155.408682, -49.364493) translate(115.908682, 9.864493)">
<g id="Rectangle-1196-Copy-5">
<use fill="black" fill-opacity="1" filter="url(#filter-8)" xlink:href="#path-7"></use>
<use fill="#FFFFFF" fill-rule="evenodd" xlink:href="#path-7"></use>
</g>
<g id="filetype_pdf" transform="translate(9.770992, 9.770992)">
<polygon id="Fill-1" points="0 58.6259542 58.6259542 58.6259542 58.6259542 0 0 0"></polygon>
<path d="M45.1888855,25.5877863 L45.1888855,21.9187786 L37.853313,21.9187786 L37.853313,36.5923664 L41.5198779,36.5923664 L41.5198779,31.7777099 L45.1888855,31.7777099 L45.1888855,28.1087023 L41.5198779,28.1087023 L41.5198779,25.5877863 L45.1888855,25.5877863 Z M29.3696489,32.9233588 L31.7757557,32.9233588 L31.7757557,25.5877863 L29.3696489,25.5877863 L29.3696489,32.9233588 Z M35.4447634,32.9233588 L35.4447634,25.5877863 C35.4447634,24.5960305 35.1027786,23.7361832 34.4139237,23.0082443 C33.7275115,22.2827481 32.8481221,21.9187786 31.7781985,21.9187786 L25.703084,21.9187786 L25.703084,36.5923664 L31.7781985,36.5923664 C32.8481221,36.5923664 33.7275115,36.2308397 34.4139237,35.5029008 C35.1027786,34.7774046 35.4447634,33.9175573 35.4447634,32.9233588 L35.4447634,32.9233588 Z M17.1070534,28.1087023 L19.5131603,28.1087023 L19.5131603,25.5877863 L17.1070534,25.5877863 L17.1070534,28.1087023 Z M23.1821679,28.1087023 L23.1821679,25.5877863 C23.1821679,24.5960305 22.8181985,23.7361832 22.0927023,23.0082443 C21.3672061,22.2827481 20.5073588,21.9187786 19.5131603,21.9187786 L13.4380458,21.9187786 L13.4380458,36.5923664 L17.1070534,36.5923664 L17.1070534,31.7777099 L19.5131603,31.7777099 C20.5073588,31.7777099 21.3672061,31.4161832 22.0927023,30.6882443 C22.8181985,29.9627481 23.1821679,29.1029008 23.1821679,28.1087023 L23.1821679,28.1087023 Z M46.483542,7.32824427 C47.783084,7.32824427 48.9091908,7.8070229 49.8643053,8.7621374 C50.8218626,9.71725191 51.2981985,10.8433588 51.2981985,12.1429008 L51.2981985,46.3682443 C51.2981985,47.670229 50.8218626,48.8158779 49.8643053,49.8076336 C48.9091908,50.8018321 47.783084,51.2977099 46.483542,51.2977099 L12.2581985,51.2977099 C10.9586565,51.2977099 9.81300763,50.8018321 8.81880916,49.8076336 C7.82461069,48.8158779 7.32873282,47.670229 7.32873282,46.3682443 L7.32873282,12.1429008 C7.32873282,10.8433588 7.82461069,9.71725191 8.81880916,8.7621374 C9.81300763,7.8070229 10.9586565,7.32824427 12.2581985,7.32824427 L46.483542,7.32824427 Z" id="Fill-2" fill="#E91E63"></path>
</g>
</g>
<g id="Group-12" transform="translate(49.364493, 62.584254) rotate(-15.000000) translate(-49.364493, -62.584254) translate(9.864493, 23.084254)">
<g id="Rectangle-1196-Copy-7">
<use fill="black" fill-opacity="1" filter="url(#filter-10)" xlink:href="#path-9"></use>
<use fill="#FFFFFF" fill-rule="evenodd" xlink:href="#path-9"></use>
</g>
<g id="filetype_forms" transform="translate(9.770992, 9.770992)">
<polygon id="Fill-1" points="0 58.6259542 58.6259542 58.6259542 58.6259542 0 0 0"></polygon>
<path d="M24.4274809,24.4250382 L41.5267176,24.4250382 L41.5267176,19.539542 L24.4274809,19.539542 L24.4274809,24.4250382 Z M24.4274809,31.7532824 L41.5267176,31.7532824 L41.5267176,26.8677863 L24.4274809,26.8677863 L24.4274809,31.7532824 Z M24.4274809,39.0839695 L41.5267176,39.0839695 L41.5267176,34.1984733 L24.4274809,34.1984733 L24.4274809,39.0839695 Z M17.0992366,24.4250382 L21.9847328,24.4250382 L21.9847328,19.539542 L17.0992366,19.539542 L17.0992366,24.4250382 Z M17.0992366,31.7532824 L21.9847328,31.7532824 L21.9847328,26.8677863 L17.0992366,26.8677863 L17.0992366,31.7532824 Z M17.0992366,39.0839695 L21.9847328,39.0839695 L21.9847328,34.1984733 L17.0992366,34.1984733 L17.0992366,39.0839695 Z M43.9694656,9.77099237 L14.6564885,9.77099237 C11.9694656,9.77099237 9.77099237,11.9694656 9.77099237,14.6564885 L9.77099237,43.9694656 C9.77099237,46.6564885 11.9694656,48.8549618 14.6564885,48.8549618 L43.9694656,48.8549618 C46.6564885,48.8549618 48.8549618,46.6564885 48.8549618,43.9694656 L48.8549618,14.6564885 C48.8549618,11.9694656 46.6564885,9.77099237 43.9694656,9.77099237 L43.9694656,9.77099237 Z" id="Fill-2" fill="#651FFF"></path>
</g>
</g>
<g id="Group-11" transform="translate(107.814782, 114.998541) rotate(-10.000000) translate(-107.814782, -114.998541) translate(68.314782, 75.498541)">
<g id="Rectangle-1196-Copy-6">
<use fill="black" fill-opacity="1" filter="url(#filter-12)" xlink:href="#path-11"></use>
<use fill="#FFFFFF" fill-rule="evenodd" xlink:href="#path-11"></use>
</g>
<g id="filetype_excel" transform="translate(9.770992, 9.770992)">
<polygon id="Fill-1" points="-5.68434189e-14 58.6259542 58.6259542 58.6259542 58.6259542 -1.56319402e-13 -5.68434189e-14 -1.56319402e-13"></polygon>
<g id="Group-10" transform="translate(4.885496, 4.885496)" fill="#22BE73">
<path d="M47.1621374,41.632 L28.848855,41.632 L28.848855,38.3025344 L33.2873282,38.3025344 L33.2873282,34.4161221 L28.848855,34.4161221 L28.848855,32.1981069 L33.2873282,32.1981069 L33.2873282,28.3116947 L28.848855,28.3116947 L28.848855,26.0936794 L33.2873282,26.0936794 L33.2873282,22.2072672 L28.848855,22.2072672 L28.848855,19.9868092 L33.2873282,19.9868092 L33.2873282,16.1028397 L28.848855,16.1028397 L28.848855,13.8823817 L33.2873282,13.8823817 L33.2873282,9.99841221 L28.848855,9.99841221 L28.848855,6.66894656 L47.1621374,6.66894656 L47.1621374,41.632 L47.1621374,41.632 Z M16.3444275,33.1141374 C15.4015267,30.7984122 14.2509924,28.5632977 13.5743511,26.1425344 C12.819542,28.3947481 11.7422901,30.5223817 10.8775573,32.730626 C9.6610687,32.7135267 8.4470229,32.6646718 7.23053435,32.613374 C8.65709924,29.821313 10.0348092,27.0072672 11.5053435,24.2323053 C10.2546565,21.3742901 8.88427481,18.572458 7.59694656,15.731542 C8.81832061,15.6582595 10.0396947,15.5874198 11.2610687,15.5190229 C12.0867176,17.690626 12.9929771,19.832916 13.6745038,22.0582595 C14.4073282,19.6985649 15.5016794,17.4781069 16.4372519,15.1990229 C17.6928244,15.1086412 18.9532824,15.032916 20.2112977,14.9718473 C18.7309924,18.0057405 17.2433588,21.0420763 15.7337405,24.0661985 C17.260458,27.1758168 18.8189313,30.2610076 20.3505344,33.3681832 C19.0143511,33.2900153 17.6806107,33.2069618 16.3444275,33.1141374 L16.3444275,33.1141374 Z M48.8329771,37.2203969 C48.8280916,27.591084 48.8158779,17.961771 48.8427481,8.32757252 C48.8036641,7.38467176 48.8720611,6.34161832 48.300458,5.51841221 C47.4845802,4.9590229 46.4512977,5.0249771 45.5132824,4.98589313 C39.9584733,5.01520611 34.4036641,5.00299237 28.848855,5.00299237 L28.848855,0.564519084 L25.551145,0.564519084 C17.0381679,2.06680916 8.5178626,3.52757252 4.68958206e-13,5.00787786 L4.68958206e-13,43.852458 C8.46900763,45.3327634 16.9429008,46.7544427 25.4021374,48.2909313 L28.848855,48.2909313 L28.848855,43.2979542 C34.6039695,43.2857405 40.359084,43.3126107 46.1068702,43.2979542 C47.0351145,43.2588702 48.4225954,43.2295573 48.6448855,42.0765802 C48.9819847,40.4839084 48.8036641,38.8350534 48.8329771,37.2203969 L48.8329771,37.2203969 Z" id="Fill-2"></path>
<path d="M35.5077863,13.8821374 L43.2781679,13.8821374 L43.2781679,9.99816794 L35.5077863,9.99816794 L35.5077863,13.8821374 Z M35.5077863,19.9865649 L43.2781679,19.9865649 L43.2781679,16.1025954 L35.5077863,16.1025954 L35.5077863,19.9865649 Z M35.5077863,26.0909924 L43.2781679,26.0909924 L43.2781679,22.2070229 L35.5077863,22.2070229 L35.5077863,26.0909924 Z M35.5077863,32.1954198 L43.2781679,32.1954198 L43.2781679,28.3114504 L35.5077863,28.3114504 L35.5077863,32.1954198 Z M35.5077863,38.3022901 L43.2781679,38.3022901 L43.2781679,34.4183206 L35.5077863,34.4183206 L35.5077863,38.3022901 Z" id="Combined-Shape"></path>
</g>
</g>
</g>
<g id="Group-4" transform="translate(388.820630, 86.064353) rotate(-350.000000) translate(-388.820630, -86.064353) translate(349.320630, 46.564353)">
<g id="Rectangle-1196-Copy">
<use fill="black" fill-opacity="1" filter="url(#filter-14)" xlink:href="#path-13"></use>
<use fill="#FFFFFF" fill-rule="evenodd" xlink:href="#path-13"></use>
</g>
<g id="filetype_audio" transform="translate(9.770992, 9.770992)">
<polygon id="Fill-1" points="-1.13686838e-13 58.6259542 58.6259542 58.6259542 58.6259542 -6.39488462e-14 -1.13686838e-13 -6.39488462e-14"></polygon>
<path d="M29.3129771,7.32824427 L43.9694656,7.32824427 L43.9694656,17.0601527 L34.2375573,17.0601527 L34.2375573,41.5658015 C34.2375573,44.2381679 33.2629008,46.5270229 31.3160305,48.4348092 C29.3691603,50.3450382 27.0607634,51.2977099 24.3883969,51.2977099 C21.7160305,51.2977099 19.4271756,50.3450382 17.5193893,48.4348092 C15.6091603,46.5270229 14.6564885,44.2381679 14.6564885,41.5658015 C14.6564885,38.8934351 15.6091603,36.5850382 17.5193893,34.6381679 C19.4271756,32.6912977 21.7160305,31.7166412 24.3883969,31.7166412 C25.9932824,31.7166412 27.6323664,32.1758779 29.3129771,33.0919084 L29.3129771,7.32824427 Z" id="Fill-2" fill="#E91E63"></path>
</g>
</g>
<g id="Group-2" transform="translate(411.603053, 1.221374)">
<g id="Group-3">
<g id="Rectangle-1196">
<use fill="black" fill-opacity="1" filter="url(#filter-16)" xlink:href="#path-15"></use>
<use fill="#FFFFFF" fill-rule="evenodd" xlink:href="#path-15"></use>
</g>
<polygon id="Fill-1" points="9.77099237 68.3969466 68.3969466 68.3969466 68.3969466 9.77099237 9.77099237 9.77099237"></polygon>
</g>
<g id="filetype_book" transform="translate(9.770992, 9.770992)">
<polygon id="Fill-1" points="0 58.6259542 58.6259542 58.6259542 58.6259542 0 0 0"></polygon>
<path d="M14.6564885,9.77099237 L26.870229,9.77099237 L26.870229,29.3129771 L20.7633588,25.648855 L14.6564885,29.3129771 L14.6564885,9.77099237 Z M43.9694656,4.88549618 L14.6564885,4.88549618 C11.9572519,4.88549618 9.77099237,7.07175573 9.77099237,9.77099237 L9.77099237,48.8549618 C9.77099237,51.5541985 11.9572519,53.740458 14.6564885,53.740458 L43.9694656,53.740458 C46.6687023,53.740458 48.8549618,51.5541985 48.8549618,48.8549618 L48.8549618,9.77099237 C48.8549618,7.07175573 46.6687023,4.88549618 43.9694656,4.88549618 L43.9694656,4.88549618 Z" id="Fill-2" fill="#FF6D40"></path>
</g>
</g>
</g>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 26 KiB

View File

@@ -0,0 +1,29 @@
<div *ngIf="folderNode" data-automation-id="breadcrumb" class="adf-breadcrumb-container">
<li *ngFor="let item of route; let last = last"
[class.active]="last"
[ngSwitch]="last"
title="{{ item.name | translate }}"
class="adf-breadcrumb-item">
<a *ngSwitchDefault href="#" [attr.data-automation-id]="'breadcrumb_' + item.name"
class="adf-breadcrumb-item-anchor"
(click)="onRoutePathClick(item, $event)">
{{ item.name | translate }}
</a>
<div *ngSwitchCase="true" class="adf-breadcrumb-item-current">
{{ item.name | translate }}
</div>
<mat-icon class="adf-breadcrumb-item-chevron" *ngIf="!last">
chevron_right
</mat-icon>
</li>
</div>
<div *ngIf="!folderNode && hasRoot">
<li class="adf-breadcrumb-item">
<div class="adf-breadcrumb-item-current">
{{ root | translate }}
</div>
</li>
</div>

View File

@@ -0,0 +1,78 @@
$breadcrumb-chevron-spacer: 2px;
@mixin adf-breadcrumb-theme($theme) {
$primary: map-get($theme, primary);
$accent: map-get($theme, accent);
$warn: map-get($theme, warn);
.adf-breadcrumb {
display: flex;
flex: 1;
line-height: 24px;
font-size: 14px;
font-weight: 600;
letter-spacing: -0.2px;
&-container {
margin: 0;
padding: 0;
list-style-type: none;
cursor: default;
display: flex;
overflow: hidden;
height: 25px;
}
&-item {
padding-right: $breadcrumb-chevron-spacer;
overflow: hidden;
display: flex;
line-height: 24px;
font-size: 14px;
font-weight: 600;
letter-spacing: -0.2px;
text-align: left;
opacity: 0.6;
&:hover,
&.active {
opacity: 1;
}
&.active {
flex: 1 0 auto;
}
&-chevron {
opacity: 1;
}
&.mat-primary {
color: mat-color($primary);
}
&.mat-accent {
color: mat-color($accent);
}
&.mat-warn {
color: mat-color($warn);
}
&-anchor {
color: inherit;
text-decoration: none;
display: block;
width: 100%;
white-space: nowrap;
text-overflow: ellipsis;
overflow: hidden;
}
&.active {
display: block;
}
}
}
}

View File

@@ -0,0 +1,212 @@
/*!
* @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 { CUSTOM_ELEMENTS_SCHEMA, SimpleChange } from '@angular/core';
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { PathElementEntity } from 'alfresco-js-api';
import { DataTableModule } from '@alfresco/core';
import { fakeNodeWithCreatePermission } from '../mock';
import { MaterialModule } from '../material.module';
import { DocumentListService, DocumentListComponent } from '../document-list';
import { BreadcrumbComponent } from './breadcrumb.component';
declare let jasmine: any;
describe('Breadcrumb', () => {
let component: BreadcrumbComponent;
let fixture: ComponentFixture<BreadcrumbComponent>;
let element: HTMLElement;
let documentList: DocumentListComponent;
beforeEach(async(() => {
TestBed.configureTestingModule({
imports: [
DataTableModule,
MaterialModule
],
declarations: [
DocumentListComponent,
BreadcrumbComponent
],
providers: [
DocumentListService
],
schemas: [
CUSTOM_ELEMENTS_SCHEMA
]
}).compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(BreadcrumbComponent);
element = fixture.nativeElement;
component = fixture.componentInstance;
documentList = TestBed.createComponent<DocumentListComponent>(DocumentListComponent).componentInstance;
});
it('should prevent default click behavior', () => {
let event = jasmine.createSpyObj('event', ['preventDefault']);
component.onRoutePathClick(null, event);
expect(event.preventDefault).toHaveBeenCalled();
});
it('should root be present as default node if the path is null', () => {
let change = new SimpleChange(null, fakeNodeWithCreatePermission, true);
component.root = 'default';
component.ngOnChanges({'folderNode': change});
expect(component.route[0].name).toBe('default');
});
it('should emit navigation event', (done) => {
let node = <PathElementEntity> {id: '-id-', name: 'name'};
component.navigate.subscribe(val => {
expect(val).toBe(node);
done();
});
component.onRoutePathClick(node, null);
});
it('should update document list on click', (done) => {
spyOn(documentList, 'loadFolderByNodeId').and.stub();
let node = <PathElementEntity> {id: '-id-', name: 'name'};
component.target = documentList;
component.onRoutePathClick(node, null);
setTimeout(() => {
expect(documentList.loadFolderByNodeId).toHaveBeenCalledWith(node.id);
done();
}, 0);
});
it('should not parse the route when node not provided', () => {
expect(component.parseRoute(null)).toEqual([]);
});
it('should not parase the route when node has no path', () => {
const node: any = {};
expect(component.parseRoute(node)).toEqual([]);
});
it('should append the node to the route', () => {
const node: any = {
id: 'test-id',
name: 'test-name',
path: {
elements: [
{ id: 'element-id', name: 'element-name' }
]
}
};
const route = component.parseRoute(node);
expect(route.length).toBe(2);
expect(route[1].id).toBe(node.id);
expect(route[1].name).toBe(node.name);
});
it('should trim the route if custom root id provided', () => {
const node: any = {
id: 'test-id',
name: 'test-name',
path: {
elements: [
{ id: 'element-1-id', name: 'element-1-name' },
{ id: 'element-2-id', name: 'element-2-name' },
{ id: 'element-3-id', name: 'element-3-name' }
]
}
};
component.rootId = 'element-2-id';
const route = component.parseRoute(node);
expect(route.length).toBe(3);
expect(route[0].id).toBe('element-2-id');
expect(route[0].name).toBe('element-2-name');
expect(route[2].id).toBe(node.id);
expect(route[2].name).toBe(node.name);
});
it('should rename root node if custom name provided', () => {
const node: any = {
id: 'test-id',
name: 'test-name',
path: {
elements: [
{ id: 'element-1-id', name: 'element-1-name' },
{ id: 'element-2-id', name: 'element-2-name' },
{ id: 'element-3-id', name: 'element-3-name' }
]
}
};
component.root = 'custom root';
const route = component.parseRoute(node);
expect(route.length).toBe(4);
expect(route[0].id).toBe('element-1-id');
expect(route[0].name).toBe('custom root');
});
it('should replace root id if nothing to trim in the path', () => {
const node: any = {
id: 'test-id',
name: 'test-name',
path: {
elements: [
{ id: 'element-1-id', name: 'element-1-name' },
{ id: 'element-2-id', name: 'element-2-name' },
{ id: 'element-3-id', name: 'element-3-name' }
]
}
};
component.rootId = 'custom-id';
const route = component.parseRoute(node);
expect(route.length).toBe(4);
expect(route[0].id).toBe('custom-id');
expect(route[0].name).toBe('element-1-name');
});
it('should replace both id and name of the root element', () => {
const node: any = {
id: 'test-id',
name: 'test-name',
path: {
elements: [
{ id: 'element-1-id', name: 'element-1-name' },
{ id: 'element-2-id', name: 'element-2-name' },
{ id: 'element-3-id', name: 'element-3-name' }
]
}
};
component.root = 'custom-name';
component.rootId = 'custom-id';
const route = component.parseRoute(node);
expect(route.length).toBe(4);
expect(route[0].id).toBe('custom-id');
expect(route[0].name).toBe('custom-name');
});
});

View File

@@ -0,0 +1,112 @@
/*!
* @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 { Component, EventEmitter, Input, OnChanges, Output, SimpleChanges, ViewEncapsulation } from '@angular/core';
import { MinimalNodeEntryEntity, PathElementEntity } from 'alfresco-js-api';
import { DocumentListComponent } from '../document-list';
@Component({
selector: 'adf-breadcrumb',
templateUrl: './breadcrumb.component.html',
styleUrls: ['./breadcrumb.component.scss'],
encapsulation: ViewEncapsulation.None,
host: {
'class': 'adf-breadcrumb'
}
})
export class BreadcrumbComponent implements OnChanges {
@Input()
folderNode: MinimalNodeEntryEntity = null;
@Input()
root: string = null;
@Input()
rootId: string = null;
@Input()
target: DocumentListComponent;
route: PathElementEntity[] = [];
get hasRoot(): boolean {
return !!this.root;
}
@Output()
navigate: EventEmitter<PathElementEntity> = new EventEmitter<PathElementEntity>();
ngOnChanges(changes: SimpleChanges): void {
if (changes.folderNode) {
const node: MinimalNodeEntryEntity = changes.folderNode.currentValue;
this.route = this.parseRoute(node);
}
}
parseRoute(node: MinimalNodeEntryEntity): PathElementEntity[] {
if (node && node.path) {
const route = <PathElementEntity[]> (node.path.elements || []).slice();
route.push(<PathElementEntity> {
id: node.id,
name: node.name
});
const rootPos = this.getElementPosition(route, this.rootId);
if (rootPos > 0) {
route.splice(0, rootPos);
}
if (rootPos === -1 && this.rootId) {
route[0].id = this.rootId;
}
if (this.root) {
route[0].name = this.root;
}
return route;
}
return [];
}
private getElementPosition(route: PathElementEntity[], nodeId: string): number {
let result: number = -1;
if (route && route.length > 0 && nodeId) {
result = route.findIndex(el => el.id === nodeId);
}
return result;
}
onRoutePathClick(route: PathElementEntity, event?: Event): void {
if (event) {
event.preventDefault();
}
if (route) {
this.navigate.emit(route);
if (this.target) {
this.target.loadFolderByNodeId(route.id);
}
}
}
}

View File

@@ -0,0 +1,41 @@
/*!
* @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 { CommonModule } from '@angular/common';
import { NgModule } from '@angular/core';
import { MaterialModule } from '../material.module';
import { TranslateModule } from '@ngx-translate/core';
import { BreadcrumbComponent } from './breadcrumb.component';
import { DropdownBreadcrumbComponent } from './dropdown-breadcrumb.component';
@NgModule({
imports: [
CommonModule,
MaterialModule,
TranslateModule
],
exports: [
BreadcrumbComponent,
DropdownBreadcrumbComponent
],
declarations: [
BreadcrumbComponent,
DropdownBreadcrumbComponent
]
})
export class BreadcrumbModule {}

View File

@@ -0,0 +1,32 @@
<ng-container *ngIf="route.length > 0">
<button
tabindex="0"
class="adf-dropdown-breadcumb-trigger"
(click)="open()"
data-automation-id="dropdown-breadcrumb-trigger">
<mat-icon [class.isRoot]="!hasPreviousNodes()">folder</mat-icon>
</button>
<mat-icon class="adf-dropddown-breadcrumb-item-chevron">chevron_right</mat-icon>
<mat-select
*ngIf="hasPreviousNodes()"
class="adf-dropdown-breadcrumb-path"
tabindex="0"
data-automation-id="dropdown-breadcrumb-path" >
<mat-option
*ngFor="let node of previousNodes;"
(click)="onRoutePathClick(node, $event)"
class="adf-dropdown-breadcrumb-path-option"
tabindex="0"
data-automation-class="dropdown-breadcrumb-path-option">
{{ node.name }}
</mat-option>
</mat-select>
<span
class="adf-current-folder"
[class.isRoot]="!hasPreviousNodes()"
data-automation-id="current-folder">{{ currentNode.name }}</span>
</ng-container>

View File

@@ -0,0 +1,59 @@
@mixin adf-breadcrumb-dropdown-theme($theme) {
$primary: map-get($theme, primary);
$dropdownHorizontalOffset: 30px;
.adf {
&-dropdown-breadcrumb {
display: flex;
justify-content: flex-start;
height: 25px;
}
&-dropdown-breadcumb-trigger {
cursor: pointer;
padding: 0;
border: none;
background: transparent;
}
&-dropdown-breadcumb-trigger.isRoot {
cursor: not-allowed;
}
&-dropdown-breadcrumb-path {
width: 0;
height: 0;
overflow: hidden;
margin-top: 35px;
margin-left: -$dropdownHorizontalOffset;
}
&-current-folder {
margin-left: $dropdownHorizontalOffset;
line-height: 26px;
white-space: pre-wrap;
overflow: hidden;
text-overflow: ellipsis;
}
&-current-folder.isRoot {
margin-left: 0;
}
&-dropdown-breadcrumb-path-option.mat-option {
height: 28px;
line-height: 28px;
padding: 0 12px;
font-size: 13px;
}
&-dropdown-breadcrumb-path-option.mat-option:first-child {
padding-top: 4px;
}
&-dropdown-breadcrumb-path-option.mat-option:last-child {
padding-bottom: 4px;
}
}
}

View File

@@ -0,0 +1,157 @@
/*!
* @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 { CUSTOM_ELEMENTS_SCHEMA, SimpleChange } from '@angular/core';
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { By } from '@angular/platform-browser';
import { DataTableModule } from '@alfresco/core';
import { fakeNodeWithCreatePermission } from '../mock';
import { MaterialModule } from '../material.module';
import { DocumentListComponent, DocumentListService } from '../document-list';
import { DropdownBreadcrumbComponent } from './dropdown-breadcrumb.component';
describe('DropdownBreadcrumb', () => {
let component: DropdownBreadcrumbComponent;
let fixture: ComponentFixture<DropdownBreadcrumbComponent>;
let element: HTMLElement;
let documentList: DocumentListComponent;
beforeEach(async(() => {
TestBed.configureTestingModule({
imports: [
DataTableModule,
MaterialModule
],
declarations: [
DocumentListComponent,
DropdownBreadcrumbComponent
],
providers: [
DocumentListService
],
schemas: [
CUSTOM_ELEMENTS_SCHEMA
]
}).compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(DropdownBreadcrumbComponent);
element = fixture.nativeElement;
component = fixture.componentInstance;
documentList = TestBed.createComponent<DocumentListComponent>(DocumentListComponent).componentInstance;
});
function openSelect() {
const folderIcon = fixture.debugElement.query(By.css('[data-automation-id="dropdown-breadcrumb-trigger"]'));
folderIcon.triggerEventHandler('click', null);
fixture.detectChanges();
}
function triggerComponentChange(fakeNodeData) {
const change = new SimpleChange(null, fakeNodeData, true);
component.ngOnChanges({'folderNode': change});
fixture.detectChanges();
}
function clickOnTheFirstOption() {
const option = fixture.debugElement.query(By.css('[data-automation-class="dropdown-breadcrumb-path-option"]'));
option.triggerEventHandler('click', null);
}
it('should display only the current folder name if there is no previous folders', () => {
fakeNodeWithCreatePermission.path.elements = [];
triggerComponentChange(fakeNodeWithCreatePermission);
openSelect();
const currentFolder = fixture.debugElement.query(By.css('[data-automation-id="current-folder"]'));
const path = fixture.debugElement.query(By.css('[data-automation-id="dropdown-breadcrumb-path"]'));
expect(path).toBeNull();
expect(currentFolder).not.toBeNull();
expect(currentFolder.nativeElement.innerText.trim()).toEqual('Test');
});
it('should display only the path in the selectbox', () => {
fakeNodeWithCreatePermission.path.elements = [
{ id: '1', name: 'Stark Industries' },
{ id: '2', name: 'User Homes' },
{ id: '3', name: 'J.A.R.V.I.S' }
];
triggerComponentChange(fakeNodeWithCreatePermission);
openSelect();
const path = fixture.debugElement.query(By.css('[data-automation-id="dropdown-breadcrumb-path"]'));
const options = fixture.debugElement.queryAll(By.css('[data-automation-class="dropdown-breadcrumb-path-option"]'));
expect(path).not.toBeNull();
expect(options.length).toBe(3);
});
it('should display the path in reverse order', () => {
fakeNodeWithCreatePermission.path.elements = [
{ id: '1', name: 'Stark Industries' },
{ id: '2', name: 'User Homes' },
{ id: '3', name: 'J.A.R.V.I.S' }
];
triggerComponentChange(fakeNodeWithCreatePermission);
openSelect();
const options = fixture.debugElement.queryAll(By.css('[data-automation-class="dropdown-breadcrumb-path-option"]'));
expect(options.length).toBe(3);
expect(options[0].nativeElement.innerText.trim()).toBe('J.A.R.V.I.S');
expect(options[1].nativeElement.innerText.trim()).toBe('User Homes');
expect(options[2].nativeElement.innerText.trim()).toBe('Stark Industries');
});
it('should emit navigation event when clicking on an option', (done) => {
fakeNodeWithCreatePermission.path.elements = [{ id: '1', name: 'Stark Industries' }];
component.navigate.subscribe(val => {
expect(val).toEqual({ id: '1', name: 'Stark Industries' });
done();
});
triggerComponentChange(fakeNodeWithCreatePermission);
openSelect();
clickOnTheFirstOption();
});
it('should update document list when clicking on an option', () => {
spyOn(documentList, 'loadFolderByNodeId').and.stub();
component.target = documentList;
fakeNodeWithCreatePermission.path.elements = [{ id: '1', name: 'Stark Industries' }];
triggerComponentChange(fakeNodeWithCreatePermission);
openSelect();
clickOnTheFirstOption();
expect(documentList.loadFolderByNodeId).toHaveBeenCalledWith('1');
});
it('should open the selectbox when clicking on the folder icon', async(() => {
triggerComponentChange(fakeNodeWithCreatePermission);
spyOn(component.selectbox, 'open');
openSelect();
expect(component.selectbox.open).toHaveBeenCalled();
}));
});

View File

@@ -0,0 +1,68 @@
/*!
* @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 { Component, OnChanges, SimpleChanges, ViewChild, ViewEncapsulation } from '@angular/core';
import { MatSelect } from '@angular/material';
import { PathElementEntity } from 'alfresco-js-api';
import { BreadcrumbComponent } from './breadcrumb.component';
@Component({
selector: 'adf-dropdown-breadcrumb',
templateUrl: './dropdown-breadcrumb.component.html',
styleUrls: ['./dropdown-breadcrumb.component.scss'],
encapsulation: ViewEncapsulation.None,
host: {
'class': 'adf-dropdown-breadcrumb'
}
})
export class DropdownBreadcrumbComponent extends BreadcrumbComponent implements OnChanges {
@ViewChild(MatSelect)
selectbox: MatSelect;
currentNode: PathElementEntity;
previousNodes: PathElementEntity[];
ngOnChanges(changes: SimpleChanges): void {
super.ngOnChanges(changes);
this.recalculateNodes();
}
/**
* Calculate the current and previous nodes from the route array
*/
private recalculateNodes(): void {
this.currentNode = this.route[this.route.length - 1];
this.previousNodes = this.route.slice(0, this.route.length - 1).reverse();
}
/**
* Opens the selectbox overlay
*/
open(): void {
if (this.selectbox) {
this.selectbox.open();
}
}
/**
* Return if route has more than one element (means: we are not in the root directory)
*/
hasPreviousNodes(): boolean {
return this.previousNodes.length > 0;
}
}

View File

@@ -0,0 +1,18 @@
/*!
* @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.
*/
export * from './public-api';

View File

@@ -0,0 +1,21 @@
/*!
* @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.
*/
export * from './breadcrumb.component';
export * from './dropdown-breadcrumb.component';
export * from './breadcrumb.module';

View File

@@ -0,0 +1,99 @@
<header matDialogTitle
class="adf-content-node-selector-title"
data-automation-id="content-node-selector-title">{{title}}</header>
<section matDialogContent
class="adf-content-node-selector-content"
(node-select)="onNodeSelect($event)">
<mat-form-field floatPlaceholder="never" class="adf-content-node-selector-content-input">
<input #searchInput
matInput
placeholder="Search"
(input)="search(searchInput.value)"
[value]="searchTerm"
data-automation-id="content-node-selector-search-input">
<mat-icon *ngIf="searchTerm.length > 0"
matSuffix (click)="clear()"
class="adf-content-node-selector-content-input-icon"
data-automation-id="content-node-selector-search-clear">clear</mat-icon>
<mat-icon *ngIf="searchTerm.length === 0"
matSuffix
class="adf-content-node-selector-content-input-icon"
data-automation-id="content-node-selector-search-icon">search</mat-icon>
</mat-form-field>
<adf-sites-dropdown
(change)="siteChanged($event)"
data-automation-id="content-node-selector-sites-combo"></adf-sites-dropdown>
<adf-toolbar>
<adf-toolbar-title>
<adf-dropdown-breadcrumb *ngIf="needBreadcrumbs()"
class="adf-content-node-selector-content-breadcrumb"
[target]="documentList"
[folderNode]="breadcrumbFolderNode"
data-automation-id="content-node-selector-content-breadcrumb">
</adf-dropdown-breadcrumb>
</adf-toolbar-title>
</adf-toolbar>
<div class="adf-content-node-selector-content-list" data-automation-id="content-node-selector-content-list">
<adf-document-list
#documentList
adf-highlight
adf-highlight-selector=".cell-value adf-datatable-cell .adf-datatable-cell-value"
[node]="nodes"
[rowFilter]="rowFilter"
[imageResolver]="imageResolver"
[currentFolderId]="folderIdToShow"
selectionMode="single"
[contextMenuActions]="false"
[contentActions]="false"
[allowDropFiles]="false"
[enablePagination]="!showingSearchResults"
paginationStrategy="{{paginationStrategy}}"
[pageSize]="pageSize"
(folderChange)="onFolderChange()"
(ready)="onFolderLoaded()"
data-automation-id="content-node-selector-document-list">
<empty-folder-content>
<ng-template>
<div>{{ 'NODE_SELECTOR.NO_RESULTS' | translate }}</div>
</ng-template>
</empty-folder-content>
</adf-document-list>
<adf-infinite-pagination
*ngIf="showingSearchResults && isSearchTermLongEnough()"
[pagination]="pagination"
[pageSize]="pageSize"
[loading]="loadingSearchResults"
(loadMore)="getNextPageOfSearch($event)"
data-automation-id="content-node-selector-search-pagination">
{{ 'ADF-DOCUMENT-LIST.LAYOUT.LOAD_MORE' | translate }}
</adf-infinite-pagination>
</div>
</section>
<footer matDialogActions class="adf-content-node-selector-actions">
<button *ngIf="inDialog"
mat-button
class="adf-content-node-selector-actions-cancel"
(click)="close()"
data-automation-id="content-node-selector-actions-cancel">{{ 'NODE_SELECTOR.CANCEL' | translate }}
</button>
<button mat-button
[disabled]="!chosenNode"
class="adf-content-node-selector-actions-choose"
(click)="choose()"
data-automation-id="content-node-selector-actions-choose">{{ 'NODE_SELECTOR.CHOOSE' | translate }}
</button>
</footer>

View File

@@ -0,0 +1,123 @@
@mixin adf-content-node-selector-theme($theme) {
$primary: map-get($theme, primary);
$accent: map-get($theme, accent);
.adf-content-node-selector-dialog {
.mat-dialog-container {
padding: 0;
}
.adf-content-node-selector {
&-title,
&-content,
&-actions {
padding: 16px;
margin: 0;
}
&-title::first-letter {
text-transform: uppercase;
}
&-content {
padding-top: 0;
&-input {
width: 100%;
&-icon {
color: rgba(0, 0, 0, 0.38);
cursor: pointer;
&:hover {
color: rgba(0, 0, 0, 1);
}
}
}
.mat-input-underline .mat-input-ripple {
height: 1px;
transition: none;
}
.adf-site-dropdown-list-element {
width: 100%;
margin-bottom: 20px;
.mat-select-trigger {
font-size: 14px;
}
}
.adf-toolbar .mat-toolbar {
border: none;
}
&-list {
height: 200px;
overflow: auto;
border: 1px solid rgba(0, 0, 0, 0.07);
.adf-highlight {
color: mat-color($accent);;
}
.adf-data-table {
border: none;
.adf-no-content-container {
text-align: center;
}
thead {
display: none;
}
.adf-data-table-cell {
padding-top: 8px;
padding-bottom: 8px;
border-top: none;
height: 30px;
}
tbody tr {
height: auto !important;
&:last-child {
.adf-data-table-cell {
border-bottom: none;
}
}
}
}
}
}
&-actions {
padding: 8px;
background-color: rgb(250, 250, 250);
display: flex;
justify-content: flex-end;
color: rgb(121, 121, 121);
&:last-child {
margin-bottom: 0px;
}
&-cancel {
font-weight: normal;
}
&-choose {
font-weight: normal;
&[disabled] {
opacity: 0.6;
}
}
}
}
}
}

View File

@@ -0,0 +1,689 @@
/*!
* @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 { CUSTOM_ELEMENTS_SCHEMA, DebugElement, EventEmitter } from '@angular/core';
import { async, ComponentFixture, fakeAsync, TestBed, tick } from '@angular/core/testing';
import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material';
import { By } from '@angular/platform-browser';
import { MinimalNodeEntryEntity } from 'alfresco-js-api';
import { ContentService, TranslationService, SearchService, SiteModel, SitesApiService } from '@alfresco/core';
import { DataTableModule } from '@alfresco/core';
import { Observable } from 'rxjs/Rx';
import { MaterialModule } from '../material.module';
import { EmptyFolderContentDirective, DocumentListComponent, DocumentListService } from '../document-list';
import { DropdownSitesComponent } from '../site-dropdown';
import { DropdownBreadcrumbComponent } from '../breadcrumb';
import { ContentNodeSelectorComponent } from './content-node-selector.component';
import { ContentNodeSelectorService } from './content-node-selector.service';
const ONE_FOLDER_RESULT = {
list: {
entries: [
{
entry: {
id: '123', name: 'MyFolder', isFile: false, isFolder: true,
createdByUser: { displayName: 'John Doe' },
modifiedByUser: { displayName: 'John Doe' }
}
}
],
pagination: {
hasMoreItems: true
}
}
};
describe('ContentNodeSelectorComponent', () => {
let component: ContentNodeSelectorComponent;
let fixture: ComponentFixture<ContentNodeSelectorComponent>;
let element: DebugElement;
let data: any;
let searchService: SearchService;
let searchSpy: jasmine.Spy;
let _resolve: Function;
let _reject: Function;
function typeToSearchBox(searchTerm = 'string-to-search') {
let searchInput = fixture.debugElement.query(By.css('[data-automation-id="content-node-selector-search-input"]'));
searchInput.nativeElement.value = searchTerm;
searchInput.triggerEventHandler('input', {});
fixture.detectChanges();
}
function respondWithSearchResults(result) {
_resolve(result);
}
function setupTestbed(plusProviders) {
TestBed.configureTestingModule({
imports: [
DataTableModule,
MaterialModule
],
declarations: [
DocumentListComponent,
EmptyFolderContentDirective,
DropdownSitesComponent,
DropdownBreadcrumbComponent,
ContentNodeSelectorComponent
],
providers: [
ContentService,
SitesApiService,
TranslationService,
DocumentListService,
SearchService,
ContentNodeSelectorService,
...plusProviders
],
schemas: [CUSTOM_ELEMENTS_SCHEMA]
});
}
afterEach(() => {
fixture.destroy();
TestBed.resetTestingModule();
});
describe('Dialog features', () => {
beforeEach(async(() => {
data = {
title: 'Move along citizen...',
select: new EventEmitter<MinimalNodeEntryEntity>(),
rowFilter: () => {},
imageResolver: () => 'piccolo',
currentFolderId: 'cat-girl-nuku-nuku'
};
setupTestbed([{ provide: MAT_DIALOG_DATA, useValue: data }]);
TestBed.compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(ContentNodeSelectorComponent);
element = fixture.debugElement;
component = fixture.componentInstance;
fixture.detectChanges();
});
describe('Data injecting with the "Material dialog way"', () => {
it('should show the INJECTED title', () => {
const titleElement = fixture.debugElement.query(By.css('[data-automation-id="content-node-selector-title"]'));
expect(titleElement).not.toBeNull();
expect(titleElement.nativeElement.innerText).toBe('Move along citizen...');
});
it('should pass through the injected currentFolderId to the documentlist', () => {
let documentList = fixture.debugElement.query(By.directive(DocumentListComponent));
expect(documentList).not.toBeNull('Document list should be shown');
expect(documentList.componentInstance.currentFolderId).toBe('cat-girl-nuku-nuku');
});
it('should pass through the injected rowFilter to the documentlist', () => {
let documentList = fixture.debugElement.query(By.directive(DocumentListComponent));
expect(documentList).not.toBeNull('Document list should be shown');
expect(documentList.componentInstance.rowFilter).toBe(data.rowFilter);
});
it('should pass through the injected imageResolver to the documentlist', () => {
let documentList = fixture.debugElement.query(By.directive(DocumentListComponent));
expect(documentList).not.toBeNull('Document list should be shown');
expect(documentList.componentInstance.imageResolver).toBe(data.imageResolver);
});
it('should trigger the INJECTED select event when selection has been made', (done) => {
const expectedNode = <MinimalNodeEntryEntity> {};
data.select.subscribe((nodes) => {
expect(nodes.length).toBe(1);
expect(nodes[0]).toBe(expectedNode);
done();
});
component.chosenNode = expectedNode;
component.choose();
});
});
describe('Cancel button', () => {
let dummyMdDialogRef;
beforeEach(() => {
dummyMdDialogRef = <MatDialogRef<ContentNodeSelectorComponent>> { close: () => {} };
});
it('should be shown if dialogRef is injected', () => {
const componentInstance = new ContentNodeSelectorComponent(null, null, data, dummyMdDialogRef);
expect(componentInstance.inDialog).toBeTruthy();
});
it('should should call the close method in the injected dialogRef', () => {
spyOn(dummyMdDialogRef, 'close');
const componentInstance = new ContentNodeSelectorComponent(null, null, data, dummyMdDialogRef);
componentInstance.close();
expect(dummyMdDialogRef.close).toHaveBeenCalled();
});
});
});
describe('General component features', () => {
beforeEach(async(() => {
setupTestbed([]);
TestBed.compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(ContentNodeSelectorComponent);
element = fixture.debugElement;
component = fixture.componentInstance;
searchService = TestBed.get(SearchService);
searchSpy = spyOn(searchService, 'getQueryNodesPromise').and.callFake(() => {
return new Promise((resolve, reject) => {
_resolve = resolve;
_reject = reject;
});
});
});
describe('Parameters', () => {
it('should show the title', () => {
component.title = 'Move along citizen...';
fixture.detectChanges();
const titleElement = fixture.debugElement.query(By.css('[data-automation-id="content-node-selector-title"]'));
expect(titleElement).not.toBeNull();
expect(titleElement.nativeElement.innerText).toBe('Move along citizen...');
});
it('should trigger the select event when selection has been made', (done) => {
const expectedNode = <MinimalNodeEntryEntity> {};
component.select.subscribe((nodes) => {
expect(nodes.length).toBe(1);
expect(nodes[0]).toBe(expectedNode);
done();
});
component.chosenNode = expectedNode;
component.choose();
});
});
describe('Breadcrumbs', () => {
let documentListService,
sitesApiService,
expectedDefaultFolderNode;
beforeEach(() => {
expectedDefaultFolderNode = <MinimalNodeEntryEntity> { path: { elements: [] } };
documentListService = TestBed.get(DocumentListService);
sitesApiService = TestBed.get(SitesApiService);
spyOn(documentListService, 'getFolderNode').and.returnValue(Promise.resolve(expectedDefaultFolderNode));
spyOn(documentListService, 'getFolder').and.returnValue(Observable.throw('No results for test'));
spyOn(sitesApiService, 'getSites').and.returnValue(Observable.of([]));
spyOn(component.documentList, 'loadFolderNodesByFolderNodeId').and.returnValue(Promise.resolve());
component.currentFolderId = 'cat-girl-nuku-nuku';
fixture.detectChanges();
});
it('should show the breadcrumb for the currentFolderId by default', (done) => {
fixture.detectChanges();
fixture.whenStable().then(() => {
fixture.detectChanges();
const breadcrumb = fixture.debugElement.query(By.directive(DropdownBreadcrumbComponent));
expect(breadcrumb).not.toBeNull();
expect(breadcrumb.componentInstance.folderNode).toBe(expectedDefaultFolderNode);
done();
});
});
it('should not show the breadcrumb if search was performed as last action', (done) => {
typeToSearchBox();
respondWithSearchResults(ONE_FOLDER_RESULT);
fixture.whenStable().then(() => {
fixture.detectChanges();
const breadcrumb = fixture.debugElement.query(By.directive(DropdownBreadcrumbComponent));
expect(breadcrumb).toBeNull();
done();
});
});
it('should show the breadcrumb again on folder navigation in the results list', (done) => {
typeToSearchBox();
respondWithSearchResults(ONE_FOLDER_RESULT);
fixture.whenStable().then(() => {
fixture.detectChanges();
component.onFolderChange();
fixture.detectChanges();
const breadcrumb = fixture.debugElement.query(By.directive(DropdownBreadcrumbComponent));
expect(breadcrumb).not.toBeNull();
done();
});
});
it('should show the breadcrumb for the selected node when search results are displayed', (done) => {
const alfrescoContentService = TestBed.get(ContentService);
spyOn(alfrescoContentService, 'hasPermission').and.returnValue(true);
typeToSearchBox();
respondWithSearchResults(ONE_FOLDER_RESULT);
fixture.whenStable().then(() => {
fixture.detectChanges();
const chosenNode = <MinimalNodeEntryEntity> { path: { elements: [] } };
component.onNodeSelect({ detail: { node: { entry: chosenNode} } });
fixture.detectChanges();
const breadcrumb = fixture.debugElement.query(By.directive(DropdownBreadcrumbComponent));
expect(breadcrumb).not.toBeNull();
expect(breadcrumb.componentInstance.folderNode).toBe(chosenNode);
done();
});
});
it('should NOT show the breadcrumb for the selected node when not on search results list', (done) => {
const alfrescoContentService = TestBed.get(ContentService);
spyOn(alfrescoContentService, 'hasPermission').and.returnValue(true);
typeToSearchBox();
respondWithSearchResults(ONE_FOLDER_RESULT);
fixture.whenStable().then(() => {
fixture.detectChanges();
component.onFolderChange();
fixture.detectChanges();
const chosenNode = <MinimalNodeEntryEntity> { path: { elements: [] } };
component.onNodeSelect({ detail: { node: { entry: chosenNode} } });
fixture.detectChanges();
const breadcrumb = fixture.debugElement.query(By.directive(DropdownBreadcrumbComponent));
expect(breadcrumb).not.toBeNull();
expect(breadcrumb.componentInstance.folderNode).toBe(expectedDefaultFolderNode);
done();
});
});
});
describe('Search functionality', () => {
function defaultSearchOptions(rootNodeId = undefined, skipCount = 0) {
return {
include: ['path', 'allowableOperations'],
skipCount,
rootNodeId,
nodeType: 'cm:folder',
maxItems: 10,
orderBy: null
};
}
beforeEach(() => {
const documentListService = TestBed.get(DocumentListService);
const expectedDefaultFolderNode = <MinimalNodeEntryEntity> { path: { elements: [] } };
spyOn(documentListService, 'getFolderNode').and.returnValue(Promise.resolve(expectedDefaultFolderNode));
spyOn(component.documentList, 'loadFolderNodesByFolderNodeId').and.returnValue(Promise.resolve());
component.currentFolderId = 'cat-girl-nuku-nuku';
fixture.detectChanges();
});
it('should load the results by calling the search api on search change', () => {
typeToSearchBox('kakarot');
expect(searchSpy).toHaveBeenCalledWith('kakarot*', defaultSearchOptions());
});
it('should reset the currently chosen node in case of starting a new search', () => {
component.chosenNode = <MinimalNodeEntryEntity> {};
typeToSearchBox('kakarot');
expect(component.chosenNode).toBeNull();
});
it('should NOT call the search api if the searchTerm length is less than 4 characters', () => {
typeToSearchBox('1');
typeToSearchBox('12');
typeToSearchBox('123');
expect(searchSpy).not.toHaveBeenCalled();
});
xit('should debounce the search call by 500 ms', () => {
});
it('should call the search api on changing the site selectbox\'s value', () => {
typeToSearchBox('vegeta');
expect(searchSpy.calls.count()).toBe(1, 'Search count should be one after only one search');
component.siteChanged(<SiteModel> { guid: 'namek' });
expect(searchSpy.calls.count()).toBe(2, 'Search count should be two after the site change');
expect(searchSpy.calls.argsFor(1)).toEqual(['vegeta*', defaultSearchOptions('namek') ]);
});
it('should show the search icon by default without the X (clear) icon', () => {
fixture.detectChanges();
let searchIcon = fixture.debugElement.query(By.css('[data-automation-id="content-node-selector-search-icon"]'));
let clearIcon = fixture.debugElement.query(By.css('[data-automation-id="content-node-selector-search-clear"]'));
expect(searchIcon).not.toBeNull('Search icon should be in the DOM');
expect(clearIcon).toBeNull('Clear icon should NOT be in the DOM');
});
it('should show the X (clear) icon without the search icon when the search contains at least one character', () => {
fixture.detectChanges();
typeToSearchBox('123');
let searchIcon = fixture.debugElement.query(By.css('[data-automation-id="content-node-selector-search-icon"]'));
let clearIcon = fixture.debugElement.query(By.css('[data-automation-id="content-node-selector-search-clear"]'));
expect(searchIcon).toBeNull('Search icon should NOT be in the DOM');
expect(clearIcon).not.toBeNull('Clear icon should be in the DOM');
});
it('should clear the search field, nodes and chosenNode when clicking on the X (clear) icon', () => {
component.chosenNode = <MinimalNodeEntryEntity> {};
component.nodes = { list: {
entries: [ { entry: component.chosenNode } ]
} };
component.searchTerm = 'piccolo';
component.showingSearchResults = true;
component.clear();
expect(component.searchTerm).toBe('');
expect(component.nodes).toEqual(null);
expect(component.chosenNode).toBeNull();
expect(component.showingSearchResults).toBeFalsy();
});
it('should show the current folder\'s content instead of search results if search was not performed', () => {
let documentList = fixture.debugElement.query(By.directive(DocumentListComponent));
expect(documentList).not.toBeNull('Document list should be shown');
expect(documentList.componentInstance.currentFolderId).toBe('cat-girl-nuku-nuku');
});
it('should pass through the rowFilter to the documentList', () => {
const filter = () => {};
component.rowFilter = filter;
fixture.detectChanges();
let documentList = fixture.debugElement.query(By.directive(DocumentListComponent));
expect(documentList).not.toBeNull('Document list should be shown');
expect(documentList.componentInstance.rowFilter).toBe(filter);
});
it('should pass through the imageResolver to the documentList', () => {
const resolver = () => 'piccolo';
component.imageResolver = resolver;
fixture.detectChanges();
let documentList = fixture.debugElement.query(By.directive(DocumentListComponent));
expect(documentList).not.toBeNull('Document list should be shown');
expect(documentList.componentInstance.imageResolver).toBe(resolver);
});
it('should show the result list when search was performed', async(() => {
typeToSearchBox();
respondWithSearchResults(ONE_FOLDER_RESULT);
fixture.whenStable().then(() => {
fixture.detectChanges();
let documentList = fixture.debugElement.query(By.css('[data-automation-id="content-node-selector-document-list"]'));
expect(documentList).not.toBeNull('Document list should be shown');
expect(documentList.componentInstance.currentFolderId).toBeUndefined();
});
}));
it('should highlight the results when search was performed in the next timeframe', fakeAsync(() => {
spyOn(component.highlighter, 'highlight');
typeToSearchBox('shenron');
respondWithSearchResults(ONE_FOLDER_RESULT);
expect(component.highlighter.highlight).not.toHaveBeenCalled();
tick();
expect(component.highlighter.highlight).toHaveBeenCalledWith('shenron');
}));
it('should show the default text instead of result list if search was cleared', async(() => {
typeToSearchBox();
respondWithSearchResults(ONE_FOLDER_RESULT);
fixture.whenStable().then(() => {
fixture.detectChanges();
let clearButton = fixture.debugElement.query(By.css('[data-automation-id="content-node-selector-search-clear"]'));
expect(clearButton).not.toBeNull('Clear button should be in DOM');
clearButton.triggerEventHandler('click', {});
fixture.detectChanges();
let documentList = fixture.debugElement.query(By.css('[data-automation-id="content-node-selector-document-list"]'));
expect(documentList).not.toBeNull('Document list should be shown');
expect(documentList.componentInstance.currentFolderId).toBe('cat-girl-nuku-nuku');
});
}));
it('should reload the original documentlist when clearing the search input', async(() => {
typeToSearchBox('shenron');
respondWithSearchResults(ONE_FOLDER_RESULT);
fixture.whenStable().then(() => {
typeToSearchBox('');
fixture.detectChanges();
let documentList = fixture.debugElement.query(By.css('[data-automation-id="content-node-selector-document-list"]'));
expect(documentList.componentInstance.currentFolderId).toBe('cat-girl-nuku-nuku');
});
}));
it('should set the folderIdToShow to the default "currentFolderId" if siteId is undefined', () => {
component.siteChanged(<SiteModel> { guid: 'Kame-Sennin Muten Roshi' });
fixture.detectChanges();
let documentList = fixture.debugElement.query(By.css('[data-automation-id="content-node-selector-document-list"]'));
expect(documentList.componentInstance.currentFolderId).toBe('Kame-Sennin Muten Roshi');
component.siteChanged(<SiteModel> { guid: undefined });
fixture.detectChanges();
documentList = fixture.debugElement.query(By.css('[data-automation-id="content-node-selector-document-list"]'));
expect(documentList.componentInstance.currentFolderId).toBe('cat-girl-nuku-nuku');
});
describe('Pagination "Load more" button', () => {
it('should NOT be shown by default', () => {
fixture.detectChanges();
const pagination = fixture.debugElement.query(By.css('[data-automation-id="content-node-selector-search-pagination"]'));
expect(pagination).toBeNull();
});
it('should be shown when diplaying search results', async(() => {
typeToSearchBox('shenron');
respondWithSearchResults(ONE_FOLDER_RESULT);
fixture.whenStable().then(() => {
fixture.detectChanges();
const pagination = fixture.debugElement.query(By.css('[data-automation-id="content-node-selector-search-pagination"]'));
expect(pagination).not.toBeNull();
});
}));
it('should NOT be shown when modifying searchTerm to be less then 4 characters after search results have been diplayed', async(() => {
typeToSearchBox('shenron');
respondWithSearchResults(ONE_FOLDER_RESULT);
fixture.whenStable().then(() => {
typeToSearchBox('sh');
fixture.detectChanges();
const pagination = fixture.debugElement.query(By.css('[data-automation-id="content-node-selector-search-pagination"]'));
expect(pagination).toBeNull();
});
}));
it('button\'s callback should load the next batch of results by calling the search api', () => {
const skipCount = 8;
component.searchTerm = 'kakarot';
component.getNextPageOfSearch({skipCount});
expect(searchSpy).toHaveBeenCalledWith('kakarot*', defaultSearchOptions(undefined, skipCount));
});
it('should set its loading state to true after search was started', () => {
component.showingSearchResults = true;
component.pagination = { hasMoreItems: true };
typeToSearchBox('shenron');
fixture.detectChanges();
const spinnerSelector = By.css('[data-automation-id="content-node-selector-search-pagination"] [data-automation-id="adf-infinite-pagination-spinner"]');
const paginationLoading = fixture.debugElement.query(spinnerSelector);
expect(paginationLoading).not.toBeNull();
});
it('should set its loading state to true after search was performed', async(() => {
component.showingSearchResults = true;
component.pagination = { hasMoreItems: true };
typeToSearchBox('shenron');
fixture.detectChanges();
respondWithSearchResults(ONE_FOLDER_RESULT);
fixture.whenStable().then(() => {
fixture.detectChanges();
const spinnerSelector = By.css('[data-automation-id="content-node-selector-search-pagination"] [data-automation-id="adf-infinite-pagination-spinner"]');
const paginationLoading = fixture.debugElement.query(spinnerSelector);
expect(paginationLoading).toBeNull();
});
}));
});
xit('should trigger some kind of error when error happened during search', () => {
});
});
describe('Cancel button', () => {
it('should not be shown if dialogRef is NOT injected', () => {
const closeButton = fixture.debugElement.query(By.css('[content-node-selector-actions-cancel]'));
expect(closeButton).toBeNull();
});
});
describe('Choose button', () => {
const entry: MinimalNodeEntryEntity = <MinimalNodeEntryEntity> {};
let hasPermission;
beforeEach(() => {
const alfrescoContentService = TestBed.get(ContentService);
spyOn(alfrescoContentService, 'hasPermission').and.callFake(() => hasPermission);
});
it('should be disabled by default', () => {
fixture.detectChanges();
let chooseButton = fixture.debugElement.query(By.css('[data-automation-id="content-node-selector-actions-choose"]'));
expect(chooseButton.nativeElement.disabled).toBe(true);
});
it('should become enabled after loading node with the necessary permissions', () => {
hasPermission = true;
component.documentList.folderNode = entry;
component.documentList.ready.emit();
fixture.detectChanges();
let chooseButton = fixture.debugElement.query(By.css('[data-automation-id="content-node-selector-actions-choose"]'));
expect(chooseButton.nativeElement.disabled).toBe(false);
});
it('should remain disabled after loading node without the necessary permissions', () => {
hasPermission = false;
component.documentList.folderNode = entry;
component.documentList.ready.emit();
fixture.detectChanges();
let chooseButton = fixture.debugElement.query(By.css('[data-automation-id="content-node-selector-actions-choose"]'));
expect(chooseButton.nativeElement.disabled).toBe(true);
});
it('should be enabled when clicking on a node (with the right permissions) in the list (onNodeSelect)', () => {
hasPermission = true;
component.onNodeSelect({ detail: { node: { entry } } });
fixture.detectChanges();
let chooseButton = fixture.debugElement.query(By.css('[data-automation-id="content-node-selector-actions-choose"]'));
expect(chooseButton.nativeElement.disabled).toBe(false);
});
it('should remain disabled when clicking on a node (with the WRONG permissions) in the list (onNodeSelect)', () => {
hasPermission = false;
component.onNodeSelect({ detail: { node: { entry } } });
fixture.detectChanges();
let chooseButton = fixture.debugElement.query(By.css('[data-automation-id="content-node-selector-actions-choose"]'));
expect(chooseButton.nativeElement.disabled).toBe(true);
});
it('should become disabled when clicking on a node (with the WRONG permissions) after previously selecting a right node', () => {
hasPermission = true;
component.onNodeSelect({ detail: { node: { entry } } });
fixture.detectChanges();
hasPermission = false;
component.onNodeSelect({ detail: { node: { entry } } });
fixture.detectChanges();
let chooseButton = fixture.debugElement.query(By.css('[data-automation-id="content-node-selector-actions-choose"]'));
expect(chooseButton.nativeElement.disabled).toBe(true);
});
it('should be disabled when resetting the chosen node', () => {
hasPermission = true;
component.onNodeSelect({ detail: { node: { entry: <MinimalNodeEntryEntity> {} } } });
fixture.detectChanges();
component.resetChosenNode();
fixture.detectChanges();
let chooseButton = fixture.debugElement.query(By.css('[data-automation-id="content-node-selector-actions-choose"]'));
expect(chooseButton.nativeElement.disabled).toBe(true);
});
});
});
});

View File

@@ -0,0 +1,291 @@
/*!
* @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 { Component, EventEmitter, Inject, Input, OnInit, Optional, Output, ViewChild, ViewEncapsulation } from '@angular/core';
import { ContentService, HighlightDirective, SiteModel } from '@alfresco/core';
import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material';
import { MinimalNodeEntryEntity, NodePaging, Pagination } from 'alfresco-js-api';
import { DocumentListComponent, PaginationStrategy } from '../document-list/components/document-list.component';
import { RowFilter } from '../document-list/data/row-filter.model';
import { ImageResolver } from '../document-list/data/image-resolver.model';
import { ContentNodeSelectorService } from './content-node-selector.service';
export interface ContentNodeSelectorComponentData {
title: string;
currentFolderId?: string;
rowFilter?: RowFilter;
imageResolver?: ImageResolver;
select: EventEmitter<MinimalNodeEntryEntity[]>;
}
@Component({
selector: 'adf-content-node-selector',
styleUrls: ['./content-node-selector.component.scss'],
templateUrl: './content-node-selector.component.html',
encapsulation: ViewEncapsulation.None
})
export class ContentNodeSelectorComponent implements OnInit {
nodes: NodePaging | null = null;
siteId: null | string;
searchTerm: string = '';
showingSearchResults: boolean = false;
loadingSearchResults: boolean = false;
inDialog: boolean = false;
chosenNode: MinimalNodeEntryEntity | null = null;
folderIdToShow: string | null = null;
paginationStrategy: PaginationStrategy;
pagination: Pagination;
skipCount: number = 0;
@Input()
title: string;
@Input()
currentFolderId: string | null = null;
@Input()
rowFilter: RowFilter = null;
@Input()
imageResolver: ImageResolver = null;
@Input()
pageSize: number = 10;
@Output()
select: EventEmitter<MinimalNodeEntryEntity[]> = new EventEmitter<MinimalNodeEntryEntity[]>();
@ViewChild(DocumentListComponent)
documentList: DocumentListComponent;
@ViewChild(HighlightDirective)
highlighter: HighlightDirective;
constructor(private contentNodeSelectorService: ContentNodeSelectorService,
private contentService: ContentService,
@Optional() @Inject(MAT_DIALOG_DATA) data?: ContentNodeSelectorComponentData,
@Optional() private containingDialog?: MatDialogRef<ContentNodeSelectorComponent>) {
if (data) {
this.title = data.title;
this.select = data.select;
this.currentFolderId = data.currentFolderId;
this.rowFilter = data.rowFilter;
this.imageResolver = data.imageResolver;
}
if (this.containingDialog) {
this.inDialog = true;
}
}
ngOnInit() {
this.folderIdToShow = this.currentFolderId;
this.paginationStrategy = PaginationStrategy.Infinite;
}
/**
* Updates the site attribute and starts a new search
*
* @param chosenSite Sitemodel to search within
*/
siteChanged(chosenSite: SiteModel): void {
this.siteId = chosenSite.guid;
this.updateResults();
}
/**
* Updates the searchTerm attribute and starts a new search
*
* @param searchTerm string value to search against
*/
search(searchTerm: string): void {
this.searchTerm = searchTerm;
this.updateResults();
}
/**
* Returns whether breadcrumb has to be shown or not
*/
needBreadcrumbs() {
const whenInFolderNavigation = !this.showingSearchResults,
whenInSelectingSearchResult = this.showingSearchResults && this.chosenNode;
return whenInFolderNavigation || whenInSelectingSearchResult;
}
/**
* Returns the actually selected|entered folder node or null in case of searching for the breadcrumb
*/
get breadcrumbFolderNode(): MinimalNodeEntryEntity | null {
if (this.showingSearchResults && this.chosenNode) {
return this.chosenNode;
} else {
return this.documentList.folderNode;
}
}
/**
* Clear the search input
*/
clear(): void {
this.searchTerm = '';
this.nodes = null;
this.skipCount = 0;
this.chosenNode = null;
this.showingSearchResults = false;
this.folderIdToShow = this.currentFolderId;
}
/**
* Update the result list depending on the criterias
*/
private updateResults(): void {
if (this.searchTerm.length === 0) {
this.folderIdToShow = this.siteId || this.currentFolderId;
} else {
this.startNewSearch();
}
}
/**
* Load the first page of a new search result
*/
private startNewSearch(): void {
this.nodes = null;
this.skipCount = 0;
this.chosenNode = null;
this.folderIdToShow = null;
this.querySearch();
}
/**
* Loads the next batch of search results
*
* @param event Pagination object
*/
getNextPageOfSearch(event: Pagination): void {
this.skipCount = event.skipCount;
this.querySearch();
}
/**
* Perform the call to searchService with the proper parameters
*/
private querySearch(): void {
if (this.isSearchTermLongEnough()) {
this.loadingSearchResults = true;
this.contentNodeSelectorService.search(this.searchTerm, this.siteId, this.skipCount, this.pageSize)
.subscribe(this.showSearchResults.bind(this));
}
}
/**
* Show the results of the search
*
* @param results Search results
*/
private showSearchResults(results: NodePaging): void {
this.showingSearchResults = true;
this.loadingSearchResults = false;
// Documentlist hack, since data displaying for preloaded nodes is a little bit messy there
if (!this.nodes) {
this.nodes = results;
} else {
this.documentList.data.loadPage(results, true);
}
this.pagination = results.list.pagination;
this.highlight();
}
/**
* Predicate method to decide whether searchTerm fulfills the necessary criteria
*/
isSearchTermLongEnough(): boolean {
return this.searchTerm.length > 3;
}
/**
* Hightlight the actual searchterm in the next frame
*/
highlight(): void {
setTimeout(() => {
this.highlighter.highlight(this.searchTerm);
}, 0);
}
/**
* Invoked when user selects a node
*
* @param event CustomEvent for node-select
*/
onNodeSelect(event: any): void {
this.attemptNodeSelection(event.detail.node.entry);
}
/**
* Sets showingSearchResults state to be able to differentiate between search results or folder results
*/
onFolderChange(): void {
this.showingSearchResults = false;
}
/**
* Attempts to set the currently loaded node
*/
onFolderLoaded(): void {
this.attemptNodeSelection(this.documentList.folderNode);
}
/**
* Selects node as chosen if it has the right permission, clears the selection otherwise
*
* @param entry
*/
private attemptNodeSelection(entry: MinimalNodeEntryEntity): void {
if (this.contentService.hasPermission(entry, 'create')) {
this.chosenNode = entry;
} else {
this.resetChosenNode();
}
}
/**
* Clears the chosen node
*/
resetChosenNode(): void {
this.chosenNode = null;
}
/**
* Emit event with the chosen node
*/
choose(): void {
this.select.next([this.chosenNode]);
}
/**
* Close the dialog
*/
close(): void {
this.containingDialog.close();
}
}

View File

@@ -0,0 +1,54 @@
/*!
* @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 { CommonModule } from '@angular/common';
import { NgModule } from '@angular/core';
import { MaterialModule } from '../material.module';
import { TranslateModule } from '@ngx-translate/core';
import { ContentNodeSelectorComponent } from './content-node-selector.component';
import { ContentNodeSelectorService } from './content-node-selector.service';
import { SitesDropdownModule } from '../site-dropdown';
import { BreadcrumbModule } from '../breadcrumb';
import { PaginationModule, ToolbarModule } from '@alfresco/core';
import { DocumentListModule } from '../document-list';
@NgModule({
imports: [
CommonModule,
MaterialModule,
TranslateModule,
SitesDropdownModule,
BreadcrumbModule,
ToolbarModule,
DocumentListModule,
PaginationModule
],
exports: [
ContentNodeSelectorComponent
],
entryComponents: [
ContentNodeSelectorComponent
],
declarations: [
ContentNodeSelectorComponent
],
providers: [
ContentNodeSelectorService
]
})
export class ContentNodeSelectorModule {}

View File

@@ -0,0 +1,54 @@
/*!
* @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 { SearchOptions, SearchService } from '@alfresco/core';
import { Injectable } from '@angular/core';
import { NodePaging } from 'alfresco-js-api';
import { Observable } from 'rxjs/Rx';
/**
* Internal service used by ContentNodeSelector component.
*/
@Injectable()
export class ContentNodeSelectorService {
constructor(private searchService: SearchService) {}
/**
* Performs a search for content node selection
*
* @param searchTerm The term to search for
* @param skipCount From where to start the loading
* @param rootNodeId The root is to start the search from
* @param maxItems How many items to load
*/
public search(searchTerm: string, rootNodeId: string, skipCount: number, maxItems: number): Observable<NodePaging> {
searchTerm = searchTerm + '*';
let searchOpts: SearchOptions = {
include: ['path', 'allowableOperations'],
skipCount,
rootNodeId,
nodeType: 'cm:folder',
maxItems,
orderBy: null
};
return this.searchService.getNodeQueryResults(searchTerm, searchOpts);
}
}

View File

@@ -0,0 +1,18 @@
/*!
* @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.
*/
export * from './public-api';

View File

@@ -0,0 +1,21 @@
/*!
* @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.
*/
export * from './content-node-selector.component';
export * from './content-node-selector.service';
export * from './content-node-selector.module';

View File

@@ -0,0 +1,87 @@
/*!
* @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 { CommonModule } from '@angular/common';
import { NgModule } from '@angular/core';
import { FormsModule, ReactiveFormsModule } from '@angular/forms';
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
import { CoreModule, TRANSLATION_PROVIDER } from '@alfresco/core';
import { MaterialModule } from './material.module';
import { SocialModule } from './social';
import { TagModule } from './tag';
import { WebScriptModule } from './webscript';
import { DocumentListModule } from './document-list';
import { UploadModule } from './upload';
import { SearchModule } from './search';
import { SitesDropdownModule } from './site-dropdown';
import { BreadcrumbModule } from './breadcrumb';
import { VersionManagerModule } from './version-manager';
import { ContentNodeSelectorModule } from './content-node-selector';
import { DialogModule } from './dialogs';
import { DirectiveModule } from './directive';
@NgModule({
imports: [
CoreModule,
SocialModule,
TagModule,
CommonModule,
WebScriptModule,
FormsModule,
ReactiveFormsModule,
SearchModule,
BrowserAnimationsModule,
DocumentListModule,
UploadModule,
MaterialModule,
SitesDropdownModule,
BreadcrumbModule,
VersionManagerModule,
ContentNodeSelectorModule,
DialogModule,
DirectiveModule
],
providers: [
{
provide: TRANSLATION_PROVIDER,
multi: true,
useValue: {
name: '@adf/content-services',
source: 'assets/@adf/content-services'
}
}
],
exports: [
CoreModule,
SocialModule,
TagModule,
WebScriptModule,
DocumentListModule,
UploadModule,
SearchModule,
SitesDropdownModule,
BreadcrumbModule,
VersionManagerModule,
ContentNodeSelectorModule,
DialogModule,
DirectiveModule
]
})
export class ContentModule {
}

View File

@@ -0,0 +1,55 @@
/*!
* @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 { CommonModule } from '@angular/common';
import { NgModule } from '@angular/core';
import { MaterialModule } from '../material.module';
import { DownloadZipDialogComponent } from './download-zip.dialog';
import { FolderDialogComponent } from './folder.dialog';
import { FormsModule, ReactiveFormsModule } from '@angular/forms';
import { TranslateModule } from '@ngx-translate/core';
import { NodesApiService, NotificationService, TranslationService } from '@alfresco/core';
@NgModule({
imports: [
CommonModule,
MaterialModule,
TranslateModule,
FormsModule,
ReactiveFormsModule
],
declarations: [
DownloadZipDialogComponent,
FolderDialogComponent
],
providers: [
NodesApiService,
NotificationService,
TranslationService
],
exports: [
DownloadZipDialogComponent,
FolderDialogComponent
],
entryComponents: [
DownloadZipDialogComponent,
FolderDialogComponent
]
})
export class DialogModule {}

View File

@@ -0,0 +1,10 @@
<h1 matDialogTitle>{{ 'CORE.DIALOG.DOWNLOAD_ZIP.TITLE' | translate }}</h1>
<div mat-dialog-content>
<mat-progress-bar color="primary" mode="indeterminate"></mat-progress-bar>
</div>
<div mat-dialog-actions>
<span class="spacer"></span>
<button mat-button color="primary" (click)="cancelDownload()">
{{ 'CORE.DIALOG.DOWNLOAD_ZIP.ACTIONS.CANCEL' | translate }}
</button>
</div>

View File

@@ -0,0 +1,5 @@
.spacer { flex: 1 1 auto; }
.adf-download-zip-dialog .mat-dialog-actions .mat-button-wrapper {
text-transform: uppercase;
}

View File

@@ -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 { Component, Inject, OnInit, ViewEncapsulation } from '@angular/core';
import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material';
import { DownloadEntry, MinimalNodeEntity } from 'alfresco-js-api';
import { LogService, AlfrescoApiService } from '@alfresco/core';
@Component({
selector: 'adf-download-zip-dialog',
templateUrl: './download-zip.dialog.html',
styleUrls: ['./download-zip.dialog.scss'],
host: { 'class': 'adf-download-zip-dialog' },
encapsulation: ViewEncapsulation.None
})
export class DownloadZipDialogComponent implements OnInit {
// flag for async threads
private cancelled = false;
constructor(private apiService: AlfrescoApiService,
private dialogRef: MatDialogRef<DownloadZipDialogComponent>,
@Inject(MAT_DIALOG_DATA) private data: { nodeIds?: string[] },
private logService: LogService) {
}
private get downloadsApi() {
return this.apiService.getInstance().core.downloadsApi;
}
private get nodesApi() {
return this.apiService.getInstance().core.nodesApi;
}
private get contentApi() {
return this.apiService.getInstance().content;
}
ngOnInit() {
if (this.data && this.data.nodeIds && this.data.nodeIds.length > 0) {
// change timeout to have a delay for demo purposes
setTimeout(() => {
if (!this.cancelled) {
this.downloadZip(this.data.nodeIds);
} else {
this.logService.log('Cancelled');
}
}, 0);
}
}
cancelDownload() {
this.cancelled = true;
this.dialogRef.close(false);
}
downloadZip(nodeIds: string[]) {
if (nodeIds && nodeIds.length > 0) {
const promise: any = this.downloadsApi.createDownload({ nodeIds });
promise.on('progress', progress => this.logService.log('Progress', progress));
promise.on('error', error => this.logService.error('Error', error));
promise.on('abort', data => this.logService.log('Abort', data));
promise.on('success', (data: DownloadEntry) => {
if (data && data.entry && data.entry.id) {
const url = this.contentApi.getContentUrl(data.entry.id, true);
// the call is needed only to get the name of the package
this.nodesApi.getNode(data.entry.id).then((downloadNode: MinimalNodeEntity) => {
this.logService.log(downloadNode);
const fileName = downloadNode.entry.name;
this.waitAndDownload(data.entry.id, url, fileName);
});
}
});
}
}
waitAndDownload(downloadId: string, url: string, fileName: string) {
if (this.cancelled) {
return;
}
this.downloadsApi.getDownload(downloadId).then((d: DownloadEntry) => {
if (d.entry) {
if (d.entry.status === 'DONE') {
this.download(url, fileName);
} else {
setTimeout(() => {
this.waitAndDownload(downloadId, url, fileName);
}, 1000);
}
}
});
}
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);
}
this.dialogRef.close(true);
}
}

View File

@@ -0,0 +1,45 @@
/*!
* @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 { FormControl } from '@angular/forms';
const I18N_ERRORS_PATH = 'CORE.FOLDER_DIALOG.FOLDER_NAME.ERRORS';
export function forbidSpecialCharacters({ value }: FormControl) {
const specialCharacters: RegExp = /([\*\"\<\>\\\/\?\:\|])/;
const isValid: boolean = !specialCharacters.test(value);
return (isValid) ? null : {
message: `${I18N_ERRORS_PATH}.SPECIAL_CHARACTERS`
};
}
export function forbidEndingDot({ value }: FormControl) {
const isValid: boolean = ((value || '').trim().split('').pop() !== '.');
return isValid ? null : {
message: `${I18N_ERRORS_PATH}.ENDING_DOT`
};
}
export function forbidOnlySpaces({ value }: FormControl) {
const isValid: boolean = !!((value || '')).trim();
return isValid ? null : {
message: `${I18N_ERRORS_PATH}.ONLY_SPACES`
};
}

View File

@@ -0,0 +1,64 @@
<h2 mat-dialog-title>
{{
(editing
? 'CORE.FOLDER_DIALOG.EDIT_FOLDER_TITLE'
: 'CORE.FOLDER_DIALOG.CREATE_FOLDER_TITLE'
) | translate
}}
</h2>
<mat-dialog-content>
<form [formGroup]="form" (submit)="submit()">
<mat-input-container class="adf-full-width">
<input
placeholder="{{ 'CORE.FOLDER_DIALOG.FOLDER_NAME.LABEL' | translate }}"
matInput
required
[formControl]="form.controls['name']"
/>
<mat-hint *ngIf="form.controls['name'].dirty">
<span *ngIf="form.controls['name'].errors?.required">
{{ 'CORE.FOLDER_DIALOG.FOLDER_NAME.ERRORS.REQUIRED' | translate }}
</span>
<span *ngIf="!form.controls['name'].errors?.required && form.controls['name'].errors?.message">
{{ form.controls['name'].errors?.message | translate }}
</span>
</mat-hint>
</mat-input-container>
<br />
<br />
<mat-input-container class="adf-full-width">
<textarea
matInput
placeholder="{{ 'CORE.FOLDER_DIALOG.FOLDER_DESCRIPTION.LABEL' | translate }}"
rows="4"
[formControl]="form.controls['description']"></textarea>
</mat-input-container>
</form>
</mat-dialog-content>
<mat-dialog-actions class="adf-dialog-buttons">
<span class="adf-fill-remaining-space"></span>
<button
mat-button
mat-dialog-close>
{{ 'CORE.FOLDER_DIALOG.CANCEL_BUTTON.LABEL' | translate }}
</button>
<button class="adf-dialog-action-button"
mat-button
(click)="submit()"
[disabled]="!form.valid">
{{
(editing
? 'CORE.FOLDER_DIALOG.UPDATE_BUTTON.LABEL'
: 'CORE.FOLDER_DIALOG.CREATE_BUTTON.LABEL'
) | translate
}}
</button>
</mat-dialog-actions>

View File

@@ -0,0 +1,20 @@
.adf-fill-remaining-space {
flex: 1 1 auto;
}
.adf-full-width {
width: 100%;
}
@mixin adf-dialog-theme($theme) {
$primary: map-get($theme, primary);
.adf-dialog-buttons button {
text-transform: uppercase;
}
.adf-dialog-action-button:enabled {
color: mat-color($primary);
}
}

View File

@@ -0,0 +1,271 @@
/*!
* @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 { async, TestBed } from '@angular/core/testing';
import { ComponentFixture } from '@angular/core/testing';
import { FormsModule, ReactiveFormsModule } from '@angular/forms';
import { MatDialogRef } from '@angular/material';
import { MaterialModule } from '../material.module';
import { BrowserDynamicTestingModule } from '@angular/platform-browser-dynamic/testing';
import { Observable } from 'rxjs/Rx';
import { NodesApiService, NotificationService, TranslationService } from '@alfresco/core';
import { FolderDialogComponent } from './folder.dialog';
describe('FolderDialogComponent', () => {
let fixture: ComponentFixture<FolderDialogComponent>;
let component: FolderDialogComponent;
let translationService: TranslationService;
let nodesApi: NodesApiService;
let notificationService: NotificationService;
let dialogRef;
beforeEach(async(() => {
dialogRef = {
close: jasmine.createSpy('close')
};
TestBed.configureTestingModule({
imports: [
MaterialModule,
FormsModule,
ReactiveFormsModule,
BrowserDynamicTestingModule
],
declarations: [
FolderDialogComponent
],
providers: [
{ provide: MatDialogRef, useValue: dialogRef }
]
});
// entryComponents are not supported yet on TestBed, that is why this ugly workaround:
// https://github.com/angular/angular/issues/10760
TestBed.overrideModule(BrowserDynamicTestingModule, {
set: {entryComponents: [ FolderDialogComponent ]}
});
TestBed.compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(FolderDialogComponent);
component = fixture.componentInstance;
nodesApi = TestBed.get(NodesApiService);
notificationService = TestBed.get(NotificationService);
translationService = TestBed.get(TranslationService);
spyOn(translationService, 'get').and.returnValue(Observable.of('message'));
});
describe('Edit', () => {
beforeEach(() => {
component.data = {
folder: {
id: 'node-id',
name: 'folder-name',
properties: {
['cm:description']: 'folder-description'
}
}
};
fixture.detectChanges();
});
it('should init form with folder name and description', () => {
expect(component.name).toBe('folder-name');
expect(component.description).toBe('folder-description');
});
it('should update form input', () => {
component.form.controls['name'].setValue('folder-name-update');
component.form.controls['description'].setValue('folder-description-update');
expect(component.name).toBe('folder-name-update');
expect(component.description).toBe('folder-description-update');
});
it('should submit updated values if form is valid', () => {
spyOn(nodesApi, 'updateNode').and.returnValue(Observable.of({}));
component.form.controls['name'].setValue('folder-name-update');
component.form.controls['description'].setValue('folder-description-update');
component.submit();
expect(nodesApi.updateNode).toHaveBeenCalledWith(
'node-id',
{
name: 'folder-name-update',
properties: {
'cm:title': 'folder-name-update',
'cm:description': 'folder-description-update'
}
}
);
});
it('should call dialog to close with form data when submit is succesfluly', () => {
const folder = {
data: 'folder-data'
};
spyOn(nodesApi, 'updateNode').and.returnValue(Observable.of(folder));
component.submit();
expect(dialogRef.close).toHaveBeenCalledWith(folder);
});
it('should not submit if form is invalid', () => {
spyOn(nodesApi, 'updateNode');
component.form.controls['name'].setValue('');
component.form.controls['description'].setValue('');
component.submit();
expect(component.form.valid).toBe(false);
expect(nodesApi.updateNode).not.toHaveBeenCalled();
});
it('should not call dialog to close if submit fails', () => {
spyOn(nodesApi, 'updateNode').and.returnValue(Observable.throw('error'));
spyOn(component, 'handleError').and.callFake(val => val);
component.submit();
expect(component.handleError).toHaveBeenCalled();
expect(dialogRef.close).not.toHaveBeenCalled();
});
});
describe('Create', () => {
beforeEach(() => {
component.data = {
parentNodeId: 'parentNodeId',
folder: null
};
fixture.detectChanges();
});
it('should init form with empty inputs', () => {
expect(component.name).toBe('');
expect(component.description).toBe('');
});
it('should update form input', () => {
component.form.controls['name'].setValue('folder-name-update');
component.form.controls['description'].setValue('folder-description-update');
expect(component.name).toBe('folder-name-update');
expect(component.description).toBe('folder-description-update');
});
it('should submit updated values if form is valid', () => {
spyOn(nodesApi, 'createFolder').and.returnValue(Observable.of({}));
component.form.controls['name'].setValue('folder-name-update');
component.form.controls['description'].setValue('folder-description-update');
component.submit();
expect(nodesApi.createFolder).toHaveBeenCalledWith(
'parentNodeId',
{
name: 'folder-name-update',
properties: {
'cm:title': 'folder-name-update',
'cm:description': 'folder-description-update'
}
}
);
});
it('should call dialog to close with form data when submit is succesfluly', () => {
const folder = {
data: 'folder-data'
};
component.form.controls['name'].setValue('name');
component.form.controls['description'].setValue('description');
spyOn(nodesApi, 'createFolder').and.returnValue(Observable.of(folder));
component.submit();
expect(dialogRef.close).toHaveBeenCalledWith(folder);
});
it('should not submit if form is invalid', () => {
spyOn(nodesApi, 'createFolder');
component.form.controls['name'].setValue('');
component.form.controls['description'].setValue('');
component.submit();
expect(component.form.valid).toBe(false);
expect(nodesApi.createFolder).not.toHaveBeenCalled();
});
it('should not call dialog to close if submit fails', () => {
spyOn(nodesApi, 'createFolder').and.returnValue(Observable.throw('error'));
spyOn(component, 'handleError').and.callFake(val => val);
component.form.controls['name'].setValue('name');
component.form.controls['description'].setValue('description');
component.submit();
expect(component.handleError).toHaveBeenCalled();
expect(dialogRef.close).not.toHaveBeenCalled();
});
});
describe('handleError()', () => {
it('should raise error for 409', () => {
spyOn(notificationService, 'openSnackMessage').and.stub();
const error = {
message: '{ "error": { "statusCode" : 409 } }'
};
component.handleError(error);
expect(notificationService.openSnackMessage).toHaveBeenCalled();
expect(translationService.get).toHaveBeenCalledWith('CORE.MESSAGES.ERRORS.EXISTENT_FOLDER');
});
it('should raise generic error', () => {
spyOn(notificationService, 'openSnackMessage').and.stub();
const error = {
message: '{ "error": { "statusCode" : 123 } }'
};
component.handleError(error);
expect(notificationService.openSnackMessage).toHaveBeenCalled();
expect(translationService.get).toHaveBeenCalledWith('CORE.MESSAGES.ERRORS.GENERIC');
});
});
});

View File

@@ -0,0 +1,140 @@
/*!
* @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 { Observable } from 'rxjs/Rx';
import { Component, Inject, OnInit, Optional } from '@angular/core';
import { FormBuilder, FormGroup, Validators } from '@angular/forms';
import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material';
import { MinimalNodeEntryEntity } from 'alfresco-js-api';
import { NodesApiService, NotificationService, TranslationService } from '@alfresco/core';
import { forbidEndingDot, forbidOnlySpaces, forbidSpecialCharacters } from './folder-name.validators';
@Component({
selector: 'adf-folder-dialog',
styleUrls: ['./folder.dialog.scss'],
templateUrl: './folder.dialog.html'
})
export class FolderDialogComponent implements OnInit {
form: FormGroup;
folder: MinimalNodeEntryEntity = null;
constructor(
private formBuilder: FormBuilder,
private dialog: MatDialogRef<FolderDialogComponent>,
private nodesApi: NodesApiService,
private translation: TranslationService,
private notification: NotificationService,
@Optional()
@Inject(MAT_DIALOG_DATA)
public data: any
) {}
get editing(): boolean {
return !!this.data.folder;
}
ngOnInit() {
const { folder } = this.data;
let name = '';
let description = '';
if (folder) {
const { properties } = folder;
name = folder.name || '';
description = properties ? properties['cm:description'] : '';
}
const validators = {
name: [
Validators.required,
forbidSpecialCharacters,
forbidEndingDot,
forbidOnlySpaces
]
};
this.form = this.formBuilder.group({
name: [ name, validators.name ],
description: [ description ]
});
}
get name(): string {
let { name } = this.form.value;
return (name || '').trim();
}
get description(): string {
let { description } = this.form.value;
return (description || '').trim();
}
private get properties(): any {
const { name: title, description } = this;
return {
'cm:title': title,
'cm:description': description
};
}
private create(): Observable<MinimalNodeEntryEntity> {
const { name, properties, nodesApi, data: { parentNodeId} } = this;
return nodesApi.createFolder(parentNodeId, { name, properties });
}
private edit(): Observable<MinimalNodeEntryEntity> {
const { name, properties, nodesApi, data: { folder: { id: nodeId }} } = this;
return nodesApi.updateNode(nodeId, { name, properties });
}
submit() {
const { form, dialog, editing } = this;
if (!form.valid) { return; }
(editing ? this.edit() : this.create())
.subscribe(
(folder: MinimalNodeEntryEntity) => dialog.close(folder),
(error) => this.handleError(error)
);
}
handleError(error: any): any {
let i18nMessageString = 'CORE.MESSAGES.ERRORS.GENERIC';
try {
const { error: { statusCode } } = JSON.parse(error.message);
if (statusCode === 409) {
i18nMessageString = 'CORE.MESSAGES.ERRORS.EXISTENT_FOLDER';
}
} catch (err) { /* Do nothing, keep the original message */ }
this.translation.get(i18nMessageString).subscribe(message => {
this.notification.openSnackMessage(message, 3000);
});
return error;
}
}

View File

@@ -0,0 +1,18 @@
/*!
* @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.
*/
export * from './public-api';

View File

@@ -0,0 +1,21 @@
/*!
* @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.
*/
export * from './download-zip.dialog';
export * from './folder.dialog';
export * from './dialog.module';

View File

@@ -0,0 +1,39 @@
/*!
* @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 { CommonModule } from '@angular/common';
import { NgModule } from '@angular/core';
import { MaterialModule } from '../material.module';
import { FolderCreateDirective } from './folder-create.directive';
import { FolderEditDirective } from './folder-edit.directive';
@NgModule({
imports: [
CommonModule,
MaterialModule
],
declarations: [
FolderCreateDirective,
FolderEditDirective
],
exports: [
FolderCreateDirective,
FolderEditDirective
]
})
export class DirectiveModule {}

View File

@@ -0,0 +1,113 @@
/*!
* @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 { HttpClientModule } from '@angular/common/http';
import { Component } from '@angular/core';
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { FormsModule, ReactiveFormsModule } from '@angular/forms';
import { MatDialog, MatDialogModule } from '@angular/material';
import { By } from '@angular/platform-browser';
import { TranslateLoader, TranslateModule } from '@ngx-translate/core';
import { Observable } from 'rxjs/Rx';
import { AppConfigService, DirectiveModule, ContentService, TranslateLoaderService } from '@alfresco/core';
import { FolderCreateDirective } from './folder-create.directive';
@Component({
template: '<div [adf-create-folder]="parentNode"></div>'
})
class TestComponent {
parentNode = '';
}
describe('FolderCreateDirective', () => {
let fixture: ComponentFixture<TestComponent>;
let element;
let node: any;
let dialog: MatDialog;
let contentService: ContentService;
let dialogRefMock;
const event: any = {
type: 'click',
preventDefault: () => null
};
beforeEach(() => {
TestBed.configureTestingModule({
imports: [
HttpClientModule,
MatDialogModule,
FormsModule,
DirectiveModule,
ReactiveFormsModule,
TranslateModule.forRoot({
loader: {
provide: TranslateLoader,
useClass: TranslateLoaderService
}
})
],
declarations: [
TestComponent,
FolderCreateDirective
],
providers: [
ContentService,
AppConfigService
]
});
TestBed.compileComponents();
fixture = TestBed.createComponent(TestComponent);
element = fixture.debugElement.query(By.directive(FolderCreateDirective));
dialog = TestBed.get(MatDialog);
contentService = TestBed.get(ContentService);
});
beforeEach(() => {
node = { entry: { id: 'nodeId' } };
dialogRefMock = {
afterClosed: val => Observable.of(val)
};
spyOn(dialog, 'open').and.returnValue(dialogRefMock);
});
it('should emit folderCreate event when input value is not undefined', () => {
spyOn(dialogRefMock, 'afterClosed').and.returnValue(Observable.of(node));
contentService.folderCreate.subscribe((val) => {
expect(val).toBe(node);
});
element.triggerEventHandler('click', event);
fixture.detectChanges();
});
it('should not emit folderCreate event when input value is undefined', () => {
spyOn(dialogRefMock, 'afterClosed').and.returnValue(Observable.of(null));
spyOn(contentService.folderCreate, 'next');
element.triggerEventHandler('click', event);
fixture.detectChanges();
expect(contentService.folderCreate.next).not.toHaveBeenCalled();
});
});

View File

@@ -0,0 +1,66 @@
/*!
* @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, HostListener, Input } from '@angular/core';
import { MatDialog, MatDialogConfig } from '@angular/material';
import { MinimalNodeEntryEntity } from 'alfresco-js-api';
import { FolderDialogComponent } from '../dialogs/folder.dialog';
import { ContentService } from '@alfresco/core';
const DEFAULT_FOLDER_PARENT_ID = '-my-';
@Directive({
selector: '[adf-create-folder]'
})
export class FolderCreateDirective {
static DIALOG_WIDTH: number = 400;
@Input('adf-create-folder')
parentNodeId: string = DEFAULT_FOLDER_PARENT_ID;
@HostListener('click', [ '$event' ])
onClick(event) {
event.preventDefault();
this.openDialog();
}
constructor(
public dialogRef: MatDialog,
public content: ContentService
) {}
private get dialogConfig(): MatDialogConfig {
const { DIALOG_WIDTH: width } = FolderCreateDirective;
const { parentNodeId } = this;
return {
data: { parentNodeId },
width: `${width}px`
};
}
private openDialog(): void {
const { dialogRef, dialogConfig, content } = this;
const dialogInstance = dialogRef.open(FolderDialogComponent, dialogConfig);
dialogInstance.afterClosed().subscribe((node: MinimalNodeEntryEntity) => {
if (node) {
content.folderCreate.next(node);
}
});
}
}

View File

@@ -0,0 +1,115 @@
/*!
* @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 { HttpClientModule } from '@angular/common/http';
import { Component } from '@angular/core';
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { MatDialog, MatDialogModule } from '@angular/material';
import { By } from '@angular/platform-browser';
import { FormsModule, ReactiveFormsModule } from '@angular/forms';
import { TranslateLoader, TranslateModule } from '@ngx-translate/core';
import { Observable } from 'rxjs/Rx';
import { AppConfigService, ContentService, TranslateLoaderService, DirectiveModule } from '@alfresco/core';
import { FolderEditDirective } from './folder-edit.directive';
@Component({
template: '<div [adf-edit-folder]="folder"></div>'
})
class TestComponent {
folder = {};
}
describe('FolderEditDirective', () => {
let fixture: ComponentFixture<TestComponent>;
let element;
let node: any;
let dialog: MatDialog;
let contentService: ContentService;
let dialogRefMock;
const event = {
type: 'click',
preventDefault: () => null
};
beforeEach(() => {
TestBed.configureTestingModule({
imports: [
HttpClientModule,
MatDialogModule,
FormsModule,
ReactiveFormsModule,
DirectiveModule,
TranslateModule.forRoot({
loader: {
provide: TranslateLoader,
useClass: TranslateLoaderService
}
})
],
declarations: [
TestComponent,
FolderEditDirective
]
,
providers: [
ContentService,
AppConfigService
]
});
TestBed.compileComponents();
fixture = TestBed.createComponent(TestComponent);
element = fixture.debugElement.query(By.directive(FolderEditDirective));
dialog = TestBed.get(MatDialog);
contentService = TestBed.get(ContentService);
});
beforeEach(() => {
node = { entry: { id: 'folderId' } };
dialogRefMock = {
afterClosed: val => Observable.of(val)
};
spyOn(dialog, 'open').and.returnValue(dialogRefMock);
});
it('should emit folderEdit event when input value is not undefined', () => {
spyOn(dialogRefMock, 'afterClosed').and.returnValue(Observable.of(node));
contentService.folderEdit.subscribe((val) => {
expect(val).toBe(node);
});
element.triggerEventHandler('click', event);
fixture.detectChanges();
});
it('should not emit folderEdit event when input value is undefined', () => {
spyOn(dialogRefMock, 'afterClosed').and.returnValue(Observable.of(null));
spyOn(contentService.folderEdit, 'next');
element.triggerEventHandler('click', event);
fixture.detectChanges();
expect(contentService.folderEdit.next).not.toHaveBeenCalled();
});
});

View File

@@ -0,0 +1,69 @@
/*!
* @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, ElementRef, HostListener, Input } from '@angular/core';
import { MatDialog, MatDialogConfig } from '@angular/material';
import { MinimalNodeEntryEntity } from 'alfresco-js-api';
import { FolderDialogComponent } from '../dialogs/folder.dialog';
import { ContentService } from '@alfresco/core';
@Directive({
selector: '[adf-edit-folder]'
})
export class FolderEditDirective {
static DIALOG_WIDTH: number = 400;
@Input('adf-edit-folder')
folder: MinimalNodeEntryEntity;
@HostListener('click', [ '$event' ])
onClick(event) {
event.preventDefault();
if (this.folder) {
this.openDialog();
}
}
constructor(
public dialogRef: MatDialog,
public elementRef: ElementRef,
public content: ContentService
) {}
private get dialogConfig(): MatDialogConfig {
const { DIALOG_WIDTH: width } = FolderEditDirective;
const { folder } = this;
return {
data: { folder },
width: `${width}px`
};
}
private openDialog(): void {
const { dialogRef, dialogConfig, content } = this;
const dialogInstance = dialogRef.open(FolderDialogComponent, dialogConfig);
dialogInstance.afterClosed().subscribe((node: MinimalNodeEntryEntity) => {
if (node) {
content.folderEdit.next(node);
}
});
}
}

View File

@@ -0,0 +1,18 @@
/*!
* @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.
*/
export * from './public-api';

View File

@@ -0,0 +1,21 @@
/*!
* @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.
*/
export * from './folder-create.directive';
export * from './folder-edit.directive';
export * from './directive.module';

View File

@@ -0,0 +1,79 @@
/*!
* @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 { CUSTOM_ELEMENTS_SCHEMA } from '@angular/core';
import { async, TestBed } from '@angular/core/testing';
import { DataTableModule } from '@alfresco/core';
import { MaterialModule } from '../../../material.module';
import { DocumentListService } from '../../services/document-list.service';
import { ContentActionModel } from './../../models/content-action.model';
import { DocumentListComponent } from './../document-list.component';
import { ContentActionListComponent } from './content-action-list.component';
describe('ContentColumnList', () => {
let documentList: DocumentListComponent;
let actionList: ContentActionListComponent;
beforeEach(async(() => {
TestBed.configureTestingModule({
imports: [
DataTableModule,
MaterialModule
],
declarations: [
DocumentListComponent
],
providers: [
DocumentListService
],
schemas: [
CUSTOM_ELEMENTS_SCHEMA
]
}).compileComponents();
}));
beforeEach(() => {
documentList = (TestBed.createComponent(DocumentListComponent).componentInstance as DocumentListComponent);
actionList = new ContentActionListComponent(documentList);
});
it('should register action', () => {
spyOn(documentList.actions, 'push').and.callThrough();
let action = new ContentActionModel();
let result = actionList.registerAction(action);
expect(result).toBeTruthy();
expect(documentList.actions.push).toHaveBeenCalledWith(action);
});
it('should require document list instance to register action', () => {
actionList = new ContentActionListComponent(null);
let action = new ContentActionModel();
expect(actionList.registerAction(action)).toBeFalsy();
});
it('should require action instance to register', () => {
spyOn(documentList.actions, 'push').and.callThrough();
let result = actionList.registerAction(null);
expect(result).toBeFalsy();
expect(documentList.actions.push).not.toHaveBeenCalled();
});
});

View File

@@ -0,0 +1,45 @@
/*!
* @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.
*/
/* tslint:disable:component-selector */
import { Component } from '@angular/core';
import { ContentActionModel } from './../../models/content-action.model';
import { DocumentListComponent } from './../document-list.component';
@Component({
selector: 'content-actions',
template: ''
})
export class ContentActionListComponent {
constructor(private documentList: DocumentListComponent) {
}
/**
* Registers action handler within the parent document list component.
* @param action Action model to register.
*/
registerAction(action: ContentActionModel): boolean {
if (this.documentList && action) {
this.documentList.actions.push(action);
return true;
}
return false;
}
}

View File

@@ -0,0 +1,294 @@
/*!
* @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 { CUSTOM_ELEMENTS_SCHEMA } from '@angular/core';
import { EventEmitter } from '@angular/core';
import { async, TestBed } from '@angular/core/testing';
import { ContentService, TranslationService, NotificationService } from '@alfresco/core';
import { DataTableModule } from '@alfresco/core';
import { MaterialModule } from '../../../material.module';
import { DocumentListService } from '../../services/document-list.service';
import { FileNode } from '../../../mock';
import { ContentActionHandler } from './../../models/content-action.model';
import { DocumentActionsService } from './../../services/document-actions.service';
import { FolderActionsService } from './../../services/folder-actions.service';
import { NodeActionsService } from './../../services/node-actions.service';
import { DocumentListComponent } from './../document-list.component';
import { ContentActionListComponent } from './content-action-list.component';
import { ContentActionComponent } from './content-action.component';
describe('ContentAction', () => {
let documentList: DocumentListComponent;
let actionList: ContentActionListComponent;
let documentActions: DocumentActionsService;
let folderActions: FolderActionsService;
let contentService: ContentService;
let translateService: TranslationService;
let notificationService: NotificationService;
let nodeActionsService: NodeActionsService;
beforeEach(async(() => {
TestBed.configureTestingModule({
imports: [
DataTableModule,
MaterialModule
],
providers: [
DocumentListService
],
declarations: [
DocumentListComponent
],
schemas: [
CUSTOM_ELEMENTS_SCHEMA
]
}).compileComponents();
}));
beforeEach(() => {
contentService = TestBed.get(ContentService);
translateService = <TranslationService> { addTranslationFolder: () => {}};
nodeActionsService = new NodeActionsService(null, null, null);
notificationService = new NotificationService(null);
documentActions = new DocumentActionsService(nodeActionsService);
folderActions = new FolderActionsService(nodeActionsService, null, contentService);
documentList = (TestBed.createComponent(DocumentListComponent).componentInstance as DocumentListComponent);
actionList = new ContentActionListComponent(documentList);
});
it('should register within parent actions list', () => {
spyOn(actionList, 'registerAction').and.stub();
let action = new ContentActionComponent(actionList, null, null);
action.ngOnInit();
expect(actionList.registerAction).toHaveBeenCalled();
});
it('should setup and register model', () => {
let action = new ContentActionComponent(actionList, null, null);
action.target = 'document';
action.title = '<title>';
action.icon = '<icon>';
expect(documentList.actions.length).toBe(0);
action.ngOnInit();
expect(documentList.actions.length).toBe(1);
let model = documentList.actions[0];
expect(model.target).toBe(action.target);
expect(model.title).toBe(action.title);
expect(model.icon).toBe(action.icon);
});
it('should get action handler from document actions service', () => {
let handler = function () {
};
spyOn(documentActions, 'getHandler').and.returnValue(handler);
let action = new ContentActionComponent(actionList, documentActions, null);
action.target = 'document';
action.handler = '<handler>';
action.ngOnInit();
expect(documentActions.getHandler).toHaveBeenCalledWith(action.handler);
expect(documentList.actions.length).toBe(1);
let model = documentList.actions[0];
expect(model.handler).toBe(handler);
});
it('should get action handler from folder actions service', () => {
let handler = function () {
};
spyOn(folderActions, 'getHandler').and.returnValue(handler);
let action = new ContentActionComponent(actionList, null, folderActions);
action.target = 'folder';
action.handler = '<handler>';
action.ngOnInit();
expect(folderActions.getHandler).toHaveBeenCalledWith(action.handler);
expect(documentList.actions.length).toBe(1);
let model = documentList.actions[0];
expect(model.handler).toBe(handler);
});
it('should require target to get system handler', () => {
spyOn(folderActions, 'getHandler').and.stub();
spyOn(documentActions, 'getHandler').and.stub();
let action = new ContentActionComponent(actionList, documentActions, folderActions);
action.handler = '<handler>';
action.ngOnInit();
expect(documentList.actions.length).toBe(1);
expect(folderActions.getHandler).not.toHaveBeenCalled();
expect(documentActions.getHandler).not.toHaveBeenCalled();
action.target = 'document';
action.ngOnInit();
expect(documentActions.getHandler).toHaveBeenCalled();
action.target = 'folder';
action.ngOnInit();
expect(folderActions.getHandler).toHaveBeenCalled();
});
it('should be case insensitive for document target', () => {
spyOn(documentActions, 'getHandler').and.stub();
let action = new ContentActionComponent(actionList, documentActions, null);
action.target = 'DoCuMeNt';
action.handler = '<handler>';
action.ngOnInit();
expect(documentActions.getHandler).toHaveBeenCalledWith(action.handler);
});
it('should be case insensitive for folder target', () => {
spyOn(folderActions, 'getHandler').and.stub();
let action = new ContentActionComponent(actionList, null, folderActions);
action.target = 'FoLdEr';
action.handler = '<handler>';
action.ngOnInit();
expect(folderActions.getHandler).toHaveBeenCalledWith(action.handler);
});
it('should use custom "execute" emitter', (done) => {
let emitter = new EventEmitter();
emitter.subscribe(e => {
expect(e.value).toBe('<obj>');
done();
});
let action = new ContentActionComponent(actionList, null, null);
action.target = 'document';
action.execute = emitter;
action.ngOnInit();
expect(documentList.actions.length).toBe(1);
let model = documentList.actions[0];
model.execute('<obj>');
});
it('should sync localizable fields with model', () => {
let action = new ContentActionComponent(actionList, null, null);
action.title = 'title1';
action.ngOnInit();
expect(action.model.title).toBe(action.title);
action.title = 'title2';
action.ngOnChanges(null);
expect(action.model.title).toBe('title2');
});
it('should not find document action handler with missing service', () => {
let action = new ContentActionComponent(actionList, null, null);
expect(action.getSystemHandler('document', 'name')).toBeNull();
});
it('should not find folder action handler with missing service', () => {
let action = new ContentActionComponent(actionList, null, null);
expect(action.getSystemHandler('folder', 'name')).toBeNull();
});
it('should find document action handler via service', () => {
let handler = <ContentActionHandler> function (obj: any, target?: any) {
};
let action = new ContentActionComponent(actionList, documentActions, null);
spyOn(documentActions, 'getHandler').and.returnValue(handler);
expect(action.getSystemHandler('document', 'name')).toBe(handler);
});
it('should find folder action handler via service', () => {
let handler = <ContentActionHandler> function (obj: any, target?: any) {
};
let action = new ContentActionComponent(actionList, null, folderActions);
spyOn(folderActions, 'getHandler').and.returnValue(handler);
expect(action.getSystemHandler('folder', 'name')).toBe(handler);
});
it('should not find actions for unknown target type', () => {
spyOn(folderActions, 'getHandler').and.stub();
spyOn(documentActions, 'getHandler').and.stub();
let action = new ContentActionComponent(actionList, documentActions, folderActions);
expect(action.getSystemHandler('unknown', 'name')).toBeNull();
expect(folderActions.getHandler).not.toHaveBeenCalled();
expect(documentActions.getHandler).not.toHaveBeenCalled();
});
it('should wire model with custom event handler', (done) => {
let action = new ContentActionComponent(actionList, documentActions, folderActions);
let file = new FileNode();
let handler = new EventEmitter();
handler.subscribe((e) => {
expect(e.value).toBe(file);
done();
});
action.execute = handler;
action.ngOnInit();
action.model.execute(file);
});
it('should allow registering model without handler', () => {
let action = new ContentActionComponent(actionList, documentActions, folderActions);
spyOn(actionList, 'registerAction').and.callThrough();
action.execute = null;
action.ngOnInit();
expect(action.model.handler).toBeUndefined();
expect(actionList.registerAction).toHaveBeenCalledWith(action.model);
});
it('should register on init', () => {
let action = new ContentActionComponent(actionList, null, null);
spyOn(action, 'register').and.callThrough();
action.ngOnInit();
expect(action.register).toHaveBeenCalled();
});
it('should require action list to register action with', () => {
let action = new ContentActionComponent(actionList, null, null);
expect(action.register()).toBeTruthy();
action = new ContentActionComponent(null, null, null);
expect(action.register()).toBeFalsy();
});
});

View File

@@ -0,0 +1,159 @@
/*!
* @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.
*/
/* tslint:disable:component-selector */
import { Component, EventEmitter, Input, OnChanges, OnInit, Output } from '@angular/core';
import { ContentActionHandler } from '../../models/content-action.model';
import { DocumentActionsService } from '../../services/document-actions.service';
import { FolderActionsService } from '../../services/folder-actions.service';
import { ContentActionModel } from './../../models/content-action.model';
import { ContentActionListComponent } from './content-action-list.component';
@Component({
selector: 'content-action',
template: '',
providers: [
DocumentActionsService,
FolderActionsService
]
})
export class ContentActionComponent implements OnInit, OnChanges {
@Input()
title: string = 'Action';
@Input()
icon: string;
@Input()
handler: string;
@Input()
target: string;
@Input()
permission: string;
@Input()
disableWithNoPermission: boolean;
@Input()
disabled: boolean = false;
@Output()
execute = new EventEmitter();
@Output()
permissionEvent = new EventEmitter();
@Output()
error = new EventEmitter();
@Output()
success = new EventEmitter();
model: ContentActionModel;
constructor(
private list: ContentActionListComponent,
private documentActions: DocumentActionsService,
private folderActions: FolderActionsService) {
this.model = new ContentActionModel();
}
ngOnInit() {
this.model = new ContentActionModel({
title: this.title,
icon: this.icon,
permission: this.permission,
disableWithNoPermission: this.disableWithNoPermission,
target: this.target,
disabled: this.disabled
});
if (this.handler) {
this.model.handler = this.getSystemHandler(this.target, this.handler);
}
if (this.execute) {
this.model.execute = (value: any): void => {
this.execute.emit({ value });
};
}
this.register();
}
register(): boolean {
if (this.list) {
return this.list.registerAction(this.model);
}
return false;
}
ngOnChanges(changes) {
// update localizable properties
this.model.title = this.title;
}
getSystemHandler(target: string, name: string): ContentActionHandler {
if (target) {
let ltarget = target.toLowerCase();
if (ltarget === 'document') {
if (this.documentActions) {
this.documentActions.permissionEvent.subscribe((permision) => {
this.permissionEvent.emit(permision);
});
this.documentActions.error.subscribe((errors) => {
this.error.emit(errors);
});
this.documentActions.success.subscribe((message) => {
this.success.emit(message);
});
return this.documentActions.getHandler(name);
}
return null;
}
if (ltarget === 'folder') {
if (this.folderActions) {
this.folderActions.permissionEvent.subscribe((permision) => {
this.permissionEvent.emit(permision);
});
this.folderActions.error.subscribe((errors) => {
this.error.emit(errors);
});
this.folderActions.success.subscribe((message) => {
this.success.emit(message);
});
return this.folderActions.getHandler(name);
}
return null;
}
}
return null;
}
}

View File

@@ -0,0 +1,87 @@
/*!
* @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 { CUSTOM_ELEMENTS_SCHEMA } from '@angular/core';
import { async, TestBed } from '@angular/core/testing';
import { DataColumn, DataTableModule } from '@alfresco/core';
import { MaterialModule } from '../../../material.module';
import { LogService } from '@alfresco/core';
import { DocumentListService } from '../../services/document-list.service';
import { DocumentListComponent } from './../document-list.component';
import { ContentColumnListComponent } from './content-column-list.component';
describe('ContentColumnList', () => {
let documentList: DocumentListComponent;
let columnList: ContentColumnListComponent;
let logService: LogService;
beforeEach(async(() => {
TestBed.configureTestingModule({
imports: [
DataTableModule,
MaterialModule
],
declarations: [
DocumentListComponent
],
providers: [
DocumentListService,
LogService
],
schemas: [
CUSTOM_ELEMENTS_SCHEMA
]
}).compileComponents();
}));
beforeEach(() => {
documentList = (TestBed.createComponent(DocumentListComponent).componentInstance as DocumentListComponent);
logService = TestBed.get(LogService);
columnList = new ContentColumnListComponent(documentList, logService);
documentList.ngOnInit();
});
it('should register column within parent document list', () => {
let columns = documentList.data.getColumns();
expect(columns.length).toBe(0);
let column = <DataColumn> {};
let result = columnList.registerColumn(column);
expect(result).toBeTruthy();
expect(columns.length).toBe(1);
expect(columns[0]).toBe(column);
});
it('should require document list instance to register action', () => {
columnList = new ContentColumnListComponent(null, logService);
let col = <DataColumn> {};
expect(columnList.registerColumn(col)).toBeFalsy();
});
it('should require action instance to register', () => {
spyOn(documentList.actions, 'push').and.callThrough();
let result = columnList.registerColumn(null);
expect(result).toBeFalsy();
expect(documentList.actions.push).not.toHaveBeenCalled();
});
});

View File

@@ -0,0 +1,48 @@
/*!
* @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.
*/
/* tslint:disable:component-selector */
import { DataColumn } from '@alfresco/core';
import { LogService } from '@alfresco/core';
import { Component } from '@angular/core';
import { DocumentListComponent } from './../document-list.component';
@Component({
selector: 'content-columns',
template: ''
})
export class ContentColumnListComponent {
constructor(private documentList: DocumentListComponent, private logService: LogService ) {
this.logService.log('ContentColumnListComponent is deprecated starting with 1.7.0 and may be removed in future versions. Use DataColumnListComponent instead.');
}
/**
* Registers column model within the parent document list component.
* @param column Column definition model to register.
*/
registerColumn(column: DataColumn): boolean {
if (this.documentList && column) {
let columns = this.documentList.data.getColumns();
columns.push(column);
return true;
}
return false;
}
}

View File

@@ -0,0 +1,98 @@
/*!
* @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 { CUSTOM_ELEMENTS_SCHEMA } from '@angular/core';
import { async, TestBed } from '@angular/core/testing';
import { LogService } from '@alfresco/core';
import { DataTableModule } from '@alfresco/core';
import { MaterialModule } from '../../../material.module';
import { DocumentListService } from '../../services/document-list.service';
import { DocumentListComponent } from './../document-list.component';
import { ContentColumnListComponent } from './content-column-list.component';
import { ContentColumnComponent } from './content-column.component';
describe('ContentColumn', () => {
let documentList: DocumentListComponent;
let columnList: ContentColumnListComponent;
let logService: LogService;
beforeEach(async(() => {
TestBed.configureTestingModule({
imports: [
DataTableModule,
MaterialModule
],
declarations: [
DocumentListComponent
],
providers: [
DocumentListService,
LogService
],
schemas: [
CUSTOM_ELEMENTS_SCHEMA
]
}).compileComponents();
}));
beforeEach(() => {
documentList = (TestBed.createComponent(DocumentListComponent).componentInstance as DocumentListComponent);
logService = TestBed.get(LogService);
columnList = new ContentColumnListComponent(documentList, logService);
documentList.ngOnInit();
});
it('should register model within parent column list', () => {
spyOn(columnList, 'registerColumn').and.callThrough();
let column = new ContentColumnComponent(columnList, logService);
column.ngAfterContentInit();
expect(columnList.registerColumn).toHaveBeenCalled();
let columns = documentList.data.getColumns();
expect(columns.length).toBe(1);
expect(columns[0]).toBe(column);
});
it('should setup screen reader title for thumbnail column', () => {
let column = new ContentColumnComponent(columnList, logService);
column.key = '$thumbnail';
column.ngOnInit();
expect(column.srTitle).toBe('Thumbnail');
});
it('should register on init', () => {
let column = new ContentColumnComponent(columnList, logService);
spyOn(column, 'register').and.callThrough();
column.ngAfterContentInit();
expect(column.register).toHaveBeenCalled();
});
it('should require action list to register action with', () => {
let column = new ContentColumnComponent(columnList, logService);
expect(column.register()).toBeTruthy();
column = new ContentColumnComponent(null, logService);
expect(column.register()).toBeFalsy();
});
});

View File

@@ -0,0 +1,79 @@
/*!
* @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.
*/
/* tslint:disable:component-selector */
import { DataColumn } from '@alfresco/core';
import { LogService } from '@alfresco/core';
import { AfterContentInit, Component, ContentChild, Input, OnInit, TemplateRef } from '@angular/core';
import { ContentColumnListComponent } from './content-column-list.component';
@Component({
selector: 'content-column',
template: ''
})
export class ContentColumnComponent implements OnInit, AfterContentInit, DataColumn {
@Input()
key: string;
@Input()
type: string = 'text';
@Input()
format: string;
@Input()
sortable: boolean = false;
@Input()
title: string = '';
@ContentChild(TemplateRef)
template: any;
/**
* Title to be used for screen readers.
*/
@Input('sr-title')
srTitle: string;
@Input('class')
cssClass: string;
constructor(private list: ContentColumnListComponent, private logService: LogService) {
this.logService.log('ContentColumnComponent is deprecated starting with 1.7.0 and may be removed in future versions. Use DataColumnComponent instead.');
}
ngOnInit() {
if (!this.srTitle && this.key === '$thumbnail') {
this.srTitle = 'Thumbnail';
}
}
ngAfterContentInit() {
this.register();
}
register(): boolean {
if (this.list) {
return this.list.registerColumn(this);
}
return false;
}
}

View File

@@ -0,0 +1,85 @@
<adf-datatable
[selectionMode]="selectionMode"
[data]="data"
[actions]="contentActions"
[actionsPosition]="contentActionsPosition"
[multiselect]="multiselect"
[allowDropFiles]="allowDropFiles"
[contextMenu]="contextMenuActions"
[rowStyle]="rowStyle"
[rowStyleClass]="rowStyleClass"
[loading]="loading"
[noPermission]="noPermission"
[showHeader]="!isEmpty()"
(showRowContextMenu)="onShowRowContextMenu($event)"
(showRowActionsMenu)="onShowRowActionsMenu($event)"
(executeRowAction)="onExecuteRowAction($event)"
(rowClick)="onNodeClick($event.value?.node)"
(rowDblClick)="onNodeDblClick($event.value?.node)"
(row-select)="onNodeSelect($event.detail)"
(row-unselect)="onNodeUnselect($event.detail)">
<div *ngIf="!isEmptyTemplateDefined()">
<no-content-template>
<ng-template>
<adf-empty-list>
<div class="adf-empty-list_template adf-empty-folder">
<div class="adf-empty-folder-this-space-is-empty">{{'ADF-DOCUMENT-LIST.EMPTY.HEADER' | translate}}</div>
<div fxHide.lt-md="true" class="adf-empty-folder-drag-drop">{{ 'ADF-DATATABLE.EMPTY.DRAG-AND-DROP.TITLE' | translate }}</div>
<div fxHide.lt-md="true" class="adf-empty-folder-any-files-here-to-add">{{ 'ADF-DATATABLE.EMPTY.DRAG-AND-DROP.SUBTITLE' | translate }}</div>
<img class="adf-empty-folder-image" [src]="emptyFolderImageUrl">
</div>
<!-- <div adf-empty-list-header class="adf-empty-list-header"> {{'ADF-DOCUMENT-LIST.EMPTY.HEADER' | translate}} </div> -->
</adf-empty-list>
</ng-template>
</no-content-template>
</div>
<div *ngIf="!isNoPermissionTemplateDefined()">
<no-permission-template>
<ng-template>
<div class="adf-no-permission__template">
<mat-icon>ic_error</mat-icon>
<p class="adf-no-permission__template--text">{{ 'ADF-DOCUMENT-LIST.NO_PERMISSION' | translate }}</p>
</div>
</ng-template>
</no-permission-template>
</div>
<div>
<loading-content-template>
<ng-template>
<div class="adf-document-list-loading-container">
<mat-progress-spinner
id="adf-document-list-loading"
class="adf-document-list-loading-margin"
[color]="'primary'"
[mode]="'indeterminate'">
</mat-progress-spinner>
</div>
</ng-template>
</loading-content-template>
</div>
</adf-datatable>
<ng-container *ngIf="isPaginationEnabled()">
<adf-pagination
*ngIf="isPaginationNeeded()"
class="adf-documentlist-pagination"
(changePageSize)="onChangePageSize($event)"
(changePageNumber)="onChangePageNumber($event)"
(nextPage)="onNextPage($event)"
(prevPage)="onPrevPage($event)"
[pagination]="pagination"
[supportedPageSizes]="supportedPageSizes">
</adf-pagination>
<adf-infinite-pagination
*ngIf="!isPaginationNeeded()"
[pagination]="pagination"
[pageSize]="pageSize"
[loading]="infiniteLoading"
(loadMore)="loadNextBatch($event)">
{{ 'ADF-DOCUMENT-LIST.LAYOUT.LOAD_MORE' | translate }}
</adf-infinite-pagination>
</ng-container>

View File

@@ -0,0 +1,133 @@
@mixin adf-document-list-theme($theme) {
$foreground: map-get($theme, foreground);
$accent: map-get($theme, accent);
adf-datatable > table > tbody > tr.is-selected > td.adf-data-table-cell.adf-data-table-cell--image.image-table-cell > div > div > mat-icon > svg {
fill: mat-color($accent);
margin-top: -4px;
margin-left: -4px;
width: 32px;
height: 32px;
}
.document-list_empty_template {
text-align: center;
margin-top: 20px;
margin-bottom: 20px;
}
.document-list__this-space-is-empty {
height: 32px;
opacity: 0.26;
font-size: 24px;
line-height: 1.33;
letter-spacing: -1px;
color: mat-color($foreground, text);
}
.document-list__drag-drop {
height: 56px;
opacity: 0.54;
font-size: 56px;
line-height: 1;
letter-spacing: -2px;
color: mat-color($foreground, text);
margin-top: 40px;
}
.document-list__any-files-here-to-add {
height: 24px;
opacity: 0.54;
font-size: 16px;
line-height: 1.5;
letter-spacing: -0.4px;
color: mat-color($foreground, text)0;
margin-top: 17px;
}
.document-list__empty_doc_lib {
width: 565px;
height: 161px;
object-fit: contain;
margin-top: 17px;
}
.adf-document-list-loading-margin {
margin: auto;
}
.adf-document-list-loading-container {
min-height: 300px;
display: flex;
flex-direction: row;
height: 100%;
}
.adf-empty-list-header {
height: 32px;
opacity: 0.26 !important;
font-size: 24px;
line-height: 1.33;
letter-spacing: -1px;
color: mat-color($foreground, text);
}
.adf-documentlist-pagination {
color: mat-color($foreground, text);
.adf-pagination__block {
border-right: none;
}
}
.adf-empty-folder {
&-this-space-is-empty {
height: 32px;
opacity: 0.26;
font-size: 24px;
line-height: 1.33;
letter-spacing: -1px;
color: mat-color($foreground, text);
}
&-drag-drop {
min-height: 56px;
opacity: 0.54;
font-size: 53px;
line-height: 1;
letter-spacing: -2px;
color: mat-color($foreground, text);
margin-top: 40px;
word-break: break-all;
white-space: pre-line;
@media screen and ($mat-xsmall) {
font-size: 48px;
}
}
&-any-files-here-to-add {
min-height: 24px;
opacity: 0.54;
font-size: 16px;
line-height: 1.5;
letter-spacing: -0.4px;
color: mat-color($foreground, text);
margin-top: 17px;
word-break: break-all;
white-space: pre-line;
}
&-image {
width: 565px;
max-width: 100%;
object-fit: contain;
margin-top: 17px;
@media screen and ($mat-xsmall) {
width: 250px;
}
}
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,894 @@
/*!
* @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 {
DataCellEvent,
DataColumn,
DataRowActionEvent,
DataSorting,
DataTableComponent,
ObjectDataColumn
} from '@alfresco/core';
import { AlfrescoApiService, AppConfigService, DataColumnListComponent, UserPreferencesService } from '@alfresco/core';
import {
AfterContentInit, Component, ContentChild, ElementRef, EventEmitter, HostListener, Input, NgZone,
OnChanges, OnInit, Output, SimpleChanges, TemplateRef, ViewChild, ViewEncapsulation
} from '@angular/core';
import {
DeletedNodesPaging,
MinimalNodeEntity,
MinimalNodeEntryEntity,
NodePaging,
Pagination,
PersonEntry,
SitePaging
} from 'alfresco-js-api';
import { Observable, Subject } from 'rxjs/Rx';
import { presetsDefaultModel } from '../models/preset.model';
import { ImageResolver } from './../data/image-resolver.model';
import { RowFilter } from './../data/row-filter.model';
import { ShareDataRow } from './../data/share-data-row.model';
import { ShareDataTableAdapter } from './../data/share-datatable-adapter';
import { ContentActionModel } from './../models/content-action.model';
import { PermissionStyleModel } from './../models/permissions-style.model';
import { DocumentListService } from './../services/document-list.service';
import { NodeEntityEvent, NodeEntryEvent } from './node.event';
declare var require: any;
export enum PaginationStrategy {
Finite,
Infinite
}
@Component({
selector: 'adf-document-list',
styleUrls: ['./document-list.component.scss'],
templateUrl: './document-list.component.html',
encapsulation: ViewEncapsulation.None
})
export class DocumentListComponent implements OnInit, OnChanges, AfterContentInit {
static SINGLE_CLICK_NAVIGATION: string = 'click';
static DOUBLE_CLICK_NAVIGATION: string = 'dblclick';
static DEFAULT_PAGE_SIZE: number = 20;
@ContentChild(DataColumnListComponent) columnList: DataColumnListComponent;
@Input()
permissionsStyle: PermissionStyleModel[] = [];
@Input()
locationFormat: string = '/';
@Input()
navigate: boolean = true;
@Input()
navigationMode: string = DocumentListComponent.DOUBLE_CLICK_NAVIGATION; // click|dblclick
@Input()
thumbnails: boolean = false;
@Input()
selectionMode: string = 'single'; // null|single|multiple
@Input()
multiselect: boolean = false;
@Input()
enablePagination: boolean = true;
@Input()
contentActions: boolean = false;
@Input()
contentActionsPosition: string = 'right'; // left|right
@Input()
contextMenuActions: boolean = false;
@Input()
pageSize: number = DocumentListComponent.DEFAULT_PAGE_SIZE;
@Input()
emptyFolderImageUrl: string = require('../../assets/images/empty_doc_lib.svg');
@Input()
allowDropFiles: boolean = false;
@Input()
sorting: string[];
@Input()
rowStyle: string;
@Input()
rowStyleClass: string;
@Input()
loading: boolean = false;
@Input()
paginationStrategy: PaginationStrategy = PaginationStrategy.Finite;
@Input()
supportedPageSizes: number[];
infiniteLoading: boolean = false;
noPermission: boolean = false;
selection = new Array<MinimalNodeEntity>();
skipCount: number = 0;
pagination: Pagination;
@Input()
rowFilter: RowFilter | null = null;
@Input()
imageResolver: ImageResolver | null = null;
// The identifier of a node. You can also use one of these well-known aliases: -my- | -shared- | -root-
@Input()
currentFolderId: string = null;
@Input()
folderNode: MinimalNodeEntryEntity = null;
@Input()
node: NodePaging = null;
@Output()
nodeClick: EventEmitter<NodeEntityEvent> = new EventEmitter<NodeEntityEvent>();
@Output()
nodeDblClick: EventEmitter<NodeEntityEvent> = new EventEmitter<NodeEntityEvent>();
@Output()
folderChange: EventEmitter<NodeEntryEvent> = new EventEmitter<NodeEntryEvent>();
@Output()
preview: EventEmitter<NodeEntityEvent> = new EventEmitter<NodeEntityEvent>();
@Output()
ready: EventEmitter<any> = new EventEmitter();
@Output()
error: EventEmitter<any> = new EventEmitter();
@ViewChild(DataTableComponent)
dataTable: DataTableComponent;
errorMessage;
actions: ContentActionModel[] = [];
emptyFolderTemplate: TemplateRef<any>;
noPermissionTemplate: TemplateRef<any>;
contextActionHandler: Subject<any> = new Subject();
data: ShareDataTableAdapter;
private layoutPresets = {};
private currentNodeAllowableOperations: string[] = [];
private CREATE_PERMISSION = 'create';
private defaultPageSizes = [5, 10, 15, 20];
constructor(private documentListService: DocumentListService,
private ngZone: NgZone,
private elementRef: ElementRef,
private apiService: AlfrescoApiService,
private appConfig: AppConfigService,
private preferences: UserPreferencesService) {
this.supportedPageSizes = appConfig.get('document-list.supportedPageSizes', this.defaultPageSizes);
}
getContextActions(node: MinimalNodeEntity) {
if (node && node.entry) {
let actions = this.getNodeActions(node);
if (actions && actions.length > 0) {
return actions.map((currentAction: ContentActionModel) => {
return {
model: currentAction,
node: node,
subject: this.contextActionHandler
};
});
}
}
return null;
}
contextActionCallback(action) {
if (action) {
this.executeContentAction(action.node, action.model);
}
}
get hasCustomLayout(): boolean {
return this.columnList && this.columnList.columns && this.columnList.columns.length > 0;
}
getDefaultPageSize(): number {
let result = this.preferences.paginationSize;
const pageSizes = this.supportedPageSizes || this.defaultPageSizes;
if (pageSizes && pageSizes.length > 0 && pageSizes.indexOf(result) < 0) {
result = pageSizes[0];
}
return result;
}
ngOnInit() {
this.pageSize = this.getDefaultPageSize();
this.loadLayoutPresets();
this.data = new ShareDataTableAdapter(this.documentListService, null, this.getDefaultSorting());
this.data.thumbnails = this.thumbnails;
this.data.permissionsStyle = this.permissionsStyle;
if (this.rowFilter) {
this.data.setFilter(this.rowFilter);
}
if (this.imageResolver) {
this.data.setImageResolver(this.imageResolver);
}
this.contextActionHandler.subscribe(val => this.contextActionCallback(val));
this.enforceSingleClickNavigationForMobile();
}
ngAfterContentInit() {
let schema: DataColumn[] = [];
if (this.hasCustomLayout) {
schema = this.columnList.columns.map(c => <DataColumn> c);
}
if (!this.data) {
this.data = new ShareDataTableAdapter(this.documentListService, schema, this.getDefaultSorting());
} else if (schema && schema.length > 0) {
this.data.setColumns(schema);
}
let columns = this.data.getColumns();
if (!columns || columns.length === 0) {
this.setupDefaultColumns(this.currentFolderId);
}
}
ngOnChanges(changes: SimpleChanges) {
if (changes.folderNode && changes.folderNode.currentValue) {
this.loadFolder();
} else if (changes.currentFolderId && changes.currentFolderId.currentValue) {
if (changes.currentFolderId.previousValue !== changes.currentFolderId.currentValue) {
this.resetPagination();
this.folderNode = null;
}
if (!this.hasCustomLayout) {
this.setupDefaultColumns(changes.currentFolderId.currentValue);
}
this.loadFolderByNodeId(changes.currentFolderId.currentValue);
} else if (this.data) {
if (changes.node && changes.node.currentValue) {
this.resetSelection();
this.data.loadPage(changes.node.currentValue);
} else if (changes.rowFilter) {
this.data.setFilter(changes.rowFilter.currentValue);
if (this.currentFolderId) {
this.loadFolderNodesByFolderNodeId(this.currentFolderId, this.pageSize, this.skipCount);
}
} else if (changes.imageResolver) {
this.data.setImageResolver(changes.imageResolver.currentValue);
}
}
}
reload(merge: boolean = false) {
this.ngZone.run(() => {
this.resetSelection();
if (this.folderNode) {
this.loadFolder(merge);
} else if (this.currentFolderId) {
this.loadFolderByNodeId(this.currentFolderId);
} else if (this.node) {
this.data.loadPage(this.node);
this.ready.emit();
}
});
}
isEmptyTemplateDefined(): boolean {
if (this.dataTable) {
if (this.emptyFolderTemplate) {
return true;
}
}
return false;
}
isNoPermissionTemplateDefined(): boolean {
if (this.dataTable) {
if (this.noPermissionTemplate) {
return true;
}
}
return false;
}
isMobile(): boolean {
return !!/Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent);
}
isEmpty() {
return !this.data || this.data.getRows().length === 0;
}
isPaginationEnabled() {
return this.enablePagination && !this.isEmpty();
}
isPaginationNeeded() {
return this.paginationStrategy === PaginationStrategy.Finite;
}
getNodeActions(node: MinimalNodeEntity | any): ContentActionModel[] {
let target = null;
if (node && node.entry) {
if (node.entry.isFile) {
target = 'document';
}
if (node.entry.isFolder) {
target = 'folder';
}
if (target) {
let ltarget = target.toLowerCase();
let actionsByTarget = this.actions.filter(entry => {
return entry.target.toLowerCase() === ltarget;
}).map(action => new ContentActionModel(action));
actionsByTarget.forEach((action) => {
this.checkPermission(node, action);
});
return actionsByTarget;
}
}
return [];
}
checkPermission(node: any, action: ContentActionModel): ContentActionModel {
if (action.permission) {
if (this.hasPermissions(node)) {
let permissions = node.entry.allowableOperations;
let findPermission = permissions.find(permission => permission === action.permission);
if (!findPermission && action.disableWithNoPermission === true) {
action.disabled = true;
}
}
}
return action;
}
private hasPermissions(node: any): boolean {
return node.entry.allowableOperations ? true : false;
}
@HostListener('contextmenu', ['$event'])
onShowContextMenu(e?: Event) {
if (e && this.contextMenuActions) {
e.preventDefault();
}
}
performNavigation(node: MinimalNodeEntity): boolean {
if (this.canNavigateFolder(node)) {
this.currentFolderId = node.entry.id;
this.folderNode = node.entry;
this.skipCount = 0;
this.currentNodeAllowableOperations = node.entry['allowableOperations'] ? node.entry['allowableOperations'] : [];
this.loadFolder();
this.folderChange.emit(new NodeEntryEvent(node.entry));
return true;
}
return false;
}
/**
* Invoked when executing content action for a document or folder.
* @param node Node to be the context of the execution.
* @param action Action to be executed against the context.
*/
executeContentAction(node: MinimalNodeEntity, action: ContentActionModel) {
if (node && node.entry && action) {
let handlerSub;
if (typeof action.handler === 'function') {
handlerSub = action.handler(node, this, action.permission);
} else {
handlerSub = Observable.of(true);
}
if (typeof action.execute === 'function') {
handlerSub.subscribe(() => {
action.execute(node);
});
}
}
}
loadFolder(merge: boolean = false) {
if (merge) {
this.infiniteLoading = true;
} else {
this.loading = true;
}
let nodeId = this.folderNode ? this.folderNode.id : this.currentFolderId;
if (nodeId) {
this.loadFolderNodesByFolderNodeId(nodeId, this.pageSize, this.skipCount, merge).catch(err => this.error.emit(err));
}
}
// gets folder node and its content
loadFolderByNodeId(nodeId: string) {
this.loading = true;
this.resetSelection();
if (nodeId === '-trashcan-') {
this.loadTrashcan();
} else if (nodeId === '-sharedlinks-') {
this.loadSharedLinks();
} else if (nodeId === '-sites-') {
this.loadSites();
} else if (nodeId === '-mysites-') {
this.loadMemberSites();
} else if (nodeId === '-favorites-') {
this.loadFavorites();
} else if (nodeId === '-recent-') {
this.loadRecent();
} else {
this.documentListService
.getFolderNode(nodeId)
.then(node => {
this.folderNode = node;
this.currentFolderId = node.id;
this.skipCount = 0;
this.currentNodeAllowableOperations = node['allowableOperations'] ? node['allowableOperations'] : [];
return this.loadFolderNodesByFolderNodeId(node.id, this.pageSize, this.skipCount);
})
.catch(err => {
if (JSON.parse(err.message).error.statusCode === 403) {
this.loading = false;
this.noPermission = true;
}
this.error.emit(err);
});
}
}
loadFolderNodesByFolderNodeId(id: string, maxItems: number, skipCount: number, merge: boolean = false): Promise<any> {
return new Promise((resolve, reject) => {
this.resetSelection();
this.documentListService
.getFolder(null, {
maxItems: maxItems,
skipCount: skipCount,
rootFolderId: id
})
.subscribe(
val => {
if (this.isCurrentPageEmpty(val, skipCount)) {
this.updateSkipCount(skipCount - maxItems);
this.loadFolderNodesByFolderNodeId(id, maxItems, skipCount - maxItems).then(
() => resolve(true),
error => reject(error)
);
} else {
this.data.loadPage(<NodePaging> val, merge);
this.pagination = val.list.pagination;
this.loading = false;
this.infiniteLoading = false;
this.ready.emit();
resolve(true);
}
},
error => {
reject(error);
});
});
}
resetSelection() {
this.dataTable.resetSelection();
this.selection = [];
}
resetPagination() {
this.skipCount = 0;
}
private loadTrashcan(): void {
const options = {
include: ['path', 'properties'],
maxItems: this.pageSize,
skipCount: this.skipCount
};
this.apiService.nodesApi.getDeletedNodes(options)
.then((page: DeletedNodesPaging) => this.onPageLoaded(page))
.catch(error => this.error.emit(error));
}
private loadSharedLinks(): void {
const options = {
include: ['properties', 'allowableOperations', 'path'],
maxItems: this.pageSize,
skipCount: this.skipCount
};
this.apiService.sharedLinksApi.findSharedLinks(options)
.then((page: NodePaging) => this.onPageLoaded(page))
.catch(error => this.error.emit(error));
}
private loadSites(): void {
const options = {
include: ['properties'],
maxItems: this.pageSize,
skipCount: this.skipCount
};
this.apiService.sitesApi.getSites(options)
.then((page: NodePaging) => this.onPageLoaded(page))
.catch(error => this.error.emit(error));
}
private loadMemberSites(): void {
const options = {
include: ['properties'],
maxItems: this.pageSize,
skipCount: this.skipCount
};
this.apiService.peopleApi.getSiteMembership('-me-', options)
.then((result: SitePaging) => {
let page: NodePaging = {
list: {
entries: result.list.entries
.map(({ entry: { site } }: any) => ({
entry: site
})),
pagination: result.list.pagination
}
};
this.onPageLoaded(page);
})
.catch(error => this.error.emit(error));
}
private loadFavorites(): void {
const options = {
maxItems: this.pageSize,
skipCount: this.skipCount,
where: '(EXISTS(target/file) OR EXISTS(target/folder))',
include: ['properties', 'allowableOperations', 'path']
};
this.apiService.favoritesApi.getFavorites('-me-', options)
.then((result: NodePaging) => {
let page: NodePaging = {
list: {
entries: result.list.entries
.map(({ entry: { target } }: any) => ({
entry: target.file || target.folder
}))
.map(({ entry }: any) => {
entry.properties = {
'cm:title': entry.title,
'cm:description': entry.description
};
return { entry };
}),
pagination: result.list.pagination
}
};
this.onPageLoaded(page);
})
.catch(error => this.error.emit(error));
}
private loadRecent(): void {
this.apiService.peopleApi.getPerson('-me-')
.then((person: PersonEntry) => {
const username = person.entry.id;
const query = {
query: {
query: '*',
language: 'afts'
},
filterQueries: [
{ query: `cm:modified:[NOW/DAY-30DAYS TO NOW/DAY+1DAY]` },
{ query: `cm:modifier:${username} OR cm:creator:${username}` },
{ query: `TYPE:"content" AND -TYPE:"app:filelink" AND -TYPE:"fm:post"` }
],
include: ['path', 'properties', 'allowableOperations'],
sort: [{
type: 'FIELD',
field: 'cm:modified',
ascending: false
}],
paging: {
maxItems: this.pageSize,
skipCount: this.skipCount
}
};
return this.apiService.searchApi.search(query);
})
.then((page: NodePaging) => this.onPageLoaded(page))
.catch(error => this.error.emit(error));
}
private onPageLoaded(page: NodePaging) {
if (page) {
this.data.loadPage(page);
this.pagination = page.list.pagination;
this.loading = false;
this.ready.emit();
}
}
private isCurrentPageEmpty(node, skipCount): boolean {
return !this.hasNodeEntries(node) && this.hasPages(skipCount);
}
private hasPages(skipCount): boolean {
return skipCount > 0 && this.isPaginationEnabled();
}
private hasNodeEntries(node): boolean {
return node && node.list && node.list.entries && node.list.entries.length > 0;
}
/**
* Creates a set of predefined columns.
*/
setupDefaultColumns(preset: string = 'default'): void {
if (this.data) {
const columns = this.getLayoutPreset(preset);
this.data.setColumns(columns);
}
}
onPreviewFile(node: MinimalNodeEntity) {
if (node) {
this.preview.emit(new NodeEntityEvent(node));
}
}
onNodeClick(node: MinimalNodeEntity) {
const domEvent = new CustomEvent('node-click', {
detail: {
sender: this,
node: node
},
bubbles: true
});
this.elementRef.nativeElement.dispatchEvent(domEvent);
const event = new NodeEntityEvent(node);
this.nodeClick.emit(event);
if (!event.defaultPrevented) {
if (this.navigate && this.navigationMode === DocumentListComponent.SINGLE_CLICK_NAVIGATION) {
if (node && node.entry) {
if (node.entry.isFile) {
this.onPreviewFile(node);
}
if (node.entry.isFolder) {
this.performNavigation(node);
}
}
}
}
}
onNodeDblClick(node: MinimalNodeEntity) {
const domEvent = new CustomEvent('node-dblclick', {
detail: {
sender: this,
node: node
},
bubbles: true
});
this.elementRef.nativeElement.dispatchEvent(domEvent);
const event = new NodeEntityEvent(node);
this.nodeDblClick.emit(event);
if (!event.defaultPrevented) {
if (this.navigate && this.navigationMode === DocumentListComponent.DOUBLE_CLICK_NAVIGATION) {
if (node && node.entry) {
if (node.entry.isFile) {
this.onPreviewFile(node);
}
if (node.entry.isFolder) {
this.performNavigation(node);
}
}
}
}
}
onNodeSelect(event: { row: ShareDataRow, selection: Array<ShareDataRow> }) {
this.selection = event.selection.map(entry => entry.node);
const domEvent = new CustomEvent('node-select', {
detail: {
node: event.row.node,
selection: this.selection
},
bubbles: true
});
this.elementRef.nativeElement.dispatchEvent(domEvent);
}
onNodeUnselect(event: { row: ShareDataRow, selection: Array<ShareDataRow> }) {
this.selection = event.selection.map(entry => entry.node);
const domEvent = new CustomEvent('node-unselect', {
detail: {
node: event.row.node,
selection: this.selection
},
bubbles: true
});
this.elementRef.nativeElement.dispatchEvent(domEvent);
}
onShowRowContextMenu(event: DataCellEvent) {
if (this.contextMenuActions) {
let args = event.value;
let node = (<ShareDataRow> args.row).node;
if (node) {
args.actions = this.getContextActions(node) || [];
}
}
}
onShowRowActionsMenu(event: DataCellEvent) {
if (this.contentActions) {
let args = event.value;
let node = (<ShareDataRow> args.row).node;
if (node) {
args.actions = this.getNodeActions(node) || [];
}
}
}
onExecuteRowAction(event: DataRowActionEvent) {
if (this.contentActions) {
let args = event.value;
let node = (<ShareDataRow> args.row).node;
let action = (<ContentActionModel> args.action);
this.executeContentAction(node, action);
}
}
onChangePageSize(event: Pagination): void {
this.preferences.paginationSize = event.maxItems;
this.pageSize = event.maxItems;
this.skipCount = 0;
this.reload();
}
onChangePageNumber(page: Pagination): void {
this.pageSize = page.maxItems;
this.skipCount = page.skipCount;
this.reload();
}
onNextPage(event: Pagination): void {
this.skipCount = event.skipCount;
this.reload();
}
loadNextBatch(event: Pagination) {
this.skipCount = event.skipCount;
this.reload(true);
}
onPrevPage(event: Pagination): void {
this.skipCount = event.skipCount;
this.reload();
}
private enforceSingleClickNavigationForMobile(): void {
if (this.isMobile()) {
this.navigationMode = DocumentListComponent.SINGLE_CLICK_NAVIGATION;
}
}
private getDefaultSorting(): DataSorting {
let defaultSorting: DataSorting;
if (this.sorting) {
const [key, direction] = this.sorting;
defaultSorting = new DataSorting(key, direction);
}
return defaultSorting;
}
canNavigateFolder(node: MinimalNodeEntity): boolean {
if (this.isCustomSource(this.currentFolderId)) {
return false;
}
if (node && node.entry && node.entry.isFolder) {
return true;
}
return false;
}
isCustomSource(folderId: string): boolean {
const sources = ['-trashcan-', '-sharedlinks-', '-sites-', '-mysites-', '-favorites-', '-recent-'];
if (sources.indexOf(folderId) > -1) {
return true;
}
return false;
}
updateSkipCount(newSkipCount) {
this.skipCount = newSkipCount;
}
hasCurrentNodePermission(permission: string): boolean {
let hasPermission: boolean = false;
if (this.currentNodeAllowableOperations.length > 0) {
let permFound = this.currentNodeAllowableOperations.find(element => element === permission);
hasPermission = permFound ? true : false;
}
return hasPermission;
}
hasCreatePermission() {
return this.hasCurrentNodePermission(this.CREATE_PERMISSION);
}
private loadLayoutPresets(): void {
const externalSettings = this.appConfig.get('document-list.presets', null);
if (externalSettings) {
this.layoutPresets = Object.assign({}, presetsDefaultModel, externalSettings);
} else {
this.layoutPresets = presetsDefaultModel;
}
}
private getLayoutPreset(name: string = 'default'): DataColumn[] {
return (this.layoutPresets[name] || this.layoutPresets['default']).map(col => new ObjectDataColumn(col));
}
}

View File

@@ -0,0 +1,68 @@
/*!
* @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 { CUSTOM_ELEMENTS_SCHEMA } from '@angular/core';
import { async, TestBed } from '@angular/core/testing';
import { DataTableComponent, DataTableModule } from '@alfresco/core';
import { MaterialModule } from '../../../material.module';
import { DocumentListService } from '../../services/document-list.service';
import { DocumentListComponent } from './../document-list.component';
import { EmptyFolderContentDirective } from './empty-folder-content.directive';
describe('EmptyFolderContent', () => {
let emptyFolderContent: EmptyFolderContentDirective;
let documentList: DocumentListComponent;
beforeEach(async(() => {
TestBed.configureTestingModule({
imports: [
DataTableModule,
MaterialModule
],
declarations: [
DocumentListComponent
],
providers: [
DocumentListService
],
schemas: [
CUSTOM_ELEMENTS_SCHEMA
]
}).compileComponents();
}));
beforeEach(() => {
documentList = (TestBed.createComponent(DocumentListComponent).componentInstance as DocumentListComponent);
documentList.dataTable = new DataTableComponent(null, null);
emptyFolderContent = new EmptyFolderContentDirective(documentList);
});
it('is defined', () => {
expect(emptyFolderContent).toBeDefined();
});
it('set template', () => {
emptyFolderContent.template = '<example>';
emptyFolderContent.ngAfterContentInit();
expect(emptyFolderContent.template).toBe(documentList.emptyFolderTemplate);
expect(emptyFolderContent.template).toBe(documentList.dataTable.noContentTemplate);
});
});

View File

@@ -0,0 +1,36 @@
/*!
* @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 { AfterContentInit, ContentChild, Directive, TemplateRef } from '@angular/core';
import { DocumentListComponent } from './../document-list.component';
@Directive({
selector: 'empty-folder-content'
})
export class EmptyFolderContentDirective implements AfterContentInit {
@ContentChild(TemplateRef)
template: any;
constructor(private documentList: DocumentListComponent) {
}
ngAfterContentInit() {
this.documentList.emptyFolderTemplate = this.template;
this.documentList.dataTable.noContentTemplate = this.template;
}
}

View File

@@ -0,0 +1,66 @@
/*!
* @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 { async, TestBed } from '@angular/core/testing';
import { MatProgressSpinnerModule } from '@angular/material';
import { DataTableComponent, DataTableModule } from '@alfresco/core';
import { DocumentListService } from '../../services/document-list.service';
import { MaterialModule } from '../../../material.module';
import { DocumentListComponent } from './../document-list.component';
import { NoPermissionContentDirective } from './no-permission-content.directive';
describe('NoPermissionContentDirective', () => {
let noPermissionContent: NoPermissionContentDirective;
let documentList: DocumentListComponent;
beforeEach(async(() => {
TestBed.configureTestingModule({
imports: [
MaterialModule,
DataTableModule,
MatProgressSpinnerModule
],
declarations: [
DocumentListComponent
],
providers: [
DocumentListService
]
}).compileComponents();
}));
beforeEach(() => {
documentList = (TestBed.createComponent(DocumentListComponent).componentInstance as DocumentListComponent);
documentList.dataTable = new DataTableComponent(null, null);
noPermissionContent = new NoPermissionContentDirective(documentList);
});
it('should be defined', () => {
expect(noPermissionContent).toBeDefined();
});
it('should set template', () => {
noPermissionContent.template = '<example>';
noPermissionContent.ngAfterContentInit();
expect(noPermissionContent.template).toBe(documentList.noPermissionTemplate);
expect(noPermissionContent.template).toBe(documentList.dataTable.noPermissionTemplate);
});
});

View File

@@ -0,0 +1,36 @@
/*!
* @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 { AfterContentInit, ContentChild, Directive, TemplateRef } from '@angular/core';
import { DocumentListComponent } from './../document-list.component';
@Directive({
selector: 'no-permission-content'
})
export class NoPermissionContentDirective implements AfterContentInit {
@ContentChild(TemplateRef)
template: any;
constructor(private documentList: DocumentListComponent) {
}
ngAfterContentInit() {
this.documentList.noPermissionTemplate = this.template;
this.documentList.dataTable.noPermissionTemplate = this.template;
}
}

View File

@@ -0,0 +1,33 @@
/*!
* @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 { BaseEvent } from '@alfresco/core';
import { MinimalNodeEntity, MinimalNodeEntryEntity } from 'alfresco-js-api';
export class NodeEntityEvent extends BaseEvent<MinimalNodeEntity> {
constructor(entity: MinimalNodeEntity) {
super();
this.value = entity;
}
}
export class NodeEntryEvent extends BaseEvent<MinimalNodeEntryEntity> {
constructor(entity: MinimalNodeEntryEntity) {
super();
this.value = entity;
}
}

View File

@@ -0,0 +1,20 @@
/*!
* @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 { DataColumn, DataRow } from '@alfresco/core';
export type ImageResolver = (row: DataRow, column: DataColumn) => string;

View File

@@ -0,0 +1,20 @@
/*!
* @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 { ShareDataRow } from './share-data-row.model';
export type RowFilter = (value: ShareDataRow, index: number, array: ShareDataRow[]) => any;

View File

@@ -0,0 +1,97 @@
/*!
* @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 { DataRow } from '@alfresco/core';
import { ObjectUtils } from '@alfresco/core';
import { MinimalNode, MinimalNodeEntity } from 'alfresco-js-api';
import { PermissionStyleModel } from './../models/permissions-style.model';
import { DocumentListService } from './../services/document-list.service';
export class ShareDataRow implements DataRow {
static ERR_OBJECT_NOT_FOUND: string = 'Object source not found';
cache: { [key: string]: any } = {};
isSelected: boolean = false;
isDropTarget: boolean;
cssClass: string = '';
get node(): MinimalNodeEntity {
return this.obj;
}
constructor(private obj: MinimalNodeEntity, private documentListService: DocumentListService, private permissionsStyle: PermissionStyleModel[]) {
if (!obj) {
throw new Error(ShareDataRow.ERR_OBJECT_NOT_FOUND);
}
this.isDropTarget = this.isFolderAndHasPermissionToUpload(obj);
if (permissionsStyle) {
this.cssClass = this.getPermissionClass(obj);
}
}
getPermissionClass(nodeEntity: MinimalNodeEntity): string {
let permissionsClasses = '';
this.permissionsStyle.forEach((currentPermissionsStyle: PermissionStyleModel) => {
if (this.applyPermissionStyleToFolder(nodeEntity.entry, currentPermissionsStyle) || this.applyPermissionStyleToFile(nodeEntity.entry, currentPermissionsStyle)) {
if (this.documentListService.hasPermission(nodeEntity.entry, currentPermissionsStyle.permission)) {
permissionsClasses += ` ${currentPermissionsStyle.css}`;
}
}
});
return permissionsClasses;
}
private applyPermissionStyleToFile(node: MinimalNode, currentPermissionsStyle: PermissionStyleModel): boolean {
return (currentPermissionsStyle.isFile && node.isFile);
}
private applyPermissionStyleToFolder(node: MinimalNode, currentPermissionsStyle: PermissionStyleModel): boolean {
return (currentPermissionsStyle.isFolder && node.isFolder);
}
isFolderAndHasPermissionToUpload(obj: MinimalNodeEntity): boolean {
return this.isFolder(obj) && this.documentListService.hasPermission(obj.entry, 'create');
}
isFolder(obj: MinimalNodeEntity): boolean {
return obj.entry && obj.entry.isFolder;
}
cacheValue(key: string, value: any): any {
this.cache[key] = value;
return value;
}
getValue(key: string): any {
if (this.cache[key] !== undefined) {
return this.cache[key];
}
return ObjectUtils.getValue(this.obj.entry, key);
}
hasValue(key: string): boolean {
return this.getValue(key) !== undefined;
}
}

View File

@@ -0,0 +1,485 @@
/*!
* @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 { async, TestBed } from '@angular/core/testing';
import { DataColumn, DataRow, DataSorting } from '@alfresco/core';
import { FileNode, FolderNode } from './../../mock';
import { DocumentListService } from './../services/document-list.service';
import { ShareDataRow } from './share-data-row.model';
import { ShareDataTableAdapter } from './share-datatable-adapter';
describe('ShareDataTableAdapter', () => {
let documentListService: DocumentListService;
beforeEach(async(() => {
TestBed.configureTestingModule({
imports: [],
providers: [
DocumentListService
]
}).compileComponents();
}));
beforeEach(() => {
documentListService = TestBed.get(DocumentListService);
});
it('should setup rows and columns with constructor', () => {
let schema = [<DataColumn> {}];
let adapter = new ShareDataTableAdapter(documentListService, schema);
expect(adapter.getRows()).toEqual([]);
expect(adapter.getColumns()).toEqual(schema);
});
it('should setup columns when constructor is missing schema', () => {
let adapter = new ShareDataTableAdapter(documentListService, null);
expect(adapter.getColumns()).toEqual([]);
});
it('should set new columns', () => {
let columns = [<DataColumn> {}, <DataColumn> {}];
let adapter = new ShareDataTableAdapter(documentListService, null);
adapter.setColumns(columns);
expect(adapter.getColumns()).toEqual(columns);
});
it('should reset columns', () => {
let columns = [<DataColumn> {}, <DataColumn> {}];
let adapter = new ShareDataTableAdapter(documentListService, columns);
expect(adapter.getColumns()).toEqual(columns);
adapter.setColumns(null);
expect(adapter.getColumns()).toEqual([]);
});
it('should set new rows', () => {
let rows = [<DataRow> {}, <DataRow> {}];
let adapter = new ShareDataTableAdapter(documentListService, null);
expect(adapter.getRows()).toEqual([]);
adapter.setRows(rows);
expect(adapter.getRows()).toEqual(rows);
});
it('should reset rows', () => {
let rows = [<DataRow> {}, <DataRow> {}];
let adapter = new ShareDataTableAdapter(documentListService, null);
adapter.setRows(rows);
expect(adapter.getRows()).toEqual(rows);
adapter.setRows(null);
expect(adapter.getRows()).toEqual([]);
});
it('should sort new rows', () => {
let adapter = new ShareDataTableAdapter(documentListService, null);
spyOn(adapter, 'sort').and.callThrough();
let rows = [<DataRow> {}];
adapter.setRows(rows);
expect(adapter.sort).toHaveBeenCalled();
});
it('should fail when getting value for missing row', () => {
let adapter = new ShareDataTableAdapter(documentListService, null);
let check = () => {
return adapter.getValue(null, <DataColumn> {});
};
expect(check).toThrowError(adapter.ERR_ROW_NOT_FOUND);
});
it('should fail when getting value for missing column', () => {
let adapter = new ShareDataTableAdapter(documentListService, null);
let check = () => {
return adapter.getValue(<DataRow> {}, null);
};
expect(check).toThrowError(adapter.ERR_COL_NOT_FOUND);
});
it('should covert cell value to formatted date', () => {
let rawValue = new Date(2015, 6, 15, 21, 43, 11); // Wed Jul 15 2015 21:43:11 GMT+0100 (BST);
let dateValue = 'Jul 15, 2015, 9:43:11 PM';
let file = new FileNode();
file.entry.createdAt = rawValue;
let col = <DataColumn> {
key: 'createdAt',
type: 'date',
format: 'medium' // Jul 15, 2015, 9:43:11 PM
};
let row = new ShareDataRow(file, documentListService, null);
let adapter = new ShareDataTableAdapter(documentListService, null);
let value = adapter.getValue(row, col);
expect(value).toBe(dateValue);
});
it('should use default date format as fallback', () => {
let rawValue = new Date(2015, 6, 15, 21, 43, 11); // Wed Jul 15 2015 21:43:11 GMT+0100 (BST);
let dateValue = 'Jul 15, 2015, 9:43:11 PM';
let file = new FileNode();
file.entry.createdAt = rawValue;
let col = <DataColumn> {
key: 'createdAt',
type: 'date',
format: null
};
let row = new ShareDataRow(file, documentListService, null);
let adapter = new ShareDataTableAdapter(documentListService, null);
let value = adapter.getValue(row, col);
expect(value).toBe(dateValue);
});
it('should return date value as string', () => {
let rawValue = new Date(2015, 6, 15, 21, 43, 11); // Wed Jul 15 2015 21:43:11 GMT+0100 (BST);
let file = new FileNode();
file.entry.createdAt = rawValue;
let col = <DataColumn> {
key: 'createdAt',
type: 'string'
};
let row = new ShareDataRow(file, documentListService, null);
let adapter = new ShareDataTableAdapter(documentListService, null);
let value = adapter.getValue(row, col);
expect(value).toBe(rawValue);
});
it('should log error when having date conversion issues', () => {
let dateValue = <Date> {};
let file = new FileNode();
file.entry.createdAt = <any> dateValue;
let col = <DataColumn> {
key: 'createdAt',
type: 'date',
format: 'medium'
};
let row = new ShareDataRow(file, documentListService, null);
let adapter = new ShareDataTableAdapter(documentListService, null);
spyOn(console, 'error').and.stub();
let value = adapter.getValue(row, col);
expect(value).toBe('Error');
expect(console.error).toHaveBeenCalled();
});
it('should generate fallback icon for a file thumbnail with missing mime type', () => {
let adapter = new ShareDataTableAdapter(documentListService, null);
let file = new FileNode();
file.entry.content.mimeType = null;
let row = new ShareDataRow(file, documentListService, null);
let col = <DataColumn> {type: 'image', key: '$thumbnail'};
let value = adapter.getValue(row, col);
expect(value).toContain(`assets/images/ft_ic_miscellaneous`);
expect(value).toContain(`svg`);
});
it('should generate fallback icon for a file with no content entry', () => {
let adapter = new ShareDataTableAdapter(documentListService, null);
let file = new FileNode();
file.entry.content = null;
let row = new ShareDataRow(file, documentListService, null);
let col = <DataColumn> {type: 'image', key: '$thumbnail'};
let value = adapter.getValue(row, col);
expect(value).toContain(`assets/images/ft_ic_miscellaneous`);
expect(value).toContain(`svg`);
});
it('should return image value unmodified', () => {
let imageUrl = 'http://<address>';
let file = new FileNode();
file.entry['icon'] = imageUrl;
let adapter = new ShareDataTableAdapter(documentListService, null);
let row = new ShareDataRow(file, documentListService, null);
let col = <DataColumn> {type: 'image', key: 'icon'};
let value = adapter.getValue(row, col);
expect(value).toBe(imageUrl);
});
it('should resolve folder icon', () => {
let adapter = new ShareDataTableAdapter(documentListService, null);
let row = new ShareDataRow(new FolderNode(), documentListService, null);
let col = <DataColumn> {type: 'image', key: '$thumbnail'};
let value = adapter.getValue(row, col);
expect(value).toContain(`assets/images/ft_ic_folder`);
expect(value).toContain(`svg`);
});
it('should resolve file thumbnail', () => {
let imageUrl: string = 'http://<addresss>';
spyOn(documentListService, 'getDocumentThumbnailUrl').and.returnValue(imageUrl);
let adapter = new ShareDataTableAdapter(documentListService, null);
adapter.thumbnails = true;
let file = new FileNode();
let row = new ShareDataRow(file, documentListService, null);
let col = <DataColumn> {type: 'image', key: '$thumbnail'};
let value = adapter.getValue(row, col);
expect(value).toBe(imageUrl);
expect(documentListService.getDocumentThumbnailUrl).toHaveBeenCalledWith(file);
});
it('should resolve fallback file icon for unknown node', () => {
let adapter = new ShareDataTableAdapter(documentListService, null);
let file = new FileNode();
file.entry.isFile = false;
file.entry.isFolder = false;
file.entry.content = null;
let row = new ShareDataRow(file, documentListService, null);
let col = <DataColumn> {type: 'image', key: '$thumbnail'};
let value = adapter.getValue(row, col);
expect(value).toContain(`assets/images/ft_ic_miscellaneous`);
expect(value).toContain(`svg`);
});
it('should resolve file icon for content type', () => {
let adapter = new ShareDataTableAdapter(documentListService, null);
let file = new FileNode();
file.entry.isFile = false;
file.entry.isFolder = false;
file.entry.content.mimeType = 'image/png';
let row = new ShareDataRow(file, documentListService, null);
let col = <DataColumn> {type: 'image', key: '$thumbnail'};
let value = adapter.getValue(row, col);
expect(value).toContain(`assets/images/ft_ic_raster_image`);
expect(value).toContain(`svg`);
});
it('should put folders on top upon sort', () => {
let file1 = new FileNode('file1');
let file2 = new FileNode('file2');
let folder = new FolderNode();
let col = <DataColumn> {key: 'name'};
let adapter = new ShareDataTableAdapter(documentListService, [col]);
adapter.setSorting(new DataSorting('name', 'asc'));
adapter.setRows([
new ShareDataRow(file2, documentListService, null),
new ShareDataRow(file1, documentListService, null),
new ShareDataRow(folder, documentListService, null)
]);
let sorted = adapter.getRows();
expect((<ShareDataRow> sorted[0]).node).toBe(folder);
expect((<ShareDataRow> sorted[1]).node).toBe(file1);
expect((<ShareDataRow> sorted[2]).node).toBe(file2);
});
it('should sort by dates up to ms', () => {
let file1 = new FileNode('file1');
file1.entry['dateProp'] = new Date(2016, 6, 30, 13, 14, 1);
let file2 = new FileNode('file2');
file2.entry['dateProp'] = new Date(2016, 6, 30, 13, 14, 2);
let col = <DataColumn> {key: 'dateProp'};
let adapter = new ShareDataTableAdapter(documentListService, [col]);
adapter.setRows([
new ShareDataRow(file2, documentListService, null),
new ShareDataRow(file1, documentListService, null)
]);
adapter.sort('dateProp', 'asc');
let rows = adapter.getRows();
expect((<ShareDataRow> rows[0]).node).toBe(file1);
expect((<ShareDataRow> rows[1]).node).toBe(file2);
adapter.sort('dateProp', 'desc');
expect((<ShareDataRow> rows[0]).node).toBe(file2);
expect((<ShareDataRow> rows[1]).node).toBe(file1);
});
it('should sort by file size', () => {
let file1 = new FileNode('file1');
let file2 = new FileNode('file2');
let file3 = new FileNode('file3');
let file4 = new FileNode('file4');
file1.entry.content.sizeInBytes = 146; // 146 bytes
file2.entry.content.sizeInBytes = 10075; // 9.84 KB
file3.entry.content.sizeInBytes = 4224120; // 4.03 MB
file4.entry.content.sizeInBytes = 2852791665; // 2.66 GB
let col = <DataColumn> {key: 'content.sizeInBytes'};
let adapter = new ShareDataTableAdapter(documentListService, [col]);
adapter.setRows([
new ShareDataRow(file3, documentListService, null),
new ShareDataRow(file4, documentListService, null),
new ShareDataRow(file1, documentListService, null),
new ShareDataRow(file2, documentListService, null)
]);
adapter.sort('content.sizeInBytes', 'asc');
let rows = adapter.getRows();
expect((<ShareDataRow> rows[0]).node).toBe(file1);
expect((<ShareDataRow> rows[1]).node).toBe(file2);
expect((<ShareDataRow> rows[2]).node).toBe(file3);
expect((<ShareDataRow> rows[3]).node).toBe(file4);
adapter.sort('content.sizeInBytes', 'desc');
expect((<ShareDataRow> rows[0]).node).toBe(file4);
expect((<ShareDataRow> rows[1]).node).toBe(file3);
expect((<ShareDataRow> rows[2]).node).toBe(file2);
expect((<ShareDataRow> rows[3]).node).toBe(file1);
});
it('should sort by name', () => {
let file1 = new FileNode('file1');
let file2 = new FileNode('file11');
let file3 = new FileNode('file20');
let file4 = new FileNode('file11-1'); // auto rename
let file5 = new FileNode('a');
let file6 = new FileNode('b');
let col = <DataColumn> {key: 'name'};
let adapter = new ShareDataTableAdapter(documentListService, [col]);
adapter.setRows([
new ShareDataRow(file4, documentListService, null),
new ShareDataRow(file6, documentListService, null),
new ShareDataRow(file3, documentListService, null),
new ShareDataRow(file1, documentListService, null),
new ShareDataRow(file2, documentListService, null),
new ShareDataRow(file5, documentListService, null)
]);
adapter.sort('name', 'asc');
let rows = adapter.getRows();
expect((<ShareDataRow> rows[0]).node).toBe(file5);
expect((<ShareDataRow> rows[1]).node).toBe(file6);
expect((<ShareDataRow> rows[2]).node).toBe(file1);
expect((<ShareDataRow> rows[3]).node).toBe(file2);
expect((<ShareDataRow> rows[4]).node).toBe(file4);
expect((<ShareDataRow> rows[5]).node).toBe(file3);
adapter.sort('name', 'desc');
expect((<ShareDataRow> rows[0]).node).toBe(file3);
expect((<ShareDataRow> rows[1]).node).toBe(file4);
expect((<ShareDataRow> rows[2]).node).toBe(file2);
expect((<ShareDataRow> rows[3]).node).toBe(file1);
expect((<ShareDataRow> rows[4]).node).toBe(file6);
expect((<ShareDataRow> rows[5]).node).toBe(file5);
});
});
describe('ShareDataRow', () => {
let documentListService: DocumentListService;
beforeEach(async(() => {
TestBed.configureTestingModule({
imports: [],
providers: [
DocumentListService
]
}).compileComponents();
}));
beforeEach(() => {
documentListService = TestBed.get(DocumentListService);
});
it('should wrap node', () => {
let file = new FileNode();
let row = new ShareDataRow(file, documentListService, null);
expect(row.node).toBe(file);
});
it('should require object source', () => {
expect(() => {
return new ShareDataRow(null, documentListService, null);
}).toThrowError(ShareDataRow.ERR_OBJECT_NOT_FOUND);
});
it('should resolve value from node entry', () => {
let file = new FileNode('test');
let row = new ShareDataRow(file, documentListService, null);
expect(row.getValue('name')).toBe('test');
});
it('should check value', () => {
let file = new FileNode('test');
let row = new ShareDataRow(file, documentListService, null);
expect(row.hasValue('name')).toBeTruthy();
expect(row.hasValue('missing')).toBeFalsy();
});
it('should be set as drop target when user has permission for that node', () => {
let file = new FolderNode('test');
file.entry['allowableOperations'] = ['create'];
let row = new ShareDataRow(file, documentListService, null);
expect(row.isDropTarget).toBeTruthy();
});
it('should not be set as drop target when user has permission for that node', () => {
let file = new FolderNode('test');
let row = new ShareDataRow(file, documentListService, null);
expect(row.isDropTarget).toBeFalsy();
});
it('should not be set as drop target when element is not a Folder', () => {
let file = new FileNode('test');
let row = new ShareDataRow(file, documentListService, null);
expect(row.isDropTarget).toBeFalsy();
});
});

View File

@@ -0,0 +1,252 @@
/*!
* @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 { DataColumn, DataRow, DataSorting, DataTableAdapter } from '@alfresco/core';
import { TimeAgoPipe } from '@alfresco/core';
import { DatePipe } from '@angular/common';
import { NodePaging } from 'alfresco-js-api';
import { PermissionStyleModel } from './../models/permissions-style.model';
import { DocumentListService } from './../services/document-list.service';
import { ImageResolver } from './image-resolver.model';
import { RowFilter } from './row-filter.model';
import { ShareDataRow } from './share-data-row.model';
export class ShareDataTableAdapter implements DataTableAdapter {
ERR_ROW_NOT_FOUND: string = 'Row not found';
ERR_COL_NOT_FOUND: string = 'Column not found';
private sorting: DataSorting;
private rows: DataRow[];
private columns: DataColumn[];
private page: NodePaging;
private filter: RowFilter;
private imageResolver: ImageResolver;
thumbnails: boolean = false;
permissionsStyle: PermissionStyleModel[];
selectedRow: DataRow;
constructor(private documentListService: DocumentListService,
schema: DataColumn[] = [],
sorting?: DataSorting) {
this.rows = [];
this.columns = schema || [];
this.sorting = sorting;
}
getRows(): Array<DataRow> {
return this.rows;
}
// TODO: disable this api
setRows(rows: Array<DataRow>) {
this.rows = rows || [];
this.sort();
}
getColumns(): Array<DataColumn> {
return this.columns;
}
setColumns(columns: Array<DataColumn>) {
this.columns = columns || [];
}
getValue(row: DataRow, col: DataColumn): any {
if (!row) {
throw new Error(this.ERR_ROW_NOT_FOUND);
}
if (!col) {
throw new Error(this.ERR_COL_NOT_FOUND);
}
let dataRow: ShareDataRow = <ShareDataRow> row;
let value: any = row.getValue(col.key);
if (dataRow.cache[col.key] !== undefined) {
return dataRow.cache[col.key];
}
if (col.type === 'date') {
try {
const result = this.formatDate(col, value);
return dataRow.cacheValue(col.key, result);
} catch (err) {
console.error(`Error parsing date ${value} to format ${col.format}`);
return 'Error';
}
}
if (col.key === '$thumbnail') {
if (this.imageResolver) {
let resolved = this.imageResolver(row, col);
if (resolved) {
return resolved;
}
}
const node = (<ShareDataRow> row).node;
if (node.entry.isFolder) {
return this.documentListService.getMimeTypeIcon('folder');
}
if (node.entry.isFile) {
if (this.thumbnails) {
return this.documentListService.getDocumentThumbnailUrl(node);
}
}
if (node.entry.content) {
const mimeType = node.entry.content.mimeType;
if (mimeType) {
return this.documentListService.getMimeTypeIcon(mimeType);
}
}
return this.documentListService.getDefaultMimeTypeIcon();
}
if (col.type === 'image') {
if (this.imageResolver) {
let resolved = this.imageResolver(row, col);
if (resolved) {
return resolved;
}
}
}
return dataRow.cacheValue(col.key, value);
}
formatDate(col: DataColumn, value: any): string {
if (col.type === 'date') {
const format = col.format || 'medium';
if (format === 'timeAgo') {
const timeAgoPipe = new TimeAgoPipe();
return timeAgoPipe.transform(value);
} else {
const datePipe = new DatePipe('en-US');
return datePipe.transform(value, format);
}
}
return value;
}
getSorting(): DataSorting {
return this.sorting;
}
setSorting(sorting: DataSorting): void {
this.sorting = sorting;
this.sortRows(this.rows, this.sorting);
}
sort(key?: string, direction?: string): void {
let sorting = this.sorting || new DataSorting();
if (key) {
sorting.key = key;
sorting.direction = direction || 'asc';
}
this.setSorting(sorting);
}
setFilter(filter: RowFilter) {
this.filter = filter;
}
setImageResolver(resolver: ImageResolver) {
this.imageResolver = resolver;
}
private sortRows(rows: DataRow[], sorting: DataSorting) {
const options: Intl.CollatorOptions = {};
if (sorting && sorting.key && rows && rows.length > 0) {
if (sorting.key.includes('sizeInBytes') || sorting.key === 'name') {
options.numeric = true;
}
rows.sort((a: ShareDataRow, b: ShareDataRow) => {
if (a.node.entry.isFolder !== b.node.entry.isFolder) {
return a.node.entry.isFolder ? -1 : 1;
}
let left = a.getValue(sorting.key);
if (left) {
left = (left instanceof Date) ? left.valueOf().toString() : left.toString();
} else {
left = '';
}
let right = b.getValue(sorting.key);
if (right) {
right = (right instanceof Date) ? right.valueOf().toString() : right.toString();
} else {
right = '';
}
return sorting.direction === 'asc'
? left.localeCompare(right, undefined, options)
: right.localeCompare(left, undefined, options);
});
}
}
public loadPage(page: NodePaging, merge: boolean = false) {
this.page = page;
let rows = [];
if (page && page.list) {
let data = page.list.entries;
if (data && data.length > 0) {
rows = data.map(item => new ShareDataRow(item, this.documentListService, this.permissionsStyle));
if (this.filter) {
rows = rows.filter(this.filter);
}
// Sort by first sortable or just first column
if (this.columns && this.columns.length > 0) {
let sorting = this.getSorting();
if (sorting) {
this.sortRows(rows, sorting);
} else {
let sortable = this.columns.filter(c => c.sortable);
if (sortable.length > 0) {
this.sort(sortable[0].key, 'asc');
} else {
this.sort(this.columns[0].key, 'asc');
}
}
}
}
}
if (merge) {
this.rows = this.rows.concat(rows);
} else {
this.rows = rows;
}
}
}

View File

@@ -0,0 +1,77 @@
/*!
* @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 { CommonModule } from '@angular/common';
import { NgModule } from '@angular/core';
import { FlexLayoutModule } from '@angular/flex-layout';
import { TranslateModule } from '@ngx-translate/core';
import { DataTableModule, PaginationModule, ToolbarModule } from '@alfresco/core';
import { MaterialModule } from '../material.module';
import { UploadModule } from '../upload';
import { ContentActionListComponent } from './components/content-action/content-action-list.component';
import { ContentActionComponent } from './components/content-action/content-action.component';
import { ContentColumnListComponent } from './components/content-column/content-column-list.component';
import { ContentColumnComponent } from './components/content-column/content-column.component';
import { DocumentListComponent } from './components/document-list.component';
import { EmptyFolderContentDirective } from './components/empty-folder/empty-folder-content.directive';
import { NoPermissionContentDirective } from './components/no-permission/no-permission-content.directive';
import { DocumentActionsService } from './services/document-actions.service';
import { DocumentListService } from './services/document-list.service';
import { FolderActionsService } from './services/folder-actions.service';
import { NodeActionsService } from './services/node-actions.service';
@NgModule({
imports: [
ToolbarModule,
CommonModule,
DataTableModule,
FlexLayoutModule,
MaterialModule,
UploadModule,
TranslateModule,
PaginationModule
],
declarations: [
DocumentListComponent,
ContentColumnComponent,
ContentColumnListComponent,
ContentActionComponent,
ContentActionListComponent,
EmptyFolderContentDirective,
NoPermissionContentDirective
],
providers: [
DocumentListService,
FolderActionsService,
DocumentActionsService,
NodeActionsService
],
exports: [
DocumentListComponent,
ContentColumnComponent,
ContentColumnListComponent,
ContentActionComponent,
ContentActionListComponent,
EmptyFolderContentDirective,
NoPermissionContentDirective
]
})
export class DocumentListModule {}

View File

@@ -0,0 +1,18 @@
/*!
* @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.
*/
export * from './public-api';

View File

@@ -0,0 +1,56 @@
/*!
* @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.
*/
export class ContentActionModel {
icon: string;
title: string;
handler: ContentActionHandler;
execute: Function;
target: string;
permission: string;
disableWithNoPermission: boolean = false;
disabled: boolean = false;
constructor(obj?: any) {
if (obj) {
this.icon = obj.icon;
this.title = obj.title;
this.handler = obj.handler;
this.execute = obj.execute;
this.target = obj.target;
this.permission = obj.permission;
this.disableWithNoPermission = obj.disableWithNoPermission;
this.disabled = obj.disabled;
}
}
}
export type ContentActionHandler = (obj: any, target?: any, permission?: string) => any;
export class DocumentActionModel extends ContentActionModel {
constructor(json?: any) {
super(json);
this.target = 'document';
}
}
export class FolderActionModel extends ContentActionModel {
constructor(json?: any) {
super(json);
this.target = 'folder';
}
}

View File

@@ -0,0 +1,84 @@
/*!
* @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.
*/
// note: contains only limited subset of available fields
import { MinimalNodeEntity, MinimalNodeEntryEntity } from 'alfresco-js-api';
export class NodePaging {
list: NodePagingList;
}
export class NodePagingList {
pagination: Pagination;
entries: NodeMinimalEntry[];
}
export class NodeMinimalEntry implements MinimalNodeEntity {
entry: NodeMinimal;
}
export class Pagination {
count: number;
hasMoreItems: boolean;
totalItems: number;
skipCount: number;
maxItems: number;
}
export class NodeMinimal implements MinimalNodeEntryEntity {
id: string;
parentId: string;
name: string;
nodeType: string;
isFolder: boolean;
isFile: boolean;
modifiedAt: Date;
modifiedByUser: UserInfo;
createdAt: Date;
createdByUser: UserInfo;
content: ContentInfo;
path: PathInfoEntity;
properties: NodeProperties = {};
}
export class UserInfo {
displayName: string;
id: string;
}
export class ContentInfo {
mimeType: string;
mimeTypeName: string;
sizeInBytes: number;
encoding: string;
}
export class PathInfoEntity {
elements: PathElementEntity[];
isComplete: boolean;
name: string;
}
export class PathElementEntity {
id: string;
name: string;
}
export interface NodeProperties {
[key: string]: any;
}

View File

@@ -0,0 +1,32 @@
/*!
* @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 { PermissionsEnum } from '@alfresco/core';
export class PermissionStyleModel {
css: string;
permission: PermissionsEnum;
isFolder: boolean = true;
isFile: boolean = true;
constructor(css: string, permission: PermissionsEnum, isFile: boolean = true, isFolder: boolean = true) {
this.css = css;
this.permission = permission;
this.isFile = isFile;
this.isFolder = isFolder;
}
}

View File

@@ -0,0 +1,30 @@
/*!
* @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.
*/
export class PermissionModel {
type: string;
action: string;
permission: string;
constructor(obj?: any) {
if (obj) {
this.type = obj.type || null;
this.action = obj.action || null;
this.permission = obj.permission || null;
}
}
}

View File

@@ -0,0 +1,261 @@
/*!
* @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.
*/
export let presetsDefaultModel = {
'-trashcan-': [
{
key: '$thumbnail',
type: 'image',
srTitle: 'ADF-DOCUMENT-LIST.LAYOUT.THUMBNAIL',
sortable: false
},
{
key: 'name',
type: 'text',
title: 'ADF-DOCUMENT-LIST.LAYOUT.NAME',
cssClass: 'full-width ellipsis-cell',
sortable: true
},
{
key: 'path',
type: 'location',
title: 'ADF-DOCUMENT-LIST.LAYOUT.LOCATION',
format: this.locationFormat,
sortable: true
},
{
key: 'content.sizeInBytes',
type: 'fileSize',
title: 'ADF-DOCUMENT-LIST.LAYOUT.SIZE',
sortable: true
},
{
key: 'archivedAt',
type: 'date',
title: 'ADF-DOCUMENT-LIST.LAYOUT.DELETED_ON',
format: 'timeAgo',
sortable: true
},
{
key: 'archivedByUser.displayName',
type: 'text',
title: 'ADF-DOCUMENT-LIST.LAYOUT.DELETED_BY',
sortable: true
}
],
'-sites-': [
{
key: '$thumbnail',
type: 'image',
srTitle: 'ADF-DOCUMENT-LIST.LAYOUT.THUMBNAIL',
sortable: false
},
{
key: 'title',
type: 'text',
title: 'ADF-DOCUMENT-LIST.LAYOUT.NAME',
cssClass: 'full-width ellipsis-cell',
sortable: true
},
{
key: 'visibility',
type: 'text',
title: 'ADF-DOCUMENT-LIST.LAYOUT.STATUS',
sortable: true
}
],
'-mysites-': [
{
key: '$thumbnail',
type: 'image',
srTitle: 'ADF-DOCUMENT-LIST.LAYOUT.THUMBNAIL',
sortable: false
},
{
key: 'title',
type: 'text',
title: 'ADF-DOCUMENT-LIST.LAYOUT.NAME',
cssClass: 'full-width ellipsis-cell',
sortable: true
},
{
key: 'visibility',
type: 'text',
title: 'ADF-DOCUMENT-LIST.LAYOUT.STATUS',
sortable: true
}
],
'-favorites-': [
{
key: '$thumbnail',
type: 'image',
srTitle: 'ADF-DOCUMENT-LIST.LAYOUT.THUMBNAIL',
sortable: false
},
{
key: 'name',
type: 'text',
title: 'ADF-DOCUMENT-LIST.LAYOUT.NAME',
cssClass: 'full-width ellipsis-cell',
sortable: true
},
{
key: 'path',
type: 'location',
title: 'ADF-DOCUMENT-LIST.LAYOUT.LOCATION',
format: this.locationFormat,
sortable: true
},
{
key: 'content.sizeInBytes',
type: 'fileSize',
title: 'ADF-DOCUMENT-LIST.LAYOUT.SIZE',
sortable: true
},
{
key: 'modifiedAt',
type: 'date',
title: 'ADF-DOCUMENT-LIST.LAYOUT.MODIFIED_ON',
format: 'timeAgo',
sortable: true
},
{
key: 'modifiedByUser.displayName',
type: 'text',
title: 'ADF-DOCUMENT-LIST.LAYOUT.MODIFIED_BY',
sortable: true
}
],
'-recent-': [
{
key: '$thumbnail',
type: 'image',
srTitle: 'ADF-DOCUMENT-LIST.LAYOUT.THUMBNAIL',
sortable: false
},
{
key: 'name',
type: 'text',
title: 'ADF-DOCUMENT-LIST.LAYOUT.NAME',
cssClass: 'full-width ellipsis-cell',
sortable: true
},
{
key: 'path',
type: 'location',
title: 'ADF-DOCUMENT-LIST.LAYOUT.LOCATION',
cssClass: 'ellipsis-cell',
format: this.locationFormat,
sortable: true
},
{
key: 'content.sizeInBytes',
type: 'fileSize',
title: 'ADF-DOCUMENT-LIST.LAYOUT.SIZE',
sortable: true
},
{
key: 'modifiedAt',
type: 'date',
title: 'ADF-DOCUMENT-LIST.LAYOUT.MODIFIED_ON',
format: 'timeAgo',
sortable: true
}
],
'-sharedlinks-': [
{
key: '$thumbnail',
type: 'image',
srTitle: 'ADF-DOCUMENT-LIST.LAYOUT.THUMBNAIL',
sortable: false
},
{
key: 'name',
type: 'text',
title: 'ADF-DOCUMENT-LIST.LAYOUT.NAME',
cssClass: 'full-width ellipsis-cell',
sortable: true
},
{
key: 'path',
type: 'location',
title: 'ADF-DOCUMENT-LIST.LAYOUT.LOCATION',
cssClass: 'ellipsis-cell',
format: this.locationFormat,
sortable: true
},
{
key: 'content.sizeInBytes',
type: 'fileSize',
title: 'ADF-DOCUMENT-LIST.LAYOUT.SIZE',
sortable: true
},
{
key: 'modifiedAt',
type: 'date',
title: 'ADF-DOCUMENT-LIST.LAYOUT.MODIFIED_ON',
format: 'timeAgo',
sortable: true
},
{
key: 'modifiedByUser.displayName',
type: 'text',
title: 'ADF-DOCUMENT-LIST.LAYOUT.MODIFIED_BY',
sortable: true
},
{
key: 'sharedByUser.displayName',
type: 'text',
title: 'ADF-DOCUMENT-LIST.LAYOUT.SHARED_BY',
sortable: true
}
],
'default': [
{
key: '$thumbnail',
type: 'image',
srTitle: 'ADF-DOCUMENT-LIST.LAYOUT.THUMBNAIL',
sortable: false
},
{
key: 'name',
type: 'text',
title: 'ADF-DOCUMENT-LIST.LAYOUT.NAME',
cssClass: 'full-width ellipsis-cell',
sortable: true
},
{
key: 'content.sizeInBytes',
type: 'fileSize',
title: 'ADF-DOCUMENT-LIST.LAYOUT.SIZE',
sortable: true
},
{
key: 'modifiedAt',
type: 'date',
title: 'ADF-DOCUMENT-LIST.LAYOUT.MODIFIED_ON',
format: 'timeAgo',
sortable: true
},
{
key: 'modifiedByUser.displayName',
type: 'text',
title: 'ADF-DOCUMENT-LIST.LAYOUT.MODIFIED_BY',
sortable: true
}
]
};

View File

@@ -0,0 +1,45 @@
/*!
* @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.
*/
export * from './components/document-list.component';
export * from './components/node.event';
export * from './components/content-column/content-column.component';
export * from './components/content-column/content-column-list.component';
export * from './components/content-action/content-action.component';
export * from './components/content-action/content-action-list.component';
export * from './components/empty-folder/empty-folder-content.directive';
export * from './components/no-permission/no-permission-content.directive';
// data
export * from './data/share-datatable-adapter';
export * from './data/share-data-row.model';
export * from './data/image-resolver.model';
export * from './data/row-filter.model';
// services
export * from './services/folder-actions.service';
export * from './services/document-actions.service';
export * from './services/document-list.service';
export * from './services/node-actions.service';
// models
export * from './models/content-action.model';
export * from './models/document-library.model';
export * from './models/permissions.model';
export * from './models/permissions-style.model';
export * from './document-list.module';

View File

@@ -0,0 +1,285 @@
/*!
* @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 { ContentService, TranslationService, NotificationService } from '@alfresco/core';
import { FileNode, FolderNode, DocumentListServiceMock } from '../../mock';
import { ContentActionHandler } from '../models/content-action.model';
import { DocumentActionsService } from './document-actions.service';
import { DocumentListService } from './document-list.service';
import { NodeActionsService } from './node-actions.service';
describe('DocumentActionsService', () => {
let service: DocumentActionsService;
let documentListService: DocumentListService;
let contentService: ContentService;
let translateService: TranslationService;
let notificationService: NotificationService;
let nodeActionsService: NodeActionsService;
beforeEach(() => {
documentListService = new DocumentListServiceMock();
contentService = new ContentService(null, null, null, null);
translateService = <TranslationService> { addTranslationFolder: () => {}};
nodeActionsService = new NodeActionsService(null, null, null);
notificationService = new NotificationService(null);
service = new DocumentActionsService(nodeActionsService, documentListService, contentService);
});
it('should register default download action', () => {
expect(service.getHandler('download')).not.toBeNull();
});
it('should register custom action handler', () => {
let handler: ContentActionHandler = function (obj: any) {};
service.setHandler('<key>', handler);
expect(service.getHandler('<key>')).toBe(handler);
});
it('should not find handler that is not registered', () => {
expect(service.getHandler('<missing>')).toBeNull();
});
it('should be case insensitive for keys', () => {
let handler: ContentActionHandler = function (obj: any) {};
service.setHandler('<key>', handler);
expect(service.getHandler('<KEY>')).toBe(handler);
});
it('should not find handler with invalid key', () => {
expect(service.getHandler(null)).toBeNull();
expect(service.getHandler('')).toBeNull();
});
it('should allow action execution only when service available', () => {
let file = new FileNode();
expect(service.canExecuteAction(file)).toBeTruthy();
service = new DocumentActionsService(nodeActionsService);
expect(service.canExecuteAction(file)).toBeFalsy();
});
it('should allow action execution only for file nodes', () => {
expect(service.canExecuteAction(null)).toBeFalsy();
expect(service.canExecuteAction(new FileNode())).toBeTruthy();
expect(service.canExecuteAction(new FolderNode())).toBeFalsy();
});
it('should set new handler only by key', () => {
let handler: ContentActionHandler = function (obj: any) {};
expect(service.setHandler(null, handler)).toBeFalsy();
expect(service.setHandler('', handler)).toBeFalsy();
expect(service.setHandler('my-handler', handler)).toBeTruthy();
});
it('should register delete action', () => {
expect(service.getHandler('delete')).toBeDefined();
});
it('should not delete the file node if there are no permissions', (done) => {
spyOn(documentListService, 'deleteNode').and.callThrough();
service.permissionEvent.subscribe((permission) => {
expect(permission).toBeDefined();
expect(permission.type).toEqual('content');
expect(permission.action).toEqual('delete');
done();
});
let file = new FileNode();
service.getHandler('delete')(file);
});
it('should call the error on the returned Observable if there are no permissions', (done) => {
spyOn(documentListService, 'deleteNode').and.callThrough();
let file = new FileNode();
const deleteObservable = service.getHandler('delete')(file);
deleteObservable.subscribe({
error: (error) => {
expect(error.message).toEqual('No permission to delete');
done();
}
});
});
it('should delete the file node if there is the delete permission', () => {
spyOn(documentListService, 'deleteNode').and.callThrough();
let permission = 'delete';
let file = new FileNode();
let fileWithPermission: any = file;
fileWithPermission.entry.allowableOperations = [permission];
service.getHandler('delete')(fileWithPermission, null, permission);
expect(documentListService.deleteNode).toHaveBeenCalledWith(file.entry.id);
});
it('should not delete the file node if there is no delete permission', (done) => {
spyOn(documentListService, 'deleteNode').and.callThrough();
service.permissionEvent.subscribe((permissionBack) => {
expect(permissionBack).toBeDefined();
expect(permissionBack.type).toEqual('content');
expect(permissionBack.action).toEqual('delete');
done();
});
let permission = 'delete';
let file = new FileNode();
let fileWithPermission: any = file;
fileWithPermission.entry.allowableOperations = ['create', 'update'];
service.getHandler('delete')(fileWithPermission, null, permission);
});
it('should delete the file node if there is the delete and others permission ', () => {
spyOn(documentListService, 'deleteNode').and.callThrough();
let permission = 'delete';
let file = new FileNode();
let fileWithPermission: any = file;
fileWithPermission.entry.allowableOperations = ['create', 'update', permission];
service.getHandler('delete')(fileWithPermission, null, permission);
expect(documentListService.deleteNode).toHaveBeenCalledWith(file.entry.id);
});
it('should register download action', () => {
expect(service.getHandler('download')).toBeDefined();
});
it('should execute download action and cleanup', () => {
let file = new FileNode();
let url = 'http://<address>';
spyOn(contentService, 'getContentUrl').and.returnValue(url);
let link = jasmine.createSpyObj('a', [
'setAttribute',
'click'
]);
spyOn(document, 'createElement').and.returnValue(link);
spyOn(document.body, 'appendChild').and.stub();
spyOn(document.body, 'removeChild').and.stub();
service.getHandler('download')(file);
expect(contentService.getContentUrl).toHaveBeenCalledWith(file);
expect(document.createElement).toHaveBeenCalledWith('a');
expect(link.setAttribute).toHaveBeenCalledWith('download', 'download');
expect(document.body.appendChild).toHaveBeenCalledWith(link);
expect(link.click).toHaveBeenCalled();
expect(document.body.removeChild).toHaveBeenCalledWith(link);
});
it('should require internal service for download action', () => {
let actionService = new DocumentActionsService(nodeActionsService, null, contentService);
let file = new FileNode();
let result = actionService.getHandler('download')(file);
result.subscribe((value) => {
expect(value).toBeFalsy();
});
});
it('should require content service for download action', () => {
let actionService = new DocumentActionsService(nodeActionsService, documentListService, null);
let file = new FileNode();
let result = actionService.getHandler('download')(file);
result.subscribe((value) => {
expect(value).toBeFalsy();
});
});
it('should require file node for download action', () => {
let folder = new FolderNode();
let result = service.getHandler('download')(folder);
result.subscribe((value) => {
expect(value).toBeFalsy();
});
});
it('should delete file node', () => {
spyOn(documentListService, 'deleteNode').and.callThrough();
let permission = 'delete';
let file = new FileNode();
let fileWithPermission: any = file;
fileWithPermission.entry.allowableOperations = [permission];
const deleteObservale = service.getHandler('delete')(fileWithPermission, null, permission);
expect(documentListService.deleteNode).toHaveBeenCalledWith(file.entry.id);
expect(deleteObservale.subscribe).toBeDefined();
});
it('should support deletion only file node', () => {
spyOn(documentListService, 'deleteNode').and.callThrough();
let folder = new FolderNode();
service.getHandler('delete')(folder);
expect(documentListService.deleteNode).not.toHaveBeenCalled();
let permission = 'delete';
let file = new FileNode();
let fileWithPermission: any = file;
fileWithPermission.entry.allowableOperations = [permission];
service.getHandler('delete')(fileWithPermission, null, permission);
expect(documentListService.deleteNode).toHaveBeenCalled();
});
it('should require node id to delete', () => {
spyOn(documentListService, 'deleteNode').and.callThrough();
let file = new FileNode();
file.entry.id = null;
service.getHandler('delete')(file);
expect(documentListService.deleteNode).not.toHaveBeenCalled();
});
it('should reload target upon node deletion', () => {
spyOn(documentListService, 'deleteNode').and.callThrough();
let target = jasmine.createSpyObj('obj', ['reload']);
let permission = 'delete';
let file = new FileNode();
let fileWithPermission: any = file;
fileWithPermission.entry.allowableOperations = [permission];
service.getHandler('delete')(fileWithPermission, target, permission);
expect(documentListService.deleteNode).toHaveBeenCalled();
expect(target.reload).toHaveBeenCalled();
});
it('should emit success event upon node deletion', (done) => {
service.success.subscribe((nodeId) => {
expect(nodeId).not.toBeNull();
done();
});
spyOn(documentListService, 'deleteNode').and.callThrough();
let target = jasmine.createSpyObj('obj', ['reload']);
let permission = 'delete';
let file = new FileNode();
let fileWithPermission: any = file;
fileWithPermission.entry.allowableOperations = [permission];
service.getHandler('delete')(fileWithPermission, target, permission);
});
});

View File

@@ -0,0 +1,127 @@
/*!
* @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 { ContentService } from '@alfresco/core';
import { Injectable } from '@angular/core';
import { MinimalNodeEntity } from 'alfresco-js-api';
import { Observable } from 'rxjs/Observable';
import { Subject } from 'rxjs/Rx';
import { ContentActionHandler } from '../models/content-action.model';
import { PermissionModel } from '../models/permissions.model';
import { DocumentListService } from './document-list.service';
import { NodeActionsService } from './node-actions.service';
@Injectable()
export class DocumentActionsService {
permissionEvent: Subject<PermissionModel> = new Subject<PermissionModel>();
error: Subject<Error> = new Subject<Error>();
success: Subject<string> = new Subject<string>();
private handlers: { [id: string]: ContentActionHandler; } = {};
constructor(private nodeActionsService: NodeActionsService,
private documentListService?: DocumentListService,
private contentService?: ContentService) {
this.setupActionHandlers();
}
getHandler(key: string): ContentActionHandler {
if (key) {
let lkey = key.toLowerCase();
return this.handlers[lkey] || null;
}
return null;
}
setHandler(key: string, handler: ContentActionHandler): boolean {
if (key) {
let lkey = key.toLowerCase();
this.handlers[lkey] = handler;
return true;
}
return false;
}
canExecuteAction(obj: any): boolean {
return this.documentListService && obj && obj.entry.isFile === true;
}
private setupActionHandlers() {
this.handlers['download'] = this.download.bind(this);
this.handlers['copy'] = this.copyNode.bind(this);
this.handlers['move'] = this.moveNode.bind(this);
this.handlers['delete'] = this.deleteNode.bind(this);
}
private download(obj: any): Observable<boolean> {
if (this.canExecuteAction(obj) && this.contentService) {
let link = document.createElement('a');
document.body.appendChild(link);
link.setAttribute('download', 'download');
link.href = this.contentService.getContentUrl(obj);
link.click();
document.body.removeChild(link);
return Observable.of(true);
}
return Observable.of(false);
}
private copyNode(obj: MinimalNodeEntity, target?: any, permission?: string) {
const actionObservable = this.nodeActionsService.copyContent(obj.entry, permission);
this.prepareHandlers(actionObservable, 'content', 'copy', target, permission);
return actionObservable;
}
private moveNode(obj: MinimalNodeEntity, target?: any, permission?: string) {
const actionObservable = this.nodeActionsService.moveContent(obj.entry, permission);
this.prepareHandlers(actionObservable, 'content', 'move', target, permission);
return actionObservable;
}
private prepareHandlers(actionObservable, type: string, action: string, target?: any, permission?: string): void {
actionObservable.subscribe(
(fileOperationMessage) => {
if (target && typeof target.reload === 'function') {
target.reload();
}
this.success.next(fileOperationMessage);
},
this.error.next.bind(this.error)
);
}
private deleteNode(obj: any, target?: any, permission?: string): Observable<any> {
let handlerObservable;
if (this.canExecuteAction(obj)) {
if (this.contentService.hasPermission(obj.entry, permission)) {
handlerObservable = this.documentListService.deleteNode(obj.entry.id);
handlerObservable.subscribe(() => {
if (target && typeof target.reload === 'function') {
target.reload();
}
this.success.next(obj.entry.id);
});
return handlerObservable;
} else {
this.permissionEvent.next(new PermissionModel({type: 'content', action: 'delete', permission: permission}));
return Observable.throw(new Error('No permission to delete'));
}
}
}
}

View File

@@ -0,0 +1,198 @@
/*!
* @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 { async, TestBed } from '@angular/core/testing';
import { CookieService, LogService } from '@alfresco/core';
import { CookieServiceMock } from '@alfresco/core';
import { DocumentListService } from './document-list.service';
declare let jasmine: any;
describe('DocumentListService', () => {
let service: DocumentListService;
let fakeEntryNode = {
'entry': {
'aspectNames': ['cm:auditable'],
'createdAt': '2016-12-06T15:58:32.408+0000',
'isFolder': true,
'isFile': false,
'createdByUser': {'id': 'admin', 'displayName': 'Administrator'},
'modifiedAt': '2016-12-06T15:58:32.408+0000',
'modifiedByUser': {'id': 'admin', 'displayName': 'Administrator'},
'name': 'fake-name',
'id': '2214733d-a920-4dbe-af95-4230345fae82',
'nodeType': 'cm:folder',
'parentId': 'ed7ab80e-b398-4bed-b38d-139ae4cc592a'
}
};
let fakeAlreadyExist = {
'error': {
'errorKey': 'Duplicate child name not allowed: empty',
'statusCode': 409,
'briefSummary': '11060002 Duplicate child name not' +
' allowed: empty',
'stackTrace': 'For security reasons the stack trace is no longer displayed, but the property is kept for previous versions.',
'descriptionURL': 'https://api-explorer.alfresco.com'
}
};
let fakeFolder = {
'list': {
'pagination': {'count': 1, 'hasMoreItems': false, 'totalItems': 1, 'skipCount': 0, 'maxItems': 20},
'entries': [{
'entry': {
'createdAt': '2016-12-06T13:03:14.880+0000',
'path': {
'name': '/Company Home/Sites/swsdp/documentLibrary/empty',
'isComplete': true,
'elements': [{
'id': 'ed7ab80e-b398-4bed-b38d-139ae4cc592a',
'name': 'Company Home'
}, {'id': '99e1368f-e816-47fc-a8bf-3b358feaf31e', 'name': 'Sites'}, {
'id': 'b4cff62a-664d-4d45-9302-98723eac1319',
'name': 'swsdp'
}, {
'id': '8f2105b4-daaf-4874-9e8a-2152569d109b',
'name': 'documentLibrary'
}, {'id': '17fa78d2-4d6b-4a46-876b-4b0ea07f7f32', 'name': 'empty'}]
},
'isFolder': true,
'isFile': false,
'createdByUser': {'id': 'admin', 'displayName': 'Administrator'},
'modifiedAt': '2016-12-06T13:03:14.880+0000',
'modifiedByUser': {'id': 'admin', 'displayName': 'Administrator'},
'name': 'fake-name',
'id': 'aac546f6-1525-46ff-bf6b-51cb85f3cda7',
'nodeType': 'cm:folder',
'parentId': '17fa78d2-4d6b-4a46-876b-4b0ea07f7f32'
}
}]
}
};
beforeEach(async(() => {
TestBed.configureTestingModule({
imports: [],
providers: [
DocumentListService,
{ provide: CookieService, useClass: CookieServiceMock },
LogService
]
}).compileComponents();
}));
beforeEach(() => {
service = TestBed.get(DocumentListService);
jasmine.Ajax.install();
});
afterEach(() => {
jasmine.Ajax.uninstall();
});
it('should create a folder in the path', () => {
service.createFolder('fake-name', 'fake-path').subscribe(
res => {
expect(res).toBeDefined();
expect(res.entry).toBeDefined();
expect(res.entry.isFolder).toBeTruthy();
expect(res.entry.name).toEqual('fake-name');
expect(res.entry.nodeType).toEqual('cm:folder');
}
);
jasmine.Ajax.requests.mostRecent().respondWith({
status: 200,
contentType: 'json',
responseText: fakeEntryNode
});
});
xit('should emit an error when the folder already exist', () => {
service.createFolder('fake-name', 'fake-path').subscribe(
res => {
},
err => {
expect(err).toBeDefined();
expect(err.status).toEqual(409);
expect(err.response).toBeDefined();
}
);
jasmine.Ajax.requests.mostRecent().respondWith({
status: 409,
contentType: 'json',
responseText: fakeAlreadyExist
});
});
it('should return the folder info', () => {
service.getFolder('/fake-root/fake-name').subscribe(
res => {
expect(res).toBeDefined();
expect(res.list).toBeDefined();
expect(res.list.entries).toBeDefined();
expect(res.list.entries.length).toBe(1);
expect(res.list.entries[0].entry.isFolder).toBeTruthy();
expect(res.list.entries[0].entry.name).toEqual('fake-name');
}
);
jasmine.Ajax.requests.mostRecent().respondWith({
status: 200,
contentType: 'json',
responseText: fakeFolder
});
});
it('should delete the folder', () => {
service.deleteNode('fake-id').subscribe(
res => {
expect(res).toBeNull();
}
);
jasmine.Ajax.requests.mostRecent().respondWith({
status: 204,
contentType: 'json'
});
});
it('should copy a node', (done) => {
service.copyNode('node-id', 'parent-id').subscribe(done);
expect(jasmine.Ajax.requests.mostRecent().method).toBe('POST');
expect(jasmine.Ajax.requests.mostRecent().url).toContain('/nodes/node-id/copy');
expect(jasmine.Ajax.requests.mostRecent().params).toEqual(JSON.stringify({ targetParentId: 'parent-id' }));
jasmine.Ajax.requests.mostRecent().respondWith({ status: 200, contentType: 'json' });
});
it('should move a node', (done) => {
service.moveNode('node-id', 'parent-id').subscribe(done);
expect(jasmine.Ajax.requests.mostRecent().method).toBe('POST');
expect(jasmine.Ajax.requests.mostRecent().url).toContain('/nodes/node-id/move');
expect(jasmine.Ajax.requests.mostRecent().params).toEqual(JSON.stringify({ targetParentId: 'parent-id' }));
jasmine.Ajax.requests.mostRecent().respondWith({ status: 200, contentType: 'json' });
});
});

View File

@@ -0,0 +1,150 @@
/*!
* @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 { AlfrescoApiService, AuthenticationService, ContentService, LogService, PermissionsEnum, ThumbnailService } from '@alfresco/core';
import { Injectable } from '@angular/core';
import { Response } from '@angular/http';
import { MinimalNodeEntity, MinimalNodeEntryEntity, NodePaging } from 'alfresco-js-api';
import { Observable } from 'rxjs/Rx';
@Injectable()
export class DocumentListService {
static ROOT_ID = '-root-';
constructor(authService: AuthenticationService,
private contentService: ContentService,
private apiService: AlfrescoApiService,
private logService: LogService,
private thumbnailService: ThumbnailService) {
}
private getNodesPromise(folder: string, opts?: any): Promise<NodePaging> {
let rootNodeId = DocumentListService.ROOT_ID;
if (opts && opts.rootFolderId) {
rootNodeId = opts.rootFolderId;
}
let params: any = {
includeSource: true,
include: ['path', 'properties', 'allowableOperations']
};
if (folder) {
params.relativePath = folder;
}
if (opts) {
if (opts.maxItems) {
params.maxItems = opts.maxItems;
}
if (opts.skipCount) {
params.skipCount = opts.skipCount;
}
}
return this.apiService.getInstance().nodes.getNodeChildren(rootNodeId, params);
}
deleteNode(nodeId: string): Observable<any> {
return Observable.fromPromise(this.apiService.getInstance().nodes.deleteNode(nodeId));
}
/**
* Copy a node to destination node
*
* @param nodeId The id of the node to be copied
* @param targetParentId The id of the folder-node where the node have to be copied to
*/
copyNode(nodeId: string, targetParentId: string) {
return Observable.fromPromise(this.apiService.getInstance().nodes.copyNode(nodeId, { targetParentId }))
.catch(err => this.handleError(err));
}
/**
* Move a node to destination node
*
* @param nodeId The id of the node to be moved
* @param targetParentId The id of the folder-node where the node have to be moved to
*/
moveNode(nodeId: string, targetParentId: string) {
return Observable.fromPromise(this.apiService.getInstance().nodes.moveNode(nodeId, { targetParentId }))
.catch(err => this.handleError(err));
}
/**
* Create a new folder in the path.
* @param name Folder name
* @param parentId Parent folder ID
* @returns {any}
*/
createFolder(name: string, parentId: string): Observable<MinimalNodeEntity> {
return Observable.fromPromise(this.apiService.getInstance().nodes.createFolder(name, '/', parentId))
.catch(err => this.handleError(err));
}
/**
* Gets the folder node with the specified relative name path below the root node.
* @param folder Path to folder.
* @param opts Options.
* @returns {Observable<NodePaging>} Folder entity.
*/
getFolder(folder: string, opts?: any) {
return Observable.fromPromise(this.getNodesPromise(folder, opts))
.map(res => <NodePaging> res)
.catch(err => this.handleError(err));
}
getFolderNode(nodeId: string): Promise<MinimalNodeEntryEntity> {
let opts: any = {
includeSource: true,
include: ['path', 'properties', 'allowableOperations']
};
let nodes: any = this.apiService.getInstance().nodes;
return nodes.getNodeInfo(nodeId, opts);
}
/**
* Get thumbnail URL for the given document node.
* @param node Node to get URL for.
* @returns {string} URL address.
*/
getDocumentThumbnailUrl(node: MinimalNodeEntity) {
return this.thumbnailService.getDocumentThumbnailUrl(node);
}
getMimeTypeIcon(mimeType: string): string {
return this.thumbnailService.getMimeTypeIcon(mimeType);
}
getDefaultMimeTypeIcon(): string {
return this.thumbnailService.getDefaultMimeTypeIcon();
}
hasPermission(node: any, permission: PermissionsEnum|string): boolean {
return this.contentService.hasPermission(node, permission);
}
private handleError(error: Response) {
// in a real world app, we may send the error to some remote logging infrastructure
// instead of just logging it to the console
this.logService.error(error);
return Observable.throw(error || 'Server error');
}
}

View File

@@ -0,0 +1,272 @@
/*!
* @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 { async, TestBed } from '@angular/core/testing';
import { TranslationService, AppConfigService, NotificationService } from '@alfresco/core';
import { Observable } from 'rxjs/Rx';
import { FileNode, FolderNode } from '../../mock';
import { ContentActionHandler } from '../models/content-action.model';
import { DocumentListService } from './document-list.service';
import { FolderActionsService } from './folder-actions.service';
import { NodeActionsService } from './node-actions.service';
describe('FolderActionsService', () => {
let service: FolderActionsService;
let documentListService: DocumentListService;
beforeEach(async(() => {
TestBed.configureTestingModule({
imports: [],
providers: [
DocumentListService,
FolderActionsService,
NodeActionsService,
TranslationService,
NotificationService
]
}).compileComponents();
}));
beforeEach(() => {
let appConfig: AppConfigService = TestBed.get(AppConfigService);
appConfig.config.ecmHost = 'http://localhost:9876/ecm';
service = TestBed.get(FolderActionsService);
documentListService = TestBed.get(DocumentListService);
});
it('should register custom action handler', () => {
let handler: ContentActionHandler = function () {
};
service.setHandler('<key>', handler);
expect(service.getHandler('<key>')).toBe(handler);
});
it('should not find handler that is not registered', () => {
expect(service.getHandler('<missing>')).toBeNull();
});
it('should be case insensitive for keys', () => {
let handler: ContentActionHandler = function () {
};
service.setHandler('<key>', handler);
expect(service.getHandler('<KEY>')).toBe(handler);
});
it('should not find handler with invalid key', () => {
expect(service.getHandler(null)).toBeNull();
expect(service.getHandler('')).toBeNull();
});
it('should allow action execution only when service available', () => {
let folder = new FolderNode();
expect(service.canExecuteAction(folder)).toBeTruthy();
});
it('should allow action execution only for folder nodes', () => {
expect(service.canExecuteAction(null)).toBeFalsy();
expect(service.canExecuteAction(new FileNode())).toBeFalsy();
expect(service.canExecuteAction(new FolderNode())).toBeTruthy();
});
it('should set new handler only by key', () => {
let handler: ContentActionHandler = function () {
};
expect(service.setHandler(null, handler)).toBeFalsy();
expect(service.setHandler('', handler)).toBeFalsy();
expect(service.setHandler('my-handler', handler)).toBeTruthy();
});
it('should register delete action', () => {
expect(service.getHandler('delete')).toBeDefined();
});
it('should not delete the folder node if there are no permissions', (done) => {
spyOn(documentListService, 'deleteNode').and.callThrough();
service.permissionEvent.subscribe((permission) => {
expect(permission).toBeDefined();
expect(permission.type).toEqual('folder');
expect(permission.action).toEqual('delete');
done();
});
let folder = new FolderNode();
service.getHandler('delete')(folder);
});
it('should delete the folder node if there is the delete permission', () => {
spyOn(documentListService, 'deleteNode').and.callFake(() => {
return new Observable<any>(observer => {
observer.next();
observer.complete();
});
});
let permission = 'delete';
let folder = new FolderNode();
let folderWithPermission: any = folder;
folderWithPermission.entry.allowableOperations = [permission];
const deleteObservale = service.getHandler('delete')(folderWithPermission, null, permission);
expect(documentListService.deleteNode).toHaveBeenCalledWith(folder.entry.id);
expect(deleteObservale.subscribe).toBeDefined();
});
it('should not delete the folder node if there is no delete permission', (done) => {
spyOn(documentListService, 'deleteNode').and.callFake(() => {
return new Observable<any>(observer => {
observer.next();
observer.complete();
});
});
service.permissionEvent.subscribe((permission) => {
expect(permission).toBeDefined();
expect(permission.type).toEqual('folder');
expect(permission.action).toEqual('delete');
done();
});
let folder = new FolderNode();
let folderWithPermission: any = folder;
folderWithPermission.entry.allowableOperations = ['create', 'update'];
service.getHandler('delete')(folderWithPermission);
});
it('should call the error on the returned Observable if there is no delete permission', (done) => {
spyOn(documentListService, 'deleteNode').and.callFake(() => {
return new Observable<any>(observer => {
observer.next();
observer.complete();
});
});
let folder = new FolderNode();
let folderWithPermission: any = folder;
folderWithPermission.entry.allowableOperations = ['create', 'update'];
const deleteObservable = service.getHandler('delete')(folderWithPermission);
deleteObservable.subscribe({
error: (error) => {
expect(error.message).toEqual('No permission to delete');
done();
}
});
});
it('should delete the folder node if there is the delete and others permission ', () => {
spyOn(documentListService, 'deleteNode').and.callFake(() => {
return new Observable<any>(observer => {
observer.next();
observer.complete();
});
});
let permission = 'delete';
let folder = new FolderNode();
let folderWithPermission: any = folder;
folderWithPermission.entry.allowableOperations = ['create', 'update', permission];
service.getHandler('delete')(folderWithPermission, null, permission);
expect(documentListService.deleteNode).toHaveBeenCalledWith(folder.entry.id);
});
it('should support deletion only folder node', () => {
spyOn(documentListService, 'deleteNode').and.callFake(() => {
return new Observable<any>(observer => {
observer.next();
observer.complete();
});
});
let permission = 'delete';
let file = new FileNode();
service.getHandler('delete')(file);
expect(documentListService.deleteNode).not.toHaveBeenCalled();
let folder = new FolderNode();
let folderWithPermission: any = folder;
folderWithPermission.entry.allowableOperations = [permission];
service.getHandler('delete')(folderWithPermission, null, permission);
expect(documentListService.deleteNode).toHaveBeenCalled();
});
it('should require node id to delete', () => {
spyOn(documentListService, 'deleteNode').and.callFake(() => {
return new Observable<any>(observer => {
observer.next();
observer.complete();
});
});
let folder = new FolderNode();
folder.entry.id = null;
service.getHandler('delete')(folder);
expect(documentListService.deleteNode).not.toHaveBeenCalled();
});
it('should reload target upon node deletion', (done) => {
spyOn(documentListService, 'deleteNode').and.callFake(() => {
return new Observable<any>(observer => {
observer.next();
observer.complete();
});
});
let permission = 'delete';
let target = jasmine.createSpyObj('obj', ['reload']);
let folder = new FolderNode();
let folderWithPermission: any = folder;
folderWithPermission.entry.allowableOperations = [permission];
let deleteHandler = service.getHandler('delete')(folderWithPermission, target, permission);
deleteHandler.subscribe(() => {
expect(target.reload).toHaveBeenCalled();
done();
});
expect(documentListService.deleteNode).toHaveBeenCalled();
});
it('should emit success event upon node deletion', (done) => {
spyOn(documentListService, 'deleteNode').and.callFake(() => {
return new Observable<any>(observer => {
observer.next();
observer.complete();
});
});
service.success.subscribe((nodeId) => {
expect(nodeId).not.toBeNull();
done();
});
let permission = 'delete';
let target = jasmine.createSpyObj('obj', ['reload']);
let folder = new FolderNode();
let folderWithPermission: any = folder;
folderWithPermission.entry.allowableOperations = [permission];
service.getHandler('delete')(folderWithPermission, target, permission);
});
});

View File

@@ -0,0 +1,112 @@
/*!
* @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 { ContentService } from '@alfresco/core';
import { Injectable } from '@angular/core';
import { MinimalNodeEntity } from 'alfresco-js-api';
import { Observable, Subject } from 'rxjs/Rx';
import { ContentActionHandler } from '../models/content-action.model';
import { PermissionModel } from '../models/permissions.model';
import { DocumentListService } from './document-list.service';
import { NodeActionsService } from './node-actions.service';
@Injectable()
export class FolderActionsService {
permissionEvent: Subject<PermissionModel> = new Subject<PermissionModel>();
error: Subject<Error> = new Subject<Error>();
success: Subject<string> = new Subject<string>();
private handlers: { [id: string]: ContentActionHandler; } = {};
constructor(private nodeActionsService: NodeActionsService,
private documentListService: DocumentListService,
private contentService: ContentService) {
this.setupActionHandlers();
}
getHandler(key: string): ContentActionHandler {
if (key) {
let lkey = key.toLowerCase();
return this.handlers[lkey] || null;
}
return null;
}
setHandler(key: string, handler: ContentActionHandler): boolean {
if (key) {
let lkey = key.toLowerCase();
this.handlers[lkey] = handler;
return true;
}
return false;
}
canExecuteAction(obj: any): boolean {
return this.documentListService && obj && obj.entry.isFolder === true;
}
private setupActionHandlers() {
this.handlers['copy'] = this.copyNode.bind(this);
this.handlers['move'] = this.moveNode.bind(this);
this.handlers['delete'] = this.deleteNode.bind(this);
}
private copyNode(obj: MinimalNodeEntity, target?: any, permission?: string) {
const actionObservable = this.nodeActionsService.copyFolder(obj.entry, permission);
this.prepareHandlers(actionObservable, 'folder', 'copy', target, permission);
return actionObservable;
}
private moveNode(obj: MinimalNodeEntity, target?: any, permission?: string) {
const actionObservable = this.nodeActionsService.moveFolder(obj.entry, permission);
this.prepareHandlers(actionObservable, 'folder', 'move', target, permission);
return actionObservable;
}
private prepareHandlers(actionObservable, type: string, action: string, target?: any, permission?: string): void {
actionObservable.subscribe(
(fileOperationMessage) => {
if (target && typeof target.reload === 'function') {
target.reload();
}
this.success.next(fileOperationMessage);
},
this.error.next.bind(this.error)
);
}
private deleteNode(obj: any, target?: any, permission?: string): Observable<any> {
let handlerObservable: Observable<any>;
if (this.canExecuteAction(obj)) {
if (this.contentService.hasPermission(obj.entry, permission)) {
handlerObservable = this.documentListService.deleteNode(obj.entry.id);
handlerObservable.subscribe(() => {
if (target && typeof target.reload === 'function') {
target.reload();
}
this.success.next(obj.entry.id);
});
return handlerObservable;
} else {
this.permissionEvent.next(new PermissionModel({type: 'folder', action: 'delete', permission: permission}));
return Observable.throw(new Error('No permission to delete'));
}
}
}
}

View File

@@ -0,0 +1,132 @@
/*!
* @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 { DataColumn } from '@alfresco/core';
import { ContentService } from '@alfresco/core';
import { EventEmitter, Injectable } from '@angular/core';
import { MatDialog } from '@angular/material';
import { MinimalNodeEntryEntity } from 'alfresco-js-api';
import { Subject } from 'rxjs/Rx';
import { ContentNodeSelectorComponent, ContentNodeSelectorComponentData } from '../../content-node-selector/content-node-selector.component';
import { ShareDataRow } from '../data/share-data-row.model';
import { DocumentListService } from './document-list.service';
@Injectable()
export class NodeActionsService {
constructor(private dialog: MatDialog,
private documentListService?: DocumentListService,
private contentService?: ContentService) {}
/**
* Copy content node
*
* @param contentEntry node to copy
* @param permission permission which is needed to apply the action
*/
public copyContent(contentEntry: MinimalNodeEntryEntity, permission?: string): Subject<string> {
return this.doFileOperation('copy', 'content', contentEntry, permission);
}
/**
* Copy folder node
*
* @param contentEntry node to copy
* @param permission permission which is needed to apply the action
*/
public copyFolder(contentEntry: MinimalNodeEntryEntity, permission?: string): Subject<string> {
return this.doFileOperation('copy', 'folder', contentEntry, permission);
}
/**
* Move content node
*
* @param contentEntry node to move
* @param permission permission which is needed to apply the action
*/
public moveContent(contentEntry: MinimalNodeEntryEntity, permission?: string): Subject<string> {
return this.doFileOperation('move', 'content', contentEntry, permission);
}
/**
* Move folder node
*
* @param contentEntry node to move
* @param permission permission which is needed to apply the action
*/
public moveFolder(contentEntry: MinimalNodeEntryEntity, permission?: string): Subject<string> {
return this.doFileOperation('move', 'folder', contentEntry, permission);
}
/**
* General method for performing the given operation (copy|move)
*
* @param action the action to perform (copy|move)
* @param type type of the content (content|folder)
* @param contentEntry the contentEntry which has to have the action performed on
* @param permission permission which is needed to apply the action
*/
private doFileOperation(action: string, type: string, contentEntry: MinimalNodeEntryEntity, permission?: string): Subject<string> {
const observable: Subject<string> = new Subject<string>();
if (this.contentService.hasPermission(contentEntry, permission)) {
const data: ContentNodeSelectorComponentData = {
title: `${action} ${contentEntry.name} to ...`,
currentFolderId: contentEntry.parentId,
rowFilter: this.rowFilter.bind(this, contentEntry.id),
imageResolver: this.imageResolver.bind(this),
select: new EventEmitter<MinimalNodeEntryEntity[]>()
};
this.dialog.open(ContentNodeSelectorComponent, { data, panelClass: 'adf-content-node-selector-dialog', width: '630px' });
data.select.subscribe((selections: MinimalNodeEntryEntity[]) => {
const selection = selections[0];
this.documentListService[`${action}Node`].call(this.documentListService, contentEntry.id, selection.id)
.subscribe(
observable.next.bind(observable, `OPERATION.SUCCES.${type.toUpperCase()}.${action.toUpperCase()}`),
observable.error.bind(observable)
);
this.dialog.closeAll();
});
return observable;
} else {
observable.error(new Error(JSON.stringify({ error: { statusCode: 403 } })));
return observable;
}
}
private rowFilter(currentNodeId, row: ShareDataRow): boolean {
const node: MinimalNodeEntryEntity = row.node.entry;
if (node.id === currentNodeId || node.isFile) {
return false;
} else {
return true;
}
}
private imageResolver(row: ShareDataRow, col: DataColumn): string|null {
const entry: MinimalNodeEntryEntity = row.node.entry;
if (!this.contentService.hasPermission(entry, 'create')) {
return this.documentListService.getMimeTypeIcon('disable/folder');
}
return null;
}
}

View File

@@ -0,0 +1,165 @@
{
"FORM": {
"START_FORM": {
"TITLE": "Startformular"
},
"PREVIEW": {
"IMAGE_NOT_AVAILABLE": "Vorschau nicht verfügbar"
},
"FIELD": {
"REQUIRED": "*Erforderlich"
}
},
"ADF-DOCUMENT-LIST": {
"EMPTY": {
"HEADER": "Dieser Ordner ist leer"
},
"LAYOUT": {
"THUMBNAIL": "Miniaturansicht",
"NAME": "Name",
"LOCATION": "Speicherort",
"SIZE": "Größe",
"DELETED_ON": "Gelöscht",
"DELETED_BY": "Gelöscht von",
"STATUS": "Status",
"MODIFIED_ON": "Bearbeitet",
"MODIFIED_BY": "Bearbeitet von",
"SHARED_BY": "Freigegeben von",
"LOAD_MORE": "Mehr laden"
}
},
"ALFRESCO_DOCUMENT_LIST": {
"BUTTON": {
"ACTION_CREATE": "Erstellen ...",
"ACTION_NEW_FOLDER": "Neuer Ordner",
"CREATE": "Erstellen",
"CANCEL": "Abbrechen"
}
},
"DROPDOWN": {
"PLACEHOLDER_LABEL": "Site-Liste",
"MY_FILES_OPTION": ""
},
"NODE_SELECTOR": {
"CANCEL": "Abbrechen",
"CHOOSE": "Auswählen",
"NO_RESULTS": "Keine Ergebnisse gefunden"
},
"OPERATION": {
"SUCCES": {
"CONTENT": {
"COPY": "Kopieren erfolgreich",
"MOVE": "Verschieben erfolgreich"
},
"FOLDER": {
"COPY": "Kopieren erfolgreich",
"MOVE": "Verschieben erfolgreich"
}
},
"ERROR": {
"CONFLICT": "Dieser Name wird bereits verwendet. Versuchen Sie es mit einem anderen Namen.",
"UNKNOWN": "Die Aktion war nicht erfolgreich. Versuchen Sie es noch einmal oder wenden Sie sich an Ihr IT-Team.",
"PERMISSION": "Sie verfügen nicht über die nötigen Zugriffsrechte hierfür."
}
},
"TAG": {
"LABEL": {
"NEWTAG": "Neues Tag"
},
"MESSAGES": {
"EXIST": "Dieses Tag gibt es bereits"
},
"BUTTON": {
"ADD": "Tag hinzufügen"
}
},
"ADF_FILE_UPLOAD": {
"BUTTON": {
"MINIMIZE": "Minimieren",
"MAXIMIZE": "Maximieren",
"CLOSE": "Schließen",
"CANCEL_ALL": "Hochladen abbrechen",
"CANCEL_FILE": "Upload abbrechen",
"REMOVE_FILE": "Hochgeladene Datei entfernen"
},
"STATUS": {
"FILE_CANCELED_STATUS": "Abgebrochen"
},
"CONFIRMATION": {
"BUTTON": {
"CANCEL": "Ja",
"CONTINUE": "Nein"
},
"MESSAGE": {
"TITLE": "Upload abbrechen",
"TEXT": "Beenden Sie das Hochladen und entfernen Sie bereits hochgeladene Dateien."
}
}
},
"FILE_UPLOAD": {
"BUTTON": {
"UPLOAD_FILE": "Datei hochladen",
"UPLOAD_FOLDER": "Ordner hochladen"
},
"MESSAGES": {
"UPLOAD_CANCELED": "Hochladen abgebrochen",
"UPLOAD_COMPLETED": "Hochgeladen: {{ completed }} / {{ total }}",
"UPLOAD_PROGRESS": "Hochladen: {{ completed }} / {{ total }}",
"UPLOAD_ERROR": "{{ total }} erfolgreich hochgeladen",
"UPLOAD_ERRORS": "{{ total }} nicht erfolgreich hochgeladen",
"PROGRESS": "Hochladen läuft ...",
"FOLDER_ALREADY_EXIST": "Den Ordner {0} gibt es bereits",
"FOLDER_NOT_SUPPORTED": "Ihr Browser lässt das Hochladen von Ordnern nicht zu. Versuchen Sie es mit einem anderen Browser",
"REMOVE_FILE_ERROR": "{{ fileName }} konnte nicht entfernt werden. Versuchen Sie es noch einmal oder wenden Sie sich an Ihr IT-Team.",
"REMOVE_FILES_ERROR": "{{ total }} Dateien konnten nicht entfernt werden. Versuchen Sie es noch einmal oder wenden Sie sich an Ihr IT-Team."
},
"ACTION": {
"UNDO": "Rückgängig machen"
}
},
"SEARCH": {
"CONTROL": {},
"RESULTS": {
"SUMMARY": "{{numResults}} Ergebnis gefunden für {{searchTerm}}",
"NONE": "Keine Ergebnisse gefunden für {{searchTerm}}",
"ERROR": "Bei der Suche ist ein Problem aufgetreten. Versuchen Sie es noch einmal.",
"COLUMNS": {
"NAME": "Namen anzeigen",
"MODIFIED_BY": "Bearbeitet von",
"MODIFIED_AT": "Bearbeitet"
}
},
"ICONS": {
"ft_ic_raster_image": "Bilddatei",
"ft_ic_pdf": "PDF-Dokument",
"ft_ic_ms_excel": "Microsoft Excel-Datei",
"ft_ic_ms_word": "Microsoft Word-Dokument",
"ft_ic_ms_powerpoint": "Microsoft PowerPoint-Datei",
"ft_ic_video": "Videodatei",
"ft_ic_document": "Dokumentdatei",
"ft_ic_website": "Online-Ressource",
"ft_ic_archive": "Archivdatei",
"ft_ic_presentation": "Präsentation",
"ft_ic_spreadsheet": "Kalkulationstabellendatei"
},
"DOCUMENT_LIST": {
"COLUMNS": {
"DISPLAY_NAME": "Namen anzeigen",
"CREATED_BY": "Erstellt von",
"CREATED_ON": "Erstellt"
},
"ACTIONS": {
"FOLDER": {
"DELETE": "Löschen"
},
"DOCUMENT": {
"DOWNLOAD": "Herunterladen",
"DELETE": "Löschen"
}
}
}
},
"PERMISSON": {
"LACKOF": "Sie verfügen nicht über die nötige Berechtigung ('{{permission}}'), um {{type}} zu {{action}}"
}
}

View File

@@ -0,0 +1,186 @@
{
"FORM": {
"START_FORM": {
"TITLE": "Start Form"
},
"PREVIEW": {
"IMAGE_NOT_AVAILABLE": "Preview not available"
},
"FIELD": {
"UPLOAD": "UPLOAD",
"REQUIRED": "*Required",
"VALIDATOR": {
"INVALID_NUMBER": "Use a different number format",
"INVALID_DATE": "Use a different date format",
"INVALID_VALUE": "Enter a different value",
"NOT_GREATER_THAN": "Can't be greater than {{ maxValue }}",
"NOT_LESS_THAN": "Can't be less than {{ minValue }}",
"AT_LEAST_LONG": "Enter at least {{ minLength }} characters",
"NO_LONGER_THAN": "Enter no more than {{ maxLength }} characters"
}
}
},
"ADF-DOCUMENT-LIST": {
"EMPTY": {
"HEADER": "This folder is empty"
},
"NO_PERMISSION": "You don't have permission to view this file or folder.",
"LAYOUT": {
"CREATED": "Created",
"THUMBNAIL": "Thumbnail",
"NAME": "Name",
"LOCATION": "Location",
"SIZE": "Size",
"DELETED_ON": "Deleted",
"DELETED_BY": "Deleted by",
"STATUS": "Status",
"MODIFIED_ON": "Modified",
"MODIFIED_BY": "Modified by",
"SHARED_BY": "Shared by",
"LOAD_MORE": "Load more"
},
"MENU_ACTIONS": {
"VIEW": "View",
"REMOVE": "Remove",
"DOWNLOAD": "Download"
}
},
"ALFRESCO_DOCUMENT_LIST": {
"BUTTON": {
"ACTION_CREATE": "Create...",
"ACTION_NEW_FOLDER": "New Folder",
"CREATE": "Create",
"CANCEL": "Cancel"
}
},
"DROPDOWN": {
"PLACEHOLDER_LABEL": "Site List",
"MY_FILES_OPTION": "My files"
},
"NODE_SELECTOR": {
"CANCEL": "Cancel",
"CHOOSE": "Choose",
"NO_RESULTS": "No results found"
},
"OPERATION": {
"SUCCES": {
"CONTENT": {
"COPY": "Copy successful",
"MOVE": "Move successful"
},
"FOLDER": {
"COPY": "Copy successful",
"MOVE": "Move successful"
}
},
"ERROR": {
"CONFLICT": "This name is already in use, try a different name.",
"UNKNOWN": "The action was unsuccessful. Try again or contact your IT Team.",
"PERMISSION": "You don't have access to do this."
}
},
"TAG": {
"LABEL": {
"NEWTAG": "New Tag"
},
"MESSAGES": {
"EXIST": "Tag already exists"
},
"BUTTON": {
"ADD": "Add Tag"
}
},
"ADF_FILE_UPLOAD": {
"BUTTON": {
"MINIMIZE": "Minimize",
"MAXIMIZE": "Maximize",
"CLOSE": "Close",
"CANCEL_ALL": "Cancel uploads",
"CANCEL_FILE": "Cancel upload",
"REMOVE_FILE": "Remove uploaded file"
},
"STATUS": {
"FILE_CANCELED_STATUS": "Canceled"
},
"CONFIRMATION": {
"BUTTON": {
"CANCEL": "Yes",
"CONTINUE": "No"
},
"MESSAGE": {
"TITLE": "Cancel Upload",
"TEXT": "Stop uploading and remove files already uploaded."
}
}
},
"FILE_UPLOAD": {
"BUTTON": {
"UPLOAD_FILE": "Upload file",
"UPLOAD_FOLDER": "Upload folder"
},
"MESSAGES": {
"UPLOAD_CANCELED": "Upload canceled",
"UPLOAD_COMPLETED": "Uploaded {{ completed }} / {{ total }}",
"UPLOAD_PROGRESS": "Uploading {{ completed }} / {{ total }}",
"UPLOAD_ERROR": "{{ total }} upload unsuccessful",
"UPLOAD_ERRORS": "{{ total }} uploads unsuccessful",
"PROGRESS": "Upload in progress...",
"FOLDER_ALREADY_EXIST": "The folder {0} already exists",
"FOLDER_NOT_SUPPORTED": "Folder upload isn't supported by your browser, try a different browser",
"REMOVE_FILE_ERROR": "{{ fileName }} couldn't be removed, try again or check with your IT Team.",
"REMOVE_FILES_ERROR": "{{ total }} files couldn't be removed, try again or check with your IT Team.",
"EXCEED_MAX_FILE_SIZE": "File {{ fileName }} is larger than the allowed file size"
},
"ACTION": {
"UNDO": "Undo"
}
},
"WEBSCRIPT": {
"ERROR": "Couldn't complete the action. Share this message with your IT Team: Error during the deserialization of {{data}} as {{contentType}}"
},
"SEARCH": {
"CONTROL": {},
"RESULTS": {
"SUMMARY": "{{numResults}} result found for {{searchTerm}}",
"NONE": "No results found for {{searchTerm}}",
"ERROR": "We hit a problem during the search - try again.",
"COLUMNS": {
"NAME": "Display name",
"MODIFIED_BY": "Modified by",
"MODIFIED_AT": "Modified at"
}
},
"ICONS": {
"ft_ic_raster_image": "Image file",
"ft_ic_pdf": "PDF document",
"ft_ic_ms_excel": "Microsoft Excel file",
"ft_ic_ms_word": "Microsoft Word document",
"ft_ic_ms_powerpoint": "Microsoft PowerPoint file",
"ft_ic_video": "Video file",
"ft_ic_document": "Document file",
"ft_ic_website": "Web resource",
"ft_ic_archive": "Archive file",
"ft_ic_presentation": "Presentation file",
"ft_ic_spreadsheet": "Spreadsheet file"
},
"DOCUMENT_LIST": {
"COLUMNS": {
"DISPLAY_NAME": "Display name",
"CREATED_BY": "Created by",
"CREATED_ON": "Created"
},
"ACTIONS": {
"FOLDER": {
"DELETE": "Delete"
},
"DOCUMENT": {
"DOWNLOAD": "Download",
"DELETE": "Delete"
}
}
}
},
"PERMISSON": {
"LACKOF": "You don't have the {{permission}} permission to {{action}} the {{type}}"
}
}

View File

@@ -0,0 +1,165 @@
{
"FORM": {
"START_FORM": {
"TITLE": "Iniciar formulario"
},
"PREVIEW": {
"IMAGE_NOT_AVAILABLE": "Vista previa no disponible"
},
"FIELD": {
"REQUIRED": "*Requerido"
}
},
"ADF-DOCUMENT-LIST": {
"EMPTY": {
"HEADER": "Esta carpeta está vacía"
},
"LAYOUT": {
"THUMBNAIL": "Miniatura",
"NAME": "Nombre",
"LOCATION": "Ubicación",
"SIZE": "Tamaño",
"DELETED_ON": "Eliminado",
"DELETED_BY": "Eliminado por",
"STATUS": "Estado",
"MODIFIED_ON": "Modificado",
"MODIFIED_BY": "Modificado por",
"SHARED_BY": "Compartido por",
"LOAD_MORE": "Cargar más"
}
},
"ALFRESCO_DOCUMENT_LIST": {
"BUTTON": {
"ACTION_CREATE": "Crear...",
"ACTION_NEW_FOLDER": "Nueva carpeta",
"CREATE": "Crear",
"CANCEL": "Cancelar"
}
},
"DROPDOWN": {
"PLACEHOLDER_LABEL": "Lista de sitios",
"MY_FILES_OPTION": ""
},
"NODE_SELECTOR": {
"CANCEL": "Cancelar",
"CHOOSE": "Elegir",
"NO_RESULTS": "Ningún resultado encontrado"
},
"OPERATION": {
"SUCCES": {
"CONTENT": {
"COPY": "Copia satisfactoria",
"MOVE": "Traslado satisfactorio"
},
"FOLDER": {
"COPY": "Copia satisfactoria",
"MOVE": "Traslado satisfactorio"
}
},
"ERROR": {
"CONFLICT": "Este nombre ya está en uso; pruebe con un nombre diferente.",
"UNKNOWN": "La acción no ha sido satisfactoria. Vuelva a intentarlo o póngase en contacto con el equipo de TI.",
"PERMISSION": "No tiene acceso para hacer esto."
}
},
"TAG": {
"LABEL": {
"NEWTAG": "Nueva etiqueta"
},
"MESSAGES": {
"EXIST": "Ya existe esta etiqueta"
},
"BUTTON": {
"ADD": "Añadir etiqueta"
}
},
"ADF_FILE_UPLOAD": {
"BUTTON": {
"MINIMIZE": "Minimizar",
"MAXIMIZE": "Maximizar",
"CLOSE": "Cerrar",
"CANCEL_ALL": "Cancelar cargas",
"CANCEL_FILE": "Cancelar carga",
"REMOVE_FILE": "Eliminar fichero cargado"
},
"STATUS": {
"FILE_CANCELED_STATUS": "Cancelado"
},
"CONFIRMATION": {
"BUTTON": {
"CANCEL": "Sí",
"CONTINUE": "No"
},
"MESSAGE": {
"TITLE": "Cancelar carga",
"TEXT": "Detener la carga y eliminar los ficheros ya cargados."
}
}
},
"FILE_UPLOAD": {
"BUTTON": {
"UPLOAD_FILE": "Añadir fichero",
"UPLOAD_FOLDER": "Cargar carpeta"
},
"MESSAGES": {
"UPLOAD_CANCELED": "Carga cancelada",
"UPLOAD_COMPLETED": "Cargados {{ completed }} / {{ total }}",
"UPLOAD_PROGRESS": "Cargando {{ completed }} / {{ total }}",
"UPLOAD_ERROR": "Error en carga {{ total }}",
"UPLOAD_ERRORS": "Error en {{ total }} cargas",
"PROGRESS": "Carga en curso...",
"FOLDER_ALREADY_EXIST": "La carpeta {0} ya existe",
"FOLDER_NOT_SUPPORTED": "Su navegador no es compatible con la carga de carpetas; pruebe con un navegador diferente.",
"REMOVE_FILE_ERROR": "{{ fileName }} no se ha podido eliminar; vuelva a intentarlo o póngase en contacto con el equipo de TI.",
"REMOVE_FILES_ERROR": "{{ total }} ficheros no se han podido eliminar; vuelva a intentarlo o póngase en contacto con su equipo de TI."
},
"ACTION": {
"UNDO": "Deshacer"
}
},
"SEARCH": {
"CONTROL": {},
"RESULTS": {
"SUMMARY": "{{numResults}} resultados encontrados para {{searchTerm}}",
"NONE": "No se han encontrado resultados para {{searchTerm}}",
"ERROR": "Se ha producido un problema durante la búsqueda; vuelva a intentarlo.",
"COLUMNS": {
"NAME": "Nombre a mostrar",
"MODIFIED_BY": "Modificado por",
"MODIFIED_AT": "Modificado a las"
}
},
"ICONS": {
"ft_ic_raster_image": "Fichero de imagen",
"ft_ic_pdf": "Documento PDF",
"ft_ic_ms_excel": "Fichero de Microsoft Excel",
"ft_ic_ms_word": "Documento de Microsoft Word",
"ft_ic_ms_powerpoint": "Fichero de Microsoft PowerPoint",
"ft_ic_video": "Fichero de vídeo",
"ft_ic_document": "Fichero de documento",
"ft_ic_website": "Recurso web",
"ft_ic_archive": "Fichero de archivo",
"ft_ic_presentation": "Fichero de presentación",
"ft_ic_spreadsheet": "Fichero de hoja de cálculo"
},
"DOCUMENT_LIST": {
"COLUMNS": {
"DISPLAY_NAME": "Nombre a mostrar",
"CREATED_BY": "Creado por",
"CREATED_ON": "Creado"
},
"ACTIONS": {
"FOLDER": {
"DELETE": "Eliminar"
},
"DOCUMENT": {
"DOWNLOAD": "Descargar",
"DELETE": "Eliminar"
}
}
}
},
"PERMISSON": {
"LACKOF": "No tiene permiso {{permission}} para {{action}} el {{type}}"
}
}

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