diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 0000000..86659e2 --- /dev/null +++ b/.travis.yml @@ -0,0 +1,25 @@ +sudo: required +dist: trusty +language: node_js +node_js: "6.11.0" + +cache: + directories: + - node_modules + +apt: + sources: + - google-chrome + packages: + - google-chrome-stable + - google-chrome-beta + +install: + - npm i -g @angular/cli + - npm i + - npm test + +before_install: + - export CHROME_BIN=chromium-browser + - export DISPLAY=:99.0 + - sh -e /etc/init.d/xvfb start diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..d4d931c --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +The MIT License (MIT) + +Copyright (c) 2013-2017 The angular-translate team and Pascal Precht + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/README.md b/README.md index c07eed6..cdcbb3a 100644 --- a/README.md +++ b/README.md @@ -4,6 +4,14 @@ This project is a very simple __Angular2 file manager__. ## Features +### v1.0.0 +* update angular2-tree to verison 2.x.x +* update angular to version 4.x.x +* use ngrx/store +* prepare events for all actions +* update configuration: allowed file types filter for upload files, allow limit for uploaded file +* create examples: with backend in node, without backend on local storage + ### v0.5.4 * fix problem with open "choose file window" @@ -59,37 +67,110 @@ Install npm package In your project put this line - Loading... - -## Override API - -To override endpoints to manage files and directories provide special provider in you module - + Loading... + +### Provide configuration +In your module add following lines with configuration + + import {FileManagerModule, IFileManagerConfiguration} from '../../../main'; + ... + const fileManagerConfiguration: IFileManagerConfiguration = { + urls: { + foldersUrl: '/api/folder', + filesUrl: /api/files, + folderMoveUrl: '/api/folder/move' + }, + isMultiSelection: true, + mimeTypes: ['image/jpg', 'image/jpeg', 'image/png'], + maxFileSize: 50 * 1024 + } + ... + +You can create a simple configuration object, it should contains a subset of below options + +* __urls__ + * _foldersUrl_ - url for create (POST), delete (DELETE), edit (PUT) and get (GET) folders + * _filesUrl_ - url for upload (POST), edit (PUT), delete (DELETE) and get (GET) files + * _folderMoveUrl_ - +* __isMultiselection__ - allow/disallow multiselection +* __mimeTypes__ - list of file type mimes which are allowed to upload +* __maxFileSize__ - limit of the single file size + +Then you have to provide this constant as a configuration service + @NgModule({ - ... - providers: [ - ... - { - provide: 'fileManagerUrls', - useValue: {foldersUrl: '/api/filemanager/folder', filesUrl: '/api/filemanager/file'} - } - ] - ... + ... + imports: [ + ..., + FileManagerModule + ], + providers: [ + {provide: 'fileManagerConfiguration', useValue: fileManagerConfiguration} + ], + bootstrap: [AppComponent] }) + export class AppModule { + } + +### Create API service + +Now you should create your own API service to communicate with backend or use existing one _FileManagerBackendApiService_. +If you create your own API service it should have implemented _IFileManagerApi_ interface +* _add(node: IOuterNode, parentNodeId: string): Observable;_ - create new node of the tree +* _load(nodeId: string): Observable;_ - load tree branch (if nodeId is empty string it loads root level) +* _move(srcNode: IOuterNode, targetNode: IOuterNode | null): Observable;_ - move one node (with all its sub nodes) to another parent +* _update(node: IOuterNode): Observable;_ - update node name +* _remove(nodeId: string): Observable;_ - remove node +* _cropFile(file: IOuterFile, bounds: ICropBounds): Observable;_ - crop file to provided bounds +* _loadFiles(nodeId: string): Observable;_ - load files from given node +* _removeFile(file: IOuterFile): Observable;_ - remove single file +* _uploadFile(file: IOuterFile): Observable;_ - do actions with uploaded file (real upload is done in ng2-upload-file) + +All those actions should manipulate on two protected properties: +* _nodes: IOuterNode[]_ - list of all loaded nodes +* _files: IFileDataProperties[]_ - list of files form current node + +You can see two examples of that service: +* [_FileManagerApiService_](src/store/fileManagerApi.service.ts) - works on local storage +* [_FileManagerBackendApiService_](src/store/fileManagerBackendApi.service.ts) - works on backend (written in node) + +### Attach to any Effects + +Because of using _store_, _actions_ and _effects_ you can attach to any actions by creating your own effects service. +You are able to connect to actions for doing something special (but this is not obligatory, this is only possibility): +* _FileManagerActionsService.FILEMANAGER_CROP_FILE_ +* _FileManagerActionsService.FILEMANAGER_CROP_FILE_SUCCESS_ +* _FileManagerActionsService.FILEMANAGER_CROP_FILE_ERROR_ +* _FileManagerActionsService.FILEMANAGER_DELETE_FILE_ +* _FileManagerActionsService.FILEMANAGER_DELETE_FILE_SUCCESS_ +* _FileManagerActionsService.FILEMANAGER_DELETE_FILE_SELECTION_ +* _FileManagerActionsService.FILEMANAGER_DELETE_FILE_SELECTION_SUCCESS_ +* _FileManagerActionsService.FILEMANAGER_INVERSE_FILE_SELECTION_ +* _FileManagerActionsService.FILEMANAGER_LOAD_FILES_ +* _FileManagerActionsService.FILEMANAGER_LOAD_FILES_SUCCESS_ +* _FileManagerActionsService.FILEMANAGER_SELECT_ALL_ +* _FileManagerActionsService.FILEMANAGER_SELECT_FILE_ +* _FileManagerActionsService.FILEMANAGER_UNSELECT_FILE_ +* _FileManagerActionsService.FILEMANAGER_UNSELECT_ALL_ +* _FileManagerActionsService.FILEMANAGER_UPLOAD_FILE_ +* _FileManagerActionsService.FILEMANAGER_UPLOAD_FILE_ERROR_ +* _FileManagerActionsService.FILEMANAGER_UPLOAD_FILE_SUCCESS_ ## Demo -To run demo you have to serve frontend and backend. To do this run: +To run local demo you have to serve frontend and backend. To do this run: * frontend: - + * using local storage + npm start + + * or using real backend + + npm run startWithBackend * backend npm run backend -## TODO - -* files upload progress -* multi selection events (delete, select) +Or you can see online [demo](https://qjon.github.io/angular2-filemanager/) with _local storage_ diff --git a/angular-cli.json b/angular-cli.json index 2e5935e..f9cf3ba 100644 --- a/angular-cli.json +++ b/angular-cli.json @@ -1,11 +1,12 @@ { "$schema": "./node_modules/@angular/cli/lib/config/schema.json", "project": { - "version": "1.0.0-beta.32.3", + "version": "1.2.0", "name": "angular2-filemanager" }, "apps": [ { + "name": "withoutBackend", "root": "demo/src", "outDir": "dist", "assets": [ @@ -16,8 +17,37 @@ "index": "index.html", "main": "main.ts", "polyfills": "polyfills.ts", - "test": "test.ts", + "test": "../../src/test.ts", "tsconfig": "tsconfig.json", + "testTsconfig": "../../src/tsconfig.spec.json", + "prefix": "app", + "mobile": false, + "styles": [ + "../../node_modules/bootstrap/dist/css/bootstrap.min.css", + "../../node_modules/font-awesome/css/font-awesome.css" + ], + "scripts": [], + "environmentSource": "environments/environment.ts", + "environments": { + "dev": "environments/environment.ts", + "prod": "environments/environment.prod.ts" + } + }, + { + "name": "withBackend", + "root": "demo/src", + "outDir": "dist", + "assets": [ + "assets", + "icons", + "favicon.ico" + ], + "index": "index.html", + "main": "mainWithBackend.ts", + "polyfills": "polyfills.ts", + "test": "../../src/test.ts", + "tsconfig": "tsconfig.json", + "testTsconfig": "../../src/tsconfig.spec.json", "prefix": "app", "mobile": false, "styles": [ diff --git a/demo/backend/index.js b/demo/backend/index.js index 6f6bf53..0947435 100644 --- a/demo/backend/index.js +++ b/demo/backend/index.js @@ -37,15 +37,16 @@ app.use(express.static(basePath)); app.get('/folders', function (req, res) { var paths = []; - var subdir = req.query.nodeId || ''; - var items = fs.readdirSync(basePath + subdir); + var subNode = req.query.nodeId || ''; + var items = fs.readdirSync(basePath + subNode); for (var i = 0; i < items.length; i++) { var name = items[i]; - var stat = fs.statSync(basePath + subdir + '/' + name); + var stat = fs.statSync(basePath + subNode + '/' + name); if (stat && stat.isDirectory()) { var dir = { - id: subdir + '/' + name, + id: subNode + '/' + name, + parentId: subNode || null, name: name, children: [] }; @@ -59,79 +60,105 @@ app.get('/folders', function (req, res) { }); app.put('/folders', function (req, res) { - var folder = req.body; + var node = req.body; - if (isDirectory(folder.id)) { - var subdirs = folder.id.split('/'); - subdirs[subdirs.length - 1] = folder.name; - var newDirName = subdirs.join('/'); + if (isDirectory(node.id)) { + var subNodes = node.id.split('/'); + subNodes[subNodes.length - 1] = node.name; + var newNodeName = subNodes.join('/'); - if (isDirectory(newDirName)) { + if (isDirectory(newNodeName)) { res.sendStatus(403); res.json({msg: 'Directory already exists'}); } else { - fs.renameSync(basePath + folder.id, basePath + newDirName); + fs.renameSync(basePath + node.id, basePath + newNodeName); - if (isDirectory(newDirName)) { - folder.id = newDirName; - res.json(folder); + if (isDirectory(newNodeName)) { + node.id = newNodeName; + res.json(node); } else { res.sendStatus(403); - res.json({msg: 'Could not change directory name'}); + res.json({msg: 'Could not change node name'}); } } } else { res.sendStatus(403); - res.json({msg: 'Directory does not exist'}); + res.json({msg: 'Node does not exist'}); } - }); +app.put('/folders/move', function (req, res) { + var data = req.body; + console.log(data); + + if (data.target === null) { + data.target = ''; + } + + if (isDirectory(data.source) && isDirectory(data.target)) { + var subNodes = data.source.split('/'); + var dirName = subNodes[subNodes.length - 1]; + var newNodeName = data.target + '/' + dirName; + + fs.renameSync(basePath + data.source, basePath + newNodeName); + var dir = { + id: newNodeName, + name: dirName, + parentId: data.target, + children: [] + }; + + res.json(dir); + } else { + res.sendStatus(403); + res.json({msg: 'Node does not exist'}); + } + +}); app.post('/folders', function (req, res) { var data = req.body; - var folder = data.node; + var node = data.node; var parentFolderId = data.parentNodeId || ''; - var newDirId = parentFolderId + '/' + folder.name; + var newNodeId = parentFolderId + '/' + node.name; - if (!isDirectory(newDirId)) { - fs.mkdirSync(basePath + newDirId); + if (!isDirectory(newNodeId)) { + fs.mkdirSync(basePath + newNodeId); - if (isDirectory(newDirId)) { + if (isDirectory(newNodeId)) { res.json({ - id: newDirId, - name: folder.name, + id: newNodeId, + name: node.name, + parentId: parentFolderId || null, children: [] }); } else { res.sendStatus(403); - res.json({msg: 'Directory has not been added'}); + res.json({msg: 'Node has not been added'}); } } else { res.sendStatus(403); - res.json({msg: 'Directory exists'}); + res.json({msg: 'Node exists'}); } - }); app.delete('/folders', function (req, res) { var data = req.body; - var folderId = data.nodeId || null; + var nodeId = data.nodeId || null; - if (isDirectory(folderId)) { - fs.rmdirSync(basePath + folderId); + if (isDirectory(nodeId)) { + fs.rmdirSync(basePath + nodeId); res.json({ - success: !isDirectory(folderId) + success: !isDirectory(nodeId) }); } else { res.sendStatus(403); res.json({msg: 'Directory exists'}); } - }); @@ -155,7 +182,7 @@ function prepareFile(filePath) { name: name, thumbnailUrl: src, url: src, - mime: mimeType, + type: mimeType, width: isImage ? dimensions.width : 0, height: isImage ? dimensions.height : 0 }; @@ -182,6 +209,7 @@ app.get('/files', function (req, res) { app.post('/files', function (req, res) { var fileExist = false; + var newPath; var form = new formidable.IncomingForm(); form.multiples = true; @@ -190,7 +218,6 @@ app.post('/files', function (req, res) { form.on('file', function (field, file) { var folder = req.header('folderId'); - var newPath; file.name = file.name.replace(/[^A-Za-z0-9\-\._]/g, ''); @@ -217,7 +244,7 @@ app.post('/files', function (req, res) { res.statusCode = 409; res.end('error'); } else { - res.end('success'); + res.json(prepareFile(newPath)); } }); diff --git a/demo/src/app/app.component.html b/demo/src/app/app.component.html index 94ce9cb..e32d140 100644 --- a/demo/src/app/app.component.html +++ b/demo/src/app/app.component.html @@ -2,9 +2,9 @@

Filemanager

- Loading... + Loading... diff --git a/demo/src/app/app.component.spec.ts b/demo/src/app/app.component.spec.ts deleted file mode 100644 index be6cd8a..0000000 --- a/demo/src/app/app.component.spec.ts +++ /dev/null @@ -1,34 +0,0 @@ -/* tslint:disable:no-unused-variable */ - -import {TestBed, async} from '@angular/core/testing'; -import {AppComponent} from './app.component'; - -describe('AppComponent', () => { - beforeEach(() => { - TestBed.configureTestingModule({ - declarations: [ - AppComponent - ], - }); - TestBed.compileComponents(); - }); - - it('should create the app', async(() => { - let fixture = TestBed.createComponent(AppComponent); - let app = fixture.debugElement.componentInstance; - expect(app).toBeTruthy(); - })); - - it(`should have as title 'app works!'`, async(() => { - let fixture = TestBed.createComponent(AppComponent); - let app = fixture.debugElement.componentInstance; - expect(app.title).toEqual('app works!'); - })); - - it('should render title in a h1 tag', async(() => { - let fixture = TestBed.createComponent(AppComponent); - fixture.detectChanges(); - let compiled = fixture.debugElement.nativeElement; - expect(compiled.querySelector('h1').textContent).toContain('app works!'); - })); -}); diff --git a/demo/src/app/app.component.ts b/demo/src/app/app.component.ts index 0f84a3a..f812cc0 100644 --- a/demo/src/app/app.component.ts +++ b/demo/src/app/app.component.ts @@ -1,21 +1,22 @@ import {Component} from '@angular/core'; -import {ISelectFile} from "../../../main"; +import {FileManagerConfiguration, FileManagerDispatcherService} from '../../../main'; + @Component({ - selector: 'app-root', - templateUrl: './app.component.html', - styleUrls: ['./app.component.less'] + selector: 'app-root', + templateUrl: './app.component.html', + styleUrls: ['./app.component.less'] }) export class AppComponent { - title = 'app works!'; - - public isMultiSelection = false; + public constructor(public fileManagerConfiguration: FileManagerConfiguration, + private fileManagerDispatcher: FileManagerDispatcherService) { + } - public toggleMultiSelection() { - this.isMultiSelection = !this.isMultiSelection; - } + public toggleMultiSelection() { + this.fileManagerConfiguration.isMultiSelection = !this.fileManagerConfiguration.isMultiSelection; - public selectFile(data: ISelectFile) { - console.log(data); + if (!this.fileManagerConfiguration.isMultiSelection) { + this.fileManagerDispatcher.unSelectAllFiles(); } + } } diff --git a/demo/src/app/app.module.ts b/demo/src/app/app.module.ts index e4305a5..b8bcf8b 100644 --- a/demo/src/app/app.module.ts +++ b/demo/src/app/app.module.ts @@ -4,8 +4,18 @@ import {FormsModule} from '@angular/forms'; import {HttpModule} from '@angular/http'; import {AppComponent} from './app.component'; -import {FileManagerModule} from "../../../src/filemanager.module"; +import {FileManagerModule, IFileManagerConfiguration} from '../../../main'; +const fileManagerConfiguration: IFileManagerConfiguration = { + urls: { + foldersUrl: '/api/folder', + filesUrl: null, + folderMoveUrl: '/api/folder/move' + }, + isMultiSelection: true, + mimeTypes: ['image/jpg', 'image/jpeg', 'image/png'], + maxFileSize: 50 * 1024 +}; @NgModule({ declarations: [ @@ -18,7 +28,7 @@ import {FileManagerModule} from "../../../src/filemanager.module"; HttpModule ], providers: [ - {provide: 'fileManagerUrls', useValue: {foldersUrl: '/api/folder', filesUrl: '/api/files'}} + {provide: 'fileManagerConfiguration', useValue: fileManagerConfiguration} ], bootstrap: [AppComponent] }) diff --git a/demo/src/appWithBackend/appWithBackend.component.html b/demo/src/appWithBackend/appWithBackend.component.html new file mode 100644 index 0000000..3150356 --- /dev/null +++ b/demo/src/appWithBackend/appWithBackend.component.html @@ -0,0 +1,10 @@ +
+

Filemanager with backend

+
+ +
+ Loading... +
diff --git a/demo/src/appWithBackend/appWithBackend.component.less b/demo/src/appWithBackend/appWithBackend.component.less new file mode 100644 index 0000000..9bb0a40 --- /dev/null +++ b/demo/src/appWithBackend/appWithBackend.component.less @@ -0,0 +1,3 @@ +.configuration-bar { + margin: 10px 0; +} \ No newline at end of file diff --git a/demo/src/appWithBackend/appWithBackend.component.ts b/demo/src/appWithBackend/appWithBackend.component.ts new file mode 100644 index 0000000..0e801be --- /dev/null +++ b/demo/src/appWithBackend/appWithBackend.component.ts @@ -0,0 +1,21 @@ +import {Component} from '@angular/core'; +import {FileManagerConfiguration, FileManagerDispatcherService} from '../../../main'; + +@Component({ + selector: 'app-root', + templateUrl: './appWithBackend.component.html', + styleUrls: ['./appWithBackend.component.less'] +}) +export class AppWithBackendComponent { + public constructor(public fileManagerConfiguration: FileManagerConfiguration, + private fileManagerDispatcher: FileManagerDispatcherService) { + } + + public toggleMultiSelection() { + this.fileManagerConfiguration.isMultiSelection = !this.fileManagerConfiguration.isMultiSelection; + + if (!this.fileManagerConfiguration.isMultiSelection) { + this.fileManagerDispatcher.unSelectAllFiles(); + } + } +} diff --git a/demo/src/appWithBackend/appWithBackend.module.ts b/demo/src/appWithBackend/appWithBackend.module.ts new file mode 100644 index 0000000..8f652cb --- /dev/null +++ b/demo/src/appWithBackend/appWithBackend.module.ts @@ -0,0 +1,37 @@ +import {BrowserModule} from '@angular/platform-browser'; +import {NgModule} from '@angular/core'; +import {FormsModule} from '@angular/forms'; +import {HttpModule} from '@angular/http'; + +import {AppWithBackendComponent} from './appWithBackend.component'; +import {FileManagerModule, FileManagerApiService, IFileManagerConfiguration, FileManagerBackendApiService} from '../../../main'; + +const fileManagerConfiguration: IFileManagerConfiguration = { + urls: { + foldersUrl: '/api/folder', + filesUrl: '/api/files', + folderMoveUrl: '/api/folder/move' + }, + isMultiSelection: true, + mimeTypes: ['image/jpg', 'image/jpeg', 'image/png'], + maxFileSize: 50 * 1024 +}; + +@NgModule({ + declarations: [ + AppWithBackendComponent + ], + imports: [ + BrowserModule, + FileManagerModule, + FormsModule, + HttpModule + ], + providers: [ + {provide: 'fileManagerConfiguration', useValue: fileManagerConfiguration}, + {provide: FileManagerApiService, useClass: FileManagerBackendApiService} + ], + bootstrap: [AppWithBackendComponent] +}) +export class AppWithBackendModule { +} diff --git a/demo/src/mainWithBackend.ts b/demo/src/mainWithBackend.ts new file mode 100644 index 0000000..7acc0a6 --- /dev/null +++ b/demo/src/mainWithBackend.ts @@ -0,0 +1,11 @@ +import { enableProdMode } from '@angular/core'; +import { platformBrowserDynamic } from '@angular/platform-browser-dynamic'; + +import { environment } from './environments/environment'; +import {AppWithBackendModule} from './appWithBackend/appWithBackend.module'; + +if (environment.production) { + enableProdMode(); +} + +platformBrowserDynamic().bootstrapModule(AppWithBackendModule); diff --git a/demo/src/tsconfig.json b/demo/src/tsconfig.json index 1731556..c8d3c9a 100644 --- a/demo/src/tsconfig.json +++ b/demo/src/tsconfig.json @@ -1,18 +1,22 @@ { + "compileOnSave": false, "compilerOptions": { "baseUrl": "", "declaration": false, "emitDecoratorMetadata": true, "experimentalDecorators": true, - "lib": ["es6", "dom"], + "lib": [ + "es2016", + "dom" + ], "mapRoot": "./", - "module": "es6", + "module": "es2015", "moduleResolution": "node", "outDir": "../dist/out-tsc", "sourceMap": true, "target": "es5", "typeRoots": [ - "./example" + "../node_modules/@types" ] } } diff --git a/index.d.ts b/index.d.ts new file mode 100644 index 0000000..aad1ca8 --- /dev/null +++ b/index.d.ts @@ -0,0 +1 @@ +export * from './main'; diff --git a/karma.conf.js b/karma.conf.js index 1f2613a..798f0c2 100644 --- a/karma.conf.js +++ b/karma.conf.js @@ -4,40 +4,30 @@ module.exports = function (config) { config.set({ basePath: '', - frameworks: ['jasmine', 'angular-cli'], + frameworks: ['jasmine', '@angular/cli'], plugins: [ require('karma-jasmine'), require('karma-chrome-launcher'), - require('karma-remap-istanbul'), - require('angular-cli/plugins/karma') + require('karma-jasmine-html-reporter'), + require('karma-coverage-istanbul-reporter'), + require('@angular/cli/plugins/karma') ], - files: [ - { pattern: './src/test.ts', watched: false } - ], - preprocessors: { - './src/test.ts': ['angular-cli'] - }, - mime: { - 'text/x-typescript': ['ts','tsx'] + client: { + clearContext: false // leave Jasmine Spec Runner output visible in browser }, - remapIstanbulReporter: { - reports: { - html: 'coverage', - lcovonly: './coverage/coverage.lcov' - } + coverageIstanbulReporter: { + reports: ['html', 'lcovonly'], + fixWebpackSourcePaths: true }, angularCli: { - config: './angular-cli.json', environment: 'dev' }, - reporters: config.angularCli && config.angularCli.codeCoverage - ? ['progress', 'karma-remap-istanbul'] - : ['progress'], + reporters: ['progress', 'kjhtml'], port: 9876, colors: true, logLevel: config.LOG_INFO, autoWatch: true, browsers: ['Chrome'], - singleRun: false + singleRun: true }); }; diff --git a/main.ts b/main.ts index a3ebb64..9645b70 100644 --- a/main.ts +++ b/main.ts @@ -1,13 +1,33 @@ -import {FileManagerModule} from './src/filemanager.module'; +import {FileManagerActionsService} from './src/store/fileManagerActions.service'; +import {FileManagerApiService} from './src/store/fileManagerApi.service'; +import {FileManagerBackendApiService} from './src/store/fileManagerBackendApi.service'; import {FileManagerComponent} from './src/filemanager.component'; import {FileManagerConfiguration} from './src/configuration/fileManagerConfiguration.service'; +import {FileManagerDispatcherService} from './src/store/fileManagerDispatcher.service'; +import {FileManagerModule} from './src/filemanager.module'; +import {FilemanagerNotifcations} from './src/services/FilemanagerNotifcations'; import {FileManagerUploader} from './src/filesList/fileManagerUploader.service'; import {ISelectFile} from './src/filesList/interface/ISelectFile'; +import {IOuterFile} from './src/filesList/interface/IOuterFile'; +import {IFileDataProperties} from './src/services/imageDataConverter.service'; +import {IFileManagerApi} from './src/store/IFileManagerApi'; +import {IFileManagerConfiguration} from './src/configuration/IFileManagerConfiguration'; +import {ICropBounds} from './src/crop/ICropBounds'; export { + FileManagerActionsService, FileManagerModule, + FileManagerBackendApiService, FileManagerComponent, FileManagerConfiguration, + FileManagerDispatcherService, FileManagerUploader, + FilemanagerNotifcations, + FileManagerApiService, + ICropBounds, + IFileDataProperties, + IFileManagerApi, + IFileManagerConfiguration, + IOuterFile, ISelectFile -} +}; diff --git a/package.json b/package.json index f1366cf..9161b73 100644 --- a/package.json +++ b/package.json @@ -1,11 +1,15 @@ { "name": "@rign/angular2-filemanager", - "version": "0.5.4", + "version": "1.0.0", "license": "MIT", "angular-cli": {}, "scripts": { "backend": "node demo/backend/index.js", - "start": "ng serve --proxy-config demo/proxy.config.example.json" + "build": "ng build --app withoutBackend", + "start": "ng serve --app withoutBackend", + "startWithBackend": "ng serve --proxy-config demo/proxy.config.example.json --app withBackend ", + "test": "./node_modules/.bin/karma --config karma.conf.js start", + "gh-pages": "ng build --app withoutBackend --env=prod --base-href \"https://qjon.github.io/angular2-filemanager/\" && ./node_modules/.bin/angular-cli-ghpages -b gh-pages --name=\"RafaƂ Ignaszewski\" --email=\"rafal@ignaszewski.pl\" --no-silent" }, "repository": { "type": "git", @@ -16,52 +20,64 @@ "file", "manager" ], - "main": "main.ts", + "main": "main", "author": "Rafal Ignaszewski http://ignaszewski.pl", "dependencies": { - "@angular/common": "^2.4.0", - "@angular/compiler": "^2.4.0", - "@angular/core": "^2.4.0", - "@angular/forms": "^2.4.0", - "@angular/http": "^2.4.0", - "@angular/platform-browser": "^2.4.0", - "@angular/platform-browser-dynamic": "^2.4.0", - "@angular/router": "^3.4.0", - "@rign/angular2-tree": "^0.8.1", - "angular2-bootstrap-confirm": "^1.0.4", - "angular2-notifications": "^0.4.53", - "bootstrap": "^3.3.7", - "core-js": "^2.4.1", - "font-awesome": "^4.7.0", - "ng2-file-upload": "^1.2.0", - "ng2-img-cropper": "^0.7.7", - "rxjs": "^5.1.0", - "ts-helpers": "^1.1.1", - "zone.js": "^0.7.6" + "@angular/animations": "^4.0.0", + "@angular/common": "^4.0.0", + "@angular/compiler": "^4.0.0", + "@angular/core": "^4.0.0", + "@angular/forms": "^4.0.0", + "@angular/http": "^4.0.0", + "@angular/platform-browser": "^4.0.0", + "@angular/platform-browser-dynamic": "^4.0.0", + "@angular/router": "^4.0.0", + "@ngrx/core": "1.2.0", + "@ngrx/effects": "2.0.3", + "@ngrx/store": "2.2.2", + "@rign/angular2-tree": "2.0.1", + "@types/jasmine": "2.5.54", + "angular-confirmation-popover": "3.2.0", + "angular2-contextmenu": "0.7.7", + "angular2-notifications": "0.7.7", + "angular2-uuid": "1.1.1", + "bootstrap": "3.3.7", + "core-js": "2.4.1", + "font-awesome": "4.6.3", + "ng2-dnd": "4.2.0", + "ng2-file-upload": "1.2.0", + "ng2-img-cropper": "0.9.0", + "rxjs": "5.1.0", + "ts-helpers": "1.1.1", + "zone.js": "0.8.4" }, "devDependencies": { - "@angular/cli": "1.0.0-beta.32.3", - "@angular/compiler-cli": "^2.4.0", - "@types/jasmine": "2.5.38", - "@types/node": "^6.0.42", - "codelyzer": "~2.0.0-beta.4", + "@angular/cli": "1.2.0", + "@angular/compiler-cli": "^4.0.0", + "@angular/language-service": "^4.0.0", + "@ngrx/store-devtools": "^3.2.4", + "@types/jasmine": "~2.5.53", + "@types/jasminewd2": "~2.0.2", + "@types/node": "~6.0.60", + "angular-cli-ghpages": "^0.5.1", + "codelyzer": "~3.0.1", "easyimage": "^2.1.0", "file-loader": "^0.8.5", "formidable": "^1.0.17", "image-size": "^0.5.1", - "jasmine-core": "~2.5.2", - "jasmine-spec-reporter": "~3.2.0", - "karma": "~1.4.1", - "karma-chrome-launcher": "~2.0.0", + "jasmine-core": "~2.6.2", + "jasmine-spec-reporter": "~4.1.0", + "karma": "~1.7.0", + "karma-chrome-launcher": "~2.1.1", "karma-cli": "~1.0.1", - "karma-coverage-istanbul-reporter": "^0.2.0", + "karma-coverage-istanbul-reporter": "^1.2.1", "karma-jasmine": "~1.1.0", "karma-jasmine-html-reporter": "^0.2.2", "karma-remap-istanbul": "^0.2.1", "mime-types": "^2.1.14", - "protractor": "~5.1.0", - "ts-node": "~2.0.0", - "tslint": "~4.4.2", - "typescript": "~2.0.0" + "protractor": "~5.1.2", + "ts-node": "~3.0.4", + "tslint": "~5.3.2", + "typescript": "~2.3.3" } } diff --git a/src/_unitTestMocks/fileDataMock.ts b/src/_unitTestMocks/fileDataMock.ts new file mode 100644 index 0000000..ddf17ee --- /dev/null +++ b/src/_unitTestMocks/fileDataMock.ts @@ -0,0 +1,13 @@ +import {IOuterFile} from '../filesList/interface/IOuterFile'; + +export const fileData: IOuterFile = { + id: '39097132-ed56-3c72-bfd7-898e1cc00299', + folderId: 'dd9b20d8-260b-54c1-7eca-c22eae257edc', + name: 'avatar.jpg', + type: 'image/jpeg', + url: 'some base 64png', + thumbnailUrl: 'some base 64png', + size: 6076, + width: 125, + height: 125 +}; diff --git a/src/_unitTestMocks/folderDataMock.ts b/src/_unitTestMocks/folderDataMock.ts new file mode 100644 index 0000000..8e6be09 --- /dev/null +++ b/src/_unitTestMocks/folderDataMock.ts @@ -0,0 +1,10 @@ +import {IOuterNode} from '@rign/angular2-tree'; + +export const rootNode: IOuterNode = { + 'id': 'dd9b20d8-260b-54c1-7eca-c22eae257edc', + 'treeId': 'tree', + 'name': 'Nowy folder', + 'parentId': '', + 'children': [], + 'parents': [] +}; diff --git a/src/configuration/IFileManagerConfiguration.ts b/src/configuration/IFileManagerConfiguration.ts new file mode 100644 index 0000000..16c2860 --- /dev/null +++ b/src/configuration/IFileManagerConfiguration.ts @@ -0,0 +1,8 @@ +import {IUrlConfiguration} from './IUrlConfiguration'; + +export interface IFileManagerConfiguration { + urls: IUrlConfiguration; + isMultiSelection?: boolean; + maxFileSize?: number; + mimeTypes?: string[]; +} diff --git a/src/configuration/IUrlConfiguration.ts b/src/configuration/IUrlConfiguration.ts index bd2f3e9..2f8dac3 100644 --- a/src/configuration/IUrlConfiguration.ts +++ b/src/configuration/IUrlConfiguration.ts @@ -1,4 +1,5 @@ export class IUrlConfiguration { - filesUrl: string; + filesUrl: string | null; foldersUrl: string; + folderMoveUrl: string; } diff --git a/src/configuration/fileManagerConfiguration.service.ts b/src/configuration/fileManagerConfiguration.service.ts index 619877a..79f0397 100644 --- a/src/configuration/fileManagerConfiguration.service.ts +++ b/src/configuration/fileManagerConfiguration.service.ts @@ -1,14 +1,26 @@ -import {IContextMenu} from "@rign/angular2-tree/main"; -import {Injectable, Inject} from "@angular/core"; -import {IFileTypeFilter} from "../toolbar/interface/IFileTypeFilter"; -import {ICropSize} from "../crop/ICropSize"; -import {IUrlConfiguration} from "./IUrlConfiguration"; +import {IContextMenu} from '@rign/angular2-tree'; +import {Injectable, Inject} from '@angular/core'; +import {IFileTypeFilter} from '../toolbar/interface/IFileTypeFilter'; +import {ICropSize} from '../crop/ICropSize'; +import {IFileManagerConfiguration} from './IFileManagerConfiguration'; @Injectable() export class FileManagerConfiguration { - public contextMenuItems: IContextMenu[] = []; - public isMultiSelection: boolean = false; + public allowedCropSize: ICropSize[] = [ + { + name: 'Landscape', + width: 300, + height: 100 + }, + { + name: 'Portrait', + width: 200, + height: 300 + } + ]; + + public contextMenuItems: IContextMenu[] = []; public fileTypesFilter: IFileTypeFilter[] = [ { @@ -44,24 +56,21 @@ export class FileManagerConfiguration { } ]; - public fileUrl: string = '/api/files'; + public folderUrls: {foldersUrl: string, folderMoveUrl: string}; + public fileUrl = '/api/files'; - public allowedCropSize: ICropSize[] = [ - { - name: 'Landscape', - width: 300, - height: 100 - }, - { - name: 'Portrait', - width: 200, - height: 300 - } - ]; + public isMultiSelection: boolean; + public maxFileSize: number; - constructor(@Inject('fileManagerUrls') urls: IUrlConfiguration) { + public mimeTypes: string[] | null; - this.fileUrl = urls.filesUrl; + constructor(@Inject('fileManagerConfiguration') configuration: IFileManagerConfiguration) { + const {foldersUrl, folderMoveUrl} = configuration.urls; + this.folderUrls = {foldersUrl, folderMoveUrl}; + this.fileUrl = configuration.urls.filesUrl; + this.isMultiSelection = configuration.isMultiSelection || false; + this.maxFileSize = configuration.maxFileSize || 0; + this.mimeTypes = configuration.mimeTypes || null; } } diff --git a/src/configuration/tree.service.ts b/src/configuration/tree.service.ts index 7c83498..5b3fc0a 100644 --- a/src/configuration/tree.service.ts +++ b/src/configuration/tree.service.ts @@ -1,18 +1,19 @@ -import {Injectable, Inject} from "@angular/core"; -import {NodeService} from "@rign/angular2-tree/main"; -import {Http} from "@angular/http"; -import {IUrlConfiguration} from "./IUrlConfiguration"; +import {Injectable, Inject} from '@angular/core'; +import {NodeService} from '@rign/angular2-tree'; +import {Http} from '@angular/http'; +import {IFileManagerConfiguration} from './IFileManagerConfiguration'; @Injectable() export class TreeService extends NodeService { - public constructor(http: Http, @Inject('fileManagerUrls') urls: IUrlConfiguration) { + public constructor(protected http: Http, @Inject('fileManagerConfiguration') configuration: IFileManagerConfiguration) { super(http); this.apiConfig = { - addUrl: urls.foldersUrl, - getUrl: urls.foldersUrl, - updateUrl: urls.foldersUrl, - removeUrl: urls.foldersUrl, + addUrl: configuration.urls.foldersUrl, + getUrl: configuration.urls.foldersUrl, + updateUrl: configuration.urls.foldersUrl, + removeUrl: configuration.urls.foldersUrl, + moveUrl: configuration.urls.folderMoveUrl }; } } diff --git a/src/crop/crop.component.ts b/src/crop/crop.component.ts index db44309..7d9f07c 100644 --- a/src/crop/crop.component.ts +++ b/src/crop/crop.component.ts @@ -1,6 +1,6 @@ import { Component, Input, ViewChild, ViewContainerRef, ComponentFactoryResolver, Output, - EventEmitter + EventEmitter, OnDestroy } from "@angular/core"; import {FileModel} from "../filesList/file.model"; import {CropperSettings} from "ng2-img-cropper/src/cropperSettings"; @@ -9,28 +9,31 @@ import {ICropSize} from "./ICropSize"; import {FileManagerConfiguration} from "../configuration/fileManagerConfiguration.service"; import {Bounds} from "ng2-img-cropper/src/model/bounds"; import {ICropBounds} from "./ICropBounds"; +import {FileManagerDispatcherService} from "../store/fileManagerDispatcher.service"; @Component({ selector: 'crop-image', styleUrls: ['./crop.less'], template: ` -
-
-
-
-
-
- -
-
- -
-
+
+
+
+
+
+
+
- ` +
+ +
+
+
+ ` }) export class CropComponent { @@ -49,9 +52,9 @@ export class CropComponent { public cropSizeList: ICropSize[]; public currentCropSize: ICropSize; - private scale: number = 1; - - constructor(private resolver: ComponentFactoryResolver, private configuration: FileManagerConfiguration) { + constructor(private resolver: ComponentFactoryResolver, + private configuration: FileManagerConfiguration, + private fileManagerDispatcher: FileManagerDispatcherService) { this.cropSizeList = configuration.allowedCropSize; } @@ -78,7 +81,7 @@ export class CropComponent { ngAfterContentInit() { this.updateCropSize(this.cropSizeList[0]); - } + }; public cropImage() { let bounds: ICropBounds = { @@ -88,7 +91,7 @@ export class CropComponent { height: this.bounds.height }; - this.onCrop.emit({file: this.file, bounds: bounds}); + this.fileManagerDispatcher.cropFile(this.file, bounds); } @@ -98,8 +101,6 @@ export class CropComponent { let width = scale * this.file.getWidth(); let height = scale * this.file.getHeight(); - console.log(this.file, scale); - cropperSettings.noFileInput = true; cropperSettings.width = this.currentCropSize.width; cropperSettings.height = this.currentCropSize.height; diff --git a/src/decorators/logFunction.decorator.ts b/src/decorators/logFunction.decorator.ts index 3aef8f8..89fe99f 100644 --- a/src/decorators/logFunction.decorator.ts +++ b/src/decorators/logFunction.decorator.ts @@ -1,4 +1,4 @@ -import {environment} from "../../demo/src/environments/environment"; +import {environment} from '../../demo/src/environments/environment'; export function log(target: Object, propertyKey: string, descriptor: TypedPropertyDescriptor) { let originalMethod = descriptor.value; diff --git a/src/dropdown/dropdown.component.ts b/src/dropdown/dropdown.component.ts index ad6db11..b89ee6e 100644 --- a/src/dropdown/dropdown.component.ts +++ b/src/dropdown/dropdown.component.ts @@ -1,37 +1,19 @@ -import {Component, Input, Output, EventEmitter} from "@angular/core"; -import {IButton} from "./IButton"; +import {Component, Input, Output, EventEmitter} from '@angular/core'; +import {IButton} from './IButton'; + @Component({ - selector: 'dropdown', + selector: 'ri-dropdown', styleUrls: ['./dropdown.less'], - template: ` - ` + templateUrl: './dropdown.html' }) -export class Dropdown { +export class DropdownComponent { @Input() mainButton: IButton; @Input() buttons: IButton[]; @Output() onClick = new EventEmitter(); - public isOpen: boolean = false; + public isOpen = false; public hide() { this.isOpen = false; @@ -45,6 +27,4 @@ export class Dropdown { public toggleOpen() { this.isOpen = !this.isOpen; } - - } diff --git a/src/dropdown/dropdown.html b/src/dropdown/dropdown.html new file mode 100644 index 0000000..ac8cd66 --- /dev/null +++ b/src/dropdown/dropdown.html @@ -0,0 +1,19 @@ + diff --git a/src/filemanager.component.ts b/src/filemanager.component.ts index cb65818..989dc7d 100644 --- a/src/filemanager.component.ts +++ b/src/filemanager.component.ts @@ -1,74 +1,97 @@ -import {Component, OnInit, ViewChild, HostListener, Input, OnChanges, EventEmitter, Output} from '@angular/core'; -import {TreeComponent, NodeService, IContextMenu, IOuterNode, ITreeItemEvent} from '@rign/angular2-tree/main'; -import {FilesService} from "./filesList/files.service"; -import {IOuterFile} from "./filesList/interface/IOuterFile"; -import {FileModel} from "./filesList/file.model"; -import {log} from "./decorators/logFunction.decorator"; -import {IUploadItemEvent} from "./toolbar/interface/IUploadItemEvent"; -import {NotificationsService} from "angular2-notifications"; -import {IFileEvent} from "./filesList/interface/IFileEvent"; -import {Button} from "./toolbar/models/button.model"; -import {FilesList} from "./filesList/filesList.component"; -import {IToolbarEvent} from "./toolbar/interface/IToolbarEvent"; -import {IFileModel} from "./filesList/interface/IFileModel"; -import {FileManagerConfiguration} from "./configuration/fileManagerConfiguration.service"; -import {IFileTypeFilter} from "./toolbar/interface/IFileTypeFilter"; -import {ICropBounds} from "./crop/ICropBounds"; -import {TreeService} from "./configuration/tree.service"; +import { + Component, OnInit, ViewChild, HostListener, EventEmitter, Output +} from '@angular/core'; +import { + TreeComponent, + NodeService, + IContextMenu, + IOuterNode, + ITreeData, + ITreeState, + IConfiguration, + TreeModel, + TreeActionsService, + NodeDispatcherService +} from '@rign/angular2-tree'; +import {IOuterFile} from './filesList/interface/IOuterFile'; +import {FileModel} from './filesList/file.model'; +import {log} from './decorators/logFunction.decorator'; +import {NotificationsService} from 'angular2-notifications'; +import {IFileEvent} from './filesList/interface/IFileEvent'; +import {Button} from './toolbar/models/button.model'; +import {FilesListComponent} from './filesList/filesList.component'; +import {IToolbarEvent} from './toolbar/interface/IToolbarEvent'; +import {IFileModel} from './filesList/interface/IFileModel'; +import {FileManagerConfiguration} from './configuration/fileManagerConfiguration.service'; +import {IFileTypeFilter} from './toolbar/interface/IFileTypeFilter'; +import {Observable} from 'rxjs/Observable'; +import {Store} from '@ngrx/store'; +import {IFileManagerState} from './store/fileManagerReducer'; +import {FileTypeFilterService} from './services/fileTypeFilter.service'; +import {SearchFilterService} from './services/searchFilter.service'; +import {FileManagerDispatcherService} from './store/fileManagerDispatcher.service'; +import {FileManagerEffectsService} from './store/fileManagerEffects.service'; +import {FileManagerApiService} from './store/fileManagerApi.service'; +import {FilemanagerNotifcations, INotification} from './services/FilemanagerNotifcations'; @Component({ - selector: 'filemanager', - providers: [NodeService, FilesService, NotificationsService], + selector: 'ri-filemanager', + providers: [NodeService, NotificationsService], styleUrls: ['./main.less'], templateUrl: './filemanager.html' }) -export class FileManagerComponent implements OnInit, OnChanges { - @Input() multiSelection: boolean = false; +export class FileManagerComponent implements OnInit { @Output() onSingleFileSelect = new EventEmitter(); @ViewChild(TreeComponent) public treeComponent: TreeComponent; - @ViewChild(FilesList) - public filesList: FilesList; + @ViewChild(FilesListComponent) + public filesList: FilesListComponent; /** - * Current folder all files + * List of files for current selected directory * @typeObserv {Array} */ - private currentFolderFilesList: IFileModel[] = []; + private files$: Observable; - private searchFieldValue: string = ''; - private fileType: IFileTypeFilter; + public filteredFiles$: Observable; + + public folders: Observable; + + + public treeConfiguration: IConfiguration = { + showAddButton: false, + disableMoveNodes: false, + treeId: 'tree', + dragZone: 'tree', + dropZone: ['tree'] + }; + + public treeModel: TreeModel; + + /** UNSED **/ + public contextMenu: IContextMenu[] = []; - /** - * Folders tree structure - * @typeObserv {Array} - */ - public folders: IOuterNode[] = []; /** - * List of filtered files for current selected directory + * Current folder all files * @typeObserv {Array} */ - public files: IFileModel[] = []; + private currentFolderFilesList: IFileModel[] = []; - /** - * Current selected folder id - */ - public currentFolderId: string; public currentSelectedFile: IFileModel; - public isPreviewMode: boolean = false; - public isCropMode: boolean = false; + public isPreviewMode = false; + public isCropMode = false; public notificationOptions = { - position: ["bottom", "right"], + position: ['bottom', 'right'], timeOut: 3000, lastOnBottom: false, preventDuplicates: true, - rtl: true, + rtl: false, showProgressBar: true, pauseOnHover: true }; @@ -79,145 +102,115 @@ export class FileManagerComponent implements OnInit, OnChanges { */ public menu: IContextMenu[]; - get numberOfSelectedItems() { - if (this.files) { - return this.files - .filter((file) => file.selected) - .length; - } else { - return 0; - } - } - - constructor(private treeService: TreeService, - private filesService: FilesService, - private notifications: NotificationsService, - private configuration: FileManagerConfiguration) { + public constructor(private store: Store, + private treeActions: TreeActionsService, + private nodeDispatcherService: NodeDispatcherService, + private treeService: FileManagerApiService, + private notifications: NotificationsService, + private configuration: FileManagerConfiguration, + private fileManagerDispatcher: FileManagerDispatcherService, + private fileTypeFilter: FileTypeFilterService, + private searchFilterService: SearchFilterService, + private fileManagerEffects: FileManagerEffectsService, + private filemanagerNotifcations: FilemanagerNotifcations) { this.menu = configuration.contextMenuItems; + + this.filemanagerNotifcations.getNotificationStream() + .subscribe((notification: INotification) => { + const {type, title, message} = notification; + + this.notifications[type](title, message); + }); } ngOnInit() { - this.treeService.load() - .subscribe((items: IOuterNode[]) => { - this.folders = items; - }); + /*** START - init TREE ***/ + const treeId = this.treeConfiguration.treeId; + this.nodeDispatcherService.register(treeId, this.treeService); - this.loadFiles(''); - } + this.store.dispatch(this.treeActions.registerTree(treeId)); - ngOnChanges() { - this.configuration.isMultiSelection = this.multiSelection; - } + this.folders = this.store.select('trees') + .map((data: ITreeState) => { + return data[treeId]; + }) + .filter((data: ITreeData) => !!data) + ; - /*********************************************************************** - * FOLDER EVENTS - **********************************************************************/ + this.treeModel = new TreeModel(this.folders, this.treeConfiguration); + /*** END - init TREE ***/ - @log - public onAddFolder($event: IToolbarEvent) { - this.treeComponent.addNode($event.value); - } - @log - public onAdd(event: ITreeItemEvent) { - let node = event.node; - let parentNode = node.parentNode; - let parentNodeId = parentNode ? parentNode.id : null; - - this.treeService.save(event.node.data, parentNodeId) - .subscribe((folder: IOuterNode) => { - node.refresh(folder); + /*** START - init files ***/ + this.files$ = this.store.select('files') + .map((data: IFileManagerState): FileModel[] => { + return data.map((file: IOuterFile) => new FileModel(file)); }); - } - @log - public onRemove(event: ITreeItemEvent) { - let node = event.node; - this.treeService.remove(node.id) - .subscribe(() => { - if (node.id === this.currentFolderId) { - this.currentFolderId = null; + this.filteredFiles$ = Observable.combineLatest( + this.files$, + this.fileTypeFilter.filter$, + this.searchFilterService.filter$ + ) + .map((data: [FileModel[], IFileTypeFilter, string]): FileModel[] => { + let files = data[0]; + const fileTypeFilter = data[1]; + const search = data[2].toLocaleLowerCase(); + + if (search !== '') { + files = files.filter((file: FileModel) => { + return file.name.toLocaleLowerCase().indexOf(search) > -1; + }); + } + + + if (fileTypeFilter && fileTypeFilter.mimes.length > 0) { + files = files.filter((file: FileModel) => { + return fileTypeFilter.mimes.indexOf(file.getMime()) > -1; + }); } - node.remove(); - this.loadFiles(this.currentFolderId); + return files; }); - } - @log - public onChange(event: ITreeItemEvent) { - let node = event.node; - - this.treeService.update(node.toJSON()) - .subscribe((folder: IOuterNode) => { - node.refresh(folder); - node.collapse(); - node.expand(); + + this.treeModel.currentSelectedNode$ + .subscribe((node: IOuterNode | null) => { + this.loadFiles(node ? node.id : ''); + }); + + /*** END - init files ***/ + + this.fileManagerEffects.cropFileSuccess$ + .subscribe(() => { + this.closeModal(); }); } - @log - public onToggle(event: ITreeItemEvent) { - if (event.status) { - this.treeService.load(event.node.id) - .subscribe((folders: IOuterNode[]) => { - for (let folder of folders) { - event.node.addChild(folder); - } - }); - } else { - event.node.resetChildren(); - } + get currentSelectedFolderId(): string | null { + const value = this.treeModel.currentSelectedNode$.getValue(); + + return value ? value.id : null; } @log - public onSelect(event: ITreeItemEvent) { - if (event.status) { - this.loadFiles(event.node.id); - this.currentFolderId = event.node.id; - } else { - this.loadFiles(''); - this.currentFolderId = null; - } + public onAddFolder() { + this.treeComponent.onAdd(); } - /*********************************************************************** * FILE EVENTS **********************************************************************/ - - /** * Run when all files are uploaded * @param folderId */ public onUpload(folderId: string) { - this.loadFiles(folderId); - this.notifications.success('File upload', 'Upload complete'); } - /** - * Run when single file is uploaded - * @param eventData - */ - public onUploadItem(eventData: IUploadItemEvent) { - if (eventData.status === 409) { - this.notifications.alert('File upload', `${eventData.name} exists on the server in this directory`); - } - } - - @log - public onRenameFile(fileEventData: IFileEvent) { - } - - @log - public onDeleteFile(fileEventData: IFileEvent) { - this.removeSingleFile(fileEventData.file); - } - @log public onPreviewFile(fileEventData: IFileEvent) { this.isPreviewMode = true; @@ -230,25 +223,6 @@ export class FileManagerComponent implements OnInit, OnChanges { this.currentSelectedFile = fileEventData.file; } - @log - public onCropFile(event: any) { - let file: IFileModel = event.file; - let bounds: ICropBounds = event.bounds; - - this.filesService.crop(file, bounds) - .subscribe( - (data: any) => { - file.fromJSON(data); - this.closeModal(); - this.reloadFiles(); - this.notifications.success('Crop Image', 'Image has been cropped'); - }, - () => { - this.notifications.error('Crop Image', 'Image has not been cropped'); - } - ); - } - @log public onSelectFile(event: FileModel) { this.onSingleFileSelect.next(event.getSelectData()); @@ -262,20 +236,20 @@ export class FileManagerComponent implements OnInit, OnChanges { public onMenuButtonClick(event: IToolbarEvent) { switch (event.name) { case Button.DELETE_SELECTION: - this.files.forEach((file: IFileModel) => { - if (file.selected) { - this.removeSingleFile(file); - } - }); + // this.files.forEach((file: IFileModel) => { + // if (file.selected) { + // this.removeSingleFile(file); + // } + // }); break; case Button.SELECT_ALL: - this.filesList.allFilesSelection(true); + this.fileManagerDispatcher.selectAllFiles(); break; case Button.UNSELECT_ALL: - this.filesList.allFilesSelection(false); + this.fileManagerDispatcher.unSelectAllFiles(); break; case Button.INVERSE_SELECTION: - this.filesList.selectInversion(); + this.fileManagerDispatcher.inverseSelection(); break; case Button.REFRESH_FILES_LIST: this.reloadFiles(); @@ -283,18 +257,6 @@ export class FileManagerComponent implements OnInit, OnChanges { } } - @log - public onSearchChange(filterValue: string = '') { - this.searchFieldValue = filterValue; - this.filterFilesList(this.searchFieldValue, this.fileType); - } - - @log - public onFilterTypeChange(type: IFileTypeFilter) { - this.fileType = type; - this.filterFilesList(this.searchFieldValue, this.fileType); - } - /*********************************************************************** * OTHER FUNCTIONS **********************************************************************/ @@ -314,61 +276,13 @@ export class FileManagerComponent implements OnInit, OnChanges { private loadFiles(folderId: string) { - this.filesService.load(folderId) - .subscribe((files: IOuterFile[]) => { - this.files = []; - this.currentFolderFilesList = []; - - files.forEach((file: IOuterFile) => { - let fileModel = new FileModel(file); - this.currentFolderFilesList.push(fileModel); - }); - - this.filterFilesList(this.searchFieldValue, this.fileType); - }); + this.fileManagerDispatcher.loadFiles(folderId || ''); } private reloadFiles() { - this.loadFiles(this.currentFolderId || ''); - } - - private removeSingleFile(file: IFileModel) { - this.filesService.remove(file) - .subscribe( - () => { - this.reloadFiles(); - this.notifications.success('File delete', `${file.name} has been deleted`); - }, - (error: any) => { - this.notifications.error('File delete', `${file.name} has not been deleted`); - } - ) - } - - /** - * Filter current folder files list - * @param search - * @param type - */ - private filterFilesList(search: string, type: IFileTypeFilter = null) { - let files: IFileModel[] = this.currentFolderFilesList.copyWithin(0, 0); - - search = search.toLocaleLowerCase() || ''; - - if (search) { - search.toLocaleLowerCase(); - files = files.filter((fileModel) => { - return fileModel.name.toLocaleLowerCase().indexOf(search) > -1; - }); - } - - if (type && type.mimes.length > 0) { - - files = files.filter((fileModel) => { - return type.mimes.indexOf(fileModel.getMime()) > -1; - }); - } + const node = this.treeModel.currentSelectedNode$.getValue(); + const id = node ? node.id : ''; - this.files = files; + this.loadFiles(id); } } diff --git a/src/filemanager.html b/src/filemanager.html index 767308a..cffc14d 100644 --- a/src/filemanager.html +++ b/src/filemanager.html @@ -2,40 +2,27 @@
- - + + >
diff --git a/src/filemanager.module.ts b/src/filemanager.module.ts index f43812b..c29e835 100644 --- a/src/filemanager.module.ts +++ b/src/filemanager.module.ts @@ -1,51 +1,82 @@ -import {NgModule, CUSTOM_ELEMENTS_SCHEMA} from "@angular/core"; -import {BrowserModule} from "@angular/platform-browser"; -import {HttpModule} from "@angular/http"; -import {FormsModule, ReactiveFormsModule} from "@angular/forms"; -import {TreeModule} from "@rign/angular2-tree/main"; -import {SimpleNotificationsModule} from "angular2-notifications"; -import {ConfirmModule} from "angular2-bootstrap-confirm"; -import {FileManagerComponent} from "./filemanager.component"; -import {Toolbar} from "./toolbar/toolbar.component"; -import {FilesList} from "./filesList/filesList.component"; -import {ImageCropperComponent} from "ng2-img-cropper"; -import {CropComponent} from "./crop/crop.component"; -import {PreviewComponent} from "./preview/preview.component"; -import {Dropdown} from "./dropdown/dropdown.component"; -import {FileUploadModule} from "ng2-file-upload"; -import {FileManagerConfiguration} from "./configuration/fileManagerConfiguration.service"; -import {FileManagerUploader} from "./filesList/fileManagerUploader.service"; -import {TreeService} from "./configuration/tree.service"; +import {NgModule, CUSTOM_ELEMENTS_SCHEMA, Inject} from '@angular/core'; +import {BrowserModule} from '@angular/platform-browser'; +import {HttpModule} from '@angular/http'; +import {FormsModule, ReactiveFormsModule} from '@angular/forms'; +import {TreeModule, treeReducer} from '@rign/angular2-tree'; +import {NotificationsService, SimpleNotificationsModule} from 'angular2-notifications'; +import {FileManagerComponent} from './filemanager.component'; +import {ToolbarComponent} from './toolbar/toolbar.component'; +import {FilesListComponent} from './filesList/filesList.component'; +import {ImageCropperComponent} from 'ng2-img-cropper'; +import {CropComponent} from './crop/crop.component'; +import {PreviewComponent} from './preview/preview.component'; +import {DropdownComponent} from './dropdown/dropdown.component'; +import {FileUploadModule} from 'ng2-file-upload'; +import {FileManagerConfiguration} from './configuration/fileManagerConfiguration.service'; +import {FileManagerUploader} from './filesList/fileManagerUploader.service'; +import {TreeService} from './configuration/tree.service'; +import {EffectsModule} from '@ngrx/effects'; +import {FileManagerEffectsService} from './store/fileManagerEffects.service'; +import {StoreModule} from '@ngrx/store'; +import {fileManagerReducer} from './store/fileManagerReducer'; +import {FileManagerActionsService} from './store/fileManagerActions.service'; +import { StoreDevtoolsModule } from '@ngrx/store-devtools'; +import {FileTypeFilterService} from './services/fileTypeFilter.service'; +import {SearchFilterService} from './services/searchFilter.service'; +import {FileManagerDispatcherService} from './store/fileManagerDispatcher.service'; +import {FileTypeFilterComponent} from './toolbar/fileTypeFilter/fileTypeFilter.component'; +import {SearchFileComponent} from './toolbar/searchFile/searchFile.component'; +import {FileManagerApiService} from './store/fileManagerApi.service'; +import {ImageDataConverter} from './services/imageDataConverter.service'; +import {BrowserAnimationsModule} from '@angular/platform-browser/animations'; +import {FilemanagerNotifcations} from './services/FilemanagerNotifcations'; +import {ConfirmationPopoverModule} from 'angular-confirmation-popover'; +import {FileManagerBackendApiService} from './store/fileManagerBackendApi.service'; @NgModule({ imports: [ BrowserModule, - ConfirmModule, + BrowserAnimationsModule, + ConfirmationPopoverModule.forRoot(), + EffectsModule.run(FileManagerEffectsService), FormsModule, FileUploadModule, HttpModule, ReactiveFormsModule, SimpleNotificationsModule, + StoreModule.provideStore({files: fileManagerReducer, trees: treeReducer}), + StoreDevtoolsModule.instrumentOnlyWithExtension({}), TreeModule ], declarations: [ FileManagerComponent, - Toolbar, - FilesList, - Dropdown, + FileTypeFilterComponent, + ToolbarComponent, + FilesListComponent, + DropdownComponent, PreviewComponent, CropComponent, - ImageCropperComponent + ImageCropperComponent, + SearchFileComponent ], entryComponents: [ImageCropperComponent], providers: [ + FileManagerActionsService, + FileManagerApiService, + FileManagerBackendApiService, FileManagerConfiguration, + FileManagerDispatcherService, + FileManagerEffectsService, + FilemanagerNotifcations, FileManagerUploader, + FileTypeFilterService, + ImageDataConverter, + NotificationsService, + SearchFilterService, TreeService ], exports: [FileManagerComponent], schemas: [CUSTOM_ELEMENTS_SCHEMA] }) export class FileManagerModule { - } diff --git a/src/filesList/file.model.ts b/src/filesList/file.model.ts index b89ad05..32fcc86 100644 --- a/src/filesList/file.model.ts +++ b/src/filesList/file.model.ts @@ -1,17 +1,17 @@ -import {IOuterFile} from "./interface/IOuterFile"; -import {IFileModel} from "./interface/IFileModel"; -import {ISelectFile} from "./interface/ISelectFile"; +import {IOuterFile} from './interface/IOuterFile'; +import {IFileModel} from './interface/IFileModel'; +import {ISelectFile} from './interface/ISelectFile'; export class FileModel implements IFileModel { - static smallIconsFolder: string = '/icons/128px/'; - static bigIconsFolder: string = '/icons/512px/'; + static smallIconsFolder = '/icons/128px/'; + static bigIconsFolder = '/icons/512px/'; private _orgData: IOuterFile; private _name: string; private _iconsFolder = FileModel.smallIconsFolder; - public selected: boolean = false; + public selected = false; set name(name: string) { this._name = name; @@ -37,6 +37,11 @@ export class FileModel implements IFileModel { this._orgData = data; this.name = data.name; + this.selected = data.selected || false; + } + + public toJSON() { + return this._orgData; } public getId() { @@ -52,7 +57,7 @@ export class FileModel implements IFileModel { } public getMime() { - return this._orgData.mime; + return this._orgData.type; } public getWidth(): number { @@ -71,6 +76,6 @@ export class FileModel implements IFileModel { width: this.getWidth(), height: this.getHeight(), mime: this.getMime() - } + }; } } diff --git a/src/filesList/fileManagerUploader.service.ts b/src/filesList/fileManagerUploader.service.ts index 3e2af88..db48bac 100644 --- a/src/filesList/fileManagerUploader.service.ts +++ b/src/filesList/fileManagerUploader.service.ts @@ -1,14 +1,22 @@ -import {Injectable, Inject} from "@angular/core"; -import {FileUploader} from "ng2-file-upload"; -import {IUrlConfiguration} from "../configuration/IUrlConfiguration"; +import {Injectable, Inject} from '@angular/core'; +import {ExtendedFileUploader} from '../services/extendedFileUplaoder.service'; +import {IFileManagerConfiguration} from '../configuration/IFileManagerConfiguration'; +import {FilemanagerNotifcations} from '../services/FilemanagerNotifcations'; +import {FileUploaderOptions} from 'ng2-file-upload'; @Injectable() export class FileManagerUploader { - public uploader: FileUploader; + public uploader: ExtendedFileUploader; + public constructor(@Inject('fileManagerConfiguration') configuration: IFileManagerConfiguration, + filemanagerNotification: FilemanagerNotifcations) { + const options: FileUploaderOptions = { + allowedMimeType: configuration.mimeTypes, + url: configuration.urls.filesUrl, + maxFileSize: configuration.maxFileSize + }; - public constructor(@Inject('fileManagerUrls') urls: IUrlConfiguration) { - this.uploader = new FileUploader({url: urls.filesUrl}); + this.uploader = new ExtendedFileUploader(options, filemanagerNotification); } public clear() { @@ -25,14 +33,14 @@ export class FileManagerUploader { return options; } - public setAuthorizationToken(token:string) { + public setAuthorizationToken(token: string) { this.uploader.authToken = token; } - public setDirectoryId(directoryId: string|number): FileManagerUploader { + public setDirectoryId(directoryId: string | number): FileManagerUploader { let options = this.getDefaultOptions(); - options['headers'] = [{name: 'folderId', value: directoryId.toString()}]; + options['headers'] = [{name: 'folderId', value: directoryId.toString()}]; this.uploader.setOptions(options); diff --git a/src/filesList/files-list.less b/src/filesList/files-list.less index 1c9375c..0111c0f 100644 --- a/src/filesList/files-list.less +++ b/src/filesList/files-list.less @@ -55,7 +55,7 @@ .file-menu { display: none; position: absolute; - top: 20%; + top: 30%; left: 0; right: 0; text-align: center; @@ -89,12 +89,14 @@ background-color: @backgroundColorSelectedAlpha; } - .file-menu { + .file-menu, .file-selection-input { display: none; } - .file-selection-input { - display: block; + &:hover { + .file-selection-input { + display: block; + } } } } diff --git a/src/filesList/files.html b/src/filesList/files.html index ce97975..d419441 100644 --- a/src/filesList/files.html +++ b/src/filesList/files.html @@ -1,11 +1,13 @@
-
- +
+
+ +
{{file.name}}
- +
diff --git a/src/toolbar/fileTypeFilter/fileTypeFilter.component.ts b/src/toolbar/fileTypeFilter/fileTypeFilter.component.ts new file mode 100644 index 0000000..f9524fe --- /dev/null +++ b/src/toolbar/fileTypeFilter/fileTypeFilter.component.ts @@ -0,0 +1,40 @@ +import {Component, Input, OnInit} from '@angular/core'; +import {IFileTypeFilter} from '../interface/IFileTypeFilter'; +import {FileTypeFilterService} from '../../services/fileTypeFilter.service'; + +@Component({ + selector: 'ri-file-type-filter', + templateUrl: './fileTypeFilter.component.html' +}) + +export class FileTypeFilterComponent implements OnInit { + @Input() typeFilterList: IFileTypeFilter[] = []; + + public selectedType: IFileTypeFilter = null; + + constructor(private fileTypeFilter: FileTypeFilterService) { + this.fileTypeFilter.filter$ + .subscribe((type: IFileTypeFilter | null) => { + this.selectedType = type; + }) + } + + ngOnInit() { + /** init file type filter **/ + this.typeFilterList + .filter((type: IFileTypeFilter) => { + return type.defaultSelected; + }) + .forEach((type: IFileTypeFilter) => { + this.fileTypeFilter.setValue(type); + }); + } + + /** + * Set current filter and fire event + * @param type + */ + public setFilterType(type: IFileTypeFilter) { + this.fileTypeFilter.setValue(type); + } +} diff --git a/src/toolbar/searchFile/searchFile.component.html b/src/toolbar/searchFile/searchFile.component.html new file mode 100644 index 0000000..aae6797 --- /dev/null +++ b/src/toolbar/searchFile/searchFile.component.html @@ -0,0 +1,8 @@ +
+ + + + +
diff --git a/src/toolbar/searchFile/searchFile.component.ts b/src/toolbar/searchFile/searchFile.component.ts new file mode 100644 index 0000000..999361c --- /dev/null +++ b/src/toolbar/searchFile/searchFile.component.ts @@ -0,0 +1,22 @@ +import {Component, OnInit} from '@angular/core'; +import {FormControl} from '@angular/forms'; +import {SearchFilterService} from '../../services/searchFilter.service'; + +@Component({ + selector: 'ri-search-file', + templateUrl: './searchFile.component.html' +}) + +export class SearchFileComponent implements OnInit { + + public searchField = new FormControl(); + + constructor(private searchFilterService: SearchFilterService) { + } + + ngOnInit() { + this.searchField.valueChanges + .debounceTime(250) + .subscribe((value: string) => this.searchFilterService.setValue(value)); + } +} diff --git a/src/toolbar/toolbar.component.ts b/src/toolbar/toolbar.component.ts index e7a7995..9597e84 100644 --- a/src/toolbar/toolbar.component.ts +++ b/src/toolbar/toolbar.component.ts @@ -1,36 +1,24 @@ -import {Component, EventEmitter, Output, Input, OnChanges} from "@angular/core"; -import {IButton} from "../dropdown/IButton"; -import {Button} from "./models/button.model"; -import {ToolbarEventModel} from "./models/toolbarEvent.model"; -import {IToolbarEvent} from "./interface/IToolbarEvent"; -import {ConfirmOptions, Position} from "angular2-bootstrap-confirm"; -import {Positioning} from "angular2-bootstrap-confirm/position"; -import {FormControl} from "@angular/forms"; -import {IFileTypeFilter} from "./interface/IFileTypeFilter"; -import {FileManagerConfiguration} from "../configuration/fileManagerConfiguration.service"; -import {FileManagerUploader} from "../filesList/fileManagerUploader.service"; +import {Component, EventEmitter, Output, Input, OnChanges} from '@angular/core'; +import {IButton} from '../dropdown/IButton'; +import {Button} from './models/button.model'; +import {ToolbarEventModel} from './models/toolbarEvent.model'; +import {IToolbarEvent} from './interface/IToolbarEvent'; +import {FileManagerConfiguration} from '../configuration/fileManagerConfiguration.service'; +import {FileManagerUploader} from '../filesList/fileManagerUploader.service'; +import {FileManagerDispatcherService} from '../store/fileManagerDispatcher.service'; @Component({ selector: 'toolbar', styleUrls: ['./toolbar.less'], - providers: [ConfirmOptions, {provide: Position, useClass: Positioning}], templateUrl: './toolbar.html' }) -export class Toolbar implements OnChanges { +export class ToolbarComponent implements OnChanges { @Input() currentFolderId: string; - @Input() numberOfSelectedItems: number; @Output() onAddFolderClick = new EventEmitter(); @Output() onUpload = new EventEmitter(); - @Output() onUploadItem = new EventEmitter(); @Output() onMenuButtonClick = new EventEmitter(); - @Output() onSearchChange = new EventEmitter(); - @Output() onFilterTypeChange = new EventEmitter(); - - public searchField = new FormControl(); - - public selectedType: IFileTypeFilter = null; public selectAllButton: IButton = { symbol: Button.SELECT_ALL, @@ -52,36 +40,24 @@ export class Toolbar implements OnChanges { } ]; - /** - * List of filter types - * @typeObserv {IFileTypeFilter[]} - */ - public typeFilterList: IFileTypeFilter[]; - public constructor(public configuration: FileManagerConfiguration, - public fileManagerUploader: FileManagerUploader) { + public fileManagerUploader: FileManagerUploader, + private fileManagerDispatcher: FileManagerDispatcherService) { this.fileManagerUploader.clear(); - this.typeFilterList = configuration.fileTypesFilter; - this.fileManagerUploader.uploader.onCompleteAll = () => { this.onUpload.emit(this.currentFolderId || ''); }; - this.fileManagerUploader.uploader.onCompleteItem = (item: any, response: any, status: number, headers: any) => { - this.onUploadItem.emit({response: response, status: status, name: item.file.name}); - }; - this.searchField.valueChanges - .debounceTime(250) - .subscribe((value) => this.onSearchChange.emit(value)); - - this.typeFilterList.forEach((type) => { - if (type.defaultSelected) { - this.selectedType = type; + this.fileManagerUploader.uploader.onCompleteItem = (item: any, response: any, status: number, headers: any) => { + if (status === 200) { + this.fileManagerDispatcher.upload(JSON.parse(response)); + } else { + this.fileManagerDispatcher.uploadError(JSON.parse(response)); } - }); + }; } public ngOnChanges() { @@ -102,22 +78,4 @@ export class Toolbar implements OnChanges { let event: IToolbarEvent = new ToolbarEventModel(Button.REFRESH_FILES_LIST); this.onMenuButtonClick.emit(event); } - - public onDeleteSelection() { - let event: IToolbarEvent = new ToolbarEventModel(Button.DELETE_SELECTION); - this.onMenuButtonClick.emit(event); - } - - public getRemoveMessage() { - return 'You are try to delete ' + this.numberOfSelectedItems.toString() + ' file(s). Are you sure?'; - } - - /** - * Set current filter and fire event - * @param type - */ - public setFilterType(type: IFileTypeFilter) { - this.selectedType = type; - this.onFilterTypeChange.emit(type); - } } diff --git a/src/toolbar/toolbar.html b/src/toolbar/toolbar.html index 7f95351..0ba9729 100644 --- a/src/toolbar/toolbar.html +++ b/src/toolbar/toolbar.html @@ -13,8 +13,8 @@
- +
-
- -
+
-
- - - - -
+
diff --git a/src/tsconfig.spec.json b/src/tsconfig.spec.json new file mode 100644 index 0000000..3b8201b --- /dev/null +++ b/src/tsconfig.spec.json @@ -0,0 +1,20 @@ +{ + "extends": "../demo/src/tsconfig.json", + "compilerOptions": { + "outDir": "../out-tsc/spec", + "module": "commonjs", + "target": "es5", + "baseUrl": "", + "types": [ + "jasmine", + "node" + ] + }, + "files": [ + "test.ts" + ], + "include": [ + "**/*.spec.ts", + "**/*.d.ts" + ] +} diff --git a/tsconfig.json b/tsconfig.json index 1731556..11fd0b4 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -10,9 +10,6 @@ "moduleResolution": "node", "outDir": "../dist/out-tsc", "sourceMap": true, - "target": "es5", - "typeRoots": [ - "./example" - ] + "target": "es5" } } diff --git a/tslint.json b/tslint.json index 640d02c..fe37146 100644 --- a/tslint.json +++ b/tslint.json @@ -89,8 +89,8 @@ "check-type" ], - "directive-selector": [true, "attribute", "app", "camelCase"], - "component-selector": [true, "element", "app", "kebab-case"], + "directive-selector": [true, "attribute", ["app", "ri"], "camelCase"], + "component-selector": [true, "element", ["app", "ri"], "kebab-case"], "use-input-property-decorator": true, "use-output-property-decorator": true, "use-host-property-decorator": true,